160 lines
5.3 KiB
Python
160 lines
5.3 KiB
Python
from __future__ import annotations
|
|
|
|
from .idfc import IDFCFirstParser
|
|
from .axis import AxisParser
|
|
from .hdfc import HDFCParser
|
|
from .icici import ICICIParser
|
|
from .hsbc import HSBCParser
|
|
from .indian_bank import IndianBankModernParser, IndianBankLegacyParser
|
|
from .indusind import IndusIndParser
|
|
from .kotak import KotakParser
|
|
from .sbi import SBIModernParser, SBIOtherParser
|
|
from .central_bank_of_india import CentralBankOfIndiaParser
|
|
from .yes_bank import YesBankParser
|
|
from .city_union_bank import CityUnionBankParser
|
|
from .bank_of_baroda import BankOfBarodaParser
|
|
from .rbl_bank import RBLBankParser
|
|
from .base import extract_text
|
|
from .template_engine import TemplateBasedStatementParser, parse_with_template
|
|
|
|
PARSERS = [
|
|
IDFCFirstParser,
|
|
AxisParser,
|
|
CentralBankOfIndiaParser,
|
|
YesBankParser,
|
|
CityUnionBankParser,
|
|
BankOfBarodaParser,
|
|
RBLBankParser,
|
|
HDFCParser,
|
|
ICICIParser,
|
|
HSBCParser,
|
|
IndianBankModernParser,
|
|
IndianBankLegacyParser,
|
|
IndusIndParser,
|
|
KotakParser,
|
|
SBIOtherParser,
|
|
SBIModernParser,
|
|
]
|
|
|
|
BANK_OPTIONS = [
|
|
("auto", "Auto Detect"),
|
|
("axis", "Axis Bank"),
|
|
("central_bank_of_india", "Central Bank of India"),
|
|
("yes_bank", "YES Bank"),
|
|
("city_union_bank", "City Union Bank"),
|
|
("bank_of_baroda", "Bank of Baroda"),
|
|
("rbl_bank", "RBL Bank"),
|
|
("hdfc", "HDFC Bank"),
|
|
("icici", "ICICI Bank"),
|
|
("hsbc", "HSBC Bank"),
|
|
("idfc", "IDFC FIRST Bank"),
|
|
("indian_bank", "Indian Bank"),
|
|
("indusind", "IndusInd Bank"),
|
|
("kotak", "Kotak Mahindra Bank"),
|
|
("sbi", "State Bank of India"),
|
|
]
|
|
|
|
BANK_PARSERS = {
|
|
"axis": [AxisParser],
|
|
"central_bank_of_india": [CentralBankOfIndiaParser],
|
|
"yes_bank": [YesBankParser],
|
|
"city_union_bank": [CityUnionBankParser],
|
|
"bank_of_baroda": [BankOfBarodaParser],
|
|
"rbl_bank": [RBLBankParser],
|
|
"hdfc": [HDFCParser],
|
|
"icici": [ICICIParser],
|
|
"hsbc": [HSBCParser],
|
|
"idfc": [IDFCFirstParser],
|
|
"indian_bank": [IndianBankModernParser, IndianBankLegacyParser],
|
|
"indusind": [IndusIndParser],
|
|
"kotak": [KotakParser],
|
|
"sbi": [SBIOtherParser, SBIModernParser],
|
|
}
|
|
|
|
|
|
def detect_parser(text):
|
|
scored = sorted(((parser.detect(text), parser) for parser in PARSERS), key=lambda item: item[0], reverse=True)
|
|
if not scored or scored[0][0] <= 0:
|
|
return None, 0
|
|
return scored[0][1](), scored[0][0]
|
|
|
|
|
|
def _selected_parser(bank_key: str, text: str):
|
|
candidates = BANK_PARSERS.get(bank_key, [])
|
|
if not candidates:
|
|
return None, 0
|
|
scored = sorted(((parser.detect(text), parser) for parser in candidates), key=lambda item: item[0], reverse=True)
|
|
if scored and scored[0][0] > 0:
|
|
return scored[0][1](), scored[0][0]
|
|
return None, 0
|
|
|
|
|
|
def _valid_result(result) -> bool:
|
|
if not result or len(result) != 2:
|
|
return False
|
|
_meta, frame = result
|
|
return frame is not None and not frame.empty
|
|
|
|
|
|
def parse_pdf(path, bank_hint: str | None = None):
|
|
"""Parse a bank statement without removing any existing parser behaviour.
|
|
|
|
Existing bank-specific parsers remain first priority. The template engine
|
|
is an additive fallback when a bank parser does not recognise a new layout
|
|
or returns no transactions. A template result is accepted only after
|
|
transaction-level running-balance validation.
|
|
"""
|
|
hint = (bank_hint or "auto").strip().lower()
|
|
text = extract_text(path)
|
|
primary_error: Exception | None = None
|
|
|
|
if hint and hint != "auto":
|
|
# Preserve HSBC's specialised OCR parser as the first route.
|
|
candidates = BANK_PARSERS.get(hint, [])
|
|
scored = sorted(
|
|
((parser.detect(text), parser) for parser in candidates),
|
|
key=lambda item: item[0],
|
|
reverse=True,
|
|
)
|
|
for score, parser_class in scored:
|
|
if score <= 0:
|
|
continue
|
|
try:
|
|
result = parser_class().parse(path, text)
|
|
if _valid_result(result):
|
|
return result
|
|
except (ValueError, RuntimeError) as exc:
|
|
primary_error = exc
|
|
|
|
# New layouts from a known bank are routed by table structure rather
|
|
# than rejected merely because the bank-specific detector changed.
|
|
try:
|
|
return parse_with_template(path, text, hint)
|
|
except (ValueError, RuntimeError) as template_error:
|
|
label = dict(BANK_OPTIONS).get(hint, "the selected bank")
|
|
detail = str(primary_error or template_error)
|
|
raise ValueError(
|
|
f"The uploaded statement could not be reliably parsed as {label}. "
|
|
f"{detail}"
|
|
) from template_error
|
|
|
|
parser, _score = detect_parser(text)
|
|
if parser is not None:
|
|
try:
|
|
result = parser.parse(path, text)
|
|
if _valid_result(result):
|
|
return result
|
|
except (ValueError, RuntimeError) as exc:
|
|
primary_error = exc
|
|
|
|
# Auto Detect now falls back to structural templates. Bank detection remains
|
|
# a hint for metadata and naming, never a hard gate for transaction parsing.
|
|
try:
|
|
return parse_with_template(path, text, "auto")
|
|
except (ValueError, RuntimeError) as template_error:
|
|
detail = str(primary_error or template_error)
|
|
raise ValueError(
|
|
"Unsupported or unreconciled statement format. "
|
|
f"{detail}"
|
|
) from template_error
|