fractafrag/services/api/app/schemas/schemas.py
John Lightner 5936ab167e feat(M001): Desire Economy
Completed slices:
- S01: Desire Embedding & Clustering
- S02: Fulfillment Flow & Frontend

Branch: milestone/M001
2026-03-25 02:22:50 -05:00

241 lines
8 KiB
Python

"""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
is_system: bool
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
class UserUpdate(BaseModel):
username: Optional[str] = Field(None, min_length=3, max_length=30, pattern=r"^[a-zA-Z0-9_-]+$")
email: Optional[EmailStr] = None
class ByokKeysUpdate(BaseModel):
anthropic_key: Optional[str] = Field(None, description="Anthropic API key")
openai_key: Optional[str] = Field(None, description="OpenAI API key")
ollama_endpoint: Optional[str] = Field(None, description="Ollama endpoint URL")
# ════════════════════════════════════════════════════════════
# 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
status: str = Field(default="published", pattern=r"^(draft|published)$")
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
status: Optional[str] = Field(None, pattern=r"^(draft|published|archived)$")
change_note: Optional[str] = Field(None, max_length=200)
class ShaderPublic(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: UUID
author_id: Optional[UUID]
title: str
description: Optional[str]
glsl_code: str
status: str
is_public: bool
is_ai_generated: bool
is_system: bool
ai_provider: Optional[str]
system_label: 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]
current_version: int
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
is_system: bool
system_label: Optional[str]
style_metadata: Optional[dict]
created_at: datetime
class ShaderVersionPublic(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: UUID
shader_id: UUID
version_number: int
glsl_code: str
title: str
description: Optional[str]
tags: list[str]
style_metadata: Optional[dict]
change_note: Optional[str]
thumbnail_url: Optional[str]
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
cluster_count: int = 0
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
style_metadata: Optional[dict] = None
class GenerateStatusResponse(BaseModel):
job_id: str
status: str
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):
full_key: str
# ════════════════════════════════════════════════════════════
# PAGINATION
# ════════════════════════════════════════════════════════════
class PaginatedResponse(BaseModel):
items: list
cursor: Optional[str] = None
has_more: bool = False