forked from eyeseast/python-nytcongress
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathnytcongress.py
309 lines (243 loc) · 10.5 KB
/
nytcongress.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
"""
A Python client for the New York Times Congress API
"""
__author__ = "Chris Amico (eyeseast@gmail.com)"
__version__ = "0.1.0"
import datetime
import httplib2
import os
import urllib
try:
import json
except ImportError:
import simplejson as json
__all__ = ('NytCongress', 'NytCongressError', 'NytNotFoundError', 'get_congress')
DEBUG = False
def get_congress(year):
"Return the Congress number for a given year"
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 NytCongressError(Exception):
"""
Exception for New York Times Congress API errors
"""
class NytNotFoundError(NytCongressError):
"""
Exception for things not found
"""
# Clients
class Client(object):
BASE_URI = "http://api.nytimes.com/svc/politics/v3/us/legislative/congress/"
def __init__(self, apikey, cache='.cache'):
self.apikey = apikey
self.http = httplib2.Http(cache)
def fetch(self, path, *args, **kwargs):
parse = kwargs.pop('parse', lambda r: r['results'][0])
kwargs['api-key'] = self.apikey
if not path.lower().startswith(self.BASE_URI):
url = self.BASE_URI + "%s.json?" % path
url = (url % args) + urllib.urlencode(kwargs)
else:
url = path + '?' + urllib.urlencode(kwargs)
resp, content = self.http.request(url)
if not resp.status in (200, 304):
content = json.loads(content)
errors = '; '.join(e['error'] for e in content['errors'])
if resp.status == 404:
raise NytNotFoundError(errors)
else:
raise NytCongressError(errors)
result = json.loads(content)
if callable(parse):
result = parse(result)
if DEBUG:
result['_url'] = url
return result
class MembersClient(Client):
def get(self, member_id):
"Takes a bioguide_id, returns a legislator"
path = "members/%s"
result = self.fetch(path, member_id)
return result
def filter(self, chamber, congress=CURRENT_CONGRESS, **kwargs):
"Takes a chamber, Congress, and optional state and district, returning a list of members"
path = "%s/%s/members"
result = self.fetch(path, congress, chamber, **kwargs)
return result
def bills(self, member_id, type='introduced'):
"Same as BillsClient.by_member"
path = "members/%s/bills/%s"
result = self.fetch(path, member_id, type)
return result
def new(self, **kwargs):
"Returns a list of new members"
path = "members/new"
result = self.fetch(path, **kwargs)
return result
def departing(self, chamber, congress=CURRENT_CONGRESS):
"Takes a chamber and congress and returns a list of departing members"
path = "%s/%s/members/leaving"
result = self.fetch(path, congress, chamber)
return result
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/%s/bills/%s"
result = self.fetch(path, member_id, type)
return result
def get(self, bill_id, congress=CURRENT_CONGRESS):
path = "%s/bills/%s"
result = self.fetch(path, congress, bill_id)
return result
def amendments(self, bill_id, congress=CURRENT_CONGRESS):
path = "%s/bills/%s/amendments"
result = self.fetch(path, congress, bill_id)
return result
def related(self, bill_id, congress=CURRENT_CONGRESS):
path = "%s/bills/%s/related"
result = self.fetch(path, congress, bill_id)
return result
def subjects(self, bill_id, congress=CURRENT_CONGRESS):
path = "%s/bills/%s/subjects"
result = self.fetch(path, congress, bill_id)
return result
def cosponsors(self, bill_id, congress=CURRENT_CONGRESS):
path = "%s/bills/%s/cosponsors"
result = self.fetch(path, congress, bill_id)
return result
def recent(self, chamber, congress=CURRENT_CONGRESS, type='introduced'):
"Takes a chamber, Congress, and type (introduced|updated), returns a list of recent bills"
if not str(chamber).lower() in ('house', 'senate'):
raise NytCongressError("Argument for `chamber` must be House or Senate; %s given" % chamber)
path = "%s/%s/bills/%s"
result = self.fetch(path, congress, chamber, type)
return result
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')
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.
"""
if not str(chamber).lower() in ('house', 'senate'):
raise TypeError("by_month() requires chamber, year and month. Got %s, %s, %s" \
% (chamber, year, month))
now = datetime.datetime.now()
year = year or now.year
month = month or now.month
path = "%s/votes/%s/%s"
result = self.fetch(path, chamber, year, month, parse=lambda r: r['results'])
return result
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
format = "%Y-%m-%d"
path = "%s/votes/%s/%s"
result = self.fetch(path, chamber, start.strftime(format), end.strftime(format),
parse=lambda r: r['results'])
return result
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):
path = "%s/%s/sessions/%s/votes/%s"
result = self.fetch(path, congress, chamber, session, rollcall_num,
parse=lambda r: r['results'])
return result
# votes by type
def by_type(self, chamber, vote_type, congress=CURRENT_CONGRESS):
"Return votes by type: missed, party, lone no, perfect"
path = "%s/%s/votes/%s"
result = self.fetch(path, congress, chamber, vote_type)
return result
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 = "%s/nominations"
result = self.fetch(path, congress)
return result
class CommitteesClient(Client):
def filter(self, chamber, congress=CURRENT_CONGRESS):
path = "%s/%s/committees"
result = self.fetch(path, congress, chamber)
return result
def get(self, chamber, committee_id, congress=CURRENT_CONGRESS):
path = "%s/%s/committees/%s"
result = self.fetch(path, congress, chamber, committee_id)
return result
class NominationsClient(Client):
def filter(self, nomination_type, congress=CURRENT_CONGRESS):
path = "%s/nominees/%s"
result = self.fetch(path, congress, nomination_type, parse = lambda r: r['results'])
return result
def get(self, nominee, congress=CURRENT_CONGRESS):
path = "%s/nominees/%s"
result = self.fetch(path, congress, nominee)
return result
def by_state(self, state, congress=CURRENT_CONGRESS):
path = "%s/nominees/state/%s"
result = self.fetch(path, congress, state, parse = lambda r: r['results'])
return result
class NytCongress(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.
NytCongress 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('NYT_CONGRESS_API_KEY'), cache='.cache'):
super(NytCongress, 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)