mirror of
https://github.com/unitedstates/congress.git
synced 2025-12-19 17:16:58 -05:00
251 lines
9.7 KiB
Python
251 lines
9.7 KiB
Python
import re
|
|
import logging
|
|
import datetime
|
|
import time
|
|
import json
|
|
from lxml import etree
|
|
|
|
import utils
|
|
|
|
from bill_info import sponsor_for as sponsor_for_bill, action_for
|
|
|
|
def process_amendment(amdt_data, bill_id, options):
|
|
amdt = build_amendment_json_dict(amdt_data, options)
|
|
path = output_for_amdt(amdt['amendment_id'], "json")
|
|
|
|
logging.info("[%s] Saving %s to %s..." % (bill_id, amdt['amendment_id'], path))
|
|
|
|
# output JSON - so easy!
|
|
utils.write(
|
|
json.dumps(amdt, sort_keys=True, indent=2, default=utils.format_datetime),
|
|
path
|
|
)
|
|
|
|
with open(output_for_amdt(amdt['amendment_id'], "xml"), 'wb') as xml_file:
|
|
xml_file.write(create_govtrack_xml(amdt, options))
|
|
|
|
def build_amendment_json_dict(amdt_dict, options):
|
|
# good set of tests for each situation:
|
|
# samdt712-113 - amendment to bill
|
|
# samdt112-113 - amendment to amendment on bill
|
|
# samdt4904-111 - amendment to treaty
|
|
# samdt4922-111 - amendment to amendment to treaty
|
|
|
|
amendment_id = build_amendment_id(amdt_dict['type'].lower(), amdt_dict['number'], amdt_dict['congress'])
|
|
|
|
amends_bill = amends_bill_for(amdt_dict.get('amendedBill')) # almost always present
|
|
amends_treaty = None # amends_treaty_for(amdt_dict) # the bulk data does not provide amendments to treaties (THOMAS did)
|
|
amends_amendment = amends_amendment_for(amdt_dict.get('amendedAmendment')) # sometimes present
|
|
if not amends_bill and not amends_treaty:
|
|
raise Exception("Choked finding out what bill or treaty the amendment amends.")
|
|
|
|
actions = actions_for(amdt_dict['actions']['actions']['item'])
|
|
|
|
amdt = {
|
|
'amendment_id': amendment_id,
|
|
'amendment_type': amdt_dict['type'].lower(),
|
|
'chamber': amdt_dict['type'][0].lower(),
|
|
'number': int(amdt_dict['number']),
|
|
'congress': amdt_dict['congress'],
|
|
|
|
'amends_bill': amends_bill,
|
|
'amends_treaty': amends_treaty,
|
|
'amends_amendment': amends_amendment,
|
|
|
|
'sponsor': sponsor_for(amdt_dict['sponsors']['item'][0], amdt_dict['type'].lower()),
|
|
|
|
'purpose': amdt_dict['purpose'][0] if type(amdt_dict['purpose']) is list else amdt_dict['purpose'],
|
|
|
|
'introduced_at': amdt_dict['submittedDate'][:10],
|
|
'actions': actions,
|
|
|
|
'updated_at': amdt_dict['updateDate'],
|
|
}
|
|
|
|
# duplicate attributes creates lists when parsed, this block deduplicates
|
|
if 'description' in amdt_dict:
|
|
amdt['description'] = amdt_dict['description']
|
|
if type(amdt_dict['description']) is list:
|
|
amdt['description'] = amdt['description'][0]
|
|
|
|
if amdt_dict['type'][0].lower() == 's':
|
|
amdt['proposed_at'] = amdt_dict['proposedDate']
|
|
|
|
# needs to come *after* the setting of introduced_at
|
|
amdt['status'], amdt['status_at'] = amendment_status_for(amdt)
|
|
|
|
return amdt
|
|
|
|
|
|
def create_govtrack_xml(amdt, options):
|
|
govtrack_type_codes = {'hr': 'h', 's': 's', 'hres': 'hr', 'sres': 'sr', 'hjres': 'hj', 'sjres': 'sj', 'hconres': 'hc', 'sconres': 'sc'}
|
|
root = etree.Element("amendment")
|
|
root.set("session", amdt['congress'])
|
|
root.set("chamber", amdt['amendment_type'][0])
|
|
root.set("number", str(amdt['number']))
|
|
root.set("updated", utils.format_datetime(amdt['updated_at']))
|
|
|
|
make_node = utils.make_node
|
|
|
|
if amdt.get("amends_bill", None):
|
|
make_node(root, "amends", None,
|
|
type=govtrack_type_codes[amdt["amends_bill"]["bill_type"]],
|
|
number=str(amdt["amends_bill"]["number"]),
|
|
sequence=str(amdt["house_number"]) if amdt.get("house_number", None) else "")
|
|
elif amdt.get("amends_treaty", None):
|
|
make_node(root, "amends", None,
|
|
type="treaty",
|
|
number=str(amdt["amends_treaty"]["number"]))
|
|
|
|
make_node(root, "status", amdt['status'], datetime=amdt['status_at'])
|
|
|
|
if amdt['sponsor'] and amdt['sponsor']['type'] == 'person':
|
|
v = amdt['sponsor']['bioguide_id']
|
|
if not options.get("govtrack", False):
|
|
make_node(root, "sponsor", None, bioguide_id=v)
|
|
else:
|
|
v = str(utils.translate_legislator_id('bioguide', v, 'govtrack'))
|
|
make_node(root, "sponsor", None, id=v)
|
|
elif amdt['sponsor'] and amdt['sponsor']['type'] == 'committee':
|
|
make_node(root, "sponsor", None, committee=amdt['sponsor']['name'])
|
|
else:
|
|
make_node(root, "sponsor", None)
|
|
|
|
make_node(root, "offered", None, datetime=amdt['introduced_at'])
|
|
|
|
make_node(root, "description", amdt["description"] if amdt["description"] else amdt["purpose"])
|
|
if amdt["description"]:
|
|
make_node(root, "purpose", amdt["purpose"])
|
|
|
|
actions = make_node(root, "actions", None)
|
|
for action in amdt['actions']:
|
|
a = make_node(actions,
|
|
action['type'] if action['type'] in ("vote",) else "action",
|
|
None,
|
|
datetime=action['acted_at'])
|
|
if action['type'] == 'vote':
|
|
a.set("how", action["how"])
|
|
a.set("result", action["result"])
|
|
if action.get("roll") != None:
|
|
a.set("roll", str(action["roll"]))
|
|
if action.get('text'):
|
|
make_node(a, "text", action['text'])
|
|
if action.get('in_committee'):
|
|
make_node(a, "committee", None, name=action['in_committee'])
|
|
for cr in action['references']:
|
|
make_node(a, "reference", None, ref=cr['reference'], label=cr['type'])
|
|
|
|
return etree.tostring(root, pretty_print=True)
|
|
|
|
|
|
def build_amendment_id(amdt_type, amdt_number, congress):
|
|
return "%s%s-%s" % (amdt_type, amdt_number, congress)
|
|
|
|
def amends_bill_for(amends_bill):
|
|
from bills import build_bill_id
|
|
bill_id = build_bill_id(amends_bill['type'].lower(), amends_bill['number'], amends_bill['congress'])
|
|
return {
|
|
'bill_id': bill_id,
|
|
'bill_type': amends_bill['type'].lower(),
|
|
'congress': int(amends_bill['congress']),
|
|
'number': int(amends_bill['number'])
|
|
}
|
|
|
|
def amends_amendment_for(amends_amdt):
|
|
if amends_amdt is None:
|
|
return None
|
|
|
|
amdt_id = build_amendment_id(amends_amdt['type'].lower(), amends_amdt['number'], amends_amdt['congress'])
|
|
return {
|
|
'amendment_id': amdt_id,
|
|
'amendment_type': amends_amdt.get('type','').lower(),
|
|
'congress': int(amends_amdt.get('congress','')),
|
|
'number': int(amends_amdt.get('number','')),
|
|
'purpose': amends_amdt.get('purpose', ''),
|
|
'description': amends_amdt.get('description','')
|
|
}
|
|
|
|
|
|
def actions_for(action_list):
|
|
actions = [action_for(action) for action in action_list]
|
|
parse_amendment_actions(actions)
|
|
return actions
|
|
|
|
def parse_amendment_actions(actions):
|
|
for action in actions:
|
|
# House Vote
|
|
m = re.match(r"On agreeing to the .* amendments? (\(.*\) )?(?:as (?:modified|amended) )?(Agreed to|Failed) (without objection|by [^\.:]+|by (?:recorded vote|the Yeas and Nays): (\d+) - (\d+)(, \d+ Present)? \(Roll [nN]o. (\d+)\))\.", action['text'])
|
|
if m:
|
|
action["where"] = "h"
|
|
action["type"] = "vote"
|
|
action["vote_type"] = "vote"
|
|
|
|
if m.group(2) == "Agreed to":
|
|
action["result"] = "pass"
|
|
else:
|
|
action["result"] = "fail"
|
|
|
|
action["how"] = m.group(3)
|
|
if "recorded vote" in m.group(3) or "the Yeas and Nays" in m.group(3):
|
|
action["how"] = "roll"
|
|
action["roll"] = int(m.group(7))
|
|
|
|
# Senate Vote
|
|
m = re.match(r"(Motion to table )?Amendment SA \d+(?:, .*?)? (as modified )?(agreed to|not agreed to) in Senate by ([^\.:\-]+|Yea-Nay( Vote)?. (\d+) - (\d+)(, \d+ Present)?. Record Vote Number: (\d+))\.", action['text'])
|
|
if m:
|
|
action["type"] = "vote"
|
|
action["vote_type"] = "vote"
|
|
action["where"] = "s"
|
|
|
|
if m.group(3) == "agreed to":
|
|
action["result"] = "pass"
|
|
if m.group(1): # is a motion to table, so result is sort of reversed.... eeek
|
|
action["result"] = "fail"
|
|
else:
|
|
if m.group(1): # is a failed motion to table, so this doesn't count as a vote on agreeing to the amendment
|
|
continue
|
|
action["result"] = "fail"
|
|
|
|
action["how"] = m.group(4)
|
|
if "Yea-Nay" in m.group(4):
|
|
action["how"] = "roll"
|
|
action["roll"] = int(m.group(9))
|
|
|
|
# Withdrawn
|
|
m = re.match(r"Proposed amendment SA \d+ withdrawn in Senate", action['text'])
|
|
if m:
|
|
action['type'] = 'withdrawn'
|
|
|
|
|
|
def amendment_status_for(amdt):
|
|
status = 'offered'
|
|
status_date = amdt['introduced_at']
|
|
|
|
for action in amdt['actions']:
|
|
if action['type'] == 'vote':
|
|
status = action['result'] # 'pass', 'fail'
|
|
status_date = action['acted_at']
|
|
if action['type'] == 'withdrawn':
|
|
status = 'withdrawn'
|
|
status_date = action['acted_at']
|
|
|
|
return status, status_date
|
|
|
|
def sponsor_for(sponsor, amendment_type):
|
|
if sponsor.get('bioguideId') is None:
|
|
# A committee can sponsor an amendment!
|
|
# Change e.g. "Rules Committee" to "House Rules" for the committee name,
|
|
# for backwards compatibility.
|
|
name = re.sub(r"(.*) Committee$", ("House" if (amendment_type[0] == "h") else "Senate" ) + r" \1", sponsor['name'])
|
|
return {
|
|
"type": "committee",
|
|
"name": name,
|
|
#"committee_id": None, # TODO
|
|
}
|
|
|
|
return sponsor_for_bill(sponsor)
|
|
|
|
def output_for_amdt(amendment_id, format):
|
|
amendment_type, number, congress = utils.split_bill_id(amendment_id)
|
|
return "%s/%s/amendments/%s/%s%s/%s" % (utils.data_dir(), congress, amendment_type, amendment_type, number, "data.%s" % format)
|