#!/usr/bin/env python3 # -*- coding: utf-8 -*- import time import datetime from contextlib import closing import pytz import dateutil import requests from mastodon.exceptions import * """Internal helpers, dragons probably""" def __datetime_to_epoch(self, date_time): """ Converts a python datetime to unix epoch, accounting for time zones and such. Assumes UTC if timezone is not given. """ date_time_utc = None if date_time.tzinfo == None: date_time_utc = date_time.replace(tzinfo = pytz.utc) else: date_time_utc = date_time.astimezone(pytz.utc) epoch_utc = datetime.datetime.utcfromtimestamp(0).replace(tzinfo = pytz.utc) return (date_time_utc - epoch_utc).total_seconds() def __api_request(self, method, endpoint, params = {}, files = {}, do_ratelimiting = True): """ Internal API request helper. """ response = None headers = None next_url = None # "pace" mode ratelimiting: Assume constant rate of requests, sleep a little less long than it # would take to not hit the rate limit at that request rate. if do_ratelimiting and self.ratelimit_method == "pace": if self.ratelimit_remaining == 0: to_next = self.ratelimit_reset - time.time() if to_next > 0: # As a precaution, never sleep longer than 5 minutes to_next = min(to_next, 5 * 60) time.sleep(to_next) else: time_waited = time.time() - self.ratelimit_lastcall time_wait = float(self.ratelimit_reset - time.time()) / float(self.ratelimit_remaining) remaining_wait = time_wait - time_waited if remaining_wait > 0: to_next = remaining_wait / self.ratelimit_pacefactor to_next = min(to_next, 5 * 60) time.sleep(to_next) # Generate request headers if self.access_token is not None: headers = {'Authorization': 'Bearer ' + self.access_token} if self.debug_requests is True: print('Mastodon: Request to endpoint "' + endpoint + '" using method "' + method + '".') print('Parameters: ' + str(params)) print('Headers: ' + str(headers)) print('Files: ' + str(files)) # Make request request_complete = False while not request_complete: request_complete = True response_object = None try: if method == 'GET': response_object = requests.get(self.api_base_url + endpoint, data = params, headers = headers, files = files, timeout = self.request_timeout) if method == 'POST': response_object = requests.post(self.api_base_url + endpoint, data = params, headers = headers, files = files, timeout = self.request_timeout) if method == 'DELETE': response_object = requests.delete(self.api_base_url + endpoint, data = params, headers = headers, files = files, timeout = self.request_timeout) except Exception as e: import traceback traceback.print_exc() raise MastodonNetworkError("Could not complete request: %s" % e) if response_object == None: raise MastodonIllegalArgumentError("Illegal request.") # Handle response if self.debug_requests == True: print('Mastodon: Response received with code ' + str(response_object.status_code) + '.') print('response headers: ' + str(response_object.headers)) #print('Response text content: ' + str(response_object.text)) if response_object.status_code == 404: raise MastodonAPIError('Endpoint not found.') if response_object.status_code == 500: raise MastodonAPIError('General API problem.') try: response = response_object.json() except: import traceback traceback.print_exc() raise MastodonAPIError("Could not parse response as JSON, response code was %s, bad json content was '%s'" % (response_object.status_code, response_object.content)) # Handle rate limiting if 'X-RateLimit-Remaining' in response_object.headers and do_ratelimiting: self.ratelimit_remaining = int(response_object.headers['X-RateLimit-Remaining']) self.ratelimit_limit = int(response_object.headers['X-RateLimit-Limit']) try: ratelimit_reset_datetime = dateutil.parser.parse(response_object.headers['X-RateLimit-Reset']) self.ratelimit_reset = self.__datetime_to_epoch(ratelimit_reset_datetime) # Adjust server time to local clock server_time_datetime = dateutil.parser.parse(response_object.headers['Date']) server_time = self.__datetime_to_epoch(server_time_datetime) server_time_diff = time.time() - server_time self.ratelimit_reset += server_time_diff self.ratelimit_lastcall = time.time() except Exception as e: import traceback traceback.print_exc() raise MastodonRatelimitError("Rate limit time calculations failed: %s" % e) if "error" in response and response["error"] == "Throttled": if self.ratelimit_method == "throw": raise MastodonRatelimitError("Hit rate limit.") if self.ratelimit_method == "wait" or self.ratelimit_method == "pace": to_next = self.ratelimit_reset - time.time() if to_next > 0: # As a precaution, never sleep longer than 5 minutes request_complete = False to_next = min(to_next, 5 * 60) time.sleep(to_next) return response def __stream(self, endpoint, listener, params = {}): """ Internal streaming API helper. """ headers = {} if self.access_token != None: headers = {'Authorization': 'Bearer ' + self.access_token} url = self.api_base_url + endpoint with closing(requests.get(url, headers = headers, data = params, stream = True)) as r: listener.handle_stream(r.iter_lines()) def __generate_params(self, params, exclude = []): """ Internal named-parameters-to-dict helper. Note for developers: If called with locals() as params, as is the usual practice in this code, the __generate_params call (or at least the locals() call) should generally be the first thing in your function. """ params = dict(params) del params['self'] param_keys = list(params.keys()) for key in param_keys: if params[key] == None or key in exclude: del params[key] param_keys = list(params.keys()) for key in param_keys: if isinstance(params[key], list): params[key + "[]"] = params[key] del params[key] return params