Initial commit

This commit is contained in:
kfj001 committed 2026-09-06 10:18:26 -07:00
commit 5f2db08b43
100 files changed
+16985

No files matched your search

+335
View File
@@ -0,0 +1,335 @@
"""Read-only access to the dashboard's data sources (spec §5).
The dashboard never mutates the relational DB. It reads:
* the flat ``v_daily_file_records`` view (or ``daily_file_records``) for the
upcoming-events queue — one bill event per ``(date, bill_id)``, enriched
with per-bill ``tags`` from the LLM lane's ``bill_tags`` table;
* ``bill_tags`` for the tag cloud (§4.1);
* ``members`` / ``committees`` / ``bill_committees`` / ``committee_members`` for
the legislators reference data (§3.2 / §4.2) and the
committee-of-watched-legislator float path.
Everything here returns plain JSON-serialisable dicts so the API layer can
ship them verbatim into the API_CONTRACT shapes.
"""
from __future__ import annotations
import os
import re
import sqlite3
from typing import Any, Dict, List, Optional, Sequence
from .. import relational
FLAT_TABLE = relational.FLAT_TABLE
VIEW_NAME = "v_" + FLAT_TABLE
# Fields a bill event carries into the API_CONTRACT §1 Bill shape.
# `tags` is injected separately (from bill_tags), not a stored column.
_EVENT_FIELDS = (
"bill_id", "bill_number", "chamber_origin", "author", "short_title",
"description", "committee", "hearing_time", "location", "date",
"session", "item_number", "supplement", "source",
)
def _slug(name: Optional[str]) -> str:
if not name:
return ""
return re.sub(r"[^a-z0-9]+", "-", name.strip().lower()).strip("-") or "unknown"
def _title_case(s: str) -> str:
return " ".join(w[:1].upper() + w[1:] for w in s.split() if w)
def _table_exists(conn: sqlite3.Connection, name: str) -> bool:
cur = conn.execute(
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (name,))
return cur.fetchone() is not None
def _view_exists(conn: sqlite3.Connection, name: str) -> bool:
cur = conn.execute(
"SELECT 1 FROM sqlite_master WHERE type='view' AND name=?", (name,))
return cur.fetchone() is not None
def _flat_source(conn: sqlite3.Connection) -> str:
"""Prefer the flat-preserving view (the QA contract); fall back to the table."""
return VIEW_NAME if _view_exists(conn, VIEW_NAME) else FLAT_TABLE
# ---------------------------------------------------------------------------
# Tags (LLM lane: bill_tags table, §4.1)
# ---------------------------------------------------------------------------
def load_bill_tags(db_path: str = relational.DEFAULT_DB) -> Dict[str, List[str]]:
"""Map ``bill_id`` -> [tag, ...] from the ``bill_tags`` table.
Absent/empty table -> ``{}`` so a bill with no analysis degrades to
``tags: []`` (DESIGN §6.6 — no chips, ranking by legislator only).
"""
tags: Dict[str, List[str]] = {}
if not os.path.exists(db_path):
return tags
conn = relational.connect(db_path, foreign_keys=False)
try:
if not _table_exists(conn, "bill_tags"):
return tags
rows = conn.execute(
"SELECT bill_id, tag FROM bill_tags "
"WHERE bill_id IS NOT NULL AND tag IS NOT NULL AND tag != '' "
"ORDER BY bill_id, tag").fetchall()
finally:
conn.close()
for r in rows:
bid = r["bill_id"]
tag = r["tag"]
bucket = tags.setdefault(bid, [])
if tag not in bucket:
bucket.append(tag)
return tags
def load_tag_cloud(db_path: str = relational.DEFAULT_DB,
as_of: Optional[str] = None,
top: Optional[int] = None) -> Dict[str, Any]:
"""The /tags cloud (§4.1): ``[{tag_id, label, count}, ...]`` sorted desc.
Count = number of bills carrying the tag. May be sparse early (LLM lane
warming) — the UI shows a "filling in" hint, never a 0-count chip.
"""
import datetime
conn = relational.connect(db_path, foreign_keys=False)
try:
if not _table_exists(conn, "bill_tags"):
data = []
else:
data = conn.execute(
"SELECT tag, COUNT(DISTINCT bill_id) AS n FROM bill_tags "
"WHERE tag IS NOT NULL AND tag != '' GROUP BY tag "
"ORDER BY n DESC, tag").fetchall()
finally:
conn.close()
out = []
for r in data:
out.append({
"tag_id": "tag:" + str(r["tag"]),
"label": _title_case(str(r["tag"])),
"count": int(r["n"]),
})
if top:
out = out[:top]
return {
"as_of": as_of or _now_iso(),
"tags": out,
}
def _now_iso() -> str:
import datetime
return datetime.datetime.now(datetime.timezone.utc).strftime(
"%Y-%m-%dT%H:%M:%SZ")
# ---------------------------------------------------------------------------
# Upcoming-events queue (the daily-file calendar, §2 / §5 "upcoming")
# ---------------------------------------------------------------------------
def _in_window(date: Optional[str], frm: Optional[str], to: Optional[str],
as_of: Optional[str]) -> bool:
"""API_CONTRACT §5 'upcoming' gate on event_date (= date here).
* a T-1 day (date < as_of) is EXCLUDED when an as_of floor is set;
* 'this week'/'this month' are from/to windows over event_date.
Dates are ISO ``YYYY-MM-DD`` so string compare == chronological.
"""
if not date:
return not frm and not to
if frm and date < frm:
return False
if to and date > to:
return False
if as_of and date < as_of:
return False
return True
def list_events(db_path: str = relational.DEFAULT_DB,
upcoming: bool = False,
from_date: Optional[str] = None,
to_date: Optional[str] = None,
chamber: Optional[str] = None,
tags: Optional[Sequence[str]] = None,
q: Optional[str] = None,
author: Optional[str] = None,
as_of: Optional[str] = None) -> List[Dict[str, Any]]:
"""Build the bill-event queue, one event per ``(date, bill_id)``.
* ``upcoming=True`` + ``as_of`` -> keep only future-dated events
(API_CONTRACT §5). When the caller omits ``as_of`` the default is the
latest date present, so a demo on a single historical day still shows its
queue; pin ``as_of`` for a true "future-of-now" view.
* dedupe *within* a date by ``bill_id`` (multiple agenda items the same day
collapse to one); across dates they are distinct events.
* enriched with per-bill ``tags`` from the LLM lane.
* optional filters: chamber, tags (any-match), free-text ``q``, author.
Returns list of dicts matching API_CONTRACT §1 **without** the ``relevance``
block (the API layer adds that for logged-in views).
"""
conn = relational.connect(db_path, foreign_keys=False)
try:
rows = conn.execute(
"SELECT * FROM " + _flat_source(conn) +
" WHERE bill_id IS NOT NULL AND bill_id != '' "
"ORDER BY date, house, bill_id, committee, id").fetchall()
finally:
conn.close()
flat = [dict(r) for r in rows]
bill_tags = load_bill_tags(db_path)
tag_filter = {t.strip().lower() for t in (tags or []) if str(t).strip()}
ql = (q or "").strip().lower()
al = (author or "").strip().lower()
if as_of is None:
dates = sorted({r["date"] for r in flat if r.get("date")})
as_of = dates[-1] if dates else None
# Collapse to one event per (date, bill_id); prefer a row carrying a hearing.
seen: Dict[tuple, Dict[str, Any]] = {}
order: List[tuple] = []
for r in flat:
key = (r.get("date"), r.get("bill_id"))
if key in seen:
if not seen[key].get("_has_time") and r.get("hearing_time"):
seen[key] = r
continue
r["_has_time"] = bool(r.get("hearing_time"))
seen[key] = r
order.append(key)
events = []
for key in order:
r = seen[key]
origin = str(r.get("chamber_origin") or r.get("house") or "").lower()
if chamber and origin != chamber.lower():
continue
if not _in_window(r.get("date"), from_date, to_date, as_of):
continue
btags = bill_tags.get(r["bill_id"], [])
if tag_filter and not (tag_filter & {t.lower() for t in btags}):
continue
if ql:
hay = " ".join(str(r.get(f) or "") for f in
("short_title", "description", "bill_number",
"author", "committee")).lower()
if ql not in hay:
continue
if al and al not in str(r.get("author") or "").lower():
continue
ev = {f: r.get(f) for f in _EVENT_FIELDS}
for f in ("hearing_time", "location", "committee", "short_title",
"author", "item_number", "supplement", "date", "session"):
if ev.get(f) == "":
ev[f] = None
ev["tags"] = btags if btags else None
ev["source"] = r.get("source") or "ASSEMBLY_DAILY_FILE"
events.append(ev)
return events
# ---------------------------------------------------------------------------
# Legislators + districts reference data (§3.2 / §4.2)
# ---------------------------------------------------------------------------
def load_legislators(db_path: str = relational.DEFAULT_DB,
q: Optional[str] = None) -> List[Dict[str, Any]]:
"""Browse legislators (members table) with their committees + domains.
``domains`` = the domain tags of the committees a member sits on
(committee -> its bills' tags). Degrades to an empty list when the
correlation lane has no roster (no committee_members).
"""
conn = relational.connect(db_path, foreign_keys=False)
try:
members = [dict(r) for r in conn.execute(
"SELECT name_norm, full_name, chamber, kind "
"FROM members ORDER BY full_name, chamber").fetchall()]
rows = conn.execute(
"SELECT cm.member, c.name_raw AS committee "
"FROM committee_members cm "
"JOIN committees c ON c.name_key=cm.committee "
"AND c.chamber=cm.chamber AND c.session=cm.session "
"ORDER BY cm.member, c.name_raw").fetchall()
served: Dict[str, List[str]] = {}
for r in rows:
served.setdefault(r["member"], []).append(r["committee"])
if _table_exists(conn, "bill_tags"):
cmt_rows = conn.execute(
"SELECT c.name_raw AS cn, t.tag FROM bill_committees bc "
"JOIN committees c ON c.name_key=bc.committee "
"AND c.chamber=bc.chamber AND c.session=bc.session "
"JOIN bill_tags t ON t.bill_id=bc.bill_id").fetchall()
cmt_tags: Dict[str, set] = {}
for r in cmt_rows:
cmt_tags.setdefault(r["cn"], set()).add(r["tag"])
else:
cmt_tags = {}
finally:
conn.close()
ql = (q or "").strip().lower()
out = []
for m in members:
slug = m["name_norm"]
committees = served.get(slug, [])
domains = sorted({t for cn in committees for t in cmt_tags.get(cn, set())})
if ql and ql not in str(m.get("full_name") or slug).lower():
continue
out.append({
"legislator_id": slug,
"name": m.get("full_name") or slug.title(),
"party": None,
"chamber": (m.get("chamber") or "").upper(),
"district": None,
"committees": committees,
"domains": domains,
})
return out
def committee_roster_for(db_path: str = relational.DEFAULT_DB,
legislator_ids=None) -> Dict[str, List[str]]:
"""Map a watched legislator (by its ``member`` slug) to the committee
name_keys that legislator is known to serve on, via the correlation lane's
``committee_members`` join.
Used by the ranking to fire the ``committee-of-watched-legislator`` float
path. When the correlation lane found no roster (``committee_members``
empty — the honest default), this returns ``{}`` and that path does not
fire, so nothing is fabricated.
"""
conn = relational.connect(db_path, foreign_keys=False)
try:
if not _table_exists(conn, "committee_members"):
return {}
rows = conn.execute(
"SELECT DISTINCT cm.member AS m, cm.committee AS ck "
"FROM committee_members cm").fetchall()
finally:
conn.close()
roster: Dict[str, List[str]] = {}
for r in rows:
m = r["m"]
if legislator_ids is None or m in legislator_ids:
bucket = roster.setdefault(m, [])
if r["ck"] not in bucket:
bucket.append(r["ck"])
return roster