77d95fdcc4
#4 - api/routers/tasks.py: update_task, delete_task, create_subtask, update_subtask, delete_subtask controllavano solo is_current, non version_has_measurements: la versione con misure veniva modificata sul posto, cambiando i limiti di quote gia' giudicate pass/fail. Nuovi helper _task_for_write / _subtask_for_write riusano recipe_service.create_new_version (lo stesso copy-on-write di create_task): se la versione ha misure creano una nuova versione, ritrovano il task per order_index e la quota per marker_number (chiavi che _copy_tasks_to_version preserva), e la scrittura va sulla copia. La versione misurata resta intatta. Trovato e corretto nello stesso punto: il task ritrovato nella copia non ha .version precaricato (new_version.tasks viene da selectinload, che non popola il backref figlio->genitore), e TaskResponse.recipe_id legge task.version.recipe_id - senza assegnare esplicitamente candidate.version = new_version, update_task va in MissingGreenlet (lazy load fuori da un await) invece di rispondere 200. #26 - models/api/task.py: nuovo validate_tolerance_order (ltl<=lwl<=nominal<= uwl<=utl sui soli campi valorizzati), usato come model_validator su SubtaskCreate e richiamato dal router in update_subtask sui valori risultanti (esistenti + parziali) prima del flush, perche' SubtaskUpdate non ha un insieme fisso di campi da validare da solo. Test: src/backend/tests/test_tasks.py, TestCopyOnWriteOnEdit (5) e TestToleranceOrder (4); adattato test_update_subtask esistente (nominal 12.0 violava il nuovo ordine contro utl=10.5 invariato). PROVA: uv run --extra server --extra dev pytest src/backend/tests/test_tasks.py -q -> 16 passed Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
138 lines
4.7 KiB
Python
138 lines
4.7 KiB
Python
"""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)
|