diff --git a/intercom/client.py b/intercom/client.py index ce37617b..6e359b43 100644 --- a/intercom/client.py +++ b/intercom/client.py @@ -1,15 +1,19 @@ # -*- coding: utf-8 -*- import requests +import time +from datetime import datetime +from pytz import utc class Client(object): - def __init__(self, personal_access_token='my_personal_access_token'): + def __init__(self, personal_access_token='my_personal_access_token', throttle=False): self.personal_access_token = personal_access_token self.base_url = 'https://api.intercom.io' self.rate_limit_details = {} self.http_session = requests.Session() + self.throttle = throttle @property def _auth(self): @@ -81,6 +85,16 @@ def jobs(self): return job.Job(self) def _execute_request(self, request, params): + # throttle + if self.throttle and self.rate_limit_details.get('remaining', None) == 0: + # wait until the we've reached the reset time + utcnow = datetime.utcnow().replace(tzinfo=utc) + if utcnow < self.rate_limit_details['reset_at']: + # the delta between the times + delta = self.rate_limit_details['reset_at'] - utcnow + # sleep until one second after the reset + time.sleep(delta.seconds + 1) + result = request.execute(self.base_url, self._auth, params) self.rate_limit_details = request.rate_limit_details return result diff --git a/intercom/request.py b/intercom/request.py index 820f8599..b66e92c7 100644 --- a/intercom/request.py +++ b/intercom/request.py @@ -33,7 +33,10 @@ def __init__(self, http_method, path, http_session=None): self.http_session = http_session def execute(self, base_url, auth, params): - return self.send_request_to_path(base_url, auth, params) + resp = self.send_request_to_path(base_url, auth, params) + parsed_body = self.parse_body(resp) + self.set_rate_limit_details(resp) + return parsed_body def send_request_to_path(self, base_url, auth, params=None): """ Construct an API request, send it to the API, and parse the @@ -81,10 +84,8 @@ def send_request_to_path(self, base_url, auth, params=None): resp.encoding, resp.status_code) logger.debug(" content:\n%s", resp.content) - parsed_body = self.parse_body(resp) self.raise_errors_on_failure(resp) - self.set_rate_limit_details(resp) - return parsed_body + return resp def parse_body(self, resp): if resp.content and resp.content.strip(): diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py new file mode 100644 index 00000000..fea48f68 --- /dev/null +++ b/tests/unit/test_client.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- # noqa + +import time +import unittest + +from datetime import datetime +from datetime import timedelta +from intercom.client import Client +from intercom.request import Request +from mock import Mock +from mock import patch +from nose.tools import istest +from pytz import utc + + +class ClientTest(unittest.TestCase): # noqa + + def setUp(self): # noqa + self.client = Client() + now = datetime.utcnow().replace(tzinfo=utc) + reset_at = now + timedelta(seconds=2) + # set the reset to be in 2 seconds + self.rate_limit_details = { + 'remaining': 0, + 'limit': 20, + 'reset_at': reset_at + } + headers = { + 'x-ratelimit-limit': 20, + 'x-ratelimit-remaining': 0, + 'x-ratelimit-reset': time.mktime(reset_at.timetuple()) + } + payload = """{ + "user_id": "1224242", + "update_last_request_at": true, + "custom_attributes": {} + }""" + + if not hasattr(payload, 'decode'): + # python 3 + payload = payload.encode('utf-8') + + self.response = Mock( + content=payload, + encoding='utf-8', + headers=headers) + self.started_at = now + + @istest + def it_can_control_throttle_on_creation(self): # noqa + client = Client() + self.assertFalse(client.throttle) + client = Client(throttle=True) + self.assertTrue(client.throttle) + + @istest + def it_will_sleep_if_throttle_is_on(self): # noqa + client = Client(throttle=True) + client.rate_limit_details = self.rate_limit_details + with patch.object(Request, 'send_request_to_path', return_value=self.response): + # this call should take approximately 2 seconds + client.users.find(email="john@example.com") + finished_at = datetime.utcnow().replace(tzinfo=utc) + self.assertEqual(2, int((finished_at - self.started_at).seconds)) + + @istest + def it_wont_sleep_if_throttle_is_off(self): # noqa + client = Client() + client.rate_limit_details = self.rate_limit_details + with patch.object(Request, 'send_request_to_path', return_value=self.response): + # this call should take approximately 2 seconds + client.users.find(email="john@example.com") + finished_at = datetime.utcnow().replace(tzinfo=utc) + self.assertEqual(0, int((finished_at - self.started_at).seconds)) diff --git a/tests/unit/test_request.py b/tests/unit/test_request.py index fdfa7794..a3a94bd8 100644 --- a/tests/unit/test_request.py +++ b/tests/unit/test_request.py @@ -327,7 +327,7 @@ def it_needs_encoding_or_apparent_encoding(self): mock_method.return_value = resp with assert_raises(TypeError): request = Request('GET', 'events') - request.send_request_to_path('', ('x', 'y'), resp) + request.execute('', ('x', 'y'), resp) @istest def it_allows_the_timeout_to_be_changed(self):