"""Pydantic request/response models for accounts, subscriptions, notifications. The User* schemas are fastapi-users' base schemas extended with display_name; the Subscription*/Notification* models are the app's first request bodies and shape the JSON the frontend sends and receives. """ import uuid from typing import Literal from fastapi_users import schemas from pydantic import BaseModel, Field, field_validator import grading # Canonical metric keys a subscription may watch (kept in lockstep with grading). ALLOWED_METRICS = tuple(grading.CLIMO_METRICS) DEFAULT_METRICS = ["tmax", "feels", "precip"] # --- users (fastapi-users) --------------------------------------------------- class UserRead(schemas.BaseUser[uuid.UUID]): display_name: str | None = None class UserCreate(schemas.BaseUserCreate): display_name: str | None = None class UserUpdate(schemas.BaseUserUpdate): display_name: str | None = None # --- subscriptions ----------------------------------------------------------- def _check_metrics(v: list[str]) -> list[str]: if not v: raise ValueError("pick at least one metric") bad = [m for m in v if m not in ALLOWED_METRICS] if bad: raise ValueError(f"unknown metric(s): {', '.join(bad)}") # de-dupe, preserve order seen, out = set(), [] for m in v: if m not in seen: seen.add(m) out.append(m) return out class SubscriptionIn(BaseModel): lat: float = Field(ge=-90, le=90) lon: float = Field(ge=-180, le=180) label: str | None = None threshold: int = Field(ge=95, le=99) metrics: list[str] = Field(default_factory=lambda: list(DEFAULT_METRICS)) kind: Literal["observed", "forecast"] = "observed" two_sided: bool = True @field_validator("metrics") @classmethod def _metrics(cls, v): return _check_metrics(v) class SubscriptionPatch(BaseModel): threshold: int | None = Field(default=None, ge=95, le=99) metrics: list[str] | None = None two_sided: bool | None = None active: bool | None = None @field_validator("metrics") @classmethod def _metrics(cls, v): return _check_metrics(v) if v is not None else v class SubscriptionOut(BaseModel): id: int cell_id: str label: str | None lat: float lon: float threshold: int metrics: list[str] kind: str two_sided: bool active: bool last_notified_at: float | None created_at: float model_config = {"from_attributes": True} # --- notifications ----------------------------------------------------------- class NotificationOut(BaseModel): id: int subscription_id: int event_date: str metric: str direction: str kind: str percentile: float value: float | None grade: str | None title: str body: str | None created_at: float read_at: float | None model_config = {"from_attributes": True} class NotificationList(BaseModel): notifications: list[NotificationOut] unread_count: int