from datetime import datetime from uuid import UUID from pydantic import BaseModel, ConfigDict, Field, model_validator class InitializeRequest(BaseModel): username: str = Field(min_length=3, max_length=64, pattern=r"^[A-Za-z0-9_.-]+$") password: str = Field(min_length=12, max_length=256) class LoginRequest(BaseModel): username: str password: str class UserOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID username: str class SessionOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID current: bool created_at: datetime last_seen_at: datetime expires_at: datetime ip_address: str | None user_agent: str | None class NameUpdate(BaseModel): name: str = Field(min_length=1, max_length=120) class FolderCreate(NameUpdate): pass class FolderOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID name: str class ListCreate(BaseModel): name: str = Field(min_length=1, max_length=120) folder_id: UUID | None = None class ListOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID folder_id: UUID | None name: str is_inbox: bool class TagCreate(BaseModel): name: str = Field(min_length=1, max_length=80) color: str = Field(default="#f15a29", min_length=1, max_length=32) class TagOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID name: str color: str class TaskCreate(BaseModel): title: str = Field(min_length=1, max_length=500) list_id: UUID description: str = "" priority: int = Field(default=0, ge=0, le=3) due_at: datetime | None = None parent_id: UUID | None = None tag_ids: list[UUID] = Field(default_factory=list) class TaskUpdate(BaseModel): title: str | None = Field(default=None, min_length=1, max_length=500) description: str | None = None priority: int | None = Field(default=None, ge=0, le=3) due_at: datetime | None = None completed: bool | None = None list_id: UUID | None = None tag_ids: list[UUID] | None = None version: int = Field(ge=1) @model_validator(mode="after") def reject_null_non_nullable_fields(self): for field in ("title", "description", "priority", "completed", "list_id"): if field in self.model_fields_set and getattr(self, field) is None: raise ValueError(f"{field} cannot be null") return self class TaskOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID list_id: UUID parent_id: UUID | None title: str description: str priority: int completed: bool due_at: datetime | None version: int class TaskDetailOut(TaskOut): tags: list[TagOut] = Field(default_factory=list) subtasks: list[TaskOut] = Field(default_factory=list) class TaskPage(BaseModel): items: list[TaskDetailOut] next_cursor: str | None = None total: int = 0 page: int = 1 page_size: int = 50 class BatchTaskUpdate(BaseModel): task_ids: list[UUID] = Field(min_length=1) completed: bool | None = None list_id: UUID | None = None due_at: datetime | None = None tag_ids: list[UUID] | None = None soft_delete: bool | None = None @model_validator(mode="after") def require_operation(self): operations = {"completed", "list_id", "due_at", "tag_ids", "soft_delete"} if not self.model_fields_set.intersection(operations): raise ValueError("at least one batch operation is required") if self.soft_delete is False: raise ValueError("soft_delete can only be true") return self class BatchResult(BaseModel): updated: int