🐛 Fix issues and improve functionality

- Fix issue with Daily Reward Client not running
- Update Wish Client to support multiple banner retrieval
- Fix issue with setting salt for dynamic secret
- Improve APIModel to support multiple aliases
- Add asyncio.sleep() to Wish paginator to prevent excessive requests

Co-authored-by: xtaodada <xtao@xtaolink.cn>
This commit is contained in:
洛水居室 2023-05-08 08:53:48 +08:00
parent 5d2d3609bb
commit 4250481727
No known key found for this signature in database
GPG Key ID: C9DE87DA724B88FC
15 changed files with 150 additions and 98 deletions

View File

@ -8,7 +8,7 @@ from httpx import AsyncClient, TimeoutException, Response, HTTPError, Timeout
from simnet.client.cookies import Cookies from simnet.client.cookies import Cookies
from simnet.client.headers import Headers from simnet.client.headers import Headers
from simnet.errors import TimedOut, NetworkError, BadRequest, raise_for_ret_code from simnet.errors import TimedOut, NetworkError, BadRequest, raise_for_ret_code
from simnet.utils.ds import generate_dynamic_secret from simnet.utils.ds import generate_dynamic_secret, DSType
from simnet.utils.enum_ import Region, Game from simnet.utils.enum_ import Region, Game
from simnet.utils.types import ( from simnet.utils.types import (
RT, RT,
@ -187,7 +187,7 @@ class BaseClient(AsyncContextManager["BaseClient"]):
header (HeaderTypes): The header to use. header (HeaderTypes): The header to use.
lang (Optional[str], optional): The language to use for overseas regions. Defaults to None. lang (Optional[str], optional): The language to use for overseas regions. Defaults to None.
ds (str, optional): The DS string to use. Defaults to None. ds (str, optional): The DS string to use. Defaults to None.
ds_type (str, optional): The DS type to use. Defaults to None. ds_type (Optional[DSType], optional): The DS type to use. Defaults to None.
new_ds (bool, optional): Whether to generate a new DS. Defaults to False. new_ds (bool, optional): Whether to generate a new DS. Defaults to False.
data (Any, optional): The data to use. Defaults to None. data (Any, optional): The data to use. Defaults to None.
params (Optional[QueryParamTypes], optional): The query parameters to use. Defaults to None. params (Optional[QueryParamTypes], optional): The query parameters to use. Defaults to None.
@ -309,7 +309,7 @@ class BaseClient(AsyncContextManager["BaseClient"]):
headers: Optional[HeaderTypes] = None, headers: Optional[HeaderTypes] = None,
lang: Optional[str] = None, lang: Optional[str] = None,
new_ds: bool = False, new_ds: bool = False,
ds_type: str = None, ds_type: Optional[DSType] = None,
): ):
"""Make a request to the lab API and return the data. """Make a request to the lab API and return the data.
@ -325,7 +325,7 @@ class BaseClient(AsyncContextManager["BaseClient"]):
headers (Optional[HeaderTypes]): The headers to include in the request. headers (Optional[HeaderTypes]): The headers to include in the request.
lang (Optional[str]): The language of the request (e.g., "en", "zh"). lang (Optional[str]): The language of the request (e.g., "en", "zh").
new_ds (bool): Whether to use a new dataset for the request. new_ds (bool): Whether to use a new dataset for the request.
ds_type (str): The type of dataset to use for the request (e.g., "news", "qa"). ds_type (Optional[DSType]): The type of dataset to use for the request (e.g., "news", "qa").
Returns: Returns:
Any: The data returned by the lab API. Any: The data returned by the lab API.

View File

@ -218,15 +218,19 @@ class GenshinBattleChronicleClient(BaseChronicleClient):
Returns: Returns:
Character: The requested genshin user with all their possible data. Character: The requested genshin user with all their possible data.
""" """
user, abyss1, abyss2, activities = await asyncio.gather( index, character, abyss1, abyss2, activities = await asyncio.gather(
self.get_genshin_user(player_id, lang=lang), self._request_genshin_record("index", player_id, lang=lang),
self._request_genshin_record(
"character", player_id, lang=lang, method="POST"
),
self.get_genshin_spiral_abyss(player_id, lang=lang, previous=False), self.get_genshin_spiral_abyss(player_id, lang=lang, previous=False),
self.get_genshin_spiral_abyss(player_id, lang=lang, previous=True), self.get_genshin_spiral_abyss(player_id, lang=lang, previous=True),
self.get_genshin_activities(player_id, lang=lang), self.get_genshin_activities(player_id, lang=lang),
) )
user = {**index, **character}
abyss = SpiralAbyssPair(current=abyss1, previous=abyss2) abyss = SpiralAbyssPair(current=abyss1, previous=abyss2)
return FullGenshinUserStats(**user.dict(), abyss=abyss, activities=activities) return FullGenshinUserStats(**user, abyss=abyss, activities=activities)
async def get_genshin_activities( async def get_genshin_activities(
self, player_id: Optional[int] = None, *, lang: Optional[str] = None self, player_id: Optional[int] = None, *, lang: Optional[str] = None

View File

@ -2,11 +2,14 @@
import asyncio import asyncio
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
from httpx import QueryParams
from simnet.client.base import BaseClient from simnet.client.base import BaseClient
from simnet.utils.ds import hex_digest
from simnet.client.routes import REWARD_URL from simnet.client.routes import REWARD_URL
from simnet.models.lab.daily import DailyRewardInfo, DailyReward, ClaimedDailyReward from simnet.models.lab.daily import DailyRewardInfo, DailyReward, ClaimedDailyReward
from simnet.utils.ds import hex_digest
from simnet.utils.enum_ import Game, Region from simnet.utils.enum_ import Game, Region
from simnet.utils.player import recognize_genshin_server, recognize_starrail_server
__all__ = ("DailyRewardClient",) __all__ = ("DailyRewardClient",)
@ -40,19 +43,18 @@ class DailyRewardClient(BaseClient):
Returns: Returns:
A dictionary containing the response data. A dictionary containing the response data.
""" """
new_ds: bool = False
headers: Optional[Dict[str, str]] = None headers: Optional[Dict[str, str]] = None
params = QueryParams(params)
base_url = REWARD_URL.get_url(self.region, self.game or game)
params = params.merge(base_url.params)
if self.region == Region.CHINESE: if self.region == Region.CHINESE:
headers = {} headers = {}
if challenge is not None and validate is not None: if challenge is not None and validate is not None:
headers["x-rpc-challenge"] = challenge headers["x-rpc-challenge"] = challenge
headers["x-rpc-validate"] = validate headers["x-rpc-validate"] = validate
headers["x-rpc-seccode"] = f"{validate}|jordan" headers["x-rpc-seccode"] = f"{validate}|jordan"
headers["Referer"] = (
"https://webstatic.mihoyo.com/bbs/event/signin-ys/index.html?"
"bbs_auth_required=true&act_id=e202009291139501&utm_source=bbs&utm_medium=mys&utm_campaign=icon"
)
headers["x-rpc-device_name"] = "Chrome 20 2023" headers["x-rpc-device_name"] = "Chrome 20 2023"
headers["x-rpc-channel"] = "chrome" headers["x-rpc-channel"] = "chrome"
headers["x-rpc-device_model"] = "Chrome 2023" headers["x-rpc-device_model"] = "Chrome 2023"
@ -61,13 +63,31 @@ class DailyRewardClient(BaseClient):
device_id = self.device_id device_id = self.device_id
hash_value = hex_digest(device_id) hash_value = hex_digest(device_id)
headers["x-rpc-device_fp"] = hash_value[:13] headers["x-rpc-device_fp"] = hash_value[:13]
new_ds = endpoint == "sign" if self.game == Game.GENSHIN:
headers["referer"] = (
"https://webstatic.mihoyo.com/bbs/event/signin-ys/index.html?"
"bbs_auth_required=true&act_id=e202009291139501&utm_source=bbs&utm_medium=mys&utm_campaign=icon"
)
params = params.set("uid", self.player_id)
params = params.set("region", recognize_genshin_server(self.player_id))
if self.game == Game.STARRAIL:
headers["referer"] = (
"https://webstatic.mihoyo.com/bbs/event/signin/hkrpg/index.html?"
"bbs_auth_required=true&act_id=e202304121516551&"
"bbs_auth_required=true&bbs_presentation_style=fullscreen&"
"utm_source=bbs&utm_medium=mys&utm_campaign=icon"
)
params = params.set("uid", self.player_id)
params = params.set("region", recognize_starrail_server(self.player_id))
base_url = REWARD_URL.get_url(self.region, self.game or game) url = base_url / endpoint
url = (base_url / endpoint).update_query(**base_url.query)
return await self.request_lab( return await self.request_lab(
method, url, params=params, headers=headers, lang=lang, new_ds=new_ds url,
method,
params=params,
headers=headers,
lang=lang,
) )
async def get_reward_info( async def get_reward_info(
@ -105,7 +125,7 @@ class DailyRewardClient(BaseClient):
""" """
data = await self.request_daily_reward( data = await self.request_daily_reward(
"home", "home",
game=game, game=game or self.game,
lang=lang, lang=lang,
) )
return [DailyReward(**i) for i in data["awards"]] return [DailyReward(**i) for i in data["awards"]]
@ -129,7 +149,7 @@ class DailyRewardClient(BaseClient):
page. page.
""" """
data = await self.request_daily_reward( data = await self.request_daily_reward(
"award", params=dict(current_page=page), game=game, lang=lang "award", params=dict(current_page=page), game=game or self.game, lang=lang
) )
return [ClaimedDailyReward(**i) for i in data["list"]] return [ClaimedDailyReward(**i) for i in data["list"]]
@ -159,7 +179,7 @@ class DailyRewardClient(BaseClient):
break break
fetched_items = await self._get_claimed_rewards_page( fetched_items = await self._get_claimed_rewards_page(
page, game=game, lang=lang page, game=game or self.game, lang=lang
) )
if not fetched_items: if not fetched_items:
break break
@ -206,7 +226,7 @@ class DailyRewardClient(BaseClient):
await self.request_daily_reward( await self.request_daily_reward(
"sign", "sign",
method="POST", method="POST",
game=game, game=game or self.game,
lang=lang, lang=lang,
challenge=challenge, challenge=challenge,
validate=validate, validate=validate,
@ -216,7 +236,7 @@ class DailyRewardClient(BaseClient):
return None return None
info, rewards = await asyncio.gather( info, rewards = await asyncio.gather(
self.get_reward_info(game=game, lang=lang), self.get_reward_info(game=game or self.game, lang=lang),
self.get_monthly_rewards(game=game, lang=lang), self.get_monthly_rewards(game=game or self.game, lang=lang),
) )
return rewards[info.claimed_rewards - 1] return rewards[info.claimed_rewards - 1]

View File

@ -102,7 +102,9 @@ class BaseWishClient(BaseClient):
game=game, game=game,
lang=lang, lang=lang,
authkey=authkey, authkey=authkey,
params=dict(gacha_type=banner_type, size=size, end_id=end_id), params=dict(
gacha_type=banner_type, size=size, end_id=end_id, game_biz=Game.value
),
) )
async def get_banner_names( async def get_banner_names(

View File

@ -6,7 +6,6 @@ from simnet.models.genshin.wish import Wish
from simnet.utils.enum_ import Game from simnet.utils.enum_ import Game
from simnet.utils.paginator import WishPaginator from simnet.utils.paginator import WishPaginator
__all__ = ("GenshinWishClient",) __all__ = ("GenshinWishClient",)
@ -15,17 +14,16 @@ class GenshinWishClient(BaseWishClient):
async def wish_history( async def wish_history(
self, self,
banner_type: int, banner_types: Optional[List[int]] = None,
limit: Optional[int] = None, limit: Optional[int] = None,
lang: Optional[str] = None, lang: Optional[str] = None,
authkey: Optional[str] = None, authkey: Optional[str] = None,
end_id: int = 0, end_id: int = 0,
) -> List[Wish]: ) -> List[Wish]:
""" """Get the wish history for a list of banner types.
Get the wish history for a list of banner types.
Args: Args:
banner_type (int, optional): The banner types to get the wish history for. banner_types (Optional[List[int]], optional): The banner types to get the wish history for.
limit (Optional[int] , optional): The maximum number of wishes to retrieve. limit (Optional[int] , optional): The maximum number of wishes to retrieve.
If not provided, all available wishes will be returned. If not provided, all available wishes will be returned.
lang (Optional[str], optional): The language code to use for the request. lang (Optional[str], optional): The language code to use for the request.
@ -36,9 +34,12 @@ class GenshinWishClient(BaseWishClient):
Returns: Returns:
List[Wish]: A list of GenshinWish objects representing the retrieved wishes. List[Wish]: A list of GenshinWish objects representing the retrieved wishes.
""" """
banner_types = banner_types or [100, 200, 301, 302]
banner_names = await self.get_banner_names( banner_names = await self.get_banner_names(
game=Game.GENSHIN, lang=lang, authkey=authkey game=Game.GENSHIN, lang=lang, authkey=authkey
) )
wishes = []
for banner_type in banner_types:
paginator = WishPaginator( paginator = WishPaginator(
end_id, end_id,
partial( partial(
@ -50,5 +51,5 @@ class GenshinWishClient(BaseWishClient):
) )
items = await paginator.get(limit) items = await paginator.get(limit)
banner_name = banner_names[banner_type] banner_name = banner_names[banner_type]
wish = [Wish(**i, banner_name=banner_name) for i in items] wishes.extend([Wish(**i, banner_name=banner_name) for i in items])
return wish return sorted(wishes, key=lambda wish: wish.time.timestamp())

View File

@ -1,5 +1,6 @@
from functools import partial from functools import partial
from typing import Optional, List from typing import Optional, List
from simnet.client.components.wish.base import BaseWishClient from simnet.client.components.wish.base import BaseWishClient
from simnet.models.starrail.wish import StarRailWish from simnet.models.starrail.wish import StarRailWish
from simnet.utils.enum_ import Game from simnet.utils.enum_ import Game
@ -13,7 +14,7 @@ class StarRailWishClient(BaseWishClient):
async def wish_history( async def wish_history(
self, self,
banner_type: int, banner_types: Optional[List[int]] = None,
limit: Optional[int] = None, limit: Optional[int] = None,
lang: Optional[str] = None, lang: Optional[str] = None,
authkey: Optional[str] = None, authkey: Optional[str] = None,
@ -23,7 +24,7 @@ class StarRailWishClient(BaseWishClient):
Get the wish history for a list of banner types. Get the wish history for a list of banner types.
Args: Args:
banner_type (int, optional): The banner types to get the wish history for. banner_types (Optional[List[int]], optional): The banner types to get the wish history for.
limit (Optional[int] , optional): The maximum number of wishes to retrieve. limit (Optional[int] , optional): The maximum number of wishes to retrieve.
If not provided, all available wishes will be returned. If not provided, all available wishes will be returned.
lang (Optional[str], optional): The language code to use for the request. lang (Optional[str], optional): The language code to use for the request.
@ -34,6 +35,9 @@ class StarRailWishClient(BaseWishClient):
Returns: Returns:
List[StarRailWish]: A list of StarRailWish objects representing the retrieved wishes. List[StarRailWish]: A list of StarRailWish objects representing the retrieved wishes.
""" """
banner_types = banner_types or [1, 2, 11, 12]
wishes = []
for banner_type in banner_types:
paginator = WishPaginator( paginator = WishPaginator(
end_id, end_id,
partial( partial(
@ -44,5 +48,5 @@ class StarRailWishClient(BaseWishClient):
), ),
) )
items = await paginator.get(limit) items = await paginator.get(limit)
wish = [StarRailWish(**i) for i in items] wishes.extend([StarRailWish(**i) for i in items])
return wish return sorted(wishes, key=lambda wish: wish.time.timestamp())

View File

@ -2,13 +2,16 @@ from typing import Optional
from simnet.client.components.auth import AuthClient from simnet.client.components.auth import AuthClient
from simnet.client.components.chronicle.genshin import GenshinBattleChronicleClient from simnet.client.components.chronicle.genshin import GenshinBattleChronicleClient
from simnet.client.components.daily import DailyRewardClient
from simnet.client.components.wish.genshin import GenshinWishClient from simnet.client.components.wish.genshin import GenshinWishClient
from simnet.utils.enum_ import Game from simnet.utils.enum_ import Game
__all__ = ("GenshinClient",) __all__ = ("GenshinClient",)
class GenshinClient(GenshinBattleChronicleClient, GenshinWishClient, AuthClient): class GenshinClient(
GenshinBattleChronicleClient, GenshinWishClient, AuthClient, DailyRewardClient
):
"""A simple http client for StarRail endpoints.""" """A simple http client for StarRail endpoints."""
game: Optional[Game] = Game.GENSHIN game: Optional[Game] = Game.GENSHIN

View File

@ -2,11 +2,13 @@ from typing import Optional
from simnet.client.components.auth import AuthClient from simnet.client.components.auth import AuthClient
from simnet.client.components.chronicle.genshin import GenshinBattleChronicleClient from simnet.client.components.chronicle.genshin import GenshinBattleChronicleClient
from simnet.client.components.daily import DailyRewardClient
from simnet.client.components.wish.genshin import GenshinWishClient from simnet.client.components.wish.genshin import GenshinWishClient
from simnet.utils.enum_ import Region from simnet.utils.enum_ import Region
from simnet.utils.types import CookieTypes, HeaderTypes, TimeoutTypes from simnet.utils.types import CookieTypes, HeaderTypes, TimeoutTypes
class GenshinClient(GenshinBattleChronicleClient, GenshinWishClient, AuthClient):
class GenshinClient(GenshinBattleChronicleClient, GenshinWishClient, AuthClient, DailyRewardClient):
def __init__( def __init__(
self, self,
cookies: Optional[CookieTypes] = None, cookies: Optional[CookieTypes] = None,

View File

@ -1,3 +1,5 @@
from typing import Any
from pydantic import BaseModel from pydantic import BaseModel
try: try:
@ -9,6 +11,13 @@ except ImportError:
class APIModel(BaseModel): class APIModel(BaseModel):
"""A Pydantic BaseModel class used for modeling JSON data returned by an API.""" """A Pydantic BaseModel class used for modeling JSON data returned by an API."""
def __init__(self, **data: Any) -> None:
for field_name, field in self.__fields__.items():
aliases = field.field_info.extra.get("aliases")
if aliases and aliases in data:
data[field_name] = data.pop(aliases)
super().__init__(**data)
class Config: class Config:
"""A nested class defining configuration options for the APIModel.""" """A nested class defining configuration options for the APIModel."""

View File

@ -1,3 +1,5 @@
from typing import Optional
from simnet.models.base import APIModel from simnet.models.base import APIModel
__all__ = ("BaseCharacter",) __all__ = ("BaseCharacter",)
@ -20,10 +22,10 @@ class BaseCharacter(APIModel):
""" """
id: int id: int
name: str name: Optional[str] = None
element: str element: Optional[str] = None
rarity: int rarity: Optional[str] = None
icon: str icon: Optional[str] = None
collab: bool = False collab: bool = False

View File

@ -27,6 +27,7 @@ class PartialCharacter(BaseCharacter):
constellation (int): The number of constellations that are currently active for the character. constellation (int): The number of constellations that are currently active for the character.
""" """
icon: str = Field(alias="image")
level: int level: int
friendship: int = Field(alias="fetter") friendship: int = Field(alias="fetter")
constellation: int = Field(alias="actived_constellation_num") constellation: int = Field(alias="actived_constellation_num")

View File

@ -30,21 +30,21 @@ class Stats(APIModel):
unlocked_domains (int): Number of domains unlocked by the user. unlocked_domains (int): Number of domains unlocked by the user.
""" """
achievements: int = Field(alias="achievement_number") achievements: int = Field(aliases="achievement_number")
days_active: int = Field(alias="active_day_number") days_active: int = Field(aliases="active_day_number")
characters: int = Field(alias="avatar_number") characters: int = Field(aliases="avatar_number")
spiral_abyss: str = Field(alias="spiral_abyss") spiral_abyss: str = Field(aliases="spiral_abyss")
anemoculi: int = Field(alias="anemoculus_number") anemoculi: int = Field(aliases="anemoculus_number")
geoculi: int = Field(alias="geoculus_number") geoculi: int = Field(aliases="geoculus_number")
dendroculi: int = Field(alias="dendroculus_number") dendroculi: int = Field(aliases="dendroculus_number")
electroculi: int = Field(alias="electroculus_number") electroculi: int = Field(aliases="electroculus_number")
common_chests: int = Field(alias="common_chest_number") common_chests: int = Field(aliases="common_chest_number")
exquisite_chests: int = Field(alias="exquisite_chest_number") exquisite_chests: int = Field(aliases="exquisite_chest_number")
precious_chests: int = Field(alias="precious_chest_number") precious_chests: int = Field(aliases="precious_chest_number")
luxurious_chests: int = Field(alias="luxurious_chest_number") luxurious_chests: int = Field(aliases="luxurious_chest_number")
remarkable_chests: int = Field(alias="magic_chest_number") remarkable_chests: int = Field(aliases="magic_chest_number")
unlocked_waypoints: int = Field(alias="way_point_number") unlocked_waypoints: int = Field(aliases="way_point_number")
unlocked_domains: int = Field(alias="domain_number") unlocked_domains: int = Field(aliases="domain_number")
class Offering(APIModel): class Offering(APIModel):
@ -183,9 +183,9 @@ class PartialGenshinUserStats(APIModel):
info: UserInfo = Field("role") info: UserInfo = Field("role")
stats: Stats stats: Stats
characters: List[PartialCharacter] = Field(alias="avatars") characters: List[PartialCharacter] = Field(aliases="avatars")
explorations: List[Exploration] = Field(alias="world_explorations") explorations: List[Exploration] = Field(aliases="world_explorations")
teapot: Optional[Teapot] = Field(alias="homes") teapot: Optional[Teapot] = Field(aliases="homes")
@validator("teapot", pre=True) @validator("teapot", pre=True)
def format_teapot(cls, v: Any) -> Optional[Dict[str, Any]]: def format_teapot(cls, v: Any) -> Optional[Dict[str, Any]]:

View File

@ -1,4 +1,3 @@
import calendar
from datetime import timezone, timedelta, datetime from datetime import timezone, timedelta, datetime
from typing import NamedTuple from typing import NamedTuple
@ -25,8 +24,7 @@ class DailyRewardInfo(NamedTuple):
"""The number of rewards that the user has missed since the start of the month.""" """The number of rewards that the user has missed since the start of the month."""
cn_timezone = timezone(timedelta(hours=8)) cn_timezone = timezone(timedelta(hours=8))
now = datetime.now(cn_timezone) now = datetime.now(cn_timezone)
month_days = calendar.monthrange(now.year, now.month)[1] return now.day - self.claimed_rewards
return month_days - self.claimed_rewards
class DailyReward(APIModel): class DailyReward(APIModel):

View File

@ -15,14 +15,12 @@ class DSType(Enum):
Enumeration of dynamic secret types. Enumeration of dynamic secret types.
Attributes: Attributes:
WEB (str): Android dynamic secret type.
ANDROID (str): Android dynamic secret type. ANDROID (str): Android dynamic secret type.
ANDROID_NEW (str): New Android dynamic secret type.
SIGN (str): Sign dynamic secret type.
""" """
WEB = "web"
ANDROID = "android" ANDROID = "android"
ANDROID_NEW = "android_new"
SIGN = "sign"
def hex_digest(text): def hex_digest(text):
@ -87,14 +85,20 @@ def generate_dynamic_secret(
salt = "6s25p5ox5y14umn1p61aqyyvbvvl3lrt" salt = "6s25p5ox5y14umn1p61aqyyvbvvl3lrt"
app_version = "1.5.0" app_version = "1.5.0"
elif region == Region.CHINESE: elif region == Region.CHINESE:
if new_ds:
if ds_type is None: if ds_type is None:
salt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs" salt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs"
elif ds_type == DSType.ANDROID: elif ds_type == DSType.ANDROID:
client_type = "2"
salt = "KZazpG4cO2QECFDBUCxdhS8cYCsQHfzn" salt = "KZazpG4cO2QECFDBUCxdhS8cYCsQHfzn"
client_type = "2" else:
elif ds_type == DSType.ANDROID_NEW: raise ValueError(f"Unknown ds_type: {ds_type}")
client_type = "2" else:
if ds_type is None:
salt = "X7UOLLnTuNS3kgTJ1BUHOvKpiqp3kmym"
elif ds_type == DSType.ANDROID:
salt = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v" salt = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v"
client_type = "2"
else: else:
raise ValueError(f"Unknown ds_type: {ds_type}") raise ValueError(f"Unknown ds_type: {ds_type}")
else: else:

View File

@ -1,3 +1,4 @@
import asyncio
from typing import List, Dict, Callable, Any, Awaitable from typing import List, Dict, Callable, Any, Awaitable
@ -45,6 +46,7 @@ class WishPaginator:
break break
all_items.extend(filtered_items) all_items.extend(filtered_items)
await asyncio.sleep(0.5)
# Return up to the specified limit. # Return up to the specified limit.
return all_items[: min(len(all_items), limit)] return all_items[: min(len(all_items), limit)] if limit else all_items