← back to Bankrupt Leads

src/poller.py

87 lines

"""
CourtListener RECAP poller.

Pulls recent Ch. 7 / 11 / Subchapter V filings from the free RECAP archive.
Docs: https://www.courtlistener.com/help/api/rest/recap/

Usage:
    python poller.py --since 2026-04-01 --courts nysb,cacb,txsb
"""
from __future__ import annotations

import argparse
import os
from datetime import date, datetime, timedelta

import httpx
import psycopg

COURTLISTENER_TOKEN = os.environ.get("COURTLISTENER_TOKEN")
PG_DSN = os.environ.get("BANKRUPT_LEADS_PG", "postgresql:///bankrupt_leads")

BASE = "https://www.courtlistener.com/api/rest/v4"
HEADERS = {"Authorization": f"Token {COURTLISTENER_TOKEN}"} if COURTLISTENER_TOKEN else {}

# Chapter codes appear in the docket nature_of_suit / cause fields inconsistently
# across courts — we filter downstream once we have the filing text.
BANKRUPTCY_NATURE = "bankruptcy"


def fetch_new_dockets(court: str, since: date) -> list[dict]:
    """List dockets filed >= `since` in a given bankruptcy court."""
    params = {
        "court": court,
        "date_filed__gte": since.isoformat(),
        "nature_of_suit__icontains": BANKRUPTCY_NATURE,
        "order_by": "-date_filed",
        "page_size": 100,
    }
    with httpx.Client(headers=HEADERS, timeout=30) as client:
        r = client.get(f"{BASE}/dockets/", params=params)
        r.raise_for_status()
        return r.json().get("results", [])


def upsert_case(conn, docket: dict, court: str) -> None:
    case_id = f"{court}-{docket['docket_number']}"
    with conn.cursor() as cur:
        cur.execute(
            """
            INSERT INTO bankrupt_leads.cases
                (case_id, court, docket_number, chapter, debtor_name, filing_date)
            VALUES (%s, %s, %s, %s, %s, %s)
            ON CONFLICT (case_id) DO UPDATE SET last_checked_at = now()
            """,
            (
                case_id,
                court,
                docket["docket_number"],
                # Chapter is inferred later from filing text; placeholder for now.
                "unknown",
                docket.get("case_name", "")[:500],
                docket["date_filed"],
            ),
        )


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("--since", default=(date.today() - timedelta(days=1)).isoformat())
    ap.add_argument("--courts", default="nysb,cacb,txsb,deb,njb")
    args = ap.parse_args()

    since = datetime.fromisoformat(args.since).date()
    courts = [c.strip() for c in args.courts.split(",") if c.strip()]

    with psycopg.connect(PG_DSN) as conn:
        for court in courts:
            dockets = fetch_new_dockets(court, since)
            print(f"{court}: {len(dockets)} new dockets since {since}")
            for d in dockets:
                upsert_case(conn, d, court)
            conn.commit()


if __name__ == "__main__":
    main()