Repository navigation
Create household reports through POST /reports and calculate them in one household function call - #1057
Draft
anth-volk wants to merge 31 commits into
Draft
Create household reports through POST /reports and calculate them in one household function call#1057anth-volk wants to merge 31 commits into
anth-volk wants to merge 31 commits into
Conversation
A household function given a household report run claims the report run and each of its simulation runs under its call ID, records each run's calculated household or failure document in household_result, then marks the report run succeeded. The tests seed household report runs as the simulation service creates them, with or without a reform. They fail because HouseholdReportRunStore does not exist yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…orker role HouseholdReportRunStore extends the report run store with the writes a household function makes for a household report run: a check that the report run names exactly the call's simulation runs (a reform may be absent), each simulation run's completion or failure with its household document in household_result, and the report run's completion. Each applies only while the call holds its row running. The run tables' simulation_runs mapping gains household_result, and the condition that a call holds a row running becomes public so the new store shares it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ort run (failing) A household report run's calculation is its baseline's household function request and, optionally, its reform's, one household calculated the same way, each request's calculation ID naming its simulation run. A household function given the report run's ID claims the report run, calculates and records each side, and marks the report run succeeded or failed; a restarted call keeps a side it already recorded, and a pre-emption records nothing. The tests fail because the contract model, the recording code and the functions' household_report_run_id keyword do not exist yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… call The household contract gains HouseholdReportRunCalculation: a report run's baseline request and, optionally, its reform's, each request's calculation ID naming its simulation run, both calculating one household the same way. calculate_household_us and _uk take household_report_run_id. With it, the call claims the report run under its Modal call ID, checks the report run names the call's simulation runs, then claims, calculates and records each side: the result or failure document in household_result, a failure's one-line summary in error_message. Both sides are always calculated; the report run then succeeds, or fails with the first failed side's summary. A report run another call holds or that finished is not calculated, a restarted call keeps a side it already recorded, an unexpected error or cancellation fails every run left unfinished, and a pre-emption records nothing. A call naming both a household job and a report run is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The service reads a household report's catalog version, stored household and each side's policy in one transaction, refusing a missing row and a side's policy naming parameters the catalog version lacks on that side. It then creates, in one transaction, a household simulation and a pending simulation run for each side, the report (type household, its household and options) and its pending report run, with no reform rows for a report of the baseline alone. The tests fail because the household report records and store do not exist yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd runs HouseholdReportStore reads the country's catalog version and model, the stored household and each side's policy values in one transaction, raising ReportSideError for the side whose policy is missing or sets parameters the catalog version lacks. insert_household_report creates, in one transaction, a household simulation and a pending simulation run for each side (never reused), the report of type household with its options, and its pending report run, naming no reform for a report of the baseline alone. Recording the call and failing an unstarted run reuse the economy report run writes. references_checked and insert_simulation_with_run lose their underscores, as #1054 renames them, so the two branches agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he call (failing) POST /reports takes a household report, told apart from an economy one by report_type: a stored household, the baseline's policy and optionally the reform's, and add_missing_variables. The service reads the inputs on the default worker's catalog version, refuses what is missing or cannot be calculated before writing, creates the report and its runs, and starts one household function call for the report run, recording its call ID or failing the run and its simulation runs. The route answers the economy codes plus unknown_household, no_household_function and household_not_calculable, and locates refused fields without the type. The tests fail because the household report models, service, caller method and codes do not exist yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…old function call POST /reports takes a household report, told apart from an economy report by report_type, and refused fields are located without the type, as #1054 locates them for budget windows. HouseholdReportSubmissionService finds the default worker's household function, reads the catalog version, the stored household and each side's policy, builds each side's household contract request for the household's default year with its simulation run's ID, creates the report and its runs, then starts one household function call with the report run's ID. It records the call's ID, or fails the report run and its simulation runs when Modal does not confirm the call; a run the function claimed first is answered as it stands. Refusals use the economy answers: 400 adds unknown_household and no_household_function, 422 adds household_not_calculable for a household with no default year, one that cannot be read, or one the contract refuses. A foreign-key refusal of the household answers unknown_household. ModalHouseholdFunctionCaller gains start_report_run, which passes household_report_run_id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Public API sends a household report through the generated client's HouseholdReportRequest, with or without a reform, and reads the 201 as a HouseholdReportCreated with its year and sides, an economy report's 201 still as an EconomyReportCreated, and the household refusals with their codes. Run against a client generated from #1052's base, the tests fail at import: HouseholdReportCreated does not exist there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s writes The entrypoint README describes POST /reports for household reports: the request, what the service creates and starts, and its refusal codes. The persistence READMEs describe the household report records and store, and the Modal worker role's household report run writes. The changelog fragment describes the change for the release notes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (failing) A side whose record changed nothing, or one found failed with no recorded failure, fails the report run instead of letting it succeed. The household function hands the payload over as sent, and the recording code validates it only once the call holds the report run, so a payload the contract refuses, or a claim that raises, fails the report run at once instead of leaving it to the stuck-run sweep. The tests fail because the recording code takes an already validated calculation, outside its error handling, and finishes a report run on any side without a failure reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…refused payloads at once A report run now succeeds only when every side's simulation run did. A side whose record changed nothing, because another writer finished the run first, fails the report run with household_simulation_run_unavailable, and a side found failed with no recorded failure fails it with "no failure was recorded". The household function hands the payload to run_household_report as sent. The claim and the payload's validation now run inside the recording code's error handling, so a payload the contract refuses, or a claim that raises after it may have committed, fails the report run at once instead of leaving it, and its simulation runs, to the stuck-run sweep. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 1 found HouseholdReportSide naming a Literal of side names in the contract and a record of new simulation IDs in the persistence library. The persistence record becomes HouseholdReportSimulation, as the economy report's is ReportSimulation. The tests fail because the record still has the old name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e 422s point The persistence library's record of a household report side's new simulation and run is HouseholdReportSimulation, so HouseholdReportSide names only the contract's Literal of side names. The household_not_calculable code's comment and the entry README now say a contract refusal is located at the field it traces to: the household, a side's policy, or add_missing_variables. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…runs (failing) Once a household function holds a report run, it reads the report run's baseline and reform simulation runs from the database. A payload the contract refuses, or a report run naming other runs than the payload, then fails those runs with the report run, rather than leaving them pending for the stuck-run sweep; a report run whose runs can't be read is failed alone. The call answers a report run another writer finished with that run's status. The tests fail because the store has no read of a household report run's simulation runs and the call checks them only against the payload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…run's own runs Once the household function holds a report run, it reads the baseline and reform simulation runs the report run names (HouseholdReportRunStore.household_report_simulation_runs, replacing the check against the payload), then validates the payload and compares its runs with those. Any error after that read, a refused payload or a mismatch included, fails the report run and each of its own runs left unfinished at once; only a failed read leaves the runs to the sweep. Round 2's quality findings: a side's reason is empty only for a run that succeeded, so a report run's failure never falls back to a placeholder; a report run another writer finished is answered with that run's status; the migration doc describes household report runs, and the docstrings and README say a report run fails with its first unsucceeded side's reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(failing) A side's claim can raise after rolling back, leaving its run pending with no call. When a household report call stops, every one of the report run's runs it left unfinished is now to be failed while still active, whether or not this call holds it, as household simulation runs are never shared. The test of a raising claim fails because the side being calculated is failed only while this call holds it running. Two more tests cover a payload that adds a reform the report run lacks, or leaves out the one it names; they describe existing behaviour and pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ctive When a household report call stops, each of the report run's simulation runs it left unfinished is failed while still pending or running, whether the call holds it or the side's claim rolled back, instead of only while the call holds it running. Household simulation runs are never shared, so no other call works on them. Round 3's quality findings: run_household_report's docstring and the migration doc say an error in the report run's claim, or in the read of its runs, fails the report run alone; the v2 app's docstring and sizing comment describe a report run's call, its two calculations without axes within the same timeout; the migration doc's household section no longer says every call is one calculation; and the test module's docstring says a report run fails when any side's run didn't succeed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l fails Review round 4 found no bugs. Its quality findings: - the docs now say a claim error fails the report run only if the claim applied, and that the failure write for unfinished runs checks no call, relying on new simulation runs for each household report and on sides this call finished or found held by another call being out of the list; - the v2 app's docstring and sizing comment say a reform is optional, a validation call records nothing, and the simulation service builds the sides without axes; - a test checks that an error leaves alone a side this call recorded or found held by another call, and that a raising claim's runs are failed with no call. These describe existing behaviour and passed when first run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ated model HouseholdReportRunCalculation subclasses StrictHouseholdModel from another module, which ruff doesn't follow, so the guard test test_every_model_is_runtime_evaluated_for_ruff failed. The base joins runtime-evaluated-base-classes in ruff.toml, as StrictContractModel is listed, so ruff keeps the imports the model's annotations need at runtime. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…puts (failing) A restart of a household report run whose call never started (C1 · 19), or a re-run (C1 · 17), rebuilds the household function's payload from the stored report: its country, household, year, add_missing_variables and each side's simulation run and policy values. One public builder, household_report_calculation, is to serve creation and those, using the year it is given rather than the household's default year now. The tests fail because the builder exists only inside the service's _prepare. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
household_report_calculation builds the household function's payload from a household report's stored inputs: country, stored household, year, add_missing_variables, each side's simulation run and policy values (HouseholdReportSideInputs), and the worker. Creating a report passes the household's default year; a restart (C1 · 19) or re-run (C1 · 17) passes the stored report's year and option, so it calculates what was created. It refuses a household it can't read, or one the contract refuses, with household_not_calculable, as creation did. The service's _prepare now checks the default year and calls it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r (failing) Review round 5 found no bugs. Its quality findings for the builder: - it takes the baseline and an optional reform as their own parameters, so no call can leave out the baseline; - it takes the country from the stored household; - a side's values are policy_values, which also fit a baseline; - two sides recording one simulation run raise HouseholdReportBuildError, not a raw validation error; - the baseline-only test checks the baseline's policy values. The tests fail because the builder still takes a mapping of sides, a country and a side's reform. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d a reform household_report_calculation takes the baseline and an optional reform as parameters instead of a mapping that could leave out the baseline, which is now always checked first. It takes the country from the stored household. HouseholdReportSideInputs.reform becomes policy_values, which fits the baseline too. Two sides recording one simulation run raise HouseholdReportBuildError. The docstring says a restart reads the household and policy values as they are now, and that a refusal's locations are a report request's fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…indows #1054 (budget windows) merged into main, and also made the POST /reports body a union. The merge keeps both report types: - one ReportRequest of all three types (economy, budget_window, household) in a new report_api_models module, replacing the two-type unions each branch had, and report_request_errors importing it; - create_report dispatching household reports to _household_report_answer and the other two to a new _economy_report_answer, which keeps the router function under Ruff's complexity limit; - response_model listing the three answers, and the route's summary and description naming all three; - both packages' exports and the Stage 12 persistence README keeping both sides. #1054's tests that used "household" as an unknown report type now use "not_a_report_type", and the unknown-type message lists the three types. The OpenAPI tests check the three-type discriminator and 201 answers, and the household request's fields in a test of their own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…les (failing)
Decided by the user: a household report's only caller, v3's household
report page, needs every variable calculated, as the Public API's stored-household
route always gives it. So a household report has no add_missing_variables
option. The request refuses the field, the report stores no options
(reports.inputs is {}), and each side's household function request adds
missing variables. The payload builder takes no option.
/v2/household-calculations keeps its own switch for /calculate's callers.
The tests fail because the request still takes the option and stores it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decided by the user: a household report's only caller, v3's household
report page, needs every variable calculated, as the Public API's
stored-household route always gives it. HouseholdReportRequest no longer
takes add_missing_variables, so a body sending it is refused with 422
extra_forbidden. A household report stores no options (reports.inputs is
{}), and household_report_calculation, taking no option, gives every
side's request add_missing_variables=True, so a restart or re-run matches
what was created without reading anything more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ariables
The entry README says a household report has no options, adds every
variable the household lacks as the Public API's stored-household route
does, and stores {} in reports.inputs; its refusals are no longer located
at an option. The household_not_calculable code's comment matches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…is the service's fault (failing) Review round 6 found no bugs. One of its quality findings: a household report sets add_missing_variables itself, so a contract refusal of it should raise HouseholdReportBuildError, not a 422 located at a request field the report no longer takes. The shared mapping still traces it to the household calculation request's field of that name. The test fails because the refusal is answered as the caller's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he option
A contract refusal of add_missing_variables on a household report's side
raises HouseholdReportBuildError, as a refusal of any field the service
builds does, instead of a 422 located at a request field the report no
longer takes; the household calculation route's mapping is unchanged.
The module docstring says a household report has no options, and the
Modal-side store tests seed household reports with inputs {}, as the
service now creates them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been 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 #1055
Same-Repository Draft PR Check
PolicyEngine/policyengine-sim-api, not a personal fork.Changelog
changelog.d/c1-15-household-reports.added.md(added).Summary
The Simulation Entrypoint creates household reports and their household simulations in the Public API's run tables. One household function call per report run calculates them and writes each simulation run's
household_result. The Public API's/v2/reportswill create household reports through this route.POST /reportstakes household reportsreport_type: "household". Permissionsimulation:submit.country,label,description;household_id, a household the Public API stores;baseline: {policy_id};reform: {policy_id}, null or omitted for a report of the baseline alone.policy_idis null for current law. The report is for the household'sdefault_year. The request names no year, region, dataset, dynamic, .py version or record ID, and has no options. The household's saved SPM selection applies to both sides.report_request_errors.py. Adynamic_idon a side, or any unknown field, is refused with 422.What the service does (
household_report_submissions.py)Finds the household function: the v2 version manifest's default worker's household function for the country.
Reads the inputs in one transaction: the catalog version for that worker's .py version, the model, the household and each side's policy values.
Refuses, before writing anything, what is missing or can't be calculated:
unknown_catalog_version,unknown_household,unknown_policy(naming the side),no_household_function;missing_parameters, one reason per parameter atbody.<side>.policy_id.<name>;household_not_calculable: the household has no default year or can't be read, or the household contract refuses what would be calculated. Each refusal is located at the field it traces to.Creates the rows in one transaction:
type = 'household',household_id,year, the reform's policy,inputs = {});triggerinitial.No row has a Modal call ID. A foreign-key refusal on insert answers 400 (
unknown_householdfor the household).Starts one call of the household function, by name, with the new keyword
household_report_run_idand aHouseholdReportRunCalculation. That is each side'sHouseholdExecutionRequest, whosecalculation_idis the side's simulation run.Records or fails: it records the call's ID on the report run while it is pending with none. If Modal refuses the call or doesn't confirm it within 10 s, it fails the report run and its simulation runs and answers 503
modal_call_refusedwithreport_id; a run the function claimed meanwhile is answered as it stands.Builds the payload with one public builder,
household_report_calculation(household=, year=, baseline=, reform=, worker=). It builds the payload from a report's stored inputs, so a restart of a call that never started (C1 · 19) or a re-run (C1 · 17) uses the same code.Answers 201:
report_id,report_run_id,status,year, and each side's simulation and simulation run (the reform's null for a baseline-only report).Every other answer is an economy report's: 503
not_configured,manifest_unavailableordatabase_unavailablebefore any write, and 500internal_error.What the household function does (
stage12_runtime/household_report_recording.py)As the Modal worker role, under its own Modal call ID, the call:
claim_report_run;HouseholdExecutionResult, or theHouseholdExecutionFailureand a one-line summary, tohousehold_resultanderror_message;How it handles each other case:
HouseholdReportRunStore, aReportRunStore, inlibs/policyengine-stage12-persistence. Itssimulation_runsmapping gainshousehold_result./readyand the roles' grants are unchanged: the Modal worker role already updatessimulation_runs.household_resultand the report run's status columns.No
add_missing_variablesoptionBy the user's decision, household reports always add the household's missing variables and take no option for it. Their one caller, v3's household report page, needs every value.
/v2/household-calculationskeeps its own switch for the callers of/calculate, who get only the values they ask for.Builds on, and what merges first
Builds on Create economy reports and simulations through POST /reports and POST /simulations #1052 (tracker C1 · 9,
POST /reportsfor economy reports). This branch was built stacked on it; Create economy reports and simulations through POST /reports and POST /simulations #1052 merged intomainasfaa0f298, andmainis merged in here (e25c235d), so this PR targetsmainand its diff is its own changes.Calculate budget windows on the detailed-save path, five years at a time #1054 (C1 · 13, budget windows), also built on Create economy reports and simulations through POST /reports and POST /simulations #1052, also makes the
POST /reportsbody a union. Whichever of the two merges second:report_request_errors.py;ReportRequestthe three-type union;create_reportdispatch and itsresponse_model;"household"as an unknown type.The two helpers this PR makes public in
economy_run_records.py(references_checked,insert_simulation_with_run) are renamed exactly as Calculate budget windows on the detailed-save path, five years at a time #1054 renames them, so those hunks merge cleanly.Deployment order:
promote_entrypointdoesn't wait fordeploy_stage12_v2. A new entrypoint revision can therefore sendhousehold_report_run_idto a household function deployed before this change. That call fails, and the stuck-run sweep fails its report run. Create economy reports and simulations through POST /reports and POST /simulations #1052's run-ID calls to the coordinator have the same window, and nothing uses household reports until the Public API's C1 · 16.Environment variables and secrets
No new variable or secret.
MODAL_WORKER_DATABASE_URL(read by the household functions' new report run writes)policyengine-modal-worker-databasemodal.Secret.from_name(..., required_keys=["MODAL_WORKER_DATABASE_URL"]), attached withsecrets=[modal_worker_database_secret]tocalculate_household_usandcalculate_household_uk; read byhousehold_report_run_store()v2_app.py. Not verified here: that the secret exists in thestagingandmainModal environments, and that its role holds the grants there. B1 · 7 and B1 · 2 recorded both.SIMULATION_SERVICE_DATABASE_URLSIMULATION_SERVICE_DATABASE_URL_SECRET_NAME--set-secretson every deployMODAL_TOKEN_ID,MODAL_TOKEN_SECRETMODAL_TOKEN_ID_SECRET_NAME,MODAL_TOKEN_SECRET_SECRET_NAME--set-secretson every deploy/household-calculationscalls.SIMULATION_V2_MODAL_ENVIRONMENT,SIMULATION_V2_MANIFEST_NAMETests
Each change started as a failing test, committed with "(failing)" in its subject and seen failing for the expected reason, then the change that makes it pass. Run by node ID, only the tests this PR adds or changes:
libs/policyengine-stage12-persistence:test_household_report_run_store.py, 6 cases.masterc4419dcea, with its privileges applied and verified, connected as the Modal worker role.REPORT_RUN_TEST_DATABASE_URL, so they skip in CI, as the other report run store tests do.libs/policyengine-simulation-contract:test_household_report_execution.py, 9 cases.projects/policyengine-simulation-executor:test_stage12_household_reports.py, 22 cases, and 3 new cases intest_stage12_modal_app.py.libs/policyengine-simulation-persistence:test_household_report_store.py, 8 cases, against the same database acting as the simulation service's role.projects/policyengine-simulation-entry:test_household_report_submissions.py, 25 cases;test_household_report_routes.py, 16 cases;test_household_workers.py;test_openapi.py: the changedtest_economy_routes_declare_every_answer, the newtest_a_report_request_is_an_economy_or_a_household_report, andtest_normalized_contract_matches_old_gateway, whose schema list changed.projects/policyengine-apis-integ:test_household_report_generated_client.py, 6 cases, against the client regenerated from this branch. Against a client generated from Create economy reports and simulations through POST /reports and POST /simulations #1052's branch, it fails to importHouseholdReportCreated.Before each commit: CI's pinned Ruff (format and lint), the rules registry render, the quality guards,
uv lock --check, and Pyright for the changed packages. No other tests were run here; CI runs the rest.Review
Read-only adversarial rounds, in the review-code format:
Two bugs:
Both fixed test-first. Its quality findings fixed: the persistence record renamed
HouseholdReportSimulation, and thehousehold_not_calculablelocations documented.One bug: that second fix still left the report's simulation runs pending. Fixed: the call reads the report run's own runs once it holds it, and fails them on any later error. Quality findings fixed: the answer for a report run another writer finished, a side's reason always set when it didn't succeed, and the migration doc and docstrings.
One bug: a side whose claim raised and rolled back stayed pending. Fixed: unfinished runs are failed while still active, whoever holds them; household simulation runs are never shared. Documentation findings fixed.
The code round 3's fix added: no bugs. Its quality findings (docs, and tests of existing behaviour) are fixed, with no production code change.
The public payload builder, added after round 4 for C1 · 19: no bugs. Its quality findings are fixed test-first:
baseline/reformparameters;policy_valuesas the side field's name;HouseholdReportBuildError.Removing the
add_missing_variablesoption (the user's decision): no bugs. Its quality findings are fixed:{};Deferred quality findings: some duplication between
household_report_submissionsandhousehold_calculations(contract error locating, SPM fields), and no Postgres sweep test seeding a household report run.CI
This PR targets
main, so the fullpr.ymlrun applies. The first run, one25c235d, failed intest_every_model_is_runtime_evaluated_for_ruff: the new contract model's base wasn't inruff.toml'sruntime-evaluated-base-classes. That is fixed in9b14bf2c.🤖 Generated with Claude Code