"""Pydantic schemas for production runs and their events.""" from datetime import datetime from typing import Literal, Optional from pydantic import BaseModel, ConfigDict, Field class ProductionRunCreate(BaseModel): station_code: str = Field(..., min_length=1, max_length=100) recipe_id: int = Field(..., gt=0) version_id: Optional[int] = Field(default=None, gt=0) lot_number: Optional[str] = Field(default=None, max_length=100) serial_number: Optional[str] = Field(default=None, max_length=100) class ProductionEventResponse(BaseModel): model_config = ConfigDict(from_attributes=True) id: int event_type: str user_id: int supervisor_id: Optional[int] task_id: Optional[int] = None note: Optional[str] created_at: datetime class ProductionRunResponse(BaseModel): model_config = ConfigDict(from_attributes=True) id: int station_id: int recipe_id: int version_id: int operator_id: int lot_number: Optional[str] serial_number: Optional[str] status: str measurement_interval_minutes: Optional[int] next_measurement_at: Optional[datetime] cycle_count: int started_at: datetime paused_at: Optional[datetime] closed_at: Optional[datetime] closed_by: Optional[int] statistics_path: Optional[str] = None # Derived server-side so every client agrees on the countdown regardless of # clock skew. Negative once the interval has elapsed: how long the run has been # overdue is a fact the operator must see, not deduce. seconds_to_next_measurement: Optional[int] = None overdue: bool = False server_time: Optional[datetime] = None # The measurement tasks of this run's version, in execution order. The client # needs them to bring the operator back to the measurement when the interval # expires, whichever screen they are on, and to know which task closes a cycle. measurement_task_ids: list[int] = Field(default_factory=list) class ProductionRunWithEventsResponse(ProductionRunResponse): events: list[ProductionEventResponse] = Field(default_factory=list) class SupervisorAction(BaseModel): """Credentials of the supervisor authorising a line stop or a close.""" supervisor_username: str = Field(..., min_length=1) supervisor_password: str = Field(..., min_length=1) note: Optional[str] = None class CycleCompletePayload(BaseModel): # Which measurement task was just finished. The interval restarts only on the # last one of the recipe; omitting it closes the cycle outright. task_id: Optional[int] = Field(default=None, gt=0) note: Optional[str] = None class RemeasurePayload(BaseModel): """The piece was turned over: measure it again inside the same cycle.""" task_id: Optional[int] = Field(default=None, gt=0) note: Optional[str] = None ProductionEventType = Literal[ "start", "cycle_completed", "task_measured", "remeasure", "line_stop", "resume", "close", ]