"""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]