"""Heat code extraction for MTR (Material Test Report) PDFs. Public API: extract_heat_codes(file_bytes, filename=None) -> list[str] Returns a deduplicated, order-preserving list of heat codes found in the document. Empty list if none found — caller's responsibility to route to the no-heat-code fallback. Strategy (in order; results unioned, then filtered + deduped): 1. pdfplumber table extraction — look for columns whose header matches a heat-code label and pull the column's non-empty values. 2. Label-anchored regex on extracted text — handles inline "Heat No.: ABC123" and similar patterns. 3. Filename hint — if the filename contains a plausible heat-code-shaped token AND that token also appears in document text, include it. 4. OCR fallback for image-only PDFs — runs at 300 DPI with psm 6, then reruns strategies 2+3 on the OCR text. Filtering rejects: - ASTM/SA/EN/ISO/ASME spec numbers ("A105", "SA-312", "EN 10204", "ASME B16.5") - Standalone 4-digit years (2020–2030) - Common ALL-CAPS technical words (MAX, MIN, PASS, FAIL, etc.) - Template placeholders ("__", "___", "—", "-") - Single characters or 1–3 char tokens (too short to be heat codes) Designed to drop into `app/lib/utils/heat_code.py` in the email automation repo with no changes — public API and imports are repo-compatible. """ from __future__ import annotations import io import logging import re from typing import Iterable logger = logging.getLogger(__name__) # ─── Label patterns ──────────────────────────────────────────────────────────── # Synonyms for "heat code" that may appear as labels in MTRs. Case-insensitive. # Order matters only for logging; matching is union. _LABEL_PATTERNS: tuple[str, ...] = ( r"heat\s*code", r"heat\s*no\.?", r"heat\s*nr\.?", r"heat\s*#", r"heat\s*number", r"lot\s*no\.?", # Bonney Forge and other small-mill MTRs r"lot\s*number", ) # Matches a single label occurrence, capturing label and trailing token. # Heat code token: 4-15 chars, alphanumeric with optional dashes. _LABEL_ANCHORED_RE = re.compile( r"(?i)(?P