Initial commit

This commit is contained in:
Jack Levy
2026-02-28 21:08:19 -05:00
commit e418dd9ae0
85 changed files with 5261 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
from app.models.bill import Bill, BillAction, BillDocument
from app.models.brief import BillBrief
from app.models.follow import Follow
from app.models.member import Member
from app.models.news import NewsArticle
from app.models.setting import AppSetting
from app.models.trend import TrendScore
from app.models.committee import Committee, CommitteeBill
__all__ = [
"Bill",
"BillAction",
"BillDocument",
"BillBrief",
"Follow",
"Member",
"NewsArticle",
"AppSetting",
"TrendScore",
"Committee",
"CommitteeBill",
]

View File

@@ -0,0 +1,88 @@
from sqlalchemy import (
Column, String, Integer, Date, DateTime, Text, ForeignKey, Index
)
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class Bill(Base):
__tablename__ = "bills"
# Natural key: "{congress}-{bill_type_lower}-{bill_number}" e.g. "119-hr-1234"
bill_id = Column(String, primary_key=True)
congress_number = Column(Integer, nullable=False)
bill_type = Column(String(10), nullable=False) # hr, s, hjres, sjres, hconres, sconres, hres, sres
bill_number = Column(Integer, nullable=False)
title = Column(Text)
short_title = Column(Text)
sponsor_id = Column(String, ForeignKey("members.bioguide_id"), nullable=True)
introduced_date = Column(Date)
latest_action_date = Column(Date)
latest_action_text = Column(Text)
status = Column(String(100))
chamber = Column(String(50))
congress_url = Column(String)
govtrack_url = Column(String)
# Ingestion tracking
last_checked_at = Column(DateTime(timezone=True))
actions_fetched_at = Column(DateTime(timezone=True))
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
sponsor = relationship("Member", back_populates="bills", foreign_keys=[sponsor_id])
actions = relationship("BillAction", back_populates="bill", order_by="desc(BillAction.action_date)")
documents = relationship("BillDocument", back_populates="bill")
briefs = relationship("BillBrief", back_populates="bill", order_by="desc(BillBrief.created_at)")
news_articles = relationship("NewsArticle", back_populates="bill", order_by="desc(NewsArticle.published_at)")
trend_scores = relationship("TrendScore", back_populates="bill", order_by="desc(TrendScore.score_date)")
committee_bills = relationship("CommitteeBill", back_populates="bill")
__table_args__ = (
Index("ix_bills_congress_number", "congress_number"),
Index("ix_bills_latest_action_date", "latest_action_date"),
Index("ix_bills_introduced_date", "introduced_date"),
Index("ix_bills_chamber", "chamber"),
Index("ix_bills_sponsor_id", "sponsor_id"),
)
class BillAction(Base):
__tablename__ = "bill_actions"
id = Column(Integer, primary_key=True, autoincrement=True)
bill_id = Column(String, ForeignKey("bills.bill_id", ondelete="CASCADE"), nullable=False)
action_date = Column(Date)
action_text = Column(Text)
action_type = Column(String(100))
chamber = Column(String(50))
created_at = Column(DateTime(timezone=True), server_default=func.now())
bill = relationship("Bill", back_populates="actions")
__table_args__ = (
Index("ix_bill_actions_bill_id", "bill_id"),
Index("ix_bill_actions_action_date", "action_date"),
)
class BillDocument(Base):
__tablename__ = "bill_documents"
id = Column(Integer, primary_key=True, autoincrement=True)
bill_id = Column(String, ForeignKey("bills.bill_id", ondelete="CASCADE"), nullable=False)
doc_type = Column(String(50)) # bill_text | committee_report | amendment
doc_version = Column(String(50)) # Introduced, Enrolled, etc.
govinfo_url = Column(String)
raw_text = Column(Text)
fetched_at = Column(DateTime(timezone=True))
created_at = Column(DateTime(timezone=True), server_default=func.now())
bill = relationship("Bill", back_populates="documents")
briefs = relationship("BillBrief", back_populates="document")
__table_args__ = (
Index("ix_bill_documents_bill_id", "bill_id"),
)

View File

@@ -0,0 +1,31 @@
from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime, Index
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class BillBrief(Base):
__tablename__ = "bill_briefs"
id = Column(Integer, primary_key=True, autoincrement=True)
bill_id = Column(String, ForeignKey("bills.bill_id", ondelete="CASCADE"), nullable=False)
document_id = Column(Integer, ForeignKey("bill_documents.id", ondelete="SET NULL"), nullable=True)
brief_type = Column(String(20), nullable=False, server_default="full") # full | amendment
summary = Column(Text)
key_points = Column(JSONB) # list[str]
risks = Column(JSONB) # list[str]
deadlines = Column(JSONB) # list[{date: str, description: str}]
topic_tags = Column(JSONB) # list[str]
llm_provider = Column(String(50))
llm_model = Column(String(100))
created_at = Column(DateTime(timezone=True), server_default=func.now())
bill = relationship("Bill", back_populates="briefs")
document = relationship("BillDocument", back_populates="briefs")
__table_args__ = (
Index("ix_bill_briefs_bill_id", "bill_id"),
Index("ix_bill_briefs_topic_tags", "topic_tags", postgresql_using="gin"),
)

View File

@@ -0,0 +1,33 @@
from sqlalchemy import Column, Integer, String, Date, ForeignKey, Index
from sqlalchemy.orm import relationship
from app.database import Base
class Committee(Base):
__tablename__ = "committees"
id = Column(Integer, primary_key=True, autoincrement=True)
committee_code = Column(String(20), unique=True, nullable=False)
name = Column(String(500))
chamber = Column(String(10))
committee_type = Column(String(50)) # Standing, Select, Joint, etc.
committee_bills = relationship("CommitteeBill", back_populates="committee")
class CommitteeBill(Base):
__tablename__ = "committee_bills"
id = Column(Integer, primary_key=True, autoincrement=True)
committee_id = Column(Integer, ForeignKey("committees.id", ondelete="CASCADE"), nullable=False)
bill_id = Column(String, ForeignKey("bills.bill_id", ondelete="CASCADE"), nullable=False)
referral_date = Column(Date)
committee = relationship("Committee", back_populates="committee_bills")
bill = relationship("Bill", back_populates="committee_bills")
__table_args__ = (
Index("ix_committee_bills_bill_id", "bill_id"),
Index("ix_committee_bills_committee_id", "committee_id"),
)

View File

@@ -0,0 +1,17 @@
from sqlalchemy import Column, Integer, String, DateTime, UniqueConstraint
from sqlalchemy.sql import func
from app.database import Base
class Follow(Base):
__tablename__ = "follows"
id = Column(Integer, primary_key=True, autoincrement=True)
follow_type = Column(String(20), nullable=False) # bill | member | topic
follow_value = Column(String, nullable=False) # bill_id | bioguide_id | tag string
created_at = Column(DateTime(timezone=True), server_default=func.now())
__table_args__ = (
UniqueConstraint("follow_type", "follow_value", name="uq_follows_type_value"),
)

View File

@@ -0,0 +1,24 @@
from sqlalchemy import Column, String, DateTime
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class Member(Base):
__tablename__ = "members"
bioguide_id = Column(String, primary_key=True)
name = Column(String, nullable=False)
first_name = Column(String)
last_name = Column(String)
party = Column(String(50))
state = Column(String(50))
chamber = Column(String(50))
district = Column(String(50))
photo_url = Column(String)
official_url = Column(String)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
bills = relationship("Bill", back_populates="sponsor", foreign_keys="Bill.sponsor_id")

View File

@@ -0,0 +1,25 @@
from sqlalchemy import Column, Integer, String, Text, Float, DateTime, ForeignKey, Index
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class NewsArticle(Base):
__tablename__ = "news_articles"
id = Column(Integer, primary_key=True, autoincrement=True)
bill_id = Column(String, ForeignKey("bills.bill_id", ondelete="CASCADE"), nullable=False)
source = Column(String(200))
headline = Column(Text)
url = Column(String, unique=True)
published_at = Column(DateTime(timezone=True))
relevance_score = Column(Float, default=0.0)
created_at = Column(DateTime(timezone=True), server_default=func.now())
bill = relationship("Bill", back_populates="news_articles")
__table_args__ = (
Index("ix_news_articles_bill_id", "bill_id"),
Index("ix_news_articles_published_at", "published_at"),
)

View File

@@ -0,0 +1,12 @@
from sqlalchemy import Column, String, DateTime
from sqlalchemy.sql import func
from app.database import Base
class AppSetting(Base):
__tablename__ = "app_settings"
key = Column(String, primary_key=True)
value = Column(String)
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())

View File

@@ -0,0 +1,25 @@
from sqlalchemy import Column, Integer, String, Date, Float, ForeignKey, Index, UniqueConstraint
from sqlalchemy.orm import relationship
from app.database import Base
class TrendScore(Base):
__tablename__ = "trend_scores"
id = Column(Integer, primary_key=True, autoincrement=True)
bill_id = Column(String, ForeignKey("bills.bill_id", ondelete="CASCADE"), nullable=False)
score_date = Column(Date, nullable=False)
newsapi_count = Column(Integer, default=0)
gnews_count = Column(Integer, default=0)
gtrends_score = Column(Float, default=0.0)
composite_score = Column(Float, default=0.0)
bill = relationship("Bill", back_populates="trend_scores")
__table_args__ = (
UniqueConstraint("bill_id", "score_date", name="uq_trend_scores_bill_date"),
Index("ix_trend_scores_bill_id", "bill_id"),
Index("ix_trend_scores_score_date", "score_date"),
Index("ix_trend_scores_composite", "composite_score"),
)