fractafrag/services/api/app/schemas/schemas.py
John Lightner 05d39fdda8 M0: Foundation scaffold — Docker Compose, DB schema, FastAPI app, all service stubs
Track A (Infrastructure & Data Layer):
- docker-compose.yml with all 7 services (nginx, frontend, api, mcp, renderer, worker, postgres, redis)
- docker-compose.override.yml for local dev (hot reload, port exposure)
- PostgreSQL init.sql with full schema (15 tables, pgvector indexes, creator economy stubs)
- .env.example with all required environment variables

Track A+B (API Layer):
- FastAPI app with 10 routers (auth, shaders, feed, votes, generate, desires, users, payments, mcp_keys, health)
- SQLAlchemy ORM models for all 15 tables
- Pydantic schemas for all request/response types
- JWT auth middleware (access + refresh tokens, Redis blocklist)
- Redis rate limiting middleware
- Celery worker config with job stubs (render, embed, generate, feed cache, expire bounties)
- Alembic migration framework

Service stubs:
- MCP server (health endpoint, 501 for all tools)
- Renderer service (Express + Puppeteer scaffold, 501 for /render)
- Frontend (package.json with React/Vite/Three.js/TanStack/Tailwind deps)
- Nginx reverse proxy config (/, /api, /mcp, /renders)

Project:
- DECISIONS.md with 11 recorded architectural decisions
- README.md with architecture overview
- Sample shader seed data (plasma, fractal noise, raymarched sphere)
2026-03-24 20:45:08 -05:00

204 lines
7 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
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