Repository navigation
Fall back to the committed OBR workbooks when obr.uk answers 200 with a non-workbook page - #536
Merged
Merged
Conversation
… a non-workbook page obr.uk can answer the EFO URLs with HTTP 200 and an HTML "No Access" page instead of the xlsx. openpyxl then raised zipfile.BadZipFile, which is not a requests.RequestException, so it escaped _download_workbook before the committed-workbook fallback ran, and get_targets() dropped the nine receipts and NICs targets (34 -> 25). A body that openpyxl cannot read (BadZipFile, InvalidFileException, KeyError, OSError, ValueError) is now a permanent download failure: it is not retried, control reaches the fallback, and the warning names the URL, the status, the Content-Type and the parse error. Tests run every failure kind (200 HTML/empty/truncated zip/zip without xlsx manifest/zip without workbook part, 403, 404, 500 until retries run out, connection error) against both EFO URLs and get_targets(), with a positive control that a real workbook body is used without the fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A zip with malformed XML inside raised xml.etree.ElementTree.ParseError (a SyntaxError), which escaped the named-exception tuple; zlib.error, NotImplementedError, RuntimeError and TypeError could too. Catch Exception around the single load_workbook call on the response body: a genuine openpyxl break still surfaces, because the fallback is parsed by the same call outside any try. Chain the parse error as the cause of the reported ValueError. Tests: add a malformed-XML row; pin the fallback warning text and the ValueError raised for a URL with no committed workbook; fold the 403 and connection-error tests into the response-kind table they duplicated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Second review round: the broad catch only guarantees that an openpyxl break on every workbook still surfaces, so the comment now says that; the message says the body "could not be read as an xlsx workbook", which also covers a served file openpyxl rejects. The changelog says the defect drops every target from the affected workbook (nine in the observed receipts case), and the no-fallback test asserts the parse error is chained as the cause. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 4, 2026
Contributor
Author
|
Queued in release PR #544 for the 10/8 uk-data batch. It lands only on Max's go (d833). |
MaxGhenis
added a commit
that referenced
this pull request
Oct 7, 2026
…ount (F1) test_full_target_set_available_offline (from #536) asserted at least 30 OBR targets offline. The release combines four member PRs that each replace OBR targets with better sources, which no single PR's CI saw together: #490 swaps obr/housing_benefit for DWP Housing Benefit targets, #510 swaps obr/pension_credit for DWP Pension Credit spend and caseload, #533 swaps the two OBR salary-sacrifice NI relief targets for HMRC's, and #530 merges the two OBR UC targets into obr/universal_credit. That leaves 29 OBR targets (34 on main), so release CI failed with 29 >= 30. The test now checks what it was for: offline, the committed workbooks give exactly the OBR targets that the same workbooks give when served, plus the receipts and NICs names. A broken fallback still fails it. Release-only fix for the 10/8 uk-data release (d833). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
21 of 52 tasks
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.
What
_download_workbookinpolicyengine_uk_data/targets/sources/obr.pyfalls back to the committed EFO workbooks instorage/obr_efo/when the download fails. obr.uk can also answer HTTP 200 with an HTML page titled "No Access - Office for Budget Responsibility" instead of the xlsx. The UK hub saw this on the receipts URL from Max's machine on 2026-10-04. On a 200,openpyxl.load_workbookthen raisedzipfile.BadZipFile. That is not arequests.RequestException, so it escaped before the fallback ran.get_targets()loggedFailed to download/parse OBR receipts: File is not a zip file, and nine targets disappeared:obr/income_tax,obr/ni,obr/vat,obr/fuel_duties,obr/capital_gains_tax,obr/sdlt;obr/ni_employee,obr/ni_employer,obr/ni_self_employed.Fix. A 200 body that openpyxl cannot read is now a permanent download failure:
The except clause wraps only the one
load_workbookcall on the response body, and it catchesException. A first version caught a named tuple (BadZipFile,InvalidFileException,KeyError,OSError,ValueError). Review showed that a zip with malformed XML inside raisesxml.etree.ElementTree.ParseError, aSyntaxErrorsubclass, which escaped that tuple. So couldzlib.error,NotImplementedError,RuntimeErrorandTypeError. An openpyxl break on every workbook still surfaces, because the fallback goes through the sameload_workbookoutside any try. The parse error is chained as the__cause__of the reported error. The docstring now says the fallback covers both 403s and 200 pages that are not workbooks.get_targets()'s broad catch-and-log is unchanged.The fallback warning now reads:
Impact, from real calibrations (2026-10-04)
I ran four seeded calibrations of one saved input, changing only the code (
mainor this branch) and what obr.uk returns.Setup
mainb45c373 (base) and this branch 7605e63 (head). They differ only inobr.py, its tests and a changelog fragment;uv.lockandpyproject.tomlare identical. Both ran in one environment built from that lock, withPYTHONPATHselecting the code.calibration_input_2025.h5, sha25602b1aa09ec86…), built frommainthrough every step ofcreate_datasets.pyup to calibration, including calibration-year materialisation.PE_UK_DATA_OA_CLONES: "1"in.github/workflows/push.yaml; 10 is only the default increate_datasets.py). Each run takes about 8 minutes.storage/.calibrate_local_areasfor constituencies exactly ascreate_datasets.pycalls it: 512 epochs, 650 areas,torch.manual_seed(0), 8 threads.requests.getis wrapped for the two EFO URLs only; every other target source downloads normally.Results. Weighted 2025 totals from the calibrated dataset (£bn):
mainmainmainmainand this branch take the same path to the same targets. With the block page, this branch reproduces that result through the committed workbooks._parse_nics"no row matched" warnings appear, which happens only after the receipts workbook loads. So that build fetched the workbooks directly and had these targets, matching run D.Two pre-existing fits on
mainare outside this PR's scope; the same values appear in B and D:obr/nitarget (total NICs, £200.08bn) maps to theni_employeevariable, whose estimate is £48.45bn;obr/vatends at 1.76× its target.They are being raised separately.
The calibration inputs and outputs are FRS-derived, so they stay on the hub's machine (
~/reviews/uk-hub/jobs/obr-fallback-r2/), along with the harness (impact.py) and run logs. Only the aggregates above leave it.Unit-level check, with the receipts request stubbed and expenditure live:
get_targets()returns 25 targets onmainand 34 on this branch.Invariants
get_targets()returns the nine OBR receipts and NICs targets, provided the committed workbook exists and is readable. Enforced bytest_every_failed_download_keeps_receipts_and_nics_targets, withrequests.getmonkeypatched (no network). Out of scope: a 200 whose body is a valid xlsx that lacks the expected OBR sheets or rows. It loads, skips the fallback, and the per-table parsers then log and drop those targets. The ten kinds:ConnectionError._download_workbookreturns the committed workbook for every failure kind, at both EFO URLs insources.yaml. Enforced bytest_every_failed_download_uses_committed_workbook, which uses a spy on_fallback_workbookand checks that the returned object is the fallback's.ConnectionErrorcost_DOWNLOAD_MAX_ATTEMPTS(4). Enforced by the same parametrized test.efo_receipts.xlsxis parsed from the response, with one request and the fallback never called:test_workbook_response_is_parsed_without_fallback.HTTPErrorfor a 403, andValueErrorchained fromBadZipFilefor an HTML 200 (test_unknown_url_with_failed_download_still_raises). The warning text is pinned bytest_non_workbook_200_warning_names_url_status_and_parse_error.The two older tests,
test_403_uses_fallback_without_retryingandtest_connection_failure_uses_fallback, are folded into the response-kind table, which covers both cases at both URLs. Hypothesis is not a dependency, so the property is enforced by exhaustive parametrization over the response kinds.Tests
pytest policyengine_uk_data/tests/test_obr_*.py -q: 54 passed, 5 skipped. The skips are the enhanced-FRS-gated signal tests intest_obr_nic_signal.py. Of these,test_obr_efo_fallback.pyaccounts for 36 passed.main'sobr.py, and exactly the non-workbook-200 cases failed (15 failed, 18 passed), with the hub's error:File is not a zip file. The malformed-XML row fails against the first version's exception tuple.ruff format --checkandruff checkare clean on both changed files.Review
There were two rounds of independent review by an Opus 5.5 agent, then GPT-6.1 Sol reviews at 7605e63. The first asked for real-run impact and a narrower invariant, both addressed above, and found no code defect. The second approved with two wording nits (the clone count and the mutation-check revision), both fixed in this description.
The Opus rounds:
axiom: n/a: data pipeline infrastructure (OBR target download), no policy change.
Part of the uk-data batched release (d833).
🤖 Generated with Claude Code