Coverage for src / quber / providers / landing / orchestrator.py: 45%
42 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-09-23 22:14 -0400
« prev ^ index » next coverage.py v7.14.0, created at 2026-09-23 22:14 -0400
1"""Run one ADE parse end to end: submit, write artifacts, summarize.
3`run_parse` takes a source PDF or image (local path or ``s3://`` URI), an
4output destination (local directory or ``s3://`` prefix), a model name, and
5an optional 1-based page number. It parses the document through ADE, writes
6the artifacts through the same output contract the other extraction routines
7honor, and returns a run summary with the credit usage the platform reported.
9Artifacts per run: ``<base>.ade.json`` (raw parse response), ``<base>.ade.md``
10(the parse markdown), and ``<base>.ade.complete.json`` written strictly last as
11the completion marker.
12"""
14from __future__ import annotations
16import json
17import tempfile
18from dataclasses import dataclass, field
19from pathlib import Path
20from typing import Optional
22from loguru import logger
24from quber.files.cache import resolve_document
25from quber.files.output import output_sink, write_completion_marker
26from quber.files.pdf import slice_page
27from quber.providers.landing.client import DEFAULT_MODEL, parse_document
30@dataclass
31class RunSummary:
32 """What one ADE run produced, for the CLI to print and callers to log."""
34 base: str
35 pages: Optional[int]
36 #: Credits the platform billed for the run (v2 bills fractional credits).
37 credits: Optional[float]
38 model_version: Optional[str]
39 artifacts: list[str] = field(default_factory=list)
42def run_parse(
43 source: str,
44 output_dir: str = "output",
45 model: str = DEFAULT_MODEL,
46 page: Optional[int] = None,
47) -> RunSummary:
48 """Parse ``source`` through ADE and write artifacts to ``output_dir``."""
49 local = resolve_document(source)
50 base = local.stem
51 if page is not None:
52 base = f"{base}.p{page}"
54 with tempfile.TemporaryDirectory(prefix="quber-ade-") as tmp:
55 submitted = local
56 if page is not None:
57 submitted = slice_page(local, page, Path(tmp) / f"{base}.pdf")
58 doc = parse_document(submitted, model=model)
60 meta = doc.get("metadata") or {}
62 # v1 metadata reports credit_usage and version; v2 reports
63 # billing.total_credits and model_version.
64 credits = meta.get("credit_usage")
65 if credits is None:
66 credits = (meta.get("billing") or {}).get("total_credits")
67 summary = RunSummary(
68 base=base,
69 pages=meta.get("page_count"),
70 credits=credits,
71 model_version=meta.get("version") or meta.get("model_version"),
72 )
74 with output_sink(output_dir) as out:
75 json_path = out / f"{base}.ade.json"
76 json_path.write_text(json.dumps(doc, indent=1), encoding="utf-8")
77 md_path = out / f"{base}.ade.md"
78 md_path.write_text(doc.get("markdown") or "", encoding="utf-8")
79 summary.artifacts = [json_path.name, md_path.name]
81 write_completion_marker(
82 output_dir,
83 f"{base}.ade.complete.json",
84 {
85 "artifacts": summary.artifacts,
86 "page_count": summary.pages,
87 "credit_usage": summary.credits,
88 "version": summary.model_version,
89 "job_id": meta.get("job_id"),
90 },
91 )
92 logger.info(
93 "ADE run complete: base={} pages={} credits={}",
94 summary.base,
95 summary.pages,
96 summary.credits,
97 )
98 return summary