Repository navigation
Request UK local-area breakdowns, fix the Northern Ireland region name, and update .py to 6.10.0 - #4024
Merged
Conversation
`AnnualEconomyQuery` gains `breakdown: Literal["local_areas"] | None`, which asks for the UK constituency and local-authority impacts only. `calculation_options()` includes it only when it is set, so the options hash of every request without it is unchanged. The budget-window query still rejects it as an undeclared parameter, and the served `/specification` publishes it with its enum because it is generated from the same model. Tests written first. Before the change, 11 failed: the parser rejected `breakdown` as `extra_forbidden` (so the unknown-value tests saw `extra_forbidden` instead of `literal_error`, and a repeated value was not reported as repeated), the model had no `breakdown` attribute, and the specification had no `breakdown` parameter (KeyError). The budget-window rejection tests and the no-breakdown route test passed before the change; they record behaviour that must stay the same. Refs #4021 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`_build_economic_impact_setup_options` now rejects a breakdown before any cache read or worker capability check unless the request is for the UK, with region `uk` or `country/*`, and not `target=cliff`. The route returns those ValueErrors as 400. `SimulationOptions` gains `breakdown`, and `_setup_sim_options` fills it from the request options. The field is excluded from the payload when it is None, so national and budget-window submissions are unchanged. The options hash already includes `breakdown` (previous commit), so a breakdown request never matches a national lookup pattern, or the other way round. When the simulation API answers a breakdown submission with 400 or 422, the annual path now returns an error result, which the route sends as 502 with the simulation API's reason (each field of a 422 body is named). Before, the HTTPStatusError reached the generic handler as a 500. Nothing is stored, so a later request submits again once the simulation API can run breakdowns. Other statuses, and every request without a breakdown, still raise as before. `_build_budget_window_submission_error_message` is renamed `_build_submission_error_message`, since both paths now use it. The served specification and docs/canonical-spm.md describe the breakdown result and the 502. Tests written first. Before the change, 21 failed for the expected reasons: six unsupported UK requests did not raise ValueError; the two US requests passed the missing check and failed later on the SPM capability check; three route requests returned 200 instead of 400; the payload had no `breakdown` (KeyError, and `[None]` instead of `['local_areas']`); and 400 and 422 refusals propagated as HTTPStatusError. Tests that pass before the change pin unchanged behaviour: national payloads carry no `breakdown`, the two options hashes never match each other's lookup pattern, other refusal statuses still raise, and the HTTP client forwards only non-null fields. One test, for the unknown-value check that the query parser already makes unreachable, was added after the code. Refs #4021 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Public API's UK metadata names Northern Ireland `country/ni`
(`country.py`), but .py's UK region registry, in both 6.7.5 and 6.10.0
(`policyengine/countries/uk/regions.py`), builds the country regions as
`country/{england,scotland,wales,northern_ireland}` and looks codes up
exactly, with no aliases. The simulation executor's region normaliser
leaves `country/ni` as it is, so a Northern Ireland request named a region
the registry does not have.
`_setup_region` now maps `country/ni` to `country/northern_ireland` for
the UK. Only the simulation payload changes: the cache scope, the options
hash and the stored record keep `country/ni`, so existing Northern
Ireland cache entries are still found. Other UK regions are unchanged.
Tests written first. Before the change, 2 failed: `_setup_region("uk",
"country/ni")` and the built payload both returned `country/ni` instead
of `country/northern_ireland`. The tests that other UK regions pass
through unchanged passed before and after.
Refs #4021
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit f7552f3)
.py 6.10.0's UK release manifest names `microcosm_uk_2024_25` as its
default dataset, so the v2 catalog extractor refused the installed bundle
("uk default dataset does not match the reviewed selection"), which is
how `tests/integration/test_v2_catalog_installed.py` failed on #3991.
`REVIEWED_DEFAULT_DATASETS["uk"]` is now `microcosm_uk_2024_25`.
The catalog fixtures and the publication test name the same dataset. The
installed-catalog test's exact counts are updated after review for
policyengine-uk 2.132.6: 7 more variables, 1 more parameter node and
parameter, 3 more parameter values, and 1,011 more regions, because the
6.10.0 UK registry lists its 650 constituencies and 361 local authorities
now that the bundle routes them to a local-area release.
Not test-first: this follows the version bump in the previous commit,
and is checked by the installed-catalog test and the catalog unit tests,
which pass.
Refs #4021
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With .py 6.10.0 the UK default dataset is `microcosm_uk_2024_25`, which matched no dataset family prefix, so the UK metadata's dataset option fell back to the generic label and title "Certified dataset". The `microcosm_` prefix now maps to "Microcosm" (title "Certified Microcosm dataset"), the label the US Microcosm datasets (`populace_*`) already use. `enhanced_frs_*` keeps "Enhanced FRS". Tests written first. With the 6.10.0 bundle installed, 3 failed before the change: `microcosm_uk_2024_25` and `microcosm_uk_2024_25_dense` were labelled "Certified dataset", and the UK metadata's default dataset label was "Certified dataset" instead of "Microcosm". The v2 metadata route integration test is updated to expect the same label. Refs #4021 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`_validate_breakdown` accepted any text after a lower-case `country/`, so `country/%20` (decoded to `country/ `), `country/NI` and `country/france` passed. The simulation API then refused them (a 502 here, where 400 is documented) or the job failed and that failure was cached for 30 days. The region must now be exactly one of `LOCAL_AREA_BREAKDOWN_REGIONS`: `uk`, the four UK country regions the metadata publishes (`country/england`, `country/scotland`, `country/wales`, `country/ni`), or `country/northern_ireland`, the code .py's UK region registry uses for Northern Ireland. Every other value, including other casings and surrounding spaces, is a 400, and the message lists the accepted countries. The UK country list moves from an inline literal in `country.py` to `constants.UK_COUNTRY_REGIONS`, which the metadata and this check both read, and the Northern Ireland mapping moves to `constants.UK_REGION_REGISTRY_CODES`. The UK metadata's region list is unchanged. The query description, `docs/canonical-spm.md` and the changelog name the accepted regions instead of `country/*`. Tests written first. Before the change, 9 failed for the expected reason: `country/ `, `country/%20`, `country/NI`, `country/france` and `country/scotland ` raised no ValueError (7 cases across the setup and service tests), and the route returned 200 instead of 400 for `region=country/france` and for a raw `region=country/%20`. The new service test that an invalid breakdown is refused before any reform-impact cache call, worker lookup or submission failed for `country/france` and `country/ ` and passed for a constituency. `Country/scotland`, `UK`, ` uk` and the accepted regions already behaved correctly and pin that. Refs #4021 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A route-level test with the real economy service, in-memory caches and a mocked simulation API submits a breakdown, then polls it to completion (`_handle_execution_state`), then reads it back from storage (`_handle_completed_impact`) without polling again. Both answers carry `MOCK_LOCAL_AREA_BREAKDOWN_RESULT`'s fields unchanged, plus only the `policyengine_bundle` and `resolved_app_name` the service adds to every economy result. One record is stored, with status ok, the breakdown options and a hash that starts `[breakdown=local_areas&`. A national request for the same report then submits its own job instead of reading the breakdown. The fixture's mocked `resolve_app_name` now keeps the requested model version, as the real client does. It returned a fixed version before, so the stored hash never matched a later lookup and every request submitted again. These tests describe existing behaviour and passed when written; no service code changes. Refs #4021 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`services/economic_impact_request.py`, the request type the economy routes pass to the service, now defines `LocalAreaBreakdown = Literal["local_areas"]` and `LOCAL_AREA_BREAKDOWN`. `AnnualEconomyQuery`, `SimulationOptions` and `_breakdown_refusal`'s parameter use the alias, and the service imports the constant instead of defining its own. The module imports nothing else from the package, so `query_parameters.py` can import it without loading the service. No behaviour change: the query, payload and OpenAPI tests pass unchanged. mypy on `economy_service.py` reports the same 11 errors it did before this branch. Refs #4021 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The per-field formatting of a request-validation body (`detail` as a list
of `{loc, msg}`) moves from the breakdown path into the shared
`_build_submission_error_message`, so budget-window refusals read the
same way: `body.window_size: Input should be less than or equal to 75`
instead of the Python representation of the list, which also quoted the
rejected input. Other bodies keep their existing message (the first of
`detail`, `message` or `error`, then the response text, then the
exception text).
Tests written first. Before the change, 2 failed for the expected
reason: the builder and a budget-window 422 refusal both returned the
list's Python representation. The breakdown refusal test passes
unchanged.
Refs #4021
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A breakdown submission's `_telemetry.simulation_kind` is now `local_area_breakdown`, so observability can tell it apart from the national or country run for the same region. `geography_type` and `geography_code` still name the region (`national`/`uk` or `country`/`scotland`, for example). The mark uses the existing field because the simulation API's `TelemetryEnvelope` (policyengine-simulation-observability) has `extra="forbid"`: a new `breakdown` key would make the entry reject the submission. The test checks the telemetry keeps exactly its existing seven fields. Tests written first. Before the change, 2 failed for the expected reason: breakdown submissions for `uk` and `country/scotland` reported `simulation_kind` `national` and `country`. The national cases passed before and after. Refs #4021 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`_select_region_datasets` followed a region's `dataset_path` only for US
regions. With .py 6.10.0 the UK registry lists 650 constituencies and
361 local authorities whose path names `microcosm_uk_2024_25_dense`, but
the catalog published them against `microcosm_uk_2024_25`, the national
file, which has no area codes, while filtering them on
`constituency_code_oa` or `la_code_oa`.
Every subnational region with a dataset path now uses that dataset, so
UK constituencies and local authorities point at
`microcosm_uk_2024_25_dense`, which joins the UK dataset records the same
way US regional datasets do (name and year from the path). UK countries
have no path and still filter the national file. Only US regions without
a path are counted as fallbacks, as before. The v2 metadata's national
dataset is read from the national region, so it is unchanged.
The installed-catalog test now checks each UK region type's dataset, and
counts 4 datasets instead of 3. docs/migration/stage-9-v2-metadata.md
describes the UK selection, and the changelog fragment records it.
Tests written first. Before the change, 2 failed for the expected
reason: the unit test's UK datasets were only `{'microcosm_uk_2024_25'}`,
and the installed 6.10.0 catalog had 3 datasets instead of 4.
Refs #4021
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A breakdown job that failed (for example a timeout or out-of-memory on the local-area dataset) was stored by `_set_reform_impact_error`, and `_handle_failed_impact` returned that error for the 30-day cache lifetime, so the app's Retry got the same error back without a new run. For breakdown requests only, `_handle_execution_state` now deletes the job's computing record with the existing `delete_reform_impact` (the cleanup used for in-flight records) and returns the error, which the route sends as 502. The next request finds no record and submits a new job. No submission claim is held at poll time; it was released when the job was handed off. Failed national jobs are still stored and replayed. A completed job for a breakdown request must also have `kind == "local_areas"`. Any other result, including a national result from a release that ignored the field, or no result, is handled the same way: an error naming the kind it got, and no record kept, so a national result is never cached under the breakdown hash. docs/canonical-spm.md, the served specification's 502 description and the changelog fragment describe both. Tests written first. Before the change, 4 failed for the expected reason: after a failed breakdown job the retry returned the stored error instead of submitting again, and a completed breakdown job whose result had no kind, kind `national`, or no result was stored and returned as ok. The test that a failed national job is still replayed passed before and after. Refs #4021 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion `constants.LOCAL_AREA_BREAKDOWN_REGIONS` is now one ordered tuple: `uk`, the four UK countries the metadata publishes, then `country/northern_ireland`, .py's code for Northern Ireland. The breakdown check, its 400 message and the published `breakdown` query description are all built from it, so `country/northern_ireland`, which the check already accepted, is now listed in both places. The message reads `breakdown=local_areas needs region uk or a UK country, one of uk, country/england, country/scotland, country/wales, country/ni, country/northern_ireland; not 'country/france'`. The changelog fragment lists the same regions. `query_parameters.py` imports the tuple from `constants.py`, which reads only package metadata at import and imports nothing from `query_parameters.py`. Tests written first. Before the change, 2 failed for the expected reason: the message listed only the four metadata countries, and the published description did not name `uk` in the list or `country/northern_ireland` at all. Refs #4021 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A request-plumbing test submits a Northern Ireland breakdown (`region=country/ni`) and polls it once. The simulation payload sends `country/northern_ireland` with `breakdown`, and the stored record keeps region `country/ni`, the breakdown options and the breakdown hash, which is why the second request polls the same job instead of submitting. The unsupported-breakdown test now checks, for every case, that the request is rejected before the cache version is read and that the simulation client is never called. The SPM capability assertion applies only to the US cases, and the test says why: a UK request has no SPM selection, so it never fetches the capability either way. These tests describe existing behaviour and passed when written; no service code changes. Refs #4021 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous commit removed a failed breakdown job's record with `delete_reform_impact`, which matches on scope, options hash and `status == "computing"` and searches only the newest 1,000 records across every country. That caused two faults: - after 1,000 newer economy jobs, the failed job's record was not found, so every request kept polling the failed job and got 502 for 30 days; - a late poll of a failed job J1 that finished after a Retry had stored J2, for the same scope and hash, deleted J2's record, which led to a duplicate submission. It also let a client that retries on every 502 start a new run on the large local-area dataset after each failure. Now `_record_breakdown_failure` marks only the failed job's own record, found by its execution ID, as an error that expires after `LOCAL_AREA_BREAKDOWN_FAILURE_SECONDS` (120). Until then every request gets that error, which ends "A new local-area breakdown can be requested 2 minutes after this failure."; after it, the next request finds no record and submits a new job. No other record is read or changed. To support this, `ReformImpactCache.set` and `update` take an optional `record_ttl_seconds` for the record key only (the scope and recency indexes keep their 30-day lifetime, and an entry whose record has expired is skipped when read), and `ReformImpactsService.set_error_reform_impact` takes `keep_seconds`. National failures are stored for 30 days as before. `_handle_execution_state` and the new helper annotate the stored record as `CachedReformImpact`, the type they receive, instead of the v1 ORM `ReformImpact`. docs/canonical-spm.md, the specification's 502 description and the changelog fragment describe the two-minute window. Tests written first. Before the change, 6 failed for the expected reason: - the failure message had no retry note, and the request two minutes later still got the error instead of a new job; - after 1,000 newer records, the request 121 seconds after the failure polled the failed job again and returned the error, not a new job; - a late poll of failed J1 deleted J2's computing record; - the three wrong-kind cases had no retry note. The test that a failed national job is still replayed, now also after 121 seconds, passed before and after. The cache test that a briefly kept error expires and leaves the next job's record was added with the code; on the previous code it fails with a TypeError, because `keep_seconds` did not exist. Refs #4021 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A changelog fragment for callers: after the .py 6.10.0 update, UK national and country economy results no longer include `constituency_impact` or `local_authority_impact`, because .py runs them on a national dataset with no area codes. Clients get those impacts with a separate request that adds `breakdown=local_areas`. Refs #4021 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`policyengine_api/data/local_authorities_2021.csv`, which the UK
metadata's `economy_options.region` list reads, had 360 local
authorities. It lacked Ards and North Down (`N09000011`), the 11th
Northern Ireland district, so `local_authority/Ards and North Down` could
not be selected, and it gave the four unitary authorities created on
1 April 2023 (Cumberland `E06000063`, Westmorland and Furness
`E06000064`, North Yorkshire `E06000065`, Somerset `E06000066`) the
shared (0, 0) placeholder position.
The file now has the missing row (`N09000011,-3.0,16.0,Ards and North
Down`, after `N09000010`, in the file's code order and number format),
and the four unitaries take their x and y from .py 6.10.0's packaged
`local_authorities_2023.csv`. Its codes, names and positions now equal
that 2023 list for all 361 authorities. The file name is unchanged, to
avoid renaming it everywhere it is read.
The new name resolves in .py 6.10.0's installed UK region registry,
whose label "Ards and North Down" belongs to `local_authority/N09000011`;
the simulation API matches a `local_authority/{name}` request to the
registry by code or label, ignoring case. A test checks that every
offered local-authority name resolves this way.
Tests written first. Before the change, 6 failed for the expected
reason: the metadata and the file had 360 local authorities, not 361;
`local_authority/Ards and North Down` was not offered; Northern Ireland
had 10 districts, not 11; the file differed from .py's list in the
missing code and the four unitaries' (0, 0) positions; and those four
were still at (0, 0). The registry-resolution test passed before and
after for the 360 existing names.
Refs #4021
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
to PolicyEngine/policyengine-app-v2
that referenced
this pull request
Oct 22, 2026
…ncies
The withheld note under a local-authority map now gives an exact count
in UK-wide reports ("2 of 361 local authorities are not shown."), as the
constituency note does. The count is the number of local authorities in
the country metadata's region list that the breakdown's
local_authority_impact list leaves out.
Local authorities were not counted before because the Public API listed
360 and .py 6.10.0 lists 361 (Ards and North Down was missing).
PolicyEngine/policyengine-api#4024 adds it, so the two lists name the
same 361 areas. Breakdowns are offered only for results from .py 6.10.0
on, which that deployment serves.
Both levels now follow one rule: a level gets a count only for a UK-wide
report, and only when every area the breakdown returned is in the
level's list. A breakdown that returns an unlisted area, for example
Ards and North Down against an earlier Public API deployment, gets the
note without a count. Nation reports still get no count: the metadata
gives each area's name, label and type, not its code or nation.
Tests: 9 failed before the change, each because local authorities got
null instead of a count (adapter, output hook, v3 Local areas tab, v2
local authority tab, and v2 sections).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
marked this pull request as ready for review
October 22, 2026 21:59
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #4024 +/- ##
===========================================
+ Coverage 51.11% 87.79% +36.67%
===========================================
Files 242 258 +16
Lines 14441 15950 +1509
Branches 2307 2621 +314
===========================================
+ Hits 7382 14003 +6621
+ Misses 6436 1280 -5156
- Partials 623 667 +44 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Brings in #4026 (raise the economy cache schema version from 3 to 4) and the 5.7.0 "Update PolicyEngine API" release commit. Conflicts: - tests/unit/services/test_reform_impacts_service.py: both sides added a new test at the end of the file. This branch added test_a_briefly_kept_error_expires_and_leaves_other_records; master added test_impact_cached_under_previous_economy_schema_version_is_not_matched. Kept both, this branch's test first. The imports master added for its test (constants, get_economy_impact_cache_version) merged without conflict. Merged without conflict, checked by hand: - policyengine_api/constants.py: RUNTIME_CACHE_SCHEMA_VERSIONS ["economy_impact"] is 4 (master), and UK_COUNTRY_REGIONS, UK_REGION_REGISTRY_CODES and LOCAL_AREA_BREAKDOWN_REGIONS are kept (this branch). - pyproject.toml: package version 5.7.0 (master) and the policyengine[models]==6.10.0 pin (this branch). - uv.lock: policyengine-api 5.7.0 (master) with this branch's .py 6.10.0 dependency set (policyengine 6.10.0, policyengine-core 3.33.3, policyengine-uk 2.132.6). `uv lock --check` passes. - tests/unit/test_constants.py: both sides' additions kept. - CHANGELOG.md and changelog.d/: master's 5.7.0 entry and its removal of the released remove-economy-comparison fragment, plus master's economy-cache-decile-fractions fragment; this branch's fragments are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PavelMakarchuk
approved these changes
Oct 23, 2026
PavelMakarchuk
left a comment
Collaborator
There was a problem hiding this comment.
Thanks Anthony, this looks good. I approve, with a few small notes.
I checked it locally on .py 6.10.0 (policyengine-uk 2.132.6): the make test set passes (3,385 tests), and ruff and mypy pass. The head already includes #4026, so the cache version stays e4… and nothing from 6.7.5 is reused.
- The breakdown has its own options hash, a non-
local_areasresult is never stored, and a failed breakdown expires after two minutes. This matches what app#1398 expects, includingmessageon the 502 andpolicyengine_bundle.policyengine_versionon cached results. - The local authority list now matches .py's registry exactly (361 codes and names, no shared map positions), and the constituencies match too (650, 18 in NI).
Notes:
- The unit "Test" job depends on the bundle check, so CI hasn't run the unit tests yet. After #881's deploy finishes, please re-run the whole workflow, not only the bundle job. Codecov's 11% patch number should also correct itself then.
country/niandcountry/northern_irelandare both accepted for a breakdown, but they make separate cache entries, so the same NI run can happen twice. The app always sendscountry/ni, so this is minor. It could be a follow-up to normalise the region before the cache lookup.- For the record: this also moves UK household calculations to policyengine-uk 2.132.6 and the UK national default to Microcosm, so UK numbers will shift after it ships.
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4021
Summary
.py 6.10.0 runs UK national economy reports on
microcosm_uk_2024_25, a national dataset with no area codes, so a national run no longer returns constituency or local-authority impacts. The simulation API (PolicyEngine/policyengine-sim-api#881) adds a separate run on the local-area dataset that returns only those impacts. This PR lets the Public API request that run, fixes the Northern Ireland region name, and moves the Public API to .py 6.10.0. It supersedes #3991, whose commit is cherry-picked here with-x.Merge order: merge this PR last. Merge it only after PolicyEngine/policyengine-sim-api#881 is merged and deployed (so the simulation API serves .py 6.10.0 with the
local_area_breakdowncapability), and after the app-v2 PR that adds the local-area Compute button (v2 and v3) is merged. Until sim-api deploys 6.10.0, the required check "Ensure PolicyEngine bundle is supported by simulation API" fails withPolicyEngine .py bundle 6.10.0 is NOT deployed in the simulation API. That failure is expected, and so is the "CI complete" failure that follows from it; the jobs that depend on the bundle check (Docker, Cloud Run container, Test environment variables, Test) are skipped until then.#4026 (raise the economy cache schema version from 3 to 4) is merged into
master, andmasterhas been merged into this branch, so this PR carries schema version 4: every economy result, including a local-area breakdown, is cached under ane4cache version.What changes for callers
GET /{country_id}/economy/{policy_id}/over/{baseline_policy_id}acceptsbreakdown=local_areas. The budget-window route does not.target=cliff. The region must be exactlyuk, one of the UK countries the metadata publishes (country/england,country/scotland,country/wales,country/ni), orcountry/northern_ireland, the code .py uses for Northern Ireland. Other regions (including other casings, surrounding spaces and unknown countries), other values and repeated values return 400 before any cache read or submission. The 400 message and the publishedbreakdowndescription list all six accepted regions.breakdownis part of the calculation options only when it is set, so a breakdown request has its own options hash and cached result, and the options hash of every request without it is unchanged. Neither hash matches the other's lookup pattern."breakdown": "local_areas"only when it is set. National and budget-window payloads are unchanged.kind: "local_areas",constituency_impact,local_authority_impact, and the dataset and version fields) is stored and returned like any other economy result: its fields are unchanged, and the service adds only thepolicyengine_bundleandresolved_app_nameit adds to every economy result.kindis notlocal_areas, returns 502, and its result is never stored, so a national result is never cached under the breakdown hash. The error is kept for two minutes on that job's own record, found by its execution ID: every request in that time gets the same error, which ends "A new local-area breakdown can be requested 2 minutes after this failure." After that the record expires and the next request (the app's Retry) submits a new job, instead of receiving the stored error for 30 days. No other job's record is read or changed. Failed national jobs are still stored and replayed for 30 days as before.constituency_impactorlocal_authority_impact; clients get them withbreakdown=local_areas.local_authority/Ards and North Down, codeN09000011), so it lists 361 local authorities, and the four unitary authorities created in April 2023 have their own hex positions instead of (0, 0). The list's codes, names and positions now equal .py 6.10.0's packaged 2023 list.body.breakdown: Extra inputs are not permitted). Budget-window refusals now use the same format instead of the Python representation of the error list.simulation_kindlocal_area_breakdown;geography_typeandgeography_codestill name its region. The simulation API's telemetry envelope forbids extra fields, so no new field is added.country/ni, but .py's UK region registry (6.7.5 and 6.10.0) names itcountry/northern_irelandand has no aliases. The simulation payload now usescountry/northern_ireland; the cache scope keepscountry/ni.microcosm_uk_2024_25_dense, and lists that dataset. Before, they were published against the national file, which has no area codes. UK countries still filter the national file.Commits
Each red-green commit was written test-first: the tests were run and failed for the stated reason, then the code was written and the same tests passed. The commits are:
AnnualEconomyQuery.breakdown: Literal["local_areas"] | None;calculation_options()includes it only when set; the served/specificationpublishes it with its enum. Red: 11 failures, with the parser rejectingbreakdownasextra_forbidden, nobreakdownattribute, and no parameter in the specification._build_economic_impact_setup_options,SimulationOptions.breakdownexcluded when None, and the 400/422 refusal handling in_handle_create_impact. Red: 21 failures, with unsupported requests not raising, route requests returning 200 instead of 400, nobreakdownin the payload, and refusals propagating asHTTPStatusError. Some tests passed before the change; they pin behaviour that must not change (national payloads, lookup patterns, other statuses, the HTTP client's forwarding of non-null fields). One test, for an unknown-value branch the query parser already makes unreachable, was added after the code.country/nireturned unchanged by_setup_regionand in the built payload.-x; not test-first).pyproject.toml,docker/Dockerfile,uv.lockand its changelog fragment, unchanged.REVIEWED_DEFAULT_DATASETS["uk"] = "microcosm_uk_2024_25", the catalog fixtures, and the installed-catalog test's exact counts, updated after review: 7 more variables, 1 more parameter node and parameter, 3 more parameter values (policyengine-uk 2.132.6), and 1,011 more regions (the 650 constituencies and 361 local authorities the 6.10.0 UK registry now lists).Commits added after the first review:
country/, socountry/%20,country/NIandcountry/francereached the simulation API. The region must now be exactly one ofuk, the four published UK country codes, orcountry/northern_ireland. The UK country list moves toconstants.UK_COUNTRY_REGIONS, whichcountry.py's metadata and this check both read (the metadata is unchanged), and the Northern Ireland mapping toconstants.UK_REGION_REGISTRY_CODES. Red: 9 failures: 7 cases (country/,country/%20,country/NI,country/france,country/scotland) raised noValueError, and the route returned 200 instead of 400 forcountry/franceand a rawregion=country/%20. A new test checks that an invalid breakdown makes no reform-impact cache call, worker lookup or submission._handle_execution_state), reads it back from storage (_handle_completed_impact), and checks the stored record and that a national request does not read it. The test fixture's mockedresolve_app_namenow keeps the requested model version, as the real client does.LocalAreaBreakdown = Literal["local_areas"]andLOCAL_AREA_BREAKDOWNinservices/economic_impact_request.py, used byAnnualEconomyQuery,SimulationOptions, the service and_breakdown_refusal._build_submission_error_message. Red: 2 failures, the builder and a budget-window 422 refusal returning the list's Python representation.ukandcountry/scotlandreportingsimulation_kindnationalandcountry.Commits added after the second review:
_select_region_datasetsnow follows any subnational region'sdataset_path, not only US ones, so UK constituencies and local authorities point atmicrocosm_uk_2024_25_dense, which joins the UK dataset records the same way US regional datasets do. The installed-catalog test checks each UK region type's dataset and now counts 4 datasets.docs/migration/stage-9-v2-metadata.mddescribes the UK selection. Red: 2 failures, the unit test's UK datasets being onlymicrocosm_uk_2024_25, and the installed catalog having 3 datasets instead of 4.kind: "local_areas", had its computing record deleted with the existingdelete_reform_impact, and the error is returned as 502. No submission claim is held at poll time; it was released when the job was handed off. Red: 4 failures: the retry after a failed breakdown job returned the stored error instead of submitting, and completed breakdown jobs with no kind, kindnationalor no result were stored and returned as ok. The test that a failed national job is still replayed passed before and after.constants.LOCAL_AREA_BREAKDOWN_REGIONSis one ordered tuple that the check, the message and the description all use. Red: 2 failures, the message and the description not listingcountry/northern_ireland.country/northern_irelandwhile the stored record keepscountry/ni, and the second request polls the same job. The unsupported-breakdown test now checks that every case is rejected before the cache version is read and without any simulation client call; its SPM capability assertion applies only to the US cases, since a UK request has no SPM selection.Commits added after the third review:
delete_reform_impactmatched on scope, options hash andstatus == "computing"and searched only the newest 1,000 records across every country. After 1,000 newer jobs it found nothing, so the failed job was polled and returned 502 for 30 days; and a late poll of a failed job J1 could delete the record of the next job J2 for the same scope and hash. Now only the failed job's own record, found by its execution ID, becomes an error that expires after 120 seconds.ReformImpactCache.setandupdatetake an optionalrecord_ttl_secondsfor the record key only (the indexes keep their 30-day lifetime, and an entry whose record has expired is skipped when read), andset_error_reform_impacttakeskeep_seconds._handle_execution_stateand the new helper annotate the record asCachedReformImpact. Red: 6 failures: no retry note in the message and an error instead of a new job after two minutes; after 1,000 newer records, the failed job polled again 121 seconds later; J2's record deleted by a late poll of J1; and no retry note in the three wrong-kind cases. The national replay test, now also after 121 seconds, passed before and after. A cache test that a briefly kept error expires and leaves the next job's record was added with the code; on the previous code it fails with a TypeError, becausekeep_secondsdid not exist.Commit added at the user's request:
policyengine_api/data/local_authorities_2021.csvgainsN09000011,-3.0,16.0,Ards and North DownafterN09000010, and Cumberland, Westmorland and Furness, North Yorkshire and Somerset (E06000063toE06000066) take their x and y from .py 6.10.0'slocal_authorities_2023.csv. The file name is unchanged. New tests compare every code, name and position with .py's list read from the installed package, and check that every offered local-authority name resolves to a code in .py's installed UK region registry by label, ignoring case, as the simulation API matches it. Red: 6 failures: 360 authorities instead of 361 in the metadata and the file,local_authority/Ards and North Downnot offered, 10 Northern Ireland districts instead of 11, the file differing from .py's list, and the four unitaries still at (0, 0). The registry test passed before and after for the 360 existing names.Merge of
master(merged, not rebased, so the commits above keep their SHAs):tests/unit/services/test_reform_impacts_service.py, where both sides added a test at the end of the file; both tests are kept.constants.py,pyproject.toml,uv.lock,tests/unit/test_constants.pyand the changelog merged without conflict: the schema version is 4, the UK region constants are kept, the package version is 5.7.0, and the lock keeps .py 6.10.0's dependency set.Testing
Run locally on the merge of
master(commit 19), with .py 6.10.0 installed fromuv.lock:make format(no changes),ruff checkanduv run --frozen --extra dev mypy: clean.uv lock --check: the lock matchespyproject.toml.tests/unit/test_constants.py,tests/unit/services/test_reform_impacts_service.py,tests/unit/services/test_economy_service.py,tests/unit/services/test_economy_service_request_plumbing.py,tests/unit/services/test_report_output_service.pyandtests/unit/runtime_cache/: 374 passed. These are the file with the merge conflict and the tests that read the economy cache version.RUN_V2_CATALOG_COMPATIBILITY=1 pytest tests/integration/test_v2_catalog_installed.py: passed.python scripts/run_quality_guards.py: migration-contracts, rules-registry and ci-scripts passed.make testwas not run on the merge; CI runs it.Run locally on commit 18, before the merge of
master:make test(unit, contract,tests/to_refactorand the budget-window dedupe integration test): 3381 passed on commit 18 (3344 before the first review's commits, 3365 before the second review's, 3373 before the third review's, 3376 before the local-authority commit).RUN_V2_CATALOG_COMPATIBILITY=1 pytest tests/integration/test_v2_catalog_installed.py: passed (it failed on Update PolicyEngine bundle to 6.10.0 #3991 withuk default dataset does not match the reviewed selection). It now also checks each UK region type's dataset.tests/unit/test_query_parameters.py,tests/unit/routes/test_economy_spm_query.py,tests/unit/test_openapi_spm.py,tests/unit/services/test_economy_service.py,tests/unit/services/test_economy_service_request_plumbing.py,tests/unit/libs/test_simulation_entrypoint.py,tests/contract/,tests/unit/v2/,tests/unit/test_constants.py,tests/unit/services/test_metadata_service.py: all passed.make format,ruff check(whole repository) anduv run --frozen --extra dev mypy: clean.economy_service.pyis outside mypy's configured files; checking it directly gives the same 11 existing errors before and after this PR.python scripts/run_quality_guards.py: migration-contracts, rules-registry and ci-scripts passed.python scripts/export_migration_contracts.py: no change to the generated files.uv lock --check: the lock matchespyproject.toml.Not run locally: the Postgres-backed integration tests (
tests/integration/test_v2_catalog_publication.py,tests/integration/test_v2_metadata_routes.py), whose expected UK dataset name and label are updated here, and the container builds. CI runs them.Environment variables
This PR changes
docker/Dockerfile(thepolicyengine[models]pin only) and the .py pin that the bundle check reads. It adds no environment variables and reads none in new code.docker/Dockerfilehas noENVorARG. It is built bypr.yml(test_container_builds) andpush.yml(docker), which log in to ghcr.io withsecrets.GITHUB_TOKEN, the token GitHub Actions provides to every job.stagingGitHub environment with job-levelenv:SIM_ENTRYPOINT: ${{ vars.SIM_ENTRYPOINT }}(GitHub Actions environment variable) andSIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }}(GitHub Actions environment secret). Both names exist in thestagingenvironment..github/scripts/simulation_entrypoint_env.shselectsSIMULATION_ENTRYPOINT_URLwhenSIM_ENTRYPOINT=cloud_run_simulation_entrypoint, which is the value the Update PolicyEngine bundle to 6.10.0 #3991 run logged. That run reached the simulation API and reported only that 6.10.0 is not deployed, so the wiring works. IfSIM_ENTRYPOINTwere changed toold_gateway_direct, the job would needOLD_SIMULATION_GATEWAY_URL, which itsenv:block does not pass; that is unchanged by this PR.SIM_ENTRYPOINT,SIMULATION_ENTRYPOINT_URLorOLD_SIMULATION_GATEWAY_URL, andGATEWAY_AUTH_*), with no change to how they are provided.Not covered here
policyengine_bundle.datasetin a breakdown result names the dataset the simulation API's submit response reports, which is the national default; the result's owndatasetfield names the local-area file. This is unchanged here and is tracked in the cross-repository plan.docs/migration/stage-12-worker-contract.mddescribes the simulation API's Stage 12 adapter, which skips breakdown requests (sim-api#881); it is not updated here.No inline
# noqasuppressions are added.🤖 Generated with Claude Code