From 2b53f309c295ea28f8a16361b364a288fa99dc1a Mon Sep 17 00:00:00 2001 From: Miwory Date: Wed, 25 Feb 2026 12:22:37 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D1=81=20=D0=BF?= =?UTF-8?q?=D0=B0=D0=B3=D0=B8=D0=BD=D0=B0=D1=86=D0=B8=D0=B5=D0=B9=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=B2=D0=BE=D0=B7=D0=B2=D1=80=D0=B0=D1=82=20=D0=B3?= =?UTF-8?q?=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=82=D0=BE=D1=80=D0=B0=20=D1=81?= =?UTF-8?q?=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- src/oxidetwitch/api.py | 2239 ++++++++++++++++++++++--------------- src/oxidetwitch/schema.py | 4 +- 3 files changed, 1355 insertions(+), 890 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index acf59a1..a530cc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oxidetwitch" -version = "1.0.0" +version = "1.1.0" description = "Client for Twitch API" readme = "README.md" authors = [{ name = "Miwory", email = "miwory.uwu@gmail.com" }] diff --git a/src/oxidetwitch/api.py b/src/oxidetwitch/api.py index 9552dbf..d8c0966 100644 --- a/src/oxidetwitch/api.py +++ b/src/oxidetwitch/api.py @@ -1,3 +1,4 @@ +from collections.abc import AsyncGenerator from datetime import datetime from typing import Literal from zoneinfo import ZoneInfo @@ -7,6 +8,7 @@ from oxidehttp.client import OxideHTTP from . import schema as s from .eventsub import statuses as sub_status +from .eventsub import subscriptions as sub from .eventsub import types as sub_type @@ -117,34 +119,48 @@ class TwitchAPIClient(OxideHTTP): started_at: datetime | None = None, ended_at: datetime | None = None, first: int = 20, - after: str | None = None, - ) -> s.ExtensionAnalytics: - req = await self.get( - '/analytics/extensions', - headers={'Authorization': f'Bearer {access_token}'}, - params=self.clean_dict( - { - 'extension_id': extension_id, - 'type': analytics_type, - 'started_at': started_at, - 'ended_at': ended_at, - 'first': first, - 'after': after, - } - ), - ) + ) -> AsyncGenerator[s.ExtensionAnalyticsData]: + after = None - match req.status_code: - case st.OK: - return s.ExtensionAnalytics.model_validate(await req.json()) + while True: + req = await self.get( + '/analytics/extensions', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'extension_id': extension_id, + 'type': analytics_type, + 'started_at': started_at, + 'ended_at': ended_at, + 'first': first, + 'after': after, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + analytics = s.ExtensionAnalytics.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for data in analytics.data: + yield data + + if isinstance(analytics.pagination, s.Pagination): + after = analytics.pagination.cursor + + if not after: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def get_game_analytics( self, @@ -155,34 +171,48 @@ class TwitchAPIClient(OxideHTTP): started_at: datetime | None = None, ended_at: datetime | None = None, first: int = 20, - after: str | None = None, - ) -> s.GameAnalytics: - req = await self.get( - '/analytics/games', - headers={'Authorization': f'Bearer {access_token}'}, - params=self.clean_dict( - { - 'game_id': game_id, - 'type': analytics_type, - 'started_at': started_at, - 'ended_at': ended_at, - 'first': first, - 'after': after, - } - ), - ) + ) -> AsyncGenerator[s.GameAnalyticsData]: + after = None - match req.status_code: - case st.OK: - return s.GameAnalytics.model_validate(await req.json()) + while True: + req = await self.get( + '/analytics/games', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'game_id': game_id, + 'type': analytics_type, + 'started_at': started_at, + 'ended_at': ended_at, + 'first': first, + 'after': after, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + analytics = s.GameAnalytics.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for data in analytics.data: + yield data + + if isinstance(analytics.pagination, s.Pagination): + after = analytics.pagination.cursor + + if not after: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def get_bits_leaderboard( self, @@ -246,32 +276,46 @@ class TwitchAPIClient(OxideHTTP): *, user_id: int | list[int] | None = None, first: int = 20, - after: str | None = None, - ) -> s.ExtensionTransactions: - req = await self.get( - '/extensions/transactions', - headers={'Authorization': f'Bearer {access_token}'}, - params=self.clean_dict( - { - 'extension_id': extension_id, - 'user_id': user_id, - 'first': first, - 'after': after, - } - ), - ) + ) -> AsyncGenerator[s.ExtensionTransactionsData]: + after = None - match req.status_code: - case st.OK: - return s.ExtensionTransactions.model_validate(await req.json()) + while True: + req = await self.get( + '/extensions/transactions', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'extension_id': extension_id, + 'user_id': user_id, + 'first': first, + 'after': after, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + transactions = s.ExtensionTransactions.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for data in transactions.data: + yield data + + if isinstance(transactions.pagination, s.Pagination): + after = transactions.pagination.cursor + + if not after: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def get_channel_information( self, @@ -372,31 +416,45 @@ class TwitchAPIClient(OxideHTTP): broadcaster_id: int, *, first: int = 20, - after: str | None = None, - ) -> s.FollowedChannels: - req = await self.get( - '/channels/followed', - headers={'Authorization': f'Bearer {access_token}'}, - params=self.clean_dict( - { - 'broadcaster_id': broadcaster_id, - 'first': first, - 'after': after, - } - ), - ) + ) -> AsyncGenerator[tuple[int, s.FollowedChannel]]: + after = None - match req.status_code: - case st.OK: - return s.FollowedChannels.model_validate(await req.json()) + while True: + req = await self.get( + '/channels/followed', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'first': first, + 'after': after, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + followed_channels = s.FollowedChannels.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for data in followed_channels.data: + yield followed_channels.total, data + + if isinstance(followed_channels.pagination, s.Pagination): + after = followed_channels.pagination.cursor + + if not after: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def get_channel_followers( self, @@ -405,32 +463,46 @@ class TwitchAPIClient(OxideHTTP): *, user_id: int | None = None, first: int = 20, - after: str | None = None, - ) -> s.ChannelFollowers: - req = await self.get( - '/channels/followers', - headers={'Authorization': f'Bearer {access_token}'}, - params=self.clean_dict( - { - 'broadcaster_id': broadcaster_id, - 'user_id': user_id, - 'first': first, - 'after': after, - } - ), - ) + ) -> AsyncGenerator[tuple[int, s.ChannelFollower]]: + after = None - match req.status_code: - case st.OK: - return s.ChannelFollowers.model_validate(await req.json()) + while True: + req = await self.get( + '/channels/followers', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'user_id': user_id, + 'first': first, + 'after': after, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + channel_followers = s.ChannelFollowers.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for data in channel_followers.data: + yield channel_followers.total, data + + if isinstance(channel_followers.pagination, s.Pagination): + after = channel_followers.pagination.cursor + + if not after: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def create_custom_rewards( self, @@ -569,43 +641,57 @@ class TwitchAPIClient(OxideHTTP): *, redemption_id: int | list[int] | None = None, sort: Literal['OLDEST', 'NEWEST'] = 'OLDEST', - after: str | None = None, first: int = 20, - ) -> s.CustomRewardRedemptions: - req = await self.get( - '/channel_points/custom_rewards/redemptions', - headers={'Authorization': f'Bearer {access_token}'}, - params=self.clean_dict( - { - 'broadcaster_id': broadcaster_id, - 'reward_id': reward_id, - 'status': status, - 'redemption_id': redemption_id, - 'sort': sort, - 'after': after, - 'first': first, - } - ), - ) + ) -> AsyncGenerator[s.CustomRewardRedemption]: + after = None - match req.status_code: - case st.OK: - return s.CustomRewardRedemptions.model_validate( - await req.json() - ) + while True: + req = await self.get( + '/channel_points/custom_rewards/redemptions', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'reward_id': reward_id, + 'status': status, + 'redemption_id': redemption_id, + 'sort': sort, + 'after': after, + 'first': first, + } + ), + ) - case ( - st.BAD_REQUEST - | st.UNAUTHORIZED - | st.NOT_FOUND - | st.TOO_MANY_REQUESTS - ): - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + redemptions = s.CustomRewardRedemptions.model_validate( + json + ) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for data in redemptions.data: + yield data + + if isinstance(redemptions.pagination, s.Pagination): + after = redemptions.pagination.cursor + + if not after: + break + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.NOT_FOUND + | st.TOO_MANY_REQUESTS + ): + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def update_custom_reward( self, @@ -737,37 +823,51 @@ class TwitchAPIClient(OxideHTTP): broadcaster_id: int, *, first: int = 20, - after: str | None = None, cache_time: int | None = None, - ) -> s.CharityDonations: - req = await self.get( - '/charity/donations', - params=self.clean_dict( - { - 'broadcaster_id': broadcaster_id, - 'first': first, - 'after': after, - } - ), - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - ) + ) -> AsyncGenerator[s.CharityDonation]: + after = None - match req.status_code: - case st.OK: - return s.CharityDonations.model_validate(await req.json()) + while True: + req = await self.get( + '/charity/donations', + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'first': first, + 'after': after, + } + ), + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + donations = s.CharityDonations.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for donation in donations.data: + yield donation + + if isinstance(donations.pagination, s.Pagination): + after = donations.pagination.cursor + + if not after: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def get_chatters( self, @@ -776,38 +876,52 @@ class TwitchAPIClient(OxideHTTP): moderator_id: int, *, first: int = 20, - after: str | None = None, cache_time: int | None = None, - ) -> s.Chatters: - req = await self.get( - '/chat/chatters', - params=self.clean_dict( - { - 'broadcaster_id': broadcaster_id, - 'moderator_id': moderator_id, - 'first': first, - 'after': after, - } - ), - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - ) + ) -> AsyncGenerator[tuple[int, s.ChattersData]]: + after = None - match req.status_code: - case st.OK: - return s.Chatters.model_validate(await req.json()) + while True: + req = await self.get( + '/chat/chatters', + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'moderator_id': moderator_id, + 'first': first, + 'after': after, + } + ), + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + chatters = s.Chatters.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for data in chatters.data: + yield chatters.total, data + + if isinstance(chatters.pagination, s.Pagination): + after = chatters.pagination.cursor + + if not after: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def get_channel_emotes( self, @@ -1020,38 +1134,52 @@ class TwitchAPIClient(OxideHTTP): access_token: str, user_id: int, *, - after: str | None = None, broadcaster_id: int | None = None, cache_time: int | None = None, - ) -> s.UserEmotes: - req = await self.get( - '/chat/emotes/global', - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - params=self.clean_dict( - { - 'user_id': user_id, - 'after': after, - 'broadcaster_id': broadcaster_id, - } - ), - ) + ) -> AsyncGenerator[tuple[str, s.ChannelEmote]]: + after = None - match req.status_code: - case st.OK: - return s.UserEmotes.model_validate(await req.json()) + while True: + req = await self.get( + '/chat/emotes/global', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + { + 'user_id': user_id, + 'after': after, + 'broadcaster_id': broadcaster_id, + } + ), + ) - case st.UNAUTHORIZED: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + user_emotes = s.UserEmotes.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for data in user_emotes.data: + yield user_emotes.template, data + + if isinstance(user_emotes.pagination, s.Pagination): + after = user_emotes.pagination.cursor + + if not after: + break + + case st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def update_chat_settings( self, @@ -1306,44 +1434,58 @@ class TwitchAPIClient(OxideHTTP): ended_at: datetime | None = None, first: int = 20, before: str | None = None, - after: str | None = None, is_featured: bool | None = None, cache_time: int | None = None, - ) -> s.Clips: - req = await self.get( - '/clips', - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - params=self.clean_dict( - { - 'broadcaster_id': broadcaster_id, - 'game_id': game_id, - 'clip_id': clip_id, - 'started_at': started_at, - 'ended_at': ended_at, - 'first': first, - 'before': before, - 'after': after, - 'is_featured': is_featured, - } - ), - ) + ) -> AsyncGenerator[s.Clip]: + after = None - match req.status_code: - case st.OK: - return s.Clips.model_validate(await req.json()) + while True: + req = await self.get( + '/clips', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'game_id': game_id, + 'clip_id': clip_id, + 'started_at': started_at, + 'ended_at': ended_at, + 'first': first, + 'before': before, + 'after': after, + 'is_featured': is_featured, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + clips = s.Clips.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for clip in clips.data: + yield clip + + if isinstance(clips.pagination, s.Pagination): + after = clips.pagination.cursor + + if after is None: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def get_clips_downloads( self, @@ -1480,33 +1622,47 @@ class TwitchAPIClient(OxideHTTP): conduit_id: str, *, status: str | None = None, - after: str | None = None, cache_time: int | None = None, - ) -> s.ConduitShards: - req = await self.get( - '/eventsub/conduits/shards', - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - params=self.clean_dict( - {'id': conduit_id, 'status': status, 'after': after} - ), - ) + ) -> AsyncGenerator[s.ConduitShard]: + after = None - match req.status_code: - case st.OK: - return s.ConduitShards.model_validate(await req.json()) + while True: + req = await self.get( + '/eventsub/conduits/shards', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + {'id': conduit_id, 'status': status, 'after': after} + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + shards = s.ConduitShards.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for shard in shards.data: + yield shard + + if isinstance(shards.pagination, s.Pagination): + after = shards.pagination.cursor + + if after is None: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def update_conduit_shards( self, @@ -1595,46 +1751,60 @@ class TwitchAPIClient(OxideHTTP): user_id: int | None = None, game_id: int | None = None, fulfillment_status: Literal['CLAIMED', 'FULFILLED'] | None = None, - after: str | None = None, first: int = 20, cache_time: int | None = None, - ) -> s.DropsEntitlements: - req = await self.get( - '/entitlements/drops', - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - params=self.clean_dict( - { - 'drop_id': drop_id, - 'user_id': user_id, - 'game_id': game_id, - 'fulfillment_status': fulfillment_status, - 'after': after, - 'first': first, - } - ), - ) + ) -> AsyncGenerator[s.DropEntitlement]: + after = None - match req.status_code: - case st.OK: - return s.DropsEntitlements.model_validate(await req.json()) + while True: + req = await self.get( + '/entitlements/drops', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + { + 'drop_id': drop_id, + 'user_id': user_id, + 'game_id': game_id, + 'fulfillment_status': fulfillment_status, + 'after': after, + 'first': first, + } + ), + ) - case ( - st.BAD_REQUEST - | st.UNAUTHORIZED - | st.FORBIDDEN - | st.INTERNAL_SERVER_ERROR - ): - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + entitlements = s.DropsEntitlements.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for entitlement in entitlements.data: + yield entitlement + + if isinstance(entitlements.pagination, s.Pagination): + after = entitlements.pagination.cursor + + if after is None: + break + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.INTERNAL_SERVER_ERROR + ): + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def update_drops_entitlements( self, @@ -1815,37 +1985,51 @@ class TwitchAPIClient(OxideHTTP): extension_id: str, *, first: int | None = None, - after: str | None = None, cache_time: int | None = None, - ) -> s.ExtensionLiveChannels: - req = await self.get( - '/extensions/live', - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - params=self.clean_dict( - { - 'extension_id': extension_id, - 'first': first, - 'after': after, - } - ), - ) + ) -> AsyncGenerator[s.ExtensionLiveChannel]: + after = None - match req.status_code: - case st.OK: - return s.ExtensionLiveChannels.model_validate(await req.json()) + while True: + req = await self.get( + '/extensions/live', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + { + 'extension_id': extension_id, + 'first': first, + 'after': after, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + extensions = s.ExtensionLiveChannels.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for extension in extensions.data: + yield extension + + if isinstance(extensions.paginaiton, s.Pagination): + after = extensions.paginaiton.cursor + + if after is None: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def get_extension_secrets( self, @@ -2157,79 +2341,107 @@ class TwitchAPIClient(OxideHTTP): sub_type: sub_type.Any | None = None, user_id: int | None = None, subscription_id: str | None = None, - after: str | None = None, cache_time: int | None = None, - ) -> s.EventsubBaseSubscriptions: - req = await self.get( - '/eventsub/subscriptions', - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - params=self.clean_dict( - { - 'status': status, - 'type': sub_type, - 'user_id': user_id, - 'subscription_id': subscription_id, - 'after': after, - } - ), - ) + ) -> AsyncGenerator[tuple[int, sub.Any]]: + after = None - match req.status_code: - case st.OK: - return s.EventsubBaseSubscriptions.model_validate( - await req.json() - ) + while True: + req = await self.get( + '/eventsub/subscriptions', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + { + 'status': status, + 'type': sub_type, + 'user_id': user_id, + 'subscription_id': subscription_id, + 'after': after, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + subscriptions = s.EventsubBaseSubscriptions.model_validate( + json + ) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for subscription in subscriptions.data: + yield subscriptions.total, subscription + + if isinstance(subscriptions.pagination, s.Pagination): + after = subscriptions.pagination.cursor + + if after is None: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def get_top_games( self, access_token: str, *, first: int = 20, - after: str | None = None, before: str | None = None, cache_time: int | None = None, - ) -> s.Games: - req = await self.get( - '/games/top', - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - params=self.clean_dict( - { - 'first': first, - 'after': after, - 'before': before, - } - ), - ) + ) -> AsyncGenerator[s.Game]: + after = None - match req.status_code: - case st.OK: - return s.Games.model_validate(await req.json()) + while True: + req = await self.get( + '/games/top', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + { + 'first': first, + 'after': after, + 'before': before, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + games = s.Games.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for game in games.data: + yield game + + if isinstance(games.pagination, s.Pagination): + after = games.pagination.cursor + + if after is None: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def get_games( self, @@ -2482,34 +2694,48 @@ class TwitchAPIClient(OxideHTTP): user_id: int | list[int], *, first: int = 20, - after: str | None = None, before: str | None = None, - ) -> s.BannedUsers: - req = await self.get( - '/moderation/banned', - headers={'Authorization': f'Bearer {access_token}'}, - params=self.clean_dict( - { - 'broadcaster_id': broadcaster_id, - 'user_id': user_id, - 'first': first, - 'after': after, - 'before': before, - } - ), - ) + ) -> AsyncGenerator[s.BannedUser]: + after = None - match req.status_code: - case st.OK: - return s.BannedUsers.model_validate(await req.json()) + while True: + req = await self.get( + '/moderation/banned', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'user_id': user_id, + 'first': first, + 'after': after, + 'before': before, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + banned = s.BannedUsers.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for user in banned.data: + yield user + + if isinstance(banned.pagination, s.Pagination): + after = banned.pagination.cursor + + if after is None: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def ban_user( self, @@ -2600,35 +2826,49 @@ class TwitchAPIClient(OxideHTTP): ], *, user_id: int | None = None, - after: str | None = None, first: int | None = None, - ) -> s.UnbanRequests: - req = await self.get( - '/moderation/unbans', - headers={'Authorization': f'Bearer {access_token}'}, - params=self.clean_dict( - { - 'broadcaster_id': broadcaster_id, - 'moderator_id': moderator_id, - 'status': status, - 'user_id': user_id, - 'after': after, - 'first': first, - } - ), - ) + ) -> AsyncGenerator[s.UnbanRequest]: + after = None - match req.status_code: - case st.OK: - return s.UnbanRequests.model_validate(await req.json()) + while True: + req = await self.get( + '/moderation/unbans', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'moderator_id': moderator_id, + 'status': status, + 'user_id': user_id, + 'after': after, + 'first': first, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + unban_requests = s.UnbanRequests.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for unban_request in unban_requests.data: + yield unban_request + + if isinstance(unban_requests.pagination, s.Pagination): + after = unban_requests.pagination.cursor + + if after is None: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def resolve_unban_request( self, @@ -2673,38 +2913,52 @@ class TwitchAPIClient(OxideHTTP): moderator_id: int, *, first: int | None = None, - after: str | None = None, cache_time: int | None = None, - ) -> s.BlockedTerms: - req = await self.get( - '/moderation/blocked_terms', - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - params=self.clean_dict( - { - 'broadcaster_id': broadcaster_id, - 'moderator_id': moderator_id, - 'first': first, - 'after': after, - } - ), - ) + ) -> AsyncGenerator[s.BlockedTerm]: + after = None - match req.status_code: - case st.OK: - return s.BlockedTerms.model_validate(await req.json()) + while True: + req = await self.get( + '/moderation/blocked_terms', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'moderator_id': moderator_id, + 'first': first, + 'after': after, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + blocked_terms = s.BlockedTerms.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for blocked_term in blocked_terms.data: + yield blocked_term + + if isinstance(blocked_terms.pagination, s.Pagination): + after = blocked_terms.pagination.cursor + + if after is None: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def add_blocked_term( self, @@ -2803,38 +3057,54 @@ class TwitchAPIClient(OxideHTTP): access_token: str, user_id: int, *, - after: str | None = None, first: int | None = None, cache_time: int | None = None, - ) -> s.ModeratedChannels: - req = await self.get( - '/moderation/channels', - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - params=self.clean_dict( - { - 'user_id': user_id, - 'after': after, - 'first': first, - } - ), - ) + ) -> AsyncGenerator[s.ModeratedChannel]: + after = None - match req.status_code: - case st.OK: - return s.ModeratedChannels.model_validate(await req.json()) + while True: + req = await self.get( + '/moderation/channels', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + { + 'user_id': user_id, + 'after': after, + 'first': first, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + moderated_channels = s.ModeratedChannels.model_validate( + json + ) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for moderated_channel in moderated_channels.data: + yield moderated_channel + + if isinstance(moderated_channels.pagination, s.Pagination): + after = moderated_channels.pagination.cursor + + if after is None: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def get_moderators( self, @@ -2843,38 +3113,52 @@ class TwitchAPIClient(OxideHTTP): *, user_id: int | list[int] | None = None, first: int = 20, - after: str | None = None, cache_time: int | None = None, - ) -> s.Moderators: - req = await self.get( - '/moderation/moderators', - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - params=self.clean_dict( - { - 'broadcaster_id': broadcaster_id, - 'user_id': user_id, - 'first': first, - 'after': after, - } - ), - ) + ) -> AsyncGenerator[s.Moderator]: + after = None - match req.status_code: - case st.OK: - return s.Moderators.model_validate(await req.json()) + while True: + req = await self.get( + '/moderation/moderators', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'user_id': user_id, + 'first': first, + 'after': after, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + moderators = s.Moderators.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for moderator in moderators.data: + yield moderator + + if isinstance(moderators.pagination, s.Pagination): + after = moderators.pagination.cursor + + if after is None: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def add_channel_moderator( self, access_token: str, broadcaster_id: int, user_id: int @@ -2930,38 +3214,52 @@ class TwitchAPIClient(OxideHTTP): broadcaster_id: int, *, first: int = 20, - after: str | None = None, cache_time: int | None = None, - ) -> s.VIPs: - req = await self.get( - '/channels/vips', - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - params=self.clean_dict( - { - 'user_id': user_id, - 'broadcaster_id': broadcaster_id, - 'first': first, - 'after': after, - } - ), - ) + ) -> AsyncGenerator[s.VIP]: + after = None - match req.status_code: - case st.OK: - return s.VIPs.model_validate(await req.json()) + while True: + req = await self.get( + '/channels/vips', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + { + 'user_id': user_id, + 'broadcaster_id': broadcaster_id, + 'first': first, + 'after': after, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + vips = s.VIPs.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for vip in vips.data: + yield vip + + if isinstance(vips.pagination, s.Pagination): + after = vips.pagination.cursor + + if after is None: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def add_channel_vip( self, access_token: str, broadcaster_id: int, user_id: int @@ -3133,38 +3431,52 @@ class TwitchAPIClient(OxideHTTP): *, poll_id: int | list[int] | None = None, first: int = 20, - after: str | None = None, cache_time: int | None = None, - ) -> s.Polls: - req = await self.get( - '/polls', - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - params=self.clean_dict( - { - 'broadcaster_id': broadcaster_id, - 'id': poll_id, - 'first': first, - 'after': after, - } - ), - ) + ) -> AsyncGenerator[s.Poll]: + after = None - match req.status_code: - case st.OK: - return s.Polls.model_validate(await req.json()) + while True: + req = await self.get( + '/polls', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'id': poll_id, + 'first': first, + 'after': after, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + polls = s.Polls.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for poll in polls.data: + yield poll + + if isinstance(polls.pagination, s.Pagination): + after = polls.pagination.cursor + + if after is None: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def create_poll( self, @@ -3243,38 +3555,52 @@ class TwitchAPIClient(OxideHTTP): *, prediction_id: int | list[int] | None = None, first: int = 20, - after: str | None = None, cache_time: int | None = None, - ) -> s.Predictions: - req = await self.get( - '/predictions', - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - params=self.clean_dict( - { - 'broadcaster_id': broadcaster_id, - 'id': prediction_id, - 'first': first, - 'after': after, - } - ), - ) + ) -> AsyncGenerator[s.Prediction]: + after = None - match req.status_code: - case st.OK: - return s.Predictions.model_validate(await req.json()) + while True: + req = await self.get( + '/predictions', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'id': prediction_id, + 'first': first, + 'after': after, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + predictions = s.Predictions.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for prediction in predictions.data: + yield prediction + + if isinstance(predictions.pagination, s.Pagination): + after = predictions.pagination.cursor + + if after is None: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def create_prediction( self, @@ -3411,41 +3737,58 @@ class TwitchAPIClient(OxideHTTP): segment_id: int | list[int] | None = None, start_time: datetime | None = None, first: int = 20, - after: str | None = None, cache_time: int | None = None, - ) -> s.Schedules: - req = await self.get( - '/schedule', - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - params=self.clean_dict( - { - 'broadcaster_id': broadcaster_id, - 'id': segment_id, - 'start_time': start_time, - 'first': first, - 'after': after, - } - ), - ) + ) -> AsyncGenerator[s.Schedule]: + after = None - match req.status_code: - case st.OK: - return s.Schedules.model_validate(await req.json()) + while True: + req = await self.get( + '/schedule', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'id': segment_id, + 'start_time': start_time, + 'first': first, + 'after': after, + } + ), + ) - case ( - st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN | st.NOT_FOUND - ): - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + schedules = s.Schedules.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for schedule in schedules.data: + yield schedule + + if isinstance(schedules.pagination, s.Pagination): + after = schedules.pagination.cursor + + if after is None: + break + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.NOT_FOUND + ): + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def get_channel_icalendar( self, @@ -3467,7 +3810,8 @@ class TwitchAPIClient(OxideHTTP): match req.status_code: case st.OK: - return await req.json() + mem = await req.get_bytes() + return bytes(mem) case st.BAD_REQUEST: raise s.ClientError( @@ -3631,33 +3975,47 @@ class TwitchAPIClient(OxideHTTP): query: str, *, first: int = 20, - after: str | None = None, cache_time: int | None = None, - ) -> s.Categories: - req = await self.get( - '/search/categories', - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - params=self.clean_dict( - {'query': query, 'first': first, 'after': after} - ), - ) + ) -> AsyncGenerator[s.Category]: + after = None - match req.status_code: - case st.OK: - return s.Categories.model_validate(await req.json()) + while True: + req = await self.get( + '/search/categories', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + {'query': query, 'first': first, 'after': after} + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + categories = s.Categories.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for category in categories.data: + yield category + + if isinstance(categories.pagination, s.Pagination): + after = categories.pagination.cursor + + if not after: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def search_channels( self, @@ -3666,38 +4024,52 @@ class TwitchAPIClient(OxideHTTP): *, live_only: bool = False, first: int = 20, - after: str | None = None, cache_time: int | None = None, - ) -> s.Channels: - req = await self.get( - '/search/channels', - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - params=self.clean_dict( - { - 'query': query, - 'live_only': live_only, - 'first': first, - 'after': after, - } - ), - ) + ) -> AsyncGenerator[s.Channel]: + after = None - match req.status_code: - case st.OK: - return s.Channels.model_validate(await req.json()) + while True: + req = await self.get( + '/search/channels', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + { + 'query': query, + 'live_only': live_only, + 'first': first, + 'after': after, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + channels = s.Channels.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for channel in channels.data: + yield channel + + if isinstance(channels.pagination, s.Pagination): + after = channels.pagination.cursor + + if not after: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def get_stream_key( self, @@ -3740,42 +4112,56 @@ class TwitchAPIClient(OxideHTTP): language: str | None = None, first: int = 20, before: str | None = None, - after: str | None = None, cache_time: int | None = None, - ) -> s.Streams: - req = await self.get( - '/streams', - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - params=self.clean_dict( - { - 'user_id': user_id, - 'user_login': user_login, - 'game_id': game_id, - 'type': stream_type, - 'language': language, - 'first': first, - 'before': before, - 'after': after, - } - ), - ) + ) -> AsyncGenerator[s.Stream]: + after = None - match req.status_code: - case st.OK: - return s.Streams.model_validate(await req.json()) + while True: + req = await self.get( + '/streams', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + { + 'user_id': user_id, + 'user_login': user_login, + 'game_id': game_id, + 'type': stream_type, + 'language': language, + 'first': first, + 'before': before, + 'after': after, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + streams = s.Streams.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for stream in streams.data: + yield stream + + if isinstance(streams.pagination, s.Pagination): + after = streams.pagination.cursor + + if not after: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def get_followed_streams( self, @@ -3783,37 +4169,51 @@ class TwitchAPIClient(OxideHTTP): user_id: int, *, first: int = 20, - after: str | None = None, cache_time: int | None = None, - ) -> s.Streams: - req = await self.get( - '/streams/followed', - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - params=self.clean_dict( - { - 'user_id': user_id, - 'first': first, - 'after': after, - } - ), - ) + ) -> AsyncGenerator[s.Stream]: + after = None - match req.status_code: - case st.OK: - return s.Streams.model_validate(await req.json()) + while True: + req = await self.get( + '/streams/followed', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + { + 'user_id': user_id, + 'first': first, + 'after': after, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + streams = s.Streams.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for stream in streams.data: + yield stream + + if isinstance(streams.pagination, s.Pagination): + after = streams.pagination.cursor + + if not after: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def create_stream_marker( self, @@ -3857,41 +4257,58 @@ class TwitchAPIClient(OxideHTTP): *, first: int = 20, before: str | None = None, - after: str | None = None, cache_time: int | None = None, - ) -> s.StreamMarkers: - req = await self.get( - '/streams/markers', - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - params=self.clean_dict( - { - 'user_id': user_id, - 'video_id': video_id, - 'first': first, - 'before': before, - 'after': after, - } - ), - ) + ) -> AsyncGenerator[s.StreamMarkersData]: + after = None - match req.status_code: - case st.OK: - return s.StreamMarkers.model_validate(await req.json()) + while True: + req = await self.get( + '/streams/markers', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + { + 'user_id': user_id, + 'video_id': video_id, + 'first': first, + 'before': before, + 'after': after, + } + ), + ) - case ( - st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN | st.NOT_FOUND - ): - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + markers = s.StreamMarkers.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for marker in markers.data: + yield marker + + if isinstance(markers.pagination, s.Pagination): + after = markers.pagination.cursor + + if not after: + break + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.NOT_FOUND + ): + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def get_broadcaster_subscriptions( self, @@ -3899,41 +4316,59 @@ class TwitchAPIClient(OxideHTTP): broadcaster_id: int, *, first: int = 20, - after: str | None = None, before: str | None = None, cache_time: int | None = None, - ) -> s.BroadcasterSubscriptions: - req = await self.get( - '/subscriptions', - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - params=self.clean_dict( - { - 'broadcaster_id': broadcaster_id, - 'first': first, - 'after': after, - 'before': before, - } - ), - ) + ) -> AsyncGenerator[tuple[int, int, s.BroadcasterSubscription]]: + after = None - match req.status_code: - case st.OK: - return s.BroadcasterSubscriptions.model_validate( - await req.json() - ) + while True: + req = await self.get( + '/subscriptions', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'first': first, + 'after': after, + 'before': before, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + subscriptions = s.BroadcasterSubscriptions.model_validate( + json + ) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for subscription in subscriptions.data: + yield ( + subscriptions.total, + subscriptions.points, + subscription, + ) + + if isinstance(subscriptions.pagination, s.Pagination): + after = subscriptions.pagination.cursor + + if not after: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def check_user_subscription( self, @@ -4129,37 +4564,51 @@ class TwitchAPIClient(OxideHTTP): broadcaster_id: int, *, first: int = 20, - after: str | None = None, cache_time: int | None = None, - ) -> s.UserBlockList: - req = await self.get( - '/users/blocks', - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - params=self.clean_dict( - { - 'broadcaster_id': broadcaster_id, - 'first': first, - 'after': after, - } - ), - ) + ) -> AsyncGenerator[s.UserBlock]: + after = None - match req.status_code: - case st.OK: - return s.UserBlockList.model_validate(await req.json()) + while True: + req = await self.get( + '/users/blocks', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'first': first, + 'after': after, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + user_block_list = s.UserBlockList.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for user in user_block_list.data: + yield user + + if isinstance(user_block_list.pagination, s.Pagination): + after = user_block_list.pagination.cursor + + if after is None: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def block_user( self, @@ -4312,43 +4761,57 @@ class TwitchAPIClient(OxideHTTP): sort: Literal['time', 'trending', 'views'] = 'time', video_type: Literal['all', 'archive', 'highlight', 'upload'] = 'all', first: int = 20, - after: str | None = None, cache_time: int | None = None, - ) -> s.Videos: - req = await self.get( - '/videos', - headers=self.clean_dict( - { - 'Authorization': f'Bearer {access_token}', - 'X-Cache-TTL': str(cache_time), - } - ), - params=self.clean_dict( - { - 'video_id': video_id, - 'user_id': user_id, - 'game_id': game_id, - 'language': language, - 'period': period, - 'sort': sort, - 'video_type': video_type, - 'first': first, - 'after': after, - } - ), - ) + ) -> AsyncGenerator[s.Video]: + after = None - match req.status_code: - case st.OK: - return s.Videos.model_validate(await req.json()) + while True: + req = await self.get( + '/videos', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + { + 'video_id': video_id, + 'user_id': user_id, + 'game_id': game_id, + 'language': language, + 'period': period, + 'sort': sort, + 'video_type': video_type, + 'first': first, + 'after': after, + } + ), + ) - case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: - raise s.ClientError( - req.status_code, (await req.json())['message'] - ) + match req.status_code: + case st.OK: + json = await req.json() + videos = s.Videos.model_validate(json) - case _: - raise s.InternalError(req.status_code, 'Internal Server Error') + for video in videos.data: + yield video + + if isinstance(videos.pagination, s.Pagination): + after = videos.pagination.cursor + + if after is None: + break + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError( + req.status_code, 'Internal Server Error' + ) async def delete_videos( self, access_token: str, video_id: int | list[int] diff --git a/src/oxidetwitch/schema.py b/src/oxidetwitch/schema.py index 2312b39..6d627e1 100644 --- a/src/oxidetwitch/schema.py +++ b/src/oxidetwitch/schema.py @@ -355,6 +355,7 @@ class CustomRewardRedemption(BaseSchema): class CustomRewardRedemptions(BaseSchema): data: list[CustomRewardRedemption] + pagination: Pagination | dict[Any, Any] | None = None class CharityCampaignCurrentAmount(BaseSchema): @@ -854,7 +855,7 @@ class Game(BaseSchema): class Games(BaseSchema): data: list[Game] - paginaiton: Pagination | dict[Any, Any] | None = None + pagination: Pagination | dict[Any, Any] | None = None class CreatorGoal(BaseSchema): @@ -1392,6 +1393,7 @@ class UserBlock(BaseSchema): class UserBlockList(BaseSchema): data: list[UserBlock] + pagination: Pagination | dict[Any, Any] | None = None class UserExtension(BaseSchema): -- 2.47.2