76 lines
2.2 KiB
Python
76 lines
2.2 KiB
Python
"""bill_id codec + session/chamber/number helpers, per spec §4."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Optional, Tuple
|
|
|
|
# Universal bill_id pattern across all daily-file links (spec §4).
|
|
# 20252026 0 AB 28 -> 202520260AB28
|
|
BILL_ID_RE = re.compile(
|
|
r"^(?P<session>\d{8})0?(?P<chamber>AB|SB|ACR|SCR|HR|SJR|ABR)"
|
|
r"(?P<number>\d+)$"
|
|
)
|
|
|
|
# Chamber prefix -> origin house (spec §8 #7: origin, not publishing house).
|
|
ASSEMBLY_PREFIXES = {"AB", "ACR", "HR", "ABR"}
|
|
SENATE_PREFIXES = {"SB", "SCR", "SJR"}
|
|
|
|
_SESSION_RE = re.compile(r"^(?P<a>\d{4})(?P<b>\d{4})$")
|
|
_BILL_DISPLAY_RE = re.compile(r"^(?P<prefix>AB|SB|ACR|SCR|HR|SJR|ABR)\s*(?P<num>\d+)$")
|
|
_SUPPLEMENT_RE = re.compile(r"Supplemental\s+(\d+)", re.IGNORECASE)
|
|
|
|
|
|
def origin_house(chamber: str) -> str:
|
|
if chamber in ASSEMBLY_PREFIXES:
|
|
return "assembly"
|
|
if chamber in SENATE_PREFIXES:
|
|
return "senate"
|
|
return "unknown"
|
|
|
|
|
|
def normalise_session(raw: str) -> str:
|
|
"""'20252026' -> '2025-2026'."""
|
|
m = _SESSION_RE.match(raw or "")
|
|
if m:
|
|
return f"{m['a']}-{m['b']}"
|
|
return raw
|
|
|
|
|
|
def parse_bill_id(bill_id: str) -> Optional[dict]:
|
|
"""Decode a canonical bill_id. Returns dict or None if it does not match.
|
|
|
|
Keys: session (YYYY-YYYY), session_raw, chamber (prefix upper), number
|
|
(int, unpadded), display (e.g. 'AB 28'), house (origin), raw.
|
|
"""
|
|
if not bill_id:
|
|
return None
|
|
m = BILL_ID_RE.match(bill_id.strip())
|
|
if not m:
|
|
return None
|
|
session_raw = m["session"]
|
|
chamber = m["chamber"].upper()
|
|
number = int(m["number"])
|
|
return {
|
|
"session": normalise_session(session_raw),
|
|
"session_raw": session_raw,
|
|
"chamber": chamber,
|
|
"number": number,
|
|
"display": f"{chamber} {number}",
|
|
"house": origin_house(chamber),
|
|
"raw": bill_id,
|
|
}
|
|
|
|
|
|
def parse_display(display: str) -> Optional[Tuple[str, int]]:
|
|
"""'AB 00028' or 'AB 28' -> ('AB', 28)."""
|
|
m = _BILL_DISPLAY_RE.match((display or "").strip())
|
|
if not m:
|
|
return None
|
|
return m["prefix"].upper(), int(m["num"])
|
|
|
|
|
|
def parse_supplement(text: str) -> Optional[int]:
|
|
m = _SUPPLEMENT_RE.search(text or "")
|
|
return int(m.group(1)) if m else None
|