"""Pydantic schemas for RecipeTask and RecipeSubtask operations.""" from typing import Any, Literal, Optional from pydantic import BaseModel, ConfigDict, Field, model_validator from src.backend.models.orm.task import TASK_TYPES # Kept in step with the ORM enum so the two cannot drift apart. TaskType = Literal[TASK_TYPES] def validate_tolerance_order( ltl: Optional[float], lwl: Optional[float], nominal: Optional[float], uwl: Optional[float], utl: Optional[float], ) -> None: """ltl <= lwl <= nominal <= uwl <= utl, checked only across the values set. A one-sided tolerance - only utl, say - is not an error: what is wrong is limits that invert the sequence they do share, like utl landing below ltl. Shared by SubtaskCreate's own validator and by the router's partial-update check (SubtaskUpdate has no fixed set of fields to compare). """ ordered = [ ("ltl", ltl), ("lwl", lwl), ("nominal", nominal), ("uwl", uwl), ("utl", utl), ] present = [(name, value) for name, value in ordered if value is not None] for (prev_name, prev), (name, value) in zip(present, present[1:]): if prev > value: raise ValueError( f"Tolerance limits out of order: {prev_name}={prev} > {name}={value}" ) class SubtaskCreate(BaseModel): """Schema for creating a subtask.""" marker_number: int = Field(..., ge=1) description: str = Field(..., min_length=1, max_length=500) measurement_type: Optional[str] = Field(None, max_length=100) nominal: Optional[float] = None utl: Optional[float] = None uwl: Optional[float] = None lwl: Optional[float] = None ltl: Optional[float] = None unit: str = Field("mm", max_length=20) image_path: Optional[str] = Field(None, max_length=500) vision_output: Optional[str] = None @model_validator(mode="after") def _check_tolerance_order(self) -> "SubtaskCreate": validate_tolerance_order(self.ltl, self.lwl, self.nominal, self.uwl, self.utl) return self class SubtaskUpdate(BaseModel): """Schema for updating a subtask.""" marker_number: Optional[int] = Field(None, ge=1) description: Optional[str] = Field(None, min_length=1, max_length=500) measurement_type: Optional[str] = Field(None, max_length=100) nominal: Optional[float] = None utl: Optional[float] = None uwl: Optional[float] = None lwl: Optional[float] = None ltl: Optional[float] = None unit: Optional[str] = Field(None, max_length=20) image_path: Optional[str] = Field(None, max_length=500) vision_output: Optional[str] = None class SubtaskResponse(BaseModel): """Schema for subtask response.""" model_config = ConfigDict(from_attributes=True) id: int task_id: int marker_number: int description: str measurement_type: Optional[str] = None nominal: Optional[float] = None utl: Optional[float] = None uwl: Optional[float] = None lwl: Optional[float] = None ltl: Optional[float] = None unit: str image_path: Optional[str] = None vision_output: Optional[str] = None class TaskCreate(BaseModel): """Schema for creating a task.""" title: str = Field(..., min_length=1, max_length=255) task_type: TaskType = "note" directive: Optional[str] = None description: Optional[str] = None file_path: Optional[str] = Field(None, max_length=500) file_type: Optional[str] = Field(None, pattern="^(image|pdf)$") annotations_json: Optional[dict[str, Any]] = None vision_json: Optional[dict] = None subtasks: list[SubtaskCreate] = [] class TaskUpdate(BaseModel): """Schema for updating a task.""" title: Optional[str] = Field(None, min_length=1, max_length=255) task_type: Optional[TaskType] = None directive: Optional[str] = None description: Optional[str] = None file_path: Optional[str] = Field(None, max_length=500) file_type: Optional[str] = Field(None, pattern="^(image|pdf)$") annotations_json: Optional[dict[str, Any]] = None vision_json: Optional[dict] = None class TaskResponse(BaseModel): """Schema for task response.""" model_config = ConfigDict(from_attributes=True) id: int version_id: int recipe_id: Optional[int] = None order_index: int task_type: str title: str directive: Optional[str] = None description: Optional[str] = None file_path: Optional[str] = None file_type: Optional[str] = None annotations_json: Optional[dict[str, Any]] = None vision_json: Optional[dict] = None subtasks: list[SubtaskResponse] = [] class TaskReorderRequest(BaseModel): """Schema for reordering tasks.""" task_ids: list[int] = Field(..., min_length=1)