feat: agenda ingestion - migration + ingest_agenda.py (MON-210) - #299
Conversation
Implements the agenda_items table and ingest_agenda.py exactly as specified in ADR-030 (MON-209): séance publique ODJ points only, upserted with last_seen_at stamped on every run, no DELETE. Wires the step into run_ingestion_prod.py as non-critical, matching the party-fix/summaries pattern so an agenda-feed outage never blocks core ingestion.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🧪 dbt data-health check — ✅ passedValidates prod data health. A failure means current prod data is stale/broken, not necessarily that this PR is wrong. |
Walid-peach
left a comment
There was a problem hiding this comment.
Attention Score
82/100 - SAFE TO SKIM
- Base 100
- Should Fix count: 1 → -6
- Live-DB path (idempotency, migration apply) not executed by the author → -8
- Non-empty Testing/Validation Gaps section → -4
Reason: The change is additive (new table + new script), the parsing logic is thoroughly unit-tested against real AN-shaped fixtures, and the only real gap is a DB-environment limitation the author already flagged.
Summary
Implements the agenda_items table and ingest_agenda.py exactly as scoped by ADR-030/MON-209: séance-publique-only ODJ points, soft-state upsert with last_seen_at, cancellations reflected via reunion_etat/point_etat/cancelled_at rather than DELETE. Wired into run_ingestion_prod.py as a non-critical step, matching the existing party-fix/summaries pattern. Parsing logic (single-vs-list pointODJ, séance/commission filtering, cancelled réunion, missing dossierRef) is covered by a new pure-function test file against a committed JSON fixture, and the full non-integration suite plus ruff pass.
Must Fix
None.
Should Fix
ingest_agenda.py's standalone--sincedefault (90 days) differs fromrun_ingestion_prod.py's effective default (365 days, inherited fromargs.since). Since the orchestrator always passes--sinceexplicitly, this only bites someone runningingest_agenda.pydirectly without a flag — worth a one-line comment or aligning the default so it's not a surprise later.
Nice to Have
_first_refsilently takes only the firstdossierRefwhen a point carries several. Given the ADR'sdossier_idis a single TEXT column this is the right call, but a short comment noting the multi-dossier case is intentionally collapsed would save a future reader from wondering.point_etatcan end up stale relative toreunion_etatif the AN feed cancels the réunion without updating each point's owncycleDeVie— not a bug in this script (it stores what the feed says), but worth a note for MON-212 since the ADR's freshness/state filtering leans on both fields.
Testing / Validation Gaps
- The acceptance criteria "
make migrateapplies cleanly" and "a second run over the same fixture leaves the row count unchanged" were not verified against a live Postgres — Docker wasn't available in this environment (already called out in the PR description). Recommend a reviewer runsmake migrate+python scripts/ingest_agenda.py --zip-path <fixture>twice before merging, to confirm the upsert/idempotency behavior end-to-end.
Documentation / Reviewer Notes
CLAUDE.mdandREADME.mdwere updated with the new table/script — accurate and consistent with the ADR-030 column set.- No dbt mart added, consistent with the ADR's explicit "no mart" decision.
Verdict
Ready with minor changes
Standalone runs previously defaulted to a 90-day window while the orchestrator always passes 365 days via --since, an inconsistency flagged in PR review. Also notes that point_etat isn't guaranteed to track reunion_etat when a sitting is cancelled, for MON-212's benefit.
What
Adds the agenda ingestion pipeline for MON-110: a new
agenda_itemstable andscripts/ingest_agenda.py, which pulls the AN agenda export and lands séance publique ODJ points in the database.Why
ADR-030 (MON-209) fixed the table shape and refresh semantics for the agenda feature.
This PR implements exactly that: one denormalized
agenda_itemstable, upserted only, scoped to séance publique.It unblocks MON-212 (
GET /agendaendpoints) and MON-211 (plain-French summaries).Changes
data/migrations/009_agenda.sql— createsagenda_itemsper the ADR-030 column set,CREATE TABLE IF NOT EXISTS(safe to re-run), with indexes onsitting_start(date-window queries) anddossier_id(the join tovotes).scripts/ingest_agenda.py— downloads/reads theAgenda.json.zipexport, filters to@xsi:type = seance_type(séance publique only), flattens each réunion's ODJ point(s) (pointODJis a bare object for a single point and a list for several — both handled), and upserts onpoint_uidwithlast_seen_atstamped on every touched row. Cancellations are reflected viareunion_etat/point_etatandcancelled_at, never viaDELETE(upsert-only rule, CLAUDE.md decision 8).scripts/run_ingestion_prod.py— wires in an "Agenda" step viarun_step(..., critical=False), matching the party-fix/summaries pattern: an agenda-feed outage must not block core deputies/votes/positions ingestion. No workflow file changes needed —.github/workflows/ingest_prod.ymlalready invokesrun_ingestion_prod.py.tests/test_agenda_parsers.py+tests/fixtures/agenda_sample.json— pure-function tests over a committed JSON fixture covering: the single-point vs listpointODJshape, séance-vs-commission filtering, a cancelled réunion (Annulé), a point with nodossierRef, and the--sincewindow.CLAUDE.md/README.md— documented the new table and script.Out of scope by design (ADR-030 / follow-up issues):
summary_plain/themepopulation (MON-211),GET /agenda(MON-212).Testing
python3 -m pytest tests/ -m "not integration" -q— 352 passed (baseline unchanged).python3 -m ruff check .andpython3 -m ruff format --check .— clean.tests/fixtures/agenda_sample.jsonand ranfetch_all_reunions+parse_reunionsagainst it end-to-end — correctly parses 4 séance ODJ points, filters out the commission réunion, and reflects the cancelled réunion's état.--zip-pathupsert against a running Postgres and themake migrateidempotency check — Docker wasn't available in this environment. The parsing/transform logic (the part with real risk of AN-feed-shape bugs) is covered by the unit tests above; the upsert SQL itself mirrors the already-proveningest_votes.pypattern.Risks / Notes
009_agenda.sql) — additive only (CREATE TABLE IF NOT EXISTS+ two indexes), no changes to existing tables.make migratethenpython scripts/ingest_agenda.py --zip-path <fixture>twice) before merge, since it wasn't possible to run here.run_ingestion_prod.py's summary log block now has a 5-line body instead of 4; purely cosmetic.Breaking Changes
None.