diff --git a/CHANGELOG.md b/CHANGELOG.md index a98c0bf..3a236f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## Unreleased + +### Fixed + +- **An impossible calendar date discarded a whole house.** `2025-02-30T00:00:00Z` + has the shape of an ISO timestamp and names a day that does not exist. + `strptime` raised a bare `ValueError`, `_record()` catches `UnreadableDate` + alone, so the error reached the house handler, wrote one `fetch_error`, and + abandoned every later bill in that house. It now raises `UnreadableDate`, the + bad field goes to null, and the bill keeps everything else. This is the defect + 0.15.0 fixed for other date shapes and missed for this one. + + ## 0.15.0 — 2026-08-18 **Two of these change published numbers. Re-run the bills probe, and read the diff --git a/commoner_probe/bill_catalog_api.py b/commoner_probe/bill_catalog_api.py index 581f632..29ff528 100644 --- a/commoner_probe/bill_catalog_api.py +++ b/commoner_probe/bill_catalog_api.py @@ -136,7 +136,16 @@ def _date(value: object, field: str = "date") -> str | None: # read differently on two machines walking the same source. stamped = _ISO_STAMP.match(text) if stamped and _in_range(stamped): - return datetime.strptime(stamped.group(1), "%Y-%m-%d").strftime("%Y-%m-%d") + try: + return datetime.strptime(stamped.group(1), "%Y-%m-%d").strftime("%Y-%m-%d") + except ValueError as exc: + # `2025-02-30` has the shape of a date and is not one. A bare + # ValueError here escaped `_record()`, which catches UnreadableDate + # alone, reached the house handler, and abandoned every later bill + # in that house over one field. + raise UnreadableDate( + f"{field}: {text!r} has the shape of an ISO timestamp and names a day " + f"that does not exist ({exc}).") from exc raise UnreadableDate( f"{field}: {text!r} matches no date format this endpoint is known to serve " f"({', '.join(_DATE_FORMATS)}, or ISO 8601). It is not being truncated into " diff --git a/tests/test_bill_dates.py b/tests/test_bill_dates.py index db21b6a..554edb7 100644 --- a/tests/test_bill_dates.py +++ b/tests/test_bill_dates.py @@ -374,3 +374,30 @@ def test_another_adapter_s_history_survives(self, tmp_path): tracked = [r["bill_status"] for r in rows if r.get("kind") == "prs_bill_track"] assert tracked == ["Pending", "Passed"], tracked assert [r["assent_date"] for r in rows if r.get("kind") == "bill_record"] == ["2025-12-20"] + + +class TestAnImpossibleCalendarDateStaysOneField: + """A P1 from Codex that merged unfixed on #142. The dispatcher read one + comment of a two-comment review.""" + + def test_an_impossible_day_raises_unreadable_not_value_error(self): + with pytest.raises(UnreadableDate): + _date("2025-02-30T00:00:00Z", "assent_date") + + def test_the_house_survives_an_impossible_day(self, tmp_path): + """`_record()` catches `UnreadableDate` alone, so a bare `ValueError` + reached the house handler, wrote one `fetch_error`, and abandoned every + later bill in that house.""" + from commoner_probe.bill_catalog_api import BillsProbe + + good = {"billNumber": "1", "billYear": "2025", "billName": "Good Bill", + "billIntroducedDate": "2025-01-02 00:00:00.0"} + bad = {"billNumber": "2", "billYear": "2025", "billName": "Bad Bill", + "billIntroducedDate": "2025-02-30T00:00:00Z"} + probe = BillsProbe(tmp_path / "out", sleep=0, houses=["ls"]) + probe.bills_all = lambda house: iter([bad, good]) + + rows = probe.probe() + assert [r["fetch_status"] for r in rows] == ["parse_error", "ok"], rows + assert rows[0]["bill_name"] == "Bad Bill" + assert rows[0]["introduced_date"] is None