feat(joplin): add --joplin flag to fetch weekly timesheet note from Joplin
- Add joplin.py with fetch_week_note() that walks Work > Timesheets > YYYY and returns the body of the matching YYYY-WNN note via joppy ClientApi - Add filter_rows_by_date() to parser.py to extract only rows belonging to a specific day based on '# ... YYYY-MM-DD' headings in the document - Update cli.py: input and --joplin are now a mutually exclusive required group; add --token flag with JOPLIN_TOKEN env var fallback; --date is parsed into a real date object used for both output and day filtering - Add joppy as a runtime dependency (lazy-imported in cli.py) - Add tests for filter_rows_by_date and full mocked coverage of joplin.py - Update AGENTS.md with Joplin usage, notebook structure, and test rules The actual Joplin structure has notes directly inside the year notebook (Work > Timesheets > YYYY), not in per-week sub-notebooks as initially assumed. fetch_week_note() reflects this flat structure.
This commit is contained in:
parent
d6689a6c83
commit
ecdd28e8a3
8 changed files with 513 additions and 17 deletions
|
|
@ -4,7 +4,7 @@ import sys
|
|||
from datetime import date
|
||||
|
||||
from .output import print_summary, write_csv
|
||||
from .parser import aggregate_rows, parse_document
|
||||
from .parser import aggregate_rows, filter_rows_by_date, parse_document
|
||||
from .projects import load_project_map
|
||||
from .utils import format_date
|
||||
|
||||
|
|
@ -13,10 +13,29 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
parser = argparse.ArgumentParser(
|
||||
description="Parse a markdown timesheet table and output a CSV file."
|
||||
)
|
||||
parser.add_argument(
|
||||
|
||||
source = parser.add_mutually_exclusive_group(required=True)
|
||||
source.add_argument(
|
||||
"input",
|
||||
nargs="?",
|
||||
help="Path to the markdown file containing the timesheet table, or '-' to read from stdin.",
|
||||
)
|
||||
source.add_argument(
|
||||
"--joplin",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=(
|
||||
"Fetch the weekly timesheet note from Joplin instead of reading a file. "
|
||||
"Only entries for today (or --date) are included. "
|
||||
"Requires a Joplin API token via --token or the JOPLIN_TOKEN environment variable."
|
||||
),
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--token",
|
||||
help="Joplin API token. Falls back to the JOPLIN_TOKEN environment variable.",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
|
|
@ -44,23 +63,66 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
return parser
|
||||
|
||||
|
||||
def _resolve_token(args: argparse.Namespace) -> str:
|
||||
token = args.token or os.environ.get("JOPLIN_TOKEN")
|
||||
if not token:
|
||||
print(
|
||||
"Error: Joplin API token required. "
|
||||
"Provide --token or set the JOPLIN_TOKEN environment variable.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
return token
|
||||
|
||||
|
||||
def _parse_date(date_str: str | None) -> date:
|
||||
"""Parse DD/MM/YY date string, or return today."""
|
||||
if date_str is None:
|
||||
return date.today()
|
||||
try:
|
||||
from datetime import datetime
|
||||
|
||||
return datetime.strptime(date_str, "%d/%m/%y").date()
|
||||
except ValueError:
|
||||
print(
|
||||
f"Error: invalid date format {date_str!r}, expected DD/MM/YY.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = build_parser().parse_args()
|
||||
|
||||
date_str = args.date or format_date(date.today())
|
||||
target_date = _parse_date(args.date)
|
||||
date_str = format_date(target_date)
|
||||
|
||||
if args.input == "-":
|
||||
content = sys.stdin.read()
|
||||
else:
|
||||
if args.joplin:
|
||||
# Late import so joppy is only required when --joplin is used
|
||||
from .joplin import fetch_week_note
|
||||
|
||||
token = _resolve_token(args)
|
||||
try:
|
||||
with open(args.input, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
except FileNotFoundError:
|
||||
print(f"Error: file not found: {args.input}", file=sys.stderr)
|
||||
content = fetch_week_note(token, target_date)
|
||||
except RuntimeError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
lines = content.splitlines()
|
||||
rows = parse_document(lines)
|
||||
lines = content.splitlines()
|
||||
rows = filter_rows_by_date(lines, target_date)
|
||||
else:
|
||||
if args.input == "-":
|
||||
content = sys.stdin.read()
|
||||
else:
|
||||
try:
|
||||
with open(args.input, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
except FileNotFoundError:
|
||||
print(f"Error: file not found: {args.input}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
lines = content.splitlines()
|
||||
rows = parse_document(lines)
|
||||
|
||||
if not rows:
|
||||
print("Warning: no timesheet rows found in input.", file=sys.stderr)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue