"""Pydantic request/response models for the NEMOCITY HTTP API. Shapes follow ARCHITECTURE.md §HTTP API exactly (wire names keep godseed's wish_*; "petition" is user-facing copy only): GET /api/state -> StateResponse (includes server_now for the shared sim clock) POST /api/wish {text} -> WishAccepted (errors: 400/429/503 with a city-hall `detail`) POST /api/fix -> WishAccepted (409 when traffic is flowing smoothly) GET /api/wishes -> list[WishSummary] GET /api/wishes/{id} -> raw WishTrace dict (passed through, engine-owned shape) Features and WishTraces are deliberately NOT strict models here: the engine owns those shapes and the server passes them through untouched (the "her" pattern — transport never reinterprets city facts). """ from __future__ import annotations from typing import Any, Optional from pydantic import BaseModel, ConfigDict, Field, field_validator # The moderation fast-path enforces the real petition limit (<=160 chars) with a # readable reason; this transport-level cap only stops abusive megabyte bodies before # they reach moderation. TRANSPORT_MAX_WISH_CHARS = 2000 class WishRequest(BaseModel): """Body of POST /api/wish (a petition).""" model_config = ConfigDict(str_strip_whitespace=True) text: str = Field(..., min_length=1, max_length=TRANSPORT_MAX_WISH_CHARS) @field_validator("text") @classmethod def _not_blank(cls, value: str) -> str: if not value.strip(): raise ValueError("a petition needs words") return value.strip() class WishAccepted(BaseModel): """Response of POST /api/wish and POST /api/fix.""" wish_id: str position: int = Field(..., ge=1, description="1-based place in the queue") class CurrentWish(BaseModel): """The petition city hall is processing right now.""" wish_id: str text_preview: str class QueueInfo(BaseModel): length: int = 0 current: Optional[CurrentWish] = None class WorldState(BaseModel): """Snapshot of the deterministic city: ordered event list + counters.""" version: int = Field(..., ge=0, description="total number of features") epoch: int = Field(..., ge=0, description="number of granted petitions") features: list[dict[str, Any]] class WishSummary(BaseModel): """One row of the ledger index (GET /api/wishes).""" wish_id: str text: str epitaph: str = "" epoch: int ts: float class StateResponse(BaseModel): """GET /api/state. server_now anchors every client's shared sim clock (simTime = server_now - CITY_EPOCH_S); clients slew toward it, never snap.""" world: WorldState queue: QueueInfo wishes_recent: list[WishSummary] server_now: float