Files
TieMeasureFlow/src/backend/models/api/task.py
T
Adriano 77d95fdcc4 fix(tasks): copy-on-write su modifica/cancellazione e ordine tolleranze (#4, #26)
#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>
2026-09-21 22:48:08 +02:00

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)