Repository navigation
Conversation
GET /v2/reports/{report_id}?run_id= is to answer that run of the report,
whichever run is the newest: a report run of an economy or household
report, or a budget-window run of a budget window, and 404 for any other
run. Today the query refuses run_id (422).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /v2/reports/{report_id}?run_id= answers that run, its status,
versions and, once it has succeeded, its result: one of an economy or
household report's own report runs, or a budget window's budget-window
run with its years. A run that is not one of the report's answers 404.
Without run_id the report is read with its newest run, as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once the simulation service finds or creates runs by key, a create or re-run may end on a run that already exists, which needn't be the report's newest (a newer forced run, for example), and a re-run while a run is under way answers that run rather than 409. The report is to be read back with the run the answer names, and the re-run client is to read report_run_id (and a budget window's budget_window_run_id) from the 201. Today both answer the newest run, and the client returns nothing. Adds StartedReportRun, the type the client is to return; the existing re-run stand-ins answer the run they store. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The re-run client reads report_run_id, and a budget window's budget_window_run_id, from the simulation service's 201 and returns them (StartedReportRun); an answer naming no run is unreadable, as one naming another report is. A create or re-run reads the report back with the run the answer names, so a request that joins an existing run (once the simulation service finds runs by key) answers that run even when a newer one exists, and a re-run that joins a run under way answers it with 201. The route descriptions say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /v2/reports and POST /v2/reports/{report_id}/runs are to take the
user's credential optionally (a bearer token, the unauthenticated-user
header, or a server forwarding either), and record a row in
user_report_run_associations for the run the request ended on: trigger
initial for a create and manual for a re-run, requested_at when the
Public API received the request, and one record per user and run, so a
user asking again keeps their first. A request without a credential
records nothing; a browser credential that is malformed or names a
signed-in user is refused with 401 before the simulation service is
asked. The user's saved reports naming the report get last_run_at from
their newest request, and a record that can't be written answers 503
naming the report and its run.
Adds the record package's session container and an empty service, so
the tests fail on behaviour rather than imports.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /v2/reports and POST /v2/reports/{report_id}/runs take the user's
credential optionally (NativeRouteDependencies.optional_user: a bearer
token, the unauthenticated-user header, or a server forwarding either).
With one, the requester is checked before the simulation service is
asked (a browser credential naming a signed-in user answers 401), and
once the service has answered, the new report_run_records service
records the request in one transaction: the user is provisioned as any
user write provisions it, a user_report_run_associations row names the
run the request ended on (trigger initial or manual, requested_at when
the Public API received the request, ON CONFLICT (user_id,
report_run_id) DO NOTHING), and the user's saved reports naming the
report advance last_run_at to that time with GREATEST, so a concurrent
older request never sets it back. A record that can't be written
answers 503 with report_id and report_run_id. Without a credential
nothing is recorded. A budget window's request isn't recorded yet.
The reports routes module drops postponed annotations, since FastAPI
resolves the requester's dependency annotation at runtime.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a signed-out browser signs in to an account that already exists, the merge moves the browser's saved items and deletes its user, whose report-run records the foreign key's CASCADE deletes. They are to move into the account: a run the account already has keeps the account's record, and the account's saved reports take the time of a request that moved in when it is later. Today the browser's records are lost. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a signed-out browser signs in to an existing account, the merge now copies the browser's user_report_run_associations rows to the account before deleting the browser's user (whose own rows the CASCADE removes): each copy keeps its requested_at and trigger, and a run the account already has a record of keeps the account's. The account's saved reports of each report a copied record names take its requested_at as last_run_at when it is later. The runtime role's SELECT, INSERT and DELETE on the table cover it; no UPDATE grant. The copy reads the records and inserts them with IDs made in Python, and last_run_at advances under a WHERE condition rather than GREATEST, so the statements also run in SQLite, where the user service tests merge accounts. One insert connector serves a request's records and the copies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saving a detailed-save report through /v2/user-reports, or repointing a saved report to one, is to set last_run_at to the owner's newest requested_at among the report's runs (null without one), whatever the caller sends; a patch giving last_run_at or changing only other fields keeps the server's value. Today the caller's value is stored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saving a detailed-save report through /v2/user-reports, or repointing a saved report to one, sets last_run_at to the owner's newest requested_at among the report's runs (null without one), whatever the caller sends, and so does a patch that sends last_run_at; a saved report naming only a v1 report output keeps the caller's value, as v3's flagship reports send it. The route descriptions say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v2-integration-check.yml's report step also runs test_v2_report_reads_by_run.py and test_v2_user_report_runs.py, with the job's existing database settings, after the managed roles get their privileges; a workflow test requires both. Adds the changelog fragment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The route stand-ins take the requester and keep each one, and new route tests check that a signed-out browser's create and re-run name its user, that a request without a credential names none, that a malformed browser credential is refused before the service, and that the 401 and the 503 naming an unrecorded run are answered as the integration tests expect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A sign-in merge is to leave every saved report the account holds with the account's newest request for its report: a saved report moved in from the browser takes the account's own records, newer or not, even for a report the browser never ran. - A browser merged into an account while the simulation service is asked is not recorded again under a re-created user with its ID (503 naming the run); one converted in place meanwhile is recorded as itself (201). - The OpenAPI document is to say the two POST report routes' user credential is optional (an empty security requirement). - A signed-in user's request provisions and records that user (passes: coverage the round found missing). The stand-in and row helpers move to tests/integration/ user_report_run_support.py, so the new test modules share them; the stand-in can run work during the simulation call, and the client can act for a given principal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A sign-in merge now gives every saved report the account holds, of a report whose saved report moved in or whose record was copied, the account's newest request for that report as last_run_at (null without one), whatever it held before; read_saved_report_ids is read before the saved reports move. The updates go in report ID order. - The requester check now returns the user it found (CheckedRequester), and the record names that user, read FOR SHARE without inserting: a browser merged into an account while the simulation service is asked is answered 503 rather than recorded under a re-created user with its ID (RequesterUserGoneError), and one converted in place is recorded as itself. A principal with no user at the check is provisioned as before. - The two POST report routes' OpenAPI security lists an empty requirement, so the user credential reads as optional. - A record that fails is logged with its cause; both POST routes document the 403 for a forwarded machine-client token; the SQLite merge test's docstring says only what it checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… into v2plan/c1-28-user-report-runs
C1 · 24's answer 8: a window request records each year's report run for the user, with one requested_at: a create the window run it created, a re-run the window run it joined, not another. The 503 for an unrecorded request names the run to read as run_id, which for a window is its budget-window run. The stand-in creates and joins windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C1 · 24's answer 8: a budget window's create or re-run records, for its user, the report run of each year of the budget-window run it ended on, with one requested_at and trigger. ReportRunRequest names either the report run or the budget-window run, and the record service reads the window run's years in its own transaction, so a failed read is a failed record. A re-run decides by the stored report's kind; a window's answer naming no window run is answered 502, as one that can't be read back. The 503 for an unrecorded request names the run to read as run_id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C1 · 24's answer on force: POST /v2/reports/{report_id}/runs takes
force from an admin user, sends it and records the request, and refuses
it before the simulation service is asked from anyone else: 401 without
a user credential, 403 for a signed-out browser, a standard user or an
author. Today anyone may force.
The tests that forced as a browser or without a credential now force as
an admin (store_signed_in_user), or re-run unforced where one new run
is enough.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C1 · 24's answer on force: POST /v2/reports/{report_id}/runs refuses
force: true, after reading the report and before the simulation service
is asked, from anyone but an admin user: 401 with the bearer challenge
without a user credential (ForcedRerunRequiresSignInError), 403 for a
signed-out browser, a signed-in user not stored yet, or a standard or
author user (ForcedRerunRequiresAdminError). The requester check now
carries the user's stored type. Unforced re-runs need no credential, as
before. The route's description and its 401 and 403 answers say so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C1 · 24's answers 7 and 6. A read without run_id is to answer the requesting user's newest recorded run of the report (for a window, the window run of its newest recorded year), else the newest succeeded run that wasn't forced, else the newest unforced run; a forced run is never the default, so a report with only forced runs reads as not run. Lists read the same way. A report's label and description are to be the requesting user's saved report's, null for anyone else or a read naming no user; a malformed browser credential on a read is refused. Five existing read tests now expect that default: an older succeeded run, or the unforced one, is read before a newer pending, running or forced run, ties between unsucceeded runs still go to the larger ID, and a report read without a user has no label. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C1 · 24's answers 7 and 6.
- A read without run_id answers the requesting user's newest recorded
run of the report (a window: the window run of its newest recorded
year), else the report's default run: its newest succeeded run that
wasn't forced (a window's: not recalculated), else its newest run that
wasn't forced; a forced run is never the default. Lists read the same
way. read_report_rows ranks the default; reads_requests reads the
user's newest requests, their window runs and saved-report labels.
- A report's label and description are the requesting user's saved
report's name and description (the one saved last), null otherwise;
ReportDefinitionRead.label and the report items' label are nullable,
and reports.label is no longer read.
- GET /v2/reports/{report_id} and GET /v2/reports take the optional user
credential (401 for a malformed or signed-in browser credential); the
OpenAPI document marks it optional.
- A create or re-run reads its answer back as the user it recorded the
request for, so a browser converted in place meanwhile still reads.
- StoredReportRows.newest_run and newest_budget_window_run become run
and budget_window_run, since they hold the run a read answers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… into v2plan/c1-28-user-report-runs # Conflicts: # policyengine_api/fastapi_routes/v2/reports/routes.py
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A create or re-run whose report can't be read back is to name the run in its 502 (run_id, and in the message's read URL), since a read without run_id may now answer another run. - CI's integration step is to run the four modules added since round 1, after the managed roles get their privileges. - Coverage the round found missing (passes): the newest request wins whichever run was created last; a recalculated window run is never the default; of two saved reports of one report, the one saved last names it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The 502 after a create or re-run whose report can't be read back names the run the answer named (run_id, and in the read URL of its message), null for a window answer naming no window run; a read without run_id may answer another run. - CI's integration step runs the four modules added since round 1, after the managed roles get their privileges. - The household read test expects no label for a read naming no user; C1 · 18's 502 test expects the run. - The report items' and run_id's descriptions say which run a read answers rather than "the newest"; the GET routes document the 403 for a forwarded machine-client token; the service's docstrings follow. - The read view takes the requester and the recorded user as two parameters rather than one of two types. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Two existing tests expected the 502 without run_id: the OpenAPI test of the created records' IDs now lists each answer's ID fields, and the create integration test expects the run in the body and the read URL. - The 502 descriptions, the create route's description, the route helpers' docstrings and the changelog say the answer names run_id. - "Newest run" wording left in the stored-report types, transformations and run reads now says which run a read answers. - Tests: the re-run 502 for a window answer naming no window run answers a null run_id; the OpenAPI document gives that nullable run_id and the reads' 403; the window create 502 names its run; the request-order test asks for a run that is neither the default nor the newest. Not changed: the workflow test reads the workflow as text, as the tests beside it do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A budget window's re-run whose answer names no window run answers a
502 with a null run_id that points to the report's run list (GET
/v2/reports/{report_id}/runs), since run_id=null can't be read, and
records nothing; today it points to a read of the default run.
- The create 502 test compares run_id with the run the stand-in answered.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A budget window's re-run whose answer names no window run answers its
502 with a null run_id and a message pointing to GET
/v2/reports/{report_id}/runs, which lists the run, rather than to a
read of the report's default run; the 502's description and the
service protocol say so.
- Hand-wrapped fragments in the route helpers' docstrings and the create
route's description are rewrapped.
Not changed: test names that say "newest run" store one run or use a
stand-in, so they still describe what they check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ailing) C1 · 24's answer 6, as the user chose: POST /v2/reports stores label and description nowhere. Both become optional (omitted or null), and the simulation service's request bodies (economy, household, budget window) carry neither, since after C1 · 27 sim accepts and ignores them; a report's name and description come from the user's saved report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C1 · 24's answer 6, as the user chose: POST /v2/reports stores label and description nowhere. Both are optional (omitted or null) for economy reports, budget windows and household reports, and a value that is sent is still checked; the simulation service's request bodies carry neither, since after C1 · 27 sim accepts and ignores them. A report's name and description are each user's saved report's. This needs C1 · 27 deployed first, since sim's main still requires label. Four existing tests that required a label now refuse an empty one instead, and the household request schema no longer requires it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The re-run 502 for a window answer naming no window run is to say so, not that the report couldn't be read back. - The create request schemas are to say label and description are stored nowhere. - A create without a label is accepted for household reports and budget windows too (passes: coverage the round found missing). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A budget window's re-run whose answer named no window run says so in its 502 and its error, rather than that the report couldn't be read back, since no read was tried. - The create request schemas describe label and description as optional and stored nowhere (the household report's in its docstring too). - Hand-wrapped fragments in the create helper's docstring and the re-run route's description are rewrapped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The re-run 502's OpenAPI description and the RerunReportUnreadable response's and error's docstrings describe the null-run_id case as a window answer naming no window run, with no cause, not a failed read. - The create fields' description is a constant named for what it is, and says a report's label and description are the name and description of each user's saved report. - The re-run route's description is rewrapped. Documentation only; no further review round. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## v2plan/c1-25-shared-report-keys #4313 +/- ##
===================================================================
- Coverage 93.45% 93.41% -0.04%
===================================================================
Files 369 377 +8
Lines 21478 21823 +345
Branches 3401 3455 +54
===================================================================
+ Hits 20072 20387 +315
- Misses 823 842 +19
- Partials 583 594 +11
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4312
Summary
Records each user's report runs in
/v2/reports, reads a report by run and as each user sees it, and takes names only from saved reports. Stacked on #4303 (itsuser_report_run_associationstable and per-user column changes); retargets tomasteronce #4303 merges.Who asks.
POST /v2/reports,POST /v2/reports/{report_id}/runs,GET /v2/reports/{report_id}andGET /v2/reportstake an optional user credential: a bearer access token, thePolicyEngine-Unauthenticated-Userheader, or a server forwarding either (NativeRouteDependencies.optional_user). Without one the request acts as nobody; a credential that is sent but not valid is refused as on the user routes (401; 403 for a forwarded machine-client token). The OpenAPI document marks the credential optional.Records (new
services/v2/report_run_records):user_report_run_associationsrow for the run the answer names, created or joined: triggerinitialormanual,requested_atwhen the Public API received the request,ON CONFLICT (user_id, report_run_id) DO NOTHING, so a user asking again for a run keeps their first record. A budget window's request records each year's report run of its window run.FOR SHAREwithout inserting: a browser merged into an account while the simulation service was asked is answered 503 rather than recorded under a re-created user, and one converted in place is recorded as itself.report_idandrun_id.last_run_atto the request's time (never backwards). Saving or repointing a saved detailed-save report through/v2/user-reportssetslast_run_atfrom the owner's records, whatever the caller sends; a saved report naming only a v1 report output keeps the caller's value.last_run_atfrom the account's records. The runtime role's existingSELECT,INSERTandDELETEgrants onuser_report_run_associationscover it; noUPDATE.Which run is answered.
StartedReportRunfor re-runs), which needn't be the newest once sim shares runs; their 502 when it can't be read back namesrun_id.GET /v2/reports/{report_id}?run_id=reads that run (a report run, or a budget window's budget-window run); any other run answers 404.run_id, a read or list answers the requesting user's newest recorded run, else the newest succeeded run that wasn't forced, else the newest run that wasn't forced (a window's: not recalculated). A forced run is never the default.Names. A report's
labelanddescriptionare the requesting user's saved report's (the one saved last), null otherwise;reports.labelanddescriptionare no longer read.POST /v2/reportstakeslabelanddescriptionas optional and sends neither to the simulation service.Forced re-runs are for admin users only: 401 without a credential, 403 below admin, before the simulation service is asked.
The decisions it follows
C1 · 24's answers, recorded in the shared-reports design:
labelanddescriptionare stored nowhere.GET /v2/reports/{report_id}/runs, never in its place; a repeated request keeps the firstrequested_at.user_simulation_run_associations; nothing here writes one.force: admin users only.Deploying, with the simulation service's find-or-create (C1 · 27)
mainstill requireslabel, which this no longer sends, so this must not deploy before C1 · 27.report_run_id(and a window'sbudget_window_run_id) as the run the request ended on, joined or created, always 201; the 409run_in_progressno longer occurs. Built against those shapes with stand-ins for the simulation service, since C1 · 27 isn't merged yet.trigger(now nullable in Share detailed-save reports and runs by key, and record each user's report runs #4303) andreport_runs' cliff columns. A later task drops them with the per-user columns ofreports, after this deploys.Testing
alembic -c alembic-v2.ini checkclean (no revision here).v2-integration-check.yml's report step runs the six new integration modules after the managed roles get their privileges.lint-imports,python -m scripts.rules validate,python scripts/run_quality_guards.py, ruff, ruff format and mypy pass. Nothing underpolicyengine_api/data/v2/changes.V2_MIGRATION_DATABASE_URL,V2_ALEMBIC_DISPOSABLE_TESTandDEPLOYMENT_ENVIRONMENT, literal values in the job's ownenv:for its own PostgreSQL service container.🤖 Generated with Claude Code