Skip to content

Commit 259c390

Browse files
anth-volkclaude
andcommitted
Accept a simulation or report kind given as text
NewSimulation and NewReport typed kind as an enum but neither checked nor converted it, so a kind given as plain text (as a mapping of v1 records easily does) passed their checks and then failed with AttributeError when the store read kind.value. Both now convert the kind to its enum member when they are created, and reject an unknown kind with a ValueError that names the field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent d67a37d commit 259c390

3 files changed

Lines changed: 45 additions & 0 deletions

File tree

‎libs/policyengine-simulation-persistence/src/policyengine_simulation_persistence/reports.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
check_country,
4343
check_legacy_id,
4444
insert_or_get_existing,
45+
parse_kind,
4546
simulations,
4647
)
4748

@@ -129,6 +130,8 @@ class NewReport:
129130

130131
def __post_init__(self) -> None:
131132
"""Reject malformed fields without touching the database."""
133+
# A frozen dataclass sets its own field through object.__setattr__.
134+
object.__setattr__(self, "kind", parse_kind(self.kind, ReportKind))
132135
check_country(self.country)
133136
if not FIRST_YEAR <= self.year <= LAST_YEAR:
134137
msg = f"year must be between {FIRST_YEAR} and {LAST_YEAR}"

‎libs/policyengine-simulation-persistence/src/policyengine_simulation_persistence/simulations.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,18 @@ def check_legacy_id(field_name: str, value: int | None) -> None:
145145
raise ValueError(msg)
146146

147147

148+
def parse_kind[KindT: StrEnum](value: object, kind_type: type[KindT]) -> KindT:
149+
"""Return the ``kind_type`` member for ``value``, which may be its text.
150+
151+
Raise ValueError naming ``kind`` when the value is not one of its members.
152+
"""
153+
try:
154+
return kind_type(value)
155+
except ValueError:
156+
msg = f"kind must be one of {', '.join(kind_type)}"
157+
raise ValueError(msg) from None
158+
159+
148160
def check_country(country: str) -> None:
149161
"""Reject a country that is not a lowercase two-letter code."""
150162
if not COUNTRY_PATTERN.fullmatch(country):
@@ -200,6 +212,8 @@ class NewSimulation:
200212

201213
def __post_init__(self) -> None:
202214
"""Reject malformed fields without touching the database."""
215+
# A frozen dataclass sets its own field through object.__setattr__.
216+
object.__setattr__(self, "kind", parse_kind(self.kind, SimulationKind))
203217
check_country(self.country)
204218
_check_catalog_key("region_code", self.region_code)
205219
_check_catalog_key("dataset_name", self.dataset_name)

‎libs/policyengine-simulation-persistence/tests/test_definitions.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,31 @@ def test_report_options_must_be_a_json_object() -> None:
7070
year=2026,
7171
options=["not", "an", "object"], # type: ignore[arg-type]
7272
)
73+
74+
75+
def test_a_kind_given_as_text_becomes_the_enum() -> None:
76+
simulation = NewSimulation(kind="economy", country="us", region_code="state/ca") # type: ignore[arg-type]
77+
report = NewReport(
78+
kind="household", # type: ignore[arg-type]
79+
country="us",
80+
baseline_simulation_id=uuid4(),
81+
year=2026,
82+
)
83+
84+
assert simulation.kind is SimulationKind.ECONOMY
85+
assert report.kind is ReportKind.HOUSEHOLD
86+
87+
88+
def test_an_unknown_simulation_kind_is_rejected() -> None:
89+
with pytest.raises(ValueError, match="kind"):
90+
NewSimulation(kind="microsimulation", country="us", region_code="state/ca") # type: ignore[arg-type]
91+
92+
93+
def test_an_unknown_report_kind_is_rejected() -> None:
94+
with pytest.raises(ValueError, match="kind"):
95+
NewReport(
96+
kind="microsimulation", # type: ignore[arg-type]
97+
country="us",
98+
baseline_simulation_id=uuid4(),
99+
year=2026,
100+
)

0 commit comments

Comments
 (0)