Skip to content

Commit 55e4f2f

Browse files
committed
Extract budget-window setup utilities
1 parent 0d0252a commit 55e4f2f

3 files changed

Lines changed: 141 additions & 71 deletions

File tree

policyengine_api/services/economy_service.py

Lines changed: 11 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
normalize_us_region,
2222
)
2323
from policyengine_api.data.places import validate_place_code
24+
from policyengine_api.utils import budget_window as budget_window_utils
2425
from policyengine.simulation import SimulationOptions
2526
from policyengine.utils.data.datasets import get_default_dataset
2627
import json
@@ -73,9 +74,9 @@ class ImpactStatus(Enum):
7374

7475
COMPLETE_STATUSES = [ImpactStatus.OK.value, ImpactStatus.ERROR.value]
7576
COMPUTING_STATUS = ImpactStatus.COMPUTING.value
76-
BUDGET_WINDOW_MAX_ACTIVE_YEARS = 20
77-
BUDGET_WINDOW_MAX_YEARS = 75
78-
BUDGET_WINDOW_MAX_END_YEAR = 2099
77+
BUDGET_WINDOW_MAX_ACTIVE_YEARS = budget_window_utils.BUDGET_WINDOW_MAX_ACTIVE_YEARS
78+
BUDGET_WINDOW_MAX_YEARS = budget_window_utils.BUDGET_WINDOW_MAX_YEARS
79+
BUDGET_WINDOW_MAX_END_YEAR = budget_window_utils.BUDGET_WINDOW_MAX_END_YEAR
7980

8081

8182
class EconomicImpactSetupOptions(BaseModel):
@@ -296,36 +297,21 @@ def get_budget_window_economic_impact(
296297
if country_id == "us":
297298
region = normalize_us_region(region)
298299

299-
if target != "general":
300-
raise ValueError(
301-
"Budget-window calculations only support target='general'"
302-
)
303-
304-
start_year_int = int(start_year)
305-
if not 1 <= window_size <= BUDGET_WINDOW_MAX_YEARS:
306-
raise ValueError(
307-
f"window_size must be between 1 and {BUDGET_WINDOW_MAX_YEARS}"
308-
)
309-
end_year = start_year_int + window_size - 1
310-
if end_year > BUDGET_WINDOW_MAX_END_YEAR:
311-
raise ValueError(
312-
f"budget-window end_year must be {BUDGET_WINDOW_MAX_END_YEAR} or earlier"
313-
)
314-
315-
start_year = str(start_year_int)
316-
years = self._build_budget_window_years(
300+
budget_window_setup = budget_window_utils.build_budget_window_request_setup(
317301
start_year=start_year,
318302
window_size=window_size,
303+
target=target,
319304
)
320-
setup_options = self._build_budget_window_setup_options(
305+
start_year = budget_window_setup.start_year
306+
years = budget_window_setup.years
307+
setup_options = self._build_economic_impact_setup_options(
321308
country_id=country_id,
322309
policy_id=policy_id,
323310
baseline_policy_id=baseline_policy_id,
324311
region=region,
325312
dataset=dataset,
326-
start_year=start_year,
327-
window_size=window_size,
328-
options=options,
313+
time_period=budget_window_setup.time_period,
314+
options=dict(options),
329315
api_version=api_version,
330316
target=target,
331317
)
@@ -378,52 +364,6 @@ def get_budget_window_economic_impact(
378364
print(f"Error getting budget-window economic impact: {str(e)}")
379365
raise e
380366

381-
def _build_budget_window_years(
382-
self,
383-
*,
384-
start_year: str,
385-
window_size: int,
386-
) -> list[str]:
387-
start_year_int = int(start_year)
388-
return [str(start_year_int + index) for index in range(window_size)]
389-
390-
def _build_budget_window_time_period(
391-
self,
392-
*,
393-
start_year: str,
394-
window_size: int,
395-
) -> str:
396-
return f"budget_window:{start_year}:{window_size}"
397-
398-
def _build_budget_window_setup_options(
399-
self,
400-
*,
401-
country_id: str,
402-
policy_id: int,
403-
baseline_policy_id: int,
404-
region: str,
405-
dataset: str,
406-
start_year: str,
407-
window_size: int,
408-
options: dict,
409-
api_version: str,
410-
target: Literal["general", "cliff"],
411-
) -> EconomicImpactSetupOptions:
412-
return self._build_economic_impact_setup_options(
413-
country_id=country_id,
414-
policy_id=policy_id,
415-
baseline_policy_id=baseline_policy_id,
416-
region=region,
417-
dataset=dataset,
418-
time_period=self._build_budget_window_time_period(
419-
start_year=start_year,
420-
window_size=window_size,
421-
),
422-
options=dict(options),
423-
api_version=api_version,
424-
target=target,
425-
)
426-
427367
def _build_budget_window_cache_key(
428368
self,
429369
setup_options: EconomicImpactSetupOptions,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from dataclasses import dataclass
2+
from typing import Literal
3+
4+
BUDGET_WINDOW_MAX_ACTIVE_YEARS = 20
5+
BUDGET_WINDOW_MAX_YEARS = 75
6+
BUDGET_WINDOW_MAX_END_YEAR = 2099
7+
8+
9+
@dataclass(frozen=True)
10+
class BudgetWindowRequestSetup:
11+
start_year: str
12+
window_size: int
13+
years: list[str]
14+
time_period: str
15+
16+
17+
def build_budget_window_years(*, start_year: str, window_size: int) -> list[str]:
18+
start_year_int = int(start_year)
19+
return [str(start_year_int + index) for index in range(window_size)]
20+
21+
22+
def build_budget_window_time_period(*, start_year: str, window_size: int) -> str:
23+
return f"budget_window:{start_year}:{window_size}"
24+
25+
26+
def build_budget_window_request_setup(
27+
*,
28+
start_year: str,
29+
window_size: int,
30+
target: Literal["general", "cliff"],
31+
) -> BudgetWindowRequestSetup:
32+
if target != "general":
33+
raise ValueError("Budget-window calculations only support target='general'")
34+
35+
start_year_int = int(start_year)
36+
if not 1 <= window_size <= BUDGET_WINDOW_MAX_YEARS:
37+
raise ValueError(f"window_size must be between 1 and {BUDGET_WINDOW_MAX_YEARS}")
38+
39+
end_year = start_year_int + window_size - 1
40+
if end_year > BUDGET_WINDOW_MAX_END_YEAR:
41+
raise ValueError(
42+
f"budget-window end_year must be {BUDGET_WINDOW_MAX_END_YEAR} or earlier"
43+
)
44+
45+
normalized_start_year = str(start_year_int)
46+
return BudgetWindowRequestSetup(
47+
start_year=normalized_start_year,
48+
window_size=window_size,
49+
years=build_budget_window_years(
50+
start_year=normalized_start_year,
51+
window_size=window_size,
52+
),
53+
time_period=build_budget_window_time_period(
54+
start_year=normalized_start_year,
55+
window_size=window_size,
56+
),
57+
)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import pytest
2+
3+
from policyengine_api.utils.budget_window import (
4+
BUDGET_WINDOW_MAX_END_YEAR,
5+
BUDGET_WINDOW_MAX_YEARS,
6+
build_budget_window_request_setup,
7+
build_budget_window_time_period,
8+
build_budget_window_years,
9+
)
10+
11+
12+
def test_build_budget_window_years():
13+
assert build_budget_window_years(start_year="2026", window_size=3) == [
14+
"2026",
15+
"2027",
16+
"2028",
17+
]
18+
19+
20+
def test_build_budget_window_time_period():
21+
assert (
22+
build_budget_window_time_period(start_year="2026", window_size=3)
23+
== "budget_window:2026:3"
24+
)
25+
26+
27+
def test_build_budget_window_request_setup_normalizes_start_year():
28+
setup = build_budget_window_request_setup(
29+
start_year="02026",
30+
window_size=2,
31+
target="general",
32+
)
33+
34+
assert setup.start_year == "2026"
35+
assert setup.window_size == 2
36+
assert setup.years == ["2026", "2027"]
37+
assert setup.time_period == "budget_window:2026:2"
38+
39+
40+
def test_build_budget_window_request_setup_rejects_cliff_target():
41+
with pytest.raises(
42+
ValueError,
43+
match="Budget-window calculations only support target='general'",
44+
):
45+
build_budget_window_request_setup(
46+
start_year="2026",
47+
window_size=2,
48+
target="cliff",
49+
)
50+
51+
52+
def test_build_budget_window_request_setup_rejects_oversized_window():
53+
with pytest.raises(
54+
ValueError,
55+
match=f"window_size must be between 1 and {BUDGET_WINDOW_MAX_YEARS}",
56+
):
57+
build_budget_window_request_setup(
58+
start_year="2026",
59+
window_size=BUDGET_WINDOW_MAX_YEARS + 1,
60+
target="general",
61+
)
62+
63+
64+
def test_build_budget_window_request_setup_rejects_end_year_after_max():
65+
with pytest.raises(
66+
ValueError,
67+
match=f"budget-window end_year must be {BUDGET_WINDOW_MAX_END_YEAR} or earlier",
68+
):
69+
build_budget_window_request_setup(
70+
start_year="2090",
71+
window_size=20,
72+
target="general",
73+
)

0 commit comments

Comments
 (0)