"""Pydantic schemas for Station and StationRecipeAssignment.""" from src.backend.models.api.tipi import UtcDateTime from typing import Optional from pydantic import BaseModel, ConfigDict, Field, field_validator def _normalize_http_url(value: Optional[str], field: str) -> Optional[str]: """Vuoto/spazi -> None; altrimenti http(s) senza '/' finale.""" if value is None: return None value = value.strip() if not value: return None if not value.lower().startswith(("http://", "https://")): raise ValueError(f"{field} must start with http:// or https://") return value.rstrip("/") def _normalize_worker_url(value: Optional[str]) -> Optional[str]: """Vuoto/spazi -> None (worker centrale); altrimenti http(s) senza '/' finale (#45).""" return _normalize_http_url(value, "vision_worker_url") def _normalize_camera_url(value: Optional[str]) -> Optional[str]: """Vuoto/spazi -> None (nessuna camera); altrimenti http(s) senza '/' finale (#72).""" return _normalize_http_url(value, "camera_url") def _normalize_camera_source(value: Optional[str]) -> Optional[str]: """Strip; vuoto -> None (#72).""" if value is None: return None return value.strip() or None class StationCreate(BaseModel): code: str = Field(..., min_length=1, max_length=100) name: str = Field(..., min_length=1, max_length=255) location: Optional[str] = Field(default=None, max_length=255) notes: Optional[str] = None active: bool = True # worker di visione della stazione (#45); vuoto = centrale vision_worker_url: Optional[str] = Field(default=None, max_length=500) # camera della stazione (#72); vuoto = nessuna camera camera_url: Optional[str] = Field(default=None, max_length=500) camera_source: Optional[str] = Field(default=None, max_length=200) _check_worker_url = field_validator("vision_worker_url")(_normalize_worker_url) _check_camera_url = field_validator("camera_url")(_normalize_camera_url) _check_camera_source = field_validator("camera_source")(_normalize_camera_source) class StationUpdate(BaseModel): name: Optional[str] = Field(default=None, min_length=1, max_length=255) location: Optional[str] = Field(default=None, max_length=255) notes: Optional[str] = None active: Optional[bool] = None # worker di visione della stazione (#45); "" -> None riporta al centrale vision_worker_url: Optional[str] = Field(default=None, max_length=500) # camera della stazione (#72); vuoto = nessuna camera camera_url: Optional[str] = Field(default=None, max_length=500) camera_source: Optional[str] = Field(default=None, max_length=200) _check_worker_url = field_validator("vision_worker_url")(_normalize_worker_url) _check_camera_url = field_validator("camera_url")(_normalize_camera_url) _check_camera_source = field_validator("camera_source")(_normalize_camera_source) class StationResponse(BaseModel): model_config = ConfigDict(from_attributes=True) id: int code: str name: str location: Optional[str] notes: Optional[str] active: bool vision_worker_url: Optional[str] = None # #45 camera_url: Optional[str] = None # #72 camera_source: Optional[str] = None # #72 created_by: int created_at: UtcDateTime class StationRecipeAssignmentCreate(BaseModel): recipe_id: int = Field(..., gt=0) class StationRecipeAssignmentResponse(BaseModel): model_config = ConfigDict(from_attributes=True) id: int station_id: int recipe_id: int assigned_by: int assigned_at: UtcDateTime class RecipeSummary(BaseModel): model_config = ConfigDict(from_attributes=True) id: int code: str name: str active: bool image_path: Optional[str] = None description: Optional[str] = None # The selection screen has to know before the operator starts: what a recipe # demands is the difference between an enabled Avvia and a disabled one. requires_lot: bool = False requires_serial: bool = False class StationWithRecipesResponse(StationResponse): recipes: list[RecipeSummary] = Field(default_factory=list) class StationResetResponse(BaseModel): """Outcome of clearing every recipe assignment of a station.""" station_id: int removed: int