aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'mastodon')
-rw-r--r--mastodon/Mastodon.py86
1 files changed, 60 insertions, 26 deletions
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py
index bc1c52b..bb16d95 100644
--- a/mastodon/Mastodon.py
+++ b/mastodon/Mastodon.py
@@ -7,7 +7,10 @@ import mimetypes
7import time 7import time
8import random 8import random
9import string 9import string
10from datetime import datetime 10import pytz
11import datetime
12import dateutil
13import dateutil.parser
11 14
12class Mastodon: 15class Mastodon:
13 """ 16 """
@@ -62,12 +65,12 @@ class Mastodon:
62 ### 65 ###
63 # Authentication, including constructor 66 # Authentication, including constructor
64 ### 67 ###
65 def __init__(self, client_id, client_secret = None, access_token = None, api_base_url = __DEFAULT_BASE_URL, debug_requests = False, ratelimit_method = "wait", ratelimit_pacefactor = 0.9): 68 def __init__(self, client_id, client_secret = None, access_token = None, api_base_url = __DEFAULT_BASE_URL, debug_requests = False, ratelimit_method = "wait", ratelimit_pacefactor = 1.1):
66 """ 69 """
67 Create a new API wrapper instance based on the given client_secret and client_id. If you 70 Create a new API wrapper instance based on the given client_secret and client_id. If you
68 give a client_id and it is not a file, you must also give a secret. 71 give a client_id and it is not a file, you must also give a secret.
69 72
70 You can also directly specify an access_token, directly or as a file. 73 You can also specify an access_token, directly or as a file (as written by log_in).
71 74
72 Mastodon.py can try to respect rate limits in several ways, controlled by ratelimit_method. 75 Mastodon.py can try to respect rate limits in several ways, controlled by ratelimit_method.
73 "throw" makes functions throw a MastodonRatelimitError when the rate 76 "throw" makes functions throw a MastodonRatelimitError when the rate
@@ -92,7 +95,7 @@ class Mastodon:
92 self.ratelimit_reset = time.time() 95 self.ratelimit_reset = time.time()
93 self.ratelimit_remaining = 150 96 self.ratelimit_remaining = 150
94 self.ratelimit_lastcall = time.time() 97 self.ratelimit_lastcall = time.time()
95 self.ratelimit_pacefactor = 0.9 98 self.ratelimit_pacefactor = ratelimit_pacefactor
96 99
97 if os.path.isfile(self.client_id): 100 if os.path.isfile(self.client_id):
98 with open(self.client_id, 'r') as secret_file: 101 with open(self.client_id, 'r') as secret_file:
@@ -426,15 +429,26 @@ class Mastodon:
426 ### 429 ###
427 # Internal helpers, dragons probably 430 # Internal helpers, dragons probably
428 ### 431 ###
429 def __api_request(self, method, endpoint, params = {}, files = {}, do_ratelimiting = True): 432 def __datetime_to_epoch(self, date_time):
430 """ 433 """
431 Internal API request helper. 434 Converts a python datetime to unix epoch, accounting for
435 time zones and such.
432 436
433 TODO FIXME: time.time() does not match server time neccesarily. Using the time from the request 437 Assumes UTC if timezone is not given.
434 would be correct. 438 """
439 date_time_utc = None
440 if date_time.tzinfo == None:
441 date_time_utc = date_time.replace(tzinfo = pytz.utc)
442 else:
443 date_time_utc = date_time.astimezone(pytz.utc)
435 444
436 TODO FIXME: Date parsing can fail. Should probably use a proper "date parsing" module rather than 445 epoch_utc = datetime.datetime.utcfromtimestamp(0).replace(tzinfo = pytz.utc)
437 rely on the server to return the right thing. 446
447 return (date_time_utc - epoch_utc).total_seconds()
448
449 def __api_request(self, method, endpoint, params = {}, files = {}, do_ratelimiting = True):
450 """
451 Internal API request helper.
438 """ 452 """
439 response = None 453 response = None
440 headers = None 454 headers = None
@@ -445,6 +459,8 @@ class Mastodon:
445 if self.ratelimit_remaining == 0: 459 if self.ratelimit_remaining == 0:
446 to_next = self.ratelimit_reset - time.time() 460 to_next = self.ratelimit_reset - time.time()
447 if to_next > 0: 461 if to_next > 0:
462 # As a precaution, never sleep longer than 5 minutes
463 to_next = min(to_next, 5 * 60)
448 time.sleep(to_next) 464 time.sleep(to_next)
449 else: 465 else:
450 time_waited = time.time() - self.ratelimit_lastcall 466 time_waited = time.time() - self.ratelimit_lastcall
@@ -452,7 +468,9 @@ class Mastodon:
452 remaining_wait = time_wait - time_waited 468 remaining_wait = time_wait - time_waited
453 469
454 if remaining_wait > 0: 470 if remaining_wait > 0:
455 time.sleep(remaining_wait * self.ratelimit_pacefactor) 471 to_next = remaining_wait / self.ratelimit_pacefactor
472 to_next = min(to_next, 5 * 60)
473 time.sleep(to_next)
456 474
457 # Generate request headers 475 # Generate request headers
458 if self.access_token != None: 476 if self.access_token != None:
@@ -503,21 +521,34 @@ class Mastodon:
503 raise MastodonAPIError("Could not parse response as JSON, respose code was " + str(response_object.status_code)) 521 raise MastodonAPIError("Could not parse response as JSON, respose code was " + str(response_object.status_code))
504 522
505 # Handle rate limiting 523 # Handle rate limiting
506 if 'X-RateLimit-Remaining' in response_object.headers and do_ratelimiting: 524 try:
507 self.ratelimit_remaining = int(response_object.headers['X-RateLimit-Remaining']) 525 if 'X-RateLimit-Remaining' in response_object.headers and do_ratelimiting:
508 self.ratelimit_limit = int(response_object.headers['X-RateLimit-Limit']) 526 self.ratelimit_remaining = int(response_object.headers['X-RateLimit-Remaining'])
509 self.ratelimit_reset = (datetime.strptime(response_object.headers['X-RateLimit-Reset'], "%Y-%m-%dT%H:%M:%S.%fZ") - datetime(1970, 1, 1)).total_seconds() 527 self.ratelimit_limit = int(response_object.headers['X-RateLimit-Limit'])
510 self.ratelimit_lastcall = time.time() 528
511 529 ratelimit_reset_datetime = dateutil.parser.parse(response_object.headers['X-RateLimit-Reset'])
512 if "error" in response and response["error"] == "Throttled": 530 self.ratelimit_reset = self.__datetime_to_epoch(ratelimit_reset_datetime)
513 if self.ratelimit_method == "throw": 531
514 raise MastodonRatelimitError("Hit rate limit.") 532 # Adjust server time to local clock
515 533 server_time_datetime = dateutil.parser.parse(response_object.headers['Date'])
516 if self.ratelimit_method == "wait" or self.ratelimit_method == "pace": 534 server_time = self.__datetime_to_epoch(server_time_datetime)
517 to_next = self.ratelimit_reset - time.time() 535 server_time_diff = time.time() - server_time
518 if to_next > 0: 536 self.ratelimit_reset += server_time_diff
519 time.sleep(to_next) 537 self.ratelimit_lastcall = time.time()
520 request_complete = False 538
539 if "error" in response and response["error"] == "Throttled":
540 if self.ratelimit_method == "throw":
541 raise MastodonRatelimitError("Hit rate limit.")
542
543 if self.ratelimit_method == "wait" or self.ratelimit_method == "pace":
544 to_next = self.ratelimit_reset - time.time()
545 if to_next > 0:
546 # As a precaution, never sleep longer than 5 minutes
547 to_next = min(to_next, 5 * 60)
548 time.sleep(to_next)
549 request_complete = False
550 except:
551 raise MastodonRatelimitError("Rate limit time calculations failed.")
521 552
522 return response 553 return response
523 554
@@ -547,6 +578,9 @@ class Mastodon:
547class MastodonIllegalArgumentError(ValueError): 578class MastodonIllegalArgumentError(ValueError):
548 pass 579 pass
549 580
581class MastodonFileNotFoundError(IOError):
582 pass
583
550class MastodonNetworkError(IOError): 584class MastodonNetworkError(IOError):
551 pass 585 pass
552 586
Powered by cgit v1.2.3 (git 2.41.0)