"""FlowDeck — Pydantic request models for API validation.""" from __future__ import annotations from typing import Optional from fastapi import UploadFile from pydantic import BaseModel, Field, model_validator from app.middleware.security import ALLOWED_EXTENSIONS, MAX_UPLOAD_SIZE, _ext # ── File Save ──────────────────────────────────────────────── class FileSaveRequest(BaseModel): """Request model for saving/updating a file via Gitea.""" path: str = Field(..., min_length=1, description="File path in the repository") content: str = Field(..., description="File content (UTF-8 encoded)") message: str = Field(default="Update via FlowDeck", description="Commit message") sha: Optional[str] = Field(default=None, description="SHA of the file being updated (required for updates)") @model_validator(mode="after") def validate_path_extension(self): ext = _ext(self.path) if ext and ext not in ALLOWED_EXTENSIONS: raise ValueError(f"File extension '{ext}' is not allowed") return self # ── Upload ─────────────────────────────────────────────────── class UploadValidationResult(BaseModel): """Result of validating an upload.""" filename: str size: int extension: str valid: bool error: Optional[str] = None def validate_upload_request(file: UploadFile) -> Optional[str]: """Validate an uploaded file (size + extension). Returns error message or None.""" # Size check — we can't read the full file without a size attribute, # but Starlette's UploadFile has a size property from Content-Length if hasattr(file, 'size') and file.size is not None and file.size > MAX_UPLOAD_SIZE: return f"File '{file.filename}' exceeds maximum size of 10 MB" # Extension check if file.filename: ext = _ext(file.filename) if ext and ext not in ALLOWED_EXTENSIONS: return f"File extension '{ext}' is not allowed" return None # ── Issue Create / Update ──────────────────────────────────── class IssueCreateRequest(BaseModel): """Request model for creating a Gitea issue.""" title: str = Field(..., min_length=1, max_length=500) body: str = Field(default="") labels: str = Field(default="", description="Comma-separated label IDs") milestone: str = Field(default="", description="Milestone ID") assignee: str = Field(default="") class IssueUpdateRequest(BaseModel): """Request model for updating a Gitea issue (partial update).""" title: Optional[str] = Field(default=None, max_length=500) body: Optional[str] = Field(default=None) state: Optional[str] = Field(default=None, pattern=r"^(open|closed)$") labels: Optional[str] = Field(default=None, description="Comma-separated label IDs") milestone: Optional[str] = Field(default=None) assignee: Optional[str] = Field(default=None) # ── Card Move ──────────────────────────────────────────────── class CardMoveRequest(BaseModel): """Request model for moving a card between columns.""" owner: str = Field(..., min_length=1) repo: str = Field(..., min_length=1) issue_id: int = Field(..., gt=0) column: str = Field(..., min_length=1) # ── Column Mapping ─────────────────────────────────────────── class ColMappingRequest(BaseModel): """Request model for column-to-label mapping.""" owner: str = Field(..., min_length=1) repo: str = Field(..., min_length=1) column: str = Field(..., min_length=1) gitea_label: str = Field(..., min_length=1) close_issue: bool = Field(default=False)