Merge remote-tracking branch 'refs/remotes/origin/master' into ratelimits

# Conflicts:
#	mastodon/Mastodon.py
This commit is contained in:
Lorenz Diener 2016-11-25 20:57:53 +01:00
commit ab58894041
S'han modificat 2 arxius amb 213 adicions i 80 eliminacions

Veure arxiu

@ -1,5 +1,7 @@
Mastodon.py Mastodon.py
=========== ===========
.. py:module:: mastodon
.. py:class: Mastodon
.. code-block:: python .. code-block:: python
@ -37,14 +39,114 @@ as a single python module. By default, it talks to the
`Mastodon flagship instance`_, but it can be set to talk to any `Mastodon flagship instance`_, but it can be set to talk to any
node running Mastodon. node running Mastodon.
A note about IDs
----------------
Mastodons API uses IDs in several places: User IDs, Toot IDs, ...
While debugging, it might be tempting to copy-paste in IDs from the
web interface into your code. This will not work, as the IDs on the web
interface and in the URLs are not the same as the IDs used internally
in the API, so don't do that.
Return values
-------------
Unless otherwise specified, all data is returned as python Unless otherwise specified, all data is returned as python
dictionaries, matching the JSON format used by the API. dictionaries, matching the JSON format used by the API.
For complete documentation on what every function returns,
check the `Mastodon API docs`_, or just play around a bit - the
format of the data is generally very easy to understand.
.. py:module:: mastodon User dicts
.. py:class: Mastodon ~~~~~~~~~~
.. code-block:: python
mastodon.account(<numerical id>)
# Returns the following dictionary:
{
'display_name': The user's display name
'acct': The user's account name as username@domain (@domain omitted for local users)
'following_count': How many people they follow
'url': Their URL; usually 'https://mastodon.social/users/<acct>'
'statuses_count': How many statuses they have
'followers_count': How many followers they have
'avatar': URL for their avatar
'note': Their bio
'header': URL for their header image
'id': Same as <numerical id>
'username': The username (what you @ them with)
}
Toot dicts
~~~~~~~~~~
.. code-block:: python
mastodon.toot("Hello from Python")
# Returns the following dictionary:
{
'sensitive': Denotes whether the toot is marked sensitive
'created_at': Creation time
'mentions': A list of account dicts mentioned in the toot
'uri': Descriptor for the toot
EG 'tag:mastodon.social,2016-11-25:objectId=<id>:objectType=Status'
'tags': A list of hashtag dicts used in the toot
'in_reply_to_id': Numerical id of the toot this toot is in response to
'id': Numerical id of this toot
'reblogs_count': Number of reblogs
'favourites_count': Number of favourites
'reblog': Denotes whether the toot is a reblog
'url': URL of the toot
'content': Content of the toot, as HTML: '<p>Hello from Python</p>'
'favourited': Denotes whether the logged in user has favourited this toot
'account': Account dict for the logged in account
}
Relationship dicts
~~~~~~~~~~~~~~~~~~
.. code-block:: python
mastodon.account_follow(<numerical id>)
# Returns the following dictionary:
{
'followed_by': Boolean denoting whether they follow you back
'following': Boolean denoting whether you follow them
'id': Numerical id (same one as <numerical id>)
'blocking': Boolean denoting whether you are blocking them
}
Notification dicts
~~~~~~~~~~~~~~~~~~
.. code-block:: python
mastodon.notifications()[0]
# Returns the following dictionary:
{
'id': id of the notification.
'type': "mention", "reblog", "favourite" or "follow".
'status': In case of "mention", the mentioning status.
In case of reblog / favourite, the reblogged / favourited status.
'account': User dict of the user from whom the notification originates.
}
Context dicts
~~~~~~~~~~~~~
.. code-block:: python
mastodon.status_context(<numerical id>)
# Returns the following dictionary:
{
'descendants': A list of toot dicts
'ancestors': A list of toot dicts
}
Media dicts
~~~~~~~~~~~
.. code-block:: python
mastodon.media_post("image.jpg", "image/jpeg")
# Returns the following dictionary:
{
'text_url': The display text for the media (what shows up in toots)
'preview_url': The URL for the media preview
'type': Media type, EG 'image'
'url': The URL for the media
}
App registration and user authentication App registration and user authentication
---------------------------------------- ----------------------------------------
@ -91,7 +193,6 @@ This function allows you to get information about a users notifications.
.. automethod:: Mastodon.notifications .. automethod:: Mastodon.notifications
Reading data: Accounts Reading data: Accounts
---------------------- ----------------------
These functions allow you to get information about accounts and These functions allow you to get information about accounts and
@ -103,7 +204,6 @@ their relationships.
.. automethod:: Mastodon.account_following .. automethod:: Mastodon.account_following
.. automethod:: Mastodon.account_followers .. automethod:: Mastodon.account_followers
.. automethod:: Mastodon.account_relationships .. automethod:: Mastodon.account_relationships
.. automethod:: Mastodon.account_suggestions
.. automethod:: Mastodon.account_search .. automethod:: Mastodon.account_search
Writing data: Statuses Writing data: Statuses
@ -113,11 +213,11 @@ interact with already posted statuses.
.. automethod:: Mastodon.status_post .. automethod:: Mastodon.status_post
.. automethod:: Mastodon.toot .. automethod:: Mastodon.toot
.. automethod:: Mastodon.status_delete
.. automethod:: Mastodon.status_reblog .. automethod:: Mastodon.status_reblog
.. automethod:: Mastodon.status_unreblog .. automethod:: Mastodon.status_unreblog
.. automethod:: Mastodon.status_favourite .. automethod:: Mastodon.status_favourite
.. automethod:: Mastodon.status_unfavourite .. automethod:: Mastodon.status_unfavourite
.. automethod:: Mastodon.status_delete
Writing data: Accounts Writing data: Accounts
---------------------- ----------------------
@ -137,6 +237,7 @@ to attach media to statuses.
.. automethod:: Mastodon.media_post .. automethod:: Mastodon.media_post
.. _Mastodon: https://github.com/Gargron/mastodon .. _Mastodon: https://github.com/Gargron/mastodon
.. _Mastodon flagship instance: http://mastodon.social/ .. _Mastodon flagship instance: http://mastodon.social/
.. _Mastodon api docs: https://github.com/Gargron/mastodon/wiki/API .. _Mastodon api docs: https://github.com/Gargron/mastodon/wiki/API

Veure arxiu

@ -29,7 +29,7 @@ class Mastodon:
@staticmethod @staticmethod
def create_app(client_name, scopes = ['read', 'write', 'follow'], redirect_uris = None, to_file = None, api_base_url = __DEFAULT_BASE_URL): def create_app(client_name, scopes = ['read', 'write', 'follow'], redirect_uris = None, to_file = None, api_base_url = __DEFAULT_BASE_URL):
""" """
Creates a new app with given client_name and scopes (read, write, follow) Create a new app with given client_name and scopes (read, write, follow)
Specify redirect_uris if you want users to be redirected to a certain page after authenticating. Specify redirect_uris if you want users to be redirected to a certain page after authenticating.
Specify to_file to persist your apps info to a file so you can use them in the constructor. Specify to_file to persist your apps info to a file so you can use them in the constructor.
@ -64,7 +64,7 @@ class Mastodon:
### ###
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): 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):
""" """
Creates a new API wrapper instance based on the given client_secret and client_id. If you Create a new API wrapper instance based on the given client_secret and client_id. If you
give a client_id and it is not a file, you must also give a secret. give a client_id and it is not a file, you must also give a secret.
You can also directly specify an access_token, directly or as a file. You can also directly specify an access_token, directly or as a file.
@ -108,15 +108,15 @@ class Mastodon:
def log_in(self, username, password, scopes = ['read', 'write', 'follow'], to_file = None): def log_in(self, username, password, scopes = ['read', 'write', 'follow'], to_file = None):
""" """
Logs in and sets access_token to what was returned. Note that your Log in and sets access_token to what was returned. Note that your
username is the e-mail you use to log in into mastodon. username is the e-mail you use to log in into mastodon.
Can persist access token to file, to be used in the constructor. Can persist access token to file, to be used in the constructor.
Will throw an exception if username / password are wrong, scopes are not Will throw a MastodonIllegalArgumentError if username / password
valid or granted scopes differ from requested. are wrong, scopes are not valid or granted scopes differ from requested.
Returns the access_token, as well. Returns the access_token.
""" """
params = self.__generate_params(locals()) params = self.__generate_params(locals())
params['client_id'] = self.client_id params['client_id'] = self.client_id
@ -125,7 +125,7 @@ class Mastodon:
params['scope'] = " ".join(scopes) params['scope'] = " ".join(scopes)
try: try:
response = self.__api_request('POST', '/oauth/token', params) response = self.__api_request('POST', '/oauth/token', params, do_ratelimiting = False)
self.access_token = response['access_token'] self.access_token = response['access_token']
except: except:
raise MastodonIllegalArgumentError('Invalid user name, password or scopes.') raise MastodonIllegalArgumentError('Invalid user name, password or scopes.')
@ -147,35 +147,45 @@ class Mastodon:
## ##
def timeline(self, timeline = "home", max_id = None, since_id = None, limit = None): def timeline(self, timeline = "home", max_id = None, since_id = None, limit = None):
""" """
Returns statuses, most recent ones first. Timeline can be home, mentions, public Fetch statuses, most recent ones first. Timeline can be home, mentions, public
or tag/hashtag. See the following functions documentation for what those do. or tag/hashtag. See the following functions documentation for what those do.
The default timeline is the "home" timeline. The default timeline is the "home" timeline.
Returns a list of toot dicts.
""" """
params = self.__generate_params(locals(), ['timeline']) params = self.__generate_params(locals(), ['timeline'])
return self.__api_request('GET', '/api/v1/timelines/' + timeline, params) return self.__api_request('GET', '/api/v1/timelines/' + timeline, params)
def timeline_home(self, max_id = None, since_id = None, limit = None): def timeline_home(self, max_id = None, since_id = None, limit = None):
""" """
Returns the authenticated users home timeline (i.e. followed users and self). Fetch the authenticated users home timeline (i.e. followed users and self).
Returns a list of toot dicts.
""" """
return self.timeline('home', max_id = max_id, since_id = since_id, limit = limit) return self.timeline('home', max_id = max_id, since_id = since_id, limit = limit)
def timeline_mentions(self, max_id = None, since_id = None, limit = None): def timeline_mentions(self, max_id = None, since_id = None, limit = None):
""" """
Returns the authenticated users mentions. Fetches the authenticated users mentions.
Returns a list of toot dicts.
""" """
return self.timeline('mentions', max_id = max_id, since_id = since_id, limit = limit) return self.timeline('mentions', max_id = max_id, since_id = since_id, limit = limit)
def timeline_public(self, max_id = None, since_id = None, limit = None): def timeline_public(self, max_id = None, since_id = None, limit = None):
""" """
Returns the public / visible-network timeline. Fetches the public / visible-network timeline.
Returns a list of toot dicts.
""" """
return self.timeline('public', max_id = max_id, since_id = since_id, limit = limit) return self.timeline('public', max_id = max_id, since_id = since_id, limit = limit)
def timeline_hashtag(self, hashtag, max_id = None, since_id = None, limit = None): def timeline_hashtag(self, hashtag, max_id = None, since_id = None, limit = None):
""" """
Returns all toots with a given hashtag. Fetch a timeline of toots with a given hashtag.
Returns a list of toot dicts.
""" """
return self.timeline('tag/' + str(hashtag), max_id = max_id, since_id = since_id, limit = limit) return self.timeline('tag/' + str(hashtag), max_id = max_id, since_id = since_id, limit = limit)
@ -184,25 +194,33 @@ class Mastodon:
### ###
def status(self, id): def status(self, id):
""" """
Returns a status. Fetch information about a single toot.
Returns a toot dict.
""" """
return self.__api_request('GET', '/api/v1/statuses/' + str(id)) return self.__api_request('GET', '/api/v1/statuses/' + str(id))
def status_context(self, id): def status_context(self, id):
""" """
Returns ancestors and descendants of the status. Fetch information about ancestors and descendants of a toot.
Returns a context dict.
""" """
return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/context') return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/context')
def status_reblogged_by(self, id): def status_reblogged_by(self, id):
""" """
Returns a list of users that have reblogged a status. Fetch a list of users that have reblogged a status.
Returns a list of user dicts.
""" """
return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/reblogged_by') return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/reblogged_by')
def status_favourited_by(self, id): def status_favourited_by(self, id):
""" """
Returns a list of users that have favourited a status. Fetch a list of users that have favourited a status.
Returns a list of user dicts.
""" """
return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/favourited_by') return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/favourited_by')
@ -211,8 +229,10 @@ class Mastodon:
### ###
def notifications(self): def notifications(self):
""" """
Returns notifications (mentions, favourites, reblogs, follows) for the authenticated Fetch notifications (mentions, favourites, reblogs, follows) for the authenticated
user. user.
Returns a list of notification dicts.
""" """
return self.__api_request('GET', '/api/v1/notifications') return self.__api_request('GET', '/api/v1/notifications')
@ -221,53 +241,61 @@ class Mastodon:
### ###
def account(self, id): def account(self, id):
""" """
Returns account. Fetch account information by user id.
Returns a user dict.
""" """
return self.__api_request('GET', '/api/v1/accounts/' + str(id)) return self.__api_request('GET', '/api/v1/accounts/' + str(id))
def account_verify_credentials(self): def account_verify_credentials(self):
""" """
Returns authenticated user's account. Fetch authenticated user's account information.
Returns a user dict.
""" """
return self.__api_request('GET', '/api/v1/accounts/verify_credentials') return self.__api_request('GET', '/api/v1/accounts/verify_credentials')
def account_statuses(self, id, max_id = None, since_id = None, limit = None): def account_statuses(self, id, max_id = None, since_id = None, limit = None):
""" """
Returns statuses by user. Same options as timeline are permitted. Fetch statuses by user id. Same options as timeline are permitted.
Returns a list of toot dicts.
""" """
params = self.__generate_params(locals(), ['id']) params = self.__generate_params(locals(), ['id'])
return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/statuses', params) return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/statuses', params)
def account_following(self, id): def account_following(self, id):
""" """
Returns users the given user is following. Fetch users the given user is following.
Returns a list of user dicts.
""" """
return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/following') return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/following')
def account_followers(self, id): def account_followers(self, id):
""" """
Returns users the given user is followed by. Fetch users the given user is followed by.
Returns a list of user dicts.
""" """
return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/followers') return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/followers')
def account_relationships(self, id): def account_relationships(self, id):
""" """
Returns relationships (following, followed_by, blocking) of the logged in user to Fetch relationships (following, followed_by, blocking) of the logged in user to
a given account. id can be a list. a given account. id can be a list.
Returns a list of relationship dicts.
""" """
params = self.__generate_params(locals()) params = self.__generate_params(locals())
return self.__api_request('GET', '/api/v1/accounts/relationships', params) return self.__api_request('GET', '/api/v1/accounts/relationships', params)
def account_suggestions(self):
"""
Returns accounts that the system suggests the authenticated user to follow.
"""
return self.__api_request('GET', '/api/v1/accounts/suggestions')
def account_search(self, q, limit = None): def account_search(self, q, limit = None):
""" """
Returns matching accounts. Will lookup an account remotely if the search term is Fetch matching accounts. Will lookup an account remotely if the search term is
in the username@domain format and not yet in the database. in the username@domain format and not yet in the database.
Returns a list of user dicts.
""" """
params = self.__generate_params(locals()) params = self.__generate_params(locals())
return self.__api_request('GET', '/api/v1/accounts/search', params) return self.__api_request('GET', '/api/v1/accounts/search', params)
@ -277,10 +305,10 @@ class Mastodon:
### ###
def status_post(self, status, in_reply_to_id = None, media_ids = None): def status_post(self, status, in_reply_to_id = None, media_ids = None):
""" """
Posts a status. Can optionally be in reply to another status and contain Post a status. Can optionally be in reply to another status and contain
up to four pieces of media (Uploaded via media_post()). up to four pieces of media (Uploaded via media_post()).
Returns the new status. Returns a toot dict with the new status.
""" """
params = self.__generate_params(locals()) params = self.__generate_params(locals())
return self.__api_request('POST', '/api/v1/statuses', params) return self.__api_request('POST', '/api/v1/statuses', params)
@ -288,41 +316,46 @@ class Mastodon:
def toot(self, status): def toot(self, status):
""" """
Synonym for status_post that only takes the status text as input. Synonym for status_post that only takes the status text as input.
Returns a toot dict with the new status.
""" """
return self.status_post(status) return self.status_post(status)
def status_delete(self, id): def status_delete(self, id):
""" """
Deletes a status Delete a status
Returns an empty dict for good measure.
""" """
return self.__api_request('DELETE', '/api/v1/statuses/' + str(id)) return self.__api_request('DELETE', '/api/v1/statuses/' + str(id))
def status_reblog(self, id): def status_reblog(self, id):
"""Reblogs a status. """Reblog a status.
Returns a new status that wraps around the reblogged one.""" Returns a toot with with a new status that wraps around the reblogged one.
"""
return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/reblog") return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/reblog")
def status_unreblog(self, id): def status_unreblog(self, id):
""" """
Un-reblogs a status. Un-reblog a status.
Returns the status that used to be reblogged. Returns a toot dict with the status that used to be reblogged.
""" """
return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/unreblog") return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/unreblog")
def status_favourite(self, id): def status_favourite(self, id):
""" """
Favourites a status. Favourite a status.
Returns the favourited status. Returns a toot dict with the favourited status.
""" """
return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/favourite") return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/favourite")
def status_unfavourite(self, id): def status_unfavourite(self, id):
"""Favourites a status. """Favourite a status.
Returns the un-favourited status. Returns a toot dict with the un-favourited status.
""" """
return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/unfavourite") return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/unfavourite")
@ -331,33 +364,33 @@ class Mastodon:
### ###
def account_follow(self, id): def account_follow(self, id):
""" """
Follows a user. Follow a user.
Returns the updated relationship to the user. Returns a relationship dict containing the updated relationship to the user.
""" """
return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/follow") return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/follow")
def account_unfollow(self, id): def account_unfollow(self, id):
""" """
Unfollows a user. Unfollow a user.
Returns the updated relationship to the user. Returns a relationship dict containing the updated relationship to the user.
""" """
return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/unfollow") return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/unfollow")
def account_block(self, id): def account_block(self, id):
""" """
Blocks a user. Block a user.
Returns the updated relationship to the user. Returns a relationship dict containing the updated relationship to the user.
""" """
return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/block") return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/block")
def account_unblock(self, id): def account_unblock(self, id):
""" """
Unblocks a user. Unblock a user.
Returns the updated relationship to the user. Returns a relationship dict containing the updated relationship to the user.
""" """
return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/unblock") return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/unblock")
@ -366,20 +399,18 @@ class Mastodon:
### ###
def media_post(self, media_file, mime_type = None): def media_post(self, media_file, mime_type = None):
""" """
Posts an image. media_file can either be image data or Post an image. media_file can either be image data or
a file name. If image data is passed directly, the mime a file name. If image data is passed directly, the mime
type has to be specified manually, otherwise, it is type has to be specified manually, otherwise, it is
determined from the file name. determined from the file name.
Returns the uploaded media metadata object. Importantly, this contains
the ID that can then be used in status_post() to attach the media to
a toot.
Throws a MastodonIllegalArgumentError if the mime type of the Throws a MastodonIllegalArgumentError if the mime type of the
passed data or file can not be determined properly. passed data or file can not be determined properly.
"""
if os.path.isfile(media_file): Returns a media dict. This contains the id that can be used in
status_post to attach the media file to a toot.
"""
if os.path.isfile(media_file) and mime_type == None:
mime_type = mimetypes.guess_type(media_file)[0] mime_type = mimetypes.guess_type(media_file)[0]
media_file = open(media_file, 'rb') media_file = open(media_file, 'rb')
@ -395,7 +426,7 @@ class Mastodon:
### ###
# Internal helpers, dragons probably # Internal helpers, dragons probably
### ###
def __api_request(self, method, endpoint, params = {}, files = {}): def __api_request(self, method, endpoint, params = {}, files = {}, do_ratelimiting = True):
""" """
Internal API request helper. Internal API request helper.
@ -410,7 +441,7 @@ class Mastodon:
# "pace" mode ratelimiting: Assume constant rate of requests, sleep a little less long than it # "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. # would take to not hit the rate limit at that request rate.
if self.ratelimit_method == "pace": if do_ratelimiting and self.ratelimit_method == "pace":
if self.ratelimit_remaining == 0: if self.ratelimit_remaining == 0:
to_next = self.ratelimit_reset - time.time() to_next = self.ratelimit_reset - time.time()
if to_next > 0: if to_next > 0:
@ -472,20 +503,21 @@ class Mastodon:
raise MastodonAPIError("Could not parse response as JSON, respose code was " + str(response_object.status_code)) raise MastodonAPIError("Could not parse response as JSON, respose code was " + str(response_object.status_code))
# Handle rate limiting # Handle rate limiting
self.ratelimit_remaining = int(response_object.headers['X-RateLimit-Remaining']) if 'X-RateLimit-Remaining' in response_object.headers and do_ratelimiting:
self.ratelimit_limit = int(response_object.headers['X-RateLimit-Limit']) self.ratelimit_remaining = int(response_object.headers['X-RateLimit-Remaining'])
self.ratelimit_reset = (datetime.strptime(response_object.headers['X-RateLimit-Reset'], "%Y-%m-%dT%H:%M:%S.%fZ") - datetime(1970, 1, 1)).total_seconds() self.ratelimit_limit = int(response_object.headers['X-RateLimit-Limit'])
self.ratelimit_lastcall = time.time() self.ratelimit_reset = (datetime.strptime(response_object.headers['X-RateLimit-Reset'], "%Y-%m-%dT%H:%M:%S.%fZ") - datetime(1970, 1, 1)).total_seconds()
self.ratelimit_lastcall = time.time()
if "error" in response and response["error"] == "Throttled": if "error" in response and response["error"] == "Throttled":
if self.ratelimit_method == "throw": if self.ratelimit_method == "throw":
raise MastodonRatelimitError("Hit rate limit.") raise MastodonRatelimitError("Hit rate limit.")
if self.ratelimit_method == "wait" or self.ratelimit_method == "pace": if self.ratelimit_method == "wait" or self.ratelimit_method == "pace":
to_next = self.ratelimit_reset - time.time() to_next = self.ratelimit_reset - time.time()
if to_next > 0: if to_next > 0:
time.sleep(to_next) time.sleep(to_next)
request_complete = False request_complete = False
return response return response