"""Request and response models. Only `source` is mandatory. Everything else has a working default so that {"source": {"url": "..."}} produces a usable PDF. """ from __future__ import annotations from datetime import datetime from typing import Literal from pydantic import BaseModel, Field, model_validator from .config import get_settings EngineName = Literal["auto", "weasyprint", "chromium"] PagePosition = Literal[ "top-left", "top-center", "top-right", "bottom-left", "bottom-center", "bottom-right", ] JobStatus = Literal["queued", "running", "done", "failed", "cancelled", "expired"] class Source(BaseModel): url: str | None = Field(default=None, description="Adresa HTML dokumentu ke konverzi.") html: str | None = Field(default=None, description="HTML poslane primo v tele requestu.") base_url: str | None = Field( default=None, description="Zaklad pro relativni cesty. Pouziva se hlavne spolu s polem html.", ) @model_validator(mode="after") def exactly_one_source(self) -> "Source": if bool(self.url) == bool(self.html): raise ValueError("Vyplnte prave jedno z poli source.url a source.html.") return self class Margin(BaseModel): top: str = "20mm" right: str = "15mm" bottom: str = "20mm" left: str = "15mm" class PageSettings(BaseModel): format: str = Field(default="A4", description="Nazev formatu (A4, A5, Letter) nebo rozmer 210mm 297mm.") orientation: Literal["portrait", "landscape"] = "portrait" margin: Margin = Field(default_factory=Margin) class PageNumbers(BaseModel): enabled: bool = False format: str = Field(default="{page} / {pages}", description="Zastupne symboly {page} a {pages}.") position: PagePosition = "bottom-center" mode: Literal["auto", "css", "overlay"] = Field( default="auto", description=( "auto zvoli css u necleneneho dokumentu a overlay u clenene nebo u Chromia. " "css pouziva CSS countery, overlay dopisuje cisla do hotoveho PDF." ), ) start_at: int = 1 class TocSettings(BaseModel): enabled: bool = False depth: int = Field(default=3, ge=1, le=6) title: str = "Obsah" class AssetSettings(BaseModel): allow_remote: bool = True timeout_seconds: int = Field(default_factory=lambda: get_settings().asset_timeout_seconds, ge=1) class ChunkSettings(BaseModel): enabled: bool = True pages_per_chunk: int = Field(default=50, ge=1) class WaitFor(BaseModel): """Chromium only. Ignored by the WeasyPrint engine.""" state: Literal["load", "domcontentloaded", "networkidle"] = "load" selector: str | None = None timeout_seconds: int = Field(default=30, ge=1) class ConvertRequest(BaseModel): source: Source engine: EngineName = Field(default_factory=lambda: get_settings().default_engine) # type: ignore[arg-type] page: PageSettings = Field(default_factory=PageSettings) page_numbers: PageNumbers = Field(default_factory=PageNumbers) toc: TocSettings = Field(default_factory=TocSettings) outline: bool = Field(default=True, description="Generovat zalozky PDF z nadpisu h1 az h6.") pdf_profile: Literal["pdf/a-1b", "pdf/a-2b", "pdf/a-3b", "pdf/a-4b", "pdf/ua-1"] | None = None assets: AssetSettings = Field(default_factory=AssetSettings) chunking: ChunkSettings = Field(default_factory=ChunkSettings) wait_for: WaitFor = Field(default_factory=WaitFor) filename: str | None = Field(default=None, description="Nazev souboru ve Content-Disposition.") callback_url: str | None = Field( default=None, description="Volitelna adresa, na kterou se po dokonceni jobu posle POST se stavem jobu.", ) model_config = { "json_schema_extra": { "examples": [ {"source": {"url": "https://example.com/dokument.html"}}, { "source": {"url": "https://example.com/velky-dokument.html"}, "engine": "weasyprint", "page": {"format": "A4", "orientation": "portrait"}, "page_numbers": {"enabled": True, "format": "{page} / {pages}"}, "toc": {"enabled": True, "depth": 3, "title": "Obsah"}, "chunking": {"enabled": True, "pages_per_chunk": 50}, }, ] } } class MissingAsset(BaseModel): url: str reason: str class JobProgress(BaseModel): pages_rendered: int = 0 chunks_done: int = 0 chunks_total: int = 0 pass_number: int = 0 class ErrorInfo(BaseModel): error_code: str message: str detail: dict | None = None class JobState(BaseModel): job_id: str status: JobStatus created_at: datetime started_at: datetime | None = None finished_at: datetime | None = None expires_at: datetime | None = None progress: JobProgress = Field(default_factory=JobProgress) engine_used: str | None = None page_count: int | None = None missing_assets: list[MissingAsset] = Field(default_factory=list) warnings: list[str] = Field(default_factory=list) error: ErrorInfo | None = None result_url: str | None = None class JobAccepted(BaseModel): job_id: str status: JobStatus created_at: datetime result_url: str class HealthResponse(BaseModel): status: Literal["ok", "degraded"] app: str version: str engines: dict[str, bool] queue: dict[str, int]