Skip to content

Commit c2e05d9

Browse files
committed
Merge pull request Clever#18 from Clever/support_rate_limiting_py
Support rate limiting python
2 parents c9c2a6f + 68b5a5a commit c2e05d9

File tree

5 files changed

+63
-24
lines changed

5 files changed

+63
-24
lines changed

.drone.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
image: python2.7
22
script:
3+
- pip install -r test/requirements.txt
34
- python setup.py develop && python setup.py test
45
notify:
56
email:

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ If you'd like more control over pagination, or to limit the number of resources
5656
print students.next()
5757
```
5858

59-
The `retrieve` class method takes in a Clever ID, and returns a specific resource. The object (or list of objects in the case of `all`) supports accessing properties using either dot notation or dictionary notation:
59+
The `retrieve` class method takes in a Clever ID and returns a specific resource. The object (or list of objects in the case of `all`) supports accessing properties using either dot notation or dictionary notation:
6060

6161
```python
6262
demo_school = clever.School.retrieve("4fee004cca2e43cf27000001")

clever/__init__.py

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ def __init__(self, message, http_body=None, http_status=None, json_body=None):
137137
class AuthenticationError(CleverError):
138138
pass
139139

140+
class TooManyRequestsError(CleverError):
141+
def __init__(self, message, res):
142+
super(TooManyRequestsError, self).__init__(message, res['body'], res['code'])
143+
self.http_headers = res['headers']
144+
140145

141146
def convert_to_clever_object(klass, resp, auth):
142147
# TODO: to support includes we'll have to infer klass from resp['uri']
@@ -209,11 +214,12 @@ def jsonencode(cls, d):
209214
return json.dumps(d)
210215

211216
def request(self, meth, url, params={}):
212-
rbody, rcode, my_auth = self.request_raw(meth, url, params)
213-
resp = self.interpret_response(rbody, rcode)
217+
res, my_auth = self.request_raw(meth, url, params)
218+
resp = self.interpret_response(res)
214219
return resp, my_auth
215220

216-
def handle_api_error(self, rbody, rcode, resp):
221+
def handle_api_error(self, res, resp):
222+
rbody, rheaders, rcode = res['body'], res['headers'], res['code']
217223
try:
218224
error = resp['error']
219225
except (KeyError, TypeError):
@@ -224,6 +230,8 @@ def handle_api_error(self, rbody, rcode, resp):
224230
raise InvalidRequestError(error, rbody, rcode, resp)
225231
elif rcode == 401:
226232
raise AuthenticationError(error, rbody, rcode, resp)
233+
elif rcode == 429:
234+
raise TooManyRequestsError(error, res)
227235
else:
228236
raise APIError(error, rbody, rcode, resp)
229237

@@ -264,29 +272,30 @@ def request_raw(self, meth, url, params={}):
264272
headers['Authorization'] = 'Basic {}'.format(base64.b64encode(my_auth['api_key'] + ':'))
265273
elif my_auth.get('token', None) != None:
266274
headers['Authorization'] = 'Bearer {}'.format(my_auth['token'])
267-
if _httplib == 'requests':
268-
rbody, rcode = self.requests_request(meth, abs_url, headers, params)
269-
elif _httplib == 'pycurl':
270-
rbody, rcode = self.pycurl_request(meth, abs_url, headers, params)
271-
elif _httplib == 'urlfetch':
272-
rbody, rcode = self.urlfetch_request(meth, abs_url, headers, params)
273-
elif _httplib == 'urllib2':
274-
rbody, rcode = self.urllib2_request(meth, abs_url, headers, params)
275+
make_request = {
276+
'requests': self.requests_request,
277+
'pycurl': self.pycurl_request,
278+
'urlfetch': self.urlfetch_request,
279+
'urllib2': self.urllib2_request
280+
}
281+
if _httplib in make_request:
282+
res = make_request[_httplib](meth, abs_url, headers, params)
275283
else:
276284
raise CleverError(
277285
"Clever Python library bug discovered: invalid httplib %s. Please report to [email protected]" % (_httplib, ))
278286
logger.debug('API request to %s returned (response code, response body) of (%d, %r)' %
279-
(abs_url, rcode, rbody))
280-
return rbody, rcode, my_auth
287+
(abs_url, res['code'], res['body']))
288+
return res, my_auth
281289

282-
def interpret_response(self, rbody, rcode):
290+
def interpret_response(self, http_res):
291+
rbody, rcode= http_res['body'], http_res['code']
283292
try:
284-
resp = json.loads(rbody)
293+
resp = json.loads(rbody) if rcode != 429 else {'error': 'Too Many Requests'}
285294
except Exception:
286295
raise APIError("Invalid response body from API: %s (HTTP response code was %d)" %
287296
(rbody, rcode), rbody, rcode)
288297
if not (200 <= rcode < 300):
289-
self.handle_api_error(rbody, rcode, resp)
298+
self.handle_api_error(http_res, resp)
290299
return resp
291300

292301
def requests_request(self, meth, abs_url, headers, params):
@@ -324,11 +333,12 @@ def requests_request(self, meth, abs_url, headers, params):
324333
# are succeptible to the same and should be updated.
325334
content = result.content
326335
status_code = result.status_code
336+
headers = result.headers
327337
except Exception, e:
328338
# Would catch just requests.exceptions.RequestException, but can
329339
# also raise ValueError, RuntimeError, etc.
330340
self.handle_requests_error(e)
331-
return content, status_code
341+
return {'body': content, 'headers': headers, 'code': status_code}
332342

333343
def handle_requests_error(self, e):
334344
if isinstance(e, requests.exceptions.RequestException):
@@ -346,6 +356,7 @@ def handle_requests_error(self, e):
346356

347357
def pycurl_request(self, meth, abs_url, headers, params):
348358
s = StringIO.StringIO()
359+
rheader = StringIO.StringIO()
349360
curl = pycurl.Curl()
350361

351362
meth = meth.lower()
@@ -374,6 +385,7 @@ def pycurl_request(self, meth, abs_url, headers, params):
374385
curl.setopt(pycurl.CONNECTTIMEOUT, 30)
375386
curl.setopt(pycurl.TIMEOUT, 80)
376387
curl.setopt(pycurl.HTTPHEADER, ['%s: %s' % (k, v) for k, v in headers.iteritems()])
388+
curl.setopt(pycurl.HEADERFUNCTION, rheader.write)
377389
if verify_ssl_certs:
378390
curl.setopt(pycurl.CAINFO, CLEVER_CERTS)
379391
else:
@@ -383,9 +395,7 @@ def pycurl_request(self, meth, abs_url, headers, params):
383395
curl.perform()
384396
except pycurl.error, e:
385397
self.handle_pycurl_error(e)
386-
rbody = s.getvalue()
387-
rcode = curl.getinfo(pycurl.RESPONSE_CODE)
388-
return rbody, rcode
398+
return {'body': s.getvalue(), 'headers': rheader.getvalue(), 'code': curl.getinfo(pycurl.RESPONSE_CODE)}
389399

390400
def handle_pycurl_error(self, e):
391401
if e[0] in [pycurl.E_COULDNT_CONNECT,
@@ -429,7 +439,7 @@ def urlfetch_request(self, meth, abs_url, headers, params):
429439
result = urlfetch.fetch(**args)
430440
except urlfetch.Error, e:
431441
self.handle_urlfetch_error(e, abs_url)
432-
return result.content, result.status_code
442+
return {'body': result.content, 'headers': result.headers, 'code': result.status_code}
433443

434444
def handle_urlfetch_error(self, e, abs_url):
435445
if isinstance(e, urlfetch.InvalidURLError):
@@ -467,13 +477,15 @@ def urllib2_request(self, meth, abs_url, headers, params):
467477
try:
468478
response = urllib2.urlopen(req)
469479
rbody = response.read()
480+
rheader = response.info()
470481
rcode = response.code
471482
except urllib2.HTTPError, e:
472483
rcode = e.code
484+
rheader = None
473485
rbody = e.read()
474486
except (urllib2.URLError, ValueError), e:
475487
self.handle_urllib2_error(e, abs_url)
476-
return rbody, rcode
488+
return {'body': rbody, 'headers': rheader, 'code': rcode}
477489

478490
def handle_urllib2_error(self, e, abs_url):
479491
msg = "Unexpected error communicating with Clever. If this problem persists, let us know at [email protected]."

test/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
httmock

test/test_clever.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22
import os
33
import sys
44
import unittest
5+
import httmock
56

67
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
78
import clever
89
from clever import importer
910
json = importer.import_json()
1011

12+
import requests
13+
from httmock import response, HTTMock
14+
1115

1216
def functional_test(auth):
1317
class FunctionalTests(CleverTestCase):
@@ -119,12 +123,33 @@ def test_nonexistent_object(self):
119123
self.assertFalse(isinstance(e.json_body, dict)) # 404 does not have a body
120124
self.assertTrue(isinstance(e.http_body, str))
121125

126+
#generates httmock responses for TooManyRequestsErrorTest
127+
def too_many_requests_content(url, request):
128+
headers = {
129+
'X-Ratelimit-Bucket': 'all, none',
130+
'X-Ratelimit-Limit' : '200, 1200',
131+
'X-Ratelimit-Reset' : '135136, 31634',
132+
'X-Ratelimit-Remaining' : '0, 0'
133+
}
134+
return response(429, "", headers, None, 5, None)
135+
136+
class TooManyRequestsErrorTest(CleverTestCase):
137+
138+
def test_rate_limiter(self):
139+
with HTTMock(too_many_requests_content):
140+
r = requests.get('https://test.rate.limiting')
141+
res = {'body': r.content, 'headers': r.headers, 'code': 429}
142+
APIRequestor = clever.APIRequestor()
143+
self.assertRaises(clever.TooManyRequestsError, lambda : APIRequestor.interpret_response(res))
144+
122145
if __name__ == '__main__':
123146
suite = unittest.TestSuite()
124147
for TestClass in [
125148
functional_test({"api_key": "DEMO_KEY"}),
126149
functional_test({"token": "7f76343d50b9e956138169e8cbb4630bb887b18"}),
127150
AuthenticationErrorTest,
128-
InvalidRequestErrorTest]:
151+
InvalidRequestErrorTest,
152+
TooManyRequestsErrorTest]:
129153
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestClass))
130154
unittest.TextTestRunner(verbosity=2).run(suite)
155+

0 commit comments

Comments
 (0)