Make template parser primary and automate bank detection

This commit is contained in:
A R R R Associates
2026-08-04 12:49:58 +05:30
parent d40f68f51a
commit 565425c914
2 changed files with 44 additions and 58 deletions
@@ -15,7 +15,7 @@ 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
from .template_engine import parse_with_template
PARSERS = [
IDFCFirstParser,
@@ -89,71 +89,50 @@ def _selected_parser(bank_key: str, text: str):
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 _template_first(path, text: str, hint: str):
"""Try the validated layout engine before bank-specific parsing.
Any template error is deliberately swallowed here so the existing bank
parser path remains an unchanged and reliable fallback.
"""
try:
return parse_with_template(path, text=text, bank_hint=hint)
except Exception:
return None
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
template_result = _template_first(path, text, hint)
if template_result is not None:
return template_result
# Preserve the existing HSBC OCR route for image-only statements and for
# users/API clients that explicitly provide an HSBC override.
if hint == "hsbc":
return HSBCParser().parse(path, text)
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:
parser, score = _selected_parser(hint, text)
if parser is None:
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
f"The uploaded statement could not be parsed by a validated layout template and does not match "
f"the selected {label} format. Please verify the statement or use automatic detection."
)
return parser.parse(path, text)
parser, _score = detect_parser(text)
if parser is not None:
parser, score = detect_parser(text)
if parser is None and not (text or "").strip():
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)
return HSBCParser().parse(path, text)
except ValueError:
pass
if parser is None:
raise ValueError(
"Unsupported or unreconciled statement format. "
f"{detail}"
) from template_error
"Unsupported statement format. The validated template engine and all available bank-specific parsers "
"were unable to reconcile this statement."
)
return parser.parse(path, text)