"""Typed contracts for bounded historical telemetry trends.""" from datetime import datetime from typing import Literal from uuid import UUID from pydantic import BaseModel, ConfigDict, Field, model_validator TrendScopeType = Literal["fleet", "site", "room", "miner"] TrendRange = Literal["6h", "24h", "7d"] TrendMetric = Literal["hashrate", "power"] TrendAttribution = Literal["current_placement_cohort", "miner_identity", "fleet"] class MonitoringTrendQuery(BaseModel): model_config = ConfigDict(populate_by_name=True) scope: TrendScopeType = "fleet" scope_id: UUID | None = None time_range: TrendRange = Field(default="24h", alias="range") @model_validator(mode="after") def validate_scope_identity(self) -> "MonitoringTrendQuery": if self.scope == "fleet" and self.scope_id is not None: raise ValueError("scope_id is not allowed for fleet scope") if self.scope != "fleet" and self.scope_id is None: raise ValueError("scope_id is required for this scope") return self class MonitoringScope(BaseModel): type: TrendScopeType id: UUID | None = None label: str class MonitoringSiteOption(BaseModel): id: UUID name: str class MonitoringRoomOption(BaseModel): id: UUID site_id: UUID name: str class MonitoringAvailableScopes(BaseModel): fleet: bool = True sites: list[MonitoringSiteOption] = Field(default_factory=list) rooms: list[MonitoringRoomOption] = Field(default_factory=list) class MonitoringTrendPoint(BaseModel): bucket_start: datetime bucket_end: datetime value: float | None = None observation_count: int metric_sample_count: int observed_miner_count: int contributing_miner_count: int scoped_miner_count: int incomplete: bool = False class MonitoringTrendSeries(BaseModel): metric: TrendMetric unit: Literal["TH/s", "W"] label: str points: list[MonitoringTrendPoint] class MonitoringTrendData(BaseModel): scope: MonitoringScope series: list[MonitoringTrendSeries] available_scopes: MonitoringAvailableScopes class MonitoringTrendMeta(BaseModel): generated_at: datetime latest_observed_at: datetime | None = None source: Literal["raw_snapshots"] = "raw_snapshots" attribution: TrendAttribution time_range: TrendRange = Field(alias="range") bucket_seconds: int maximum_points: int = 200 completeness: Literal["not_guaranteed"] = "not_guaranteed" current_bucket_incomplete: bool database_query_count: int = Field(ge=1, le=2) model_config = ConfigDict(populate_by_name=True) class MonitoringTrendResponse(BaseModel): data: MonitoringTrendData meta: MonitoringTrendMeta error: None = None