Source code for aeat.adapters.inbound.declaracion._parsers._pdfplumber_backend
"""Pdfplumber-backed page text extraction for declaración PDFs.Wraps :mod:`pdfplumber` behind:func:`~adapters.inbound.declaracion._parsers._pdfplumber_backend.extract_pages_text`and:func:`~adapters.inbound.declaracion._parsers._pdfplumber_backend.extract_pages_text_from_bytes`,which return one stripped text string per page. Errors from the underlyinglibrary and pathological inputs (missing file, scan-only PDF without an OCRlayer) are translated into:class:`~adapters.inbound.declaracion._errors.DeclaracionParseError`.A pypdfium2 fast path is consulted before the canonical pdfplumber primitive.The fast path only commits its output when at least one declaration-contentcanary (NIF row or declarant row) matches; this keeps unrelated PDFs out of thefast lane and avoids cache poisoning. The bytes route follows the same canarydiscipline without writing decrypted content to disk."""from__future__importannotationsimportrefromfunctoolsimportlru_cachefrompathlibimportPathfrom.....core.loggingimportget_loggerfrom...pdfimportextract_pages_text_from_bytesas_extract_pages_text_from_bytes_implfrom...pdfimportextract_pages_text_with_fast_pathas_extract_pages_text_with_fast_path_implfrom.._errorsimportDeclaracionParseError_logger=get_logger(__name__)_INPUT_PDF_SOURCE_LABEL="<input-pdf>"_TAX_ID_CANARY_RE=re.compile(r"\bNIF\s*[:\-]?\s*[A-Z0-9][A-Z0-9 .\-]{3,31}?(?=\s+(?:CSV|Fecha)\b|\s*$)",re.IGNORECASE,)_DECLARANT_ROW_CANARY_RE=re.compile(r"\b[XYZ]?[0-9]{7,8}[A-Z]\s+20[0-9]{2}\s+(?:[1-4]T|0A|[0-1][0-9])\b",re.IGNORECASE,)
[docs]defextract_pages_text(pdf_path:Path)->tuple[str,...]:"""Extract the text of each page in order. The declaration backend first tries the pypdfium2 fast path and falls back to the shared pdfplumber primitive when the fast path is unavailable or its declaration canaries do not match. Args: pdf_path: Filesystem path of the PDF to read. Returns: Tuple with one stripped string per page in the source order. Empty pages preserve their slot as the empty string. Raises: DeclaracionParseError: When the PDF is missing or no extractable text can be read from any page. """return_extract_pages_text_with_fast_path_impl(pdf_path,error_class=DeclaracionParseError,not_found_label="declaración PDF not found",pdf_label="the PDF",fast_path_extractor=_extract_pages_text_with_pdfium,)
def_extract_pages_text_with_pdfium(pdf_path:Path)->tuple[str,...]|None:"""Run the cached pypdfium2 path extraction for one filesystem PDF."""resolved=pdf_path.resolve()stat=resolved.stat()return_extract_pages_text_with_pdfium_cached(str(resolved),stat.st_size,stat.st_mtime_ns)@lru_cache(maxsize=256)def_extract_pages_text_with_pdfium_cached(path:str,byte_count:int,modified_ns:int,)->tuple[str,...]|None:"""Return canary-validated pypdfium2 page text for a stable file revision."""delbyte_count,modified_nstry:importpypdfium2aspdfiumdocument=pdfium.PdfDocument(path)try:pages:list[str]=[]forpageindocument:text_page=page.get_textpage()try:pages.append((text_page.get_text_range()or"").strip())finally:text_page.close()page.close()finally:document.close()except(ImportError,OSError,ValueError,RuntimeError)asexc:_logger.debug("pypdfium2 failed to extract declaración PDF text from <input-pdf>: %s",type(exc).__name__,exc_info=True,)returnNoneifnotany(pages):returnNonetext="\n".join(pages)ifnot(_TAX_ID_CANARY_RE.search(text)or_DECLARANT_ROW_CANARY_RE.search(text)):returnNonereturntuple(pages)_PDFIUM_BYTES_CACHE:dict[str,tuple[str,...]]={}def_extract_pages_text_with_pdfium_from_bytes(pdf_bytes:bytes)->tuple[str,...]|None:"""Return canary-validated pypdfium2 page text for in-memory PDF bytes."""fromhashlibimportsha256digest=sha256(pdf_bytes).hexdigest()ifdigestin_PDFIUM_BYTES_CACHE:return_PDFIUM_BYTES_CACHE[digest]try:importpypdfium2aspdfiumdocument=pdfium.PdfDocument(pdf_bytes)try:pages:list[str]=[]forpageindocument:text_page=page.get_textpage()try:pages.append((text_page.get_text_range()or"").strip())finally:text_page.close()page.close()finally:document.close()except(ImportError,OSError,ValueError,RuntimeError)asexc:_logger.debug("pypdfium2 failed to extract declaración PDF text from bytes: %s",type(exc).__name__,exc_info=True,)returnNoneifnotany(pages):returnNonetext="\n".join(pages)ifnot(_TAX_ID_CANARY_RE.search(text)or_DECLARANT_ROW_CANARY_RE.search(text)):returnNoneresult=tuple(pages)iflen(_PDFIUM_BYTES_CACHE)>=256:first_key=next(iter(_PDFIUM_BYTES_CACHE))_PDFIUM_BYTES_CACHE.pop(first_key,None)_PDFIUM_BYTES_CACHE[digest]=resultreturnresult
[docs]defextract_pages_text_from_bytes(pdf_bytes:bytes,*,source_label:str="in-memory PDF")->tuple[str,...]:"""Extract text from PDF bytes without materialising a plaintext file. The bytes path mirrors ``extract_pages_text``: pypdfium2 gets the first chance to return canary-validated page text, then the shared pdfplumber bytes primitive handles the fallback while keeping the caller's decrypted bytes in memory. Raises: DeclaracionParseError: When no extractable text can be read from the supplied PDF bytes. """fast_pages=_extract_pages_text_with_pdfium_from_bytes(pdf_bytes)iffast_pagesisnotNone:returnfast_pagesreturn_extract_pages_text_from_bytes_impl(pdf_bytes,error_class=DeclaracionParseError,pdf_label="the PDF",source_label=source_label,)