diff --git a/changelog.d/923.changed.md b/changelog.d/923.changed.md new file mode 100644 index 000000000..8d6aff6c7 --- /dev/null +++ b/changelog.d/923.changed.md @@ -0,0 +1 @@ +UK constituency and local-authority records no longer have `x` or `y` fields in the contract or in the gateway's OpenAPI document. A record that carries either, even as `null`, is now refused. The contract's pydantic requirement returns to 2.0 or later, since nothing uses field-level `exclude_if` any more. diff --git a/libs/policyengine-simulation-contract/pyproject.toml b/libs/policyengine-simulation-contract/pyproject.toml index 1de678f91..7214eee3b 100644 --- a/libs/policyengine-simulation-contract/pyproject.toml +++ b/libs/policyengine-simulation-contract/pyproject.toml @@ -8,7 +8,7 @@ authors = [ license = {file = "../../LICENSE"} requires-python = ">=3.13" dependencies = [ - "pydantic>=2.12", + "pydantic>=2.0", "packaging>=24", # gateway_models embeds the TelemetryEnvelope from the observability lib. "policyengine-simulation-observability", diff --git a/libs/policyengine-simulation-contract/src/policyengine_simulation_contract/macro_output.py b/libs/policyengine-simulation-contract/src/policyengine_simulation_contract/macro_output.py index 3592d6442..7359f0db5 100644 --- a/libs/policyengine-simulation-contract/src/policyengine_simulation_contract/macro_output.py +++ b/libs/policyengine-simulation-contract/src/policyengine_simulation_contract/macro_output.py @@ -4,7 +4,7 @@ from typing import Generic, Literal, TypeVar -from pydantic import BaseModel, ConfigDict, Field, RootModel +from pydantic import BaseModel, ConfigDict, RootModel from policyengine_simulation_contract.spm import SPMComparisonProvenance @@ -240,10 +240,6 @@ class CliffImpactOutput(MacroOutputModel): UKGeographyDisclosureTier = Literal["reported", "approximate"] -def _is_none(value: object) -> bool: - return value is None - - class UKAreaDisclosureFields(MacroOutputModel): """Disclosure tier and 90% intervals of a published UK area's income changes. @@ -272,16 +268,10 @@ class ConstituencyImpactRecord(UKAreaDisclosureFields): ``relative_household_income_change`` the relative change in the area's total household net income, both rounded to the order of their standard error. ``population`` is the weighted number of households. - - ``x`` and ``y`` are the area's integer map position, which is being - removed: a record validates with or without them, and one without them - leaves both out of its JSON rather than writing null. """ constituency_code: str constituency_name: str - x: int | None = Field(default=None, exclude_if=_is_none) - y: int | None = Field(default=None, exclude_if=_is_none) average_household_income_change: float relative_household_income_change: float population: float @@ -295,8 +285,6 @@ class LocalAuthorityImpactRecord(UKAreaDisclosureFields): local_authority_code: str local_authority_name: str - x: int | None = Field(default=None, exclude_if=_is_none) - y: int | None = Field(default=None, exclude_if=_is_none) average_household_income_change: float relative_household_income_change: float population: float diff --git a/libs/policyengine-simulation-contract/tests/test_local_area_breakdown.py b/libs/policyengine-simulation-contract/tests/test_local_area_breakdown.py index d2cacccfd..a3a1d3bcf 100644 --- a/libs/policyengine-simulation-contract/tests/test_local_area_breakdown.py +++ b/libs/policyengine-simulation-contract/tests/test_local_area_breakdown.py @@ -23,8 +23,6 @@ CONSTITUENCY = { "constituency_code": "E14001063", "constituency_name": "Aldershot", - "x": 56, - "y": -40, "average_household_income_change": -150.0, "relative_household_income_change": -0.0026, "population": 40_530.0, @@ -37,8 +35,6 @@ LOCAL_AUTHORITY = { "local_authority_code": "E06000063", "local_authority_name": "Cumberland", - "x": 4, - "y": 18, "average_household_income_change": -120.0, "relative_household_income_change": -0.002, "population": 120_000.0, @@ -203,43 +199,41 @@ def test_each_area_level_reads_its_records_as_its_own_type(): ] -def _without_map_position(record: dict[str, object]) -> dict[str, object]: - return {key: value for key, value in record.items() if key not in {"x", "y"}} - - -# Records from an executor that no longer sends each area's map position. -BREAKDOWN_WITHOUT_MAP_POSITIONS = { - **BREAKDOWN, - "constituency_impact": [_without_map_position(CONSTITUENCY)], - "local_authority_impact": [_without_map_position(LOCAL_AUTHORITY)], -} - - -def test_a_breakdown_without_map_positions_round_trips_without_them(): - output = LocalAreaBreakdownOutput.model_validate(BREAKDOWN_WITHOUT_MAP_POSITIONS) - - assert output.model_dump(mode="json") == BREAKDOWN_WITHOUT_MAP_POSITIONS - - -def test_a_job_status_reads_a_breakdown_without_map_positions(): - response = JobStatusResponse( - status="complete", result=BREAKDOWN_WITHOUT_MAP_POSITIONS - ) - - assert isinstance(response.result, LocalAreaBreakdownOutput) - assert response.model_dump(mode="json")["result"] == ( - BREAKDOWN_WITHOUT_MAP_POSITIONS - ) - - -def test_a_national_result_reads_area_lists_without_map_positions(): +def test_a_national_result_reads_its_area_lists(): national = { **NATIONAL, - "constituency_impact": [_without_map_position(CONSTITUENCY)], - "local_authority_impact": [_without_map_position(LOCAL_AUTHORITY)], + "constituency_impact": [CONSTITUENCY], + "local_authority_impact": [LOCAL_AUTHORITY], } response = JobStatusResponse(status="complete", result=national) assert isinstance(response.result, SingleYearMacroOutput) assert response.model_dump(mode="json")["result"] == national + + +# The contract has no x or y (each area's map position), so a result whose +# records carry either is refused. +@pytest.mark.parametrize( + "result", + [ + pytest.param( + {**BREAKDOWN, "constituency_impact": [{**CONSTITUENCY, "x": 56, "y": -40}]}, + id="breakdown-constituency", + ), + pytest.param( + { + **BREAKDOWN, + "local_authority_impact": [{**LOCAL_AUTHORITY, "x": 4, "y": 18}], + }, + id="breakdown-local-authority", + ), + pytest.param( + {**NATIONAL, "constituency_impact": [{**CONSTITUENCY, "x": 56, "y": -40}]}, + id="national-constituency", + ), + ], +) +def test_a_job_status_refuses_area_records_with_x_and_y(result): + with pytest.raises(ValidationError, match="Extra inputs are not permitted"): + JobStatusResponse(status="complete", result=result) diff --git a/libs/policyengine-simulation-contract/tests/test_macro_output_uk_geography.py b/libs/policyengine-simulation-contract/tests/test_macro_output_uk_geography.py index 7a7b66640..2ac68a493 100644 --- a/libs/policyengine-simulation-contract/tests/test_macro_output_uk_geography.py +++ b/libs/policyengine-simulation-contract/tests/test_macro_output_uk_geography.py @@ -12,11 +12,9 @@ ) from pydantic import ValidationError -STORED_BEFORE_DISCLOSURE = { +RECORD_WITHOUT_DISCLOSURE = { "constituency_code": "E14001063", "constituency_name": "Aldershot", - "x": 56, - "y": -40, "average_household_income_change": -150.95, "relative_household_income_change": -0.0026, "population": 40_530.65, @@ -30,8 +28,8 @@ } -def test_records_stored_before_disclosure_control_still_validate() -> None: - record = ConstituencyImpactRecord.model_validate(STORED_BEFORE_DISCLOSURE) +def test_records_without_disclosure_fields_still_validate() -> None: + record = ConstituencyImpactRecord.model_validate(RECORD_WITHOUT_DISCLOSURE) assert record.disclosure_tier is None assert record.average_household_income_change_lower is None @@ -43,8 +41,6 @@ def test_records_carry_the_disclosure_tier_and_intervals() -> None: { "local_authority_code": "E06000063", "local_authority_name": "Cumberland", - "x": 4, - "y": 18, "average_household_income_change": -150.0, "relative_household_income_change": -0.0026, "population": 120_000.0, @@ -60,12 +56,12 @@ def test_a_withheld_tier_is_not_a_record() -> None: # Withheld areas are left out of the list, so no record holds that tier. with pytest.raises(ValidationError): ConstituencyImpactRecord.model_validate( - {**STORED_BEFORE_DISCLOSURE, "disclosure_tier": "withheld"} + {**RECORD_WITHOUT_DISCLOSURE, "disclosure_tier": "withheld"} ) def test_published_values_stay_required() -> None: - record = {**STORED_BEFORE_DISCLOSURE, **DISCLOSURE_FIELDS} + record = {**RECORD_WITHOUT_DISCLOSURE, **DISCLOSURE_FIELDS} record["average_household_income_change"] = None with pytest.raises(ValidationError): @@ -76,17 +72,9 @@ def test_an_empty_area_list_is_valid() -> None: assert GeographicImpactOutput.model_validate([]).root == [] -# x and y, each area's integer map position, are being removed. The executor -# sends them until a later change stops it, so a record validates with or -# without them. -CONSTITUENCY_WITHOUT_MAP_POSITION = { - **{ - key: value - for key, value in STORED_BEFORE_DISCLOSURE.items() - if key not in {"x", "y"} - }, - **DISCLOSURE_FIELDS, -} +# The records have no x or y (an area's integer map position), and a record +# that carries either is refused. +CONSTITUENCY_WITHOUT_MAP_POSITION = {**RECORD_WITHOUT_DISCLOSURE, **DISCLOSURE_FIELDS} LOCAL_AUTHORITY_WITHOUT_MAP_POSITION = { "local_authority_code": "E06000063", "local_authority_name": "Cumberland", @@ -108,41 +96,35 @@ def test_an_empty_area_list_is_valid() -> None: @pytest.mark.parametrize(("record_type", "payload"), AREA_RECORDS_WITHOUT_MAP_POSITION) -def test_records_without_a_map_position_validate_and_dump_without_one( +def test_records_validate_and_dump_without_x_or_y( record_type: type[ConstituencyImpactRecord | LocalAuthorityImpactRecord], payload: dict[str, object], ) -> None: record = record_type.model_validate(payload) - assert record.x is None - assert record.y is None + assert {"x", "y"}.isdisjoint(record_type.model_fields) assert record.model_dump(mode="python") == payload assert record.model_dump(mode="json") == payload assert json.loads(record.model_dump_json()) == payload @pytest.mark.parametrize(("record_type", "payload"), AREA_RECORDS_WITHOUT_MAP_POSITION) -def test_records_with_a_map_position_keep_it( - record_type: type[ConstituencyImpactRecord | LocalAuthorityImpactRecord], - payload: dict[str, object], -) -> None: - with_map_position = {**payload, "x": 56, "y": -40} - - record = record_type.model_validate(with_map_position) - - assert (record.x, record.y) == (56, -40) - assert record.model_dump(mode="json") == with_map_position - - -@pytest.mark.parametrize(("record_type", "payload"), AREA_RECORDS_WITHOUT_MAP_POSITION) -def test_a_null_map_position_is_left_out_of_the_dump( +@pytest.mark.parametrize( + "map_position", + [ + pytest.param({"x": 56, "y": -40}, id="both"), + pytest.param({"x": 56}, id="x-only"), + pytest.param({"y": -40}, id="y-only"), + pytest.param({"x": None, "y": None}, id="null"), + ], +) +def test_records_with_x_or_y_are_refused( record_type: type[ConstituencyImpactRecord | LocalAuthorityImpactRecord], payload: dict[str, object], + map_position: dict[str, int | None], ) -> None: - record = record_type.model_validate({**payload, "x": None, "y": None}) - - assert record.model_dump(mode="json") == payload - assert json.loads(record.model_dump_json()) == payload + with pytest.raises(ValidationError, match="Extra inputs are not permitted"): + record_type.model_validate({**payload, **map_position}) def test_an_area_list_without_map_positions_dumps_without_them() -> None: diff --git a/libs/policyengine-simulation-contract/uv.lock b/libs/policyengine-simulation-contract/uv.lock index 3fa7e7d15..8915800ea 100644 --- a/libs/policyengine-simulation-contract/uv.lock +++ b/libs/policyengine-simulation-contract/uv.lock @@ -1122,7 +1122,7 @@ requires-dist = [ { name = "modal", marker = "extra == 'test'", specifier = ">=1.4,<2" }, { name = "packaging", specifier = ">=24" }, { name = "policyengine-simulation-observability", editable = "../policyengine-simulation-observability" }, - { name = "pydantic", specifier = ">=2.12" }, + { name = "pydantic", specifier = ">=2.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.4" }, { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.25.3" }, diff --git a/libs/policyengine-stage12-persistence/uv.lock b/libs/policyengine-stage12-persistence/uv.lock index 20d14c19b..5e3c5dca8 100644 --- a/libs/policyengine-stage12-persistence/uv.lock +++ b/libs/policyengine-stage12-persistence/uv.lock @@ -604,7 +604,7 @@ requires-dist = [ { name = "modal", marker = "extra == 'test'", specifier = ">=1.4,<2" }, { name = "packaging", specifier = ">=24" }, { name = "policyengine-simulation-observability", editable = "../policyengine-simulation-observability" }, - { name = "pydantic", specifier = ">=2.12" }, + { name = "pydantic", specifier = ">=2.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.4" }, { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.25.3" }, diff --git a/projects/policyengine-apis-integ/tests/simulation/test_calculate.py b/projects/policyengine-apis-integ/tests/simulation/test_calculate.py index b4dcae7b9..c17f1d599 100644 --- a/projects/policyengine-apis-integ/tests/simulation/test_calculate.py +++ b/projects/policyengine-apis-integ/tests/simulation/test_calculate.py @@ -4,6 +4,7 @@ that economy-wide simulations complete successfully. """ +import inspect import json import time from collections.abc import Mapping @@ -14,10 +15,12 @@ import pytest from policyengine_api_simulation_client import AuthenticatedClient, Client from policyengine_api_simulation_client.models import ( + ConstituencyImpactRecord, JobStage, JobStatusResponse, JobSubmitResponse, LocalAreaBreakdownOutput, + LocalAuthorityImpactRecord, SimulationRequest, ) @@ -363,10 +366,6 @@ def test_result_assertions_accept_generated_client_objects() -> None: } -def _without_x_and_y(record: Mapping) -> dict: - return {key: value for key, value in record.items() if key not in {"x", "y"}} - - def test_breakdown_assertions_accept_generated_client_objects() -> None: status = JobStatusResponse.from_dict( {"status": "complete", "result": _BREAKDOWN_RESULT} @@ -376,33 +375,11 @@ def test_breakdown_assertions_accept_generated_client_objects() -> None: assert_uk_local_area_breakdown(status.result) -def test_generated_client_reads_area_records_without_x_and_y() -> None: - # Once the executor stops sending x and y, records arrive without them. - breakdown = { - **_BREAKDOWN_RESULT, - "constituency_impact": [_without_x_and_y(_BREAKDOWN_CONSTITUENCY)], - "local_authority_impact": [_without_x_and_y(_BREAKDOWN_LOCAL_AUTHORITY)], - } - - status = JobStatusResponse.from_dict({"status": "complete", "result": breakdown}) - - assert isinstance(status.result, LocalAreaBreakdownOutput) - assert status.result.to_dict() == breakdown - - -def test_generated_client_reads_area_records_with_x_and_y() -> None: - # Breakdowns that started before the executor stopped sending x and y - # still carry them, until the contract removes the two fields. - breakdown = { - **_BREAKDOWN_RESULT, - "constituency_impact": [{**_BREAKDOWN_CONSTITUENCY, "x": 56, "y": -40}], - "local_authority_impact": [{**_BREAKDOWN_LOCAL_AUTHORITY, "x": 4, "y": 18}], - } - - status = JobStatusResponse.from_dict({"status": "complete", "result": breakdown}) - - assert isinstance(status.result, LocalAreaBreakdownOutput) - assert status.result.to_dict() == breakdown +@pytest.mark.parametrize( + "record_model", [ConstituencyImpactRecord, LocalAuthorityImpactRecord] +) +def test_generated_client_area_records_have_no_x_or_y(record_model: type) -> None: + assert {"x", "y"}.isdisjoint(inspect.signature(record_model).parameters) @pytest.mark.parametrize( diff --git a/projects/policyengine-simulation-entry/uv.lock b/projects/policyengine-simulation-entry/uv.lock index e9a587c24..c3bfa7d53 100644 --- a/projects/policyengine-simulation-entry/uv.lock +++ b/projects/policyengine-simulation-entry/uv.lock @@ -881,7 +881,7 @@ requires-dist = [ { name = "modal", marker = "extra == 'test'", specifier = ">=1.4,<2" }, { name = "packaging", specifier = ">=24" }, { name = "policyengine-simulation-observability", editable = "../../libs/policyengine-simulation-observability" }, - { name = "pydantic", specifier = ">=2.12" }, + { name = "pydantic", specifier = ">=2.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.4" }, { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.25.3" }, diff --git a/projects/policyengine-simulation-executor/uv.lock b/projects/policyengine-simulation-executor/uv.lock index 9163cc649..f2f2b57f3 100644 --- a/projects/policyengine-simulation-executor/uv.lock +++ b/projects/policyengine-simulation-executor/uv.lock @@ -1883,7 +1883,7 @@ requires-dist = [ { name = "modal", marker = "extra == 'test'", specifier = ">=1.4,<2" }, { name = "packaging", specifier = ">=24" }, { name = "policyengine-simulation-observability", editable = "../../libs/policyengine-simulation-observability" }, - { name = "pydantic", specifier = ">=2.12" }, + { name = "pydantic", specifier = ">=2.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.4" }, { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.25.3" }, diff --git a/projects/policyengine-simulation-gateway/fixtures/gateway_responses.py b/projects/policyengine-simulation-gateway/fixtures/gateway_responses.py index 655fd6a26..be92b6f7b 100644 --- a/projects/policyengine-simulation-gateway/fixtures/gateway_responses.py +++ b/projects/policyengine-simulation-gateway/fixtures/gateway_responses.py @@ -13,8 +13,6 @@ { "constituency_code": "E14001063", "constituency_name": "Aldershot", - "x": 56, - "y": -40, "average_household_income_change": -150.0, "relative_household_income_change": -0.0026, "population": 40530.0, diff --git a/projects/policyengine-simulation-gateway/tests/golden/openapi.json b/projects/policyengine-simulation-gateway/tests/golden/openapi.json index edf892ffb..3e1104b2d 100644 --- a/projects/policyengine-simulation-gateway/tests/golden/openapi.json +++ b/projects/policyengine-simulation-gateway/tests/golden/openapi.json @@ -1106,28 +1106,6 @@ "type": "string", "title": "Constituency Name" }, - "x": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "X" - }, - "y": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Y" - }, "average_household_income_change": { "type": "number", "title": "Average Household Income Change" @@ -1151,7 +1129,7 @@ "population" ], "title": "ConstituencyImpactRecord", - "description": "Reform impact on household net income in one UK parliamentary constituency.\n\n``average_household_income_change`` is the average change per household and\n``relative_household_income_change`` the relative change in the area's total\nhousehold net income, both rounded to the order of their standard error.\n``population`` is the weighted number of households.\n\n``x`` and ``y`` are the area's integer map position, which is being\nremoved: a record validates with or without them, and one without them\nleaves both out of its JSON rather than writing null." + "description": "Reform impact on household net income in one UK parliamentary constituency.\n\n``average_household_income_change`` is the average change per household and\n``relative_household_income_change`` the relative change in the area's total\nhousehold net income, both rounded to the order of their standard error.\n``population`` is the weighted number of households." }, "DecileOutput": { "properties": { @@ -1879,28 +1857,6 @@ "type": "string", "title": "Local Authority Name" }, - "x": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "X" - }, - "y": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Y" - }, "average_household_income_change": { "type": "number", "title": "Average Household Income Change" diff --git a/projects/policyengine-simulation-gateway/tests/test_simulation_job_status.py b/projects/policyengine-simulation-gateway/tests/test_simulation_job_status.py index 48f2c3ea1..f42ef2dbf 100644 --- a/projects/policyengine-simulation-gateway/tests/test_simulation_job_status.py +++ b/projects/policyengine-simulation-gateway/tests/test_simulation_job_status.py @@ -10,10 +10,6 @@ from policyengine_simulation_observability.identifiers import OBSERVABILITY_ID_HEADER -def _without_x_and_y(record: dict[str, object]) -> dict[str, object]: - return {key: value for key, value in record.items() if key not in {"x", "y"}} - - class TestSubmitSimulationEndpoint: """Tests for POST /simulate/economy/comparison endpoint.""" @@ -166,21 +162,10 @@ def test__given_complete_local_area_breakdown__then_polling_returns_it( assert response.json()["status"] == "complete" assert response.json()["result"] == LOCAL_AREA_BREAKDOWN_RESULT - @pytest.mark.parametrize( - "map_position", - [ - pytest.param({}, id="left-out"), - pytest.param({"x": None, "y": None}, id="null"), - ], - ) def test__given_breakdown_records_without_x_and_y__then_json_has_no_x_or_y( - self, mock_modal, client: TestClient, map_position: dict[str, None] + self, mock_modal, client: TestClient ): - # Once the executor stops sending x and y on UK area records, the - # JSON must not invent null keys. - constituency = _without_x_and_y( - LOCAL_AREA_BREAKDOWN_RESULT["constituency_impact"][0] - ) + constituency = LOCAL_AREA_BREAKDOWN_RESULT["constituency_impact"][0] local_authority = { "local_authority_code": "E06000063", "local_authority_name": "Cumberland", @@ -199,8 +184,7 @@ def test__given_breakdown_records_without_x_and_y__then_json_has_no_x_or_y( ) mock_modal["func"].last_call.result = { **LOCAL_AREA_BREAKDOWN_RESULT, - "constituency_impact": [{**constituency, **map_position}], - "local_authority_impact": [{**local_authority, **map_position}], + "local_authority_impact": [local_authority], } response = client.get(f"/jobs/{submit_response.json()['job_id']}") @@ -213,9 +197,7 @@ def test__given_breakdown_records_without_x_and_y__then_json_has_no_x_or_y( def test__given_national_records_without_x_and_y__then_json_has_no_x_or_y( self, mock_modal, client: TestClient ): - constituency = _without_x_and_y( - LOCAL_AREA_BREAKDOWN_RESULT["constituency_impact"][0] - ) + constituency = LOCAL_AREA_BREAKDOWN_RESULT["constituency_impact"][0] submit_response = client.post( "/simulate/economy/comparison", json={"country": "uk", "scope": "macro", "region": "uk", "reform": {}}, @@ -228,6 +210,27 @@ def test__given_national_records_without_x_and_y__then_json_has_no_x_or_y( assert response.status_code == 200 assert response.json()["result"]["constituency_impact"] == [constituency] + def test__given_records_with_x_and_y__then_the_job_fails( + self, mock_modal, client: TestClient + ): + # The contract no longer has x and y, so a result whose records carry + # them fails validation. This is why the contract change waits until no + # breakdown started before the executor stopped sending them is running. + constituency = LOCAL_AREA_BREAKDOWN_RESULT["constituency_impact"][0] + submit_response = client.post( + "/simulate/economy/comparison", + json={"country": "uk", "scope": "macro", "region": "uk", "reform": {}}, + ) + mock_modal["func"].last_call.result = { + **LOCAL_AREA_BREAKDOWN_RESULT, + "constituency_impact": [{**constituency, "x": 56, "y": -40}], + } + + response = client.get(f"/jobs/{submit_response.json()['job_id']}") + + assert response.status_code == 500 + assert response.json()["status"] == "failed" + def test__given_running_job__then_polling_returns_202( self, mock_modal, client: TestClient ): diff --git a/projects/policyengine-simulation-gateway/uv.lock b/projects/policyengine-simulation-gateway/uv.lock index 8e709799d..082898381 100644 --- a/projects/policyengine-simulation-gateway/uv.lock +++ b/projects/policyengine-simulation-gateway/uv.lock @@ -1304,7 +1304,7 @@ requires-dist = [ { name = "modal", marker = "extra == 'test'", specifier = ">=1.4,<2" }, { name = "packaging", specifier = ">=24" }, { name = "policyengine-simulation-observability", editable = "../../libs/policyengine-simulation-observability" }, - { name = "pydantic", specifier = ">=2.12" }, + { name = "pydantic", specifier = ">=2.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.4" }, { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.25.3" },