""" Pydantic models for Range PromQL Query tool input/output validation. These models define the structure and validation rules for the query_range MCP tool. """ from datetime import datetime from typing import List from pydantic import BaseModel, Field, field_validator, model_validator class RangeQueryRequest(BaseModel): """ Input model for range query requests. Validates PromQL query string, RFC3339 timestamps, and duration step format. Ensures start timestamp is before or equal to end timestamp. Example: ```python request = RangeQueryRequest( query="http_requests_total{method='GET'}", start="2024-01-01T00:00:00Z", end="2024-01-01T23:59:59Z", step="15s" ) ``` """ query: str = Field( ..., description="PromQL query string to execute", min_length=1, examples=['http_requests_total{method="GET"}', "rate(cpu_usage[5m])"], ) start: str = Field( ..., description="Start timestamp in RFC3339 format", examples=["2024-01-01T00:00:00Z", "2024-01-01T12:30:45+00:00"], ) end: str = Field( ..., description="End timestamp in RFC3339 format", examples=["2024-01-01T23:59:59Z", "2024-01-02T00:00:00+00:00"], ) step: str = Field( ..., description="Duration step (e.g., '15s', '1m', '1h')", pattern=r"^[0-9]+(s|m|h|d|w|y)$", examples=["15s", "1m", "5m", "1h"], ) cluster_id: str = Field( ..., description="Cluster identifier", ) env: str = Field( ..., description="Environment name used in biz_type resolution (e.g. prod, staging, dev)", ) deploy_type: str = Field( ..., description=( "Cluster deploy type used in biz_type resolution " "(dedicated, premium, resource_pool, starter, essential)" ), ) @field_validator("start", "end") @classmethod def validate_timestamp(cls, v: str) -> str: """Validate RFC3339 timestamp format.""" try: # Try parsing as RFC3339 datetime.fromisoformat(v.replace("Z", "+00:00")) return v except ValueError as e: raise ValueError(f"Invalid timestamp format: {v}. Must be RFC3339.") from e @field_validator("env", "deploy_type", mode="before") @classmethod def normalize_scope_text(cls, v: str) -> str: return v.strip().lower() @field_validator("cluster_id", mode="before") @classmethod def normalize_cluster_id(cls, v: object) -> str: if not isinstance(v, str): raise ValueError("cluster_id must be a string. Quote numeric IDs explicitly.") return str(v).strip() @model_validator(mode="after") def validate_time_range(self) -> "RangeQueryRequest": """Validate that start time is before or equal to end time.""" try: start_dt = datetime.fromisoformat(self.start.replace("Z", "+00:00")) end_dt = datetime.fromisoformat(self.end.replace("Z", "+00:00")) if start_dt > end_dt: raise ValueError("Start timestamp must be before or equal to end timestamp") return self except ValueError as e: if "Start timestamp" in str(e): raise # If timestamp parsing failed, let field_validator handle it return self class DataPoint(BaseModel): """A single data point in a time-series.""" timestamp: float = Field(..., description="Unix timestamp in seconds") value: str = Field(..., description="Metric value as string (Prometheus float64)") class Statistics(BaseModel): """ Statistical summary for a time-series. Contains minimum, maximum, and average values calculated from time-series data points. Values may be NaN if all data points are null/empty. """ min: float = Field(..., description="Minimum value in the series (NaN if all values null)") max: float = Field(..., description="Maximum value in the series (NaN if all values null)") avg: float = Field(..., description="Average value in the series (NaN if all values null)") class ProcessedTimeSeries(BaseModel): """ Processed time-series with statistics and trend representation. Contains metric labels (with sensitive values redacted), statistical summary, sampled trend points, and truncation flag. """ labels: dict[str, str] = Field(..., description="Metric labels (sensitive values redacted)") statistics: Statistics = Field(..., description="Statistical summary") trend: List[DataPoint] = Field( ..., description="Sampled data points for trend representation (max 20)", max_length=20 ) truncated: bool = Field( False, description="True if series exceeded 1000 points and was truncated" )