"""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]