"""Fractafrag — Pydantic Request/Response Schemas.""" from __future__ import annotations from datetime import datetime from uuid import UUID from typing import Optional from pydantic import BaseModel, EmailStr, Field, ConfigDict # ════════════════════════════════════════════════════════════ # AUTH # ════════════════════════════════════════════════════════════ class UserRegister(BaseModel): username: str = Field(..., min_length=3, max_length=30, pattern=r"^[a-zA-Z0-9_-]+$") email: EmailStr password: str = Field(..., min_length=8, max_length=128) turnstile_token: str class UserLogin(BaseModel): email: EmailStr password: str turnstile_token: str class TokenResponse(BaseModel): access_token: str token_type: str = "bearer" class UserPublic(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID username: str role: str subscription_tier: str is_verified_creator: bool created_at: datetime class UserMe(UserPublic): email: str ai_credits_remaining: int trust_tier: str last_active_at: Optional[datetime] = None # ════════════════════════════════════════════════════════════ # SHADERS # ════════════════════════════════════════════════════════════ class ShaderCreate(BaseModel): title: str = Field(..., min_length=1, max_length=120) description: Optional[str] = Field(None, max_length=1000) glsl_code: str = Field(..., min_length=10) tags: list[str] = Field(default_factory=list, max_length=10) shader_type: str = Field(default="2d", pattern=r"^(2d|3d|audio-reactive)$") is_public: bool = True style_metadata: Optional[dict] = None fulfills_desire_id: Optional[UUID] = None class ShaderUpdate(BaseModel): title: Optional[str] = Field(None, min_length=1, max_length=120) description: Optional[str] = Field(None, max_length=1000) glsl_code: Optional[str] = Field(None, min_length=10) tags: Optional[list[str]] = None is_public: Optional[bool] = None class ShaderPublic(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID author_id: Optional[UUID] title: str description: Optional[str] glsl_code: str is_public: bool is_ai_generated: bool ai_provider: Optional[str] thumbnail_url: Optional[str] preview_url: Optional[str] render_status: str style_metadata: Optional[dict] tags: list[str] shader_type: str forked_from: Optional[UUID] view_count: int score: float created_at: datetime updated_at: datetime class ShaderFeedItem(BaseModel): """Lighter shader representation for feed responses.""" model_config = ConfigDict(from_attributes=True) id: UUID author_id: Optional[UUID] title: str thumbnail_url: Optional[str] preview_url: Optional[str] glsl_code: str tags: list[str] shader_type: str score: float view_count: int is_ai_generated: bool style_metadata: Optional[dict] created_at: datetime # ════════════════════════════════════════════════════════════ # VOTES & ENGAGEMENT # ════════════════════════════════════════════════════════════ class VoteCreate(BaseModel): value: int = Field(..., ge=-1, le=1) class DwellReport(BaseModel): shader_id: UUID dwell_secs: float = Field(..., gt=0) replayed: bool = False session_id: Optional[str] = None # ════════════════════════════════════════════════════════════ # DESIRES / BOUNTIES # ════════════════════════════════════════════════════════════ class DesireCreate(BaseModel): prompt_text: str = Field(..., min_length=5, max_length=500) style_hints: Optional[dict] = None class DesirePublic(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID author_id: Optional[UUID] prompt_text: str style_hints: Optional[dict] tip_amount_cents: int status: str heat_score: float fulfilled_by_shader: Optional[UUID] fulfilled_at: Optional[datetime] created_at: datetime # ════════════════════════════════════════════════════════════ # AI GENERATION # ════════════════════════════════════════════════════════════ class GenerateRequest(BaseModel): prompt: str = Field(..., min_length=5, max_length=500) provider: Optional[str] = None # anthropic, openai, ollama — auto-selected if None style_metadata: Optional[dict] = None class GenerateStatusResponse(BaseModel): job_id: str status: str # queued, generating, rendering, complete, failed shader_id: Optional[UUID] = None error: Optional[str] = None # ════════════════════════════════════════════════════════════ # API KEYS # ════════════════════════════════════════════════════════════ class ApiKeyCreate(BaseModel): name: str = Field(..., min_length=1, max_length=100) class ApiKeyPublic(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID key_prefix: str name: Optional[str] trust_tier: str rate_limit_per_hour: int last_used_at: Optional[datetime] created_at: datetime class ApiKeyCreated(ApiKeyPublic): """Returned only on creation — includes the full key (shown once).""" full_key: str # ════════════════════════════════════════════════════════════ # PAGINATION # ════════════════════════════════════════════════════════════ class PaginatedResponse(BaseModel): items: list cursor: Optional[str] = None has_more: bool = False