themis / phase1 /scripts /bharat_courts_source.py
vg15o2's picture
Moonley backend (HF Space build)
1d9bd9b
Raw
History Blame Contribute Delete
6.3 kB
"""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"]