diff options
Diffstat (limited to 'mastodon/Mastodon.py')
-rw-r--r-- | mastodon/Mastodon.py | 102 |
1 files changed, 82 insertions, 20 deletions
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py index ab9ac1f..3851fbf 100644 --- a/mastodon/Mastodon.py +++ b/mastodon/Mastodon.py | |||
@@ -120,10 +120,27 @@ class AttribAccessDict(dict): | |||
120 | raise AttributeError("Attribute-style access is read only") | 120 | raise AttributeError("Attribute-style access is read only") |
121 | super(AttribAccessDict, self).__setattr__(attr, val) | 121 | super(AttribAccessDict, self).__setattr__(attr, val) |
122 | 122 | ||
123 | |||
123 | ### | 124 | ### |
124 | # The actual Mastodon class | 125 | # List helper class. |
126 | # Defined at top level so it can be pickled. | ||
125 | ### | 127 | ### |
128 | class AttribAccessList(list): | ||
129 | def __getattr__(self, attr): | ||
130 | if attr in self: | ||
131 | return self[attr] | ||
132 | else: | ||
133 | raise AttributeError("Attribute not found: " + str(attr)) | ||
126 | 134 | ||
135 | def __setattr__(self, attr, val): | ||
136 | if attr in self: | ||
137 | raise AttributeError("Attribute-style access is read only") | ||
138 | super(AttribAccessList, self).__setattr__(attr, val) | ||
139 | |||
140 | |||
141 | ### | ||
142 | # The actual Mastodon class | ||
143 | ### | ||
127 | class Mastodon: | 144 | class Mastodon: |
128 | """ | 145 | """ |
129 | Thorough and easy to use Mastodon | 146 | Thorough and easy to use Mastodon |
@@ -1636,13 +1653,23 @@ class Mastodon: | |||
1636 | # Reading data: Bookmarks | 1653 | # Reading data: Bookmarks |
1637 | ### | 1654 | ### |
1638 | @api_version("3.1.0", "3.1.0", __DICT_VERSION_STATUS) | 1655 | @api_version("3.1.0", "3.1.0", __DICT_VERSION_STATUS) |
1639 | def bookmarks(self): | 1656 | def bookmarks(self, max_id=None, min_id=None, since_id=None, limit=None): |
1640 | """ | 1657 | """ |
1641 | Get a list of statuses bookmarked by the logged-in user. | 1658 | Get a list of statuses bookmarked by the logged-in user. |
1642 | 1659 | ||
1643 | Returns a list of `toot dicts`_. | 1660 | Returns a list of `toot dicts`_. |
1644 | """ | 1661 | """ |
1645 | return self.__api_request('GET', '/api/v1/bookmarks') | 1662 | if max_id != None: |
1663 | max_id = self.__unpack_id(max_id) | ||
1664 | |||
1665 | if min_id != None: | ||
1666 | min_id = self.__unpack_id(min_id) | ||
1667 | |||
1668 | if since_id != None: | ||
1669 | since_id = self.__unpack_id(since_id) | ||
1670 | |||
1671 | params = self.__generate_params(locals()) | ||
1672 | return self.__api_request('GET', '/api/v1/bookmarks', params) | ||
1646 | 1673 | ||
1647 | ### | 1674 | ### |
1648 | # Writing data: Statuses | 1675 | # Writing data: Statuses |
@@ -3043,8 +3070,8 @@ class Mastodon: | |||
3043 | Returns the next page or None if no further data is available. | 3070 | Returns the next page or None if no further data is available. |
3044 | """ | 3071 | """ |
3045 | if isinstance(previous_page, list) and len(previous_page) != 0: | 3072 | if isinstance(previous_page, list) and len(previous_page) != 0: |
3046 | if hasattr(previous_page[-1], '_pagination_next'): | 3073 | if hasattr(previous_page, '_pagination_next'): |
3047 | params = copy.deepcopy(previous_page[-1]._pagination_next) | 3074 | params = copy.deepcopy(previous_page._pagination_next) |
3048 | else: | 3075 | else: |
3049 | return None | 3076 | return None |
3050 | else: | 3077 | else: |
@@ -3067,8 +3094,8 @@ class Mastodon: | |||
3067 | Returns the previous page or None if no further data is available. | 3094 | Returns the previous page or None if no further data is available. |
3068 | """ | 3095 | """ |
3069 | if isinstance(next_page, list) and len(next_page) != 0: | 3096 | if isinstance(next_page, list) and len(next_page) != 0: |
3070 | if hasattr(next_page[0], '_pagination_prev'): | 3097 | if hasattr(next_page, '_pagination_prev'): |
3071 | params = copy.deepcopy(next_page[0]._pagination_prev) | 3098 | params = copy.deepcopy(next_page._pagination_prev) |
3072 | else: | 3099 | else: |
3073 | return None | 3100 | return None |
3074 | else: | 3101 | else: |
@@ -3233,7 +3260,7 @@ class Mastodon: | |||
3233 | if (key in json_object and isinstance(json_object[key], six.text_type)): | 3260 | if (key in json_object and isinstance(json_object[key], six.text_type)): |
3234 | if json_object[key].lower() == 'true': | 3261 | if json_object[key].lower() == 'true': |
3235 | json_object[key] = True | 3262 | json_object[key] = True |
3236 | if json_object[key].lower() == 'False': | 3263 | if json_object[key].lower() == 'false': |
3237 | json_object[key] = False | 3264 | json_object[key] = False |
3238 | return json_object | 3265 | return json_object |
3239 | 3266 | ||
@@ -3446,6 +3473,7 @@ class Mastodon: | |||
3446 | if isinstance(response, list) and \ | 3473 | if isinstance(response, list) and \ |
3447 | 'Link' in response_object.headers and \ | 3474 | 'Link' in response_object.headers and \ |
3448 | response_object.headers['Link'] != "": | 3475 | response_object.headers['Link'] != "": |
3476 | response = AttribAccessList(response) | ||
3449 | tmp_urls = requests.utils.parse_header_links( | 3477 | tmp_urls = requests.utils.parse_header_links( |
3450 | response_object.headers['Link'].rstrip('>').replace('>,<', ',<')) | 3478 | response_object.headers['Link'].rstrip('>').replace('>,<', ',<')) |
3451 | for url in tmp_urls: | 3479 | for url in tmp_urls: |
@@ -3470,7 +3498,12 @@ class Mastodon: | |||
3470 | del next_params['since_id'] | 3498 | del next_params['since_id'] |
3471 | if "min_id" in next_params: | 3499 | if "min_id" in next_params: |
3472 | del next_params['min_id'] | 3500 | del next_params['min_id'] |
3473 | response[-1]._pagination_next = next_params | 3501 | response._pagination_next = next_params |
3502 | |||
3503 | # Maybe other API users rely on the pagination info in the last item | ||
3504 | # Will be removed in future | ||
3505 | if isinstance(response[-1], AttribAccessDict): | ||
3506 | response[-1]._pagination_next = next_params | ||
3474 | 3507 | ||
3475 | if url['rel'] == 'prev': | 3508 | if url['rel'] == 'prev': |
3476 | # Be paranoid and extract since_id or min_id specifically | 3509 | # Be paranoid and extract since_id or min_id specifically |
@@ -3489,8 +3522,13 @@ class Mastodon: | |||
3489 | prev_params['since_id'] = since_id | 3522 | prev_params['since_id'] = since_id |
3490 | if "max_id" in prev_params: | 3523 | if "max_id" in prev_params: |
3491 | del prev_params['max_id'] | 3524 | del prev_params['max_id'] |
3492 | response[0]._pagination_prev = prev_params | 3525 | response._pagination_prev = prev_params |
3493 | 3526 | ||
3527 | # Maybe other API users rely on the pagination info in the first item | ||
3528 | # Will be removed in future | ||
3529 | if isinstance(response[0], AttribAccessDict): | ||
3530 | response[0]._pagination_prev = prev_params | ||
3531 | |||
3494 | # New and fantastico (post-2.6.0): min_id pagination | 3532 | # New and fantastico (post-2.6.0): min_id pagination |
3495 | matchgroups = re.search(r"[?&]min_id=([^&]+)", prev_url) | 3533 | matchgroups = re.search(r"[?&]min_id=([^&]+)", prev_url) |
3496 | if matchgroups: | 3534 | if matchgroups: |
@@ -3504,7 +3542,12 @@ class Mastodon: | |||
3504 | prev_params['min_id'] = min_id | 3542 | prev_params['min_id'] = min_id |
3505 | if "max_id" in prev_params: | 3543 | if "max_id" in prev_params: |
3506 | del prev_params['max_id'] | 3544 | del prev_params['max_id'] |
3507 | response[0]._pagination_prev = prev_params | 3545 | response._pagination_prev = prev_params |
3546 | |||
3547 | # Maybe other API users rely on the pagination info in the first item | ||
3548 | # Will be removed in future | ||
3549 | if isinstance(response[0], AttribAccessDict): | ||
3550 | response[0]._pagination_prev = prev_params | ||
3508 | 3551 | ||
3509 | return response | 3552 | return response |
3510 | 3553 | ||
@@ -3570,7 +3613,8 @@ class Mastodon: | |||
3570 | 3613 | ||
3571 | def close(self): | 3614 | def close(self): |
3572 | self.closed = True | 3615 | self.closed = True |
3573 | self.connection.close() | 3616 | if not self.connection is None: |
3617 | self.connection.close() | ||
3574 | 3618 | ||
3575 | def is_alive(self): | 3619 | def is_alive(self): |
3576 | return self._thread.is_alive() | 3620 | return self._thread.is_alive() |
@@ -3581,6 +3625,14 @@ class Mastodon: | |||
3581 | else: | 3625 | else: |
3582 | return True | 3626 | return True |
3583 | 3627 | ||
3628 | def _sleep_attentive(self): | ||
3629 | if self._thread != threading.current_thread(): | ||
3630 | raise RuntimeError ("Illegal call from outside the stream_handle thread") | ||
3631 | time_remaining = self.reconnect_async_wait_sec | ||
3632 | while time_remaining>0 and not self.closed: | ||
3633 | time.sleep(0.5) | ||
3634 | time_remaining -= 0.5 | ||
3635 | |||
3584 | def _threadproc(self): | 3636 | def _threadproc(self): |
3585 | self._thread = threading.current_thread() | 3637 | self._thread = threading.current_thread() |
3586 | 3638 | ||
@@ -3602,16 +3654,26 @@ class Mastodon: | |||
3602 | self.reconnecting = True | 3654 | self.reconnecting = True |
3603 | connect_success = False | 3655 | connect_success = False |
3604 | while not connect_success: | 3656 | while not connect_success: |
3605 | connect_success = True | 3657 | if self.closed: |
3658 | # Someone from outside stopped the streaming | ||
3659 | self.running = False | ||
3660 | break | ||
3606 | try: | 3661 | try: |
3607 | self.connection = self.connect_func() | 3662 | the_connection = self.connect_func() |
3608 | if self.connection.status_code != 200: | 3663 | if the_connection.status_code != 200: |
3609 | time.sleep(self.reconnect_async_wait_sec) | 3664 | exception = MastodonNetworkError(f"Could not connect to server. " |
3610 | connect_success = False | 3665 | f"HTTP status: {the_connection.status_code}") |
3611 | exception = MastodonNetworkError("Could not connect to server.") | ||
3612 | listener.on_abort(exception) | 3666 | listener.on_abort(exception) |
3667 | self._sleep_attentive() | ||
3668 | if self.closed: | ||
3669 | # Here we have maybe a rare race condition. Exactly on connect, someone | ||
3670 | # stopped the streaming before. We close the previous established connection: | ||
3671 | the_connection.close() | ||
3672 | else: | ||
3673 | self.connection = the_connection | ||
3674 | connect_success = True | ||
3613 | except: | 3675 | except: |
3614 | time.sleep(self.reconnect_async_wait_sec) | 3676 | self._sleep_attentive() |
3615 | connect_success = False | 3677 | connect_success = False |
3616 | self.reconnecting = False | 3678 | self.reconnecting = False |
3617 | else: | 3679 | else: |