Repository navigation
Relay simulation run stage and progress on computing economy responses #3916
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Annual economy responses relay the simulation entrypoint's optional stage fields (`stage`, `progress`, `progress_max`, timestamps and `segments`) while a run is computing. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |
|
|
||
| import httpx | ||
| from policyengine_observability import instrument_httpx | ||
| from pydantic import BaseModel, ConfigDict, Field, ValidationError | ||
| from policyengine_api.gcp_logging import logger | ||
| from policyengine_api.libs.gateway_auth import ( | ||
| GatewayAuthError, | ||
|
|
@@ -69,6 +70,45 @@ def _attach_current_request_id(request: httpx.Request) -> None: | |
| request.headers[OBSERVABILITY_ID_HEADER] = observability_id | ||
|
|
||
|
|
||
| class SimulationRunSegments(BaseModel): | ||
| model_config = ConfigDict(extra="ignore", frozen=True) | ||
|
|
||
| completed: int = Field(ge=0) | ||
| total: int = Field(ge=1) | ||
|
|
||
|
|
||
| class SimulationRunProgress(BaseModel): | ||
| """Stage fields of a running Simulation Entrypoint job. | ||
|
|
||
| The entrypoint adds these optional fields to ``GET /jobs/{job_id}`` for | ||
| jobs on workers that report stages (policyengine-sim-api#781). The API | ||
| relays them while an economy run is computing. ``stage`` stays a string: | ||
| later entrypoint versions may add stages, and clients must accept them. | ||
| """ | ||
|
|
||
| model_config = ConfigDict(extra="ignore", frozen=True) | ||
|
|
||
| stage: Optional[str] = None | ||
| progress: Optional[int] = Field(default=None, ge=0, le=100) | ||
| progress_max: Optional[int] = Field(default=None, ge=0, le=100) | ||
| submitted_at: Optional[str] = None | ||
| started_at: Optional[str] = None | ||
| stage_started_at: Optional[str] = None | ||
| segments: Optional[SimulationRunSegments] = None | ||
|
|
||
| @classmethod | ||
| def from_status(cls, data: dict) -> Optional["SimulationRunProgress"]: | ||
| """Read the stage fields; never fail a poll because of them.""" | ||
| try: | ||
| run_progress = cls.model_validate(data) | ||
| except ValidationError: | ||
| return None | ||
| return run_progress if run_progress.to_dict() else None | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The description says computing bodies are unchanged while workers predate policyengine-sim-api#781. Once that PR's gateway is deployed, though, it stores and returns That's additive and harmless, but the description should say so, and a test for a status with only |
||
|
|
||
| def to_dict(self) -> dict: | ||
| return self.model_dump(mode="json", exclude_none=True) | ||
|
|
||
|
|
||
| @dataclass | ||
| class ModalSimulationExecution: | ||
| """ | ||
|
|
@@ -81,6 +121,7 @@ class ModalSimulationExecution: | |
| error: Optional[str] = None | ||
| policyengine_bundle: Optional[dict] = None | ||
| resolved_app_name: Optional[str] = None | ||
| run_progress: Optional[SimulationRunProgress] = None | ||
|
|
||
| @property | ||
| def name(self) -> str: | ||
|
|
@@ -441,6 +482,7 @@ def get_execution_by_id(self, job_id: str) -> ModalSimulationExecution: | |
| error=data.get("error"), | ||
| policyengine_bundle=data.get("policyengine_bundle"), | ||
| resolved_app_name=data.get("resolved_app_name"), | ||
| run_progress=SimulationRunProgress.from_status(data), | ||
| ) | ||
|
|
||
| except httpx.HTTPStatusError as e: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,7 +23,10 @@ | |
| from policyengine_api.data.v1_models import ReformImpact | ||
| from policyengine_api.data.places import validate_place_code | ||
| from policyengine_api.gcp_logging import logger | ||
| from policyengine_api.libs.simulation_entrypoint import simulation_entrypoint | ||
| from policyengine_api.libs.simulation_entrypoint import ( | ||
| SimulationRunProgress, | ||
| simulation_entrypoint, | ||
| ) | ||
| from policyengine_api.observability import ( | ||
| runtime as observability_runtime, | ||
| set_runtime_context, | ||
|
|
@@ -128,6 +131,9 @@ class EconomicImpactResult(BaseModel): | |
| status: ImpactStatus | ||
| data: Optional[dict] = None | ||
| message: Optional[str] = None | ||
| # Stage fields relayed from the Simulation Entrypoint while computing. | ||
| # Kept out of to_dict(); the route adds them only when present. | ||
| run_progress: Optional[dict] = None | ||
|
|
||
| model_config = {"frozen": True} # Make model immutable | ||
|
|
||
|
|
@@ -149,11 +155,11 @@ def completed(cls, data: dict) -> "EconomicImpactResult": | |
| return cls(status=ImpactStatus.OK, data=data) | ||
|
|
||
| @classmethod | ||
| def computing(cls) -> "EconomicImpactResult": | ||
| def computing(cls, run_progress: Optional[dict] = None) -> "EconomicImpactResult": | ||
| """ | ||
| Create an EconomicImpactResult for a computing impact calculation. | ||
| """ | ||
| return cls(status=ImpactStatus.COMPUTING, data=None) | ||
| return cls(status=ImpactStatus.COMPUTING, data=None, run_progress=run_progress) | ||
|
|
||
| @classmethod | ||
| def error(cls, message: str) -> "EconomicImpactResult": | ||
|
|
@@ -1278,7 +1284,14 @@ def _handle_execution_state( | |
| {"message": "Sim API execution is still running"}, | ||
| severity="INFO", | ||
| ) | ||
| return EconomicImpactResult.computing() | ||
| run_progress = getattr(execution, "run_progress", None) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In production, Could |
||
| return EconomicImpactResult.computing( | ||
| run_progress=( | ||
| run_progress.to_dict() | ||
| if isinstance(run_progress, SimulationRunProgress) | ||
| else None | ||
| ) | ||
| ) | ||
|
|
||
| else: | ||
| raise ValueError(f"Unexpected sim API execution state: {execution_state}") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The OpenAPI spec documents these as
format: date-time, but asOptional[str]any string gets relayed unchecked.AwareDatetimewould validate them, andto_dict()already usesmodel_dump(mode="json"), so they'd still serialise as ISO 8601 strings.