Repository navigation
Serve a compact parameter search file for each catalog version at /v2/parameters/search-file - #4049
Conversation
|
Review at Verdict: correct and well tested. Two small points on the new gzip negotiation, and one operational consequence to write down. Stacked on #4046, so I reviewed only
Checked and fine: the gzip response sets What I ran: fresh clone, 🤖 Generated with Claude Code |
0e6bd7f to
7cbc75a
Compare
9905cfe to
b1b7c12
Compare
b1b7c12 to
f95842c
Compare
A pure module builds the file v3's parameter search reads from a version's parameters, parameter nodes and current-law values: every parameter a reform can change (by the parameter tree's rule) with its path, stored label, unit, description and whether a draft can edit its current-law value; the labels of the other parameters under gov, which the search's concept clusters, aliases and member labels read; and every node under gov that has a stored label. Labels stay as stored (null when none), so the search's placeholder detection keeps working when the reader applies v1's fallback. Editability is stated for the earliest value plus the dates from which it changes, because a bracket that stops applying has a null value from that date; the file therefore never depends on the day it is built. The file is encoded as compact UTF-8 JSON, gzip-compressed with no timestamp so the same rows always give the same bytes, with the SHA-256 of the uncompressed JSON. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The new table parameter_search_files holds a catalog version's search file once per file format: the gzip-compressed JSON, the SHA-256 of the uncompressed JSON and both sizes. A later file format adds a row rather than changing a stored one, and a file is deleted with its version. The revision is autogenerated with no corrections and writes no rows; catalog publication writes the files. The Public API's runtime role may read the table and the catalog writer may read and insert rows. The parameter tree revision test now names its own revision instead of the head. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Publication now builds, in its transaction and under its lock, the search file of each version it publishes from the installed packages' catalog, and inserts it if the version has none: each new version, and the installed version when an earlier release stored it before search files existed. Files are never built from rows an earlier release stored, so other stored versions are left without one. A stored file of the same format must equal the one built from the installed packages; publication fails if it differs or if a version it publishes has no file. The evidence lists each file written with its sizes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rch-file The route reads the selected version's stored file of the current format with one indexed read and sends the stored gzip bytes unchanged to a client that accepts gzip, or the uncompressed JSON to any other. Each representation has a strong ETag from the SHA-256 of the uncompressed JSON, and a request whose If-None-Match names either gets 304. A named catalog version may be cached for a day and the running version for five minutes. A version without a file returns 503. The route reads its query through the shared strict catalog model, so unknown or repeated parameters and an unsupported country return 422, and OpenAPI declares the file's content schema and response headers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v3's concept clusters pair each node's key with its label, and v1's metadata
labels the root node ("Government"), so the file now lists gov itself as well
as the labelled nodes under it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The builder now returns a ParameterSearchFileDocument TypedDict whose entry shapes a test compares with the OpenAPI content models, so the two cannot drift apart unnoticed. Nodes are read as the parameter tree's TreeNodeSource, and the per-value property is named editable throughout (EditabilityChange, is_editable_value) instead of draftable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… manifest Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Starlette's GZipMiddleware compressed whenever the text "gzip" appeared in Accept-Encoding, so a client refusing it (gzip;q=0) still received gzip; for the parameter search file, that gzip body even carried the uncompressed form's strong ETag. The application now uses NegotiatedGZipMiddleware, which weighs Accept-Encoding qualities and exact coding names, and the search file route decides its encoding with the same rule, so the encoding a route chooses is final. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A version stored before search files existed now returns 503 with the message the service writes for it, which names the version and says why it has no file, as the parameter tree route does; other catalog failures keep the general message. OpenAPI describes this 503 for the route. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on roles Tests cover what the search file now shares with the other metadata routes and publication: browser scripts can read its ETag through CORS, with Vary naming Origin and Accept-Encoding; an unknown catalog version is a 404 without caching headers; publishing as the catalog writer role writes each version's file, which needs the role's declared INSERT grant; and one publication gives the installed version stored without them both its parameter tree and its search file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…angelog Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ormat 2 A version stored before catalog format 2 has no stored economy or household flags, so a search file built from the installed catalog's flags would contradict its own parameter rows. Publication now writes a file for an already-stored installed version only when its stored catalog format is 2, and does not require one from a version stored before; such a version keeps returning the 503 that names it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A request with If-None-Match now reads only the stored file's SHA-256 first; when it names the representation's ETag, the route class answers 304 without the file being loaded, and otherwise the file is read and sent as before. The metadata route helpers' ETag comparison is now the shared names_entity_tag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The file omits editable_changes when editability never changes and never writes null, so the OpenAPI model now declares it as an optional array with no null, and a test compares the builder's typed shapes with the models by JSON type and required keys as well as by field name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…onders Tests now pin accepts_gzip's precedence (an explicit gzip entry decides over the wildcard either way, x-gzip counts as gzip), that a streamed body stays uncompressed when gzip is refused and is gzipped when it is accepted, and that Starlette still provides the two responders NegotiatedGZipMiddleware builds, with the arguments it passes. A unit test follows a value list with a null between two numbers through the editability changes, in date order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The search file route's 503 message, its OpenAPI description, the error's docstring and the changelog now state the rule instead of implying that republishing helps: only catalog format 2 versions that publication has published, or run with installed, since search files were added have a file. The fixed fragment also records that Accept-Encoding: * and differently cased values now receive gzip. The shared ETag description now says the tag hashes the uncompressed body, which the gzip form shares. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
policyengine_api.content_encoding is now in the mypy file list and in the api.quality.mypy-v2 rule's enforced paths and scope, so CI's typecheck-v2 job checks it with untyped definitions disallowed; the rules document is regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The search file writer now reads a stored version's ID and catalog format with publication_reconciliation.stored_version, as the parameter tree writer does, instead of its own query. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The countries tests and the search file's revision test took test_alembic_v2_lifecycle.py to 1,588 lines, over the 1,500-line limit for test code. The lifecycle tests of the catalog tables' revisions (catalog format 2's columns, the parameter tree and the parameter search file) move unchanged to test_alembic_v2_catalog_tables.py, with their revisions and helpers. The three helpers both modules use move to tests/fixtures/integration/alembic_v2.py without their leading underscore: _disposable_url becomes disposable_v2_url, _config alembic_v2_config and _columns table_columns. The v2 Alembic check's script, its workflow test and the testing guide's commands name the new module. It reads the same disposable database settings as the lifecycle tests, which the job already sets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
f95842c to
f364c64
Compare
Master now holds the parameter tree pull request, merged with master's area impact coordinate removal (e86f19a6c4da) under the tree revision 3869eb814a90, and the same split of the v2 lifecycle tests this branch made. The search file revision c614b6bcccfc stays on 3869eb814a90 and is the single v2 head. - test_alembic_v2_lifecycle.py takes master's version: this branch's only change to it was the search file test, which its split had already moved. - test_alembic_v2_catalog_tables.py is master's module with this branch's search file test, so the tree test downgrades to e86f19a6c4da. - The revision chain test lists 3869eb814a90, then e86f19a6c4da, between the head and the catalog format 2 revision. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #4049 +/- ##
===========================================
+ Coverage 53.29% 88.30% +35.01%
===========================================
Files 276 293 +17
Lines 15936 17457 +1521
Branches 2579 2865 +286
===========================================
+ Hits 8493 15416 +6923
+ Misses 6730 1331 -5399
+ Partials 713 710 -3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Fixes #4048
Summary
one compact parameter search file per catalog version, served at
/v2/parameters/search-file, for the parameter search of the app's newer workspace (v3,app/src/libs/parameterSearch.ts). It is stacked on #4046, and v3 does not use it until it moves to the v2 routes.{format, country_id, policyengine_version, parameters, other_parameters, nodes}.parameters: every parameter a reform can change, by Serve the v2 parameter tree one level at a time, with nested bracket schedules and the reform view #4046's shared rule (parameter_tree.is_reform_changeable). Each entry haspath,label,unit,descriptionandeditable, and, only where it applies,editable_changes.editablesays whether a draft can edit the parameter's current-law value: a number, a yes/no value or an infinite threshold, as v3's draft editor allows. Where that changes over time (for example a bracket whose value becomes null from a date),editabledescribes the earliest value andeditable_changeslists{start_date, editable}for each date from which it changes. The file therefore does not depend on the day it was built.other_parameters: thepathandlabelof every other parameter undergov. v3's search reads these for its concept clusters, acronym aliases and member labels.nodes: thepathandlabelofgovand of every node under it that has a stored label, for breadcrumbs, schedule names and concept clusters.parameter_search_filestable holds one row per catalog version and file format (unique on both). Each row holds the gzip-compressed JSON (level 9, no timestamp, so the bytes are reproducible), the SHA-256 of the uncompressed JSON, and both sizes. Rows are deleted with their version.c614b6bcccfcis chained on3869eb814a90(Serve the v2 parameter tree one level at a time, with nested bracket schedules and the reform view #4046). It is autogenerated with no corrections, creates only the table and writes no rows.catalog_formatnull) get no file. Their parameter rows store no economy or household flags, so a file built from the installed catalog's flags would contradict them. They are exempt from the check above. The stored version's ID and format come from Serve the v2 parameter tree one level at a time, with nested bracket schedules and the reform view #4046's sharedpublication_reconciliation.stored_version, so both writers apply this rule the same way.parameter_search_files_written(model, version, uncompressed and compressed bytes).GET /v2/parameters/search-file?country_id=…&policyengine_version=…(Serve v2 metadata in parts loaded on demand, cacheable per catalog version #4042's strict query model; the version is optional).status/resultenvelope.Content-Encoding: gzip. Any other client receives the uncompressed JSON."<sha256>", or"<sha256>-gzip"for the gzip one. A 304 needs the tag of the representation being requested, so a client that switches encoding gets a 200.CacheableMetadataRoute, soCache-Control(a day for a named version, five minutes for the running one),Vary: Origin, Accept-Encoding, the CORS exposure ofETagand the 304 are those of every metadata route.If-None-Matchfirst reads only the stored hash. When it names the current tag, the route answers 304 without reading the file.Route contract changes
New route:
/v2/parameters/search-file, with its OpenAPI responses (200 withETag,Cache-Control,VaryandContent-Encodingheaders; 304; 404, 405, 422, 500 and 503). The contract registry gains one request (93 in all).Every route: gzip follows
Accept-Encoding's qualities. Starlette'sGZipMiddlewarecompressed whenever the text "gzip" appeared anywhere inAccept-Encoding. The newcontent_encoding.NegotiatedGZipMiddlewarereplaces it application-wide, with the same 1,000-byte threshold and level 4, and decides withaccepts_gzip: an explicitgziporx-gzipentry decides; otherwise*does; a quality of 0 refuses. On every route:gzip;q=0,br, gzip;q=0.0) now receives an uncompressed response, where it used to receive gzip;Accept-Encoding: *,br, *and differently cased values such asGZIPnow receive gzip, where they used to receive an uncompressed response;gzipped, no longer receives gzip.The search-file route uses the same rule, so its chosen encoding is final and the middleware never re-encodes it. The middleware subclasses Starlette's
GZipResponderandIdentityResponder, which Starlette does not document; the locked Starlette is 1.0.0, and a unit test fails with a clear message if either class or its constructor signature changes.Measured
Locally, with the .py branch containing PolicyEngine/policyengine.py#701 (6.10.0) installed:
parameters(a reform can change them)editable_changesother_parametersnodesRoute (the US file in local Postgres, FastAPI test client, 15 requests each): gzip 200 in 24.2 ms median (the first request 118.9 ms); 304 revalidation in 9.7 ms median; uncompressed 200 in 25.2 ms median.
Publication. The production-scale publication test passes with this PR: 52.6 s, and 115.1 MiB (120,693,440 bytes) of additional publisher memory against its 256 MiB limit. Its evidence lists both files with the sizes above.
Against the plan's estimate. The plan estimated 0.72 MB compressed for the US (92,706 parameters and 29,310 node labels, measured from v1's metadata on 2026-10-21). The file is 0.87 MB compressed and 19.9 MB uncompressed (the plan's measurement: 12 MB). Without
other_parametersit is 0.77 MB compressed (774,574 bytes) and 19.0 MB uncompressed.other_parametersis kept (decided). v3's search reads labels of parameters a reform cannot change, for its concept clusters, acronym aliases and member labels, so leaving them out could change search results. Keeping them costs 93,832 bytes compressed (0.09 MB).Staging times need a deploy.
v3's search on this file
v3's own search code (
parameterSearch.ts, unchanged) was run over its evaluation cases on three inputs, each a v1-shaped snapshot:/{country}/metadataas today (the reference);ga-hb10013 → 2,ga-sb4764 → 3,ok-sb21564 → 3,ut-hb2102 → 3ga-sb476,ok-sb2156andut-hb210as with v1 labels; alsomo-sb4585 → 1,ms-sb2869not found → 11,ny-s907716 → 18multnomah_pfa:name3 → 5No case found today is lost, and the evaluation case files are unchanged.
Labels: .py's labels are kept (decided)
The file carries the catalog's stored labels, which are .py's. .py generates labels for parameters without a stored one (
utils/parameter_labels.py): bracket parameters become " (tier N amount)" and breakdown parameters " ()". v1's/{country}/metadatadoes not have these generated labels: for those parameters it uses the last part of the name with underscores as spaces, and "bracket N" for brackets. The rank changes in the "catalog labels" rows above come from these labels.This was decided together with #4046's labels: generated labels are shortened only in #4046's parameter tree (
/v2/parameters/children). The search file, like the other catalog reads, keeps .py's labels. Having .py also publish the raw stored label was not chosen, so PolicyEngine/policyengine.py#701 and #4023 are unchanged.Search files for other versions stored earlier: left out
Migration notes
metadataroute group; the route is served by FastAPI and reads Supabase (no change). Nothing moves off Flask.alembic upgrade head, then catalog publication, which writes the files, before the new release takes traffic.# noqa: E501is added, on the import ofdatabase_connectors.reads_parameter_search_filein the metadata services module: the dotted module path alone is longer than the line limit and cannot be wrapped. It matches the two imports beside it.rules/registry.tomladdspolicyengine_api/content_encoding.pyto the scope, statement and checked paths ofapi.quality.mypy-v2, andpyproject.tomladds it to mypy's file list, so the new middleware is type-checked with the v2 code.docs/generated/rules.mdis regenerated. A review fromPolicyEngine/core-developersis requested.Follow-up (not changed here)
The other metadata routes give their gzip and uncompressed responses one strong ETag, computed before compression. Strong ETags should differ between encodings. This PR only corrects the shared header description ("a hash of the uncompressed response body, so a gzip-encoded response carries the same tag"). Changing those routes is left for a follow-up.
Waits for
master). The branch includes a merge ofmaster(below).The decisions on the labels, on
other_parametersand on search files for other stored versions are made (above).Testing
The evidence below is local. For each commit that changes tests, its changed test modules were run against its parent's code (failed first) and then at the commit (passed). Postgres modules ran on a database recreated at the checked-out code's head, with the managed roles' privileges applied.
cb45424f2Build each catalog version's parameter search fileModuleNotFoundErrorforparameter_search_filecfd535864Store each catalog version's parameter search file in its own table3869eb814a90, the import side-effect test's module list,KeyError: 'parameter_search_files'; lifecycle:UndefinedTablealembic checkclean181022df3Write the parameter search file of each catalog version being publishedNoResultFound); a differing stored file did not raisea66c0f1b7Serve a catalog version's parameter search file at /v2/parameters/search-fileImportErrorfor the content model and the unavailable error; Postgres: noETag, 422 instead of 503, no service read3d0e74bddInclude the labelled gov node in the parameter search fileassert [] == [{'path': 'gov', 'label': 'Government'}]fed8282ebType the parameter search file and name its editability consistentlyImportErrorforEditabilityChange2a195cae8Describe when parameter search file rows are written in the privilege manifest7d43be8eaGzip a response only for a client that accepts gzipgzip;q=0,br, gzip;q=0.0andgzippedwere compressed;br, *andGZIPwere not; a large search file sent withgzip;q=0came back gzipcb3189b44Name the catalog version in the search file route's 503a57d5ab73Test the search file route's CORS exposure and 404, and its publication rolesInsufficientPrivilege: permission denied for table parameter_search_fileswhen the grant is revoked and the declaration narrowedc962328edDescribe the gzip negotiation fix and the search file's 503 in the changelog8e1afa554Write no search file for an installed version stored before catalog format 2f57a986b7Answer a search file revalidation from its stored hash alone6c1b09df0Declare editable_changes as an optional list that is never nulleditable_changes4a54539a2Pin gzip negotiation precedence, streamed bodies and Starlette's respondersc6def889fSay plainly which catalog versions have a search file301e361bbType-check the gzip negotiation module with the v2 code8c54125f4Look up the stored catalog version through the shared helperf364c6459Split the v2 catalog tables' lifecycle tests under the file-length limitAt every commit, ruff passes and the focused unit set passes (the search file, metadata route, metadata service, publication, application and gzip modules: 203 to 268 tests). From the second commit on, the Postgres publication and metadata route tests pass at every commit (50 to 62 tests).
On
8c54125f4, before the lock moved to .py 6.12.0:make testset (without coverage), under the locked .py 6.10.0 from the lock file: 3,737 passed.alembic current --check-heads:c614b6bcccfc (head);alembic check: no new operations..coveragefile or names a private repository.At the head, under the locked .py 6.12.0, checked before GitHub's rebases onto
master(asb1b7c12d8;git range-diffshows every commit unchanged since):c614b6bcccfc: 23 passed.Merge of
master2f86cf1f1mergesmasterat8c3fb8495, which holds Serve the v2 parameter tree one level at a time, with nested bracket schedules and the reform view #4046 with its own merge ofmaster: the tree revision3869eb814a90now followse86f19a6c4da(the UK area impact tables' map coordinate removal), and the v2 lifecycle tests are split as this branch split them. The search file revisionc614b6bcccfcstays on3869eb814a90and is the single v2 head.test_alembic_v2_lifecycle.pyismaster's: this branch's only change to it was the search file test, which its split had already moved.test_alembic_v2_catalog_tables.pyismaster's module with this branch's search file test, so the tree test downgrades toe86f19a6c4da.3869eb814a90, thene86f19a6c4da, between the head and the catalog format 2 revision.alembic headsshowsc614b6bcccfcalone;alembic checkfinds no new operations; ruff, mypy (190 files), the quality guards, rules validate and guard,uv lock --checkand the contract export pass. One review round ran on the merge (below).Review
Round 1 found two minor bugs.
gzip;q=0) still received gzip under the uncompressed form's ETag, because Starlette's middleware compressed the body. Fixed in7d43be8ea, which changes negotiation on every route (above).govnode ("Government"), which v3's concept clusters read. Fixed in3d0e74bdd.Its quality findings led to the move onto Serve v2 metadata in parts loaded on demand, cacheable per catalog version #4042's caching route class (in the route commit, during the rebase onto Serve v2 metadata in parts loaded on demand, cacheable per catalog version #4042 and Serve the v2 parameter tree one level at a time, with nested bracket schedules and the reform view #4046),
fed8282eb,2a195cae8,cb3189b44anda57d5ab73.8e1afa554then applied Serve the v2 parameter tree one level at a time, with nested bracket schedules and the reform view #4046's rule for versions stored before catalog format 2.Round 2 found no substantiated bugs. Its final-pass findings are fixed in:
f57a986b7: the 304 reads only the hash;6c1b09df0:editable_changesis optional and never null, and the shape test compares types;4a54539a2: tests for precedence, streamed bodies, Starlette's responders and an editability change in the middle of a value list;c6def889f: the 503 message, the changelog fragments and the shared ETag description;301e361bb: the mypy file list;8c54125f4: Serve the v2 parameter tree one level at a time, with nested bracket schedules and the reform view #4046's shared stored-version helper.Merge of
master: one review round on2f86cf1f1found no substantiated bugs and no material quality findings. It noted that the search file revision's creation date is earlier than its regenerated parent's; its operations touch onlyparameter_search_files, and the lifecycle tests confirm the head schema matches the models, so it is not regenerated.🤖 Generated with Claude Code