aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/index.rst27
-rw-r--r--mastodon/Mastodon.py86
2 files changed, 87 insertions, 26 deletions
diff --git a/docs/index.rst b/docs/index.rst
index e7c9366..02676a4 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -39,6 +39,33 @@ as a single python module. By default, it talks to the
39`Mastodon flagship instance`_, but it can be set to talk to any 39`Mastodon flagship instance`_, but it can be set to talk to any
40node running Mastodon. 40node running Mastodon.
41 41
42A note about rate limits
43------------------------
44Mastodons API rate limits per IP. Mastodon.py has three modes for dealing
45with rate limiting that you can pass to the constructor, "throw", "wait"
46and "pace", "wait" being the default.
47
48In "throw" mode, Mastodon.py makes no attempt to stick to rate limits. When
49a request hits the rate limit, it simply throws a MastodonRateLimitError. This is
50for applications that need to handle all rate limiting themselves (i.e. interactive apps),
51or applications wanting to use Mastodon.py in a multi-threaded context ("wait" and "pace"
52modes are not thread safe).
53
54In "wait" mode, once a request hits the rate limit, Mastodon.py will wait until
55the rate limit resets and then try again, until the request succeeds or an error
56is encountered. This mode is for applications that would rather just not worry about rate limits
57much, don't poll the api all that often, and are okay with a call sometimes just taking
58a while.
59
60In "pace" mode, Mastodon.py will delay each new request after the first one such that,
61if requests were to continue at the same rate, only a certain fraction (set in the
62constructor as ratelimit_pacefactor) of the rate limit will be used up. The fraction can
63be (and by default, is) greater than one. If the rate limit is hit, "pace" behaves like
64"wait". This mode is probably the most advanced one and allows you to just poll in
65a loop without ever sleeping at all yourself. It is for applications that would rather
66just pretend there is no such thing as a rate limit and are fine with sometimes not
67being very interactive.
68
42A note about IDs 69A note about IDs
43---------------- 70----------------
44Mastodons API uses IDs in several places: User IDs, Toot IDs, ... 71Mastodons API uses IDs in several places: User IDs, Toot IDs, ...
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)