Newer
Older
gn-synapse / app / api / schemas.py
"""Pydantic-схемы Ingestion — конверт из docs/05-ingestion-api.md."""

from datetime import datetime
from typing import Annotated, Literal

from pydantic import BaseModel, ConfigDict, Field

# Нейминг источников/subject/action: slug из нижнего регистра, точки/дефис/подчерк.
_NAME_PATTERN = r"^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$"


class EventEnvelope(BaseModel):
    """Конверт события. Конверт валидируется жёстко, payload — мягко (воркером)."""

    model_config = ConfigDict(extra="forbid")

    source: str = Field(pattern=_NAME_PATTERN, min_length=1, max_length=64)
    subject: str = Field(pattern=_NAME_PATTERN, min_length=1, max_length=64)
    action: str = Field(pattern=_NAME_PATTERN, min_length=1, max_length=64)
    priority: Literal["low", "normal", "high", "critical"] = "normal"
    payload: dict = Field(default_factory=dict)
    dedup_key: str | None = Field(default=None, max_length=255)
    ttl_seconds: int | None = Field(default=None, ge=1, le=7 * 24 * 3600)
    scheduled_at: datetime | None = None  # резерв контракта, в MVP не читается


class AcceptedEvent(BaseModel):
    ok: Literal[True] = True
    id: str
    status: str
    deduplicated: bool = False


class BatchRejected(BaseModel):
    ok: Literal[False] = False
    index: int
    detail: str


class BatchResult(BaseModel):
    results: list[Annotated[AcceptedEvent | BatchRejected, Field(discriminator="ok")]]


class DeliverySummary(BaseModel):
    channel: str
    target: str | None
    status: str
    attempts: int
    error: str | None
    rendered_message: str | None


class EventStatus(BaseModel):
    id: str
    source: str
    subject: str
    action: str
    priority: str
    status: str
    created_at: datetime
    expires_at: datetime | None
    deliveries: list[DeliverySummary]