feat(workbench): add runtime GIS and sample integrations
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
"""Typed read-only projections consumed by the supply workbench."""
|
||||
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from pydantic import AwareDatetime, BeforeValidator, JsonValue, model_validator
|
||||
|
||||
from app.domain.schemas.shared import (
|
||||
NonEmptyStr,
|
||||
NonNegativeInt,
|
||||
PositiveInt,
|
||||
StrictBool,
|
||||
StrictModel,
|
||||
)
|
||||
|
||||
|
||||
def _workbench_datetime_wire_value(value: object):
|
||||
from datetime import datetime
|
||||
|
||||
if not isinstance(value, (datetime, str)):
|
||||
raise ValueError("datetime input must be an RFC3339 string or datetime instance")
|
||||
return value
|
||||
|
||||
|
||||
WorkbenchDateTime = Annotated[
|
||||
AwareDatetime, BeforeValidator(_workbench_datetime_wire_value)
|
||||
]
|
||||
ProjectionAvailability = Literal["ready", "not_configured", "degraded"]
|
||||
|
||||
|
||||
class WorkbenchRuntimeCondition(StrictModel):
|
||||
id: NonEmptyStr
|
||||
task_id: Literal["backend-schedule"]
|
||||
session_id: NonEmptyStr
|
||||
title: NonEmptyStr
|
||||
summary: NonEmptyStr
|
||||
status: Literal["pending", "running", "completed", "warning", "error"]
|
||||
risk_level: Literal["normal", "attention", "critical"]
|
||||
scheduled_at: WorkbenchDateTime
|
||||
updated_at: WorkbenchDateTime
|
||||
detail: NonEmptyStr
|
||||
recommendation: NonEmptyStr | None
|
||||
evidence: list[NonEmptyStr]
|
||||
model_name: Literal["TJWater scheduler"]
|
||||
|
||||
|
||||
class WorkbenchAlertSource(StrictModel):
|
||||
task_type: Literal["scheduled_task", "analysis_task", "other_task"]
|
||||
task_id: NonEmptyStr
|
||||
run_id: NonEmptyStr
|
||||
|
||||
|
||||
class WorkbenchRuntimeAlert(StrictModel):
|
||||
id: NonEmptyStr
|
||||
condition_id: NonEmptyStr | None
|
||||
source: WorkbenchAlertSource
|
||||
title: NonEmptyStr
|
||||
description: NonEmptyStr
|
||||
occurred_at: WorkbenchDateTime
|
||||
severity: Literal["warning", "critical"]
|
||||
|
||||
|
||||
class WorkbenchPersistedAlert(StrictModel):
|
||||
alert_id: NonEmptyStr
|
||||
condition_id: NonEmptyStr | None
|
||||
source: WorkbenchAlertSource
|
||||
title: NonEmptyStr
|
||||
description: NonEmptyStr
|
||||
occurred_at: WorkbenchDateTime
|
||||
severity: Literal["warning", "critical"]
|
||||
status: Literal["active", "acknowledged", "resolved"]
|
||||
producer_id: NonEmptyStr | None = None
|
||||
updated_at: WorkbenchDateTime | None = None
|
||||
|
||||
|
||||
class WorkbenchRuntimeScenario(StrictModel):
|
||||
id: NonEmptyStr
|
||||
run_id: NonEmptyStr
|
||||
name: NonEmptyStr
|
||||
description: NonEmptyStr
|
||||
status: Literal["draft", "active", "review"]
|
||||
run_type: NonEmptyStr
|
||||
run_type_label: NonEmptyStr
|
||||
run_status: NonEmptyStr
|
||||
created_at: WorkbenchDateTime
|
||||
|
||||
|
||||
class WorkbenchRuntimeAnalysisFact(StrictModel):
|
||||
label: NonEmptyStr
|
||||
value: NonEmptyStr
|
||||
|
||||
|
||||
class WorkbenchRuntimeAnalysisResult(StrictModel):
|
||||
result_type: NonEmptyStr
|
||||
created_at: WorkbenchDateTime
|
||||
facts: list[WorkbenchRuntimeAnalysisFact]
|
||||
|
||||
|
||||
class WorkbenchRuntimeLatestAnalysis(StrictModel):
|
||||
run_id: NonEmptyStr
|
||||
name: NonEmptyStr
|
||||
run_type: NonEmptyStr
|
||||
run_type_label: NonEmptyStr
|
||||
status: NonEmptyStr
|
||||
status_label: NonEmptyStr
|
||||
created_by: NonEmptyStr
|
||||
created_at: WorkbenchDateTime
|
||||
started_at: WorkbenchDateTime
|
||||
results: list[WorkbenchRuntimeAnalysisResult]
|
||||
map_payload: dict[str, JsonValue] | None
|
||||
|
||||
|
||||
class WorkbenchAnalysisHistoryItem(StrictModel):
|
||||
run_id: NonEmptyStr
|
||||
name: NonEmptyStr
|
||||
run_type: NonEmptyStr
|
||||
run_type_label: NonEmptyStr
|
||||
status: NonEmptyStr
|
||||
status_label: NonEmptyStr
|
||||
created_by: NonEmptyStr
|
||||
created_at: WorkbenchDateTime
|
||||
started_at: WorkbenchDateTime
|
||||
has_results: StrictBool
|
||||
|
||||
|
||||
class WorkbenchAnalysisHistoryPage(StrictModel):
|
||||
items: list[WorkbenchAnalysisHistoryItem]
|
||||
limit: PositiveInt
|
||||
offset: NonNegativeInt
|
||||
next_offset: NonNegativeInt | None
|
||||
|
||||
|
||||
class WorkbenchRuntimeWorkOrderStage(StrictModel):
|
||||
id: NonEmptyStr
|
||||
sequence: PositiveInt
|
||||
title: NonEmptyStr
|
||||
status: Literal["done", "current", "pending", "blocked", "skipped"]
|
||||
owner: NonEmptyStr
|
||||
time_label: NonEmptyStr
|
||||
description: NonEmptyStr
|
||||
|
||||
|
||||
class WorkbenchRuntimeWorkOrderSubtask(StrictModel):
|
||||
id: NonEmptyStr
|
||||
title: NonEmptyStr
|
||||
summary: NonEmptyStr
|
||||
status: Literal["pending", "running", "blocked", "completed", "cancelled"]
|
||||
scheduled_at: WorkbenchDateTime
|
||||
location: NonEmptyStr
|
||||
assignee: NonEmptyStr
|
||||
duration_minutes: PositiveInt
|
||||
reply_window_minutes: PositiveInt
|
||||
stages: list[WorkbenchRuntimeWorkOrderStage]
|
||||
reply_requirements: list[NonEmptyStr]
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_stages(self):
|
||||
if not self.stages:
|
||||
raise ValueError("subtask stages must not be empty")
|
||||
if [stage.sequence for stage in self.stages] != list(
|
||||
range(1, len(self.stages) + 1)
|
||||
):
|
||||
raise ValueError("subtask stages must use contiguous sequence numbers")
|
||||
if len({stage.id for stage in self.stages}) != len(self.stages):
|
||||
raise ValueError("subtask stage ids must be unique")
|
||||
if sum(stage.status == "current" for stage in self.stages) > 1:
|
||||
raise ValueError("subtask stages may contain at most one current stage")
|
||||
return self
|
||||
|
||||
|
||||
class WorkbenchRuntimeWorkOrder(StrictModel):
|
||||
id: NonEmptyStr
|
||||
code: NonEmptyStr
|
||||
scheduled_at: WorkbenchDateTime
|
||||
title: NonEmptyStr
|
||||
summary: NonEmptyStr
|
||||
status: Literal["pending", "running", "awaiting_reply", "completed"]
|
||||
risk_level: Literal["normal", "attention", "critical"]
|
||||
updated_at: WorkbenchDateTime
|
||||
detail: NonEmptyStr
|
||||
recommendation: NonEmptyStr | None
|
||||
duration_minutes: PositiveInt
|
||||
source: NonEmptyStr
|
||||
dispatcher: NonEmptyStr
|
||||
priority: Literal["normal", "urgent"]
|
||||
subtasks: list[WorkbenchRuntimeWorkOrderSubtask]
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_subtasks(self):
|
||||
if not self.subtasks:
|
||||
raise ValueError("work order subtasks must not be empty")
|
||||
if len({subtask.id for subtask in self.subtasks}) != len(self.subtasks):
|
||||
raise ValueError("work order subtask ids must be unique")
|
||||
return self
|
||||
|
||||
|
||||
class WorkbenchRuntimeSnapshot(StrictModel):
|
||||
availability: ProjectionAvailability
|
||||
source: Literal["none", "scheduled_tasks"]
|
||||
scenario_availability: ProjectionAvailability
|
||||
artifact_availability: ProjectionAvailability
|
||||
schedule_availability: ProjectionAvailability
|
||||
work_order_availability: ProjectionAvailability
|
||||
alert_availability: ProjectionAvailability
|
||||
generated_at: WorkbenchDateTime
|
||||
conditions: list[WorkbenchRuntimeCondition]
|
||||
alerts: list[WorkbenchRuntimeAlert]
|
||||
scenarios: list[WorkbenchRuntimeScenario]
|
||||
latest_analysis: WorkbenchRuntimeLatestAnalysis | None
|
||||
work_orders: list[WorkbenchRuntimeWorkOrder]
|
||||
Reference in New Issue
Block a user