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
68
src/timesheets/joplin.py
Normal file
68
src/timesheets/joplin.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
"""
|
||||
Joplin integration via the joppy ClientApi.
|
||||
|
||||
Actual notebook structure:
|
||||
Work > Timesheets > YYYY
|
||||
|
||||
Notes live directly inside the year notebook and are titled 'YYYY - WNN'.
|
||||
"""
|
||||
|
||||
from datetime import date
|
||||
from typing import Optional
|
||||
|
||||
from joppy.client_api import ClientApi
|
||||
|
||||
|
||||
def _iso_week_label(d: date) -> str:
|
||||
"""Return the note title for the week containing the given date, e.g. '2026 - W21'."""
|
||||
year, week, _ = d.isocalendar()
|
||||
return f"{year} - W{week:02d}"
|
||||
|
||||
|
||||
def _find_notebook(
|
||||
api: ClientApi, title: str, parent_id: Optional[str] = None
|
||||
) -> Optional[str]:
|
||||
"""Return the ID of a notebook matching title (and optionally parent_id), or None."""
|
||||
for nb in api.get_all_notebooks():
|
||||
if nb.title == title:
|
||||
if parent_id is None or nb.parent_id == parent_id:
|
||||
return nb.id
|
||||
return None
|
||||
|
||||
|
||||
def fetch_week_note(token: str, target_date: date) -> str:
|
||||
"""
|
||||
Fetch the body of the weekly timesheet note from Joplin for the week
|
||||
containing target_date.
|
||||
|
||||
Notebook path: Work > Timesheets > YYYY
|
||||
Note title: YYYY - WNN
|
||||
|
||||
Raises RuntimeError if any notebook or the note cannot be found.
|
||||
"""
|
||||
api = ClientApi(token=token)
|
||||
week_label = _iso_week_label(target_date)
|
||||
year_str = str(target_date.year)
|
||||
|
||||
work_id = _find_notebook(api, "Work")
|
||||
if work_id is None:
|
||||
raise RuntimeError("Joplin notebook 'Work' not found")
|
||||
|
||||
timesheets_id = _find_notebook(api, "Timesheets", parent_id=work_id)
|
||||
if timesheets_id is None:
|
||||
raise RuntimeError("Joplin notebook 'Work > Timesheets' not found")
|
||||
|
||||
year_id = _find_notebook(api, year_str, parent_id=timesheets_id)
|
||||
if year_id is None:
|
||||
raise RuntimeError(
|
||||
f"Joplin notebook 'Work > Timesheets > {year_str}' not found"
|
||||
)
|
||||
|
||||
notes = api.get_all_notes(notebook_id=year_id, fields="id,title,body")
|
||||
for note in notes:
|
||||
if note.title == week_label:
|
||||
return note.body
|
||||
|
||||
raise RuntimeError(
|
||||
f"Joplin note '{week_label}' not found in 'Work > Timesheets > {year_str}'"
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue