Skip to content

Commit 9a81e6c

Browse files
committed
Handle budget-window submission validation errors
1 parent 4a73fb0 commit 9a81e6c

2 files changed

Lines changed: 97 additions & 0 deletions

File tree

policyengine_api/services/economy_service.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from policyengine_api.utils import budget_window as budget_window_utils
2525
from policyengine.simulation import SimulationOptions
2626
from policyengine.utils.data.datasets import get_default_dataset
27+
import httpx
2728
import json
2829
import datetime
2930
import hashlib
@@ -348,6 +349,15 @@ def get_budget_window_economic_impact(
348349
budget_window_cache.store_batch_job_id(
349350
cache_key, batch_execution.batch_job_id
350351
)
352+
except httpx.HTTPStatusError as error:
353+
budget_window_cache.clear_starting_claim(cache_key, claim_token)
354+
if 400 <= error.response.status_code < 500:
355+
return BudgetWindowEconomicImpactResult.failed(
356+
self._build_budget_window_submission_error_message(error),
357+
queued_years=years,
358+
cache_status=cache_status,
359+
)
360+
raise
351361
except Exception:
352362
budget_window_cache.clear_starting_claim(cache_key, claim_token)
353363
raise
@@ -443,6 +453,26 @@ def _start_budget_window_batch(
443453

444454
return simulation_api.run_budget_window_batch(sim_params)
445455

456+
def _build_budget_window_submission_error_message(
457+
self, error: httpx.HTTPStatusError
458+
) -> str:
459+
try:
460+
response_json = error.response.json()
461+
except ValueError:
462+
response_json = None
463+
464+
if isinstance(response_json, dict):
465+
for key in ("detail", "message", "error"):
466+
value = response_json.get(key)
467+
if value:
468+
return str(value)
469+
470+
response_text = error.response.text.strip()
471+
if response_text:
472+
return response_text
473+
474+
return str(error)
475+
446476
def _get_budget_window_result_from_batch_job_id(
447477
self,
448478
*,

tests/unit/services/test_economy_service.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import sys
3+
import httpx
34
import pytest
45
from unittest.mock import patch, MagicMock
56
from typing import Literal
@@ -110,6 +111,19 @@ def make_mock_budget_impact_data(
110111
}
111112

112113

114+
def make_http_status_error(status_code: int, payload: dict) -> httpx.HTTPStatusError:
115+
request = httpx.Request(
116+
"POST",
117+
"https://policyengine-staging--policyengine-simulation-gateway-web-app.modal.run/simulate/economy/budget-window",
118+
)
119+
response = httpx.Response(status_code, json=payload, request=request)
120+
return httpx.HTTPStatusError(
121+
f"Client error '{status_code}'",
122+
request=request,
123+
response=response,
124+
)
125+
126+
113127
class TestEconomyService:
114128
class TestGetEconomicImpact:
115129
@pytest.fixture
@@ -1010,6 +1024,59 @@ def test__given_batch_submission_fails__clears_start_claim(
10101024
"budget-window-cache-key", MOCK_PROCESS_ID
10111025
)
10121026

1027+
def test__given_modal_rejects_batch_submission__returns_failed_result(
1028+
self,
1029+
economy_service,
1030+
base_params,
1031+
mock_simulation_api,
1032+
mock_budget_window_cache,
1033+
):
1034+
mock_simulation_api.run_budget_window_batch.side_effect = make_http_status_error(
1035+
400,
1036+
{
1037+
"detail": (
1038+
"Invalid Hugging Face dataset URI: "
1039+
"'hf://policyengine/nonexistent-budget-window-test.h5@0.0.0'"
1040+
)
1041+
},
1042+
)
1043+
1044+
result = economy_service.get_budget_window_economic_impact(**base_params)
1045+
1046+
assert result.status == ImpactStatus.ERROR
1047+
assert result.data is None
1048+
assert result.error == (
1049+
"Invalid Hugging Face dataset URI: "
1050+
"'hf://policyengine/nonexistent-budget-window-test.h5@0.0.0'"
1051+
)
1052+
assert result.completed_years == []
1053+
assert result.computing_years == []
1054+
assert result.queued_years == ["2026", "2027", "2028"]
1055+
assert result.cache_status == "miss"
1056+
mock_budget_window_cache.clear_starting_claim.assert_called_once_with(
1057+
"budget-window-cache-key", MOCK_PROCESS_ID
1058+
)
1059+
mock_budget_window_cache.store_batch_job_id.assert_not_called()
1060+
1061+
def test__given_modal_server_error_on_batch_submission__raises(
1062+
self,
1063+
economy_service,
1064+
base_params,
1065+
mock_simulation_api,
1066+
mock_budget_window_cache,
1067+
):
1068+
mock_simulation_api.run_budget_window_batch.side_effect = (
1069+
make_http_status_error(500, {"detail": "gateway unavailable"})
1070+
)
1071+
1072+
with pytest.raises(httpx.HTTPStatusError):
1073+
economy_service.get_budget_window_economic_impact(**base_params)
1074+
1075+
mock_budget_window_cache.clear_starting_claim.assert_called_once_with(
1076+
"budget-window-cache-key", MOCK_PROCESS_ID
1077+
)
1078+
mock_budget_window_cache.store_batch_job_id.assert_not_called()
1079+
10131080
def test__given_cliff_target__raises_value_error(
10141081
self, economy_service, base_params
10151082
):

0 commit comments

Comments
 (0)