forked from eyeseast/propublica-congress
-
Notifications
You must be signed in to change notification settings - Fork 0
/
congress.py
311 lines (234 loc) · 10.7 KB
/
congress.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
"""
A Python client for the ProPublica Congress API
API docs: https://propublica.github.io/congress-api-docs/
"""
__author__ = "Chris Amico ([email protected])"
__version__ = "0.2.0"
import datetime
import json
import os
import urllib
import httplib2
__all__ = ('Congress', 'CongressError', 'NotFound', 'get_congress', 'CURRENT_CONGRESS')
DEBUG = True
def get_congress(year):
"Return the Congress number for a given year"
if year < 1789:
raise CongressError('There was no Congress before 1789.')
return (year - 1789) / 2 + 1
def parse_date(s):
"""
Parse a date using dateutil.parser.parse if available,
falling back to datetime.datetime.strptime if not
"""
if isinstance(s, (datetime.datetime, datetime.date)):
return s
try:
from dateutil.parser import parse
except ImportError:
parse = lambda d: datetime.datetime.strptime(d, "%Y-%m-%d")
return parse(s)
CURRENT_CONGRESS = get_congress(datetime.datetime.now().year)
# Error classes
class CongressError(Exception):
"""
Exception for general Congress API errors
"""
class NotFound(CongressError):
"""
Exception for things not found
"""
# Clients
class Client(object):
BASE_URI = "https://api.propublica.org/congress/v1/"
def __init__(self, apikey=None, cache='.cache'):
self.apikey = apikey
self.http = httplib2.Http(cache)
def fetch(self, path, parse=lambda r: r['results'][0]):
"""
Make an API request, with authentication
"""
url = self.BASE_URI + path
headers = {'X-API-Key': self.apikey}
resp, content = self.http.request(url, headers=headers)
content = json.loads(content)
if callable(parse):
content = parse(content)
return content
class MembersClient(Client):
def get(self, member_id):
"Takes a bioguide_id, returns a legislator"
path = "members/{0}.json".format(member_id)
return self.fetch(path)
def filter(self, chamber, congress=CURRENT_CONGRESS, **kwargs):
"Takes a chamber, Congress, and optional state and district, returning a list of members"
path = "{0}/{1}/members.json".format(congress, chamber)
return self.fetch(path)
def bills(self, member_id, type='introduced'):
"Same as BillsClient.by_member"
path = "members/{0}/bills/{1}.json".format(member_id, type)
return self.fetch(path)
def new(self, **kwargs):
"Returns a list of new members"
path = "members/new.json"
return self.fetch(path)
def departing(self, chamber, congress=CURRENT_CONGRESS):
"Takes a chamber and congress and returns a list of departing members"
path = "{0}/{1}/members/leaving.json".format(congress, chamber)
return self.fetch(path)
def compare(self, first, second, chamber, congress=CURRENT_CONGRESS):
"""
See how often two members voted together in a given Congress.
Takes two member IDs, a chamber and a Congress number.
"""
path = "{first}/votes/{second}/{congress}/{chamber}.json"
path = path.format(first=first, second=second, congress=congress, chamber=chamber)
return self.fetch(path)
def party(self):
"Get state party counts for the current Congress"
path = "states/members/party.json"
return self.fetch(path, parse=lambda r: r['results'])
class BillsClient(Client):
def by_member(self, member_id, type='introduced'):
"""
Takes a bioguide ID and a type (introduced|updated|cosponsored|withdrawn),
returns recent bills
"""
path = "members/{member_id}/bills/{type}.json".format(member_id=member_id, type=type)
return self.fetch(path)
def get(self, bill_id, congress=CURRENT_CONGRESS, type=None):
if type:
path = "{congress}/bills/{bill_id}/{type}.json".format(
congress=congress, bill_id=bill_id)
else:
path = "{congress}/bills/{bill_id}.json".format(
congress=congress, bill_id=bill_id)
return self.fetch(path)
def amendments(self, bill_id, congress=CURRENT_CONGRESS):
return self.get(bill_id, congress, 'amendments')
def related(self, bill_id, congress=CURRENT_CONGRESS):
return self.get(bill_id, congress, 'related')
def subjects(self, bill_id, congress=CURRENT_CONGRESS):
return self.get(bill_id, congress, 'subjects')
def cosponsors(self, bill_id, congress=CURRENT_CONGRESS):
return self.get(bill_id, congress, 'cosponsors')
def recent(self, chamber, congress=CURRENT_CONGRESS, type='introduced'):
"Takes a chamber, Congress, and type (introduced|updated), returns a list of recent bills"
path = "{congress}/{chamber}/bills/{type}.json".format(
congress=congress, chamber=chamber, type=type)
return self.fetch(path)
def introduced(self, chamber, congress=CURRENT_CONGRESS):
"Shortcut for getting introduced bills"
return self.recent(chamber, congress, 'introduced')
def updated(self, chamber, congress=CURRENT_CONGRESS):
"Shortcut for getting updated bills"
return self.recent(chamber, congress, 'updated')
def passed(self, chamber, congress=CURRENT_CONGRESS):
"Shortcut for passed bills"
return self.recent(chamber, congress, 'passed')
def major(self, chamber, congress=CURRENT_CONGRESS):
"Shortcut for major bills"
return self.recent(chamber, congress, 'major')
class VotesClient(Client):
# date-based queries
def by_month(self, chamber, year=None, month=None):
"""
Return votes for a single month, defaulting to the current month.
"""
now = datetime.datetime.now()
year = year or now.year
month = month or now.month
path = "{chamber}/votes/{year}/{month}.json".format(
chamber=chamber, year=year, month=month)
return self.fetch(path)
def by_range(self, chamber, start, end):
"""
Return votes cast in a chamber between two dates,
up to one month apart.
"""
start, end = parse_date(start), parse_date(end)
if start > end:
start, end = end, start
path = "{chamber}/votes/{start:%Y-%m-%d}/{end:%Y-%m-%d}.json".format(
chamber=chamber, start=start, end=end)
return self.fetch(path)
def by_date(self, chamber, date):
"Return votes cast in a chamber on a single day"
date = parse_date(date)
return self.by_range(chamber, date, date)
def today(self, chamber):
"Return today's votes in a given chamber"
now = datetime.date.today()
return self.by_range(chamber, now, now)
# detail response
def get(self, chamber, rollcall_num, session, congress=CURRENT_CONGRESS):
"Return a specific roll-call vote, including a complete list of member positions"
path = "{congress}/{chamber}/sessions/{session}/votes/{rollcall_num}.json"
path = path.format(congress=congress, chamber=chamber,
session=session, rollcall_num=rollcall_num)
return self.fetch(path)
# votes by type
def by_type(self, chamber, type, congress=CURRENT_CONGRESS):
"Return votes by type: missed, party, lone no, perfect"
path = "{congress}/{chamber}/votes/{type}.json".format(
congress=congress, chamber=chamber, type=type)
return self.fetch(path)
def missed(self, chamber, congress=CURRENT_CONGRESS):
"Missed votes by member"
return self.by_type(chamber, 'missed', congress)
def party(self, chamber, congress=CURRENT_CONGRESS):
"How often does each member vote with their party?"
return self.by_type(chamber, 'party', congress)
def loneno(self, chamber, congress=CURRENT_CONGRESS):
"How often is each member the lone no vote?"
return self.by_type(chamber, 'loneno', congress)
def perfect(self, chamber, congress=CURRENT_CONGRESS):
"Who never misses a vote?"
return self.by_type(chamber, 'perfect', congress)
def nominations(self, congress=CURRENT_CONGRESS):
"Return votes on nominations from a given Congress"
path = "{congress}/nominations.json".format(congress=congress)
return self.fetch(path)
class CommitteesClient(Client):
def filter(self, chamber, congress=CURRENT_CONGRESS):
path = "{congress}/{chamber}/committees.json".format(
congress=congress, chamber=chamber)
return self.fetch(path)
def get(self, chamber, committee, congress=CURRENT_CONGRESS):
path = "{congress}/{chamber}/committees/{committee}.json".format(
congress=congress, chamber=chamber, committee=committee)
return self.fetch(path)
class NominationsClient(Client):
def filter(self, type, congress=CURRENT_CONGRESS):
path = "{congress}/nominees/{type}.json".format(congress=congress, type=type)
return self.fetch(path)
def get(self, nominee, congress=CURRENT_CONGRESS):
path = "{congress}/nominees/{nominee}.json".format(congress=congress, nominee=nominee)
return self.fetch(path)
def by_state(self, state, congress=CURRENT_CONGRESS):
path = "{congress}/nominees/state/{state}.json".format(
congress=congress, state=state)
return self.fetch(path)
class Congress(Client):
"""
Implements the public interface for the NYT Congress API
Methods are namespaced by topic (though some have multiple access points).
Everything returns decoded JSON, with fat trimmed.
In addition, the top-level namespace is itself a client, which
can be used to fetch generic resources, using the API URIs included
in responses. This is here so you don't have to write separate
functions that add on your API key and trim fat off responses.
Create a new instance with your API key, or set an environment
variable called NYT_CONGRESS_API_KEY.
Congress uses httplib2, and caching is pluggable. By default,
it uses httplib2.FileCache, in a directory called .cache, but it
should also work with memcache or anything else that exposes the
same interface as FileCache (per httplib2 docs).
"""
def __init__(self, apikey=os.environ.get('PROPUBLICA_API_KEY'), cache='.cache'):
super(Congress, self).__init__(apikey, cache)
self.members = MembersClient(self.apikey, cache)
self.bills = BillsClient(self.apikey, cache)
self.committees = CommitteesClient(self.apikey, cache)
self.votes = VotesClient(self.apikey, cache)
self.nominations = NominationsClient(self.apikey, cache)