Keep the ZCTA geography current, and let it reach installed clients - #109
john-zither wants to merge 5 commits into
Conversation
8544807 to
569351d
Compare
4437ff5 to
5001e86
Compare
5001e86 to
63cf568
Compare
|
Good work, and the docstring evidence is exactly right. Three things before merge. Vintage retention (blocking). We also cannot assume a caller's ZIP codes are current or historical, and one file can mix
Workflow. Unquoted hits the Minor: The branch was rebased from our side after #105 merged and briefly closed when the base |
Address the opendsm#109 review. Vintage retention (blocking). place and place_zone now lead their primary key with a Census-publication-year vintage, so a rebuild appends a vintage beside the rows earlier results were resolved against instead of deleting them. build_places scopes its delete to the year it is (re)building, so a rebuild of one year stays idempotent while other vintages are kept. Readers resolve the newest vintage for a code: get_place orders by vintage and reads its zones at that same vintage (they would otherwise collapse across vintages), and get_zcta_ids lists the current set. The packaged pack and migrate() stamp the legacy rows 2016, the GENZ2016 release they came from. The cross-vintage selection rule (silent where the ranked station is unchanged, warn on divergence, error where a code is in only one vintage) follows on top of this retention. Drop pyshp for the ZCTA-to-county relationship file. State came from a point-in-polygon against the generalized 500k state shapefile; it now comes from the Census relationship file, whose county GEOID carries the state FIPS in its first two digits. It is authoritative rather than generalized (exact at state lines), assigns a county to every ZCTA (no offshore tolerance fallback), and needs no geometry, so the pyshp build dependency is gone. Workflow. The dispatch input goes through env instead of being interpolated into the run: shell of a contents:write job, and ${YEAR:+$YEAR} plus an explicit geography branch in __main__ keep an empty input building the newest vintage rather than falling through to a registry refresh. GEOGRAPHY_FILENAMES is known by name rather than globbed at import, so an empty registry dir does not drop geography from the update set, and build_places now gates the count of ZCTAs left without a state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZSvzSCixXWkses63bw6ic
|
Rebased and pushed. All three items plus the two minors: Vintage retention. The selection rule (quiet when the points agree, warn when they diverge, error when a code only exists in one vintage) comes next, on top of this. It's all in
Workflow. The input goes through Minors. |
The packaged geography was built from `cb_2016_us_zcta510_500k.zip` -- `zcta510` is the vintage, GENZ2016 only the release year -- so the ZCTA definition in the pack is the 2010 one. Census redrew ZCTA boundaries for 2020 and republishes the Gazetteer annually. Measured against the 2025 Gazetteer, the packaged pack cannot place 851 current ZCTAs and still carries 204 that have been retired. The download scripts were deleted in 2548283 and `_migrate_places` copies rows forward, so nothing rebuilt it and no `meta` table recorded what vintage it was. Rebuilding alone would not have fixed it, because a rebuilt pack only reaches whoever upgrades the package. The refresh machinery that exists for exactly this problem excluded geography, on a premise that is false: Static content (geography packs, archive station lists) never changes and is not part of the update. So the pack could sit fifteen years stale with auto-update running the whole time. Both halves are needed and both are here. Build. `eeweather/build/geography.py` rebuilds `place` from the Census Gazetteer plus the cartographic state layer, resolving the newest vintage published for both, deriving `subdivision` by point-in-polygon (the Gazetteer has no state column) with a nearest-state fallback for coastal ZCTAs whose internal point falls outside the generalized boundaries, and stamping the vintage in `meta`. A row-count floor rejects an implausible rebuild. Zone geometries are genuinely static and are untouched, as is `place_zone`. An annual workflow rebuilds, runs the suite against the result, and opens a pull request. Distribution. Geography packs join `UPDATABLE` with a `place` row-count floor, so a truncated download can never replace a geography, and the workflow publishes to the same `registry-latest` rolling release the registry refresh already uses. An installed client picks the new pack up on its next update instead of waiting for a release. Three details the wiring needed: - `_geography_packs` globbed the packaged directory directly, so a downloaded pack would have been installed, committed, and then silently ignored on read. It now resolves through `data_path`; `packaged_geography_packs` exposes the pre-update paths the updater needs to know what a download would replace. - geography moves once a year while the station registry moves continuously, so a pack published weekly should not have to re-upload an unchanged 8 MB of geometry. A published pack may omit geography; the rest of the update proceeds and the existing pack stays. A pack missing anything else is broken and aborts as before. - the vintage is stamped under `refreshed_at`, the key `registry.update.refreshed_at` already reads, rather than a name only the build module knew. Verified against live Census: 33,144 -> 33,791 places, 851 added, 204 dropped, median internal-point shift 640 m. The nearest-state fallback takes 36 unassigned coastal ZCTAs (Peaks Island ME, Orient NY) to zero without needing a subdivision layer. Full suite green against the pre-existing 10 failures (numpy 2.5 / pandas deprecations escalated by `filterwarnings = ["error"]`); no new failures; ruff clean. test_clear_removes_updated_copies now derives its count from `UPDATABLE` rather than hardcoding two files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Theurer <john@energy-data-solutions.com>
The station registry updates itself in the background, so identical code run on different days can resolve against different underlying station data, and nothing in the result recorded which. This adds a registry_vintage field to the Provenance record, populated from the registry's own refreshed_at stamp, so every result carries the snapshot it was computed against and becomes reproducible by record. The field is additive with a keyword default of None, leaving grid sources (which do not resolve through the station registry) and all existing Provenance construction unchanged. Signed-off-by: John Theurer <john@energy-data-solutions.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Address the opendsm#109 review. Vintage retention (blocking). place and place_zone now lead their primary key with a Census-publication-year vintage, so a rebuild appends a vintage beside the rows earlier results were resolved against instead of deleting them. build_places scopes its delete to the year it is (re)building, so a rebuild of one year stays idempotent while other vintages are kept. Readers resolve the newest vintage for a code: get_place orders by vintage and reads its zones at that same vintage (they would otherwise collapse across vintages), and get_zcta_ids lists the current set. The packaged pack and migrate() stamp the legacy rows 2016, the GENZ2016 release they came from. The cross-vintage selection rule (silent where the ranked station is unchanged, warn on divergence, error where a code is in only one vintage) follows on top of this retention. Drop pyshp for the ZCTA-to-county relationship file. State came from a point-in-polygon against the generalized 500k state shapefile; it now comes from the Census relationship file, whose county GEOID carries the state FIPS in its first two digits. It is authoritative rather than generalized (exact at state lines), assigns a county to every ZCTA (no offshore tolerance fallback), and needs no geometry, so the pyshp build dependency is gone. Workflow. The dispatch input goes through env instead of being interpolated into the run: shell of a contents:write job, and ${YEAR:+$YEAR} plus an explicit geography branch in __main__ keep an empty input building the newest vintage rather than falling through to a registry refresh. GEOGRAPHY_FILENAMES is known by name rather than globbed at import, so an empty registry dir does not drop geography from the update set, and build_places now gates the count of ZCTAs left without a state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZSvzSCixXWkses63bw6ic
307f356 to
35c18de
Compare
Address the opendsm#109 review. Vintage retention (blocking). place and place_zone now lead their primary key with a Census-publication-year vintage, so a rebuild appends a vintage beside the rows earlier results were resolved against instead of deleting them. build_places scopes its delete to the year it is (re)building, so a rebuild of one year stays idempotent while other vintages are kept. Readers resolve the newest vintage for a code: get_place orders by vintage and reads its zones at that same vintage (they would otherwise collapse across vintages), and get_zcta_ids lists the current set. The packaged pack and migrate() stamp the legacy rows 2016, the GENZ2016 release they came from. The cross-vintage selection rule (silent where the ranked station is unchanged, warn on divergence, error where a code is in only one vintage) follows on top of this retention. Drop pyshp for the ZCTA-to-county relationship file. State came from a point-in-polygon against the generalized 500k state shapefile; it now comes from the Census relationship file, whose county GEOID carries the state FIPS in its first two digits. It is authoritative rather than generalized (exact at state lines), assigns a county to every ZCTA (no offshore tolerance fallback), and needs no geometry, so the pyshp build dependency is gone. Workflow. The dispatch input goes through env instead of being interpolated into the run: shell of a contents:write job, and ${YEAR:+$YEAR} plus an explicit geography branch in __main__ keep an empty input building the newest vintage rather than falling through to a registry refresh. GEOGRAPHY_FILENAMES is known by name rather than globbed at import, so an empty registry dir does not drop geography from the update set, and build_places now gates the count of ZCTAs left without a state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZSvzSCixXWkses63bw6ic Signed-off-by: John Theurer <john@energy-data-solutions.com>
build_places opened the pack with `with sqlite3.connect(path) as geography:`, which commits the transaction on exit but never closes the connection. The leaked handle is only a ResourceWarning until Python 3.13, where pytest's unraisable-exception plugin raises it as an error under filterwarnings=error, failing the 3.13/3.14 CI jobs (3.10-3.12 never surfaced it). Wrap the connection in contextlib.closing so it is closed, and keep the inner `with geography:` for the same commit-on-success / rollback-on-error semantics. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZSvzSCixXWkses63bw6ic Signed-off-by: John Theurer <john@energy-data-solutions.com>
The new geography tests opened sqlite connections they never closed: the build-result assertions in test_geography, and the synthetic :memory: pack in test_summaries' _two_vintage_pack. A leaked handle is only a ResourceWarning until Python 3.13, where pytest raises it as an error under filterwarnings=error, so it failed 3.13/3.14 CI from whichever test the garbage collector happened to run in. Close each connection at the end of its test; register the :memory: pack's connection with request.addfinalizer so it is closed at teardown. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZSvzSCixXWkses63bw6ic Signed-off-by: John Theurer <john@energy-data-solutions.com>
3124068 to
ad216e7
Compare
Your checklist for this pull request
feature/geography-build) againstpower-source.ruff checkclean. The 10 failures are pre-existing onpower-source(pandas 2.x with numpy 2.5) and are fixed by Stop constructing Timedeltas the way numpy 2.5 deprecates #106; they are unrelated to this change.CONTRIBUTING.mddocuments the build step.Signed-off-bytrailer.Co-Authored-Bytrailer. The tool's terms impose no conditions inconsistent with the project's licence or the Open Source Definition, and the output does not incorporate third-party code.Description
The self-updating registry works as designed. This changes one premise it was scoped on —
registry/update.py's module docstring:Geography does change. Census redrew the ZCTA boundaries for 2020 and republishes the Gazetteer annually. The packaged
placetable is still the 2010 definition, built fromcb_2016_us_zcta510_500k.zip—zcta510is the vintage; GENZ2016 is only the release year — and measured against the current Gazetteer it cannot place 851 ZCTAs that exist today while still carrying 204 that have been retired.Two consequences followed from the premise rather than from any defect. Geography was excluded from
UPDATABLE, so the refresh mechanism never reached it however often it ran. And the download scripts were deleted in2548283, so there was no way to build a replacement pack even if something had been willing to install one —_migrate_placescopies rows forward indefinitely.geography_us.dbhas nometatable either, so the staleness was not observable.This reuses the existing update path unchanged — no second
maybe_update, no parallel CDN channel, no additional commit marker or staleness clock. The update-side diff is eleven lines: add the geography packs toUPDATABLEso the mechanism carries a fourth file, mark themOPTIONAL_IN_PACK, and correct that docstring sentence.The substantive half is
eeweather/build/geography.py, which rebuildsplacefrom the annual Census Gazetteer ZCTA file pluscb_YYYY_us_state_500k.zip, stamped with a vintage and wired into the existing refresh workflow. The two halves are one change because neither is useful alone: adding geography toUPDATABLEinstalls nothing without a build step, and a build step produces a pack nothing installs.Smaller than it sounds, for two reasons: eeweather stores no ZCTA geometry — a ZCTA is a point, a state, and four zone assignments — so the rebuild does not need the cartographic boundary files, which Census stopped publishing after GENZ2020 anyway. The one genuinely new piece is deriving
subdivisionby point-in-polygon against the state layer, since the Gazetteer has no state column.Details worth review:
OPTIONAL_IN_PACKexists because the two cadences differ: geography moves once a year while the station registry moves continuously, so a weekly published pack should not have to re-upload an unchanged 8 MB of geometry, and a pack without one should refresh the rest rather than abort.registry_vintage, therefreshed_atstamp of the registry a request resolved against. It belongs here rather than on its own: putting geography in the updatable set is what makes that vintage vary, and with geography in the set it is stamped through the same key as the station registry rather than under a name only this module knows.