Files
minerops/backend/app/monitoring/models.py
2026-08-19 20:00:52 +00:00

99 lines
2.7 KiB
Python

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