patristic-be / src /api /dto /addbook.py
Mario33333's picture
deploy: reader-access security hardening (feature/audiobook@06a5ed8)
7c2d250 verified
Raw
History Blame Contribute Delete
5.09 kB
"""DTOs for the Add Book wizard — uploads + probe.
The wizard's three-step flow (per ``BACKEND_BUILD.md §13 Step 6`` +
``FRONTEND_BUILD.md`` Add Book page):
1. ``POST /uploads`` (multipart) → :class:`UploadResponse` — bytes land in
a temp dir, the FE gets back a stable ``uploadId`` it can hand to the
probe + create endpoints.
2. ``POST /books/probe`` body :class:`ProbeRequest` → :class:`ProbeResponse`
— wraps ``src/lib/metadata_probe.py`` and returns enough metadata for
the wizard to prefill the create form (title, author, tradition…)
plus a sample page rendering + ingest cost estimate.
3. ``POST /books`` body :class:`CreateBookRequest` (in ``dto/books.py``)
→ :class:`BookDTO` — commits the row and moves the upload bytes into
the storage backend at ``data/raw/{bookId}.pdf``.
Every DTO inherits :class:`ApiModel`. Enums (extraction mode, era,
tradition, language) reuse :mod:`src.api.dto.books` so the same TS union
types the rest of the library uses are reused on the FE.
"""
from __future__ import annotations
from pydantic import Field
from .books import (
EraEnum,
ExtractionModeEnum,
LanguageEnum,
ReligionEnum,
TraditionEnum,
)
from .common import ApiModel
class UploadResponse(ApiModel):
"""Body for ``POST /uploads`` — multipart-PDF upload.
The temp file lands at ``data/uploads/{uploadId}.pdf`` keyed by a
fresh uuid (``upload_id``). The SHA-256 is computed server-side as a
side effect of writing the bytes — the FE uses it as the cache key
for probe results so re-uploading the same file doesn't re-bill the
Gemini probe call.
"""
upload_id: str
sha256: str
size_bytes: int
class ProbeRequest(ApiModel):
"""Body for ``POST /books/probe`` — at least one of the two must be set.
The router validates "at least one" at runtime (raises
``BAD_REQUEST`` otherwise) so the wire schema can stay simple — both
fields being optional matches the FE's typical flow where the user
is editing either a URL or a freshly uploaded file.
"""
source_url: str | None = None
upload_id: str | None = None
class GuessedMetadata(ApiModel):
"""The metadata fields the probe attempts to fill in from the PDF.
Every field is ``Optional`` — when Gemini can't extract a confident
value, the field stays ``None`` and the wizard renders the input
empty. Religion fields default to ``None`` because the probe is most
confident about title/author and least confident about religion of
the author (which sometimes requires external knowledge the model
doesn't have).
"""
title_ar: str | None = None
title_en: str | None = None
author: str | None = None
author_id: str | None = None
era: EraEnum | None = None
tradition: TraditionEnum | None = None
language: LanguageEnum | None = None
book_religion: ReligionEnum | None = None
author_religion: ReligionEnum | None = None
confidence: str | None = None
rationale: str | None = None
class SuggestedLabel(ApiModel):
"""One label the probe thinks the user might want to apply.
The ``id`` is the slug Gemini returned (canonicalised to lowercase
and trimmed to <=64 chars in
:func:`src.lib.metadata_probe._clean_suggested_labels`). The wizard
shows them as togglable chips; ticked chips become
``CreateBookRequest.label_ids`` on submit. The label rows themselves
are created lazily by the create endpoint (or by
``POST /labels/seed-from-derived``).
"""
id: str
reason: str | None = None
class SamplePage(ApiModel):
"""One PDF page rendered to text for the wizard's "sanity check" pane.
For native-text PDFs ``ocr_text`` is the PyMuPDF extraction (free,
no API call). For scanned PDFs the value is whatever the probe
surfaced — which may be empty if the probe didn't sample image pages.
``clean_text`` is unset on probe (Stage 4 hasn't run); included in
the DTO so the same shape can be reused for ``GET /books/.../pages``
later.
"""
pdf_page: int
ocr_text: str
clean_text: str | None = None
class ProbeResponse(ApiModel):
"""The Add Book wizard's pre-flight result.
The FE uses this to render Step 2 of the wizard: prefilled form
+ suggested labels + sample pages + a hard cost number for the
"Run ingest" CTA. ``estimated_ingest_cost_usd`` is the
counterfactual cost of running the full pipeline at the suggested
extraction mode, computed against ``tools_registry.yaml`` so it
moves with pricing updates.
"""
pages_total: int
has_text_layer: bool
suggested_extraction_mode: ExtractionModeEnum
guessed_metadata: GuessedMetadata
suggested_labels: list[SuggestedLabel] = Field(default_factory=list)
sample_pages: list[SamplePage] = Field(default_factory=list)
estimated_ingest_cost_usd: float
probe_error: str | None = None