Mastodonplus.py/mastodon/utils.py
2017-04-10 22:48:30 +00:00

184 líneas
6,8 KiB
Python

#!/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