Source code for cadrumo.adapters.inbound.justificante._parsers.text_extraction
"""Private text-backend dispatch for the inbound justificante adapter.Callers outside :mod:`adapters.inbound.justificante` must never importfrom here. Backends expose a common ``extract_text`` entry point thatreturns the raw concatenated text of a justificante PDF; all field-levelextraction happens in :mod:`adapters.inbound.justificante._extract`.The dispatch is keyed on:class:`domain.justificante.schema.JustificanteParserBackend`, and parsefailures are normalised as:class:`domain.justificante.errors.JustificanteParseError`. The path entry pointcaches concatenated text by source digest, backend, size, and mtime; the bytesentry point stays uncached so secure-storage callers do not need to materialiseor persist plaintext files."""from__future__importannotationsfromcollectionsimportOrderedDictfrompathlibimportPathfrom.....domain.justificante.errorsimportJustificanteParseErrorfrom.....domain.justificante.schemaimportJustificanteParserBackendfrom...pdf.redactionimportINPUT_PDF_SOURCE_LABELas_INPUT_PDF_SOURCE_LABELfrom...pdf.source_provenanceimportsha256_file_TEXT_CACHE_MAXSIZE=256TEXT_CACHE:OrderedDict[tuple[str,str,int,int],str]=OrderedDict()
[docs]defextract_text(pdf_path:Path,backend:JustificanteParserBackend)->str:"""Extract concatenated text from ``pdf_path`` using ``backend``. The path result is cached for stable file revisions so repeated corpus parses do not reopen the same PDF. Args: pdf_path: Absolute path to the PDF to read. backend: Which backend to dispatch to. Returns: The concatenated text of every page in the PDF, joined by newlines. Raises: JustificanteParseError: If the path cannot be read or the selected backend is not implemented. """try:resolved=pdf_path.expanduser().resolve()stat=resolved.stat()source_digest=sha256_file(resolved)exceptOSErrorasexc:raiseJustificanteParseError(f"justificante PDF could not be read: {_INPUT_PDF_SOURCE_LABEL}",context={"path":_INPUT_PDF_SOURCE_LABEL},translated_message="adapters.inbound.justificante.errors.parse_failed",missing=("source_pdf",),)fromexcbackend_value=backend.valueifhasattr(backend,"value")elsestr(backend)cache_key=(source_digest,backend_value,stat.st_size,stat.st_mtime_ns)cached=TEXT_CACHE.get(cache_key)ifcachedisnotNone:TEXT_CACHE.move_to_end(cache_key)returncachedtext=_extract_text_uncached(resolved,backend_value)TEXT_CACHE[cache_key]=textTEXT_CACHE.move_to_end(cache_key)iflen(TEXT_CACHE)>_TEXT_CACHE_MAXSIZE:TEXT_CACHE.popitem(last=False)returntext
[docs]defextract_text_from_bytes(pdf_bytes:bytes,backend:JustificanteParserBackend)->str:"""Extract concatenated text from in-memory PDF bytes using ``backend``. Bytes are sent directly to the selected backend and are not cached, which keeps secure-storage and live-capture parsing free of plaintext filesystem artefacts. """backend_value=backend.valueifhasattr(backend,"value")elsestr(backend)return_extract_text_from_bytes_uncached(pdf_bytes,backend_value)
def_extract_text_uncached(pdf_path:Path,backend_value:str)->str:"""Dispatch one uncached filesystem parse to the selected backend."""normalized_backend=backend_value.lower()ifnormalized_backend==JustificanteParserBackend.PDFPLUMBER.value.lower():from._pdfplumber_backendimportextract_text_pdfplumberreturnextract_text_pdfplumber(pdf_path)raiseJustificanteParseError(f"unknown parser backend: {backend_value!r}")def_extract_text_from_bytes_uncached(pdf_bytes:bytes,backend_value:str)->str:"""Dispatch one in-memory parse to the selected backend."""normalized_backend=backend_value.lower()ifnormalized_backend==JustificanteParserBackend.PDFPLUMBER.value.lower():from._pdfplumber_backendimportextract_text_pdfplumber_bytesreturnextract_text_pdfplumber_bytes(pdf_bytes)raiseJustificanteParseError(f"unknown parser backend: {backend_value!r}")