Fix modern ICICI template parsing and parser fallback

This commit is contained in:
A R R R Associates
2026-08-04 13:10:24 +05:30
parent 565425c914
commit 79bfc808d3
3 changed files with 213 additions and 44 deletions
@@ -1,5 +1,9 @@
from __future__ import annotations
import logging
logger = logging.getLogger(__name__)
from .idfc import IDFCFirstParser
from .axis import AxisParser
from .hdfc import HDFCParser
@@ -89,16 +93,63 @@ def _selected_parser(bank_key: str, text: str):
return None, 0
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.
"""
def _usable_result(result) -> bool:
if result is None:
return False
try:
return parse_with_template(path, text=text, bank_hint=hint)
_meta, frame = result
return frame is not None and not frame.empty
except Exception:
return None
return False
def _attempt(parser, path, text: str, route: str):
try:
result = parser.parse(path, text)
if _usable_result(result):
meta, frame = result
logger.info(
"Bank analyzer route accepted: route=%s parser=%s rows=%s file=%s",
route, getattr(meta, "parser_name", parser.__class__.__name__), len(frame), path,
)
return result
logger.warning(
"Bank analyzer route returned no rows: route=%s parser=%s file=%s",
route, parser.__class__.__name__, path,
)
except Exception as exc:
logger.warning(
"Bank analyzer route failed: route=%s parser=%s file=%s error=%s",
route, parser.__class__.__name__, path, exc, exc_info=True,
)
return None
def _template_first(path, text: str, hint: str):
try:
result = parse_with_template(path, text=text, bank_hint=hint)
if _usable_result(result):
meta, frame = result
logger.info(
"Bank analyzer template accepted: parser=%s rows=%s file=%s",
getattr(meta, "parser_name", "TemplateBasedStatementParser"), len(frame), path,
)
return result
logger.warning("Bank analyzer template returned no rows: file=%s", path)
except Exception as exc:
logger.warning(
"Bank analyzer template rejected: file=%s error=%s", path, exc, exc_info=True,
)
return None
def _ordered_bank_candidates(text: str):
scored = sorted(
((parser.detect(text), parser) for parser in PARSERS),
key=lambda item: item[0],
reverse=True,
)
return [parser for score, parser in scored if score > 0]
def parse_pdf(path, bank_hint: str | None = None):
@@ -109,30 +160,42 @@ def parse_pdf(path, bank_hint: str | None = None):
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":
parser, score = _selected_parser(hint, text)
if parser is None:
label = dict(BANK_OPTIONS).get(hint, "the selected bank")
raise ValueError(
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 None and not (text or "").strip():
try:
return HSBCParser().parse(path, text)
except ValueError:
pass
if parser is None:
candidates = BANK_PARSERS.get(hint, [])
for parser_class in sorted(candidates, key=lambda cls: cls.detect(text), reverse=True):
result = _attempt(parser_class(), path, text, f"selected:{hint}")
if result is not None:
return result
label = dict(BANK_OPTIONS).get(hint, "the selected bank")
raise ValueError(
"Unsupported statement format. The validated template engine and all available bank-specific parsers "
"were unable to reconcile this statement."
f"The uploaded statement could not be parsed by a validated layout template or the "
f"selected {label} parser. Please verify that the PDF is readable and supported."
)
return parser.parse(path, text)
attempted: set[type] = set()
for parser_class in _ordered_bank_candidates(text):
attempted.add(parser_class)
result = _attempt(parser_class(), path, text, "auto-detected")
if result is not None:
return result
if not (text or "").strip():
result = _attempt(HSBCParser(), path, text, "image-only-ocr")
if result is not None:
return result
# Last-resort compatibility pass: a detector may miss a new layout even
# though the bank-specific parser can parse its visual table correctly.
for parser_class in PARSERS:
if parser_class in attempted:
continue
result = _attempt(parser_class(), path, text, "compatibility-fallback")
if result is not None:
return result
raise ValueError(
"The bank statement format was identified, but no transaction rows could be extracted. "
"The template engine and all bank-specific parsers were attempted. "
"Please verify that the PDF is text-readable and that this statement layout is supported."
)