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:
Jef Roosens 2026-05-22 10:33:00 +02:00
parent d6689a6c83
commit ecdd28e8a3
Signed by: Jef Roosens
GPG key ID: 119385BCAA005C21
8 changed files with 513 additions and 17 deletions

View file

@ -7,6 +7,7 @@ from timesheets.parser import (
build_description,
detect_has_duration_column,
extract_table_blocks,
filter_rows_by_date,
parse_document,
parse_table,
)
@ -215,6 +216,67 @@ class TestParseDocument:
)
# ---------------------------------------------------------------------------
# filter_rows_by_date
# ---------------------------------------------------------------------------
class TestFilterRowsByDate:
# Reuse the W21 file which has one table per day-heading
with open(WEEK_FILE, encoding="utf-8") as _f:
_WEEK_LINES = _f.read().splitlines()
def test_returns_only_matching_day(self):
from datetime import date
rows = filter_rows_by_date(self._WEEK_LINES, date(2026, 5, 22))
assert len(rows) > 0
# Friday has these projects in the sample file
projects = {r["project"] for r in rows}
assert "scrum" in projects
def test_different_day_returns_different_rows(self):
from datetime import date
rows_fri = filter_rows_by_date(self._WEEK_LINES, date(2026, 5, 22))
rows_mon = filter_rows_by_date(self._WEEK_LINES, date(2026, 5, 18))
assert rows_fri != rows_mon
assert len(rows_mon) > 0
def test_no_match_returns_empty(self):
from datetime import date
rows = filter_rows_by_date(self._WEEK_LINES, date(2026, 1, 1))
assert rows == []
def test_inline_document(self):
from datetime import date
lines = [
"# Maandag - 2026-05-18",
"| Start | End | Project | Story | Note |",
"|-------|-------|---------|-------|------||",
"| 08:00 | 08:30 | bugs | | fix |",
"",
"# Dinsdag - 2026-05-19",
"| Start | End | Project | Story | Note |",
"|-------|-------|---------|-------|------||",
"| 09:00 | 09:30 | scrum | | dsu |",
]
rows = filter_rows_by_date(lines, date(2026, 5, 18))
assert len(rows) == 1
assert rows[0]["project"] == "bugs"
rows = filter_rows_by_date(lines, date(2026, 5, 19))
assert len(rows) == 1
assert rows[0]["project"] == "scrum"
def test_empty_input(self):
from datetime import date
assert filter_rows_by_date([], date(2026, 5, 22)) == []
# ---------------------------------------------------------------------------
# build_description
# ---------------------------------------------------------------------------