Skip to content

Serve economy reports and simulations from the detailed-save tables at /v2/reports and /v2/simulations - #4281

Draft
anth-volk wants to merge 47 commits into
masterfrom
v2plan/c1-8-simulations-and-reports
Draft

anth-volk wants to merge 47 commits into
masterfrom
v2plan/c1-8-simulations-and-reports

Conversation

@anth-volk

@anth-volk anth-volk commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #4280

Summary

Adds /v2/reports (economy kind) and /v2/simulations. Reads come from the detailed-save tables, and creates go through the simulation service. Replaces #4108, whose reads through the simulation service, lookup by v1 report number and first-read runs no longer fit the design.

Reads need country_id and no user credential. They never call the simulation service or start a run.

  • GET /v2/reports/{report_id} returns the report within its reports.country, with its newest run: the one created last, with the larger ID winning a tie, whatever its status and versions. This deliberately differs from picking the newest succeeded run on the deployed versions, which would hide the out-of-date run a client needs to see.
    • The answer: the definition (label, year, region code, dataset name, each simulation's policy and dynamic, options), status (not_run or the run's), run (status, trigger, both package versions, simulation run IDs, error, times), current_versions and result.
    • result is filled only once the newest run has succeeded.
  • GET /v2/reports?report_id=… lists 1 to 25 reports in one SQL statement, in the order named, each once, without results. Unknown reports, other countries' reports and reports of other kinds are left out.
  • GET /v2/simulations/{simulation_id} returns the simulation and its newest run, with current_versions. Its country comes from its catalog version's model.

The result rebuild (services/v2/stored_reports/economy_results/) reverses the simulation service's writer part by part into SingleYearMacroOutput:

  • budget, programs, income and wealth deciles, and intra-decile shares;
  • poverty and inequality for each simulation run;
  • labour supply and its deciles;
  • congressional districts, and UK constituencies and local authorities with their disclosure fields;
  • cliffs, SPM receipts (typed as SPMComparisonProvenance) and the data and model versions.

The UK local-area breakdown is left out until its storage is decided. A golden fixture, tests/fixtures/economy_result_rows_golden.json, was produced by running the simulation service's real writer (scripts/generate_economy_result_golden.py, at the simulation service's commit cb1264a5). It covers the US, the UK, labour supply deciles −1 and 0, at-large districts and a negative budget, and a test rebuilds it exactly.

Creates. POST /v2/reports and POST /v2/simulations take country_id in the query and the body, refuse bodies over 16 KiB, and answer 201 with the record read back from the tables.

  • Report body: kind: "economy", label, description, year, region_code, an optional dataset_name, baseline and reform (each an optional policy_id and dynamic_id), include_cliffs and an optional spm.
  • Simulation body: kind, year, region_code, an optional dataset_name, policy_id and dynamic_id.
  • Checks before calling the simulation service: the country must allow society-wide calculations, and each named policy must be stored in it. No field names a version: the simulation service runs its default worker's.

Answers from the simulation service. No answer that invites a resubmission follows a record the simulation service may have created, unless it says so.

The simulation service The create answers
201 or 202 201 with the record read back. If the read-back fails, 502 with the created report_id or simulation_id, saying to read it rather than submit again.
422, or a 400 whose code is unknown_region, unknown_dataset or unknown_dynamic 422 with its code and reason
Any other 400 503 with its code and reason
A readable 503 that names the submission's own record 503 with that record's ID and sim's code
A readable 503 that names no record 503 with sim's code: nothing was created
401, 403, 404, 405, 413 or 429, which come before sim's route runs; a connection never made; a gateway token that can't be fetched 503
An answer lost after sending, or a proxy's 502 or 504 502: "The simulation service may have received the request, but its answer was lost (…). Submit the report again; if the first request went through, this creates a second report."
Its own 500, an unreadable 201 or 202, a 503 that can't be read or names another or a malformed record, or any other status 502: "The simulation service may have created the report, but it failed (…). Submit the report again; if the first request went through, this creates a second report."

For sim's 500 and 503, only the status and sim's code reach the caller, never sim's error text.

Saved records. Saved reports and saved simulations accept a detailed-save report_id or simulation_id, one reference per request. A request naming both a v1 number and a detailed-save ID gets 400, as does an update that would leave a row holding both. The reference is checked with a SELECT in the write's own transaction. A foreign-key violation (SQLSTATE 23503) on those keys, from a record deleted after that check, answers 404; the insert and update connectors turn it into the not-found error, so the services import no SQLAlchemy. The shared report answer now includes report_id.

The call to the simulation service sits behind the SimulationServiceClient protocol. The adapter, policyengine_api/libs/simulation_reports.py, reuses SimulationEntrypointClient's HTTP client, so the gateway credentials and the request and observability ID headers come from there. Wiring the released generated client is one change: add policyengine-api-simulation-client>=<the release with these routes>, relock, and replace post_report and post_simulation with the generated create_report.sync_detailed(...) and create_simulation.sync_detailed(...), as their docstrings show.

Shared with household calculations (#4274). The adapter and the household calculation client now share policyengine_api/libs/simulation_service_answers.py, so neither keeps its own copy of these:

  • the HTTP client's construction;
  • the transport-error sorting and the proxy statuses;
  • the answer and refusal models;
  • the refusal's reason, and the status and code a message gives;
  • the lost-answer and failure messages.

The SimulationService*Error types are defined once, in services/v2/simulation_service/types.py. The 422 error is SimulationServiceInvalidRequestError, the household calculations' name. Both routes answer exactly as before.

Merge order

The simulation service's POST /reports and POST /simulations don't exist yet; that work is separate. Until they are deployed, both creates answer 503, while the reads work as soon as this merges. The adapter, policyengine_api/libs/simulation_reports.py, defines the request and answer shapes those routes must use, including the three 400 codes in CALLER_ROW_REFUSAL_CODES.

Tests

  • Each test was run individually, by node ID, and seen failing for the expected reason before its change.

  • Unit tests: the result rebuild, the golden fixture, newest-run selection with equal creation times, the list query, country checks, the create services, saved-record checks and the route answers. Every simulation-service answer in the table goes through the real HTTP client over a mock transport to the route's JSON.

  • Integration tests on PostgreSQL 17, as the runtime role: US and UK rows in all 11 result tables, including UK disclosure fields and their nulls, and the saved-record foreign keys. They run in two existing v2-integration-check.yml steps.

  • Checks: alembic -c alembic-v2.ini check (no operations), lint-imports, the rules registry, the quality guards, the migration contract export (regenerated), ruff, ruff format and mypy.

  • Review: three rounds, and a follow-up of the fixes.

    • Round 1: a lost answer, and a failed read-back after the record was created, both answered 503, which invites a duplicate.
    • Round 2: every 400 from the simulation service was treated as the caller's error, and its 503 lost its code and created record. The lost-answer message also promised a check no route supports.
    • Round 3: three kinds of 503 could follow a created record.
    • Round 4, of the fixes: an unexpected status, such as a 200, answered the "nothing was created" 503.
    • After merging master with Calculate stored households through /v2/household-calculations #4274, one round on the shared code and two CI fixes. A saved record repointed to a record deleted after the check answered 503: the update connector read its row after the failed flush had expired it.
    • A round on that fix found no bug.

    All are fixed test-first.

Environment variables and secrets

No new ones.

Value Provider How it reaches the code
SIMULATION_ENTRYPOINT_URL GitHub environment secret in staging and production The deploy job's env: in push.yml, then deploy_cloud_run_candidate.sh, then the Cloud Run revision. Unchanged.
GATEWAY_AUTH_ISSUER, GATEWAY_AUTH_AUDIENCE, GATEWAY_AUTH_CLIENT_ID, GATEWAY_AUTH_CLIENT_SECRET_RESOURCE Organization secrets; the client secret itself in Secret Manager The deploy step's env:, then the deploy script and Cloud Run. Unchanged.
V2_RUNTIME_DATABASE_URL_SECRET_RESOURCE Environment secret Unchanged.

Not verified:

  • that each environment's SIMULATION_ENTRYPOINT_URL names a simulation service with the new routes;
  • that the Auth0 client holds permission for those routes;
  • that the runtime role's grants on the v2 tables are applied in staging and production.

🤖 Generated with Claude Code

anth-volk and others added 30 commits November 11, 2026 00:22
The tests store a US and a UK economy result through a port of the
simulation service's writer, then expect the rows to give the same result
back, in any row order, and expect a typed error when a part every result
has is missing. The rebuild itself is a stub.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
economy_results.transformations reverses the simulation service's writer
part by part, giving the single-year result format the app reads today:
the budget, programs, deciles, poverty, inequality, labour supply,
congressional districts, UK areas with their disclosure fields, cliffs,
the SPM receipt and the data version. An optional part with no rows is
null; a missing required part raises EconomyResultUnreadableError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The integration tests store US and UK reports, runs and results as the
simulation service writes them and read them through the routes as the
runtime role: the newest run and its result, a running or failed newest
run, a report with no run, other countries, an unreadable result, a
household report, lists in the order asked, and simulations. The unit
tests check the routes' answers over stand-in services. The services and
routes do not exist yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tables

GET /v2/reports/{id} reads the report in its country with its newest run,
whatever that run's status and versions, the versions the Public API runs
now, and the run's result once it succeeded, rebuilt from the result rows.
GET /v2/reports lists up to 25 named economy reports and their newest runs
in one query, in the order named, without results. GET
/v2/simulations/{id} reads a simulation of the country's model and its
newest run. A report with no run answers its definition; household reports
and budget windows are refused with 422 until their own routes exist. A
failed database read is a typed storage error, answered with 503.

The read test's expected result takes its model version from the run, as
the simulation service's writer stores it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(failing)

The tests define the requests the Public API sends to the simulation
service's POST /simulations and POST /reports, and how each of its answers
is read: the created IDs, refusals of the caller's data with their reasons,
an internal error, an unreadable created answer, and every other answer
or no answer as the service being unavailable. The client does not exist
yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SimulationServiceReportClient posts to the Cloud Run simulation service's
POST /simulations and POST /reports with the same HTTP client, gateway
credentials and request headers as every other simulation call, and reads
the created IDs, refusals with their reasons, internal errors, unreadable
created answers and unavailability. post_simulation and post_report are
the only code that sends a request, so the generated client replaces just
them once released. The SPM settings test now expects the whole selection,
as the household calculation client sends it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n checks (failing)

The tests expect a household-only country, a country without a countries
row and a policy the country does not store to be refused before the
simulation service is asked; POST /v2/reports and POST /v2/simulations to
answer the created record as the tables hold it, with 201; malformed
bodies, another country and oversized bodies to be refused; each failure
of the simulation service to get its own answer; and a created report the
Public API cannot read back to be unavailable. The submission service and
the POST routes do not exist yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /v2/reports and POST /v2/simulations go through
SimulationSubmissionService, which refuses a country without economy
calculations (422) and a policy the country does not store (404) before
the simulation service is asked, then sends the request and reads the
created record back from the tables (201). Report options are
include_cliffs and the SPM choice; no request names a PolicyEngine.py
version. The simulation service's refusals answer 422 with its reasons, its
internal errors and unreadable created answers 500, and its unavailability
503, as does a created record the Public API cannot read back. C1 · 9's
missing-parameter refusal belongs in the same submission checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tests expect a saved report to name a stored report of its country by
report_id, and a saved simulation a stored simulation of its country's
model by simulation_id: a missing or foreign one is not found (404),
checked with a SELECT in the write's transaction and, if it goes between
the check and the write, by the RESTRICT foreign key (SQLSTATE 23503). A
request naming both a detailed-save ID and a v1 number, or one that would
leave a saved record naming both, is refused (400). The references are not
accepted yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
user_report_associations.report_id and user_simulation_associations
.simulation_id now take a detailed-save ID on create and update: a report
stored in the country, or a simulation of the country's model, checked with
a SELECT in the write's own transaction (404 otherwise). A record deleted
between the check and the write is refused by B1 · 11's RESTRICT foreign
key, whose SQLSTATE 23503 on that key is answered as not found too. A
request naming both a detailed-save ID and a v1 number, or an update that
would leave a saved record naming both, is refused with 400; only the
production migration writes saved records holding both. The shared report
read now carries report_id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntracts (failing)

The tests expect /v2/reports and /v2/simulations to be logged as reading
Supabase and, on create, computing on the Cloud Run simulation service,
whatever the route groups' flags select; a detailed_save_reports_v2
workflow contract; and the new integration tests in the cross-database
integration workflow. The OpenAPI path list now includes the four new
paths the read and create commits added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/v2/reports joins the report route group. Requests to /v2/reports and
/v2/simulations are logged as reading Supabase, and on create as computing
on the Cloud Run simulation service, without reading the route groups'
database, entrypoint or compute flags. The detailed_save_reports_v2
workflow contract lists their five requests, the saved-record contracts
gain report_id and simulation_id, the generated migration contracts are
regenerated, and the cross-database integration workflow runs the three
new integration test files. Adds the changelog fragment and the Stage 12A
documentation of saved detailed-save records.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… route tests

The saved-simulation route test expected a simulation_id to be refused,
and the saved-report payload test refused a body naming a report_id and a
legacy_report_id at validation. Saved records now take a detailed-save ID,
and a body naming both is refused by the service with 400, which the
service tests cover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wer 502 (failing)

A submission that may have reached the simulation service must not be
answered 503, which invites the caller to submit again and create a second
record. Only a connection never made and a gateway token that cannot be
fetched stay unavailable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A connection never made and a gateway token that cannot be fetched stay
503. An answer lost after sending, a proxy's 502 or 504, the service's own
500 and a created answer that cannot be read are now 502, as for household
calculations, since the service may have created the report or simulation
and started its run, and a retry would create a second one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…its ID (failing)

The simulation service has answered that it created the report or
simulation, so the record exists: a 503 invites a second submission and
loses the ID the caller needs to read it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once the simulation service has answered with a report's or simulation's
ID, the record exists. Any failure to read it back (not stored in the
country, the database unavailable, or another read failure) now answers
502 with report_id or simulation_id and a message to read it with the GET
route, instead of a 503 that dropped the ID and invited a second
submission. The OpenAPI document gives the 502's two shapes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lier one (failing)

The shared submission mappings already answer a missing v2 database
configuration with 503 "V2 storage is unavailable", so each route's own
V2ConfigurationError entry is never used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ppings

SUBMISSION_ERROR_MAPPINGS, which both routes list first, already answers
V2ConfigurationError with 503 "V2 storage is unavailable"; each route's
own mapping now covers only its storage error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An update points a saved simulation at another simulation of its country
through the reference it already holds, and refuses one that would leave
both references set; nothing compares the simulations' economy or
household kind, and the plan asks for no such check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the newest

Covers the ID tiebreak for a report's read and list and for a simulation's
read. The larger ID is stored first, so insertion order cannot pass the
tests; reversing the tiebreak makes both fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd typed SPM receipts (failing)

scripts/generate_economy_result_golden.py runs report_run_result from
PolicyEngine/policyengine-sim-api at a named commit, read with git show,
on the US and UK fixture results and on variants with labour supply
deciles -1 and 0, DC and Wyoming at-large districts and a negative budget.
tests/fixtures/economy_result_rows_golden.json holds what it stored at
cb1264a5 (writer last changed in c02145fd); each case rebuilds exactly.

The two SPM tests fail: a stored receipt is still read as a plain dict, so
one of another shape is not refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The receipts sim's writer stores fit the Public API's comparison type,
which the v1 economy output already validates them with. Receipts of
another shape, such as a baseline and reform that differ, now make the
result unreadable instead of passing through. Answers keep the package
names as runtime version keys, as the Public API serves them elsewhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mypy reads .py as untyped; importing the module by name keeps the script
clean under an explicit mypy run without a suppression. Generating again
from cb1264a5 writes the same fixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d created record (failing)

A 400 answers 422 only for the codes naming the caller's region, dataset
or dynamic, with the service's code and reason; any other 400 answers 503.
A 503 passes on the service's code and reason, and a 503 naming the record
the service created returns its report_id or simulation_id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mulation service's code, reason and created record

The simulation service also uses 400 for its own states, such as a catalog
version missing while a release deploys. A 400 now answers 422 only with
a code in CALLER_ROW_REFUSAL_CODES (unknown_region, unknown_dataset,
unknown_dynamic); any other 400 answers 503. Refusals and the service's
503 give its code and reason. A 503 naming the record the service created
and whose run it failed answers 503 with the report_id or simulation_id
(UnavailableAfterReportCreatedResponse,
UnavailableAfterSimulationCreatedResponse), logged as
v2_report_unavailable_after_creation or
v2_simulation_unavailable_after_creation.

SimulationServiceUnavailableError carries created_record_id, and its
docstring no longer says nothing was created. The changelog says exactly
when a create answers 422, 502 and 503.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t lost answers ask to submit again (failing)

A 503 that cannot be read, one whose report_id or simulation_id is not a
UUID, and one naming only another record's ID may follow a record the
simulation service created, so each should answer 502, not the 503 that
says nothing was created. Every 502 for an answer that may follow a
created record should ask the caller to submit again and say that this
creates a second record if the first request went through.

The route tests send each of these answers through the real
SimulationServiceReportClient, over a mock transport, to the route's JSON,
as they do the two 503s that keep their answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mit lost answers again

A 503 the Public API cannot read (such as Cloud Run's own, sent in the
simulation service's place), one whose report_id or simulation_id is not a
UUID, and one naming only a record other than the submission's own may
follow a record the simulation service created. Each now raises
SimulationServiceAnswerUnreadableError, which the create routes answer
with 502. A readable 503 naming the submission's own record keeps its 503
with the record's ID, and one naming no record keeps its plain 503, which
now means the service created nothing.

The Public API will not choose record IDs, and no route finds a record by
what it was submitted with, so the 502s for a lost answer, a proxy's 502
or 504, these 503s and the service's 500 no longer ask the caller to check
for the record. They ask the caller to submit again, and say this creates
a second record if the first request went through, keeping each case's
detail: the transport error, the proxy's status, or the service's status,
code and reason.

The OpenAPI 502 and 503 entries, the error types' docstrings, the error
mapping comment and the changelog fragment say the same. The route test
of hand-built errors uses messages the client now gives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 6 commits November 11, 2026 20:16
…on only their code (failing)

A status the client doesn't expect may follow rows the simulation service
wrote, so it should answer 502; only 401, 403, 404, 405, 413 and 429, sent
before the service's route runs, keep the plain 503. The messages for the
service's 500 and 503 give its status and code but never its reason, and
say the service may have created the record and failed. Each message writes
the status and code as "503 modal_call_refused". The route tests drive the
500 without a code, the unreadable 500, the 503 without detail and a 503
naming both records through the real client, and the expected messages come
from one shared test helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…0 or 503

The simulation service's route may write rows before answering a status
the client doesn't expect, such as 200, 204 or 409, so each now answers the
502 that asks the caller to submit again. Only 401, 403, 404, 405, 413 and
429, which the gateway, Cloud Run or the service's framework sends before
the route runs, keep the plain 503.

The messages for the service's 500 and 503 give its status and code but
never its detail, which may be an exception's text holding stored data. A
400's or 422's reason, which describes the caller's own request, is still
passed on. The 500, the 503s answered 502 and the unexpected statuses say
the service "may have created the report, but it failed (500
internal_error)"; a lost answer or a proxy's 502 or 504 still says the
service "may have received the request, but its answer was lost". Every
message writes the status and code as "503 modal_call_refused". The OpenAPI
entries, docstrings and changelog fragment say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… app

The simulation service client's tests imported the route test support,
and through it the app factory, only for two message helpers. Both test
modules now import them from tests/unit/v2/simulation_service_messages.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er, and that refusals give no status (failing)

A 500's or 503's message leaves out a code that isn't lower-case letters,
digits and underscores starting with a letter, at most 64 characters,
such as exception text or an empty code. A 400's or 422's message gives
the simulation service's code and reason as the household calculation
client does, without the service's status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fusals the code and reason

A 500's, 503's or other status's message now gives the simulation
service's code only when it is lower-case letters, digits and underscores,
starting with a letter, at most 64 characters, so a code holding exception
text or an empty code reads like a missing one: "(503)", never "(503 )".
Only the message is checked; a 400 or 422 with such a code still takes its
own path.

A readable 400's or 422's message is again "The simulation service cannot
run this request (code): reason" for the caller's error and "The
simulation service refused the report (code): reason" (or "simulation")
for the service's state, as the household calculation client writes its
refusals, without the simulation service's status inside the Public API's
422 or 503. The docstrings, the 422 and 503 OpenAPI descriptions and the
changelog fragment say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eadable-answer error covers

The 503 answer naming a created record gives the simulation service's
status and code, not its reason. The unavailable error says a readable
400's reason describes the service's state, not the caller's request.
SimulationServiceAnswerUnreadableError keeps its name but now says it also
covers readable answers that leave open whether the record exists: a 503
naming only another record and any status the Public API does not expect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.80625% with 45 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.95%. Comparing base (cd513da) to head (fd4f8d3).

Files with missing lines Patch % Lines
policyengine_api/fastapi_routes/dependencies.py 40.00% 12 Missing ⚠️
.../stored_reports/economy_results/transformations.py 92.00% 1 Missing and 11 partials ⚠️
...yengine_api/services/v2/stored_reports/services.py 93.61% 2 Missing and 1 partial ⚠️
..._api/services/v2/stored_reports/transformations.py 90.00% 1 Missing and 2 partials ⚠️
...ngine_api/services/v2/stored_reports/validators.py 89.65% 2 Missing and 1 partial ⚠️
...ine_api/services/v2/stored_simulations/services.py 91.17% 2 Missing and 1 partial ⚠️
...olicyengine_api/libs/simulation_service_answers.py 97.26% 0 Missing and 2 partials ⚠️
...ine_api/services/v2/simulation_service/services.py 91.66% 2 Missing ⚠️
...ngine_api/services/v2/user_simulations/services.py 90.00% 1 Missing and 1 partial ⚠️
...licyengine_api/fastapi_routes/v2/reports/routes.py 98.41% 0 Missing and 1 partial ⚠️
... and 2 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4281      +/-   ##
==========================================
+ Coverage   92.69%   92.95%   +0.26%     
==========================================
  Files         329      356      +27     
  Lines       18783    20097    +1314     
  Branches     3114     3222     +108     
==========================================
+ Hits        17410    18682    +1272     
- Misses        818      844      +26     
- Partials      555      571      +16     
Flag Coverage Δ
integration 55.77% <76.08%> (+2.00%) ⬆️
unit 90.42% <90.56%> (-0.04%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

anth-volk and others added 8 commits November 11, 2026 22:15
…ions-and-reports

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ey as not found (failing)

The saved-report and saved-simulation inserts and updates should turn
PostgreSQL's foreign-key refusal (SQLSTATE 23503) on the record's own key
into the not-found error, and raise any other integrity error unchanged.
The four not-found cases fail: the write raises the IntegrityError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The saved-report and saved-simulation services imported SQLAlchemy to
catch the foreign-key refusal of a write, which the rule
api.v2.user-services-no-sql forbids. The insert and update connectors
now turn SQLSTATE 23503 on the report's or simulation's own key into
the not-found error, and raise any other integrity error unchanged. The
services keep the check with a SELECT in the write's own transaction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The report and simulation create routes' error responses that name the
created record subclass V2ErrorResponse, so ruff must treat their
annotations as read at runtime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ulations, reports and simulations

The household calculation client and the report and simulation client
each kept their own copy of the answer and refusal models, the refusal's
reason, the status and code a message gives, the proxy statuses, the
transport-error sorting and the HTTP client's construction. They now
share policyengine_api.libs.simulation_service_answers, and both answer
every status as before.

The simulation service's error types now live once, in
services/v2/simulation_service/types.py, which both features import.
SimulationServiceRefusalError becomes SimulationServiceInvalidRequestError,
the household calculations' name for the same 422.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ointed is not found (failing)

On PostgreSQL, repointing a saved report or simulation to a record
deleted after the service's check raises InvalidRequestError: the update
connectors read the row's attributes after the failed flush, which
SQLAlchemy has expired.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…efore the flush

A failed flush expires the rows the session holds, so the update
connectors' reads of the row after it raised InvalidRequestError, which
answered 503, in place of the not-found error. The connectors now read
the report or simulation and the country before the flush, and the
inserts name the input's values. Also say in the shared invalid-request
error's docstring that a report's or simulation's message gives the
service's code, and in the shared answer module's that each client keeps
its own refusal messages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fused repoint keeps the saved simulation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Alembic's env.py calls logging.config.fileConfig, which disables every
logger that already exists. When an Alembic test runs before these two
tests in the same process, the report and simulation routes' logger is
disabled and caplog records nothing, so both tests failed in shard 5.
A fixture now re-enables that logger for each of the two tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #4283 (admin user profiles from Auth0). The one conflict, in
fastapi_routes/dependencies.py, keeps both imports: PackageVersions for the
report and simulation services and UserProfileSource for the user type
service.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in the release commit after #4283 (version, changelog and uv.lock).
No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Serve economy reports and simulations from the detailed-save tables at /v2/reports and /v2/simulations

1 participant