Source code for aeat.adapters.inbound.financial.providers._detection
"""Provider registry and file-format auto-detection.Exposes ``detect_provider``, the entry point the financial-ingest applicationlayer calls to pick a concrete:class:`~adapters.inbound.financial.providers.FinancialProvider` for anarbitrary path. The detection strategy combines extension hinting withmagic-byte sniffing so a misnamed file (a PDF saved as ``.csv``, an XLSX insidea ``.txt``, etc.) still routes to the right parser."""from__future__importannotationsfrompathlibimportPathfrom.....core.loggingimportget_loggerfrom._baseimportFinancialProviderfrom._constantsimportCSV_EXTENSIONS,PDF_EXTENSION,XLSX_EXTENSIONfrom._csvimportCsvProviderfrom._ofximportOfxProviderfrom._pdf_n26importPdfN26Providerfrom._xlsximportXlsxProvider_logger=get_logger(__name__)
[docs]defprovider_for_extension(path:Path)->FinancialProvider|None:"""Return a :class:`FinancialProvider` keyed strictly off ``path``'s suffix. Cheap fallback used by CLI command surfaces when content-aware detection (:func:`detect_provider`) returns ``None`` but the path's suffix is unambiguous. Unlike :func:`detect_provider` this does not open the file or sniff its bytes. Returns ``None`` for every extension the project does not recognise (PDF among them — :class:`PdfN26Provider` requires content-aware detection because the bare ``.pdf`` suffix carries no statement flavour information). """suffix=path.suffix.lower()ifsuffixinCSV_EXTENSIONS:returnCsvProvider()ifsuffix==XLSX_EXTENSION:returnXlsxProvider()ifsuffixin{".ofx",".qfx"}:returnOfxProvider()returnNone
[docs]defdetect_provider(path:Path)->FinancialProvider|None:"""Return the first provider that validates the source successfully. Walks an extension- and content-prioritised candidate list and returns the first provider whose ``validate_source`` result is an ``is_valid`` :class:`~adapters.inbound.financial.providers.ProviderValidation`. Args: path: Source document to classify. Returns: The matching :class:`FinancialProvider`, or ``None`` when no provider can interpret ``path``. """providers=_ordered_candidates(path)forproviderinproviders:ifprovider.validate_source(path).is_valid:_logger.debug("detect_provider: matched %s for %s",provider.name,path.name)returnprovider_logger.warning("detect_provider: no provider matched %s",path.name)returnNone
def_ordered_candidates(path:Path)->tuple[FinancialProvider,...]:"""Order providers by extension hint, then fall back to content sniffing."""suffix=path.suffix.lower()ifsuffix==PDF_EXTENSION:return(PdfN26Provider(),CsvProvider(),XlsxProvider(),OfxProvider())ifsuffix==XLSX_EXTENSION:return(XlsxProvider(),CsvProvider(),OfxProvider(),PdfN26Provider())ifsuffixin{".ofx",".qfx"}:return(OfxProvider(),CsvProvider(),XlsxProvider(),PdfN26Provider())ifsuffixinCSV_EXTENSIONS:return(CsvProvider(),OfxProvider(),XlsxProvider(),PdfN26Provider())try:head=path.read_bytes()[:256]exceptOSError:_logger.warning("detect_provider: cannot read file header for sniffing path=%s",path,exc_info=True)return(CsvProvider(),XlsxProvider(),OfxProvider(),PdfN26Provider())upper_head=head.upper()ifhead.startswith(b"%PDF"):return(PdfN26Provider(),CsvProvider(),XlsxProvider(),OfxProvider())ifhead.startswith(b"PK"):return(XlsxProvider(),CsvProvider(),OfxProvider(),PdfN26Provider())ifb"<OFX>"inupper_headorb"<BANKTRANLIST>"inupper_head:return(OfxProvider(),CsvProvider(),XlsxProvider(),PdfN26Provider())return(CsvProvider(),XlsxProvider(),OfxProvider(),PdfN26Provider())