from __future__ import annotations from datetime import datetime from typing import Literal from uuid import UUID from pydantic import BaseModel, ConfigDict, Field, computed_field, field_validator TaskStatus = Literal["backlog", "in_progress", "done"] class ErrorDTO(BaseModel): error: str = Field(..., examples=["invalid_id"]) message: str = Field(..., examples=["Task was not found"]) class TaskCreateDTO(BaseModel): model_config = ConfigDict(extra="forbid") title: str = Field( ..., min_length=1, max_length=100, description="Task title", examples=["Implement CSV export"], ) @field_validator("title") @classmethod def validate_title(cls, value: str) -> str: value = value.strip() if not value: raise ValueError("Title must not be empty") return value class TaskUpdateDTO(BaseModel): model_config = ConfigDict(extra="forbid") title: str | None = Field( default=None, min_length=1, max_length=100, description="Updated task title", examples=["Fix workflow metrics"], ) @field_validator("title") @classmethod def validate_title(cls, value: str | None) -> str | None: if value is None: return None value = value.strip() if not value: raise ValueError("Title must not be empty") return value class TaskDTO(BaseModel): model_config = ConfigDict( extra="forbid", frozen=True, populate_by_name=True, ) id: UUID title: str = Field(..., min_length=1, max_length=100) created_at: datetime started_at: datetime | None = None done_at: datetime | None = None @computed_field(return_type=str) @property def status(self) -> TaskStatus: if self.done_at is not None: return "done" if self.started_at is not None: return "in_progress" return "backlog" class TaskListItemDTO(BaseModel): model_config = ConfigDict( extra="forbid", frozen=True, populate_by_name=True, ) id: UUID title: str = Field(..., min_length=1, max_length=100) created_at: datetime started_at: datetime | None = None done_at: datetime | None = None @computed_field(return_type=str) @property def status(self) -> TaskStatus: if self.done_at is not None: return "done" if self.started_at is not None: return "in_progress" return "backlog" class TaskListDTO(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) items: list[TaskListItemDTO] total: int = Field(..., ge=0) limit: int = Field(..., ge=1, le=1000) offset: int = Field(..., ge=0) class TaskQueryDTO(BaseModel): model_config = ConfigDict(extra="forbid") limit: int = Field(default=100, ge=1, le=1000) offset: int = Field(default=0, ge=0) status: TaskStatus | None = Field(default=None) search: str | None = Field(default=None, max_length=100) @field_validator("search") @classmethod def normalize_search(cls, value: str | None) -> str | None: if value is None: return None value = value.strip() return value or None class TaskExportRowDTO(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) id: UUID title: str status: TaskStatus created_at: datetime started_at: datetime | None = None done_at: datetime | None = None class TaskBoardItemDTO(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) id: UUID title: str = Field(..., min_length=1, max_length=100) status: TaskStatus display_date_label: str = Field(..., min_length=1, max_length=20) display_date_value: datetime | None = None class TaskBoardDTO(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) backlog_tasks: list[TaskBoardItemDTO] in_progress_tasks: list[TaskBoardItemDTO] done_tasks: list[TaskBoardItemDTO] class TaskDetailsDTO(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) id: UUID title: str = Field(..., min_length=1, max_length=100) status: TaskStatus created_at: datetime started_at: datetime | None = None done_at: datetime | None = None cycle_time: str