From 3dadb145b7ae5cb4cd2d9f7c1fc71b679289538a Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Mon, 9 Mar 2026 14:03:35 -0400 Subject: [PATCH] added congress data to database --- ...dd_congress_tracker_tables_3f71565e38de.py | 135 ++++++++++++++++ python/orm/richie/__init__.py | 5 + python/orm/richie/congress.py | 150 ++++++++++++++++++ 3 files changed, 290 insertions(+) create mode 100644 python/alembic/richie/versions/2026_02_12-add_congress_tracker_tables_3f71565e38de.py create mode 100644 python/orm/richie/congress.py diff --git a/python/alembic/richie/versions/2026_02_12-add_congress_tracker_tables_3f71565e38de.py b/python/alembic/richie/versions/2026_02_12-add_congress_tracker_tables_3f71565e38de.py new file mode 100644 index 0000000..e044930 --- /dev/null +++ b/python/alembic/richie/versions/2026_02_12-add_congress_tracker_tables_3f71565e38de.py @@ -0,0 +1,135 @@ +"""add congress tracker tables. + +Revision ID: 3f71565e38de +Revises: edd7dd61a3d2 +Create Date: 2026-02-12 16:36:09.457303 + +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import sqlalchemy as sa +from alembic import op + +from python.orm import RichieBase + +if TYPE_CHECKING: + from collections.abc import Sequence + +# revision identifiers, used by Alembic. +revision: str = "3f71565e38de" +down_revision: str | None = "edd7dd61a3d2" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + +schema = RichieBase.schema_name + + +def upgrade() -> None: + """Upgrade.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "bill", + sa.Column("congress", sa.Integer(), nullable=False), + sa.Column("bill_type", sa.String(), nullable=False), + sa.Column("number", sa.Integer(), nullable=False), + sa.Column("title", sa.String(), nullable=True), + sa.Column("title_short", sa.String(), nullable=True), + sa.Column("official_title", sa.String(), nullable=True), + sa.Column("status", sa.String(), nullable=True), + sa.Column("status_at", sa.Date(), nullable=True), + sa.Column("sponsor_bioguide_id", sa.String(), nullable=True), + sa.Column("subjects_top_term", sa.String(), nullable=True), + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), + sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), + sa.PrimaryKeyConstraint("id", name=op.f("pk_bill")), + sa.UniqueConstraint("congress", "bill_type", "number", name="uq_bill_congress_type_number"), + schema=schema, + ) + op.create_index("ix_bill_congress", "bill", ["congress"], unique=False, schema=schema) + op.create_table( + "legislator", + sa.Column("bioguide_id", sa.Text(), nullable=False), + sa.Column("thomas_id", sa.String(), nullable=True), + sa.Column("lis_id", sa.String(), nullable=True), + sa.Column("govtrack_id", sa.Integer(), nullable=True), + sa.Column("opensecrets_id", sa.String(), nullable=True), + sa.Column("fec_ids", sa.String(), nullable=True), + sa.Column("first_name", sa.String(), nullable=False), + sa.Column("last_name", sa.String(), nullable=False), + sa.Column("official_full_name", sa.String(), nullable=True), + sa.Column("nickname", sa.String(), nullable=True), + sa.Column("birthday", sa.Date(), nullable=True), + sa.Column("gender", sa.String(), nullable=True), + sa.Column("current_party", sa.String(), nullable=True), + sa.Column("current_state", sa.String(), nullable=True), + sa.Column("current_district", sa.Integer(), nullable=True), + sa.Column("current_chamber", sa.String(), nullable=True), + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), + sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), + sa.PrimaryKeyConstraint("id", name=op.f("pk_legislator")), + schema=schema, + ) + op.create_index(op.f("ix_legislator_bioguide_id"), "legislator", ["bioguide_id"], unique=True, schema=schema) + op.create_table( + "vote", + sa.Column("congress", sa.Integer(), nullable=False), + sa.Column("chamber", sa.String(), nullable=False), + sa.Column("session", sa.Integer(), nullable=False), + sa.Column("number", sa.Integer(), nullable=False), + sa.Column("vote_type", sa.String(), nullable=True), + sa.Column("question", sa.String(), nullable=True), + sa.Column("result", sa.String(), nullable=True), + sa.Column("result_text", sa.String(), nullable=True), + sa.Column("vote_date", sa.Date(), nullable=False), + sa.Column("yea_count", sa.Integer(), nullable=True), + sa.Column("nay_count", sa.Integer(), nullable=True), + sa.Column("not_voting_count", sa.Integer(), nullable=True), + sa.Column("present_count", sa.Integer(), nullable=True), + sa.Column("bill_id", sa.Integer(), nullable=True), + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), + sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), + sa.ForeignKeyConstraint(["bill_id"], [f"{schema}.bill.id"], name=op.f("fk_vote_bill_id_bill")), + sa.PrimaryKeyConstraint("id", name=op.f("pk_vote")), + sa.UniqueConstraint("congress", "chamber", "session", "number", name="uq_vote_congress_chamber_session_number"), + schema=schema, + ) + op.create_index("ix_vote_congress_chamber", "vote", ["congress", "chamber"], unique=False, schema=schema) + op.create_index("ix_vote_date", "vote", ["vote_date"], unique=False, schema=schema) + op.create_table( + "vote_record", + sa.Column("vote_id", sa.Integer(), nullable=False), + sa.Column("legislator_id", sa.Integer(), nullable=False), + sa.Column("position", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["legislator_id"], + [f"{schema}.legislator.id"], + name=op.f("fk_vote_record_legislator_id_legislator"), + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + ["vote_id"], [f"{schema}.vote.id"], name=op.f("fk_vote_record_vote_id_vote"), ondelete="CASCADE" + ), + sa.PrimaryKeyConstraint("vote_id", "legislator_id", name=op.f("pk_vote_record")), + schema=schema, + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("vote_record", schema=schema) + op.drop_index("ix_vote_date", table_name="vote", schema=schema) + op.drop_index("ix_vote_congress_chamber", table_name="vote", schema=schema) + op.drop_table("vote", schema=schema) + op.drop_index(op.f("ix_legislator_bioguide_id"), table_name="legislator", schema=schema) + op.drop_table("legislator", schema=schema) + op.drop_index("ix_bill_congress", table_name="bill", schema=schema) + op.drop_table("bill", schema=schema) + # ### end Alembic commands ### diff --git a/python/orm/richie/__init__.py b/python/orm/richie/__init__.py index 9543ad7..762387d 100644 --- a/python/orm/richie/__init__.py +++ b/python/orm/richie/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from python.orm.richie.base import RichieBase, TableBase +from python.orm.richie.congress import Bill, Legislator, Vote, VoteRecord from python.orm.richie.contact import ( Contact, ContactNeed, @@ -12,11 +13,15 @@ from python.orm.richie.contact import ( ) __all__ = [ + "Bill", "Contact", "ContactNeed", "ContactRelationship", + "Legislator", "Need", "RelationshipType", "RichieBase", "TableBase", + "Vote", + "VoteRecord", ] diff --git a/python/orm/richie/congress.py b/python/orm/richie/congress.py new file mode 100644 index 0000000..0a23b20 --- /dev/null +++ b/python/orm/richie/congress.py @@ -0,0 +1,150 @@ +"""Congress Tracker database models.""" + +from __future__ import annotations + +from datetime import date + +from sqlalchemy import ForeignKey, Index, Text, UniqueConstraint +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from python.orm.richie.base import RichieBase, TableBase + + +class Legislator(TableBase): + """Legislator model - members of Congress.""" + + __tablename__ = "legislator" + + # Natural key - bioguide ID is the authoritative identifier + bioguide_id: Mapped[str] = mapped_column(Text, unique=True, index=True) + + # Other IDs for cross-referencing + thomas_id: Mapped[str | None] + lis_id: Mapped[str | None] + govtrack_id: Mapped[int | None] + opensecrets_id: Mapped[str | None] + fec_ids: Mapped[str | None] # JSON array stored as string + + # Name info + first_name: Mapped[str] + last_name: Mapped[str] + official_full_name: Mapped[str | None] + nickname: Mapped[str | None] + + # Bio + birthday: Mapped[date | None] + gender: Mapped[str | None] # M/F + + # Current term info (denormalized for query efficiency) + current_party: Mapped[str | None] + current_state: Mapped[str | None] + current_district: Mapped[int | None] # House only + current_chamber: Mapped[str | None] # rep/sen + + # Relationships + vote_records: Mapped[list[VoteRecord]] = relationship( + "VoteRecord", + back_populates="legislator", + cascade="all, delete-orphan", + ) + + +class Bill(TableBase): + """Bill model - legislation introduced in Congress.""" + + __tablename__ = "bill" + + # Composite natural key: congress + bill_type + number + congress: Mapped[int] + bill_type: Mapped[str] # hr, s, hres, sres, hjres, sjres + number: Mapped[int] + + # Bill info + title: Mapped[str | None] + title_short: Mapped[str | None] + official_title: Mapped[str | None] + + # Status + status: Mapped[str | None] + status_at: Mapped[date | None] + + # Sponsor + sponsor_bioguide_id: Mapped[str | None] + + # Subjects + subjects_top_term: Mapped[str | None] + + # Relationships + votes: Mapped[list[Vote]] = relationship( + "Vote", + back_populates="bill", + ) + + __table_args__ = ( + UniqueConstraint("congress", "bill_type", "number", name="uq_bill_congress_type_number"), + Index("ix_bill_congress", "congress"), + ) + + +class Vote(TableBase): + """Vote model - roll call votes in Congress.""" + + __tablename__ = "vote" + + # Composite natural key: congress + chamber + session + number + congress: Mapped[int] + chamber: Mapped[str] # house/senate + session: Mapped[int] + number: Mapped[int] + + # Vote details + vote_type: Mapped[str | None] + question: Mapped[str | None] + result: Mapped[str | None] + result_text: Mapped[str | None] + + # Timing + vote_date: Mapped[date] + + # Vote counts (denormalized for efficiency) + yea_count: Mapped[int | None] + nay_count: Mapped[int | None] + not_voting_count: Mapped[int | None] + present_count: Mapped[int | None] + + # Related bill (optional - not all votes are on bills) + bill_id: Mapped[int | None] = mapped_column(ForeignKey("main.bill.id")) + + # Relationships + bill: Mapped[Bill | None] = relationship("Bill", back_populates="votes") + vote_records: Mapped[list[VoteRecord]] = relationship( + "VoteRecord", + back_populates="vote", + cascade="all, delete-orphan", + ) + + __table_args__ = ( + UniqueConstraint("congress", "chamber", "session", "number", name="uq_vote_congress_chamber_session_number"), + Index("ix_vote_date", "vote_date"), + Index("ix_vote_congress_chamber", "congress", "chamber"), + ) + + +class VoteRecord(RichieBase): + """Association table: Vote <-> Legislator with position.""" + + __tablename__ = "vote_record" + + vote_id: Mapped[int] = mapped_column( + ForeignKey("main.vote.id", ondelete="CASCADE"), + primary_key=True, + ) + legislator_id: Mapped[int] = mapped_column( + ForeignKey("main.legislator.id", ondelete="CASCADE"), + primary_key=True, + ) + position: Mapped[str] # Yea, Nay, Not Voting, Present + + # Relationships + vote: Mapped[Vote] = relationship("Vote", back_populates="vote_records") + legislator: Mapped[Legislator] = relationship("Legislator", back_populates="vote_records")