Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a884332
Pin application behaviour before the complexity refactors
anth-volk Oct 14, 2026
4a613e8
Name the magic numbers compared in data, client and v2 validation code
anth-volk Oct 14, 2026
de2d237
Look up route groups by path and pass explicit database sources as on…
anth-volk Oct 14, 2026
2b5624f
Split Flask migration logging into steps and log one ServedRequest re…
anth-volk Oct 14, 2026
f70b309
Split the ASGI shell's request handling into context, telemetry and r…
anth-volk Oct 14, 2026
189b1de
Pass a failed household cell to the calculation failure recorder as o…
anth-volk Oct 14, 2026
856aa0a
Split household route error handling into parse, calculate and map steps
anth-volk Oct 14, 2026
686d343
Share the saved-policy write configuration, mirror and failure steps
anth-volk Oct 14, 2026
74486e5
Validate the simulation identity fields in their own step
anth-volk Oct 14, 2026
aa29e6d
Map v2 household and policy failures through ordered error tables
anth-volk Oct 14, 2026
404c1d5
Move the association create checks out of the v2 router builders
anth-volk Oct 14, 2026
9098c68
Pass parameter-value filters as one value through the metadata layers
anth-volk Oct 14, 2026
640a1af
Split the comparison report state validator into lifecycle and compar…
anth-volk Oct 14, 2026
ffb37ce
Split v2 household document validation into collection, record and me…
anth-volk Oct 14, 2026
af69e95
Split v1 household translation into collection, group, membership and…
anth-volk Oct 14, 2026
ec4f21e
Read the legacy household mapping's v1 identity from its snapshot
anth-volk Oct 14, 2026
80b7b00
Describe a requested page of user-household associations as one value
anth-volk Oct 14, 2026
c37d369
Pass a v1 saved-policy revision and a requested association page as o…
anth-volk Oct 14, 2026
4f75442
Pass a report run's package versions as one value
anth-volk Oct 14, 2026
e417069
Split catalog extraction into verification, selection and normalizati…
anth-volk Oct 14, 2026
357cb38
Move the catalog staging row generators to module level
anth-volk Oct 14, 2026
960573c
Pass a household or policy mirror's outcome to its metric logger as o…
anth-volk Oct 14, 2026
d913144
Group saved-policy mirror collaborators and metric record inputs
anth-volk Oct 14, 2026
308383d
Describe a new simulation or report-output run as one value
anth-volk Oct 14, 2026
f0e8bdf
Pass an economy report's dataset, target and options as one value
anth-volk Oct 14, 2026
f8dfc3a
Drop an always-set lock flag and an unused policy helper
anth-volk Oct 14, 2026
adf837b
Check a saved-policy mirror event's changed fields and type in their …
anth-volk Oct 14, 2026
b625a07
Pass the gateway token provider's Auth0 settings as one value
anth-volk Oct 14, 2026
f640e0c
Check household entity inputs and safe JSON numbers in their own helpers
anth-volk Oct 14, 2026
da875f2
Register the ASGI exception handlers through FastAPI's decorator
anth-volk Oct 14, 2026
8087ca6
Pin the named limits, enum normalization and log-context errors
anth-volk Oct 14, 2026
c41fc79
Add a changelog fragment for the application complexity refactors
anth-volk Oct 14, 2026
a92ce80
Name the region filter value's length limit from #3918
anth-volk Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/ruff-complexity-application.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Refactored the rest of the application under ruff's complexity rules (C901, PLR): the ASGI shell, Flask migration logging, household and policy routes, v1 and v2 household translation and validation, saved-policy mirrors, catalog extraction and the metadata layers are split into named steps, related arguments travel as small value objects, and magic numbers are named, with characterization tests pinning the behaviour first; responses, error messages, logs and published schemas are unchanged.
493 changes: 311 additions & 182 deletions policyengine_api/asgi_factory.py

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions policyengine_api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ def get_bundle_default_dataset_option(country_id: str) -> dict:
"economy_impact": 3,
"report_output": 3,
}
# Runtime cache versions are stored in legacy VARCHAR(10) columns, and keep at
# least this many digest characters after their scope prefix and schema version.
RUNTIME_CACHE_VERSION_LENGTH = 10
MINIMUM_RUNTIME_CACHE_DIGEST_LENGTH = 4


def _build_runtime_cache_version(
Expand All @@ -204,8 +208,8 @@ def _build_runtime_cache_version(
"""
schema_version = str(RUNTIME_CACHE_SCHEMA_VERSIONS[scope])
prefix = "e" if scope == "economy_impact" else "r"
digest_length = 10 - len(prefix) - len(schema_version)
if digest_length < 4:
digest_length = RUNTIME_CACHE_VERSION_LENGTH - len(prefix) - len(schema_version)
if digest_length < MINIMUM_RUNTIME_CACHE_DIGEST_LENGTH:
msg = f"Runtime cache version for {scope} does not fit in VARCHAR(10)"
raise ValueError(msg)

Expand Down
38 changes: 23 additions & 15 deletions policyengine_api/country.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from datetime import date, datetime
from importlib.metadata import version as get_package_version
from pathlib import Path
from typing import Any
from typing import Any, NamedTuple

import dpath
import pandas as pd
Expand Down Expand Up @@ -87,21 +87,33 @@ def _serialize_axis_result(
return entity_values


class _HouseholdCell(NamedTuple):
"""One requested household value: an entity's variable in one period."""

entity_plural: str
entity_id: str
variable_name: str
period: str


def _record_calculation_failure(
household: dict,
calculation_warnings: list[str],
cell: _HouseholdCell,
*,
has_axes: bool,
population: Population,
entity_plural: str,
entity_id: str,
variable_name: str,
period: str,
axis_population: Population | None,
error: Exception,
) -> None:
if has_axes:
"""Record a household value that could not be calculated.

``axis_population`` is the cell's population when the household has axes:
the cell then holds one null per axis point and a warning is added.
Otherwise the cell is null and the failure is only logged.
"""
entity_plural, entity_id, variable_name, period = cell
if axis_population is not None:
count_entities = len(household[entity_plural])
axis_point_count = population.count // count_entities
axis_point_count = axis_population.count // count_entities
household[entity_plural][entity_id][variable_name][period] = [
None
] * axis_point_count
Expand Down Expand Up @@ -548,12 +560,8 @@ def calculate(
_record_calculation_failure(
household,
calculation_warnings,
has_axes=has_axes,
population=population,
entity_plural=entity_plural,
entity_id=entity_id,
variable_name=variable_name,
period=period,
_HouseholdCell(entity_plural, entity_id, variable_name, period),
axis_population=population if has_axes else None,
error=error,
)

Expand Down
7 changes: 6 additions & 1 deletion policyengine_api/data/congressional_districts.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,11 @@ class CongressionalDistrictMetadataItem(BaseModel):
]


# Numbers whose last two digits are 10 through 20 always take "th" (11th, 12th).
_ALWAYS_TH_REMAINDER_MIN = 10
_ALWAYS_TH_REMAINDER_MAX = 20


def _get_ordinal_suffix(number: int) -> str:
"""Get the ordinal suffix for a number (st, nd, rd, th).

Expand All @@ -598,7 +603,7 @@ def _get_ordinal_suffix(number: int) -> str:
22 -> "nd"

"""
if 10 <= number % 100 <= 20:
if _ALWAYS_TH_REMAINDER_MIN <= number % 100 <= _ALWAYS_TH_REMAINDER_MAX:
# Special case for 11th, 12th, 13th, etc.
suffix = "th"
else:
Expand Down
4 changes: 3 additions & 1 deletion policyengine_api/data/places.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

from policyengine_api.data.congressional_districts import get_valid_state_codes

PLACE_FIPS_CODE_LENGTH = 5


def parse_place_code(place_code: str) -> tuple[str, str]:
"""Parse a place code into its state abbreviation and FIPS components.
Expand Down Expand Up @@ -45,6 +47,6 @@ def validate_place_code(place_code: str) -> None:
msg = f"Invalid state in place code: '{state_abbrev}'"
raise ValueError(msg)

if not place_fips.isdigit() or len(place_fips) != 5:
if not place_fips.isdigit() or len(place_fips) != PLACE_FIPS_CODE_LENGTH:
msg = f"Invalid FIPS code in place: '{place_fips}'. Expected 5-digit FIPS code"
raise ValueError(msg)
8 changes: 6 additions & 2 deletions policyengine_api/data/v2/catalog/catalog_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
)

SUPPORTED_V2_COUNTRY_IDS = frozenset({"us", "uk"})
MAXIMUM_POLICYENGINE_VERSION_LENGTH = 128


class MetadataCatalogUnavailableError(RuntimeError):
Expand Down Expand Up @@ -47,8 +48,11 @@ def validate_policyengine_version(value: str) -> str:
if not isinstance(value, str) or not value or value != value.strip():
msg = "policyengine_version must be a non-empty canonical version"
raise InvalidPolicyEngineVersionError(msg)
if len(value) > 128:
msg = "policyengine_version must be at most 128 characters"
if len(value) > MAXIMUM_POLICYENGINE_VERSION_LENGTH:
msg = (
"policyengine_version must be at most "
f"{MAXIMUM_POLICYENGINE_VERSION_LENGTH} characters"
)
raise InvalidPolicyEngineVersionError(msg)
try:
parsed = Version(value)
Expand Down
Loading
Loading