"""Pydantic schemas for request/response validation. All models are plain Pydantic BaseModel (not SQLModel) so they can be used purely for API I/O without DB-session side-effects. """ from __future__ import annotations from datetime import datetime from typing import Any, Generic, List, Optional, TypeVar from uuid import UUID from pydantic import BaseModel, Field # ────────────────────────────────────────────────────────────────────── # User # ────────────────────────────────────────────────────────────────────── class UserCreate(BaseModel): email: str password: str = Field(min_length=8) username: str = Field(min_length=3, max_length=50) first_name: str last_name: str phone: Optional[str] = None class UserLogin(BaseModel): email: str password: str class UserResponse(BaseModel): id: UUID email: str username: str first_name: str last_name: str phone: Optional[str] = None role: str avatar_url: Optional[str] = None is_active: bool created_at: datetime model_config = {"from_attributes": True} # ────────────────────────────────────────────────────────────────────── # Auth / Token # ────────────────────────────────────────────────────────────────────── class TokenResponse(BaseModel): access_token: str refresh_token: str token_type: str = "bearer" # ────────────────────────────────────────────────────────────────────── # Sensor # ────────────────────────────────────────────────────────────────────── class SensorResponse(BaseModel): id: UUID name: str type: str status: str latitude: float longitude: float zone_id: Optional[UUID] = None last_value: Optional[float] = None last_reading_at: Optional[datetime] = None battery_level: Optional[int] = None model_config = {"from_attributes": True} class SensorReadingResponse(BaseModel): id: UUID sensor_id: UUID value: float unit: str quality: float recorded_at: datetime model_config = {"from_attributes": True} # ────────────────────────────────────────────────────────────────────── # Zone # ────────────────────────────────────────────────────────────────────── class ZoneResponse(BaseModel): id: UUID name: str description: Optional[str] = None color: str geojson: Optional[str] = None model_config = {"from_attributes": True} # ────────────────────────────────────────────────────────────────────── # Alert # ────────────────────────────────────────────────────────────────────── class AlertResponse(BaseModel): id: UUID sensor_id: UUID type: str severity: str message: str value: Optional[float] = None threshold: Optional[float] = None status: str created_at: datetime model_config = {"from_attributes": True} # ────────────────────────────────────────────────────────────────────── # Dashboard # ────────────────────────────────────────────────────────────────────── class DashboardStats(BaseModel): total_sensors: int active_sensors: int total_readings_24h: int active_alerts: int avg_air_quality: Optional[float] = None avg_temperature: Optional[float] = None avg_humidity: Optional[float] = None # ────────────────────────────────────────────────────────────────────── # Pagination (generic) # ────────────────────────────────────────────────────────────────────── T = TypeVar("T") class PaginatedResponse(BaseModel, Generic[T]): items: List[T] total: int page: int page_size: int pages: int