| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145 |
- #!/usr/bin/env python3
- """Fetch all House roll call votes from 119th Congress and analyze Massie's record."""
- import urllib.request
- import xml.etree.ElementTree as ET
- import json
- import time
- import os
- import sys
- MASSIE_ID = "M001184"
- YEARS = {2025: 362, 2026: 191}
- CACHE_DIR = "/home/user/polisci/vote_cache"
- os.makedirs(CACHE_DIR, exist_ok=True)
- UA = "Mozilla/5.0 (research; polisci-analysis)"
- def fetch(year, roll):
- path = f"{CACHE_DIR}/{year}_{roll:03d}.xml"
- if os.path.exists(path) and os.path.getsize(path) > 200:
- with open(path, "rb") as f:
- return f.read()
- url = f"https://clerk.house.gov/evs/{year}/roll{roll:03d}.xml"
- req = urllib.request.Request(url, headers={"User-Agent": UA})
- try:
- with urllib.request.urlopen(req, timeout=30) as r:
- data = r.read()
- with open(path, "wb") as f:
- f.write(data)
- time.sleep(0.35) # throttle
- return data
- except Exception as e:
- print(f"FAIL {year}/{roll}: {e}", file=sys.stderr)
- return None
- def parse(data, year, roll):
- try:
- root = ET.fromstring(data)
- except Exception as e:
- return None
- meta = root.find("vote-metadata")
- if meta is None:
- return None
- def t(tag):
- el = meta.find(tag)
- return (el.text or "").strip() if el is not None else ""
- info = {
- "year": year, "roll": roll,
- "date": t("action-date"),
- "question": t("vote-question"),
- "result": t("vote-result"),
- "legis_num": t("legis-num"),
- "desc": t("vote-desc"),
- "majority": t("majority"),
- }
- # party totals
- party_totals = {}
- for pt in meta.findall("vote-totals/totals-by-party"):
- party = pt.findtext("party", "").strip()
- party_totals[party] = {
- "yea": int(pt.findtext("yea-total", "0") or 0),
- "nay": int(pt.findtext("nay-total", "0") or 0),
- "present": int(pt.findtext("present-total", "0") or 0),
- "nv": int(pt.findtext("not-voting-total", "0") or 0),
- }
- info["R"] = party_totals.get("Republican", {"yea":0,"nay":0,"present":0,"nv":0})
- info["D"] = party_totals.get("Democratic", {"yea":0,"nay":0,"present":0,"nv":0})
- info["I"] = party_totals.get("Independent", {"yea":0,"nay":0,"present":0,"nv":0})
- # Massie's vote
- massie = None
- for rv in root.iter("recorded-vote"):
- leg = rv.find("legislator")
- if leg is not None and leg.get("name-id") == MASSIE_ID:
- v = rv.find("vote")
- massie = (v.text or "").strip() if v is not None else None
- break
- info["massie"] = massie
- return info
- def classify(v):
- """Given parsed vote, return (alignment, blocked_side)."""
- r_yea, r_nay = v["R"]["yea"], v["R"]["nay"]
- d_yea, d_nay = v["D"]["yea"], v["D"]["nay"]
- # Determine each party's majority position
- r_pos = "Yea" if r_yea > r_nay else ("Nay" if r_nay > r_yea else "Split")
- d_pos = "Yea" if d_yea > d_nay else ("Nay" if d_nay > d_yea else "Split")
- m = v["massie"]
- if m not in ("Yea", "Nay", "Aye", "No"):
- return ("N/A: " + (m or "absent"), None, r_pos, d_pos)
- # Normalize Aye/No to Yea/Nay
- m_norm = "Yea" if m in ("Yea", "Aye") else "Nay"
- helped_r = (r_pos != "Split" and m_norm == r_pos)
- helped_d = (d_pos != "Split" and m_norm == d_pos)
- if helped_r and helped_d:
- align = "Helped Both"
- elif helped_r:
- align = "Helped Republicans"
- elif helped_d:
- align = "Helped Democrats"
- else:
- align = "Helped Neither"
- # Blocking analysis: Massie voted against [side]'s majority position, AND that side lost the vote
- result = v["result"].lower()
- measure_passed = result in ("passed", "agreed to", "adopted")
- measure_failed = "fail" in result or "reject" in result or "not agreed" in result or "not passed" in result
- blocked = None
- # "Dem-backed measure": D majority was Yea -> they wanted it to pass.
- # If D wanted Yea, measure failed, and Massie voted Nay -> Massie helped block a Dem-backed measure.
- if d_pos == "Yea" and measure_failed and m_norm == "Nay":
- if r_pos != "Yea": # only count if Rs didn't also back it -> actually a partisan block
- blocked = "Democrat"
- # Also if D wanted Nay (to defeat it) but it passed and Massie voted Yea... that's not blocking, that's helping pass
- if r_pos == "Yea" and measure_failed and m_norm == "Nay":
- if d_pos != "Yea":
- blocked = "Republican"
- return (align, blocked, r_pos, d_pos)
- def main():
- all_votes = []
- total = sum(YEARS.values())
- done = 0
- for year, max_roll in YEARS.items():
- for roll in range(1, max_roll + 1):
- data = fetch(year, roll)
- done += 1
- if done % 25 == 0:
- print(f" fetched {done}/{total}", file=sys.stderr)
- if not data:
- continue
- v = parse(data, year, roll)
- if not v:
- continue
- align, blocked, r_pos, d_pos = classify(v)
- v["alignment"] = align
- v["blocked"] = blocked
- v["r_pos"] = r_pos
- v["d_pos"] = d_pos
- all_votes.append(v)
- with open("/home/user/polisci/votes.json", "w") as f:
- json.dump(all_votes, f)
- print(f"Saved {len(all_votes)} votes", file=sys.stderr)
- if __name__ == "__main__":
- main()
|