diff options
Diffstat (limited to 'mastodon')
-rw-r--r-- | mastodon/Mastodon.py | 86 |
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 | |||
7 | import time | 7 | import time |
8 | import random | 8 | import random |
9 | import string | 9 | import string |
10 | from datetime import datetime | 10 | import pytz |
11 | import datetime | ||
12 | import dateutil | ||
13 | import dateutil.parser | ||
11 | 14 | ||
12 | class Mastodon: | 15 | class 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: | |||
547 | class MastodonIllegalArgumentError(ValueError): | 578 | class MastodonIllegalArgumentError(ValueError): |
548 | pass | 579 | pass |
549 | 580 | ||
581 | class MastodonFileNotFoundError(IOError): | ||
582 | pass | ||
583 | |||
550 | class MastodonNetworkError(IOError): | 584 | class MastodonNetworkError(IOError): |
551 | pass | 585 | pass |
552 | 586 | ||