Example provider — parser.pdfplumber¶
The first real Wave-1 adapter built through AdapterBase — the template every subsequent
Provider copies. A CPU, page-aware PDF Parser over the MIT-licensed pdfplumber library.
See Authoring a Provider and
Writing an adapter.
provider ¶
parser.pdfplumber — the first REAL Wave-1 adapter built THROUGH AdapterBase (H-A1).
The Phase-3 provider seam — :class:~latence_core.providers.adapter.AdapterBase (P3-F2) +
:class:~latence_core.providers.profile.ProviderProfile (P3-F1) — was validated only by tests
that fabricate fake subclasses; it had zero real production consumers. This Provider is the
first real one: a CPU, page-aware PDF (and plain-text) Parser over the
pdfplumber <https://github.com/jsvine/pdfplumber>_ library (MIT), implemented as an
AdapterBase subclass — turning the hypothetical seam into a real two-adapter seam (with
the fabricated-subclass tests) and, more importantly, into a real second-real-consumer seam
alongside the in-core reference Providers. It is the template every subsequent Wave-1 adapter
(docling, GLiNER variants, embedders, PII) copies, so the ergonomics are the point.
Library choice + fallback (H-A1 "verify early"): the issue preferred kreuzberg, but an
early install-and-run verification (recorded in the package README's seam-report) found it
unsuitable on both counts the issue names — its current major (4.x) is Elastic-2.0, a
non-permissive source-available license (ADR-0012 requires permissive-or-opt-in), and its last
permissive (MIT) 3.x line fails to import without OCR system binaries (the "requires system
packages" failure mode). So, exactly as the issue's fallback clause directs, this substitutes a
permissive, pip-installable PDF lib — pdfplumber (MIT), which is page-aware (so the
PageMap/offset path is genuinely exercised, not a single blob), CPU-only, and deterministic on the
text layer. It needs no OS-level system binaries (no pandoc/tesseract to apt-install, unlike
kreuzberg), though its wheel closure does include native code — pillow (C-ext) and
pypdfium2 (bundled PDFium), both permissive and latent (the used path is pdfminer.six-only);
see the profile() license note. The package is named for the library it actually uses.
What it EXERCISES on AdapterBase (the audit's whole point — use the facilities, don't bypass):
- native-exception → typed taxonomy (:meth:
AdapterBase.guard): a corrupt / password- protected / type-spoofed PDF makes pdfminer raise its nativePdfminerException; the guard maps it to a typed :class:~latence_core.errors.ProviderErrorcarrying only the provider + op + native type name — no raw content / PII. That typed error is then caught at the per-document boundary and turned into a gracefulPARSE_ERROR:class:DocumentRecord(ADR- 0020), so one bad file never aborts the run (the Parse C2 graceful-failure the conformance suite asserts). The guard is used for its real purpose (map the native fault to the typed category); the PARSE_ERROR disposition is Parse's own graceful-degradation contract layered on top. - offset / Provenance + page preservation (:meth:
AdapterBase.carry_provenance+ :func:PageMap.from_page_texts): the per-page text pdfplumber yields is assembled with the existing core PageMap seam (boundaries exact by construction), and the produced record's Provenance is carried from the input via the base helper (source lineage intact, only the char/page span narrowed) — not a hand-rolled weaker mapper (the audit found the in-core GLiNER provider did that; this does not repeat it). Offsets round-trip; pages are correct. - device + batching (:meth:
AdapterBase.batched): a CPU parser declarescompute="cpu"; :meth:parseruns the batching path over multiple input documents (input-order-preserving, one batch at a time — streaming-friendly, ADR-0033). Device routing is trivially CPU here but flows through the seam (self.deviceis resolved insuper().__init__).
OCR is a Provider technique, never the Capability name (CONTEXT glossary): this Provider reads the PDF text layer only — a scanned page with no text layer yields empty page text (recorded, not crashed), and an OCR-first Provider is a drop-in second Parser behind the same seam. That keeps the install lean and CPU-viable (no tesseract / no vision stack — ADR-0016).
PdfPlumberParser ¶
Bases: AdapterBase
Parser Provider over pdfplumber: PDF (per-page) + text (single-page) → markdown records.
An :class:~latence_core.providers.adapter.AdapterBase subclass (H-A1): it declares a
:class:~latence_core.providers.profile.ProviderProfile, resolves its device through the base
(super().__init__), batches over multiple input documents via :meth:AdapterBase.batched,
maps the native pdfminer exception through :meth:AdapterBase.guard, and carries Provenance
via :meth:AdapterBase.carry_provenance. It satisfies the
:class:~latence_core.capability.Parser Protocol structurally (it implements parse) — the
base adds no protocol, only the reusable glue.
Config:
encoding: str — text-decode encoding for text inputs (default 'utf-8'). A per-input
config['encoding'] on the ParserInput overrides this default.
category: str | None — content category stamped onto Classification, overriding any
category inherited from the ParserInput.
sensitivity: str | None — sensitivity stamped onto Classification. If unset, the
ParserInput's inherited sensitivity is preserved.
page_separator: str — markdown joined between PDF pages (default '\n\n').
batch_size: int — how many input documents the batching path groups per call (default
32, validated >= 1). Purely memory-shaping — outputs are input-order identical
for any positive batch size (ADR-0033).
device: str — 'auto'|'cpu'|'cuda' (resolved by the base; trivially CPU here).
config_exceptions is left empty (the base default): pdfplumber/pdfminer raise no
"operator handed a bad config" native exception in this Provider's code path — an invalid
encoding hint is caught explicitly below as a per-document parse failure, and a corrupt
PDF is a runtime :class:~latence_core.errors.ProviderError, not a ConfigError. Declaring
nothing here means the guard maps every native fault to ProviderError, which is correct.
Source code in packages/latence-parser-pdfplumber/src/latence_parser_pdfplumber/provider.py
provider_name
property
¶
The registered entry-point name — the stable tag a mapped error carries (no content).
profile
classmethod
¶
The declared descriptor (ADR-0036 §2): pure-code, CPU, deterministic, permissive (MIT).
Ships no model weights (model_id=None): text-layer extraction is deterministic.
This Provider's own code is the repo's Apache-2.0; its direct dep
pdfplumber is MIT (license_code="MIT"), verified per ADR-0012 from the installed
version's PyPI metadata (License classifier OSI Approved :: MIT License) + the upstream
GitHub LICENSE. Honest dependency footprint (NOT pure-Python): pdfplumber's install
closure pulls native code — pillow (C-extension image decoder, HPND/MIT-CMU) and
pypdfium2 (bundles Google's PDFium C++, Apache-2.0/BSD-3) — and pdfminer.six (MIT)
pulls cryptography (compiled, Apache-2.0/BSD-3) for encrypted PDFs. All permissive
(no ADR-0012 violation), and this Provider's used path calls only page.extract_text()
(stays inside pdfminer.six), so pillow/pypdfium2 are latent, not on the reachable code path
— but the native footprint + its CVE surface (pillow especially) is real and is an accepted
risk gated by the IntakeScreener (which quarantines malformed/oversized/spoofed input before
Parse). CPU-only (compute="cpu"). batch=True — it batches over multiple input
documents (:meth:AdapterBase.batched), input-order-preserving and streaming-friendly. No
per-call cost (a local library, not an endpoint).
A subtlety H-A1 asks the seam-report to answer honestly: memory_mb and cost_per_1k
are inert for this adapter — cost_per_1k=None (no endpoint) and nothing consumes
memory_mb (the device seam only routes on compute; there is no memory-gated skip).
They are declared truthfully (a small honest memory_mb=96 for the pdfplumber page
buffers; cost_per_1k=None) but nothing reads them here — recorded in the README's
trim-vs-keep note.
Source code in packages/latence-parser-pdfplumber/src/latence_parser_pdfplumber/provider.py
parse ¶
Consume raw :class:ParserInputs and yield parsed markdown records, in input order.
Runs the base's batching path (:meth:AdapterBase.batched) over multiple input documents
so the seam's streaming-friendly, input-order-preserving batching is genuinely exercised
(H-A1 "exercise AdapterBase's batching path over multiple input documents"). Each batch is
parsed one document at a time; ordering is preserved by construction.
Source code in packages/latence-parser-pdfplumber/src/latence_parser_pdfplumber/provider.py
size_after_parse ¶
Assembled-markdown size in bytes — the Provenance 'size-after-parse' (S2 observability).
A one-line delegation to :attr:~latence_core.contracts.DocumentRecord.size_after_parse,
which is where the rule lives: the size is a property of the record, not of the Parser that
produced it. Kept as a module-level function because this package publishes the name in its
__all__; every Parser package delegates to the same implementation, so the signal the
Runner sums into ParseQuality.bytes_after_parse cannot drift between Providers.