Skip to content

Commit 2fa7d9e

Browse files
committed
Automatic handling of rate limit
1 parent 3c764bb commit 2fa7d9e

3 files changed

Lines changed: 50 additions & 7 deletions

File tree

README.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,30 @@ wrapper around a lower level api in ``bigcommerce.connection``. This can
103103
be accessed through ``api.connection``, and provides helper methods for
104104
get/post/put/delete operations.
105105

106+
Managing OAuth Rate Limits
107+
~~~~~~~~~~~~~~~~~~~~~~~~~~
108+
109+
You can optionally pass a ``rate_limiting_management`` object into ``bigcommerce.api.BigcommerceApi`` or ``bigcommerce.connection.OAuthConnection`` for automatic rate limiting management, ex:
110+
111+
.. code:: python
112+
113+
import bigcommerce
114+
115+
api = bigcommerce.api.BigcommerceApi(client_id='', store_hash='', access_token=''
116+
rate_limiting_management= {'min_requests_remaining':2,
117+
'wait':True,
118+
'callback_function':None})
119+
120+
``min_requests_remaining`` will determine the number of requests remaining in the rate limiting window which will invoke the management function
121+
122+
``wait`` determines whether or not we should automatically sleep until the end of the window
123+
124+
``callback_function`` is a function to run when the rate limiting management function fires. It will be invoked *after* the wait, if enabled.
125+
126+
``callback_args`` is an optional parameter which is a dictionary passed as an argument to the callback function.
127+
128+
For simple applications which run API requests in serial (and aren't interacting with many different stores, or use a separate worker for each store) the simple sleep function may work well enough for most purposes. For more complex applications that may be parallelizing API requests on a given store, it's adviseable to write your own callback function for handling the rate limiting, use a ``min_requests_remaining`` higher than your concurrency, and not use the default wait function.
129+
106130
Further documentation
107131
---------------------
108132

bigcommerce/api.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@
55

66

77
class BigcommerceApi(object):
8-
def __init__(self, host=None, basic_auth=None, client_id=None, store_hash=None, access_token=None):
8+
def __init__(self, host=None, basic_auth=None,
9+
client_id=None, store_hash=None, access_token=None, rate_limiting_management=None):
910
self.api_service = os.getenv('BC_API_ENDPOINT', 'api.bigcommerce.com')
1011
self.auth_service = os.getenv('BC_AUTH_SERVICE', 'login.bigcommerce.com')
1112

1213
if host and basic_auth:
1314
self.connection = connection.Connection(host, basic_auth)
1415
elif client_id and store_hash:
15-
self.connection = connection.OAuthConnection(client_id, store_hash, access_token, self.api_service)
16+
self.connection = connection.OAuthConnection(client_id, store_hash, access_token, self.api_service,
17+
rate_limiting_management=rate_limiting_management)
1618
else:
1719
raise Exception("Must provide either (client_id and store_hash) or (host and basic_auth)")
1820

bigcommerce/connection.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
import logging
1717
import requests
1818

19+
from math import ceil
20+
from time import sleep
21+
1922
from bigcommerce.exception import *
2023

2124
log = logging.getLogger("bigcommerce.connection")
@@ -183,12 +186,14 @@ class OAuthConnection(Connection):
183186
The verify_payload method is also provided for authenticating signed payloads passed to an application's load url.
184187
"""
185188

186-
def __init__(self, client_id, store_hash, access_token=None, host='api.bigcommerce.com', api_path='/stores/{}/v2/{}'):
189+
def __init__(self, client_id, store_hash, access_token=None, host='api.bigcommerce.com',
190+
api_path='/stores/{}/v2/{}', rate_limiting_management=None):
187191
self.client_id = client_id
188192
self.store_hash = store_hash
189193
self.host = host
190194
self.api_path = api_path
191195
self.timeout = 7.0 # can attach to session?
196+
self.rate_limiting_management = rate_limiting_management
192197

193198
self._session = requests.Session()
194199
self._session.headers = {"Accept": "application/json",
@@ -250,8 +255,20 @@ def _handle_response(self, url, res, suppress_empty=True):
250255
"""
251256
result = Connection._handle_response(self, url, res, suppress_empty)
252257
if 'X-Rate-Limit-Time-Reset-Ms' in res.headers:
253-
self.rate_limit = dict(ms_until_reset=res.headers['X-Rate-Limit-Time-Reset-Ms'],
254-
window_size_ms=res.headers['X-Rate-Limit-Time-Window-Ms'],
255-
requests_remaining=res.headers['X-Rate-Limit-Requests-Left'],
256-
requests_quota=res.headers['X-Rate-Limit-Requests-Quota'])
258+
self.rate_limit = dict(ms_until_reset=int(res.headers['X-Rate-Limit-Time-Reset-Ms']),
259+
window_size_ms=int(res.headers['X-Rate-Limit-Time-Window-Ms']),
260+
requests_remaining=int(res.headers['X-Rate-Limit-Requests-Left']),
261+
requests_quota=int(res.headers['X-Rate-Limit-Requests-Quota']))
262+
if self.rate_limiting_management:
263+
if self.rate_limiting_management['min_requests_remaining'] >= self.rate_limit['requests_remaining']:
264+
if self.rate_limiting_management['wait']:
265+
sleep(ceil(float(self.rate_limit['ms_until_reset']) / 1000))
266+
if self.rate_limiting_management.get('callback_function'):
267+
callback = self.rate_limiting_management['callback_function']
268+
args_dict = self.rate_limiting_management.get('callback_args')
269+
if args_dict:
270+
callback(args_dict)
271+
else:
272+
callback()
273+
257274
return result

0 commit comments

Comments
 (0)