from datetime import datetime from enum import StrEnum from uuid import UUID from pydantic import BaseModel, Field, field_validator, model_validator class HistoryElementType(StrEnum): PIPE = "pipe" JUNCTION = "junction" class HistoryMode(StrEnum): OBSERVED = "observed" REALTIME_COMPARISON = "realtime_comparison" ANALYSIS_COMPARISON = "analysis_comparison" class HistoryMetric(StrEnum): FLOW = "flow" PRESSURE = "pressure" class HistorySource(StrEnum): SCADA_RAW = "scada_raw" SCADA_CLEANED = "scada_cleaned" REALTIME_SIMULATION = "realtime_simulation" ANALYSIS_SIMULATION = "analysis_simulation" class ElementHistoryTarget(BaseModel): element_id: str = Field(min_length=1, max_length=128) element_type: HistoryElementType device_ids: list[str] | None = Field(default=None, max_length=100) @field_validator("element_id") @classmethod def normalize_element_id(cls, value: str) -> str: return value.strip() @field_validator("device_ids") @classmethod def normalize_device_ids(cls, value: list[str] | None) -> list[str] | None: if value is None: return None normalized = list(dict.fromkeys(item.strip() for item in value if item.strip())) if not normalized: raise ValueError("device_ids must contain at least one device") return normalized class ElementHistoryQuery(BaseModel): start_time: datetime end_time: datetime mode: HistoryMode = HistoryMode.OBSERVED run_id: UUID | None = None elements: list[ElementHistoryTarget] = Field(min_length=1, max_length=200) @model_validator(mode="after") def validate_query(self) -> "ElementHistoryQuery": if self.start_time >= self.end_time: raise ValueError("start_time must be earlier than end_time") if self.mode == HistoryMode.ANALYSIS_COMPARISON and self.run_id is None: raise ValueError("run_id is required for analysis_comparison") if self.mode != HistoryMode.ANALYSIS_COMPARISON and self.run_id is not None: raise ValueError("run_id is only valid for analysis_comparison") return self class HistoryPoint(BaseModel): time: datetime value: float | None class ElementHistorySeries(BaseModel): element_id: str element_type: HistoryElementType device_id: str | None = None metric: HistoryMetric source: HistorySource source_unit: str = Field(description="Unit used by the returned point values") display_unit: str = Field(description="Recommended UI display unit") unit_inferred: bool = False points: list[HistoryPoint] class ElementHistoryResponse(BaseModel): series: list[ElementHistorySeries]