def __init__(self, token): super(HTTPClient, self).__init__() self.limiter = RateLimiter() self.headers = { 'Authorization': 'Bot ' + token, }
def __init__(self, token): super(HTTPClient, self).__init__() py_version = '{}.{}.{}'.format(sys.version_info.major, sys.version_info.minor, sys.version_info.micro) self.limiter = RateLimiter() self.headers = { 'Authorization': 'Bot ' + token, 'User-Agent': 'DiscordBot (https://github.com/b1naryth1ef/disco {}) Python/{} requests/{}' .format(disco_version, py_version, requests_version), }
class HTTPClient(LoggingClass): """ A simple HTTP client which wraps the requests library, adding support for Discords rate-limit headers, authorization, and request/response validation. """ BASE_URL = 'https://discordapp.com/api/v7' MAX_RETRIES = 5 def __init__(self, token): super(HTTPClient, self).__init__() py_version = '{}.{}.{}'.format(sys.version_info.major, sys.version_info.minor, sys.version_info.micro) self.limiter = RateLimiter() self.headers = { 'User-Agent': 'DiscordBot (https://github.com/b1naryth1ef/disco {}) Python/{} requests/{}' .format(disco_version, py_version, requests_version), } if token: self.headers['Authorization'] = 'Bot ' + token def __call__(self, route, args=None, **kwargs): return self.call(route, args, **kwargs) def call(self, route, args=None, **kwargs): """ Makes a request to the given route (as specified in :class:`disco.api.http.Routes`) with a set of URL arguments, and keyword arguments passed to requests. Parameters ---------- route : tuple(:class:`HTTPMethod`, str) The method.URL combination that when compiled with URL arguments creates a requestable route which the HTTPClient will make the request too. args : dict(str, str) A dictionary of URL arguments that will be compiled with the raw URL to create the requestable route. The HTTPClient uses this to track rate limits as well. kwargs : dict Keyword arguments that will be passed along to the requests library Raises ------ APIException Raised when an unrecoverable error occurs, or when we've exhausted the number of retries. Returns ------- :class:`requests.Response` The response object for the request """ args = args or {} retry = kwargs.pop('retry_number', 0) # Merge or set headers if 'headers' in kwargs: kwargs['headers'].update(self.headers) else: kwargs['headers'] = self.headers # Build the bucket URL args = {k: to_bytes(v) for k, v in six.iteritems(args)} filtered = { k: (v if k in ('guild', 'channel') else '') for k, v in six.iteritems(args) } bucket = (route[0].value, route[1].format(**filtered)) # Possibly wait if we're rate limited self.limiter.check(bucket) self.log.debug('KW: %s', kwargs) # Make the actual request url = self.BASE_URL + route[1].format(**args) self.log.info('%s %s (%s)', route[0].value, url, kwargs.get('params')) r = requests.request(route[0].value, url, **kwargs) # Update rate limiter self.limiter.update(bucket, r) # If we got a success status code, just return the data if r.status_code < 400: return r elif r.status_code != 429 and 400 <= r.status_code < 500: self.log.warning('Request failed with code %s: %s', r.status_code, r.content) raise APIException(r) else: if r.status_code == 429: self.log.warning( 'Request responded w/ 429, retrying (but this should not happen, check your clock sync' ) # If we hit the max retries, throw an error retry += 1 if retry > self.MAX_RETRIES: self.log.error('Failing request, hit max retries') raise APIException(r, retries=self.MAX_RETRIES) backoff = self.random_backoff() self.log.warning( 'Request to `{}` failed with code {}, retrying after {}s ({})'. format(url, r.status_code, backoff, r.content)) gevent.sleep(backoff) # Otherwise just recurse and try again return self(route, args, retry_number=retry, **kwargs) @staticmethod def random_backoff(): """ Returns a random backoff (in milliseconds) to be used for any error the client suspects is transient. Will always return a value between 500 and 5000 milliseconds. :returns: a random backoff in milliseconds :rtype: float """ return random.randint(500, 5000) / 1000.0