diff --git a/setup.py b/setup.py index 5072649..987d424 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ def run_tests(self): cmdclass["build_docs"] = BuildDoc -requires = ["requests>=2.23.0"] +requires = ["aiohttp>=3.7.4.post0"] test_requirements = [ "black==20.8b1", diff --git a/tests/api/test_base.py b/tests/api/test_base.py index 4170162..b475b70 100644 --- a/tests/api/test_base.py +++ b/tests/api/test_base.py @@ -35,7 +35,7 @@ def test_get_request_headers_includes_authorization(): @responses.activate -def test_request_get_returns_dictionary_if_successful(): +async def test_request_get_returns_dictionary_if_successful(): responses.add( responses.GET, BASE_URL, @@ -45,14 +45,14 @@ def test_request_get_returns_dictionary_if_successful(): ) api = TwitchAPI(client_id="client") - response = api._request_get("") + response = await api._request_get("") assert isinstance(response, dict) assert response == dummy_data @responses.activate -def test_request_get_sends_headers_with_the_request(): +async def test_request_get_sends_headers_with_the_request(): responses.add( responses.GET, BASE_URL, @@ -62,14 +62,14 @@ def test_request_get_sends_headers_with_the_request(): ) api = TwitchAPI(client_id="client") - api._request_get("") + await api._request_get("") assert "Client-ID" in responses.calls[0].request.headers assert "Accept" in responses.calls[0].request.headers @responses.activate -def test_request_get_binary_body(): +async def test_request_get_binary_body(): responses.add( responses.GET, BASE_URL, @@ -79,14 +79,14 @@ def test_request_get_binary_body(): ) api = TwitchAPI(client_id="client") - response = api._request_get("", json=False) + response = await api._request_get("", json=False) assert response.content == b"binary" @responses.activate @pytest.mark.parametrize("status", [(500), (400)]) -def test_request_get_raises_exception_if_not_200_response(status, monkeypatch): +async def test_request_get_raises_exception_if_not_200_response(status, monkeypatch): responses.add( responses.GET, BASE_URL, status=status, content_type="application/json" ) @@ -99,11 +99,11 @@ def mockreturn(path): api = TwitchAPI(client_id="client") with pytest.raises(exceptions.HTTPError): - api._request_get("") + await api._request_get("") @responses.activate -def test_request_put_returns_dictionary_if_successful(): +async def test_request_put_returns_dictionary_if_successful(): responses.add( responses.PUT, BASE_URL, @@ -113,25 +113,25 @@ def test_request_put_returns_dictionary_if_successful(): ) api = TwitchAPI(client_id="client") - response = api._request_put("", dummy_data) + response = await api._request_put("", dummy_data) assert isinstance(response, dict) assert response == dummy_data @responses.activate -def test_request_put_sends_headers_with_the_request(): +async def test_request_put_sends_headers_with_the_request(): responses.add(responses.PUT, BASE_URL, status=204, content_type="application/json") api = TwitchAPI(client_id="client") - api._request_put("", dummy_data) + await api._request_put("", dummy_data) assert "Client-ID" in responses.calls[0].request.headers assert "Accept" in responses.calls[0].request.headers @responses.activate -def test_request_put_does_not_raise_exception_if_successful_and_returns_json(): +async def test_request_put_does_not_raise_exception_if_successful_and_returns_json(): responses.add( responses.PUT, BASE_URL, @@ -141,13 +141,13 @@ def test_request_put_does_not_raise_exception_if_successful_and_returns_json(): ) api = TwitchAPI(client_id="client") - response = api._request_put("", dummy_data) + response = await api._request_put("", dummy_data) assert response == dummy_data @responses.activate @pytest.mark.parametrize("status", [(500), (400)]) -def test_request_put_raises_exception_if_not_200_response(status): +async def test_request_put_raises_exception_if_not_200_response(status): responses.add( responses.PUT, BASE_URL, status=status, content_type="application/json" ) @@ -155,21 +155,21 @@ def test_request_put_raises_exception_if_not_200_response(status): api = TwitchAPI(client_id="client") with pytest.raises(exceptions.HTTPError): - api._request_put("", dummy_data) + await api._request_put("", dummy_data) @responses.activate -def test_request_delete_does_not_raise_exception_if_successful(): +async def test_request_delete_does_not_raise_exception_if_successful(): responses.add( responses.DELETE, BASE_URL, status=204, content_type="application/json" ) api = TwitchAPI(client_id="client") - api._request_delete("") + await api._request_delete("") @responses.activate -def test_request_delete_does_not_raise_exception_if_successful_and_returns_json(): +async def test_request_delete_does_not_raise_exception_if_successful_and_returns_json(): responses.add( responses.DELETE, BASE_URL, @@ -179,18 +179,18 @@ def test_request_delete_does_not_raise_exception_if_successful_and_returns_json( ) api = TwitchAPI(client_id="client") - response = api._request_delete("") + response = await api._request_delete("") assert response == dummy_data @responses.activate -def test_request_delete_sends_headers_with_the_request(): +async def test_request_delete_sends_headers_with_the_request(): responses.add( responses.DELETE, BASE_URL, status=204, content_type="application/json" ) api = TwitchAPI(client_id="client") - api._request_delete("") + await api._request_delete("") assert "Client-ID" in responses.calls[0].request.headers assert "Accept" in responses.calls[0].request.headers @@ -198,7 +198,7 @@ def test_request_delete_sends_headers_with_the_request(): @responses.activate @pytest.mark.parametrize("status", [(500), (400)]) -def test_request_delete_raises_exception_if_not_200_response(status): +async def test_request_delete_raises_exception_if_not_200_response(status): responses.add( responses.DELETE, BASE_URL, status=status, content_type="application/json" ) @@ -206,11 +206,11 @@ def test_request_delete_raises_exception_if_not_200_response(status): api = TwitchAPI(client_id="client") with pytest.raises(exceptions.HTTPError): - api._request_delete("") + await api._request_delete("") @responses.activate -def test_request_post_returns_dictionary_if_successful(): +async def test_request_post_returns_dictionary_if_successful(): responses.add( responses.POST, BASE_URL, @@ -220,22 +220,22 @@ def test_request_post_returns_dictionary_if_successful(): ) api = TwitchAPI(client_id="client") - response = api._request_post("", dummy_data) + response = await api._request_post("", dummy_data) assert isinstance(response, dict) assert response == dummy_data @responses.activate -def test_request_post_does_not_raise_exception_if_successful(): +async def test_request_post_does_not_raise_exception_if_successful(): responses.add(responses.POST, BASE_URL, status=204, content_type="application/json") api = TwitchAPI(client_id="client") - api._request_post("") + await api._request_post("") @responses.activate -def test_request_post_sends_headers_with_the_request(): +async def test_request_post_sends_headers_with_the_request(): responses.add( responses.POST, BASE_URL, @@ -245,7 +245,7 @@ def test_request_post_sends_headers_with_the_request(): ) api = TwitchAPI(client_id="client") - api._request_post("", dummy_data) + await api._request_post("", dummy_data) assert "Client-ID" in responses.calls[0].request.headers assert "Accept" in responses.calls[0].request.headers @@ -253,7 +253,7 @@ def test_request_post_sends_headers_with_the_request(): @responses.activate @pytest.mark.parametrize("status", [(500), (400)]) -def test_request_post_raises_exception_if_not_200_response(status): +async def test_request_post_raises_exception_if_not_200_response(status): responses.add( responses.POST, BASE_URL, status=status, content_type="application/json" ) @@ -261,7 +261,7 @@ def test_request_post_raises_exception_if_not_200_response(status): api = TwitchAPI(client_id="client") with pytest.raises(exceptions.HTTPError): - api._request_post("", dummy_data) + await api._request_post("", dummy_data) def test_base_reads_backoff_config_from_file(monkeypatch): diff --git a/twitch/__init__.py b/twitch/__init__.py index 80cbb6a..a595339 100644 --- a/twitch/__init__.py +++ b/twitch/__init__.py @@ -1,4 +1,4 @@ from .client import TwitchClient # noqa from .helix.api import TwitchHelix # noqa -__version__ = "0.7.1" +__version__ = "2.0-async" diff --git a/twitch/api/base.py b/twitch/api/base.py index 5b094e4..a4de20b 100644 --- a/twitch/api/base.py +++ b/twitch/api/base.py @@ -1,7 +1,8 @@ import time -import requests -from requests.compat import urljoin +# import requests +# from requests.compat import urljoin +import aiohttp from twitch.conf import backoff_config from twitch.constants import BASE_URL @@ -31,65 +32,56 @@ def _get_request_headers(self): return headers - def _request_get(self, path, params=None, json=True, url=BASE_URL): + async def _request_get(self, path, params=None, json=True, url=BASE_URL): """Perform a HTTP GET request.""" - url = urljoin(url, path) + url = f"{url}{path}" headers = self._get_request_headers() - response = requests.get(url, params=params, headers=headers) - if response.status_code >= 500: - - backoff = self._initial_backoff - for _ in range(self._max_retries): - time.sleep(backoff) - backoff_response = requests.get( - url, params=params, headers=headers, timeout=DEFAULT_TIMEOUT - ) - if backoff_response.status_code < 500: - response = backoff_response - break - backoff *= 2 - - response.raise_for_status() - if json: - return response.json() - else: - return response - - def _request_post(self, path, data=None, params=None, url=BASE_URL): + async with aiohttp.ClientSession(raise_for_status=True) as session: + async with session.request("GET", url, params=params, headers=headers) as response: + if response.status >= 500: + + backoff = self._initial_backoff + for _ in range(self._max_retries): + time.sleep(backoff) + async with session.request("GET", url, params=params, headers=headers, timeout=DEFAULT_TIMEOUT) as backoff_response: + if backoff_response.status < 500: + response = backoff_response + break + backoff *= 2 + + if json: + return await response.json() + else: + return response + + async def _request_post(self, path, data=None, params=None, url=BASE_URL): """Perform a HTTP POST request..""" - url = urljoin(url, path) + url = f"{url}{path}" headers = self._get_request_headers() - response = requests.post( - url, json=data, params=params, headers=headers, timeout=DEFAULT_TIMEOUT - ) - response.raise_for_status() - if response.status_code == 200: - return response.json() + async with aiohttp.ClientSession() as session: + async with session.request("POST", url, data=data, params=params, headers=headers, raise_for_status=True) as response: + if response.status == 200: + return await response.json() - def _request_put(self, path, data=None, params=None, url=BASE_URL): + async def _request_put(self, path, data=None, params=None, url=BASE_URL): """Perform a HTTP PUT request.""" - url = urljoin(url, path) + url = f"{url}{path}" headers = self._get_request_headers() - response = requests.put( - url, json=data, params=params, headers=headers, timeout=DEFAULT_TIMEOUT - ) - response.raise_for_status() - if response.status_code == 200: - return response.json() - - def _request_delete(self, path, params=None, url=BASE_URL): + async with aiohttp.ClientSession() as session: + async with session.request("PUT", url, data=data, params=params, headers=headers, raise_for_status=True) as response: + if response.status == 200: + return await response.json() + + async def _request_delete(self, path, params=None, url=BASE_URL): """Perform a HTTP DELETE request.""" - url = urljoin(url, path) + url = f"{url}{path}" headers = self._get_request_headers() - - response = requests.delete( - url, params=params, headers=headers, timeout=DEFAULT_TIMEOUT - ) - response.raise_for_status() - if response.status_code == 200: - return response.json() + async with aiohttp.ClientSession() as session: + async with session.request("DELETE", url, params=params, headers=headers, raise_for_status=True) as response: + if response.status == 200: + return await response.json() diff --git a/twitch/api/channel_feed.py b/twitch/api/channel_feed.py index a98051f..9e994c4 100644 --- a/twitch/api/channel_feed.py +++ b/twitch/api/channel_feed.py @@ -5,7 +5,7 @@ class ChannelFeed(TwitchAPI): - def get_posts(self, channel_id, limit=10, cursor=None, comments=5): + async def get_posts(self, channel_id, limit=10, cursor=None, comments=5): if limit > 100: raise TwitchAttributeException( "Maximum number of objects returned in one request is 100" @@ -16,50 +16,50 @@ def get_posts(self, channel_id, limit=10, cursor=None, comments=5): ) params = {"limit": limit, "cursor": cursor, "comments": comments} - response = self._request_get("feed/{}/posts".format(channel_id), params=params) + response = await self._request_get("feed/{}/posts".format(channel_id), params=params) return [Post.construct_from(x) for x in response["posts"]] - def get_post(self, channel_id, post_id, comments=5): + async def get_post(self, channel_id, post_id, comments=5): if comments > 5: raise TwitchAttributeException( "Maximum number of comments returned in one request is 5" ) params = {"comments": comments} - response = self._request_get( + response = await self._request_get( "feed/{}/posts/{}".format(channel_id, post_id), params=params ) return Post.construct_from(response) @oauth_required - def create_post(self, channel_id, content, share=None): + async def create_post(self, channel_id, content, share=None): data = {"content": content} params = {"share": share} - response = self._request_post( + response = await self._request_post( "feed/{}/posts".format(channel_id), data, params=params ) return Post.construct_from(response["post"]) @oauth_required - def delete_post(self, channel_id, post_id): - response = self._request_delete("feed/{}/posts/{}".format(channel_id, post_id)) + async def delete_post(self, channel_id, post_id): + response = await self._request_delete("feed/{}/posts/{}".format(channel_id, post_id)) return Post.construct_from(response) @oauth_required - def create_reaction_to_post(self, channel_id, post_id, emote_id): + async def create_reaction_to_post(self, channel_id, post_id, emote_id): params = {"emote_id": emote_id} url = "feed/{}/posts/{}/reactions".format(channel_id, post_id) - response = self._request_post(url, params=params) + response = await self._request_post(url, params=params) return response @oauth_required - def delete_reaction_to_post(self, channel_id, post_id, emote_id): + async def delete_reaction_to_post(self, channel_id, post_id, emote_id): params = {"emote_id": emote_id} url = "feed/{}/posts/{}/reactions".format(channel_id, post_id) - response = self._request_delete(url, params=params) + response = await self._request_delete(url, params=params) return response - def get_post_comments(self, channel_id, post_id, limit=10, cursor=None): + async def get_post_comments(self, channel_id, post_id, limit=10, cursor=None): if limit > 100: raise TwitchAttributeException( "Maximum number of objects returned in one request is 100" @@ -70,14 +70,14 @@ def get_post_comments(self, channel_id, post_id, limit=10, cursor=None): "cursor": cursor, } url = "feed/{}/posts/{}/comments".format(channel_id, post_id) - response = self._request_get(url, params=params) + response = await self._request_get(url, params=params) return [Comment.construct_from(x) for x in response["comments"]] @oauth_required - def create_post_comment(self, channel_id, post_id, content): + async def create_post_comment(self, channel_id, post_id, content): data = {"content": content} url = "feed/{}/posts/{}/comments".format(channel_id, post_id) - response = self._request_post(url, data) + response = await self._request_post(url, data) return Comment.construct_from(response) @oauth_required @@ -87,19 +87,19 @@ def delete_post_comment(self, channel_id, post_id, comment_id): return Comment.construct_from(response) @oauth_required - def create_reaction_to_comment(self, channel_id, post_id, comment_id, emote_id): + async def create_reaction_to_comment(self, channel_id, post_id, comment_id, emote_id): params = {"emote_id": emote_id} url = "feed/{}/posts/{}/comments/{}/reactions".format( channel_id, post_id, comment_id ) - response = self._request_post(url, params=params) + response = await self._request_post(url, params=params) return response @oauth_required - def delete_reaction_to_comment(self, channel_id, post_id, comment_id, emote_id): + async def delete_reaction_to_comment(self, channel_id, post_id, comment_id, emote_id): params = {"emote_id": emote_id} url = "feed/{}/posts/{}/comments/{}/reactions".format( channel_id, post_id, comment_id ) - response = self._request_delete(url, params=params) + response = await self._request_delete(url, params=params) return response diff --git a/twitch/api/channels.py b/twitch/api/channels.py index 6c78d1c..8be82aa 100644 --- a/twitch/api/channels.py +++ b/twitch/api/channels.py @@ -15,16 +15,16 @@ class Channels(TwitchAPI): @oauth_required - def get(self): - response = self._request_get("channel") + async def get(self): + response = await self._request_get("channel") return Channel.construct_from(response) - def get_by_id(self, channel_id): - response = self._request_get("channels/{}".format(channel_id)) + async def get_by_id(self, channel_id): + response = await self._request_get("channels/{}".format(channel_id)) return Channel.construct_from(response) @oauth_required - def update( + async def update( self, channel_id, status=None, game=None, delay=None, channel_feed_enabled=None ): data = {} @@ -38,15 +38,15 @@ def update( data["channel_feed_enabled"] = channel_feed_enabled post_data = {"channel": data} - response = self._request_put("channels/{}".format(channel_id), post_data) + response = await self._request_put("channels/{}".format(channel_id), post_data) return Channel.construct_from(response) @oauth_required - def get_editors(self, channel_id): - response = self._request_get("channels/{}/editors".format(channel_id)) + async def get_editors(self, channel_id): + response = await self._request_get("channels/{}/editors".format(channel_id)) return [User.construct_from(x) for x in response["users"]] - def get_followers( + async def get_followers( self, channel_id, limit=25, offset=0, cursor=None, direction=DIRECTION_DESC ): if limit > 100: @@ -61,17 +61,17 @@ def get_followers( params = {"limit": limit, "offset": offset, "direction": direction} if cursor is not None: params["cursor"] = cursor - response = self._request_get( + response = await self._request_get( "channels/{}/follows".format(channel_id), params=params ) return [Follow.construct_from(x) for x in response["follows"]] - def get_teams(self, channel_id): - response = self._request_get("channels/{}/teams".format(channel_id)) + async def get_teams(self, channel_id): + response = await self._request_get("channels/{}/teams".format(channel_id)) return [Team.construct_from(x) for x in response["teams"]] @oauth_required - def get_subscribers(self, channel_id, limit=25, offset=0, direction=DIRECTION_ASC): + async def get_subscribers(self, channel_id, limit=25, offset=0, direction=DIRECTION_ASC): if limit > 100: raise TwitchAttributeException( "Maximum number of objects returned in one request is 100" @@ -82,18 +82,18 @@ def get_subscribers(self, channel_id, limit=25, offset=0, direction=DIRECTION_AS ) params = {"limit": limit, "offset": offset, "direction": direction} - response = self._request_get( + response = await self._request_get( "channels/{}/subscriptions".format(channel_id), params=params ) return [Subscription.construct_from(x) for x in response["subscriptions"]] - def check_subscription_by_user(self, channel_id, user_id): - response = self._request_get( + async def check_subscription_by_user(self, channel_id, user_id): + response = await self._request_get( "channels/{}/subscriptions/{}".format(channel_id, user_id) ) return Subscription.construct_from(response) - def get_videos( + async def get_videos( self, channel_id, limit=10, @@ -127,30 +127,30 @@ def get_videos( } if language is not None: params["language"] = language - response = self._request_get( + response = await self._request_get( "channels/{}/videos".format(channel_id), params=params ) return [Video.construct_from(x) for x in response["videos"]] @oauth_required - def start_commercial(self, channel_id, duration=30): + async def start_commercial(self, channel_id, duration=30): data = {"duration": duration} - response = self._request_post( + response = await self._request_post( "channels/{}/commercial".format(channel_id), data=data ) return response @oauth_required - def reset_stream_key(self, channel_id): - response = self._request_delete("channels/{}/stream_key".format(channel_id)) + async def reset_stream_key(self, channel_id): + response = await self._request_delete("channels/{}/stream_key".format(channel_id)) return Channel.construct_from(response) - def get_community(self, channel_id): - response = self._request_get("channels/{}/community".format(channel_id)) + async def get_community(self, channel_id): + response = await self._request_get("channels/{}/community".format(channel_id)) return Community.construct_from(response) - def set_community(self, channel_id, community_id): - self._request_put("channels/{}/community/{}".format(channel_id, community_id)) + async def set_community(self, channel_id, community_id): + await self._request_put("channels/{}/community/{}".format(channel_id, community_id)) - def delete_from_community(self, channel_id): - self._request_delete("channels/{}/community".format(channel_id)) + async def delete_from_community(self, channel_id): + await self._request_delete("channels/{}/community".format(channel_id)) diff --git a/twitch/api/chat.py b/twitch/api/chat.py index 6b16a76..8818fb1 100644 --- a/twitch/api/chat.py +++ b/twitch/api/chat.py @@ -2,17 +2,17 @@ class Chat(TwitchAPI): - def get_badges_by_channel(self, channel_id): - response = self._request_get("chat/{}/badges".format(channel_id)) + async def get_badges_by_channel(self, channel_id): + response = await self._request_get("chat/{}/badges".format(channel_id)) return response - def get_emoticons_by_set(self, emotesets=None): + async def get_emoticons_by_set(self, emotesets=None): params = { "emotesets": emotesets, } - response = self._request_get("chat/emoticon_images", params=params) + response = await self._request_get("chat/emoticon_images", params=params) return response - def get_all_emoticons(self): - response = self._request_get("chat/emoticons") + async def get_all_emoticons(self): + response = await self._request_get("chat/emoticons") return response diff --git a/twitch/api/clips.py b/twitch/api/clips.py index 8ef9a9e..660076b 100644 --- a/twitch/api/clips.py +++ b/twitch/api/clips.py @@ -6,11 +6,11 @@ class Clips(TwitchAPI): - def get_by_slug(self, slug): - response = self._request_get("clips/{}".format(slug)) + async def get_by_slug(self, slug): + response = await self._request_get("clips/{}".format(slug)) return Clip.construct_from(response) - def get_top( + async def get_top( self, channel=None, cursor=None, @@ -40,11 +40,11 @@ def get_top( "trending": str(trending).lower(), } - response = self._request_get("clips/top", params=params) + response = await self._request_get("clips/top", params=params) return [Clip.construct_from(x) for x in response["clips"]] @oauth_required - def followed(self, limit=10, cursor=None, trending=False): + async def followed(self, limit=10, cursor=None, trending=False): if limit > 100: raise TwitchAttributeException( "Maximum number of objects returned in one request is 100" @@ -52,5 +52,5 @@ def followed(self, limit=10, cursor=None, trending=False): params = {"limit": limit, "cursor": cursor, "trending": trending} - response = self._request_get("clips/followed", params=params) + response = await self._request_get("clips/followed", params=params) return [Clip.construct_from(x) for x in response["clips"]] diff --git a/twitch/api/collections.py b/twitch/api/collections.py index 756cd05..35b8f81 100644 --- a/twitch/api/collections.py +++ b/twitch/api/collections.py @@ -5,18 +5,18 @@ class Collections(TwitchAPI): - def get_metadata(self, collection_id): - response = self._request_get("collections/{}".format(collection_id)) + async def get_metadata(self, collection_id): + response = await self._request_get("collections/{}".format(collection_id)) return Collection.construct_from(response) - def get(self, collection_id, include_all_items=False): + async def get(self, collection_id, include_all_items=False): params = {"include_all_items": include_all_items} - response = self._request_get( + response = await self._request_get( "collections/{}/items".format(collection_id), params=params ) return [Item.construct_from(x) for x in response["items"]] - def get_by_channel(self, channel_id, limit=10, cursor=None, containing_item=None): + async def get_by_channel(self, channel_id, limit=10, cursor=None, containing_item=None): if limit > 100: raise TwitchAttributeException( "Maximum number of objects returned in one request is 100" @@ -27,52 +27,52 @@ def get_by_channel(self, channel_id, limit=10, cursor=None, containing_item=None } if containing_item: params["containing_item"] = containing_item - response = self._request_get("channels/{}/collections".format(channel_id)) + response = await self._request_get("channels/{}/collections".format(channel_id)) return [Collection.construct_from(x) for x in response["collections"]] @oauth_required - def create(self, channel_id, title): + async def create(self, channel_id, title): data = { "title": title, } - response = self._request_post( + response = await self._request_post( "channels/{}/collections".format(channel_id), data=data ) return Collection.construct_from(response) @oauth_required - def update(self, collection_id, title): + async def update(self, collection_id, title): data = { "title": title, } - self._request_put("collections/{}".format(collection_id), data=data) + await self._request_put("collections/{}".format(collection_id), data=data) @oauth_required - def create_thumbnail(self, collection_id, item_id): + async def create_thumbnail(self, collection_id, item_id): data = { "item_id": item_id, } - self._request_put("collections/{}/thumbnail".format(collection_id), data=data) + await self._request_put("collections/{}/thumbnail".format(collection_id), data=data) @oauth_required - def delete(self, collection_id): - self._request_delete("collections/{}".format(collection_id)) + async def delete(self, collection_id): + await self._request_delete("collections/{}".format(collection_id)) @oauth_required - def add_item(self, collection_id, item_id, item_type): + async def add_item(self, collection_id, item_id, item_type): data = {"id": item_id, "type": item_type} - response = self._request_put( + response = await self._request_put( "collections/{}/items".format(collection_id), data=data ) return Item.construct_from(response) @oauth_required - def delete_item(self, collection_id, collection_item_id): + async def delete_item(self, collection_id, collection_item_id): url = "collections/{}/items/{}".format(collection_id, collection_item_id) - self._request_delete(url) + await self._request_delete(url) @oauth_required - def move_item(self, collection_id, collection_item_id, position): + async def move_item(self, collection_id, collection_item_id, position): data = {"position": position} url = "collections/{}/items/{}".format(collection_id, collection_item_id) - self._request_put(url, data=data) + await self._request_put(url, data=data) diff --git a/twitch/api/communities.py b/twitch/api/communities.py index a93a4aa..9f2fb81 100644 --- a/twitch/api/communities.py +++ b/twitch/api/communities.py @@ -5,16 +5,16 @@ class Communities(TwitchAPI): - def get_by_name(self, community_name): + async def get_by_name(self, community_name): params = {"name": community_name} - response = self._request_get("communities", params=params) + response = await self._request_get("communities", params=params) return Community.construct_from(response) - def get_by_id(self, community_id): - response = self._request_get("communities/{}".format(community_id)) + async def get_by_id(self, community_id): + response = await self._request_get("communities/{}".format(community_id)) return Community.construct_from(response) - def update( + async def update( self, community_id, summary=None, description=None, rules=None, email=None ): data = { @@ -23,114 +23,114 @@ def update( "rules": rules, "email": email, } - self._request_put("communities/{}".format(community_id), data=data) + await self._request_put("communities/{}".format(community_id), data=data) - def get_top(self, limit=10, cursor=None): + async def get_top(self, limit=10, cursor=None): if limit > 100: raise TwitchAttributeException( "Maximum number of objects returned in one request is 100" ) params = {"limit": limit, "cursor": cursor} - response = self._request_get("communities/top", params=params) + response = await self._request_get("communities/top", params=params) return [Community.construct_from(x) for x in response["communities"]] @oauth_required - def get_banned_users(self, community_id, limit=10, cursor=None): + async def get_banned_users(self, community_id, limit=10, cursor=None): if limit > 100: raise TwitchAttributeException( "Maximum number of objects returned in one request is 100" ) params = {"limit": limit, "cursor": cursor} - response = self._request_get( + response = await self._request_get( "communities/{}/bans".format(community_id), params=params ) return [User.construct_from(x) for x in response["banned_users"]] @oauth_required - def ban_user(self, community_id, user_id): - self._request_put("communities/{}/bans/{}".format(community_id, user_id)) + async def ban_user(self, community_id, user_id): + await self._request_put("communities/{}/bans/{}".format(community_id, user_id)) @oauth_required - def unban_user(self, community_id, user_id): - self._request_delete("communities/{}/bans/{}".format(community_id, user_id)) + async def unban_user(self, community_id, user_id): + await self._request_delete("communities/{}/bans/{}".format(community_id, user_id)) @oauth_required - def create_avatar_image(self, community_id, avatar_image): + async def create_avatar_image(self, community_id, avatar_image): data = { "avatar_image": avatar_image, } - self._request_post( + await self._request_post( "communities/{}/images/avatar".format(community_id), data=data ) @oauth_required - def delete_avatar_image(self, community_id): - self._request_delete("communities/{}/images/avatar".format(community_id)) + async def delete_avatar_image(self, community_id): + await self._request_delete("communities/{}/images/avatar".format(community_id)) @oauth_required - def create_cover_image(self, community_id, cover_image): + async def create_cover_image(self, community_id, cover_image): data = { "cover_image": cover_image, } - self._request_post( + await self._request_post( "communities/{}/images/cover".format(community_id), data=data ) @oauth_required - def delete_cover_image(self, community_id): - self._request_delete("communities/{}/images/cover".format(community_id)) + async def delete_cover_image(self, community_id): + await self._request_delete("communities/{}/images/cover".format(community_id)) - def get_moderators(self, community_id): - response = self._request_get("communities/{}/moderators".format(community_id)) + async def get_moderators(self, community_id): + response = await self._request_get("communities/{}/moderators".format(community_id)) return [User.construct_from(x) for x in response["moderators"]] @oauth_required - def add_moderator(self, community_id, user_id): - self._request_put("communities/{}/moderators/{}".format(community_id, user_id)) + async def add_moderator(self, community_id, user_id): + await self._request_put("communities/{}/moderators/{}".format(community_id, user_id)) @oauth_required - def delete_moderator(self, community_id, user_id): - self._request_delete( + async def delete_moderator(self, community_id, user_id): + await self._request_delete( "communities/{}/moderators/{}".format(community_id, user_id) ) @oauth_required - def get_permissions(self, community_id): - response = self._request_get("communities/{}/permissions".format(community_id)) + async def get_permissions(self, community_id): + response = await self._request_get("communities/{}/permissions".format(community_id)) return response @oauth_required - def report_violation(self, community_id, channel_id): + async def report_violation(self, community_id, channel_id): data = { "channel_id": channel_id, } - self._request_post( + await self._request_post( "communities/{}/report_channel".format(community_id), data=data ) @oauth_required - def get_timed_out_users(self, community_id, limit=10, cursor=None): + async def get_timed_out_users(self, community_id, limit=10, cursor=None): if limit > 100: raise TwitchAttributeException( "Maximum number of objects returned in one request is 100" ) params = {"limit": limit, "cursor": cursor} - response = self._request_get( + response = await self._request_get( "communities/{}/timeouts".format(community_id), params=params ) return [User.construct_from(x) for x in response["timed_out_users"]] @oauth_required - def add_timed_out_user(self, community_id, user_id, duration, reason=None): + async def add_timed_out_user(self, community_id, user_id, duration, reason=None): data = { "duration": duration, "reason": reason, } - self._request_put( + await self._request_put( "communities/{}/timeouts/{}".format(community_id, user_id), data=data ) @oauth_required - def delete_timed_out_user(self, community_id, user_id): - self._request_delete("communities/{}/timeouts/{}".format(community_id, user_id)) + async def delete_timed_out_user(self, community_id, user_id): + await self._request_delete("communities/{}/timeouts/{}".format(community_id, user_id)) diff --git a/twitch/api/games.py b/twitch/api/games.py index 73fcd21..9d7b010 100644 --- a/twitch/api/games.py +++ b/twitch/api/games.py @@ -4,12 +4,12 @@ class Games(TwitchAPI): - def get_top(self, limit=10, offset=0): + async def get_top(self, limit=10, offset=0): if limit > 100: raise TwitchAttributeException( "Maximum number of objects returned in one request is 100" ) params = {"limit": limit, "offset": offset} - response = self._request_get("games/top", params=params) + response = await self._request_get("games/top", params=params) return [TopGame.construct_from(x) for x in response["top"]] diff --git a/twitch/api/ingests.py b/twitch/api/ingests.py index add6841..9e57df4 100644 --- a/twitch/api/ingests.py +++ b/twitch/api/ingests.py @@ -3,6 +3,6 @@ class Ingests(TwitchAPI): - def get_server_list(self): - response = self._request_get("ingests") + async def get_server_list(self): + response = await self._request_get("ingests") return [Ingest.construct_from(x) for x in response["ingests"]] diff --git a/twitch/api/search.py b/twitch/api/search.py index d2cb3cc..ca236ac 100644 --- a/twitch/api/search.py +++ b/twitch/api/search.py @@ -4,30 +4,30 @@ class Search(TwitchAPI): - def channels(self, query, limit=25, offset=0): + async def channels(self, query, limit=25, offset=0): if limit > 100: raise TwitchAttributeException( "Maximum number of objects returned in one request is 100" ) params = {"query": query, "limit": limit, "offset": offset} - response = self._request_get("search/channels", params=params) + response = await self._request_get("search/channels", params=params) return [Channel.construct_from(x) for x in response["channels"] or []] - def games(self, query, live=False): + async def games(self, query, live=False): params = { "query": query, "live": live, } - response = self._request_get("search/games", params=params) + response = await self._request_get("search/games", params=params) return [Game.construct_from(x) for x in response["games"] or []] - def streams(self, query, limit=25, offset=0, hls=None): + async def streams(self, query, limit=25, offset=0, hls=None): if limit > 100: raise TwitchAttributeException( "Maximum number of objects returned in one request is 100" ) params = {"query": query, "limit": limit, "offset": offset, "hls": hls} - response = self._request_get("search/streams", params=params) + response = await self._request_get("search/streams", params=params) return [Stream.construct_from(x) for x in response["streams"] or []] diff --git a/twitch/api/streams.py b/twitch/api/streams.py index ebcdb15..05256d6 100644 --- a/twitch/api/streams.py +++ b/twitch/api/streams.py @@ -6,7 +6,7 @@ class Streams(TwitchAPI): - def get_stream_by_user(self, channel_id, stream_type=STREAM_TYPE_LIVE): + async def get_stream_by_user(self, channel_id, stream_type=STREAM_TYPE_LIVE): if stream_type not in STREAM_TYPES: raise TwitchAttributeException( "Stream type is not valid. Valid values are {}".format(STREAM_TYPES) @@ -15,13 +15,13 @@ def get_stream_by_user(self, channel_id, stream_type=STREAM_TYPE_LIVE): params = { "stream_type": stream_type, } - response = self._request_get("streams/{}".format(channel_id), params=params) + response = await self._request_get("streams/{}".format(channel_id), params=params) if not response["stream"]: return None return Stream.construct_from(response["stream"]) - def get_live_streams( + async def get_live_streams( self, channel=None, game=None, @@ -42,28 +42,28 @@ def get_live_streams( params["game"] = game if language is not None: params["language"] = language - response = self._request_get("streams", params=params) + response = await self._request_get("streams", params=params) return [Stream.construct_from(x) for x in response["streams"]] - def get_summary(self, game=None): + async def get_summary(self, game=None): params = {} if game is not None: params["game"] = game - response = self._request_get("streams/summary", params=params) + response = await self._request_get("streams/summary", params=params) return response - def get_featured(self, limit=25, offset=0): + async def get_featured(self, limit=25, offset=0): if limit > 100: raise TwitchAttributeException( "Maximum number of objects returned in one request is 100" ) params = {"limit": limit, "offset": offset} - response = self._request_get("streams/featured", params=params) + response = await self._request_get("streams/featured", params=params) return [Featured.construct_from(x) for x in response["featured"]] @oauth_required - def get_followed(self, stream_type=STREAM_TYPE_LIVE, limit=25, offset=0): + async def get_followed(self, stream_type=STREAM_TYPE_LIVE, limit=25, offset=0): if stream_type not in STREAM_TYPES: raise TwitchAttributeException( "Stream type is not valid. Valid values are {}".format(STREAM_TYPES) @@ -74,10 +74,10 @@ def get_followed(self, stream_type=STREAM_TYPE_LIVE, limit=25, offset=0): ) params = {"stream_type": stream_type, "limit": limit, "offset": offset} - response = self._request_get("streams/followed", params=params) + response = await self._request_get("streams/followed", params=params) return [Stream.construct_from(x) for x in response["streams"]] - def get_streams_in_community(self, community_id): - response = self._request_get("streams?community_id={}".format(community_id)) + async def get_streams_in_community(self, community_id): + response = await self._request_get("streams?community_id={}".format(community_id)) return [Stream.construct_from(x) for x in response["streams"]] diff --git a/twitch/api/teams.py b/twitch/api/teams.py index b53ef11..e014204 100644 --- a/twitch/api/teams.py +++ b/twitch/api/teams.py @@ -4,16 +4,16 @@ class Teams(TwitchAPI): - def get(self, team_name): - response = self._request_get("teams/{}".format(team_name)) + async def get(self, team_name): + response = await self._request_get("teams/{}".format(team_name)) return Team.construct_from(response) - def get_all(self, limit=10, offset=0): + async def get_all(self, limit=10, offset=0): if limit > 100: raise TwitchAttributeException( "Maximum number of objects returned in one request is 100" ) params = {"limit": limit, "offset": offset} - response = self._request_get("teams", params=params) + response = await self._request_get("teams", params=params) return [Team.construct_from(x) for x in response["teams"]] diff --git a/twitch/api/users.py b/twitch/api/users.py index f28f9d9..1c8b9bb 100644 --- a/twitch/api/users.py +++ b/twitch/api/users.py @@ -13,27 +13,27 @@ class Users(TwitchAPI): @oauth_required - def get(self): - response = self._request_get("user") + async def get(self): + response = await self._request_get("user") return User.construct_from(response) - def get_by_id(self, user_id): - response = self._request_get("users/{}".format(user_id)) + async def get_by_id(self, user_id): + response = await self._request_get("users/{}".format(user_id)) return User.construct_from(response) @oauth_required - def get_emotes(self, user_id): - response = self._request_get("users/{}/emotes".format(user_id)) + async def get_emotes(self, user_id): + response = await self._request_get("users/{}/emotes".format(user_id)) return response["emoticon_sets"] @oauth_required - def check_subscribed_to_channel(self, user_id, channel_id): - response = self._request_get( + async def check_subscribed_to_channel(self, user_id, channel_id): + response = await self._request_get( "users/{}/subscriptions/{}".format(user_id, channel_id) ) return Subscription.construct_from(response) - def get_all_follows( + async def get_all_follows( self, user_id, direction=DIRECTION_DESC, sort_by=USERS_SORT_BY_CREATED_AT ): if direction not in DIRECTIONS: @@ -49,14 +49,14 @@ def get_all_follows( follows = [] while offset is not None: params.update({"offset": offset}) - response = self._request_get( + response = await self._request_get( "users/{}/follows/channels".format(user_id), params=params ) offset = response.get("_offset") follows.extend(response["follows"]) return [Follow.construct_from(x) for x in follows] - def get_follows( + async def get_follows( self, user_id, limit=25, @@ -77,54 +77,54 @@ def get_follows( "Sort by is not valid. Valid values are {}".format(USERS_SORT_BY) ) params = {"limit": limit, "offset": offset, "direction": direction} - response = self._request_get( + response = await self._request_get( "users/{}/follows/channels".format(user_id), params=params ) return [Follow.construct_from(x) for x in response["follows"]] - def check_follows_channel(self, user_id, channel_id): - response = self._request_get( + async def check_follows_channel(self, user_id, channel_id): + response = await self._request_get( "users/{}/follows/channels/{}".format(user_id, channel_id) ) return Follow.construct_from(response) @oauth_required - def follow_channel(self, user_id, channel_id, notifications=False): + async def follow_channel(self, user_id, channel_id, notifications=False): data = {"notifications": notifications} - response = self._request_put( + response = await self._request_put( "users/{}/follows/channels/{}".format(user_id, channel_id), data ) return Follow.construct_from(response) @oauth_required - def unfollow_channel(self, user_id, channel_id): - self._request_delete("users/{}/follows/channels/{}".format(user_id, channel_id)) + async def unfollow_channel(self, user_id, channel_id): + await self._request_delete("users/{}/follows/channels/{}".format(user_id, channel_id)) @oauth_required - def get_user_block_list(self, user_id, limit=25, offset=0): + async def get_user_block_list(self, user_id, limit=25, offset=0): if limit > 100: raise TwitchAttributeException( "Maximum number of objects returned in one request is 100" ) params = {"limit": limit, "offset": offset} - response = self._request_get("users/{}/blocks".format(user_id), params=params) + response = await self._request_get("users/{}/blocks".format(user_id), params=params) return [UserBlock.construct_from(x) for x in response["blocks"]] @oauth_required - def block_user(self, user_id, blocked_user_id): - response = self._request_put( + async def block_user(self, user_id, blocked_user_id): + response = await self._request_put( "users/{}/blocks/{}".format(user_id, blocked_user_id) ) return UserBlock.construct_from(response) @oauth_required - def unblock_user(self, user_id, blocked_user_id): - self._request_delete("users/{}/blocks/{}".format(user_id, blocked_user_id)) + async def unblock_user(self, user_id, blocked_user_id): + await self._request_delete("users/{}/blocks/{}".format(user_id, blocked_user_id)) - def translate_usernames_to_ids(self, usernames): + async def translate_usernames_to_ids(self, usernames): if isinstance(usernames, list): usernames = ",".join(usernames) - response = self._request_get("users?login={}".format(usernames)) + response = await self._request_get("users?login={}".format(usernames)) return [User.construct_from(x) for x in response["users"]] diff --git a/twitch/api/videos.py b/twitch/api/videos.py index 01df787..0e60f84 100644 --- a/twitch/api/videos.py +++ b/twitch/api/videos.py @@ -12,11 +12,11 @@ class Videos(TwitchAPI): - def get_by_id(self, video_id): - response = self._request_get("videos/{}".format(video_id)) + async def get_by_id(self, video_id): + response = await self._request_get("videos/{}".format(video_id)) return Video.construct_from(response) - def get_top( + async def get_top( self, limit=10, offset=0, @@ -48,11 +48,11 @@ def get_top( "broadcast_type": ",".join(broadcast_type), } - response = self._request_get("videos/top", params=params) + response = await self._request_get("videos/top", params=params) return [Video.construct_from(x) for x in response["vods"]] @oauth_required - def get_followed_videos( + async def get_followed_videos( self, limit=10, offset=0, broadcast_type=BROADCAST_TYPE_HIGHLIGHT ): if limit > 100: @@ -69,20 +69,20 @@ def get_followed_videos( params = {"limit": limit, "offset": offset, "broadcast_type": broadcast_type} - response = self._request_get("videos/followed", params=params) + response = await self._request_get("videos/followed", params=params) return [Video.construct_from(x) for x in response["videos"]] - def download_vod(self, video_id): + async def download_vod(self, video_id): """ This will return a byte string of the M3U8 playlist data (which contains more links to segments of the vod) """ vod_id = video_id[1:] - token = self._request_get( + token = await self._request_get( "vods/{}/access_token".format(vod_id), url="https://api.twitch.tv/api/" ) params = {"nauthsig": token["sig"], "nauth": token["token"]} - m3u8 = self._request_get( + m3u8 = await self._request_get( "vod/{}".format(vod_id), url=VOD_FETCH_URL, params=params, json=False ) return m3u8.content diff --git a/twitch/helix/api.py b/twitch/helix/api.py index 37e436b..78bacc5 100644 --- a/twitch/helix/api.py +++ b/twitch/helix/api.py @@ -1,4 +1,4 @@ -from requests import post +from aiohttp import ClientSession from twitch.conf import credentials_from_config_file from twitch.constants import ( @@ -40,27 +40,22 @@ def __init__( if not client_id: self._client_id, self._oauth_token = credentials_from_config_file() - def get_oauth(self): + async def get_oauth(self): if not self._client_secret or not self._client_id: raise TwitchOAuthException( "Client Id and Client Secret are not both present." ) if not self._scopes: - response = post( - BASE_OAUTH_URL + f"token?client_id={self._client_id}" - f"&client_secret={self._client_secret}" - f"&grant_type=client_credentials" - ) - response = response.json() + async with ClientSession(raise_for_status=True) as session: + async with session.post(BASE_OAUTH_URL + f"token?client_id={self._client_id}&client_secret={self._client_secret}&grant_type=client_credentials") as response: + response = await response.json() + else: scopes_str = "+".join(self._scopes) - response = post( - BASE_OAUTH_URL + f"token?client_id={self._client_id}" - f"&client_secret={self._client_secret}" - f"&grant_type=client_credentials&scope={scopes_str}" - ) - response = response.json() + async with ClientSession(raise_for_status=True) as session: + async with session.post(BASE_OAUTH_URL + f"token?client_id={self._client_id}&client_secret={self._client_secret}&grant_type=client_credentials&scope={scopes_str}") as response: + response = await response.json() if "access_token" in response: self._oauth_token = response["access_token"] @@ -69,7 +64,7 @@ def get_oauth(self): else: raise TwitchOAuthException() - def get_streams( + async def get_streams( self, after=None, before=None, @@ -108,8 +103,7 @@ def get_streams( "user_id": user_ids, "user_login": user_logins, } - - return APICursor( + cursor = APICursor( client_id=self._client_id, oauth_token=self._oauth_token, path="streams", @@ -117,7 +111,9 @@ def get_streams( params=params, ) - def get_games(self, game_ids=None, names=None): + return await cursor.next_page() + + async def get_games(self, game_ids=None, names=None): if game_ids and len(game_ids) > 100: raise TwitchAttributeException("Maximum of 100 Game IDs can be supplied") if names and len(names) > 100: @@ -127,15 +123,17 @@ def get_games(self, game_ids=None, names=None): "id": game_ids, "name": names, } - return APIGet( + api_get = APIGet( client_id=self._client_id, oauth_token=self._oauth_token, path="games", resource=Game, params=params, - ).fetch() + ) + + return await api_get.fetch() - def get_clips( + async def get_clips( self, broadcaster_id=None, game_id=None, @@ -169,7 +167,7 @@ def get_clips( if broadcaster_id or game_id: params["first"] = page_size - return APICursor( + cursor = APICursor( client_id=self._client_id, oauth_token=self._oauth_token, path="clips", @@ -177,16 +175,20 @@ def get_clips( params=params, ) + return await cursor.next_page() + else: - return APIGet( + api_get = APIGet( client_id=self._client_id, oauth_token=self._oauth_token, path="clips", resource=Clip, params=params, - ).fetch() + ) + + return await api_get.fetch() - def get_top_games(self, after=None, before=None, page_size=20): + async def get_top_games(self, after=None, before=None, page_size=20): if page_size > 100: raise TwitchAttributeException("Maximum number of objects to return is 100") @@ -196,7 +198,7 @@ def get_top_games(self, after=None, before=None, page_size=20): "first": page_size, } - return APICursor( + cursor = APICursor( client_id=self._client_id, oauth_token=self._oauth_token, path="games/top", @@ -204,7 +206,9 @@ def get_top_games(self, after=None, before=None, page_size=20): params=params, ) - def get_videos( + return await cursor.next_page() + + async def get_videos( self, video_ids=None, user_id=None, @@ -254,23 +258,27 @@ def get_videos( params["sort"] = sort params["type"] = video_type - return APICursor( + cusor = APICursor( client_id=self._client_id, oauth_token=self._oauth_token, path="videos", resource=Video, params=params, ) + + return await cusor.next_page() else: - return APIGet( + api_get = APIGet( client_id=self._client_id, oauth_token=self._oauth_token, path="videos", resource=Video, params=params, - ).fetch() + ) - def get_streams_metadata( + return await api_get.fetch() + + async def get_streams_metadata( self, after=None, before=None, @@ -310,7 +318,7 @@ def get_streams_metadata( "user_login": user_logins, } - return APICursor( + cursor = APICursor( client_id=self._client_id, oauth_token=self._oauth_token, path="streams/metadata", @@ -318,7 +326,9 @@ def get_streams_metadata( params=params, ) - def get_user_follows(self, after=None, page_size=20, from_id=None, to_id=None): + return await cursor.next_page() + + async def get_user_follows(self, after=None, page_size=20, from_id=None, to_id=None): if not from_id and not to_id: raise TwitchAttributeException("from_id or to_id must be provided.") if page_size > 100: @@ -330,8 +340,7 @@ def get_user_follows(self, after=None, page_size=20, from_id=None, to_id=None): "from_id": from_id, "to_id": to_id, } - - return APICursor( + cursor = APICursor( client_id=self._client_id, oauth_token=self._oauth_token, path="users/follows", @@ -339,7 +348,9 @@ def get_user_follows(self, after=None, page_size=20, from_id=None, to_id=None): params=params, ) - def get_users(self, login_names=None, ids=None): + return await cursor.next_page() + + async def get_users(self, login_names=None, ids=None): """https://dev.twitch.tv/docs/api/reference#get-users""" if not login_names: login_names = [] @@ -349,15 +360,16 @@ def get_users(self, login_names=None, ids=None): raise TwitchAttributeException("Sum of names and ids must not exceed 100!") params = {"login": login_names, "id": ids} - return APIGet( + api_get = APIGet( client_id=self._client_id, oauth_token=self._oauth_token, path="users", resource=User, params=params, - ).fetch() + ) + return await api_get.fetch() - def get_tags(self, after=None, page_size=20, tag_ids=None): + async def get_tags(self, after=None, page_size=20, tag_ids=None): """https://dev.twitch.tv/docs/api/reference#get-all-stream-tags""" if tag_ids and len(tag_ids) > 100: @@ -367,10 +379,12 @@ def get_tags(self, after=None, page_size=20, tag_ids=None): params = {"after": after, "first": page_size, "tag_id": tag_ids} - return APICursor( + cursor = APICursor( client_id=self._client_id, oauth_token=self._oauth_token, path="tags/streams", resource=Tag, params=params, ) + + return await cursor.next_page() diff --git a/twitch/helix/base.py b/twitch/helix/base.py index e83edfa..0b8aa02 100644 --- a/twitch/helix/base.py +++ b/twitch/helix/base.py @@ -1,7 +1,9 @@ import logging import time -import requests +import asyncio + +from aiohttp import ClientSession from requests import codes from requests.compat import urljoin @@ -15,7 +17,7 @@ class TwitchAPIMixin(object): _rate_limit_resets = set() _rate_limit_remaining = 0 - def _wait_for_rate_limit_reset(self): + async def _wait_for_rate_limit_reset(self): if self._rate_limit_remaining == 0: current_time = int(time.time()) self._rate_limit_resets = set( @@ -35,7 +37,7 @@ def _wait_for_rate_limit_reset(self): # Calculate wait time and add 0.1s to the wait time to allow Twitch to reset # their counter wait_time = reset_time - current_time + 0.1 - time.sleep(wait_time) + await asyncio.sleep(wait_time) def _get_request_headers(self): headers = {"Client-ID": self._client_id} @@ -45,16 +47,28 @@ def _get_request_headers(self): return headers - def _request_get(self, path, params=None): + async def _request_get(self, path, params=None): url = urljoin(BASE_HELIX_URL, path) headers = self._get_request_headers() - self._wait_for_rate_limit_reset() + to_del =[] + for key in params: + if params[key] is None: + to_del.append(key) + + for key in to_del: + del params[key] + + await self._wait_for_rate_limit_reset() - response = requests.get(url, params=params, headers=headers) - logger.debug( - "Request to %s with params %s took %s", url, params, response.elapsed - ) + + async with ClientSession(raise_for_status=True) as session: + async with session.get(url, headers=headers, params=params) as response: + response_json = await response.json() + + # logger.debug( + # "Request to %s with params %s took %s", url, params, response.elapsed + # ) remaining = response.headers.get("Ratelimit-Remaining") if remaining: @@ -66,14 +80,13 @@ def _request_get(self, path, params=None): # If status code is 429, re-run _request_get which will wait for the appropriate time # to obey the rate limit - if response.status_code == codes.TOO_MANY_REQUESTS: + if response.status == codes.TOO_MANY_REQUESTS: logger.debug( "Twitch responded with 429. Rate limit reached. Waiting for the cooldown." ) - return self._request_get(path, params=params) + return await self._request_get(path, params=params) - response.raise_for_status() - return response.json() + return await response.json() class APICursor(TwitchAPIMixin): @@ -91,9 +104,6 @@ def __init__( self._total = None self._requests_count = 0 - # Pre-fetch the first page as soon as cursor is instantiated - self.next_page() - def __repr__(self): return str(self._queue) @@ -104,7 +114,7 @@ def __iter__(self): return self def __next__(self): - if not self._queue and not self.next_page(): + if not self._queue and not asyncio.run(self.next_page()): raise StopIteration() return self._queue.pop(0) @@ -115,7 +125,7 @@ def __next__(self): def __getitem__(self, index): return self._queue[index] - def next_page(self): + async def next_page(self): # Twitch stops returning a cursor when you're on the last page. So if we've made # more than 1 request to their API and we don't get a cursor back, it means # we're on the last page, so return whatever's left in the queue. @@ -125,7 +135,7 @@ def next_page(self): if self._cursor: self._params["after"] = self._cursor - response = self._request_get(self._path, params=self._params) + response = await self._request_get(self._path, params=self._params) self._requests_count += 1 self._queue = [self._resource.construct_from(data) for data in response["data"]] self._cursor = response["pagination"].get("cursor") @@ -152,6 +162,6 @@ def __init__(self, client_id, path, resource, oauth_token=None, params=None): self._oauth_token = oauth_token self._params = params - def fetch(self): - response = self._request_get(self._path, params=self._params) + async def fetch(self): + response = await self._request_get(self._path, params=self._params) return [self._resource.construct_from(data) for data in response["data"]]