● Zion Boggan
repos/Docket/services/ingest/ingest/pollers/govinfo.py
zionboggan.com ↗
105 lines · python
History for this file →
1
"""
2
GovInfo API poller  -  full bill text, Congressional Record, public laws.
3
 
4
Endpoint: GET https://api.govinfo.gov/published/{start}/{end}
5
Auth:      same api.data.gov key as Congress.gov (DOCKET_CONGRESS_KEY)
6
Rate:      36,000 req/hr  -  most generous of the four primary sources.
7
Lag:       1-2 business days behind the legislative action.
8
 
9
If DOCKET_CONGRESS_KEY is not set, this poller skips gracefully.
10
"""
11
 
12
import json
13
import logging
14
import os
15
import sqlite3
16
from datetime import datetime, timedelta
17
from typing import Optional
18
 
19
import requests
20
 
21
from ingest.change_detection import is_new_event, mark_seen
22
from ingest.db import get_connection, init_schema
23
from ingest.source_validator import validate_source_url
24
 
25
logger = logging.getLogger(__name__)
26
 
27
SOURCE = "govinfo"
28
BASE_URL = "https://api.govinfo.gov"
29
 
30
COLLECTIONS = ["BILLS", "PLAW", "CREC"]
31
 
32
 
33
def poll(days_back: int = 2, db_path: Optional[str] = None) -> int:
34
    """
35
    Fetch recently published GovInfo packages (bill text, public laws, CR).
36
 
37
    If DOCKET_CONGRESS_KEY is not set, skips gracefully and returns 0.
38
    """
39
    api_key = os.environ.get("DOCKET_CONGRESS_KEY")
40
    if not api_key:
41
        logger.warning(
42
            "GovInfo poller: DOCKET_CONGRESS_KEY is not set. "
43
            "Skipping GovInfo source (same key as Congress.gov - see congress.py)."
44
        )
45
        return 0
46
 
47
    end_dt   = datetime.utcnow()
48
    start_dt = end_dt - timedelta(days=days_back)
49
    start_str = start_dt.strftime("%Y-%m-%dT%H:%M:%SZ")
50
    end_str   = end_dt.strftime("%Y-%m-%dT%H:%M:%SZ")
51
 
52
    conn = get_connection(db_path) if db_path else get_connection()
53
    init_schema(conn)
54
    stored = 0
55
 
56
    for collection in COLLECTIONS:
57
        url = f"{BASE_URL}/published/{start_str}/{end_str}"
58
        params = {
59
            "api_key": api_key,
60
            "collection": collection,
61
            "pageSize": 100,
62
        }
63
        try:
64
            resp = requests.get(url, params=params, timeout=30)
65
            resp.raise_for_status()
66
        except requests.RequestException as exc:
67
            logger.error("GovInfo %s request failed: %s", collection, exc)
68
            continue
69
 
70
        data = resp.json()
71
        packages = data.get("packages", [])
72
        logger.info("GovInfo %s: received %d package(s)", collection, len(packages))
73
 
74
        for pkg in packages:
75
            pkg_id      = pkg.get("packageId", "")
76
            update_date = pkg.get("dateIssued", pkg.get("lastModified", ""))
77
 
78
            if not pkg_id or not is_new_event(conn, SOURCE, pkg_id, update_date):
79
                continue
80
 
81
            source_url = pkg.get("packageLink", f"https://www.govinfo.gov/content/pkg/{pkg_id}/pdf/{pkg_id}.pdf")
82
            try:
83
                validate_source_url(source_url)
84
            except Exception as exc:
85
                logger.error("GovInfo: invalid source_url %s: %s", source_url, exc)
86
                continue
87
 
88
            conn.execute(
89
                """
90
                INSERT INTO raw_events (source, external_id, payload, fetched_at)
91
                VALUES (?, ?, ?, ?)
92
                ON CONFLICT(source, external_id) DO UPDATE SET
93
                    payload    = excluded.payload,
94
                    fetched_at = excluded.fetched_at
95
                """,
96
                (SOURCE, pkg_id, json.dumps(pkg), datetime.utcnow().isoformat()),
97
            )
98
 
99
            mark_seen(conn, SOURCE, pkg_id, update_date)
100
            stored += 1
101
 
102
    conn.commit()
103
    conn.close()
104
    logger.info("GovInfo: stored %d new package(s)", stored)
105
    return stored