# app/schemas/syndarix/project.py """ Pydantic schemas for Project entity. """ import re from datetime import datetime from typing import Any from uuid import UUID from pydantic import BaseModel, ConfigDict, Field, field_validator from .enums import AutonomyLevel, ProjectStatus class ProjectBase(BaseModel): """Base project schema with common fields.""" name: str = Field(..., min_length=1, max_length=255) slug: str | None = Field(None, min_length=1, max_length=255) description: str | None = None autonomy_level: AutonomyLevel = AutonomyLevel.MILESTONE status: ProjectStatus = ProjectStatus.ACTIVE settings: dict[str, Any] = Field(default_factory=dict) @field_validator("slug") @classmethod def validate_slug(cls, v: str | None) -> str | None: """Validate slug format: lowercase, alphanumeric, hyphens only.""" if v is None: return v if not re.match(r"^[a-z0-9-]+$", v): raise ValueError( "Slug must contain only lowercase letters, numbers, and hyphens" ) if v.startswith("-") or v.endswith("-"): raise ValueError("Slug cannot start or end with a hyphen") if "--" in v: raise ValueError("Slug cannot contain consecutive hyphens") return v @field_validator("name") @classmethod def validate_name(cls, v: str) -> str: """Validate project name.""" if not v or v.strip() == "": raise ValueError("Project name cannot be empty") return v.strip() class ProjectCreate(ProjectBase): """Schema for creating a new project.""" name: str = Field(..., min_length=1, max_length=255) slug: str = Field(..., min_length=1, max_length=255) owner_id: UUID | None = None class ProjectUpdate(BaseModel): """Schema for updating a project. Note: owner_id is intentionally excluded to prevent IDOR vulnerabilities. Project ownership transfer should be done via a dedicated endpoint with proper authorization checks. """ name: str | None = Field(None, min_length=1, max_length=255) slug: str | None = Field(None, min_length=1, max_length=255) description: str | None = None autonomy_level: AutonomyLevel | None = None status: ProjectStatus | None = None settings: dict[str, Any] | None = None @field_validator("slug") @classmethod def validate_slug(cls, v: str | None) -> str | None: """Validate slug format.""" if v is None: return v if not re.match(r"^[a-z0-9-]+$", v): raise ValueError( "Slug must contain only lowercase letters, numbers, and hyphens" ) if v.startswith("-") or v.endswith("-"): raise ValueError("Slug cannot start or end with a hyphen") if "--" in v: raise ValueError("Slug cannot contain consecutive hyphens") return v @field_validator("name") @classmethod def validate_name(cls, v: str | None) -> str | None: """Validate project name.""" if v is not None and (not v or v.strip() == ""): raise ValueError("Project name cannot be empty") return v.strip() if v else v class ProjectInDB(ProjectBase): """Schema for project in database.""" id: UUID owner_id: UUID | None = None created_at: datetime updated_at: datetime model_config = ConfigDict(from_attributes=True) class ProjectResponse(ProjectBase): """Schema for project API responses.""" id: UUID owner_id: UUID | None = None created_at: datetime updated_at: datetime agent_count: int | None = 0 issue_count: int | None = 0 active_sprint_name: str | None = None model_config = ConfigDict(from_attributes=True) class ProjectListResponse(BaseModel): """Schema for paginated project list responses.""" projects: list[ProjectResponse] total: int page: int page_size: int pages: int