Skip to content

Refactor the application package within ruff's complexity limits - #3933

Draft
anth-volk wants to merge 42 commits into
ruff-complexity-economyfrom
ruff-complexity-application
Draft

anth-volk wants to merge 42 commits into
ruff-complexity-economyfrom
ruff-complexity-application

Conversation

@anth-volk

@anth-volk anth-volk commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #3930

Summary

Phase 1 step 1.8, group 8b, part 2 of 3: the rest of policyengine_api/, refactored within ruff's complexity limits. Stacked on #3932.

  • All 89 findings in scope are fixed by refactoring, with no suppressions. This PR adds no inline suppression of its own; see "Inline suppressions". Before each refactor, a commit adds characterization tests that pin current behaviour; 249 in all. A new tests/fixtures/recording_session.py records every statement, compiled for PostgreSQL, and every ORM write, in order.
  • ASGI shell (asgi_factory, complexity 34 and 113 statements):
    • Lifespan, exception handlers and router wiring move to module level.
    • Per-request work moves into named steps: request context, telemetry, the route call and the response.
    • A new test pins the exact order of runtime and logger calls.
  • Flask routes:
    • Household route errors keep the old except precedence. get_household_under_policy and _calculate each catch the error types they expect in one clause, which a helper maps to the same 404 and 400 responses. A following except Exception returns the SPM error response when the error or its cause is an SPM error, and otherwise calls module_logger.exception() itself and returns the 500 response.
    • Saved-policy writes share configuration and failure steps. They copy committed events to v2 through _copy_saved_policy_events_to_v2, which Make dual-write log failed v2 copies instead of returning HTTP 503 #3959 added.
    • Simulation identity checks move into their own step.
    • View signatures are unchanged.
  • v2 services and routes:
    • Household and policy failures map through ordered error tables (run_v2_operation).
    • Household document validation and v1 household translation are split into collection, record and membership steps.
    • ComparisonReportRecord.validate_state keeps its Pydantic signature, with its checks split into helpers.
    • The parameter-value listing route takes one frozen ParameterValueListingQuery dependency; its OpenAPI is byte-identical.
  • Data layer:
    • Catalog extraction splits into verification, selection and normalization steps, called in the same order. _normalize_region takes a private _SourceRegion protocol that declares the two region attributes it reads directly, code and region_type; it reads the rest through getattr with a default, as before.
    • Staging row generators move to module level.
    • Legacy user-policy persistence takes one revision value; a new test pins every statement and write in order.
    • Report and simulation runs take NewReportOutputRun, NewSimulationRun and ReportRunPackageVersions.
  • Migration logging and flags:
    • Route groups come from a path table.
    • A served request is logged from one ServedRequest value.
    • Explicit database sources are one value.
  • Mirroring and other services:
    • Mirror outcome loggers take one outcome value, and saved-policy mirror collaborators are grouped.
    • GatewayAuthTokenProvider takes GatewayAuthCredentials.
    • Two dead pieces are removed: policy_service._create_new_policy, which had no callers, and an always-true lock flag.
  • Magic numbers (25): each becomes a named constant or an HTTPStatus member, with messages unchanged.
  • Reconciled with Update PolicyEngine bundle to 6.3.4 #3918 (merged while this was in progress):

Inline suppressions

This PR adds no inline suppression of its own. It removes 2 and moves 3 existing ones:

  • Removed: the FBT001, FBT002 on the FastAPI current query parameter in fastapi_routes/v2/metadata/parameter_routes.py, which the ParameterValueListingQuery dependency replaces.
  • Two merged into one:
    • The BLE001 in fastapi_routes/v2/households/routes.py and the one in fastapi_routes/v2/policies/routes.py become one in the shared fastapi_routes/v2/errors.py, with the same reason (return a secret-safe typed error).
  • Moved within their files, unchanged: the ANN401 on asgi_factory's wsgi_app parameter and the BLE001 around telemetry in migration_logging.py.

2026-10-20 update

Three commits on top of the branch:

  1. 4315457e7 Catch expected household calculation errors by type (BLE001): the split had folded the household routes' specific except clauses into one except Exception, with a BLE001 suppression in each of the two handlers. The handlers now catch the expected types first and log the unexpected failures directly, as described under "Flask routes". Responses, status codes and log messages are unchanged, and the complexity rules still pass.
  2. 18c8b0c4c Type the catalog region parameter with a private protocol (ANN401): _normalize_region(source_region: Any) and its ANN401 suppression are replaced by the _SourceRegion protocol.
  3. ccf4298fe Replace the policy mirror where it is defined in the user-policy persistence test: Enable ruff structure and import rules (INP, PLC, SLF, N, A, FAST, E) #3920 made mirror_legacy_user_policy_in_session import mirror_legacy_policy_in_session when it runs, because the policies services module is a banned module-level import. The new characterization test still replaced the function as an attribute of the user-policies services module, so all 13 of its tests errored at setup. Its fixture now replaces the function in the policies services module.

Lines containing # noqa in Python files (git grep -c '# noqa' -- '*.py', summed):

Parent (#3932) This PR
Before 2026-10-20 126 126 (3 added: 2 BLE001, 1 ANN401)
Now (after #3959) 98 96

2026-10-20: rebased onto #3959

#3959 makes dual-write log a failed v2 copy instead of returning 503. It moved the saved-policy copy into its own helper, _copy_saved_policy_events_to_v2, which logs instead of returning a response. Two of this PR's commits conflicted with it:

Checks at this tip (ccf4298fe): pytest tests/unit gave 3,130 passed, 0 failed. Ruff, the quality guards and the rules guard are clean, and rendered rules are fresh.

Testing

2026-10-20 update. At this tip (4fab246dc), on Python 3.12 with the uv.lock dependencies (uv sync --frozen --extra dev --python 3.12):

  • pytest tests/unit: 3,120 passed. Before the test fix in 4fab246dc, the same run gave 3,107 passed and the 13 setup errors described above; they also occur at the previous tip, 1ff10c1ba.
  • The household route characterization tests (tests/unit/routes/test_household_route_error_responses.py, 15) and the catalog extraction tests pass.
  • ruff format --check . and ruff check . (ruff 0.15.12) are clean, including C901, PLR0911, PLR0912 and PLR0915 on routes/household_routes.py.
  • python scripts/run_quality_guards.py and python -m scripts.rules guard --base-ref origin/ruff-complexity-economy pass, and python -m scripts.rules render leaves no diff.
  • mypy (the configured files): no issues. mypy on routes/household_routes.py and data/v2/catalog/extraction.py reports the same messages as before these commits.

Earlier notes:

  • The make test set at this branch's tip: 2,848 passed, which is the base plus part 1's 166 and this PR's 249.
  • Behaviour comparisons against the base:
    • Parameter-value queries are identical.
    • 4,800 household translation and normalization outcomes are identical.
    • 63 catalog extraction mutations are identical.
  • make typecheck-v2 is clean. mypy on the 28 changed application files outside its file list gives identical output to the base.
  • ruff check, ruff format --check and the quality guards (migration-contracts, rules-registry, ci-scripts) pass.
  • The FastAPI OpenAPI, the /v2/openapi.json preview, the legacy spec and every existing model and adapter schema are byte-identical, with and without OLD_SIMULATION_GATEWAY_URL set.
  • Not run locally: tests/integration/test_v1_user_policy_dual_write.py needs MySQL and PostgreSQL; its 6 call sites were updated.
  • CI: above the bottom of its stack, so only CI complete runs until the PRs below merge.

🤖 Generated with Claude Code

@anth-volk
anth-volk added this pull request to stack #3917 October 14, 2026 18:47
@anth-volk anth-volk changed the title ruff complexity application Refactor the application package within ruff's complexity limits Oct 14, 2026
@anth-volk
anth-volk force-pushed the ruff-complexity-application branch 2 times, most recently from 984feb4 to e599f21 Compare October 14, 2026 20:04
@anth-volk
anth-volk force-pushed the ruff-complexity-application branch 6 times, most recently from 44ad18f to 30cb394 Compare October 15, 2026 00:06
@anth-volk
anth-volk force-pushed the ruff-complexity-application branch from 30cb394 to 1cf7ff3 Compare October 15, 2026 15:21
@anth-volk
anth-volk force-pushed the ruff-complexity-application branch 2 times, most recently from 1a1e093 to a92ce80 Compare October 15, 2026 17:20
@anth-volk
anth-volk force-pushed the ruff-complexity-application branch from a92ce80 to 86ca615 Compare October 16, 2026 05:40
@anth-volk
anth-volk force-pushed the ruff-complexity-application branch from 86ca615 to f9166fb Compare October 16, 2026 06:00
@anth-volk
anth-volk force-pushed the ruff-complexity-application branch 3 times, most recently from fab7882 to 4a2abbc Compare October 16, 2026 06:59
@anth-volk
anth-volk force-pushed the ruff-complexity-application branch from 4a2abbc to 103c752 Compare October 16, 2026 07:05
anth-volk and others added 29 commits October 21, 2026 02:05
…mbership steps

normalize_household_document now checks the supported collections, the
saved SPM selection, each collection's records (identity, then contents)
and the people's memberships in separate helpers, in the same order and
with the same messages and key order. The two require_json_value
validators check a container's members in their own helper.

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

translate_legacy_household now checks the source collections, translates
each collection's groups (their values and member memberships) and the
people in separate helpers that share a _LegacyTranslation holding the
person ids, memberships and values read so far. Groups and people are read
in the same order and raise the same errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
create_legacy_household_mapping takes the LegacyHouseholdSnapshot, which
supplies the v1 country, household id and API version, instead of those
three values as separate arguments. The insert statement is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
read_user_household_page and read_user_household_rows take a
UserHouseholdListing (country, user, optional household, offset and limit)
instead of five keyword arguments; V2UserHouseholdService.list_user_households
keeps its signature and builds the listing. The query is unchanged.

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

LegacyUserPolicyRevision carries a v1 revision's country, saved-policy id,
reform label, fingerprint, source revision and changed fields through
persist_legacy_user_policy_mapping, apply_existing_legacy_user_policy_mapping,
validate_existing_legacy_user_policy_mapping and the mapping insert and
update connectors, which also take the projected association instead of its
user and policy ids. UserPolicyListing describes a requested page of
associations for read_user_policy_page and read_user_policy_rows;
list_user_policies keeps its signature. The statements, writes and their
order are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
create_report_run takes a ReportRunPackageVersions (country package and
PolicyEngine.py versions) instead of two separate arguments; it validates
them in the same order and stores the same run.

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

normalize_json_value now dispatches through an ordered table of type
normalizers (float, enum, date, mapping, sequence) and unwraps scalars in
its own helper. _normalize_country checks the model identity, release
manifest, default dataset and region registry, normalizes parameter nodes,
selects each region's dataset and normalizes each region in separate
helpers, called in the same order so the same error is raised first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_catalog_rows now maps each staging table to a module-level generator
(model, variables, parameter nodes, parameters, parameter values, datasets
and regions) instead of seven nested closures. Each generator stays lazy
and yields the same rows.

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

_log_copy_operation and _log_mirror_event take the outcome, write sources,
destination result and failure category as one frozen outcome value
instead of five or six keyword arguments. The logged records are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mirror_pending_user_policy_events_after_commit takes its event service,
reform snapshot loader and mirror factory as one optional
UserPolicyMirrorCollaborators value; any collaborator left None still
takes its default. _log_mirror_operation takes the mirrored revision and
the mirror outcome as two values instead of nine keyword arguments. The
policy routes and the tests pass the new values; the logged records are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
create_simulation_run and create_report_output_run take an optional
NewSimulationRun or NewReportOutputRun holding the run's status, trigger,
output, error, source run, spec snapshot, version manifest and fixed ID,
with the same defaults, instead of up to ten keyword arguments. The report
output service and the tests build the new values; the stored rows are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build_report_spec takes an optional EconomyReportSettings instead of three
keyword arguments, with the same defaults; the spec check builds it from
the stored economy spec.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The simulation lookup was only ever called with for_update=True, so it is
renamed _lock_existing_simulation and always locks. PolicyService's
_create_new_policy had no callers and is removed.

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

_decode_mirror_event now validates the changed fields and the event type
against them in two module helpers, in the same order and with the same
messages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GatewayAuthTokenProvider takes an optional GatewayAuthCredentials (issuer,
audience, client ID, client secret and secret resource) instead of five
positional arguments; any setting left None is still read from its
environment variable. The tests build the credentials.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
find_unrecognized_household_inputs hands each known entity group to
_find_unrecognized_entity_inputs, which reports each variable through
_unrecognized_variable; findings keep their order. get_safe_json writes
infinities through _safe_json_number.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Calling app.exception_handler(...) on the module-level handlers, as the
nested decorators did, keeps their types acceptable to mypy; the handlers
are registered the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cover the paths the refactor touched that no test reached: the 128
character catalog version limit, the one-date-range legacy period rule,
the metadata page bounds, the five-digit place FIPS code, enum values that
are also tuples or dates, and invalid migration flags in the log context.
The new limit and enum tests pass on the pre-refactor code too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#3918 added _serialize_region_filter_value, which compares against the
literal 255 (PLR2004). Name it MAX_REGION_FILTER_VALUE_LENGTH, the size of
the catalog's filter_value column; the error message text is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Management API client compares the token response with HTTPStatus.OK
and retries a deletion after HTTPStatus.UNAUTHORIZED, as the gateway
token provider does. HTTPStatus is an IntEnum, so the comparisons are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
user_resource_operation maps each typed service failure #3891 added to
a status, message and headers, and logs persistence and unexpected
failures by event name only. The new characterization test pins all
eighteen cases, including subclasses that must match their base's
clause and an IntegrityError that must win over the SQLAlchemyError
clause, and passes on the current code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
user_resource_operation now runs the service call and hands any failure
to _user_failure_response, which answers with the first
UserResourceFailure in USER_RESOURCE_FAILURES that the error matches:
its status, the error's own or a fixed message, the 401's
WWW-Authenticate challenge, and a logged event for persistence
failures. The entries follow the order of the old except clauses, and
an unmatched failure is still logged and answered with the generic 500,
so each failure gets the same response and log record; the pinned
failure-mapping test passes unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user-report and user-simulation create routes call a module helper
that checks the body and query countries agree and runs the creation,
as the user-household and user-policy routes do. The /v2/me router
builds its signed-in-only dependency through _signed_in_user_dependency
and runs the upgrade through _upgrade_account. The three builders #3891
added stay within the complexity limit (C901); route registration,
order, signatures and the OpenAPI documents are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#3891 added include_compute to get_migration_context, giving it six
arguments (PLR0913). Its db_entity and sim_flow overrides were never
passed: every caller, including the request logging and the tests,
lets the route group's configuration name them. The overrides go, so
the context takes the entity and flow from the route group and, with
include_compute=False, records no flow; every logged context is
unchanged.

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

The PLR0913 findings in the code #3891 added:

- The user-household and user-policy services, their route Protocols and
  routes take an optional UserHouseholdFilter or UserPolicyFilter (the
  v2 and v1 ids to narrow a listing to) instead of two keyword
  arguments, as the metadata layers take a ParameterValueFilter. The
  routes build the filter from the same query fields.
- read_user_report_rows and read_user_simulation_rows take a
  UserReportListing or UserSimulationListing, as the household and
  policy row readers take their listings.
- create_user_identity takes the UserPrincipal whose issuer, subject and
  email it stores, instead of the three fields.

The statements, their parameters and the OpenAPI documents are
unchanged. The route and service tests, and the concurrent sign-in
test's stand-in, use the new arguments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
get_household_under_policy and _calculate each caught every exception and
passed it to one mapping helper. Each now has two except clauses:

- one for the error types the route expects (household or policy not
  found and unrecognized inputs for a stored household; unrecognized inputs
  and situation parsing errors for a request), which the helper maps to the
  same 404 and 400 responses as before;
- then except Exception, which returns the SPM error response when the
  error or its cause is an SPM error, and otherwise calls
  module_logger.exception() itself and returns the 500 response, as the
  code did before the complexity refactor.

Ruff accepts a broad except whose handler logs the traceback, so this
removes both BLE001 suppressions. Responses, status codes, log messages and
their order are unchanged, and the complexity rules still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_normalize_region took its PolicyEngine.py region as Any. It now takes a
private _SourceRegion protocol that declares the two attributes it reads
directly, code and region_type, as read-only properties; it reads every
other attribute through getattr with a default, as before. This removes the
ANN401 suppression without changing behaviour.

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

mirror_legacy_user_policy_in_session imports mirror_legacy_policy_in_session
from the policies services module when it runs, because #3920 made that
module a banned module-level import. The test's autouse fixture replaced the
function as an attribute of the user-policies services module, which no
longer has one, so all 13 tests errored at setup. The fixture now replaces
it in the policies services module, which the deferred import reads at call
time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk
anth-volk force-pushed the ruff-complexity-application branch from 5f74b2a to 7030d9c Compare October 20, 2026 22:05

This branch was successfully deployed

No deployments
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.

Refactor the application package within ruff's complexity limits

1 participant