diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0f0e2ba..756dd1f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,10 +9,14 @@ to `semantic versioning`_. .. _Keep a Changelog: http://keepachangelog.com/ .. _semantic versioning: http://semver.org/ -[vX.Y.Z] - Unreleased ---------------------- +[vX.Y.Z] - TODO +--------------- -* Nothing (yet). +Added +^^^^^ +* Allow configuring the ``krakenex.API`` object to retry the query + on certain HTTP error codes. By default, no retries will be attempted. +* TODO: docs on how, link here. [v2.1.0] - 2018-04-20 (Fryday) ------------------------------ diff --git a/krakenex/api.py b/krakenex/api.py index de361f7..279af08 100644 --- a/krakenex/api.py +++ b/krakenex/api.py @@ -27,6 +27,7 @@ import hashlib import hmac import base64 +import logging from . import version @@ -66,7 +67,15 @@ def __init__(self, key='', secret=''): 'User-Agent': 'krakenex/' + version.__version__ + ' (+' + version.__url__ + ')' }) self.response = None + + # retry-on-failure configuration + self.retries = 0 + self.cooldown = 15 + self.successcodes = [200, 201, 202] + self.retrycodes = [504, 520] + self._json_options = {} + return def json_options(self, **kwargs): @@ -124,6 +133,8 @@ def _query(self, urlpath, data, headers=None, timeout=None): :raises: :py:exc:`requests.HTTPError`: if response status not successful """ + logger = logging.getLogger('krakenex.api') + if data is None: data = {} if headers is None: @@ -131,15 +142,27 @@ def _query(self, urlpath, data, headers=None, timeout=None): url = self.uri + urlpath - self.response = self.session.post(url, data = data, headers = headers, - timeout = timeout) - - if self.response.status_code not in (200, 201, 202): - self.response.raise_for_status() + attempts = 0 + while attempts <= self.retries: + nonce = -1 if 'nonce' not in data.keys() else data['nonce'] # UGLY + logger.debug('Posting query: nonce %d, attempt %d.', nonce, attempts) + self.response = self.session.post(url, data=data, headers=headers, + timeout=timeout) + status = self.response.status_code + attempts += 1 + + if status in self.successcodes: + break + elif status in self.retrycodes and attempts <= self.retries: + logger.debug('HTTP error %d', status) + logger.debug('Sleeping for %d seconds', self.cooldown) + time.sleep(self.cooldown) + continue + else: + self.response.raise_for_status() return self.response.json(**self._json_options) - def query_public(self, method, data=None, timeout=None): """ Performs an API query that does not require a valid key/secret pair. @@ -159,7 +182,7 @@ def query_public(self, method, data=None, timeout=None): urlpath = '/' + self.apiversion + '/public/' + method - return self._query(urlpath, data, timeout = timeout) + return self._query(urlpath, data, timeout=timeout) def query_private(self, method, data=None, timeout=None): """ Performs an API query that requires a valid key/secret pair. @@ -190,7 +213,7 @@ def query_private(self, method, data=None, timeout=None): 'API-Sign': self._sign(data, urlpath) } - return self._query(urlpath, data, headers, timeout = timeout) + return self._query(urlpath, data, headers, timeout=timeout) def _nonce(self): """ Nonce counter.