"""Bharat Courts adapter for Supreme Court PDF recovery. The fast path in :mod:`pdf_sources` opens a verified individual object from the public SCI AWS archive. Some otherwise valid judgments are absent from that object map. Bharat Courts can resolve the same archive metadata and extract the PDF from the official per-year tar bundle, so this module is deliberately used only as the slower fallback. """ from __future__ import annotations import asyncio from difflib import SequenceMatcher import os import re from typing import Any, Iterable class BharatCourtsPdfError(RuntimeError): pass _CLIENT: Any | None = None _CLIENT_LOCK: asyncio.Lock | None = None _YEAR_LOCKS: dict[int, asyncio.Lock] = {} _YEAR_ROWS: dict[int, list[Any]] = {} def _norm(value: object) -> str: return re.sub(r"[^a-z0-9]+", " ", str(value or "").lower()).strip() def _year_from(*values: object) -> int | None: for value in values: match = re.search(r"\b(19|20)\d{2}\b", str(value or "")) if match: return int(match.group(0)) return None def _cache_bytes() -> int: try: gib = max(1.0, float(os.environ.get("THEMIS_BHARAT_CACHE_GB", "5"))) except ValueError: gib = 5.0 return int(gib * 1024**3) async def _client(): global _CLIENT, _CLIENT_LOCK if _CLIENT is not None: return _CLIENT if _CLIENT_LOCK is None: _CLIENT_LOCK = asyncio.Lock() async with _CLIENT_LOCK: if _CLIENT is None: try: from bharat_courts import ArchiveClient except ImportError as exc: raise BharatCourtsPdfError( "Bharat Courts archive support is not installed" ) from exc _CLIENT = ArchiveClient( cache_dir=os.environ.get("THEMIS_PDF_CACHE", "/tmp/pdf_cache"), cache_max_bytes=_cache_bytes(), metadata_cache=False, ) return _CLIENT async def _year_judgments(year: int) -> list[Any]: if year in _YEAR_ROWS: return _YEAR_ROWS[year] lock = _YEAR_LOCKS.setdefault(year, asyncio.Lock()) async with lock: if year in _YEAR_ROWS: return _YEAR_ROWS[year] client = await _client() rows = [] async for judgment in client.iter_judgments( court="sci", year=year, batch_size=500, max_results=5000 ): rows.append(judgment) _YEAR_ROWS[year] = rows return rows def _best_match( rows: Iterable[Any], *, case_name: str, neutral_citation: str, equivalent_citations: Iterable[object], decision_date: str, ) -> Any | None: neutral = _norm(neutral_citation) equivalents = {_norm(value) for value in equivalent_citations if _norm(value)} title = _norm(case_name) wanted_date = str(decision_date or "")[:10] ranked = [] for row in rows: case_id = _norm(getattr(row, "case_id", "")) citation = _norm(getattr(row, "citation", "")) row_title = _norm(getattr(row, "title", "")) score = 0.0 exact_identity = bool(neutral and case_id == neutral) exact_reporter = bool(citation and citation in equivalents) if exact_identity: score += 200.0 if exact_reporter: score += 170.0 title_ratio = SequenceMatcher(None, title, row_title).ratio() if title and row_title else 0.0 score += 100.0 * title_ratio row_date = str(getattr(row, "decision_date", "") or "")[:10] if wanted_date and row_date == wanted_date: score += 25.0 if getattr(row, "pdf_path", None): score += 5.0 ranked.append((score, exact_identity or exact_reporter, title_ratio, row)) if not ranked: return None ranked.sort(key=lambda item: item[0], reverse=True) score, exact, title_ratio, row = ranked[0] # An identity/reporter match is conclusive. A title-only match must be # strong enough that a same-year namesake cannot silently supply a PDF. return row if exact or (title_ratio >= 0.78 and score >= 88.0) else None async def resolve_and_fetch_pdf( *, year: int | str | None, path: str | None, case_name: str, neutral_citation: str, equivalent_citations: Iterable[object], decision_date: str, ) -> tuple[bytes, dict[str, Any]]: """Resolve one SCI judgment and return verified PDF bytes plus provenance.""" resolved_year = int(year) if str(year or "").isdigit() else _year_from( decision_date, neutral_citation, *equivalent_citations ) if not resolved_year: raise BharatCourtsPdfError("judgment year is unavailable") client = await _client() judgment = None if path: try: from bharat_courts import Judgment, SUPREME_COURT except ImportError as exc: raise BharatCourtsPdfError( "Bharat Courts archive support is not installed" ) from exc judgment = Judgment( case_id=neutral_citation or None, title=case_name or None, court=SUPREME_COURT, pdf_path=str(path), source="archive", year=resolved_year, ) else: rows = await _year_judgments(resolved_year) judgment = _best_match( rows, case_name=case_name, neutral_citation=neutral_citation, equivalent_citations=equivalent_citations, decision_date=decision_date, ) if judgment is None or not getattr(judgment, "pdf_path", None): raise BharatCourtsPdfError("no unambiguous Bharat Courts PDF match") try: data = await client.fetch_pdf(judgment, language="english") except Exception as exc: raise BharatCourtsPdfError(str(exc)[:300]) from exc if not data.startswith(b"%PDF"): raise BharatCourtsPdfError("archive returned a non-PDF payload") return data, { "provider": "bharat_courts", "source_name": "Bharat Courts public archive", "case_id": getattr(judgment, "case_id", None), "year": resolved_year, "path": getattr(judgment, "pdf_path", None), } __all__ = ["BharatCourtsPdfError", "resolve_and_fetch_pdf"]