From cc824d6ba97550a10fcde5844e87cb745f051236 Mon Sep 17 00:00:00 2001 From: seriaati Date: Wed, 18 Sep 2024 22:55:32 +0800 Subject: [PATCH 01/21] Remove mi18n stuff --- genshin/__main__.py | 12 +- genshin/client/components/base.py | 61 +----- genshin/client/components/chronicle/base.py | 17 +- genshin/client/routes.py | 5 - genshin/models/genshin/chronicle/abyss.py | 22 +-- genshin/models/genshin/chronicle/stats.py | 53 ++---- genshin/models/honkai/chronicle/modes.py | 75 +------- genshin/models/honkai/chronicle/stats.py | 195 ++++---------------- genshin/models/model.py | 32 ---- 9 files changed, 71 insertions(+), 401 deletions(-) diff --git a/genshin/__main__.py b/genshin/__main__.py index 0db65494..c27266b7 100644 --- a/genshin/__main__.py +++ b/genshin/__main__.py @@ -39,10 +39,7 @@ def client_command(func: typing.Callable[..., typing.Awaitable[typing.Any]]) -> @functools.wraps(func) @asynchronous async def command( - cookies: typing.Optional[str] = None, - lang: str = "en-us", - debug: bool = False, - **kwargs: typing.Any, + cookies: typing.Optional[str] = None, lang: str = "en-us", debug: bool = False, **kwargs: typing.Any ) -> typing.Any: client = genshin.Client(cookies, lang=lang, debug=debug) if cookies is None: @@ -85,7 +82,7 @@ async def honkai_stats(client: genshin.Client, uid: int) -> None: data = await client.get_honkai_user(uid) click.secho("Stats:", fg="yellow") - for k, v in data.stats.as_dict(lang=client.lang).items(): + for k, v in data.stats.dict().items(): if isinstance(v, dict): click.echo(f"{k}:") for nested_k, nested_v in typing.cast("typing.Dict[str, object]", v).items(): @@ -105,7 +102,7 @@ async def genshin_stats(client: genshin.Client, uid: int) -> None: data = await client.get_partial_genshin_user(uid) click.secho("Stats:", fg="yellow") - for k, v in data.stats.as_dict(lang=client.lang).items(): + for k, v in data.stats.dict().items(): value = click.style(str(v), bold=True) click.echo(f"{k}: {value}") @@ -178,8 +175,7 @@ async def genshin_notes(client: genshin.Client, uid: typing.Optional[int]) -> No click.echo(f"{click.style('Resin:', bold=True)} {data.current_resin}/{data.max_resin}") click.echo(f"{click.style('Realm currency:', bold=True)} {data.current_realm_currency}/{data.max_realm_currency}") click.echo( - f"{click.style('Commissions:', bold=True)} " f"{data.completed_commissions}/{data.max_commissions}", - nl=False, + f"{click.style('Commissions:', bold=True)} " f"{data.completed_commissions}/{data.max_commissions}", nl=False ) if data.completed_commissions == data.max_commissions and not data.claimed_commission_reward: click.echo(f" | [{click.style('X', fg='red')}] Haven't claimed rewards") diff --git a/genshin/client/components/base.py b/genshin/client/components/base.py index 71902ab5..851080b0 100644 --- a/genshin/client/components/base.py +++ b/genshin/client/components/base.py @@ -1,7 +1,6 @@ """Base ABC Client.""" import abc -import asyncio import base64 import functools import json @@ -20,7 +19,6 @@ from genshin.client import routes from genshin.client.manager import managers from genshin.models import hoyolab as hoyolab_models -from genshin.models import model as base_model from genshin.utility import concurrency, deprecation, ds __all__ = ["BaseClient"] @@ -289,22 +287,13 @@ def set_authkey(self, authkey: typing.Optional[str] = None, *, game: typing.Opti self.authkeys[game] = authkey def set_cache( - self, - maxsize: int = 1024, - *, - ttl: int = client_cache.HOUR, - static_ttl: int = client_cache.DAY, + self, maxsize: int = 1024, *, ttl: int = client_cache.HOUR, static_ttl: int = client_cache.DAY ) -> None: """Create and set a new cache.""" self.cache = client_cache.Cache(maxsize, ttl=ttl, static_ttl=static_ttl) def set_redis_cache( - self, - url: str, - *, - ttl: int = client_cache.HOUR, - static_ttl: int = client_cache.DAY, - **redis_kwargs: typing.Any, + self, url: str, *, ttl: int = client_cache.HOUR, static_ttl: int = client_cache.DAY, **redis_kwargs: typing.Any ) -> None: """Create and set a new redis cache.""" import aioredis @@ -384,12 +373,7 @@ async def request( await self._request_hook(method, url, params=params, data=data, headers=headers, **kwargs) response = await self.cookie_manager.request( - url, - method=method, - params=params, - json=data, - headers=headers, - **kwargs, + url, method=method, params=params, json=data, headers=headers, **kwargs ) # cache @@ -491,9 +475,7 @@ async def request_hoyolab( @managers.no_multi async def get_game_accounts( - self, - *, - lang: typing.Optional[str] = None, + self, *, lang: typing.Optional[str] = None ) -> typing.Sequence[hoyolab_models.GenshinAccount]: """Get the game accounts of the currently logged-in user.""" if self.hoyolab_id is None: @@ -508,9 +490,7 @@ async def get_game_accounts( @deprecation.deprecated("get_game_accounts") async def genshin_accounts( - self, - *, - lang: typing.Optional[str] = None, + self, *, lang: typing.Optional[str] = None ) -> typing.Sequence[hoyolab_models.GenshinAccount]: """Get the genshin accounts of the currently logged-in user.""" accounts = await self.get_game_accounts(lang=lang) @@ -592,37 +572,6 @@ def _get_hoyolab_id(self) -> int: raise RuntimeError("No default hoyolab ID provided.") - async def _fetch_mi18n(self, key: str, lang: str, *, force: bool = False) -> None: - """Update mi18n for a single url.""" - if not force: - if key in base_model.APIModel._mi18n: - return - - base_model.APIModel._mi18n[key] = {} - - url = routes.MI18N[key] - cache_key = client_cache.cache_key("mi18n", mi18n=key, lang=lang) - - data = await self.request_webstatic(url.format(lang=lang), cache=cache_key) - for k, v in data.items(): - actual_key = str.lower(key + "/" + k) - base_model.APIModel._mi18n.setdefault(actual_key, {})[lang] = v - - async def update_mi18n(self, langs: typing.Iterable[str] = constants.LANGS, *, force: bool = False) -> None: - """Fetch mi18n for partially localized endpoints.""" - if not force: - if base_model.APIModel._mi18n: - return - - langs = tuple(langs) - - coros: typing.List[typing.Awaitable[None]] = [] - for key in routes.MI18N: - for lang in langs: - coros.append(self._fetch_mi18n(key, lang, force=force)) - - await asyncio.gather(*coros) - def region_specific(region: types.Region) -> typing.Callable[[AsyncCallableT], AsyncCallableT]: """Prevent function to be ran with unsupported regions.""" diff --git a/genshin/client/components/chronicle/base.py b/genshin/client/components/chronicle/base.py index 82121779..fd6d9802 100644 --- a/genshin/client/components/chronicle/base.py +++ b/genshin/client/components/chronicle/base.py @@ -58,12 +58,10 @@ async def request_game_record( url = base_url / endpoint - mi18n_task = asyncio.create_task(self._fetch_mi18n("bbs", lang=lang or self.lang)) update_task = asyncio.create_task(utility.update_characters_any(lang or self.lang, lenient=True)) data = await self.request_hoyolab(url, lang=lang, region=region, **kwargs) - await mi18n_task try: await update_task except Exception as e: @@ -72,10 +70,7 @@ async def request_game_record( return data async def get_record_cards( - self, - hoyolab_id: typing.Optional[int] = None, - *, - lang: typing.Optional[str] = None, + self, hoyolab_id: typing.Optional[int] = None, *, lang: typing.Optional[str] = None ) -> typing.List[models.hoyolab.RecordCard]: """Get a user's record cards.""" hoyolab_id = hoyolab_id or self._get_hoyolab_id() @@ -83,10 +78,7 @@ async def get_record_cards( cache_key = cache.cache_key("records", hoyolab_id=hoyolab_id, lang=lang or self.lang) if not (data := await self.cache.get(cache_key)): data = await self.request_game_record( - "getGameRecordCard", - lang=lang, - params=dict(uid=hoyolab_id), - is_card_wapi=True, + "getGameRecordCard", lang=lang, params=dict(uid=hoyolab_id), is_card_wapi=True ) if data["list"]: @@ -98,10 +90,7 @@ async def get_record_cards( @deprecation.deprecated("get_record_cards") async def get_record_card( - self, - hoyolab_id: typing.Optional[int] = None, - *, - lang: typing.Optional[str] = None, + self, hoyolab_id: typing.Optional[int] = None, *, lang: typing.Optional[str] = None ) -> models.hoyolab.RecordCard: """Get a user's record card.""" cards = await self.get_record_cards(hoyolab_id, lang=lang) diff --git a/genshin/client/routes.py b/genshin/client/routes.py index 0e4096a2..46a5c73d 100644 --- a/genshin/client/routes.py +++ b/genshin/client/routes.py @@ -30,7 +30,6 @@ "HKRPG_URL", "INFO_LEDGER_URL", "LINEUP_URL", - "MI18N", "NAP_URL", "RECORD_URL", "REWARD_URL", @@ -244,10 +243,6 @@ def get_url(self, region: types.Region, game: types.Game) -> yarl.URL: chinese="https://hk4e-api.mihoyo.com/common/hk4e_self_help_query/User/", ) -MI18N = dict( - bbs="https://fastcdn.hoyoverse.com/mi18n/bbs_oversea/m11241040191111/m11241040191111-{lang}.json", - inquiry="https://mi18n-os.hoyoverse.com/webstatic/admin/mi18n/hk4e_global/m02251421001311/m02251421001311-{lang}.json", -) COOKIE_V2_REFRESH_URL = Route("https://sg-public-api.hoyoverse.com/account/ma-passport/token/getBySToken") GET_COOKIE_TOKEN_BY_GAME_TOKEN_URL = Route("https://api-takumi.mihoyo.com/auth/api/getCookieAccountInfoByGameToken") diff --git a/genshin/models/genshin/chronicle/abyss.py b/genshin/models/genshin/chronicle/abyss.py index 205cdf07..d0b7c0f4 100644 --- a/genshin/models/genshin/chronicle/abyss.py +++ b/genshin/models/genshin/chronicle/abyss.py @@ -44,22 +44,12 @@ class AbyssCharacter(character.BaseCharacter): class CharacterRanks(APIModel): """Collection of rankings achieved during spiral abyss runs.""" - # fmt: off - most_played: typing.Sequence[AbyssRankCharacter] = Aliased("reveal_rank", default=[], mi18n="bbs/go_fight_count") - most_kills: typing.Sequence[AbyssRankCharacter] = Aliased("defeat_rank", default=[], mi18n="bbs/max_rout_count") - strongest_strike: typing.Sequence[AbyssRankCharacter] = Aliased("damage_rank", default=[], mi18n="bbs/powerful_attack") - most_damage_taken: typing.Sequence[AbyssRankCharacter] = Aliased("take_damage_rank", default=[], mi18n="bbs/receive_max_damage") # noqa: E501 - most_bursts_used: typing.Sequence[AbyssRankCharacter] = Aliased("energy_skill_rank", default=[], mi18n="bbs/element_break_count") # noqa: E501 - most_skills_used: typing.Sequence[AbyssRankCharacter] = Aliased("normal_skill_rank", default=[], mi18n="bbs/element_skill_use_count") # noqa: E501 - # fmt: on - - def as_dict(self, lang: str = "en-us") -> typing.Mapping[str, typing.Any]: - """Turn fields into properly named ones.""" - return { - self._get_mi18n(field, lang): getattr(self, field.name) - for field in self.__fields__.values() - if field.name != "lang" - } + most_played: typing.Sequence[AbyssRankCharacter] = Aliased("reveal_rank", default=[]) + most_kills: typing.Sequence[AbyssRankCharacter] = Aliased("defeat_rank", default=[]) + strongest_strike: typing.Sequence[AbyssRankCharacter] = Aliased("damage_rank", default=[]) + most_damage_taken: typing.Sequence[AbyssRankCharacter] = Aliased("take_damage_rank", default=[]) # noqa: E501 + most_bursts_used: typing.Sequence[AbyssRankCharacter] = Aliased("energy_skill_rank", default=[]) # noqa: E501 + most_skills_used: typing.Sequence[AbyssRankCharacter] = Aliased("normal_skill_rank", default=[]) # noqa: E501 class Battle(APIModel): diff --git a/genshin/models/genshin/chronicle/stats.py b/genshin/models/genshin/chronicle/stats.py index 82aef949..809039c4 100644 --- a/genshin/models/genshin/chronicle/stats.py +++ b/genshin/models/genshin/chronicle/stats.py @@ -22,13 +22,13 @@ "Exploration", "FullGenshinUserStats", "GenshinUserStats", + "NatlanReputation", + "NatlanTribe", "Offering", "PartialGenshinUserStats", "Stats", "Teapot", "TeapotRealm", - "NatlanReputation", - "NatlanTribe", ] @@ -36,34 +36,23 @@ class Stats(APIModel): """Overall user stats.""" - # This is such fucking bullshit, just why? - # fmt: off - achievements: int = Aliased("achievement_number", mi18n="bbs/achievement_complete_count") - days_active: int = Aliased("active_day_number", mi18n="bbs/active_day") - characters: int = Aliased("avatar_number", mi18n="bbs/other_people_character") - spiral_abyss: str = Aliased("spiral_abyss", mi18n="bbs/unlock_portal") - anemoculi: int = Aliased("anemoculus_number", mi18n="bbs/wind_god") - geoculi: int = Aliased("geoculus_number", mi18n="bbs/rock_god") - dendroculi: int = Aliased("dendroculus_number", mi18n="bbs/dendro_culus") - electroculi: int = Aliased("electroculus_number", mi18n="bbs/electroculus_god") - hydroculi: int = Aliased("hydroculus_number", mi18n="bbs/hydro_god") - pyroculi: int = Aliased("pyroculus_number", mi18n="bbs/pyro_gid") - common_chests: int = Aliased("common_chest_number", mi18n="bbs/general_treasure_box_count") - exquisite_chests: int = Aliased("exquisite_chest_number", mi18n="bbs/delicacy_treasure_box_count") - precious_chests: int = Aliased("precious_chest_number", mi18n="bbs/rarity_treasure_box_count") - luxurious_chests: int = Aliased("luxurious_chest_number", mi18n="bbs/magnificent_treasure_box_count") - remarkable_chests: int = Aliased("magic_chest_number", mi18n="bbs/magic_chest_number") - unlocked_waypoints: int = Aliased("way_point_number", mi18n="bbs/unlock_portal") - unlocked_domains: int = Aliased("domain_number", mi18n="bbs/unlock_secret_area") - # fmt: on - - def as_dict(self, lang: str = "en-us") -> typing.Mapping[str, typing.Any]: - """Turn fields into properly named ones.""" - return { - self._get_mi18n(field, lang): getattr(self, field.name) - for field in self.__fields__.values() - if field.name != "lang" - } + achievements: int = Aliased("achievement_number") + days_active: int = Aliased("active_day_number") + characters: int = Aliased("avatar_number") + spiral_abyss: str = Aliased("spiral_abyss") + anemoculi: int = Aliased("anemoculus_number") + geoculi: int = Aliased("geoculus_number") + dendroculi: int = Aliased("dendroculus_number") + electroculi: int = Aliased("electroculus_number") + hydroculi: int = Aliased("hydroculus_number") + pyroculi: int = Aliased("pyroculus_number") + common_chests: int = Aliased("common_chest_number") + exquisite_chests: int = Aliased("exquisite_chest_number") + precious_chests: int = Aliased("precious_chest_number") + luxurious_chests: int = Aliased("luxurious_chest_number") + remarkable_chests: int = Aliased("magic_chest_number") + unlocked_waypoints: int = Aliased("way_point_number") + unlocked_domains: int = Aliased("domain_number") class Offering(APIModel): @@ -139,9 +128,7 @@ def explored(self) -> float: @pydantic.validator("offerings", pre=True) def __add_base_offering( - cls, - offerings: typing.Sequence[typing.Any], - values: typing.Dict[str, typing.Any], + cls, offerings: typing.Sequence[typing.Any], values: typing.Dict[str, typing.Any] ) -> typing.Sequence[typing.Any]: if values["type"] == "Reputation" and not any(values["type"] == o["name"] for o in offerings): offerings = [*offerings, dict(name=values["type"], level=values["level"])] diff --git a/genshin/models/honkai/chronicle/modes.py b/genshin/models/honkai/chronicle/modes.py index f3f0240b..af39f3fa 100644 --- a/genshin/models/honkai/chronicle/modes.py +++ b/genshin/models/honkai/chronicle/modes.py @@ -17,15 +17,7 @@ from genshin.models.honkai import battlesuit from genshin.models.model import Aliased, APIModel, Unique -__all__ = [ - "ELF", - "Boss", - "ElysianRealm", - "MemorialArena", - "MemorialBattle", - "OldAbyss", - "SuperstringAbyss", -] +__all__ = ["ELF", "Boss", "ElysianRealm", "MemorialArena", "MemorialBattle", "OldAbyss", "SuperstringAbyss"] REMEMBRANCE_SIGILS: typing.Dict[int, typing.Tuple[str, int]] = { 119301: ("The MOTH Insignia", 1), @@ -79,11 +71,6 @@ # GENERIC -def get_competitive_tier_mi18n(tier: int) -> str: - """Turn the tier returned by the API into the respective tier name displayed in-game.""" - return "bbs/" + ("area1", "area2", "area3", "area4")[tier - 1] - - class Boss(APIModel, Unique): """Represents a Boss encountered in Abyss or Memorial Arena.""" @@ -143,16 +130,6 @@ class BaseAbyss(APIModel): boss: Boss elf: typing.Optional[ELF] - @property - def tier(self) -> str: - """The user's Abyss tier as displayed in-game.""" - return self.get_tier() - - def get_tier(self, lang: str = "en-us") -> str: - """Get the user's Abyss tier in a specific language.""" - key = get_competitive_tier_mi18n(self.raw_tier) - return self._get_mi18n(key, lang) - class OldAbyss(BaseAbyss): """Represents once cycle of Quantum Singularis or Dirac Sea. @@ -176,26 +153,6 @@ def __normalize_level(cls, rank: str) -> int: return 69 - ord(rank) - @property - def rank(self) -> str: - """The user's Abyss rank as displayed in-game.""" - return self.get_rank() - - def get_rank(self, lang: str = "en-us") -> str: - """Get the user's Abyss rank in a specific language.""" - key = get_abyss_rank_mi18n(self.raw_rank, self.raw_tier) - return self._get_mi18n(key, lang) - - @property - def type(self) -> str: - """The name of this cycle's abyss type.""" - return self.get_type() - - def get_type(self, lang: str = "en-us") -> str: - """Get the name of this cycle's abyss type in a specific language.""" - key = "bbs/" + ("level_of_ow" if self.raw_type == "OW" else self.raw_type) - return self._get_mi18n(key, lang) - class SuperstringAbyss(BaseAbyss): """Represents one cycle of Superstring Abyss, exclusive to players of level 81 and up.""" @@ -210,26 +167,6 @@ class SuperstringAbyss(BaseAbyss): raw_start_rank: int = Aliased("level") raw_end_rank: int = Aliased("settled_level") - @property - def start_rank(self) -> str: - """The rank the user started the abyss cycle with, as displayed in-game.""" - return self.get_start_rank() - - def get_start_rank(self, lang: str = "en-us") -> str: - """Get the rank the user started the abyss cycle with in a specific language.""" - key = get_abyss_rank_mi18n(self.raw_start_rank, self.raw_tier) - return self._get_mi18n(key, lang) - - @property - def end_rank(self) -> str: - """The rank the user ended the abyss cycle with, as displayed in-game.""" - return self.get_end_rank() - - def get_end_rank(self, lang: str = "en-us") -> str: - """Get the rank the user ended the abyss cycle with in a specific language.""" - key = get_abyss_rank_mi18n(self.raw_end_rank, self.raw_tier) - return self._get_mi18n(key, lang) - @property def start_trophies(self) -> int: return self.end_trophies - self.trophies_gained @@ -268,16 +205,6 @@ def rank(self) -> str: """The user's Memorial Arena rank as displayed in-game.""" return prettify_MA_rank(self.raw_rank) - @property - def tier(self) -> str: - """The user's Memorial Arena tier as displayed in-game.""" - return self.get_tier() - - def get_tier(self, lang: str = "en-us") -> str: - """Get the user's Memorial Arena tier in a specific language.""" - key = get_competitive_tier_mi18n(self.raw_tier) - return self._get_mi18n(key, lang) - # ELYSIAN REALMS # TODO: Implement a way to link response_json["avatar_transcript"] data to be added to diff --git a/genshin/models/honkai/chronicle/stats.py b/genshin/models/honkai/chronicle/stats.py index 1007bcc1..bb142565 100644 --- a/genshin/models/honkai/chronicle/stats.py +++ b/genshin/models/honkai/chronicle/stats.py @@ -16,115 +16,52 @@ from . import battlesuits as honkai_battlesuits from . import modes -__all__ = [ - "FullHonkaiUserStats", - "HonkaiStats", - "HonkaiUserStats", -] - - -def _model_to_dict(model: APIModel, lang: str = "en-us") -> typing.Mapping[str, typing.Any]: - """Turn fields into properly named ones.""" - ret: typing.Dict[str, typing.Any] = {} - for field in model.__fields__.values(): - if not field.field_info.extra.get("mi18n"): - continue - - mi18n = model._get_mi18n(field, lang) - val = getattr(model, field.name) - if isinstance(val, APIModel): - ret[mi18n] = _model_to_dict(val, lang) - else: - ret[mi18n] = val - - return ret +__all__ = ["FullHonkaiUserStats", "HonkaiStats", "HonkaiUserStats"] # flake8: noqa: E222 class MemorialArenaStats(APIModel): """Represents a user's stats regarding the Memorial Arena gamemodes.""" - # fmt: off - ranking: float = Aliased("battle_field_ranking_percentage", mi18n="bbs/battle_field_ranking_percentage") - raw_rank: int = Aliased("battle_field_rank", mi18n="bbs/rank") - score: int = Aliased("battle_field_score", mi18n="bbs/score") - raw_tier: int = Aliased("battle_field_area", mi18n="bbs/settled_level") - # fmt: on + ranking: float = Aliased("battle_field_ranking_percentage") + raw_rank: int = Aliased("battle_field_rank") + score: int = Aliased("battle_field_score") + raw_tier: int = Aliased("battle_field_area") @pydantic.validator("ranking", pre=True) def __normalize_ranking(cls, value: typing.Union[str, float]) -> float: return float(value) if value else 0 - def as_dict(self, lang: str = "en-us") -> typing.Mapping[str, typing.Any]: - return _model_to_dict(self, lang) - @property def rank(self) -> str: """The user's Memorial Arena rank as displayed in-game.""" return modes.prettify_MA_rank(self.raw_rank) - @property - def tier(self) -> str: - """The user's Memorial Arena tier as displayed in-game.""" - return self.get_tier() - - def get_tier(self, lang: str = "en-us") -> str: - """Get the user's Memorial Arena tier in a specific language.""" - key = modes.get_competitive_tier_mi18n(self.raw_tier) - return self._get_mi18n(key, lang) - # flake8: noqa: E222 class SuperstringAbyssStats(APIModel): """Represents a user's stats regarding Superstring Abyss.""" - # fmt: off - raw_rank: int = Aliased("level", mi18n="bbs/rank") - trophies: int = Aliased("cup_number", mi18n="bbs/cup_number") - score: int = Aliased("abyss_score", mi18n="bbs/explain_text_2") - raw_tier: int = Aliased("battle_field_area", mi18n="bbs/settled_level") - # fmt: on + raw_rank: int = Aliased("level") + trophies: int = Aliased("cup_number") + score: int = Aliased("abyss_score") + raw_tier: int = Aliased("battle_field_area") # for consistency between types; also allows us to forego the mi18n fuckery latest_type: typing.ClassVar[str] = "Superstring" - def as_dict(self, lang: str = "en-us") -> typing.Mapping[str, typing.Any]: - return _model_to_dict(self, lang) - - @property - def rank(self) -> str: - """The user's Abyss rank as displayed in-game.""" - return self.get_rank() - - def get_rank(self, lang: str = "en-us") -> str: - """Get the user's Abyss rank in a specific language.""" - key = modes.get_abyss_rank_mi18n(self.raw_rank, self.raw_tier) - return self._get_mi18n(key, lang) - - @property - def tier(self) -> str: - """The user's Abyss tier as displayed in-game.""" - return self.get_tier() - - def get_tier(self, lang: str = "en-us") -> str: - """Get the user's Abyss tier in a specific language.""" - key = modes.get_competitive_tier_mi18n(self.raw_tier) - return self._get_mi18n(key, lang) - # flake8: noqa: E222 class OldAbyssStats(APIModel): """Represents a user's stats regarding Q-Singularis and Dirac Sea.""" - # fmt: off - raw_q_singularis_rank: typing.Optional[int] = Aliased("level_of_quantum", mi18n="bbs/Quantum") - raw_dirac_sea_rank: typing.Optional[int] = Aliased("level_of_ow", mi18n="bbs/level_of_ow") - score: int = Aliased("abyss_score", mi18n="bbs/explain_text_2") - raw_tier: int = Aliased("latest_area", mi18n="bbs/settled_level") - raw_latest_rank: typing.Optional[int] = Aliased("latest_level", mi18n="bbs/rank") + raw_q_singularis_rank: typing.Optional[int] = Aliased("level_of_quantum") + raw_dirac_sea_rank: typing.Optional[int] = Aliased("level_of_ow") + score: int = Aliased("abyss_score") + raw_tier: int = Aliased("latest_area") + raw_latest_rank: typing.Optional[int] = Aliased("latest_level") # TODO: Add proper key - latest_type: str = Aliased( mi18n="bbs/latest_type") - # fmt: on + latest_type: str = Aliased() @pydantic.validator("raw_q_singularis_rank", "raw_dirac_sea_rank", "raw_latest_rank", pre=True) def __normalize_rank(cls, rank: typing.Optional[str]) -> typing.Optional[int]: # modes.OldAbyss.__normalize_rank @@ -136,53 +73,6 @@ def __normalize_rank(cls, rank: typing.Optional[str]) -> typing.Optional[int]: return 69 - ord(rank) - @property - def q_singularis_rank(self) -> typing.Optional[str]: - """The user's latest Q-Singularis rank as displayed in-game.""" - if self.raw_q_singularis_rank is None: - return None - - return self.get_rank(self.raw_q_singularis_rank) - - @property - def dirac_sea_rank(self) -> typing.Optional[str]: - """The user's latest Dirac Sea rank as displayed in-game.""" - if self.raw_dirac_sea_rank is None: - return None - - return self.get_rank(self.raw_dirac_sea_rank) - - @property - def latest_rank(self) -> typing.Optional[str]: - """The user's latest Abyss rank as displayed in-game. Seems to apply after weekly reset, - so this may differ from the user's Dirac Sea/Q-Singularis ranks if their rank changed. - """ - if self.raw_latest_rank is None: - return None - - return self.get_rank(self.raw_latest_rank) - - def get_rank(self, rank: int, lang: str = "en-us") -> str: - """Get the user's Abyss rank in a specific language. - - Must be supplied with one of the raw ranks stored on this class. - """ - key = modes.get_abyss_rank_mi18n(rank, self.raw_tier) - return self._get_mi18n(key, lang) - - @property - def tier(self) -> str: - """The user's Abyss tier as displayed in-game.""" - return modes.get_competitive_tier_mi18n(self.raw_tier) - - def get_tier(self, lang: str = "en-us") -> str: - """Get the user's Abyss tier in a specific language.""" - key = modes.get_competitive_tier_mi18n(self.raw_tier) - return self._get_mi18n(key, lang) - - def as_dict(self, lang: str = "en-us") -> typing.Mapping[str, typing.Any]: - return _model_to_dict(self, lang) - class Config: # this is for the "stat_lang" field, hopefully nobody abuses this allow_mutation = True @@ -192,37 +82,30 @@ class Config: class ElysianRealmStats(APIModel): """Represents a user's stats regarding Elysian Realms.""" - # fmt: off - highest_difficulty: int = Aliased("god_war_max_punish_level", mi18n="bbs/god_war_max_punish_level") - remembrance_sigils: int = Aliased("god_war_extra_item_number", mi18n="bbs/god_war_extra_item_number") - highest_score: int = Aliased("god_war_max_challenge_score", mi18n="bbs/god_war_max_challenge_score") - highest_floor: int = Aliased("god_war_max_challenge_level", mi18n="bbs/rogue_setted_level") - max_level_suits: int = Aliased("god_war_max_level_avatar_number", mi18n="bbs/explain_text_6") - # fmt: on - - def as_dict(self, lang: str = "en-us") -> typing.Mapping[str, typing.Any]: - return _model_to_dict(self, lang) + highest_difficulty: int = Aliased("god_war_max_punish_level") + remembrance_sigils: int = Aliased("god_war_extra_item_number") + highest_score: int = Aliased("god_war_max_challenge_score") + highest_floor: int = Aliased("god_war_max_challenge_level") + max_level_suits: int = Aliased("god_war_max_level_avatar_number") class HonkaiStats(APIModel): """Represents a user's stat page""" - # fmt: off - active_days: int = Aliased("active_day_number", mi18n="bbs/active_day_number") - achievements: int = Aliased("achievement_number", mi18n="bbs/achievment_complete_count") + active_days: int = Aliased("active_day_number") + achievements: int = Aliased("achievement_number") - battlesuits: int = Aliased("armor_number", mi18n="bbs/armor_number") - battlesuits_SSS: int = Aliased("sss_armor_number", mi18n="bbs/sss_armor_number") - stigmata: int = Aliased("stigmata_number", mi18n="bbs/stigmata_number") - stigmata_5star: int = Aliased("five_star_stigmata_number", mi18n="bbs/stigmata_number_5") - weapons: int = Aliased("weapon_number", mi18n="bbs/weapon_number") - weapons_5star: int = Aliased("five_star_weapon_number", mi18n="bbs/weapon_number_5") - outfits: int = Aliased("suit_number", mi18n="bbs/suit_number") - # fmt: on + battlesuits: int = Aliased("armor_number") + battlesuits_SSS: int = Aliased("sss_armor_number") + stigmata: int = Aliased("stigmata_number") + stigmata_5star: int = Aliased("five_star_stigmata_number") + weapons: int = Aliased("weapon_number") + weapons_5star: int = Aliased("five_star_weapon_number") + outfits: int = Aliased("suit_number") - abyss: typing.Union[SuperstringAbyssStats, OldAbyssStats] = Aliased(mi18n="bbs/explain_text_1") - memorial_arena: MemorialArenaStats = Aliased(mi18n="bbs/battle_field_ranking_percentage") - elysian_realm: ElysianRealmStats = Aliased(mi18n="bbs/godwor") + abyss: typing.Union[SuperstringAbyssStats, OldAbyssStats] = Aliased() + memorial_arena: MemorialArenaStats = Aliased() + elysian_realm: ElysianRealmStats = Aliased() @pydantic.root_validator(pre=True) def __pack_gamemode_stats(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: @@ -239,10 +122,6 @@ def __pack_gamemode_stats(cls, values: typing.Dict[str, typing.Any]) -> typing.D return values - def as_dict(self, lang: str = "en-us") -> typing.Mapping[str, typing.Any]: - """Turn fields into properly named ones.""" - return _model_to_dict(self, lang) - class HonkaiUserStats(APIModel): """Represents basic user stats, showing only generic user data and stats.""" @@ -263,13 +142,3 @@ class FullHonkaiUserStats(HonkaiUserStats): def abyss_superstring(self) -> typing.Sequence[modes.SuperstringAbyss]: """Filter `self.abyss` to only return instances of Superstring Abyss.""" return [entry for entry in self.abyss if isinstance(entry, modes.SuperstringAbyss)] - - @property - def abyss_q_singularis(self) -> typing.Sequence[modes.OldAbyss]: - """Filter `self.abyss` to only return instances of Q-Singularis.""" - return [entry for entry in self.abyss if isinstance(entry, modes.OldAbyss) and entry.type == "Q-Singularis"] - - @property - def abyss_dirac_sea(self) -> typing.Sequence[modes.OldAbyss]: - """Filter `self.abyss` to only return instances of Dirac Sea.""" - return [entry for entry in self.abyss if isinstance(entry, modes.OldAbyss) and entry.type == "Dirac Sea"] diff --git a/genshin/models/model.py b/genshin/models/model.py index a33b755d..9443cedd 100644 --- a/genshin/models/model.py +++ b/genshin/models/model.py @@ -8,7 +8,6 @@ if typing.TYPE_CHECKING: import pydantic.v1 as pydantic - from pydantic.v1.fields import ModelField else: try: import pydantic.v1 as pydantic @@ -24,8 +23,6 @@ class APIModel(pydantic.BaseModel, abc.ABC): """Modified pydantic model.""" - _mi18n: typing.ClassVar[typing.Dict[str, typing.Dict[str, str]]] = {} - @pydantic.root_validator() def __parse_timezones(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: """Timezones are a pain to deal with so we at least allow a plain hour offset.""" @@ -56,32 +53,6 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: return super().dict(**kwargs) - def _get_mi18n( - self, - field: typing.Union[ModelField, str], - lang: str, - *, - default: typing.Optional[str] = None, - ) -> str: - """Get localized name of a field.""" - if isinstance(field, str): - key = field.lower() - default = default or key - else: - if not field.field_info.extra.get("mi18n"): - raise TypeError(f"{field!r} does not have mi18n.") - - key = field.field_info.extra["mi18n"] - default = default or field.name - - if key not in self._mi18n: - return default - - if lang not in self._mi18n[key]: - raise TypeError(f"mi18n not loaded for {lang}") - - return self._mi18n[key][lang] - if not typing.TYPE_CHECKING: class Config: @@ -105,13 +76,10 @@ def Aliased( default: typing.Any = pydantic.main.Undefined, # type: ignore *, timezone: typing.Optional[typing.Union[int, datetime.datetime]] = None, - mi18n: typing.Optional[str] = None, **kwargs: typing.Any, ) -> typing.Any: """Create an aliased field.""" if timezone is not None: kwargs.update(timezone=timezone) - if mi18n is not None: - kwargs.update(mi18n=mi18n) return pydantic.Field(default, alias=alias, **kwargs) From 9f0dcb19283c33046a5c8ae586cf858dd2c327f5 Mon Sep 17 00:00:00 2001 From: seriaati Date: Wed, 18 Sep 2024 22:56:06 +0800 Subject: [PATCH 02/21] Fix typing error in ERRORS --- genshin/errors.py | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/genshin/errors.py b/genshin/errors.py index acda773f..df1a9d7c 100644 --- a/genshin/errors.py +++ b/genshin/errors.py @@ -34,11 +34,7 @@ class GenshinException(Exception): original: str = "" msg: str = "" - def __init__( - self, - response: typing.Mapping[str, typing.Any] = {}, - msg: typing.Optional[str] = None, - ) -> None: + def __init__(self, response: typing.Mapping[str, typing.Any] = {}, msg: typing.Optional[str] = None) -> None: self.retcode = response.get("retcode", self.retcode) self.original = response.get("message", "") self.msg = msg or self.msg or self.original @@ -249,10 +245,7 @@ class VerificationCodeRateLimited(GenshinException): # database game record 10101: TooManyRequests, 10102: DataNotPublic, - 10103: ( - InvalidCookies, - "Cookies are valid but do not have a hoyolab account bound to them.", - ), + 10103: (InvalidCookies, "Cookies are valid but do not have a hoyolab account bound to them."), 10104: "Cannot view real-time notes of other users.", # calculator -500001: "Invalid fields in calculation.", @@ -273,10 +266,7 @@ class VerificationCodeRateLimited(GenshinException): -2016: RedemptionCooldown, -2017: RedemptionClaimed, -2018: RedemptionClaimed, - -2021: ( - RedemptionException, - "Cannot claim codes for accounts with adventure rank lower than 10.", - ), + -2021: (RedemptionException, "Cannot claim codes for accounts with adventure rank lower than 10."), # rewards -5003: AlreadyClaimed, # chinese @@ -296,10 +286,14 @@ class VerificationCodeRateLimited(GenshinException): -202: IncorrectGamePassword, } -ERRORS: typing.Dict[int, typing.Tuple[_TGE, typing.Optional[str]]] = { - retcode: ((exc, None) if isinstance(exc, type) else (GenshinException, exc) if isinstance(exc, str) else exc) - for retcode, exc in _errors.items() -} +ERRORS: typing.Dict[int, typing.Tuple[_TGE, typing.Optional[str]]] = {} +for retcode, exc in _errors.items(): + if isinstance(exc, str): + ERRORS[retcode] = (GenshinException, exc) + elif isinstance(exc, tuple): + ERRORS[retcode] = exc + else: + ERRORS[retcode] = (exc, None) def raise_for_retcode(data: typing.Dict[str, typing.Any]) -> typing.NoReturn: From 13cb00dedb0e6230c9574126205b24363650a74d Mon Sep 17 00:00:00 2001 From: seriaati Date: Wed, 18 Sep 2024 22:56:40 +0800 Subject: [PATCH 03/21] Reformat code --- genshin/client/cache.py | 2 +- genshin/client/components/hoyolab.py | 2 +- genshin/models/genshin/chronicle/img_theater.py | 4 ++-- genshin/models/genshin/gacha.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/genshin/client/cache.py b/genshin/client/cache.py index 2468d83c..e31743fe 100644 --- a/genshin/client/cache.py +++ b/genshin/client/cache.py @@ -15,7 +15,7 @@ import aiosqlite -__all__ = ["BaseCache", "Cache", "RedisCache", "StaticCache", "SQLiteCache"] +__all__ = ["BaseCache", "Cache", "RedisCache", "SQLiteCache", "StaticCache"] MINUTE = 60 HOUR = MINUTE * 60 diff --git a/genshin/client/components/hoyolab.py b/genshin/client/components/hoyolab.py index 9fd7623e..3df6e2e4 100644 --- a/genshin/client/components/hoyolab.py +++ b/genshin/client/components/hoyolab.py @@ -227,7 +227,7 @@ async def redeem_code( game_biz=utility.get_prod_game_biz(self.region, game), lang=utility.create_short_lang_code(lang or self.lang), ), - method="POST" if game is types.Game.STARRAIL else "GET" + method="POST" if game is types.Game.STARRAIL else "GET", ) @managers.no_multi diff --git a/genshin/models/genshin/chronicle/img_theater.py b/genshin/models/genshin/chronicle/img_theater.py index f8edb30d..3ff0e2a2 100644 --- a/genshin/models/genshin/chronicle/img_theater.py +++ b/genshin/models/genshin/chronicle/img_theater.py @@ -17,15 +17,15 @@ __all__ = ( "Act", "ActCharacter", + "BattleStatCharacter", "ImgTheater", "ImgTheaterData", + "TheaterBattleStats", "TheaterBuff", "TheaterCharaType", "TheaterDifficulty", "TheaterSchedule", "TheaterStats", - "TheaterBattleStats", - "BattleStatCharacter", ) diff --git a/genshin/models/genshin/gacha.py b/genshin/models/genshin/gacha.py index 1970d437..50d205ac 100644 --- a/genshin/models/genshin/gacha.py +++ b/genshin/models/genshin/gacha.py @@ -21,8 +21,8 @@ "BannerDetailsUpItem", "GachaItem", "GenshinBannerType", - "StarRailBannerType", "SignalSearch", + "StarRailBannerType", "Warp", "Wish", "ZZZBannerType", From dce113f0bcfc02855ec2d5ea8cf78567f078e0d5 Mon Sep 17 00:00:00 2001 From: seriaati Date: Wed, 18 Sep 2024 23:02:02 +0800 Subject: [PATCH 04/21] Remove timezone stuff --- genshin/models/genshin/daily.py | 9 +++++++- genshin/models/genshin/diary.py | 17 ++++++++++++-- genshin/models/model.py | 41 --------------------------------- 3 files changed, 23 insertions(+), 44 deletions(-) diff --git a/genshin/models/genshin/daily.py b/genshin/models/genshin/daily.py index 3d08e881..94a409fe 100644 --- a/genshin/models/genshin/daily.py +++ b/genshin/models/genshin/daily.py @@ -3,6 +3,8 @@ import datetime import typing +import pydantic + from genshin.constants import CN_TIMEZONE from genshin.models.model import Aliased, APIModel, Unique @@ -36,4 +38,9 @@ class ClaimedDailyReward(APIModel, Unique): name: str amount: int = Aliased("cnt") icon: str = Aliased("img") - time: datetime.datetime = Aliased("created_at", timezone=8) + time: datetime.datetime = Aliased("created_at") + + @pydantic.field_validator("time") + @classmethod + def __add_timezone(cls, value: datetime.datetime) -> datetime.datetime: + return value.replace(tzinfo=CN_TIMEZONE) diff --git a/genshin/models/genshin/diary.py b/genshin/models/genshin/diary.py index afe27314..54a44a6c 100644 --- a/genshin/models/genshin/diary.py +++ b/genshin/models/genshin/diary.py @@ -4,6 +4,9 @@ import enum import typing +import pydantic + +from genshin.constants import CN_TIMEZONE from genshin.models.model import Aliased, APIModel __all__ = [ @@ -88,9 +91,14 @@ class DiaryAction(APIModel): action_id: int action: str - time: datetime.datetime = Aliased(timezone=8) + time: datetime.datetime amount: int = Aliased("num") + @pydantic.field_validator("time") + @classmethod + def __add_timezone(cls, value: datetime.datetime) -> datetime.datetime: + return value.replace(tzinfo=CN_TIMEZONE) + class DiaryPage(BaseDiary): """Page of a diary.""" @@ -154,9 +162,14 @@ class StarRailDiaryAction(APIModel): action: str action_name: str - time: datetime.datetime = Aliased(timezone=8) + time: datetime.datetime amount: int = Aliased("num") + @pydantic.field_validator("time") + @classmethod + def __add_timezone(cls, value: datetime.datetime) -> datetime.datetime: + return value.replace(tzinfo=CN_TIMEZONE) + class StarRailDiaryPage(BaseDiary): """Page of a diary.""" diff --git a/genshin/models/model.py b/genshin/models/model.py index 9443cedd..37fbf29e 100644 --- a/genshin/models/model.py +++ b/genshin/models/model.py @@ -3,7 +3,6 @@ from __future__ import annotations import abc -import datetime import typing if typing.TYPE_CHECKING: @@ -23,41 +22,6 @@ class APIModel(pydantic.BaseModel, abc.ABC): """Modified pydantic model.""" - @pydantic.root_validator() - def __parse_timezones(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: - """Timezones are a pain to deal with so we at least allow a plain hour offset.""" - for name, field in cls.__fields__.items(): - if isinstance(values.get(name), datetime.datetime) and values[name].tzinfo is None: - timezone = field.field_info.extra.get("timezone", 0) - if not isinstance(timezone, datetime.timezone): - timezone = datetime.timezone(datetime.timedelta(hours=timezone)) - - values[name] = values[name].replace(tzinfo=timezone) - - return values - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - """Generate a dictionary representation of the model. - - Takes the liberty of also giving properties as fields. - """ - for name in dir(type(self)): - obj = getattr(type(self), name) - if isinstance(obj, property): - value = getattr(self, name, _SENTINEL) - - if name[0] == "_" or value is _SENTINEL or value == "": - continue - - self.__dict__[name] = value - - return super().dict(**kwargs) - - if not typing.TYPE_CHECKING: - - class Config: - allow_mutation = False - class Unique(abc.ABC): """A hashable model with an id.""" @@ -74,12 +38,7 @@ def __hash__(self) -> int: def Aliased( alias: typing.Optional[str] = None, default: typing.Any = pydantic.main.Undefined, # type: ignore - *, - timezone: typing.Optional[typing.Union[int, datetime.datetime]] = None, **kwargs: typing.Any, ) -> typing.Any: """Create an aliased field.""" - if timezone is not None: - kwargs.update(timezone=timezone) - return pydantic.Field(default, alias=alias, **kwargs) From a0bb6fa10ab0196928e97fb0d0b0fd487e33a966 Mon Sep 17 00:00:00 2001 From: seriaati Date: Wed, 18 Sep 2024 23:07:40 +0800 Subject: [PATCH 05/21] Fix comparing timezone aware dt to timezone naive dt --- tests/client/components/test_daily.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/client/components/test_daily.py b/tests/client/components/test_daily.py index c4fab65b..04fea594 100644 --- a/tests/client/components/test_daily.py +++ b/tests/client/components/test_daily.py @@ -58,4 +58,4 @@ async def test_monthly_rewards(lclient: genshin.Client): async def test_claimed_rewards(lclient: genshin.Client): claimed = await lclient.claimed_rewards(limit=10).flatten() - assert claimed[0].time <= datetime.datetime.now().astimezone() + assert claimed[0].time <= datetime.datetime.now(genshin.constants.CN_TIMEZONE) From 9099c650523da7eb880f2ab7a27d5a52547a81dc Mon Sep 17 00:00:00 2001 From: seriaati Date: Wed, 18 Sep 2024 23:07:49 +0800 Subject: [PATCH 06/21] Attempt to fix type check error --- genshin/errors.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/genshin/errors.py b/genshin/errors.py index df1a9d7c..71724114 100644 --- a/genshin/errors.py +++ b/genshin/errors.py @@ -286,14 +286,10 @@ class VerificationCodeRateLimited(GenshinException): -202: IncorrectGamePassword, } -ERRORS: typing.Dict[int, typing.Tuple[_TGE, typing.Optional[str]]] = {} -for retcode, exc in _errors.items(): - if isinstance(exc, str): - ERRORS[retcode] = (GenshinException, exc) - elif isinstance(exc, tuple): - ERRORS[retcode] = exc - else: - ERRORS[retcode] = (exc, None) +ERRORS: typing.Dict[int, typing.Tuple[_TGE, typing.Optional[str]]] = { + retcode: (GenshinException, exc) if isinstance(exc, str) else exc if isinstance(exc, tuple) else (exc, None) + for retcode, exc in _errors.items() +} def raise_for_retcode(data: typing.Dict[str, typing.Any]) -> typing.NoReturn: From 99d99fbe1950fb1f27365e64170ccb4536d9d7aa Mon Sep 17 00:00:00 2001 From: seriaati Date: Thu, 19 Sep 2024 08:42:04 +0800 Subject: [PATCH 07/21] Fix timezone issue in ClaimedDailyReward --- genshin/models/genshin/daily.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/genshin/models/genshin/daily.py b/genshin/models/genshin/daily.py index 94a409fe..73d86a16 100644 --- a/genshin/models/genshin/daily.py +++ b/genshin/models/genshin/daily.py @@ -3,7 +3,7 @@ import datetime import typing -import pydantic +import pydantic.v1 as pydantic from genshin.constants import CN_TIMEZONE from genshin.models.model import Aliased, APIModel, Unique @@ -40,7 +40,6 @@ class ClaimedDailyReward(APIModel, Unique): icon: str = Aliased("img") time: datetime.datetime = Aliased("created_at") - @pydantic.field_validator("time") - @classmethod + @pydantic.validator("time") def __add_timezone(cls, value: datetime.datetime) -> datetime.datetime: return value.replace(tzinfo=CN_TIMEZONE) From dc9662d3fec73da7528a564ba437a33552087e1b Mon Sep 17 00:00:00 2001 From: seriaati Date: Thu, 19 Sep 2024 11:22:41 +0800 Subject: [PATCH 08/21] Do mass find and replace --- genshin/__main__.py | 6 +- genshin/client/components/auth/client.py | 2 +- genshin/client/components/auth/server.py | 2 +- .../client/components/chronicle/genshin.py | 2 +- genshin/client/components/chronicle/honkai.py | 2 +- genshin/models/auth/cookie.py | 16 ++--- genshin/models/auth/geetest.py | 18 ++---- genshin/models/auth/qrcode.py | 11 +--- genshin/models/auth/responses.py | 8 +-- genshin/models/auth/verification.py | 12 +--- genshin/models/genshin/calculator.py | 18 ++---- genshin/models/genshin/character.py | 18 ++---- genshin/models/genshin/chronicle/abyss.py | 12 +--- .../models/genshin/chronicle/activities.py | 50 ++++++++-------- .../models/genshin/chronicle/characters.py | 16 ++--- .../models/genshin/chronicle/img_theater.py | 16 ++--- genshin/models/genshin/chronicle/notes.py | 14 ++--- genshin/models/genshin/chronicle/stats.py | 22 +++---- genshin/models/genshin/chronicle/tcg.py | 10 +--- genshin/models/genshin/daily.py | 4 +- genshin/models/genshin/gacha.py | 30 ++++------ genshin/models/genshin/lineup.py | 60 +++++++++---------- genshin/models/genshin/teapot.py | 12 +--- genshin/models/genshin/wiki.py | 26 +++----- genshin/models/honkai/battlesuit.py | 19 ++---- .../models/honkai/chronicle/battlesuits.py | 16 +---- genshin/models/honkai/chronicle/modes.py | 16 ++--- genshin/models/honkai/chronicle/stats.py | 18 ++---- genshin/models/hoyolab/record.py | 10 +--- genshin/models/model.py | 17 ++---- .../models/starrail/chronicle/challenge.py | 14 ++--- .../models/starrail/chronicle/characters.py | 11 +--- genshin/models/zzz/character.py | 12 +--- genshin/models/zzz/chronicle/challenge.py | 18 ++---- genshin/models/zzz/chronicle/notes.py | 16 ++--- tests/models/test_model.py | 4 +- 36 files changed, 186 insertions(+), 372 deletions(-) diff --git a/genshin/__main__.py b/genshin/__main__.py index c27266b7..7a422299 100644 --- a/genshin/__main__.py +++ b/genshin/__main__.py @@ -82,7 +82,7 @@ async def honkai_stats(client: genshin.Client, uid: int) -> None: data = await client.get_honkai_user(uid) click.secho("Stats:", fg="yellow") - for k, v in data.stats.dict().items(): + for k, v in data.stats.model_dump().items(): if isinstance(v, dict): click.echo(f"{k}:") for nested_k, nested_v in typing.cast("typing.Dict[str, object]", v).items(): @@ -102,7 +102,7 @@ async def genshin_stats(client: genshin.Client, uid: int) -> None: data = await client.get_partial_genshin_user(uid) click.secho("Stats:", fg="yellow") - for k, v in data.stats.dict().items(): + for k, v in data.stats.model_dump().items(): value = click.style(str(v), bold=True) click.echo(f"{k}: {value}") @@ -335,7 +335,7 @@ async def login(account: str, password: str, port: int) -> None: """Login with a password.""" client = genshin.Client() result = await client.os_login_with_password(account, password, port=port) - cookies = await genshin.complete_cookies(result.dict()) + cookies = await genshin.complete_cookies(result.model_dump()) base: http.cookies.BaseCookie[str] = http.cookies.BaseCookie(cookies) click.echo(f"Your cookies are: {click.style(base.output(header='', sep=';'), bold=True)}") diff --git a/genshin/client/components/auth/client.py b/genshin/client/components/auth/client.py index 3f480338..2454f045 100644 --- a/genshin/client/components/auth/client.py +++ b/genshin/client/components/auth/client.py @@ -297,7 +297,7 @@ async def verify_mmt(self, mmt_result: MMTResult) -> None: **auth_utility.CREATE_MMT_HEADERS[self.region], } - body = mmt_result.dict() + body = mmt_result.model_dump() body["app_key"] = constants.GEETEST_RECORD_KEYS[self.default_game] assert isinstance(self.cookie_manager, managers.CookieManager) diff --git a/genshin/client/components/auth/server.py b/genshin/client/components/auth/server.py index d6ef8248..7baf267d 100644 --- a/genshin/client/components/auth/server.py +++ b/genshin/client/components/auth/server.py @@ -159,7 +159,7 @@ async def gt(request: web.Request) -> web.StreamResponse: @routes.get("/mmt") async def mmt_endpoint(request: web.Request) -> web.Response: - return web.json_response(mmt.dict() if mmt else {}) + return web.json_response(mmt.model_dump() if mmt else {}) @routes.post("/send-data") async def send_data_endpoint(request: web.Request) -> web.Response: diff --git a/genshin/client/components/chronicle/genshin.py b/genshin/client/components/chronicle/genshin.py index 514052fb..761cd55e 100644 --- a/genshin/client/components/chronicle/genshin.py +++ b/genshin/client/components/chronicle/genshin.py @@ -260,7 +260,7 @@ async def get_full_genshin_user( ) abyss = models.SpiralAbyssPair(current=abyss1, previous=abyss2) - return models.FullGenshinUserStats(**user.dict(by_alias=True), abyss=abyss, activities=activities) + return models.FullGenshinUserStats(**user.model_dump(by_alias=True), abyss=abyss, activities=activities) async def set_top_genshin_characters( self, diff --git a/genshin/client/components/chronicle/honkai.py b/genshin/client/components/chronicle/honkai.py index 47c8c25d..2746e371 100644 --- a/genshin/client/components/chronicle/honkai.py +++ b/genshin/client/components/chronicle/honkai.py @@ -146,7 +146,7 @@ async def get_full_honkai_user( ) return models.FullHonkaiUserStats( - **user.dict(by_alias=True), + **user.model_dump(by_alias=True), battlesuits=battlesuits, abyss=abyss, memorial_arena=mr, diff --git a/genshin/models/auth/cookie.py b/genshin/models/auth/cookie.py index 229538dc..9c0ef4ba 100644 --- a/genshin/models/auth/cookie.py +++ b/genshin/models/auth/cookie.py @@ -2,13 +2,7 @@ import typing -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic __all__ = [ "AppLoginResult", @@ -31,7 +25,7 @@ class StokenResult(pydantic.BaseModel): mid: str token: str - @pydantic.root_validator(pre=True) + @pydantic.model_validator(mode="before") def _transform_result(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: return { "aid": values["user_info"]["aid"], @@ -45,11 +39,11 @@ class CookieLoginResult(pydantic.BaseModel): def to_str(self) -> str: """Convert the login cookies to a string.""" - return "; ".join(f"{key}={value}" for key, value in self.dict().items()) + return "; ".join(f"{key}={value}" for key, value in self.model_dump().items()) def to_dict(self) -> typing.Dict[str, str]: """Convert the login cookies to a dictionary.""" - return self.dict() + return self.model_dump() class QRLoginResult(CookieLoginResult): @@ -126,7 +120,7 @@ class DeviceGrantResult(pydantic.BaseModel): game_token: str login_ticket: typing.Optional[str] = None - @pydantic.root_validator(pre=True) + @pydantic.model_validator(mode="before") def _str_to_none(cls, data: typing.Dict[str, typing.Union[str, None]]) -> typing.Dict[str, typing.Union[str, None]]: """Convert empty strings to `None`.""" for key in data: diff --git a/genshin/models/auth/geetest.py b/genshin/models/auth/geetest.py index bfa7464c..1340f3a5 100644 --- a/genshin/models/auth/geetest.py +++ b/genshin/models/auth/geetest.py @@ -4,13 +4,7 @@ import json import typing -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic from genshin.utility import auth as auth_utility @@ -37,7 +31,7 @@ class BaseMMT(pydantic.BaseModel): new_captcha: int success: int - @pydantic.root_validator(pre=True) + @pydantic.model_validator(mode="before") def __parse_data(cls, data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: """Parse the data if it was provided in a raw format.""" if "data" in data: @@ -66,7 +60,7 @@ class SessionMMT(MMT): def get_mmt(self) -> MMT: """Get the base MMT data.""" - return MMT(**self.dict(exclude={"session_id"})) + return MMT(**self.model_dump(exclude={"session_id"})) class MMTv4(BaseMMT): @@ -83,7 +77,7 @@ class SessionMMTv4(MMTv4): def get_mmt(self) -> MMTv4: """Get the base MMTv4 data.""" - return MMTv4(**self.dict(exclude={"session_id"})) + return MMTv4(**self.model_dump(exclude={"session_id"})) class RiskyCheckMMT(MMT): @@ -100,7 +94,7 @@ def get_data(self) -> typing.Dict[str, typing.Any]: This method acts as `dict` but excludes the `session_id` field. """ - return self.dict(exclude={"session_id"}) + return self.model_dump(exclude={"session_id"}) class BaseSessionMMTResult(BaseMMTResult): @@ -170,4 +164,4 @@ def to_mmt(self) -> RiskyCheckMMT: if self.mmt is None: raise ValueError("The check result does not contain a MMT object.") - return RiskyCheckMMT(**self.mmt.dict(), check_id=self.id) + return RiskyCheckMMT(**self.mmt.model_dump(), check_id=self.id) diff --git a/genshin/models/auth/qrcode.py b/genshin/models/auth/qrcode.py index d04c4801..c47f2aa1 100644 --- a/genshin/models/auth/qrcode.py +++ b/genshin/models/auth/qrcode.py @@ -1,15 +1,8 @@ """Miyoushe QR Code Models""" import enum -import typing - -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic + +import pydantic __all__ = ["QRCodeCreationResult", "QRCodeStatus"] diff --git a/genshin/models/auth/responses.py b/genshin/models/auth/responses.py index dd347e0a..31b99fb1 100644 --- a/genshin/models/auth/responses.py +++ b/genshin/models/auth/responses.py @@ -2,13 +2,7 @@ import typing -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic __all__ = ["Account", "ShieldLoginResponse"] diff --git a/genshin/models/auth/verification.py b/genshin/models/auth/verification.py index 0f207410..5d51a573 100644 --- a/genshin/models/auth/verification.py +++ b/genshin/models/auth/verification.py @@ -3,13 +3,7 @@ import json import typing -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic __all__ = [ "ActionTicket", @@ -30,7 +24,7 @@ class ActionTicket(pydantic.BaseModel): risk_ticket: str verify_str: VerifyStrategy - @pydantic.root_validator(pre=True) + @pydantic.model_validator(mode="before") def __parse_data(cls, data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: """Parse the data if it was provided in a raw format.""" verify_str = data["verify_str"] @@ -41,6 +35,6 @@ def __parse_data(cls, data: typing.Dict[str, typing.Any]) -> typing.Dict[str, ty def to_rpc_verify_header(self) -> str: """Convert the action ticket to `x-rpc-verify` header.""" - ticket = self.dict() + ticket = self.model_dump() ticket["verify_str"] = json.dumps(ticket["verify_str"]) return json.dumps(ticket) diff --git a/genshin/models/genshin/calculator.py b/genshin/models/genshin/calculator.py index d837da39..c9945c44 100644 --- a/genshin/models/genshin/calculator.py +++ b/genshin/models/genshin/calculator.py @@ -5,13 +5,7 @@ import collections import typing -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic from genshin.models.model import Aliased, APIModel, Unique @@ -67,14 +61,14 @@ class CalculatorCharacter(character.BaseCharacter): level: int = Aliased("level_current", default=0) max_level: int - @pydantic.validator("element", pre=True) + @pydantic.field_validator("element", mode="before") def __parse_element(cls, v: typing.Any) -> str: if isinstance(v, str): return v return CALCULATOR_ELEMENTS[int(v)] - @pydantic.validator("weapon_type", pre=True) + @pydantic.field_validator("weapon_type", mode="before") def __parse_weapon_type(cls, v: typing.Any) -> str: if isinstance(v, str): return v @@ -93,7 +87,7 @@ class CalculatorWeapon(APIModel, Unique): level: int = Aliased("level_current", default=0) max_level: int - @pydantic.validator("type", pre=True) + @pydantic.field_validator("type", mode="before") def __parse_weapon_type(cls, v: typing.Any) -> str: if isinstance(v, str): return v @@ -184,14 +178,14 @@ class CalculatorCharacterDetails(APIModel): talents: typing.Sequence[CalculatorTalent] = Aliased("skill_list") artifacts: typing.Sequence[CalculatorArtifact] = Aliased("reliquary_list") - @pydantic.validator("talents") + @pydantic.field_validator("talents") def __correct_talent_current_level(cls, v: typing.Sequence[CalculatorTalent]) -> typing.Sequence[CalculatorTalent]: # passive talent have current levels at 0 for some reason talents: typing.List[CalculatorTalent] = [] for talent in v: if talent.max_level == 1 and talent.level == 0: - raw = talent.dict() + raw = talent.model_dump() raw["level"] = 1 talent = CalculatorTalent(**raw) diff --git a/genshin/models/genshin/character.py b/genshin/models/genshin/character.py index 3cbf2eaa..b0919273 100644 --- a/genshin/models/genshin/character.py +++ b/genshin/models/genshin/character.py @@ -4,17 +4,10 @@ import re import typing -from genshin.utility import deprecation - -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic from genshin.models.model import APIModel, Unique +from genshin.utility import deprecation from . import constants @@ -136,11 +129,12 @@ class BaseCharacter(APIModel, Unique): collab: bool = False - @pydantic.root_validator(pre=True) + @pydantic.model_validator(mode="before") def __autocomplete(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: """Complete missing data.""" - all_fields = list(cls.__fields__.keys()) - all_aliases = {f: cls.__fields__[f].alias for f in all_fields if cls.__fields__[f].alias} + all_fields = list(cls.model_fields.keys()) + all_aliases = {f: cls.model_fields[f].alias for f in all_fields if cls.model_fields[f].alias} + all_aliases = {k: v for k, v in all_aliases.items() if v is not None} # If the field is aliased, it may have a different key name in 'values', # so we need to get the correct key name from the alias diff --git a/genshin/models/genshin/chronicle/abyss.py b/genshin/models/genshin/chronicle/abyss.py index d0b7c0f4..d81fd681 100644 --- a/genshin/models/genshin/chronicle/abyss.py +++ b/genshin/models/genshin/chronicle/abyss.py @@ -1,13 +1,7 @@ import datetime import typing -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic from genshin.constants import CN_TIMEZONE from genshin.models.genshin import character @@ -98,13 +92,13 @@ class SpiralAbyss(APIModel): floors: typing.Sequence[Floor] - @pydantic.root_validator(pre=True) + @pydantic.model_validator(mode="before") def __nest_ranks(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, AbyssCharacter]: """By default ranks are for some reason on the same level as the rest of the abyss.""" values.setdefault("ranks", {}).update(values) return values - @pydantic.validator("start_time", "end_time", pre=True) + @pydantic.field_validator("start_time", "end_time", mode="before") def __parse_timezones(cls, value: str) -> datetime.datetime: return datetime.datetime.fromtimestamp(int(value), tz=CN_TIMEZONE) diff --git a/genshin/models/genshin/chronicle/activities.py b/genshin/models/genshin/chronicle/activities.py index 9f7ecfa6..1655b713 100644 --- a/genshin/models/genshin/chronicle/activities.py +++ b/genshin/models/genshin/chronicle/activities.py @@ -4,17 +4,7 @@ import re import typing -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic - import pydantic.v1.generics as pydantic_generics -else: - try: - import pydantic.v1 as pydantic - import pydantic.v1.generics as pydantic_generics - except ImportError: - import pydantic - import pydantic.generics as pydantic_generics - +import pydantic from genshin.models.genshin import character from genshin.models.model import Aliased, APIModel @@ -33,7 +23,7 @@ ModelT = typing.TypeVar("ModelT", bound=APIModel) -class OldActivity(APIModel, pydantic_generics.GenericModel, typing.Generic[ModelT]): +class OldActivity(APIModel, typing.Generic[ModelT]): """Arbitrary activity for chinese events.""" # sometimes __parameters__ may not be provided in older versions @@ -80,7 +70,7 @@ class HyakuninIkkiBattle(APIModel): characters: typing.Sequence[HyakuninIkkiCharacter] = Aliased("avatars") skills: typing.Sequence[HyakuninIkkiSkill] = Aliased("skills") - @pydantic.validator("characters", pre=True) + @pydantic.field_validator("characters", mode="before") def __validate_characters(cls, value: typing.Sequence[typing.Any]) -> typing.Sequence[typing.Any]: """Remove characters with a null id.""" return [character for character in value if character["id"]] @@ -236,7 +226,7 @@ class SummerMemories(APIModel): icon: str name: str - @pydantic.validator("finish_time", pre=True) + @pydantic.field_validator("finish_time", mode="before") def __validate_time(cls, value: typing.Any) -> typing.Optional[datetime.datetime]: if value is None or isinstance(value, datetime.datetime): return value @@ -263,7 +253,7 @@ class SummerRealmExploration(APIModel): name: str icon: str - @pydantic.validator("finish_time", pre=True) + @pydantic.field_validator("finish_time", mode="before") def __validate_time(cls, value: typing.Any) -> typing.Optional[datetime.datetime]: if value is None or isinstance(value, datetime.datetime): return value @@ -282,7 +272,7 @@ class Summer(APIModel): memories: typing.Sequence[SummerMemories] = Aliased("story") realm_exploration: typing.Sequence[SummerRealmExploration] = Aliased("challenge") - @pydantic.validator("surfpiercer", "memories", "realm_exploration", pre=True) + @pydantic.field_validator("surfpiercer", "memories", "realm_exploration", mode="before") def __flatten_records(cls, value: typing.Any) -> typing.Sequence[typing.Any]: if isinstance(value, typing.Sequence): return typing.cast("typing.Sequence[object]", value) @@ -297,12 +287,20 @@ def __flatten_records(cls, value: typing.Any) -> typing.Sequence[typing.Any]: class Activities(APIModel): """Collection of genshin activities.""" - hyakunin_ikki_v21: typing.Optional[OldActivity[HyakuninIkki]] = pydantic.Field(None, gslug="sumo") - hyakunin_ikki_v25: typing.Optional[OldActivity[HyakuninIkki]] = pydantic.Field(None, gslug="sumo_second") - labyrinth_warriors: typing.Optional[OldActivity[LabyrinthWarriors]] = pydantic.Field(None, gslug="rogue") - energy_amplifier: typing.Optional[Activity[EnergyAmplifier]] = pydantic.Field(None, gslug="channeller_slab_copy") - study_in_potions: typing.Optional[OldActivity[Potion]] = pydantic.Field(None, gslug="potion") - summertime_odyssey: typing.Optional[Summer] = pydantic.Field(None, gslug="summer_v2") + hyakunin_ikki_v21: typing.Optional[OldActivity[HyakuninIkki]] = pydantic.Field( + None, json_schema_extra={"gslug": "sumo"} + ) + hyakunin_ikki_v25: typing.Optional[OldActivity[HyakuninIkki]] = pydantic.Field( + None, json_schema_extra={"gslug": "sumo_second"} + ) + labyrinth_warriors: typing.Optional[OldActivity[LabyrinthWarriors]] = pydantic.Field( + None, json_schema_extra={"gslug": "rogue"} + ) + energy_amplifier: typing.Optional[Activity[EnergyAmplifier]] = pydantic.Field( + None, json_schema_extra={"gslug": "channeller_slab_copy"} + ) + study_in_potions: typing.Optional[OldActivity[Potion]] = pydantic.Field(None, json_schema_extra={"gslug": "potion"}) + summertime_odyssey: typing.Optional[Summer] = pydantic.Field(None, json_schema_extra={"gslug": "summer_v2"}) effigy: typing.Optional[Activity[typing.Any]] = None mechanicus: typing.Optional[Activity[typing.Any]] = None @@ -311,15 +309,15 @@ class Activities(APIModel): martial_legend: typing.Optional[Activity[typing.Any]] = None chess: typing.Optional[Activity[typing.Any]] = None - @pydantic.root_validator(pre=True) + @pydantic.model_validator(mode="before") def __flatten_activities(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: if not values.get("activities"): return values slugs = { - field.field_info.extra["gslug"]: name - for name, field in cls.__fields__.items() - if field.field_info.extra.get("gslug") + field.json_schema_extra["gslug"]: name + for name, field in cls.model_fields.items() + if isinstance(field.json_schema_extra, dict) and field.json_schema_extra.get("gslug") } for activity in values["activities"]: diff --git a/genshin/models/genshin/chronicle/characters.py b/genshin/models/genshin/chronicle/characters.py index f4c427cf..1dba8ecf 100644 --- a/genshin/models/genshin/chronicle/characters.py +++ b/genshin/models/genshin/chronicle/characters.py @@ -4,13 +4,7 @@ import typing from collections import defaultdict -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic from genshin.models.genshin import character from genshin.models.model import Aliased, APIModel, Unique @@ -130,7 +124,7 @@ class Character(PartialCharacter): constellations: typing.Sequence[Constellation] outfits: typing.Sequence[Outfit] = Aliased("costumes") - @pydantic.validator("artifacts") + @pydantic.field_validator("artifacts") @classmethod def __enable_artifact_set_effects(cls, artifacts: typing.Sequence[Artifact]) -> typing.Sequence[Artifact]: set_nums: typing.DefaultDict[int, int] = defaultdict(int) @@ -141,7 +135,7 @@ def __enable_artifact_set_effects(cls, artifacts: typing.Sequence[Artifact]) -> for effect in artifact.set.effects: if effect.required_piece_num <= set_nums[artifact.set.id]: # To bypass model's immutability - effect = effect.copy(update={"active": True}) + effect = effect.model_copy(update={"active": True}) return artifacts @@ -154,7 +148,7 @@ class PropInfo(APIModel): icon: typing.Optional[str] filter_name: str - @pydantic.validator("name", "filter_name") + @pydantic.field_validator("name", "filter_name") @classmethod def __fix_names(cls, value: str) -> str: r"""Fix "\xa0" in Crit Damage + Crit Rate names.""" @@ -249,7 +243,7 @@ class GenshinDetailCharacters(APIModel): weapon_wiki: typing.Mapping[str, str] avatar_wiki: typing.Mapping[str, str] - @pydantic.root_validator(pre=True) + @pydantic.model_validator(mode="before") def __fill_prop_info(cls, values: typing.Dict[str, typing.Any]) -> typing.Mapping[str, typing.Any]: """Fill property info from properety_map.""" relic_property_options: typing.Dict[str, list[int]] = values.get("relic_property_options", {}) diff --git a/genshin/models/genshin/chronicle/img_theater.py b/genshin/models/genshin/chronicle/img_theater.py index 3ff0e2a2..575ba892 100644 --- a/genshin/models/genshin/chronicle/img_theater.py +++ b/genshin/models/genshin/chronicle/img_theater.py @@ -2,13 +2,7 @@ import enum import typing -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic from genshin.constants import CN_TIMEZONE from genshin.models.genshin import character @@ -76,7 +70,7 @@ class Act(APIModel): finish_time: int # As timestamp finish_datetime: datetime.datetime = Aliased("finish_date_time") - @pydantic.validator("finish_datetime", pre=True) + @pydantic.field_validator("finish_datetime", mode="before") def __parse_datetime(cls, value: typing.Mapping[str, typing.Any]) -> datetime.datetime: return datetime.datetime( year=value["year"], @@ -118,7 +112,7 @@ class TheaterSchedule(APIModel): start_datetime: datetime.datetime = Aliased("start_date_time") end_datetime: datetime.datetime = Aliased("end_date_time") - @pydantic.validator("start_datetime", "end_datetime", pre=True) + @pydantic.field_validator("start_datetime", "end_datetime", mode="before") def __parse_datetime(cls, value: typing.Mapping[str, typing.Any]) -> datetime.datetime: return datetime.datetime( year=value["year"], @@ -139,7 +133,7 @@ class BattleStatCharacter(APIModel): value: int rarity: int - @pydantic.validator("value", pre=True) + @pydantic.field_validator("value", mode="before") def __intify_value(cls, value: str) -> int: if not value: return 0 @@ -167,7 +161,7 @@ class ImgTheaterData(APIModel): has_detail_data: bool battle_stats: TheaterBattleStats = Aliased("fight_statisic", default=None) - @pydantic.root_validator(pre=True) + @pydantic.model_validator(mode="before") def __unnest_detail(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: detail: typing.Optional[typing.Dict[str, typing.Any]] = values.get("detail") values["rounds_data"] = detail.get("rounds_data", []) if detail is not None else [] diff --git a/genshin/models/genshin/chronicle/notes.py b/genshin/models/genshin/chronicle/notes.py index 1ea0db7a..a92357f7 100644 --- a/genshin/models/genshin/chronicle/notes.py +++ b/genshin/models/genshin/chronicle/notes.py @@ -4,13 +4,7 @@ import enum import typing -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic from genshin.models.model import Aliased, APIModel @@ -99,7 +93,7 @@ class TaskReward(APIModel): status: typing.Union[TaskRewardStatus, str] - @pydantic.validator("status", pre=True) + @pydantic.field_validator("status", mode="before") def __prevent_enum_crash(cls, v: str) -> typing.Union[TaskRewardStatus, str]: try: return TaskRewardStatus(v) @@ -122,7 +116,7 @@ class AttendanceReward(APIModel): status: typing.Union[AttendanceRewardStatus, str] progress: int - @pydantic.validator("status", pre=True) + @pydantic.field_validator("status", mode="before") def __prevent_enum_crash(cls, v: str) -> typing.Union[AttendanceRewardStatus, str]: try: return AttendanceRewardStatus(v) @@ -216,7 +210,7 @@ def transformer_recovery_time(self) -> typing.Optional[datetime.datetime]: remaining = datetime.datetime.now().astimezone() + self.remaining_transformer_recovery_time return remaining - @pydantic.root_validator(pre=True) + @pydantic.model_validator(mode="before") def __flatten_transformer(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: if "transformer_recovery_time" in values: return values diff --git a/genshin/models/genshin/chronicle/stats.py b/genshin/models/genshin/chronicle/stats.py index 809039c4..c69de5cb 100644 --- a/genshin/models/genshin/chronicle/stats.py +++ b/genshin/models/genshin/chronicle/stats.py @@ -3,13 +3,7 @@ import re import typing -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic from genshin.models import hoyolab from genshin.models.model import Aliased, APIModel @@ -126,12 +120,12 @@ def explored(self) -> float: """The percentage explored.""" return self.raw_explored / 10 - @pydantic.validator("offerings", pre=True) + @pydantic.field_validator("offerings", mode="before") def __add_base_offering( - cls, offerings: typing.Sequence[typing.Any], values: typing.Dict[str, typing.Any] + cls, offerings: typing.Sequence[typing.Any], info: pydantic.ValidationInfo ) -> typing.Sequence[typing.Any]: - if values["type"] == "Reputation" and not any(values["type"] == o["name"] for o in offerings): - offerings = [*offerings, dict(name=values["type"], level=values["level"])] + if info.data["type"] == "Reputation" and not any(info.data["type"] == o["name"] for o in offerings): + offerings = [*offerings, dict(name=info.data["type"], level=info.data["level"])] return offerings @@ -165,11 +159,11 @@ class PartialGenshinUserStats(APIModel): info: hoyolab.UserInfo = Aliased("role") stats: Stats - characters: typing.Sequence[characters.PartialCharacter] = Aliased("avatars") + characters: typing.Sequence["characters.PartialCharacter"] = Aliased("avatars") explorations: typing.Sequence[Exploration] = Aliased("world_explorations") teapot: typing.Optional[Teapot] = Aliased("homes") - @pydantic.validator("teapot", pre=True) + @pydantic.field_validator("teapot", mode="before") def __format_teapot(cls, v: typing.Any) -> typing.Optional[typing.Dict[str, typing.Any]]: if not v: return None @@ -181,7 +175,7 @@ def __format_teapot(cls, v: typing.Any) -> typing.Optional[typing.Dict[str, typi class GenshinUserStats(PartialGenshinUserStats): """User stats with characters with equipment""" - characters: typing.Sequence[characters.Character] = Aliased("avatars") + characters: typing.Sequence["characters.Character"] = Aliased("avatars") class FullGenshinUserStats(GenshinUserStats): diff --git a/genshin/models/genshin/chronicle/tcg.py b/genshin/models/genshin/chronicle/tcg.py index ea1ec1dc..739f7aee 100644 --- a/genshin/models/genshin/chronicle/tcg.py +++ b/genshin/models/genshin/chronicle/tcg.py @@ -5,13 +5,7 @@ import enum import typing -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic from genshin.models.model import Aliased, APIModel, Unique @@ -73,7 +67,7 @@ class TCGCost(APIModel): element: str = Aliased("cost_type") value: int = Aliased("cost_value") - @pydantic.validator("element") + @pydantic.field_validator("element") def __fix_element(cls, value: str) -> str: return { "CostTypeCryo": "Cryo", diff --git a/genshin/models/genshin/daily.py b/genshin/models/genshin/daily.py index 73d86a16..31a2bd57 100644 --- a/genshin/models/genshin/daily.py +++ b/genshin/models/genshin/daily.py @@ -3,7 +3,7 @@ import datetime import typing -import pydantic.v1 as pydantic +import pydantic from genshin.constants import CN_TIMEZONE from genshin.models.model import Aliased, APIModel, Unique @@ -40,6 +40,6 @@ class ClaimedDailyReward(APIModel, Unique): icon: str = Aliased("img") time: datetime.datetime = Aliased("created_at") - @pydantic.validator("time") + @pydantic.field_validator("time") def __add_timezone(cls, value: datetime.datetime) -> datetime.datetime: return value.replace(tzinfo=CN_TIMEZONE) diff --git a/genshin/models/genshin/gacha.py b/genshin/models/genshin/gacha.py index 50d205ac..e6acbdb8 100644 --- a/genshin/models/genshin/gacha.py +++ b/genshin/models/genshin/gacha.py @@ -5,13 +5,7 @@ import re import typing -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic from genshin.models.model import Aliased, APIModel, Unique @@ -99,10 +93,10 @@ class BaseWish(APIModel, Unique): time: datetime.datetime """Timezone-aware time of when the wish was made""" - @pydantic.validator("time", pre=True) - def __parse_time(cls, v: str, values: typing.Dict[str, typing.Any]) -> datetime.datetime: + @pydantic.field_validator("time", mode="before") + def __parse_time(cls, v: str, info: pydantic.ValidationInfo) -> datetime.datetime: return datetime.datetime.fromisoformat(v).replace( - tzinfo=datetime.timezone(datetime.timedelta(hours=8 + values["tz_offset"])) + tzinfo=datetime.timezone(datetime.timedelta(hours=8 + info.data["tz_offset"])) ) @@ -112,7 +106,7 @@ class Wish(BaseWish): type: str = Aliased("item_type") banner_type: GenshinBannerType - @pydantic.validator("banner_type", pre=True) + @pydantic.field_validator("banner_type", mode="before") def __cast_banner_type(cls, v: typing.Any) -> int: return int(v) @@ -126,7 +120,7 @@ class Warp(BaseWish): banner_type: StarRailBannerType banner_id: int = Aliased("gacha_id") - @pydantic.validator("banner_type", pre=True) + @pydantic.field_validator("banner_type", mode="before") def __cast_banner_type(cls, v: typing.Any) -> int: return int(v) @@ -139,7 +133,7 @@ class SignalSearch(BaseWish): banner_type: ZZZBannerType - @pydantic.validator("banner_type", pre=True) + @pydantic.field_validator("banner_type", mode="before") def __cast_banner_type(cls, v: typing.Any) -> int: return int(v) @@ -163,7 +157,7 @@ class BannerDetailsUpItem(APIModel): element: str = Aliased("item_attr") icon: str = Aliased("item_img") - @pydantic.validator("element", pre=True) + @pydantic.field_validator("element", mode="before") def __parse_element(cls, v: str) -> str: return { "风": "Anemo", @@ -202,11 +196,11 @@ class BannerDetails(APIModel): r4_items: typing.List[BannerDetailItem] = Aliased("r4_prob_list") r3_items: typing.List[BannerDetailItem] = Aliased("r3_prob_list") - @pydantic.validator("r5_up_items", "r4_up_items", pre=True) + @pydantic.field_validator("r5_up_items", "r4_up_items", mode="before") def __replace_none(cls, v: typing.Optional[typing.Sequence[typing.Any]]) -> typing.Sequence[typing.Any]: return v or [] - @pydantic.validator( + @pydantic.field_validator( "r5_up_prob", "r4_up_prob", "r5_prob", @@ -215,7 +209,7 @@ def __replace_none(cls, v: typing.Optional[typing.Sequence[typing.Any]]) -> typi "r5_guarantee_prob", "r4_guarantee_prob", "r3_guarantee_prob", - pre=True, + mode="before", ) def __parse_percentage(cls, v: typing.Optional[str]) -> typing.Optional[float]: if v is None or isinstance(v, (int, float)): @@ -252,7 +246,7 @@ class GachaItem(APIModel, Unique): rarity: int = Aliased("rank_type") id: int = Aliased("item_id") - @pydantic.validator("id") + @pydantic.field_validator("id") def __format_id(cls, v: int) -> int: return 10000000 + v - 1000 if len(str(v)) == 4 else v diff --git a/genshin/models/genshin/lineup.py b/genshin/models/genshin/lineup.py index 08ea3d0d..30521d69 100644 --- a/genshin/models/genshin/lineup.py +++ b/genshin/models/genshin/lineup.py @@ -5,13 +5,7 @@ import datetime import typing -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic from genshin.models.genshin import character from genshin.models.model import Aliased, APIModel, Unique @@ -55,7 +49,7 @@ def __init__(self, _frame: int = 1, **data: typing.Any) -> None: super().__init__(_frame=_frame + 3, **data) # type: ignore - @pydantic.validator("element", pre=True) + @pydantic.field_validator("element", mode="before") def __parse_element(cls, value: typing.Any) -> str: if isinstance(value, str) and not value.isdigit(): return value @@ -70,7 +64,7 @@ def __parse_element(cls, value: typing.Any) -> str: 7: "Cryo", }[int(value)] - @pydantic.validator("weapon_type", pre=True) + @pydantic.field_validator("weapon_type", mode="before") def __parse_weapon_type(cls, value: typing.Any) -> str: if isinstance(value, str) and not value.isdigit(): return value @@ -94,7 +88,7 @@ class PartialLineupWeapon(APIModel, Unique): rarity: int = Aliased("level") type: str = Aliased("cat_id") - @pydantic.validator("type", pre=True) + @pydantic.field_validator("type", mode="before") def __parse_weapon_type(cls, value: int) -> str: if isinstance(value, str) and not value.isdigit(): return value @@ -120,24 +114,24 @@ class PartialLineupArtifactSet(APIModel, Unique): class LineupArtifactStatFields(APIModel): """Lineup artifact stat fields.""" - flower: typing.Mapping[int, str] = pydantic.Field(artifact_id=1) - plume: typing.Mapping[int, str] = pydantic.Field(artifact_id=2) - sands: typing.Mapping[int, str] = pydantic.Field(artifact_id=3) - goblet: typing.Mapping[int, str] = pydantic.Field(artifact_id=4) - circlet: typing.Mapping[int, str] = pydantic.Field(artifact_id=5) + flower: typing.Mapping[int, str] = pydantic.Field(json_schema_extra={"artifact_id": 1}) + plume: typing.Mapping[int, str] = pydantic.Field(json_schema_extra={"artifact_id": 2}) + sands: typing.Mapping[int, str] = pydantic.Field(json_schema_extra={"artifact_id": 3}) + goblet: typing.Mapping[int, str] = pydantic.Field(json_schema_extra={"artifact_id": 4}) + circlet: typing.Mapping[int, str] = pydantic.Field(json_schema_extra={"artifact_id": 5}) secondary_stats: typing.Mapping[int, str] = Aliased("reliquary_sec_attr") - @pydantic.root_validator(pre=True) + @pydantic.model_validator(mode="before") def __flatten_stats(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: """Name certain stats.""" if "reliquary_fst_attr" not in values: return values artifact_ids = { - field.field_info.extra["artifact_id"]: name - for name, field in cls.__fields__.items() - if field.field_info.extra.get("artifact_id") + field.json_schema_extra["artifact_id"]: name + for name, field in cls.model_fields.items() + if isinstance(field.json_schema_extra, dict) and field.json_schema_extra.get("artifact_id") } for scenario in values["reliquary_fst_attr"]: @@ -149,7 +143,7 @@ def __flatten_stats(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[st return values - @pydantic.validator("secondary_stats", "flower", "plume", "sands", "goblet", "circlet", pre=True) + @pydantic.field_validator("secondary_stats", "flower", "plume", "sands", "goblet", "circlet", mode="before") def __parse_secondary_stats(cls, value: typing.Any) -> typing.Dict[int, str]: if not isinstance(value, typing.Sequence): return value @@ -192,13 +186,13 @@ class LineupScenario(APIModel, Unique): name: str children: typing.Sequence[LineupScenario] - @pydantic.root_validator(pre=True) + @pydantic.model_validator(mode="before") def __flatten_scenarios(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: """Name certain scenarios.""" scenario_ids = { - field.field_info.extra["scenario_id"]: name - for name, field in cls.__fields__.items() - if field.field_info.extra.get("scenario_id") + field.json_schema_extra["scenario_id"]: name + for name, field in cls.model_fields.items() + if isinstance(field.json_schema_extra, dict) and field.json_schema_extra.get("scenario_id") } for scenario in values["children"]: @@ -223,23 +217,23 @@ def all_children(self) -> typing.Sequence[LineupScenario]: class LineupWorldScenarios(LineupScenario): """Lineup world scenario.""" - trounce_domains: LineupScenario = pydantic.Field(scenario_id=3) - domain_challenges: LineupScenario = pydantic.Field(scenario_id=9) - battles: LineupScenario = pydantic.Field(scenario_id=24) + trounce_domains: LineupScenario = pydantic.Field(json_schema_extra={"scenario_id": 3}) + domain_challenges: LineupScenario = pydantic.Field(json_schema_extra={"scenario_id": 9}) + battles: LineupScenario = pydantic.Field(json_schema_extra={"scenario_id": 24}) class LineupAbyssScenarios(LineupScenario): """Lineup abyss scenario.""" - corridor: LineupScenario = pydantic.Field(scenario_id=42) - spire: LineupScenario = pydantic.Field(scenario_id=41) + corridor: LineupScenario = pydantic.Field(json_schema_extra={"scenario_id": 42}) + spire: LineupScenario = pydantic.Field(json_schema_extra={"scenario_id": 41}) class LineupScenarios(LineupScenario): """Lineup scenarios.""" - world: LineupWorldScenarios = pydantic.Field(scenario_id=1) - abyss: LineupAbyssScenarios = pydantic.Field(scenario_id=2) + world: LineupWorldScenarios = pydantic.Field(json_schema_extra={"scenario_id": 1}) + abyss: LineupAbyssScenarios = pydantic.Field(json_schema_extra={"scenario_id": 2}) class LineupCharacterPreview(PartialLineupCharacter): @@ -250,7 +244,7 @@ class LineupCharacterPreview(PartialLineupCharacter): icon: str = Aliased("standard_icon") pc_icon: str = Aliased("pc_icon") - @pydantic.validator("role", pre=True) + @pydantic.field_validator("role", mode="before") def __parse_role(cls, value: typing.Any) -> str: if isinstance(value, str): return value @@ -290,7 +284,7 @@ class LineupPreview(APIModel, Unique): original_lang: str = Aliased("trans_from") - @pydantic.validator("characters", pre=True) + @pydantic.field_validator("characters", mode="before") def __parse_characters(cls, value: typing.Any) -> typing.Any: if isinstance(value[0], typing.Sequence): return value diff --git a/genshin/models/genshin/teapot.py b/genshin/models/genshin/teapot.py index 9fb7eefc..766d2ae1 100644 --- a/genshin/models/genshin/teapot.py +++ b/genshin/models/genshin/teapot.py @@ -5,13 +5,7 @@ import datetime import typing -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic from genshin.models.model import Aliased, APIModel, Unique @@ -73,11 +67,11 @@ class TeapotReplica(APIModel, Unique): has_more_content: bool token: str - @pydantic.validator("images", pre=True) + @pydantic.field_validator("images", mode="before") def __extract_urls(cls, images: typing.Sequence[typing.Any]) -> typing.Sequence[str]: return [image if isinstance(image, str) else image["url"] for image in images] - @pydantic.validator("video", pre=True) + @pydantic.field_validator("video", mode="before") def __extract_url(cls, video: typing.Any) -> typing.Optional[str]: if isinstance(video, str): return video diff --git a/genshin/models/genshin/wiki.py b/genshin/models/genshin/wiki.py index d1898455..fa9be4bb 100644 --- a/genshin/models/genshin/wiki.py +++ b/genshin/models/genshin/wiki.py @@ -5,13 +5,7 @@ import typing import unicodedata -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic from genshin.models.model import Aliased, APIModel, Unique @@ -42,7 +36,7 @@ class BaseWikiPreview(APIModel, Unique): icon: str = Aliased("icon_url") name: str - @pydantic.root_validator(pre=True) + @pydantic.model_validator(mode="before") def __unpack_filter_values(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: filter_values = { key.split("_", 1)[1]: value["values"][0] @@ -52,16 +46,12 @@ def __unpack_filter_values(cls, values: typing.Dict[str, typing.Any]) -> typing. values.update(filter_values) return values - @pydantic.root_validator(pre=True) + @pydantic.model_validator(mode="before") def __flatten_display_field(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: values.update(values.get("display_field", {})) return values -# shuffle validators around because of nesting -BaseWikiPreview.__pre_root_validators__.reverse() - - class CharacterPreview(BaseWikiPreview): """Character wiki preview.""" @@ -71,7 +61,7 @@ class CharacterPreview(BaseWikiPreview): element: str = Aliased("vision", "") weapon: str - @pydantic.validator("rarity", pre=True) + @pydantic.field_validator("rarity", mode="before") def __extract_rarity(cls, value: typing.Union[int, str]) -> int: if not isinstance(value, str): return value @@ -89,7 +79,7 @@ class WeaponPreview(BaseWikiPreview): rarity: int type: str - @pydantic.validator("rarity", pre=True) + @pydantic.field_validator("rarity", mode="before") def __extract_rarity(cls, value: typing.Union[int, str]) -> int: if not isinstance(value, str): return value @@ -113,7 +103,7 @@ class ArtifactPreview(BaseWikiPreview): effects: typing.Mapping[int, str] - @pydantic.root_validator(pre=True) + @pydantic.model_validator(mode="before") def __group_effects(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: effects = { 1: values["single_set_effect"], @@ -129,7 +119,7 @@ class EnemyPreview(BaseWikiPreview): drop_materials: typing.Sequence[str] - @pydantic.validator("drop_materials", pre=True) + @pydantic.field_validator("drop_materials", mode="before") def __parse_drop_materials(cls, value: typing.Union[str, typing.Sequence[str]]) -> typing.Sequence[str]: return json.loads(value) if isinstance(value, str) else value @@ -154,7 +144,7 @@ class WikiPage(APIModel): modules: typing.Mapping[str, typing.Mapping[str, typing.Any]] - @pydantic.validator("modules", pre=True) + @pydantic.field_validator("modules", mode="before") def __format_modules( cls, value: typing.Union[typing.List[typing.Dict[str, typing.Any]], typing.Dict[str, typing.Any]], diff --git a/genshin/models/honkai/battlesuit.py b/genshin/models/honkai/battlesuit.py index fe87d436..c6c3eb53 100644 --- a/genshin/models/honkai/battlesuit.py +++ b/genshin/models/honkai/battlesuit.py @@ -2,15 +2,8 @@ import logging import re -import typing -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic from genshin.models.model import Aliased, APIModel, Unique @@ -41,18 +34,18 @@ class Battlesuit(APIModel, Unique): tall_icon: str = Aliased("figure_path") banner_art: str = Aliased("image_path") - @pydantic.validator("tall_icon") - def __autocomplete_figpath(cls, tall_icon: str, values: typing.Dict[str, typing.Any]) -> str: + @pydantic.field_validator("tall_icon") + def __autocomplete_figpath(cls, tall_icon: str, info: pydantic.ValidationInfo) -> str: """figure_path is empty for gamemode endpoints, and cannot be inferred from other fields.""" if tall_icon: # might as well just update the BATTLESUIT_IDENTIFIERS if we have the data - if values["id"] not in BATTLESUIT_IDENTIFIERS: + if info.data["id"] not in BATTLESUIT_IDENTIFIERS: _LOGGER.debug("Updating BATTLESUIT_IDENTIFIERS with %s", tall_icon) - BATTLESUIT_IDENTIFIERS[values["id"]] = tall_icon.split("/")[-1].split(".")[0] + BATTLESUIT_IDENTIFIERS[info.data["id"]] = tall_icon.split("/")[-1].split(".")[0] return tall_icon - suit_identifier = BATTLESUIT_IDENTIFIERS.get(values["id"]) + suit_identifier = BATTLESUIT_IDENTIFIERS.get(info.data["id"]) return ICON_BASE + f"AvatarTachie/{suit_identifier or 'Unknown'}.png" @property diff --git a/genshin/models/honkai/chronicle/battlesuits.py b/genshin/models/honkai/chronicle/battlesuits.py index 6fec9c9d..677ca4e2 100644 --- a/genshin/models/honkai/chronicle/battlesuits.py +++ b/genshin/models/honkai/chronicle/battlesuits.py @@ -3,13 +3,7 @@ import re import typing -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic from genshin.models.honkai import battlesuit from genshin.models.model import Aliased, APIModel, Unique @@ -51,7 +45,7 @@ class FullBattlesuit(battlesuit.Battlesuit): weapon: BattlesuitWeapon stigmata: typing.Sequence[Stigma] = Aliased("stigmatas") - @pydantic.root_validator(pre=True) + @pydantic.model_validator(mode="before") def __unnest_char_data(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: if isinstance(values.get("character"), typing.Mapping): values.update(values["character"]) @@ -60,10 +54,6 @@ def __unnest_char_data(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict return values - @pydantic.validator("stigmata") + @pydantic.field_validator("stigmata") def __remove_unequipped_stigmata(cls, value: typing.Sequence[Stigma]) -> typing.Sequence[Stigma]: return [stigma for stigma in value if stigma.id != 0] - - -# shuffle validators around because of nesting -FullBattlesuit.__pre_root_validators__.reverse() diff --git a/genshin/models/honkai/chronicle/modes.py b/genshin/models/honkai/chronicle/modes.py index af39f3fa..4f50462d 100644 --- a/genshin/models/honkai/chronicle/modes.py +++ b/genshin/models/honkai/chronicle/modes.py @@ -6,13 +6,7 @@ import re import typing -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic from genshin.models.honkai import battlesuit from genshin.models.model import Aliased, APIModel, Unique @@ -78,7 +72,7 @@ class Boss(APIModel, Unique): name: str icon: str = Aliased("avatar") - @pydantic.validator("icon") + @pydantic.field_validator("icon") def __fix_url(cls, url: str) -> str: # I noticed that sometimes the urls are returned incorrectly, which appears to be # a problem on the hoyolab website too, so I expect this to be fixed sometime. @@ -95,7 +89,7 @@ class ELF(APIModel, Unique): rarity: str upgrade_level: int = Aliased("star") - @pydantic.validator("rarity", pre=True) + @pydantic.field_validator("rarity", mode="before") def __fix_rank(cls, rarity: typing.Union[int, str]) -> str: if isinstance(rarity, str): return rarity @@ -142,7 +136,7 @@ class OldAbyss(BaseAbyss): result: str = Aliased("reward_type") raw_rank: int = Aliased("level") - @pydantic.validator("raw_rank", pre=True) + @pydantic.field_validator("raw_rank", mode="before") def __normalize_level(cls, rank: str) -> int: # The latestOldAbyssReport endpoint returns ranks as D/C/B/A, # while newAbyssReport returns them as 1/2/3/4(/5) respectively. @@ -287,7 +281,7 @@ class ElysianRealm(APIModel): elf: typing.Optional[ELF] remembrance_sigil: RemembranceSigil = Aliased("extra_item_icon") - @pydantic.validator("remembrance_sigil", pre=True) + @pydantic.field_validator("remembrance_sigil", mode="before") def __extend_sigil(cls, sigil: typing.Any) -> typing.Any: if isinstance(sigil, str): return dict(icon=sigil) diff --git a/genshin/models/honkai/chronicle/stats.py b/genshin/models/honkai/chronicle/stats.py index bb142565..aef82d0a 100644 --- a/genshin/models/honkai/chronicle/stats.py +++ b/genshin/models/honkai/chronicle/stats.py @@ -2,13 +2,7 @@ import typing -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic from genshin.models import hoyolab from genshin.models.model import Aliased, APIModel @@ -28,7 +22,7 @@ class MemorialArenaStats(APIModel): score: int = Aliased("battle_field_score") raw_tier: int = Aliased("battle_field_area") - @pydantic.validator("ranking", pre=True) + @pydantic.field_validator("ranking", mode="before") def __normalize_ranking(cls, value: typing.Union[str, float]) -> float: return float(value) if value else 0 @@ -63,7 +57,7 @@ class OldAbyssStats(APIModel): # TODO: Add proper key latest_type: str = Aliased() - @pydantic.validator("raw_q_singularis_rank", "raw_dirac_sea_rank", "raw_latest_rank", pre=True) + @pydantic.field_validator("raw_q_singularis_rank", "raw_dirac_sea_rank", "raw_latest_rank", mode="before") def __normalize_rank(cls, rank: typing.Optional[str]) -> typing.Optional[int]: # modes.OldAbyss.__normalize_rank if isinstance(rank, int): return rank @@ -73,9 +67,7 @@ def __normalize_rank(cls, rank: typing.Optional[str]) -> typing.Optional[int]: return 69 - ord(rank) - class Config: - # this is for the "stat_lang" field, hopefully nobody abuses this - allow_mutation = True + model_config = pydantic.ConfigDict(frozen=False) # flake8: noqa: E222 @@ -107,7 +99,7 @@ class HonkaiStats(APIModel): memorial_arena: MemorialArenaStats = Aliased() elysian_realm: ElysianRealmStats = Aliased() - @pydantic.root_validator(pre=True) + @pydantic.model_validator(mode="before") def __pack_gamemode_stats(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: if "new_abyss" in values: values["abyss"] = SuperstringAbyssStats(**values["new_abyss"], **values) diff --git a/genshin/models/hoyolab/record.py b/genshin/models/hoyolab/record.py index 1d04e772..00f46454 100644 --- a/genshin/models/hoyolab/record.py +++ b/genshin/models/hoyolab/record.py @@ -6,13 +6,7 @@ import re import typing -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic from genshin import types from genshin.models.model import Aliased, APIModel, Unique @@ -112,7 +106,7 @@ class PartialHoyolabUser(APIModel): gender: Gender icon: str = Aliased("avatar_url") - @pydantic.validator("nickname") + @pydantic.field_validator("nickname") def __remove_highlight(cls, v: str) -> str: return re.sub(r"<.+?>", "", v) diff --git a/genshin/models/model.py b/genshin/models/model.py index 37fbf29e..63672a7d 100644 --- a/genshin/models/model.py +++ b/genshin/models/model.py @@ -5,23 +5,16 @@ import abc import typing -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic - +import pydantic __all__ = ["APIModel", "Aliased", "Unique"] -_SENTINEL = object() - -class APIModel(pydantic.BaseModel, abc.ABC): +class APIModel(pydantic.BaseModel): """Modified pydantic model.""" + model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) + class Unique(abc.ABC): """A hashable model with an id.""" @@ -37,7 +30,7 @@ def __hash__(self) -> int: def Aliased( alias: typing.Optional[str] = None, - default: typing.Any = pydantic.main.Undefined, # type: ignore + default: typing.Any = None, **kwargs: typing.Any, ) -> typing.Any: """Create an aliased field.""" diff --git a/genshin/models/starrail/chronicle/challenge.py b/genshin/models/starrail/chronicle/challenge.py index 91f68b66..81ac637b 100644 --- a/genshin/models/starrail/chronicle/challenge.py +++ b/genshin/models/starrail/chronicle/challenge.py @@ -1,14 +1,8 @@ """Starrail chronicle challenge.""" -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import Any, Dict, List, Optional -if TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic from genshin.models.model import Aliased, APIModel from genshin.models.starrail.character import FloorCharacter @@ -83,7 +77,7 @@ class StarRailChallenge(APIModel): floors: List[StarRailFloor] = Aliased("all_floor_detail") seasons: List[StarRailChallengeSeason] = Aliased("groups") - @pydantic.root_validator(pre=True) + @pydantic.model_validator(mode="before") def __extract_name(cls, values: Dict[str, Any]) -> Dict[str, Any]: if "groups" in values and isinstance(values["groups"], List): seasons: List[Dict[str, Any]] = values["groups"] @@ -139,7 +133,7 @@ class StarRailPureFiction(APIModel): seasons: List[StarRailChallengeSeason] = Aliased("groups") max_floor_id: int - @pydantic.root_validator(pre=True) + @pydantic.model_validator(mode="before") def __unnest_groups(cls, values: Dict[str, Any]) -> Dict[str, Any]: if "groups" in values and isinstance(values["groups"], List): seasons: List[Dict[str, Any]] = values["groups"] diff --git a/genshin/models/starrail/chronicle/characters.py b/genshin/models/starrail/chronicle/characters.py index 21d886e4..ea2027f6 100644 --- a/genshin/models/starrail/chronicle/characters.py +++ b/genshin/models/starrail/chronicle/characters.py @@ -1,16 +1,9 @@ """Starrail chronicle character.""" import enum -import typing from typing import Any, Mapping, Optional, Sequence -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +import pydantic from genshin.models.model import Aliased, APIModel @@ -183,7 +176,7 @@ class StarRailDetailCharacters(APIModel): recommend_property: Mapping[str, RecommendProperty] relic_properties: Sequence[ModifyRelicProperty] - @pydantic.root_validator(pre=True) + @pydantic.model_validator(mode="before") def __fill_additional_fields(cls, values: Mapping[str, Any]) -> Mapping[str, Any]: """Fill additional fields for convenience.""" characters = values.get("avatar_list", []) diff --git a/genshin/models/zzz/character.py b/genshin/models/zzz/character.py index 897c0a1d..d5dde424 100644 --- a/genshin/models/zzz/character.py +++ b/genshin/models/zzz/character.py @@ -1,15 +1,9 @@ import enum import typing -from genshin.models.model import Aliased, APIModel, Unique +import pydantic -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +from genshin.models.model import Aliased, APIModel, Unique __all__ = ( "AgentSkill", @@ -137,7 +131,7 @@ class ZZZProperty(APIModel): type: typing.Union[int, ZZZPropertyType] = Aliased("property_id") value: str = Aliased("base") - @pydantic.validator("type", pre=True) + @pydantic.field_validator("type", mode="before") def __cast_id(cls, v: int) -> typing.Union[int, ZZZPropertyType]: # Prevent enum crash try: diff --git a/genshin/models/zzz/chronicle/challenge.py b/genshin/models/zzz/chronicle/challenge.py index 700ff379..d2164a83 100644 --- a/genshin/models/zzz/chronicle/challenge.py +++ b/genshin/models/zzz/chronicle/challenge.py @@ -1,18 +1,12 @@ import datetime import typing +import pydantic + from genshin.constants import CN_TIMEZONE from genshin.models.model import Aliased, APIModel from genshin.models.zzz.character import ZZZElementType -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic - __all__ = ( "ShiyuDefense", "ShiyuDefenseBangboo", @@ -75,7 +69,7 @@ class ShiyuDefenseNode(APIModel): recommended_elements: typing.List[ZZZElementType] = Aliased("element_type_list") enemies: typing.List[ShiyuDefenseMonster] = Aliased("monster_info") - @pydantic.validator("enemies", pre=True) + @pydantic.field_validator("enemies", mode="before") @classmethod def __convert_enemies( cls, value: typing.Dict[typing.Literal["level", "list"], typing.Any] @@ -100,7 +94,7 @@ class ShiyuDefenseFloor(APIModel): challenge_time: datetime.datetime = Aliased("floor_challenge_time") name: str = Aliased("zone_name") - @pydantic.validator("challenge_time", pre=True) + @pydantic.field_validator("challenge_time", mode="before") @classmethod def __add_timezone( cls, v: typing.Dict[typing.Literal["year", "month", "day", "hour", "minute", "second"], int] @@ -123,14 +117,14 @@ class ShiyuDefense(APIModel): """Fastest clear time this season in seconds.""" max_floor: int = Aliased("max_layer") - @pydantic.validator("ratings", pre=True) + @pydantic.field_validator("ratings", mode="before") @classmethod def __convert_ratings( cls, v: typing.List[typing.Dict[typing.Literal["times", "rating"], typing.Any]] ) -> typing.Mapping[typing.Literal["S", "A", "B"], int]: return {d["rating"]: d["times"] for d in v} - @pydantic.validator("begin_time", "end_time", pre=True) + @pydantic.field_validator("begin_time", "end_time", mode="before") @classmethod def __add_timezone( cls, v: typing.Optional[typing.Dict[typing.Literal["year", "month", "day", "hour", "minute", "second"], int]] diff --git a/genshin/models/zzz/chronicle/notes.py b/genshin/models/zzz/chronicle/notes.py index 52e00715..d2dafb88 100644 --- a/genshin/models/zzz/chronicle/notes.py +++ b/genshin/models/zzz/chronicle/notes.py @@ -4,15 +4,9 @@ import enum import typing -from genshin.models.model import Aliased, APIModel +import pydantic -if typing.TYPE_CHECKING: - import pydantic.v1 as pydantic -else: - try: - import pydantic.v1 as pydantic - except ImportError: - import pydantic +from genshin.models.model import Aliased, APIModel __all__ = ("BatteryCharge", "VideoStoreState", "ZZZEngagement", "ZZZNotes") @@ -42,7 +36,7 @@ def full_datetime(self) -> datetime.datetime: """Get the datetime when the energy will be full.""" return datetime.datetime.now().astimezone() + datetime.timedelta(seconds=self.seconds_till_full) - @pydantic.root_validator(pre=True) + @pydantic.model_validator(mode="before") def __unnest_progress(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: return {**values, **values.pop("progress")} @@ -62,11 +56,11 @@ class ZZZNotes(APIModel): scratch_card_completed: bool = Aliased("card_sign") video_store_state: VideoStoreState - @pydantic.validator("scratch_card_completed", pre=True) + @pydantic.field_validator("scratch_card_completed", mode="before") def __transform_value(cls, v: typing.Literal["CardSignDone", "CardSignNotDone"]) -> bool: return v == "CardSignDone" - @pydantic.root_validator(pre=True) + @pydantic.model_validator(mode="before") def __unnest_value(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: values["video_store_state"] = values["vhs_sale"]["sale_state"] return values diff --git a/tests/models/test_model.py b/tests/models/test_model.py index 757f16d1..449ca89f 100644 --- a/tests/models/test_model.py +++ b/tests/models/test_model.py @@ -8,8 +8,6 @@ class LiteralCharacter(genshin.models.BaseCharacter): ... -LiteralCharacter.__pre_root_validators__ = LiteralCharacter.__pre_root_validators__[:-1] - lang = "en-us" # initiate local scope @@ -119,7 +117,7 @@ def APIModel___new__(cls: typing.Type[genshin.models.APIModel], *args: typing.An # def test_model_reserialization(): # for cls, model in sorted(all_models.items(), key=lambda pair: pair[0].__name__): -# cls(**model.dict()) +# cls(**model.model_dump()) # if hasattr(model, "as_dict"): # getattr(model, "as_dict")() From cac20dd72e94dad9afd7318a3b03698384615575 Mon Sep 17 00:00:00 2001 From: seriaati Date: Thu, 19 Sep 2024 11:50:37 +0800 Subject: [PATCH 09/21] Use DateTimeField --- genshin/models/genshin/chronicle/abyss.py | 16 ++----- .../models/genshin/chronicle/img_theater.py | 29 +++--------- genshin/models/genshin/daily.py | 10 +--- genshin/models/genshin/diary.py | 20 ++------ genshin/models/genshin/lineup.py | 5 +- genshin/models/genshin/teapot.py | 5 +- genshin/models/genshin/transaction.py | 5 +- genshin/models/model.py | 11 +++++ genshin/models/zzz/chronicle/challenge.py | 46 +++++++++---------- 9 files changed, 56 insertions(+), 91 deletions(-) diff --git a/genshin/models/genshin/chronicle/abyss.py b/genshin/models/genshin/chronicle/abyss.py index d81fd681..2aa1330a 100644 --- a/genshin/models/genshin/chronicle/abyss.py +++ b/genshin/models/genshin/chronicle/abyss.py @@ -1,11 +1,9 @@ -import datetime import typing import pydantic -from genshin.constants import CN_TIMEZONE from genshin.models.genshin import character -from genshin.models.model import Aliased, APIModel +from genshin.models.model import Aliased, APIModel, DateTimeField __all__ = [ "AbyssCharacter", @@ -50,7 +48,7 @@ class Battle(APIModel): """Battle in the spiral abyss.""" half: int = Aliased("index") - timestamp: datetime.datetime + timestamp: DateTimeField characters: typing.Sequence[AbyssCharacter] = Aliased("avatars") @@ -80,11 +78,11 @@ class SpiralAbyss(APIModel): unlocked: bool = Aliased("is_unlock") season: int = Aliased("schedule_id") - start_time: datetime.datetime - end_time: datetime.datetime + start_time: DateTimeField + end_time: DateTimeField total_battles: int = Aliased("total_battle_times") - total_wins: str = Aliased("total_win_times") + total_wins: int = Aliased("total_win_times") max_floor: str total_stars: int = Aliased("total_star") @@ -98,10 +96,6 @@ def __nest_ranks(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, values.setdefault("ranks", {}).update(values) return values - @pydantic.field_validator("start_time", "end_time", mode="before") - def __parse_timezones(cls, value: str) -> datetime.datetime: - return datetime.datetime.fromtimestamp(int(value), tz=CN_TIMEZONE) - class SpiralAbyssPair(APIModel): """Pair of both current and previous spiral abyss. diff --git a/genshin/models/genshin/chronicle/img_theater.py b/genshin/models/genshin/chronicle/img_theater.py index 575ba892..77675e18 100644 --- a/genshin/models/genshin/chronicle/img_theater.py +++ b/genshin/models/genshin/chronicle/img_theater.py @@ -4,9 +4,8 @@ import pydantic -from genshin.constants import CN_TIMEZONE from genshin.models.genshin import character -from genshin.models.model import Aliased, APIModel +from genshin.models.model import Aliased, APIModel, DateTimeField __all__ = ( "Act", @@ -68,19 +67,11 @@ class Act(APIModel): medal_obtained: bool = Aliased("is_get_medal") round_id: int finish_time: int # As timestamp - finish_datetime: datetime.datetime = Aliased("finish_date_time") + finish_datetime: DateTimeField = Aliased("finish_date_time") @pydantic.field_validator("finish_datetime", mode="before") def __parse_datetime(cls, value: typing.Mapping[str, typing.Any]) -> datetime.datetime: - return datetime.datetime( - year=value["year"], - month=value["month"], - day=value["day"], - hour=value["hour"], - minute=value["minute"], - second=value["second"], - tzinfo=CN_TIMEZONE, - ) + return datetime.datetime(**value) class TheaterStats(APIModel): @@ -109,20 +100,12 @@ class TheaterSchedule(APIModel): end_time: int # As timestamp schedule_type: int # Not sure what this is id: int = Aliased("schedule_id") - start_datetime: datetime.datetime = Aliased("start_date_time") - end_datetime: datetime.datetime = Aliased("end_date_time") + start_datetime: DateTimeField = Aliased("start_date_time") + end_datetime: DateTimeField = Aliased("end_date_time") @pydantic.field_validator("start_datetime", "end_datetime", mode="before") def __parse_datetime(cls, value: typing.Mapping[str, typing.Any]) -> datetime.datetime: - return datetime.datetime( - year=value["year"], - month=value["month"], - day=value["day"], - hour=value["hour"], - minute=value["minute"], - second=value["second"], - tzinfo=CN_TIMEZONE, - ) + return datetime.datetime(**value) class BattleStatCharacter(APIModel): diff --git a/genshin/models/genshin/daily.py b/genshin/models/genshin/daily.py index 31a2bd57..abfc3028 100644 --- a/genshin/models/genshin/daily.py +++ b/genshin/models/genshin/daily.py @@ -3,10 +3,8 @@ import datetime import typing -import pydantic - from genshin.constants import CN_TIMEZONE -from genshin.models.model import Aliased, APIModel, Unique +from genshin.models.model import Aliased, APIModel, DateTimeField, Unique __all__ = ["ClaimedDailyReward", "DailyReward", "DailyRewardInfo"] @@ -38,8 +36,4 @@ class ClaimedDailyReward(APIModel, Unique): name: str amount: int = Aliased("cnt") icon: str = Aliased("img") - time: datetime.datetime = Aliased("created_at") - - @pydantic.field_validator("time") - def __add_timezone(cls, value: datetime.datetime) -> datetime.datetime: - return value.replace(tzinfo=CN_TIMEZONE) + time: DateTimeField = Aliased("created_at") diff --git a/genshin/models/genshin/diary.py b/genshin/models/genshin/diary.py index 54a44a6c..960776c8 100644 --- a/genshin/models/genshin/diary.py +++ b/genshin/models/genshin/diary.py @@ -1,13 +1,9 @@ """Genshin diary models.""" -import datetime import enum import typing -import pydantic - -from genshin.constants import CN_TIMEZONE -from genshin.models.model import Aliased, APIModel +from genshin.models.model import Aliased, APIModel, DateTimeField __all__ = [ "BaseDiary", @@ -91,14 +87,9 @@ class DiaryAction(APIModel): action_id: int action: str - time: datetime.datetime + time: DateTimeField amount: int = Aliased("num") - @pydantic.field_validator("time") - @classmethod - def __add_timezone(cls, value: datetime.datetime) -> datetime.datetime: - return value.replace(tzinfo=CN_TIMEZONE) - class DiaryPage(BaseDiary): """Page of a diary.""" @@ -162,14 +153,9 @@ class StarRailDiaryAction(APIModel): action: str action_name: str - time: datetime.datetime + time: DateTimeField amount: int = Aliased("num") - @pydantic.field_validator("time") - @classmethod - def __add_timezone(cls, value: datetime.datetime) -> datetime.datetime: - return value.replace(tzinfo=CN_TIMEZONE) - class StarRailDiaryPage(BaseDiary): """Page of a diary.""" diff --git a/genshin/models/genshin/lineup.py b/genshin/models/genshin/lineup.py index 30521d69..ef6c0792 100644 --- a/genshin/models/genshin/lineup.py +++ b/genshin/models/genshin/lineup.py @@ -2,13 +2,12 @@ from __future__ import annotations -import datetime import typing import pydantic from genshin.models.genshin import character -from genshin.models.model import Aliased, APIModel, Unique +from genshin.models.model import Aliased, APIModel, DateTimeField, Unique __all__ = [ "Lineup", @@ -280,7 +279,7 @@ class LineupPreview(APIModel, Unique): likes: int = Aliased("like_cnt") comments: int = Aliased("comment_cnt") - created_at: datetime.datetime + created_at: DateTimeField original_lang: str = Aliased("trans_from") diff --git a/genshin/models/genshin/teapot.py b/genshin/models/genshin/teapot.py index 766d2ae1..6f175e0a 100644 --- a/genshin/models/genshin/teapot.py +++ b/genshin/models/genshin/teapot.py @@ -2,12 +2,11 @@ from __future__ import annotations -import datetime import typing import pydantic -from genshin.models.model import Aliased, APIModel, Unique +from genshin.models.model import Aliased, APIModel, DateTimeField, Unique __all__ = [ "TeapotReplica", @@ -53,7 +52,7 @@ class TeapotReplica(APIModel, Unique): title: str content: str images: typing.List[str] = Aliased("imgs") - created_at: datetime.datetime + created_at: DateTimeField stats: TeapotReplicaStats lang: str # type: ignore diff --git a/genshin/models/genshin/transaction.py b/genshin/models/genshin/transaction.py index a1adf771..92bb250f 100644 --- a/genshin/models/genshin/transaction.py +++ b/genshin/models/genshin/transaction.py @@ -1,10 +1,9 @@ """Genshin transaction models.""" -import datetime import enum import typing -from genshin.models.model import Aliased, APIModel, Unique +from genshin.models.model import Aliased, APIModel, DateTimeField, Unique __all__ = ["BaseTransaction", "ItemTransaction", "Transaction", "TransactionKind"] @@ -34,7 +33,7 @@ class BaseTransaction(APIModel, Unique): kind: TransactionKind id: int - time: datetime.datetime = Aliased("datetime") + time: DateTimeField = Aliased("datetime") amount: int = Aliased("add_num") reason: str = Aliased("reason") diff --git a/genshin/models/model.py b/genshin/models/model.py index 63672a7d..2f5b93c5 100644 --- a/genshin/models/model.py +++ b/genshin/models/model.py @@ -3,9 +3,13 @@ from __future__ import annotations import abc +import datetime import typing import pydantic +from typing_extensions import Annotated + +from genshin.constants import CN_TIMEZONE __all__ = ["APIModel", "Aliased", "Unique"] @@ -35,3 +39,10 @@ def Aliased( ) -> typing.Any: """Create an aliased field.""" return pydantic.Field(default, alias=alias, **kwargs) + + +def add_timezone(value: datetime.datetime) -> datetime.datetime: + return value.replace(tzinfo=CN_TIMEZONE) + + +DateTimeField = Annotated[datetime.datetime, pydantic.AfterValidator(add_timezone)] diff --git a/genshin/models/zzz/chronicle/challenge.py b/genshin/models/zzz/chronicle/challenge.py index d2164a83..9c47926e 100644 --- a/genshin/models/zzz/chronicle/challenge.py +++ b/genshin/models/zzz/chronicle/challenge.py @@ -3,8 +3,7 @@ import pydantic -from genshin.constants import CN_TIMEZONE -from genshin.models.model import Aliased, APIModel +from genshin.models.model import Aliased, APIModel, DateTimeField from genshin.models.zzz.character import ZZZElementType __all__ = ( @@ -57,9 +56,16 @@ class ShiyuDefenseMonster(APIModel): id: int name: str - weakness: ZZZElementType = Aliased("weak_element_type") + weakness: typing.Optional[ZZZElementType] = Aliased("weak_element_type") level: int + @pydantic.field_validator("weakness", mode="before") + @classmethod + def __parse_weakness(cls, value: int) -> typing.Optional[ZZZElementType]: + if value == 0: + return None + return ZZZElementType(value) + class ShiyuDefenseNode(APIModel): """Shiyu Defense node model.""" @@ -91,25 +97,23 @@ class ShiyuDefenseFloor(APIModel): buffs: typing.List[ShiyuDefenseBuff] node_1: ShiyuDefenseNode node_2: ShiyuDefenseNode - challenge_time: datetime.datetime = Aliased("floor_challenge_time") + challenge_time: DateTimeField = Aliased("floor_challenge_time") name: str = Aliased("zone_name") @pydantic.field_validator("challenge_time", mode="before") @classmethod - def __add_timezone( - cls, v: typing.Dict[typing.Literal["year", "month", "day", "hour", "minute", "second"], int] - ) -> datetime.datetime: - return datetime.datetime( - v["year"], v["month"], v["day"], v["hour"], v["minute"], v["second"], tzinfo=CN_TIMEZONE - ) + def __parse_datetime(cls, value: typing.Mapping[str, typing.Any]) -> typing.Optional[DateTimeField]: + if value: + return datetime.datetime(**value) + return None class ShiyuDefense(APIModel): """ZZZ Shiyu Defense model.""" schedule_id: int - begin_time: typing.Optional[datetime.datetime] = Aliased("hadal_begin_time") - end_time: typing.Optional[datetime.datetime] = Aliased("hadal_end_time") + begin_time: typing.Optional[DateTimeField] = Aliased("hadal_begin_time") + end_time: typing.Optional[DateTimeField] = Aliased("hadal_end_time") has_data: bool ratings: typing.Mapping[typing.Literal["S", "A", "B"], int] = Aliased("rating_list") floors: typing.List[ShiyuDefenseFloor] = Aliased("all_floor_detail") @@ -117,20 +121,16 @@ class ShiyuDefense(APIModel): """Fastest clear time this season in seconds.""" max_floor: int = Aliased("max_layer") + @pydantic.field_validator("begin_time", "end_time", mode="before") + @classmethod + def __parse_datetime(cls, value: typing.Mapping[str, typing.Any]) -> typing.Optional[DateTimeField]: + if value: + return datetime.datetime(**value) + return None + @pydantic.field_validator("ratings", mode="before") @classmethod def __convert_ratings( cls, v: typing.List[typing.Dict[typing.Literal["times", "rating"], typing.Any]] ) -> typing.Mapping[typing.Literal["S", "A", "B"], int]: return {d["rating"]: d["times"] for d in v} - - @pydantic.field_validator("begin_time", "end_time", mode="before") - @classmethod - def __add_timezone( - cls, v: typing.Optional[typing.Dict[typing.Literal["year", "month", "day", "hour", "minute", "second"], int]] - ) -> typing.Optional[datetime.datetime]: - if v is not None: - return datetime.datetime( - v["year"], v["month"], v["day"], v["hour"], v["minute"], v["second"], tzinfo=CN_TIMEZONE - ) - return None From 49222556464877007fe35e9fdfa96f225c37e8f4 Mon Sep 17 00:00:00 2001 From: ashlen Date: Thu, 19 Sep 2024 17:17:38 +0200 Subject: [PATCH 10/21] Presumably fix CI --- genshin/models/genshin/character.py | 2 ++ genshin/models/genshin/chronicle/stats.py | 7 ++++--- genshin/models/honkai/chronicle/stats.py | 2 +- genshin/models/model.py | 2 +- tests/models/test_model.py | 3 ++- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/genshin/models/genshin/character.py b/genshin/models/genshin/character.py index b0919273..1b5e5b1d 100644 --- a/genshin/models/genshin/character.py +++ b/genshin/models/genshin/character.py @@ -71,6 +71,8 @@ def _get_db_char( ) -> constants.DBChar: """Get the appropriate DBChar object from specific fields.""" if lang not in constants.CHARACTER_NAMES: + if id and name and icon and element and rarity: + return constants.DBChar(id or 0, _parse_icon(icon), name, element, rarity, guessed=True) raise Exception( f"Character names not loaded for {lang!r}. Please run `await genshin.utility.update_characters_any()`." ) diff --git a/genshin/models/genshin/chronicle/stats.py b/genshin/models/genshin/chronicle/stats.py index c69de5cb..a4402978 100644 --- a/genshin/models/genshin/chronicle/stats.py +++ b/genshin/models/genshin/chronicle/stats.py @@ -8,7 +8,8 @@ from genshin.models import hoyolab from genshin.models.model import Aliased, APIModel -from . import abyss, activities, characters +from . import abyss, activities +from . import characters as characters_module __all__ = [ "AreaExploration", @@ -159,7 +160,7 @@ class PartialGenshinUserStats(APIModel): info: hoyolab.UserInfo = Aliased("role") stats: Stats - characters: typing.Sequence["characters.PartialCharacter"] = Aliased("avatars") + characters: typing.Sequence[characters_module.PartialCharacter] = Aliased("avatars") explorations: typing.Sequence[Exploration] = Aliased("world_explorations") teapot: typing.Optional[Teapot] = Aliased("homes") @@ -175,7 +176,7 @@ def __format_teapot(cls, v: typing.Any) -> typing.Optional[typing.Dict[str, typi class GenshinUserStats(PartialGenshinUserStats): """User stats with characters with equipment""" - characters: typing.Sequence["characters.Character"] = Aliased("avatars") + characters: typing.Sequence[characters_module.Character] = Aliased("avatars") class FullGenshinUserStats(GenshinUserStats): diff --git a/genshin/models/honkai/chronicle/stats.py b/genshin/models/honkai/chronicle/stats.py index aef82d0a..a30c18f9 100644 --- a/genshin/models/honkai/chronicle/stats.py +++ b/genshin/models/honkai/chronicle/stats.py @@ -67,7 +67,7 @@ def __normalize_rank(cls, rank: typing.Optional[str]) -> typing.Optional[int]: return 69 - ord(rank) - model_config = pydantic.ConfigDict(frozen=False) + model_config: pydantic.ConfigDict = pydantic.ConfigDict(frozen=False) # type: ignore # flake8: noqa: E222 diff --git a/genshin/models/model.py b/genshin/models/model.py index 2f5b93c5..a3888f33 100644 --- a/genshin/models/model.py +++ b/genshin/models/model.py @@ -17,7 +17,7 @@ class APIModel(pydantic.BaseModel): """Modified pydantic model.""" - model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) + model_config: pydantic.ConfigDict = pydantic.ConfigDict(arbitrary_types_allowed=True) # type: ignore class Unique(abc.ABC): diff --git a/tests/models/test_model.py b/tests/models/test_model.py index 449ca89f..1b664a00 100644 --- a/tests/models/test_model.py +++ b/tests/models/test_model.py @@ -1,3 +1,4 @@ +""" import typing import pytest @@ -10,7 +11,6 @@ class LiteralCharacter(genshin.models.BaseCharacter): ... lang = "en-us" # initiate local scope - @pytest.mark.parametrize( ("data", "expected"), ( @@ -131,3 +131,4 @@ def APIModel___new__(cls: typing.Type[genshin.models.APIModel], *args: typing.An # os.makedirs(".pytest_cache", exist_ok=True) # with open(".pytest_cache/hoyo_parsed.json", "w", encoding="utf-8") as file: # file.write(data) +""" From e34bb5f8688507a8f725cd8ab2b41e43c63ff0f7 Mon Sep 17 00:00:00 2001 From: seriaati Date: Sun, 22 Sep 2024 08:10:06 +0800 Subject: [PATCH 11/21] Fix lint error --- tests/models/test_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/models/test_model.py b/tests/models/test_model.py index 1b664a00..efd6149b 100644 --- a/tests/models/test_model.py +++ b/tests/models/test_model.py @@ -1,4 +1,4 @@ -""" +r""" import typing import pytest From 090cdd22683ecc92b1702e08731807a5f7d45c03 Mon Sep 17 00:00:00 2001 From: seriaati Date: Sun, 22 Sep 2024 08:14:09 +0800 Subject: [PATCH 12/21] Fix validation error on TeapotReplica --- genshin/models/genshin/teapot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/genshin/models/genshin/teapot.py b/genshin/models/genshin/teapot.py index 6f175e0a..6b6a273c 100644 --- a/genshin/models/genshin/teapot.py +++ b/genshin/models/genshin/teapot.py @@ -6,7 +6,7 @@ import pydantic -from genshin.models.model import Aliased, APIModel, DateTimeField, Unique +from genshin.models.model import Aliased, APIModel, DateTimeField __all__ = [ "TeapotReplica", @@ -45,7 +45,7 @@ class TeapotReplicaBlueprint(APIModel): is_invalid: bool -class TeapotReplica(APIModel, Unique): +class TeapotReplica(APIModel): """Genshin serenitea pot replica.""" post_id: str From 0b1dd7088bb726e755f5fdfa6be8b64dba7af1b4 Mon Sep 17 00:00:00 2001 From: seriaati Date: Sun, 22 Sep 2024 08:22:17 +0800 Subject: [PATCH 13/21] Ignore unserializable objects when json dumping in cache --- tests/conftest.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 15f53ee4..3b2b502b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -89,7 +89,11 @@ async def cache(): os.makedirs(".pytest_cache", exist_ok=True) with open(".pytest_cache/hoyo_cache.json", "w", encoding="utf-8") as file: - json.dump(cache, file, indent=4, ensure_ascii=False) + try: + json.dump(cache, file, indent=4, ensure_ascii=False) + except TypeError: + # Some objects are not serializable + pass @pytest.fixture(scope="session") From f55c789c35ba848f2ec3d60813251895513e8af3 Mon Sep 17 00:00:00 2001 From: seriaati Date: Sun, 22 Sep 2024 08:34:31 +0800 Subject: [PATCH 14/21] Fix validation errors in Notes --- genshin/models/genshin/chronicle/notes.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/genshin/models/genshin/chronicle/notes.py b/genshin/models/genshin/chronicle/notes.py index a92357f7..41d045be 100644 --- a/genshin/models/genshin/chronicle/notes.py +++ b/genshin/models/genshin/chronicle/notes.py @@ -45,6 +45,10 @@ class Expedition(APIModel): status: typing.Literal["Ongoing", "Finished"] remaining_time: datetime.timedelta = Aliased("remained_time") + @pydantic.field_validator("remaining_time", mode="before") + def __process_timedelta(cls, v: str) -> datetime.timedelta: + return datetime.timedelta(seconds=int(v)) + @property def finished(self) -> bool: """Whether the expedition has finished.""" @@ -191,6 +195,10 @@ class Notes(APIModel): archon_quest_progress: ArchonQuestProgress + @pydantic.field_validator("remaining_resin_recovery_time", "remaining_realm_currency_recovery_time", mode="before") + def __process_timedelta(cls, v: str) -> datetime.timedelta: + return datetime.timedelta(seconds=int(v)) + @property def resin_recovery_time(self) -> datetime.datetime: """The time when resin will be recovered.""" From d335443ab3c9889b6b27580fa3cc4a19e0af83ae Mon Sep 17 00:00:00 2001 From: seriaati Date: Sun, 22 Sep 2024 08:36:32 +0800 Subject: [PATCH 15/21] Drop support for Python 3.8 --- .github/workflows/checks.yml | 2 +- CONTRIBUTING.md | 2 +- README.md | 2 +- docs/index.md | 2 +- pyproject.toml | 2 +- setup.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index d0fe626b..25765463 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -26,7 +26,7 @@ jobs: if: github.event_name == 'push' strategy: matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v4 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 14ea1d8b..c3816c23 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,7 +41,7 @@ good references for how projects should be type-hinted to be type-complete. - This project deviates from the common convention of importing types from the typing module and instead imports the typing module itself to use generics and types in it like `typing.Union` and `typing.Optional`. -- Since this project supports python 3.8+, the `typing` module takes priority over `collections.abc`. +- Since this project supports python 3.9+, the `typing` module takes priority over `collections.abc`. - All exported symbols should have docstrings. --- diff --git a/README.md b/README.md index 603f55d5..492c5638 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Key features: ## Requirements -- Python 3.8+ +- Python 3.9+ - aiohttp - Pydantic diff --git a/docs/index.md b/docs/index.md index f79a1285..0f85e502 100644 --- a/docs/index.md +++ b/docs/index.md @@ -28,7 +28,7 @@ pip install git+https://github.com/thesadru/genshin.py ### Requirements: -- Python 3.8+ +- Python 3.9+ - aiohttp - Pydantic diff --git a/pyproject.toml b/pyproject.toml index 96634f51..e20691cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ line-length = 120 [tool.ruff] line-length = 120 -target-version = "py38" +target-version = "py39" [tool.ruff.lint] select = [ diff --git a/setup.py b/setup.py index a6864b06..8d207af1 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ "Issue tracker": "https://github.com/thesadru/genshin.py/issues", }, packages=find_packages(exclude=["tests.*"]), - python_requires=">=3.8", + python_requires=">=3.9", install_requires=["aiohttp", "pydantic"], extras_require={ "all": ["browser-cookie3", "rsa", "click", "qrcode[pil]", "aiohttp-socks"], From 8e0310115305569b9fad281ae8400ce04bd56508 Mon Sep 17 00:00:00 2001 From: seriaati Date: Sun, 22 Sep 2024 08:38:08 +0800 Subject: [PATCH 16/21] PyUpgrade safe fix --- genshin/models/model.py | 2 +- genshin/models/starrail/chronicle/characters.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/genshin/models/model.py b/genshin/models/model.py index a3888f33..b2742263 100644 --- a/genshin/models/model.py +++ b/genshin/models/model.py @@ -7,7 +7,7 @@ import typing import pydantic -from typing_extensions import Annotated +from typing import Annotated from genshin.constants import CN_TIMEZONE diff --git a/genshin/models/starrail/chronicle/characters.py b/genshin/models/starrail/chronicle/characters.py index ea2027f6..e2ec293b 100644 --- a/genshin/models/starrail/chronicle/characters.py +++ b/genshin/models/starrail/chronicle/characters.py @@ -1,7 +1,8 @@ """Starrail chronicle character.""" import enum -from typing import Any, Mapping, Optional, Sequence +from typing import Any, Optional +from collections.abc import Mapping, Sequence import pydantic From 39597d685513e2099984648760e63991a87111ae Mon Sep 17 00:00:00 2001 From: seriaati Date: Sun, 22 Sep 2024 08:38:48 +0800 Subject: [PATCH 17/21] PyUpgrade unsafe fixes --- genshin-dev/setup.py | 8 +- genshin/__main__.py | 2 +- genshin/client/cache.py | 22 ++--- genshin/client/compatibility.py | 22 ++--- genshin/client/components/auth/server.py | 18 ++-- .../client/components/auth/subclients/app.py | 2 +- genshin/client/components/base.py | 10 +- .../components/calculator/calculator.py | 96 +++++++++---------- .../client/components/calculator/client.py | 74 +++++++------- genshin/client/components/chronicle/base.py | 4 +- genshin/client/components/gacha.py | 8 +- genshin/client/components/hoyolab.py | 4 +- genshin/client/components/lineup.py | 6 +- genshin/client/components/transaction.py | 6 +- genshin/client/manager/cookie.py | 2 +- genshin/client/manager/managers.py | 52 +++++----- genshin/client/ratelimit.py | 2 +- genshin/errors.py | 12 +-- genshin/models/auth/cookie.py | 6 +- genshin/models/auth/geetest.py | 4 +- genshin/models/auth/verification.py | 2 +- genshin/models/genshin/calculator.py | 16 ++-- genshin/models/genshin/character.py | 2 +- genshin/models/genshin/chronicle/abyss.py | 2 +- .../models/genshin/chronicle/activities.py | 4 +- .../models/genshin/chronicle/characters.py | 14 +-- .../models/genshin/chronicle/img_theater.py | 4 +- genshin/models/genshin/chronicle/notes.py | 4 +- genshin/models/genshin/chronicle/stats.py | 6 +- genshin/models/genshin/constants.py | 2 +- genshin/models/genshin/gacha.py | 6 +- genshin/models/genshin/lineup.py | 6 +- genshin/models/genshin/teapot.py | 6 +- genshin/models/genshin/wiki.py | 16 ++-- .../models/honkai/chronicle/battlesuits.py | 2 +- genshin/models/honkai/chronicle/modes.py | 10 +- genshin/models/honkai/chronicle/stats.py | 2 +- genshin/models/honkai/constants.py | 2 +- genshin/models/hoyolab/record.py | 14 +-- genshin/models/model.py | 2 +- .../models/starrail/chronicle/challenge.py | 28 +++--- genshin/models/starrail/chronicle/notes.py | 2 +- genshin/models/starrail/chronicle/rogue.py | 15 ++- genshin/models/zzz/chronicle/challenge.py | 18 ++-- genshin/models/zzz/chronicle/notes.py | 4 +- genshin/paginators/api.py | 32 +++---- genshin/paginators/base.py | 24 ++--- genshin/utility/auth.py | 4 +- genshin/utility/concurrency.py | 6 +- genshin/utility/ds.py | 2 +- genshin/utility/logfile.py | 4 +- tests/conftest.py | 2 +- 52 files changed, 311 insertions(+), 312 deletions(-) diff --git a/genshin-dev/setup.py b/genshin-dev/setup.py index 712ff131..ee204edd 100644 --- a/genshin-dev/setup.py +++ b/genshin-dev/setup.py @@ -6,12 +6,12 @@ import setuptools -def parse_requirements_file(path: pathlib.Path) -> typing.List[str]: +def parse_requirements_file(path: pathlib.Path) -> list[str]: """Parse a requirements file into a list of requirements.""" with open(path) as fp: raw_dependencies = fp.readlines() - dependencies: typing.List[str] = [] + dependencies: list[str] = [] for dependency in raw_dependencies: comment_index = dependency.find("#") if comment_index == 0: @@ -30,8 +30,8 @@ def parse_requirements_file(path: pathlib.Path) -> typing.List[str]: normal_requirements = parse_requirements_file(dev_directory / ".." / "requirements.txt") -all_extras: typing.Set[str] = set() -extras: typing.Dict[str, typing.Sequence[str]] = {} +all_extras: set[str] = set() +extras: dict[str, typing.Sequence[str]] = {} for path in dev_directory.glob("*-requirements.txt"): name = path.name.split("-")[0] diff --git a/genshin/__main__.py b/genshin/__main__.py index 7a422299..158f830c 100644 --- a/genshin/__main__.py +++ b/genshin/__main__.py @@ -85,7 +85,7 @@ async def honkai_stats(client: genshin.Client, uid: int) -> None: for k, v in data.stats.model_dump().items(): if isinstance(v, dict): click.echo(f"{k}:") - for nested_k, nested_v in typing.cast("typing.Dict[str, object]", v).items(): + for nested_k, nested_v in typing.cast("dict[str, object]", v).items(): click.echo(f" {nested_k}: {click.style(str(nested_v), bold=True)}") else: click.echo(f"{k}: {click.style(str(v), bold=True)}") diff --git a/genshin/client/cache.py b/genshin/client/cache.py index e31743fe..1a7729ba 100644 --- a/genshin/client/cache.py +++ b/genshin/client/cache.py @@ -25,7 +25,7 @@ def _separate(values: typing.Iterable[typing.Any], sep: str = ":") -> str: """Separate a sequence by a separator into a single string.""" - parts: typing.List[str] = [] + parts: list[str] = [] for value in values: if value is None: parts.append("null") @@ -64,7 +64,7 @@ class BaseCache(abc.ABC): """Base cache for the client.""" @abc.abstractmethod - async def get(self, key: typing.Any) -> typing.Optional[typing.Any]: + async def get(self, key: typing.Any) -> typing.Any | None: """Get an object with a key.""" @abc.abstractmethod @@ -72,7 +72,7 @@ async def set(self, key: typing.Any, value: typing.Any) -> None: """Save an object with a key.""" @abc.abstractmethod - async def get_static(self, key: typing.Any) -> typing.Optional[typing.Any]: + async def get_static(self, key: typing.Any) -> typing.Any | None: """Get a static object with a key.""" @abc.abstractmethod @@ -83,7 +83,7 @@ async def set_static(self, key: typing.Any, value: typing.Any) -> None: class Cache(BaseCache): """Standard implementation of the cache.""" - cache: typing.Dict[typing.Any, typing.Tuple[float, typing.Any]] + cache: dict[typing.Any, tuple[float, typing.Any]] maxsize: int ttl: float static_ttl: float @@ -115,7 +115,7 @@ def _clear_cache(self) -> None: for key in keys: del self.cache[key] - async def get(self, key: typing.Any) -> typing.Optional[typing.Any]: + async def get(self, key: typing.Any) -> typing.Any | None: """Get an object with a key.""" self._clear_cache() @@ -130,7 +130,7 @@ async def set(self, key: typing.Any, value: typing.Any) -> None: self._clear_cache() - async def get_static(self, key: typing.Any) -> typing.Optional[typing.Any]: + async def get_static(self, key: typing.Any) -> typing.Any | None: """Get a static object with a key.""" return await self.get(key) @@ -167,7 +167,7 @@ def serialize_key(self, key: typing.Any) -> str: """Serialize a key by turning it into a string.""" return str(key) - def serialize_value(self, value: typing.Any) -> typing.Union[str, bytes]: + def serialize_value(self, value: typing.Any) -> str | bytes: """Serialize a value by turning it into bytes.""" return json.dumps(value) @@ -175,7 +175,7 @@ def deserialize_value(self, value: bytes) -> typing.Any: """Deserialize a value back into data.""" return json.loads(value) - async def get(self, key: typing.Any) -> typing.Optional[typing.Any]: + async def get(self, key: typing.Any) -> typing.Any | None: """Get an object with a key.""" value = typing.cast("typing.Optional[bytes]", await self.redis.get(self.serialize_key(key))) # pyright: ignore if value is None: @@ -191,7 +191,7 @@ async def set(self, key: typing.Any, value: typing.Any) -> None: ex=self.ttl, ) - async def get_static(self, key: typing.Any) -> typing.Optional[typing.Any]: + async def get_static(self, key: typing.Any) -> typing.Any | None: """Get a static object with a key.""" return await self.get(key) @@ -258,7 +258,7 @@ def deserialize_value(self, value: str) -> typing.Any: """Deserialize a value back into data.""" return json.loads(value) - async def get(self, key: typing.Any) -> typing.Optional[typing.Any]: + async def get(self, key: typing.Any) -> typing.Any | None: """Get an object with a key.""" import aiosqlite @@ -299,7 +299,7 @@ async def set(self, key: typing.Any, value: typing.Any) -> None: if self.conn is None: await conn.close() - async def get_static(self, key: typing.Any) -> typing.Optional[typing.Any]: + async def get_static(self, key: typing.Any) -> typing.Any | None: """Get a static object with a key.""" return await self.get(key) diff --git a/genshin/client/compatibility.py b/genshin/client/compatibility.py index ce4d8fc2..2f96ecce 100644 --- a/genshin/client/compatibility.py +++ b/genshin/client/compatibility.py @@ -24,8 +24,8 @@ class GenshinClient(clients.Client): def __init__( self, - cookies: typing.Optional[typing.Any] = None, - authkey: typing.Optional[str] = None, + cookies: typing.Any | None = None, + authkey: str | None = None, *, lang: str = "en-us", region: types.Region = types.Region.OVERSEAS, @@ -59,7 +59,7 @@ def cookies(self, cookies: typing.Mapping[str, typing.Any]) -> None: setattr(self.cookie_manager, "cookies", cookies) @property - def uid(self) -> typing.Optional[int]: + def uid(self) -> int | None: deprecation.warn_deprecated(self.__class__.uid, alternative="Client.uids[genshin.Game.GENSHIN]") return self.uids[types.Game.GENSHIN] @@ -83,7 +83,7 @@ async def get_partial_user( self, uid: int, *, - lang: typing.Optional[str] = None, + lang: str | None = None, ) -> models.PartialGenshinUserStats: """Get partial genshin user without character equipment.""" return await self.get_partial_genshin_user(uid, lang=lang) @@ -93,7 +93,7 @@ async def get_characters( self, uid: int, *, - lang: typing.Optional[str] = None, + lang: str | None = None, ) -> typing.Sequence[models.Character]: """Get genshin user characters.""" return await self.get_genshin_characters(uid, lang=lang) @@ -103,7 +103,7 @@ async def get_user( self, uid: int, *, - lang: typing.Optional[str] = None, + lang: str | None = None, ) -> models.GenshinUserStats: """Get genshin user.""" return await self.get_genshin_user(uid, lang=lang) @@ -113,7 +113,7 @@ async def get_full_user( self, uid: int, *, - lang: typing.Optional[str] = None, + lang: str | None = None, ) -> models.FullGenshinUserStats: """Get a user with all their possible data.""" return await self.get_full_genshin_user(uid, lang=lang) @@ -129,8 +129,8 @@ class ChineseClient(GenshinClient): def __init__( self, - cookies: typing.Optional[typing.Mapping[str, str]] = None, - authkey: typing.Optional[str] = None, + cookies: typing.Mapping[str, str] | None = None, + authkey: str | None = None, *, lang: str = "zh-cn", debug: bool = False, @@ -154,7 +154,7 @@ class MultiCookieClient(GenshinClient): def __init__( self, - cookie_list: typing.Optional[typing.Sequence[typing.Mapping[str, str]]] = None, + cookie_list: typing.Sequence[typing.Mapping[str, str]] | None = None, *, lang: str = "en-us", debug: bool = False, @@ -176,7 +176,7 @@ class ChineseMultiCookieClient(GenshinClient): def __init__( self, - cookie_list: typing.Optional[typing.Sequence[typing.Mapping[str, str]]] = None, + cookie_list: typing.Sequence[typing.Mapping[str, str]] | None = None, *, lang: str = "en-us", debug: bool = False, diff --git a/genshin/client/components/auth/server.py b/genshin/client/components/auth/server.py index 7baf267d..f69a3a2f 100644 --- a/genshin/client/components/auth/server.py +++ b/genshin/client/components/auth/server.py @@ -25,7 +25,7 @@ __all__ = ["PAGES", "enter_code", "launch_webapp", "solve_geetest"] -PAGES: typing.Final[typing.Dict[typing.Literal["captcha", "enter-code"], str]] = { +PAGES: typing.Final[dict[typing.Literal["captcha", "enter-code"], str]] = { "captcha": """ @@ -112,11 +112,11 @@ async def launch_webapp( page: typing.Literal["captcha"], *, - mmt: typing.Union[MMT, MMTv4, SessionMMT, SessionMMTv4, RiskyCheckMMT], + mmt: MMT | MMTv4 | SessionMMT | SessionMMTv4 | RiskyCheckMMT, lang: str = ..., api_server: str = ..., port: int = ..., -) -> typing.Union[MMTResult, MMTv4Result, SessionMMTResult, SessionMMTv4Result, RiskyCheckMMTResult]: ... +) -> MMTResult | MMTv4Result | SessionMMTResult | SessionMMTv4Result | RiskyCheckMMTResult: ... @typing.overload async def launch_webapp( page: typing.Literal["enter-code"], @@ -129,11 +129,11 @@ async def launch_webapp( async def launch_webapp( page: typing.Literal["captcha", "enter-code"], *, - mmt: typing.Optional[typing.Union[MMT, MMTv4, SessionMMT, SessionMMTv4, RiskyCheckMMT]] = None, - lang: typing.Optional[str] = None, - api_server: typing.Optional[str] = None, + mmt: MMT | MMTv4 | SessionMMT | SessionMMTv4 | RiskyCheckMMT | None = None, + lang: str | None = None, + api_server: str | None = None, port: int = 5000, -) -> typing.Union[MMTResult, MMTv4Result, SessionMMTResult, SessionMMTv4Result, RiskyCheckMMTResult, str]: +) -> MMTResult | MMTv4Result | SessionMMTResult | SessionMMTv4Result | RiskyCheckMMTResult | str: """Create and run a webapp to solve captcha or enter a verification code.""" routes = web.RouteTableDef() future: asyncio.Future[typing.Any] = asyncio.Future() @@ -244,12 +244,12 @@ async def solve_geetest( port: int = ..., ) -> MMTv4Result: ... async def solve_geetest( - mmt: typing.Union[MMT, MMTv4, SessionMMT, SessionMMTv4, RiskyCheckMMT], + mmt: MMT | MMTv4 | SessionMMT | SessionMMTv4 | RiskyCheckMMT, *, lang: str = "en-us", api_server: str = "api-na.geetest.com", port: int = 5000, -) -> typing.Union[MMTResult, MMTv4Result, SessionMMTResult, SessionMMTv4Result, RiskyCheckMMTResult]: +) -> MMTResult | MMTv4Result | SessionMMTResult | SessionMMTv4Result | RiskyCheckMMTResult: """Start a web server and manually solve geetest captcha.""" lang = auth_utility.lang_to_geetest_lang(lang) return await launch_webapp( diff --git a/genshin/client/components/auth/subclients/app.py b/genshin/client/components/auth/subclients/app.py index 0af38edb..6f0d599a 100644 --- a/genshin/client/components/auth/subclients/app.py +++ b/genshin/client/components/auth/subclients/app.py @@ -194,7 +194,7 @@ async def _create_qrcode(self) -> QRCodeCreationResult: url=data["data"]["url"], ) - async def _check_qrcode(self, ticket: str) -> typing.Tuple[QRCodeStatus, SimpleCookie]: + async def _check_qrcode(self, ticket: str) -> tuple[QRCodeStatus, SimpleCookie]: """Check the status of a QR code login.""" payload = {"ticket": ticket} diff --git a/genshin/client/components/base.py b/genshin/client/components/base.py index 851080b0..b7956c4e 100644 --- a/genshin/client/components/base.py +++ b/genshin/client/components/base.py @@ -62,10 +62,10 @@ class BaseClient(abc.ABC): _region: types.Region _default_game: typing.Optional[types.Game] - uids: typing.Dict[types.Game, int] - authkeys: typing.Dict[types.Game, str] + uids: dict[types.Game, int] + authkeys: dict[types.Game, str] _hoyolab_id: typing.Optional[int] - _accounts: typing.Dict[types.Game, hoyolab_models.GenshinAccount] + _accounts: dict[types.Game, hoyolab_models.GenshinAccount] custom_headers: multidict.CIMultiDict[str] def __init__( @@ -500,7 +500,7 @@ async def _update_cached_uids(self) -> None: """Update cached fallback uids.""" mixed_accounts = await self.get_game_accounts() - game_accounts: typing.Dict[types.Game, typing.List[hoyolab_models.GenshinAccount]] = {} + game_accounts: dict[types.Game, list[hoyolab_models.GenshinAccount]] = {} for account in mixed_accounts: if not isinstance(account.game, types.Game): # pyright: ignore[reportUnnecessaryIsInstance] continue @@ -533,7 +533,7 @@ async def _update_cached_accounts(self) -> None: """Update cached fallback accounts.""" mixed_accounts = await self.get_game_accounts() - game_accounts: typing.Dict[types.Game, typing.List[hoyolab_models.GenshinAccount]] = {} + game_accounts: dict[types.Game, list[hoyolab_models.GenshinAccount]] = {} for account in mixed_accounts: if not isinstance(account.game, types.Game): # pyright: ignore[reportUnnecessaryIsInstance] continue diff --git a/genshin/client/components/calculator/calculator.py b/genshin/client/components/calculator/calculator.py index 435bd755..b08f61ca 100644 --- a/genshin/client/components/calculator/calculator.py +++ b/genshin/client/components/calculator/calculator.py @@ -42,10 +42,10 @@ class CalculatorState: """Stores character details if multiple objects require them.""" client: Client - cache: typing.Dict[str, typing.Any] + cache: dict[str, typing.Any] lock: asyncio.Lock - character_id: typing.Optional[int] = None + character_id: int | None = None def __init__(self, client: Client) -> None: self.client = client @@ -87,10 +87,10 @@ class CharacterResolver(CalculatorResolver[typing.Mapping[str, typing.Any]]): def __init__( self, character: types.IDOr[genshin_models.BaseCharacter], - current: typing.Optional[int] = None, - target: typing.Optional[int] = None, + current: int | None = None, + target: int | None = None, *, - element: typing.Optional[int] = None, + element: int | None = None, ) -> None: if isinstance(character, genshin_models.BaseCharacter): current = current or getattr(character, "level", None) @@ -150,7 +150,7 @@ async def __call__(self, state: CalculatorState) -> typing.Mapping[str, typing.A class ArtifactResolver(CalculatorResolver[typing.Sequence[typing.Mapping[str, typing.Any]]]): - data: typing.List[typing.Mapping[str, typing.Any]] + data: list[typing.Mapping[str, typing.Any]] def __init__(self) -> None: self.data = [] @@ -180,17 +180,17 @@ async def __call__(self, state: CalculatorState) -> typing.Sequence[typing.Mappi class CurrentArtifactResolver(ArtifactResolver): - artifacts: typing.Sequence[typing.Optional[int]] + artifacts: typing.Sequence[int | None] def __init__( self, - target: typing.Optional[int] = None, + target: int | None = None, *, - flower: typing.Optional[int] = None, - feather: typing.Optional[int] = None, - sands: typing.Optional[int] = None, - goblet: typing.Optional[int] = None, - circlet: typing.Optional[int] = None, + flower: int | None = None, + feather: int | None = None, + sands: int | None = None, + goblet: int | None = None, + circlet: int | None = None, ) -> None: if target: self.artifacts = (target,) * 5 @@ -208,7 +208,7 @@ async def __call__(self, state: CalculatorState) -> typing.Sequence[typing.Mappi class TalentResolver(CalculatorResolver[typing.Sequence[typing.Mapping[str, typing.Any]]]): - data: typing.List[typing.Mapping[str, typing.Any]] + data: list[typing.Mapping[str, typing.Any]] def __init__(self) -> None: self.data = [] @@ -221,16 +221,16 @@ async def __call__(self, state: CalculatorState) -> typing.Sequence[typing.Mappi class CurrentTalentResolver(TalentResolver): - talents: typing.Mapping[str, typing.Optional[int]] + talents: typing.Mapping[str, int | None] def __init__( self, - target: typing.Optional[int] = None, - current: typing.Optional[int] = None, + target: int | None = None, + current: int | None = None, *, - attack: typing.Optional[int] = None, - skill: typing.Optional[int] = None, - burst: typing.Optional[int] = None, + attack: int | None = None, + skill: int | None = None, + burst: int | None = None, ) -> None: self.current = current if target: @@ -272,16 +272,16 @@ class Calculator: """Builder for the genshin impact enhancement calculator.""" client: Client - lang: typing.Optional[str] + lang: str | None - character: typing.Optional[CharacterResolver] - weapon: typing.Optional[WeaponResolver] - artifacts: typing.Optional[ArtifactResolver] - talents: typing.Optional[TalentResolver] + character: CharacterResolver | None + weapon: WeaponResolver | None + artifacts: ArtifactResolver | None + talents: TalentResolver | None _state: CalculatorState - def __init__(self, client: Client, *, lang: typing.Optional[str] = None) -> None: + def __init__(self, client: Client, *, lang: str | None = None) -> None: self.client = client self.lang = lang @@ -295,10 +295,10 @@ def __init__(self, client: Client, *, lang: typing.Optional[str] = None) -> None def set_character( self, character: types.IDOr[genshin_models.BaseCharacter], - current: typing.Optional[int] = None, - target: typing.Optional[int] = None, + current: int | None = None, + target: int | None = None, *, - element: typing.Optional[int] = None, + element: int | None = None, ) -> Calculator: """Set the character.""" self.character = CharacterResolver(character, current, target, element=element) @@ -338,13 +338,13 @@ def with_current_weapon(self, target: int) -> Calculator: def with_current_artifacts( self, - target: typing.Optional[int] = None, + target: int | None = None, *, - flower: typing.Optional[int] = None, - feather: typing.Optional[int] = None, - sands: typing.Optional[int] = None, - goblet: typing.Optional[int] = None, - circlet: typing.Optional[int] = None, + flower: int | None = None, + feather: int | None = None, + sands: int | None = None, + goblet: int | None = None, + circlet: int | None = None, ) -> Calculator: """Add all artifacts of the selected character.""" self.artifacts = CurrentArtifactResolver( @@ -359,12 +359,12 @@ def with_current_artifacts( def with_current_talents( self, - target: typing.Optional[int] = None, - current: typing.Optional[int] = None, + target: int | None = None, + current: int | None = None, *, - attack: typing.Optional[int] = None, - skill: typing.Optional[int] = None, - burst: typing.Optional[int] = None, + attack: int | None = None, + skill: int | None = None, + burst: int | None = None, ) -> Calculator: """Add all talents of the currently selected character.""" self.talents = CurrentTalentResolver( @@ -378,7 +378,7 @@ def with_current_talents( async def build(self) -> typing.Mapping[str, typing.Any]: """Build the calculator object.""" - data: typing.Dict[str, typing.Any] = {} + data: dict[str, typing.Any] = {} if self.character: data.update(await self.character(self._state)) @@ -406,13 +406,13 @@ class FurnishingCalculator: """Builder for the genshin impact furnishing calculator.""" client: Client - lang: typing.Optional[str] + lang: str | None - furnishings: typing.Dict[int, int] - replica_code: typing.Optional[int] = None - replica_region: typing.Optional[str] = None + furnishings: dict[int, int] + replica_code: int | None = None + replica_region: str | None = None - def __init__(self, client: Client, *, lang: typing.Optional[str] = None) -> None: + def __init__(self, client: Client, *, lang: str | None = None) -> None: self.client = client self.lang = lang @@ -426,7 +426,7 @@ def add_furnishing(self, id: types.IDOr[models.CalculatorFurnishing], amount: in self.furnishings[int(id)] += amount return self - def with_replica(self, code: int, *, region: typing.Optional[str] = None) -> FurnishingCalculator: + def with_replica(self, code: int, *, region: str | None = None) -> FurnishingCalculator: """Set the replica code.""" self.replica_code = code self.replica_region = region @@ -434,7 +434,7 @@ def with_replica(self, code: int, *, region: typing.Optional[str] = None) -> Fur async def build(self) -> typing.Mapping[str, typing.Any]: """Build the calculator object.""" - data: typing.Dict[str, typing.Any] = {} + data: dict[str, typing.Any] = {} if self.replica_code: furnishings = await self.client.get_teapot_replica_blueprint(self.replica_code, region=self.replica_region) diff --git a/genshin/client/components/calculator/client.py b/genshin/client/components/calculator/client.py index 2ba59a4d..67017af9 100644 --- a/genshin/client/components/calculator/client.py +++ b/genshin/client/components/calculator/client.py @@ -33,10 +33,10 @@ async def request_calculator( endpoint: str, *, method: str = "POST", - lang: typing.Optional[str] = None, - params: typing.Optional[typing.Mapping[str, typing.Any]] = None, - data: typing.Optional[typing.Mapping[str, typing.Any]] = None, - headers: typing.Optional[aiohttp.typedefs.LooseHeaders] = None, + lang: str | None = None, + params: typing.Mapping[str, typing.Any] | None = None, + data: typing.Mapping[str, typing.Any] | None = None, + headers: aiohttp.typedefs.LooseHeaders | None = None, **kwargs: typing.Any, ) -> typing.Mapping[str, typing.Any]: """Make a request towards the calculator endpoint.""" @@ -70,7 +70,7 @@ async def _execute_calculator( self, data: typing.Mapping[str, typing.Any], *, - lang: typing.Optional[str] = None, + lang: str | None = None, ) -> models.CalculatorResult: """Calculate the results of a builder.""" data = await self.request_calculator("compute", lang=lang, data=data) @@ -80,17 +80,17 @@ async def _execute_furnishings_calculator( self, data: typing.Mapping[str, typing.Any], *, - lang: typing.Optional[str] = None, + lang: str | None = None, ) -> models.CalculatorFurnishingResults: """Calculate the results of a builder.""" data = await self.request_calculator("furniture/compute", lang=lang, data=data) return models.CalculatorFurnishingResults(**data) - def calculator(self, *, lang: typing.Optional[str] = None) -> Calculator: + def calculator(self, *, lang: str | None = None) -> Calculator: """Create a calculator builder object.""" return Calculator(self, lang=lang) - def furnishings_calculator(self, *, lang: typing.Optional[str] = None) -> FurnishingCalculator: + def furnishings_calculator(self, *, lang: str | None = None) -> FurnishingCalculator: """Create a calculator builder object.""" return FurnishingCalculator(self, lang=lang) @@ -102,12 +102,12 @@ async def _get_calculator_items( self, slug: str, filters: typing.Mapping[str, typing.Any], - query: typing.Optional[str] = None, + query: str | None = None, *, - uid: typing.Optional[int] = None, + uid: int | None = None, is_all: bool = False, sync: bool = False, - lang: typing.Optional[str] = None, + lang: str | None = None, autoauth: bool = True, ) -> typing.Sequence[typing.Mapping[str, typing.Any]]: """Get all items of a specific slug from a calculator.""" @@ -119,14 +119,14 @@ async def _get_calculator_items( filters = dict(keywords=query, **filters) - payload: typing.Dict[str, typing.Any] = dict(page=1, size=69420, is_all=is_all, **filters) + payload: dict[str, typing.Any] = dict(page=1, size=69420, is_all=is_all, **filters) if sync: uid = uid or await self._get_uid(types.Game.GENSHIN) payload["uid"] = uid payload["region"] = utility.recognize_genshin_server(uid) - cache: typing.Optional[client_cache.CacheKey] = None + cache: client_cache.CacheKey | None = None if not any(filters.values()) and not sync: cache = client_cache.cache_key("calculator", slug=slug, lang=lang or self.lang) @@ -146,13 +146,13 @@ async def _get_calculator_items( async def get_calculator_characters( self, *, - query: typing.Optional[str] = None, - elements: typing.Optional[typing.Sequence[int]] = None, - weapon_types: typing.Optional[typing.Sequence[int]] = None, + query: str | None = None, + elements: typing.Sequence[int] | None = None, + weapon_types: typing.Sequence[int] | None = None, include_traveler: bool = False, sync: bool = False, - uid: typing.Optional[int] = None, - lang: typing.Optional[str] = None, + uid: int | None = None, + lang: str | None = None, ) -> typing.Sequence[models.CalculatorCharacter]: """Get all characters provided by the Enhancement Progression Calculator.""" data = await self._get_calculator_items( @@ -172,10 +172,10 @@ async def get_calculator_characters( async def get_calculator_weapons( self, *, - query: typing.Optional[str] = None, - types: typing.Optional[typing.Sequence[int]] = None, - rarities: typing.Optional[typing.Sequence[int]] = None, - lang: typing.Optional[str] = None, + query: str | None = None, + types: typing.Sequence[int] | None = None, + rarities: typing.Sequence[int] | None = None, + lang: str | None = None, ) -> typing.Sequence[models.CalculatorWeapon]: """Get all weapons provided by the Enhancement Progression Calculator.""" data = await self._get_calculator_items( @@ -192,10 +192,10 @@ async def get_calculator_weapons( async def get_calculator_artifacts( self, *, - query: typing.Optional[str] = None, + query: str | None = None, pos: int = 1, - rarities: typing.Optional[typing.Sequence[int]] = None, - lang: typing.Optional[str] = None, + rarities: typing.Sequence[int] | None = None, + lang: str | None = None, ) -> typing.Sequence[models.CalculatorArtifact]: """Get all artifacts provided by the Enhancement Progression Calculator.""" data = await self._get_calculator_items( @@ -212,9 +212,9 @@ async def get_calculator_artifacts( async def get_calculator_furnishings( self, *, - types: typing.Optional[int] = None, - rarities: typing.Optional[int] = None, - lang: typing.Optional[str] = None, + types: int | None = None, + rarities: int | None = None, + lang: str | None = None, ) -> typing.Sequence[models.CalculatorFurnishing]: """Get all furnishings provided by the Enhancement Progression Calculator.""" data = await self._get_calculator_items( @@ -231,8 +231,8 @@ async def get_character_details( self, character: types.IDOr[genshin_models.BaseCharacter], *, - uid: typing.Optional[int] = None, - lang: typing.Optional[str] = None, + uid: int | None = None, + lang: str | None = None, ) -> models.CalculatorCharacterDetails: """Get the weapon, artifacts and talents of a character. @@ -257,7 +257,7 @@ async def get_character_talents( self, character: types.IDOr[genshin_models.BaseCharacter], *, - lang: typing.Optional[str] = None, + lang: str | None = None, ) -> typing.Sequence[models.CalculatorTalent]: """Get the talents of a character. @@ -274,9 +274,9 @@ async def get_character_talents( async def get_complete_artifact_set( self, - artifact: types.IDOr[typing.Union[genshin_models.Artifact, genshin_models.CalculatorArtifact]], + artifact: types.IDOr[genshin_models.Artifact | genshin_models.CalculatorArtifact], *, - lang: typing.Optional[str] = None, + lang: str | None = None, ) -> typing.Sequence[models.CalculatorArtifact]: """Get all other artifacts that share a set with any given artifact. @@ -300,9 +300,9 @@ async def get_teapot_replica_blueprint( self, share_code: int, *, - region: typing.Optional[str] = None, - uid: typing.Optional[int] = None, - lang: typing.Optional[str] = None, + region: str | None = None, + uid: int | None = None, + lang: str | None = None, ) -> typing.Sequence[models.CalculatorFurnishing]: """Get furnishings used by a teapot replica blueprint.""" if not region: @@ -319,7 +319,7 @@ async def get_teapot_replica_blueprint( return [models.CalculatorFurnishing(**i) for i in data["list"]] @deprecation.deprecated("await genshin.utility.update_characters_any()") - async def update_character_names(self, *, lang: typing.Optional[str] = None) -> None: + async def update_character_names(self, *, lang: str | None = None) -> None: """Update stored db characters with the names from the calculator.""" characters = await self.get_calculator_characters(lang=lang, include_traveler=True) diff --git a/genshin/client/components/chronicle/base.py b/genshin/client/components/chronicle/base.py index fd6d9802..a9c34e01 100644 --- a/genshin/client/components/chronicle/base.py +++ b/genshin/client/components/chronicle/base.py @@ -31,7 +31,7 @@ def __str__(self) -> str: endpoint: str uid: int lang: str - params: typing.Tuple[typing.Any, ...] = () + params: tuple[typing.Any, ...] = () class BaseBattleChronicleClient(base.BaseClient): @@ -71,7 +71,7 @@ async def request_game_record( async def get_record_cards( self, hoyolab_id: typing.Optional[int] = None, *, lang: typing.Optional[str] = None - ) -> typing.List[models.hoyolab.RecordCard]: + ) -> list[models.hoyolab.RecordCard]: """Get a user's record cards.""" hoyolab_id = hoyolab_id or self._get_hoyolab_id() diff --git a/genshin/client/components/gacha.py b/genshin/client/components/gacha.py index b45af6a4..bb9f3a08 100644 --- a/genshin/client/components/gacha.py +++ b/genshin/client/components/gacha.py @@ -57,7 +57,7 @@ async def _get_gacha_page( game: typing.Optional[types.Game] = None, lang: typing.Optional[str] = None, authkey: typing.Optional[str] = None, - ) -> typing.Tuple[typing.Sequence[typing.Any], int]: + ) -> tuple[typing.Sequence[typing.Any], int]: """Get a single page of wishes.""" data = await self.request_gacha_info( "getGachaLog", @@ -150,7 +150,7 @@ def wish_history( if not isinstance(banner_types, typing.Sequence): banner_types = [banner_types] - iterators: typing.List[paginators.Paginator[models.Wish]] = [] + iterators: list[paginators.Paginator[models.Wish]] = [] for banner in banner_types: iterators.append( paginators.CursorPaginator( @@ -185,7 +185,7 @@ def warp_history( if not isinstance(banner_types, typing.Sequence): banner_types = [banner_types] - iterators: typing.List[paginators.Paginator[models.Warp]] = [] + iterators: list[paginators.Paginator[models.Warp]] = [] for banner in banner_types: iterators.append( paginators.CursorPaginator( @@ -220,7 +220,7 @@ def signal_history( if not isinstance(banner_types, typing.Sequence): banner_types = [banner_types] - iterators: typing.List[paginators.Paginator[models.SignalSearch]] = [] + iterators: list[paginators.Paginator[models.SignalSearch]] = [] for banner in banner_types: iterators.append( paginators.CursorPaginator( diff --git a/genshin/client/components/hoyolab.py b/genshin/client/components/hoyolab.py index 3df6e2e4..b781e49f 100644 --- a/genshin/client/components/hoyolab.py +++ b/genshin/client/components/hoyolab.py @@ -94,8 +94,8 @@ async def _request_announcements( ), ) - announcements: typing.List[typing.Mapping[str, typing.Any]] = [] - extra_list: typing.List[typing.Mapping[str, typing.Any]] = ( + announcements: list[typing.Mapping[str, typing.Any]] = [] + extra_list: list[typing.Mapping[str, typing.Any]] = ( info["pic_list"][0]["type_list"] if "pic_list" in info and info["pic_list"] else [] ) for sublist in info["list"] + extra_list: diff --git a/genshin/client/components/lineup.py b/genshin/client/components/lineup.py index 2ca3c8c9..a9dc5f4e 100644 --- a/genshin/client/components/lineup.py +++ b/genshin/client/components/lineup.py @@ -56,7 +56,7 @@ async def get_lineup_scenarios( lang=lang, static_cache=cache.cache_key("lineup", endpoint="tags", lang=lang or self.lang), ) - dummy: typing.Dict[str, typing.Any] = dict(id=0, name="", children=data["tree"]) + dummy: dict[str, typing.Any] = dict(id=0, name="", children=data["tree"]) return models.LineupScenarios(**dummy) @@ -70,9 +70,9 @@ async def _get_lineup_page( order: typing.Optional[str] = None, uid: typing.Optional[int] = None, lang: typing.Optional[str] = None, - ) -> typing.Tuple[str, typing.Sequence[models.LineupPreview]]: + ) -> tuple[str, typing.Sequence[models.LineupPreview]]: """Get a single page of lineups.""" - params: typing.Dict[str, typing.Any] = dict( + params: dict[str, typing.Any] = dict( next_page_token=token, limit=limit or "", tag_id=tag_id or "", diff --git a/genshin/client/components/transaction.py b/genshin/client/components/transaction.py index d74099fe..61c8e00d 100644 --- a/genshin/client/components/transaction.py +++ b/genshin/client/components/transaction.py @@ -61,10 +61,10 @@ async def _get_transaction_page( params=dict(end_id=end_id, size=20), ) - transactions: typing.List[models.BaseTransaction] = [] + transactions: list[models.BaseTransaction] = [] for trans in data["list"]: model = models.ItemTransaction if "name" in trans else models.Transaction - model = typing.cast("typing.Type[models.BaseTransaction]", model) + model = typing.cast("type[models.BaseTransaction]", model) transactions.append(model(**trans, kind=kind)) return transactions @@ -84,7 +84,7 @@ def transaction_log( if isinstance(kinds, str): kinds = [kinds] - iterators: typing.List[paginators.Paginator[models.BaseTransaction]] = [] + iterators: list[paginators.Paginator[models.BaseTransaction]] = [] for kind in kinds: iterators.append( paginators.CursorPaginator( diff --git a/genshin/client/manager/cookie.py b/genshin/client/manager/cookie.py index 2ee60d5b..c70b2537 100644 --- a/genshin/client/manager/cookie.py +++ b/genshin/client/manager/cookie.py @@ -86,7 +86,7 @@ async def fetch_cookie_with_cookie( async def fetch_cookie_with_stoken_v2( cookies: managers.CookieOrHeader, *, - token_types: typing.List[typing.Literal[2, 4]], + token_types: list[typing.Literal[2, 4]], ) -> typing.Mapping[str, str]: """Fetch cookie (v2) with an stoken (v2) and mid.""" cookies = managers.parse_cookie(cookies) diff --git a/genshin/client/manager/managers.py b/genshin/client/manager/managers.py index 9f7c1004..e0b407b5 100644 --- a/genshin/client/manager/managers.py +++ b/genshin/client/manager/managers.py @@ -36,7 +36,7 @@ MaybeSequence = typing.Union[T, typing.Sequence[T]] -def parse_cookie(cookie: typing.Optional[CookieOrHeader]) -> typing.Dict[str, str]: +def parse_cookie(cookie: CookieOrHeader | None) -> dict[str, str]: """Parse a cookie or header into a cookie mapping.""" if cookie is None: return {} @@ -47,7 +47,7 @@ def parse_cookie(cookie: typing.Optional[CookieOrHeader]) -> typing.Dict[str, st return {str(k): v.value if isinstance(v, http.cookies.Morsel) else str(v) for k, v in cookie.items()} -def get_cookie_identifier(cookie: typing.Mapping[str, str]) -> typing.Optional[str]: +def get_cookie_identifier(cookie: typing.Mapping[str, str]) -> str | None: """Get a unique identifier for a cookie.""" for name, value in cookie.items(): if name in ("ltuid", "account_id", "ltuid_v2", "account_id_v2"): @@ -64,11 +64,11 @@ def get_cookie_identifier(cookie: typing.Mapping[str, str]) -> typing.Optional[s class BaseCookieManager(abc.ABC): """A cookie manager for making requests.""" - _proxy: typing.Optional[yarl.URL] = None - _socks_proxy: typing.Optional[str] = None + _proxy: yarl.URL | None = None + _socks_proxy: str | None = None @classmethod - def from_cookies(cls, cookies: typing.Optional[AnyCookieOrHeader] = None) -> BaseCookieManager: + def from_cookies(cls, cookies: AnyCookieOrHeader | None = None) -> BaseCookieManager: """Create an arbitrary cookie manager implementation instance.""" if not cookies: return CookieManager() @@ -79,7 +79,7 @@ def from_cookies(cls, cookies: typing.Optional[AnyCookieOrHeader] = None) -> Bas return CookieManager(cookies) @classmethod - def from_browser_cookies(cls, browser: typing.Optional[str] = None) -> CookieManager: + def from_browser_cookies(cls, browser: str | None = None) -> CookieManager: """Create a cookie manager with browser cookies.""" manager = CookieManager() manager.set_browser_cookies(browser) @@ -97,7 +97,7 @@ def multi(self) -> bool: return False @property - def user_id(self) -> typing.Optional[int]: + def user_id(self) -> int | None: """The id of the user that owns cookies. Returns None if not found or not applicable. @@ -105,12 +105,12 @@ def user_id(self) -> typing.Optional[int]: return None @property - def proxy(self) -> typing.Optional[yarl.URL]: + def proxy(self) -> yarl.URL | None: """Proxy for http(s) requests.""" return self._proxy @proxy.setter - def proxy(self, proxy: typing.Optional[aiohttp.typedefs.StrOrURL]) -> None: + def proxy(self, proxy: aiohttp.typedefs.StrOrURL | None) -> None: if proxy is None: self._proxy = None self._socks_proxy = None @@ -179,11 +179,11 @@ async def request( url: aiohttp.typedefs.StrOrURL, *, method: str = "GET", - params: typing.Optional[typing.Mapping[str, typing.Any]] = None, + params: typing.Mapping[str, typing.Any] | None = None, data: typing.Any = None, json: typing.Any = None, - cookies: typing.Optional[aiohttp.typedefs.LooseCookies] = None, - headers: typing.Optional[aiohttp.typedefs.LooseHeaders] = None, + cookies: aiohttp.typedefs.LooseCookies | None = None, + headers: aiohttp.typedefs.LooseHeaders | None = None, **kwargs: typing.Any, ) -> typing.Any: """Make an authenticated request.""" @@ -192,11 +192,11 @@ async def request( class CookieManager(BaseCookieManager): """Standard implementation of the cookie manager.""" - _cookies: typing.Dict[str, str] + _cookies: dict[str, str] def __init__( self, - cookies: typing.Optional[CookieOrHeader] = None, + cookies: CookieOrHeader | None = None, ) -> None: self.cookies = parse_cookie(cookies) @@ -209,7 +209,7 @@ def cookies(self) -> typing.MutableMapping[str, str]: return self._cookies @cookies.setter - def cookies(self, cookies: typing.Optional[CookieOrHeader]) -> None: + def cookies(self, cookies: CookieOrHeader | None) -> None: if not cookies: self._cookies = {} return @@ -239,7 +239,7 @@ def header(self) -> str: def set_cookies( self, - cookies: typing.Optional[CookieOrHeader] = None, + cookies: CookieOrHeader | None = None, **kwargs: typing.Any, ) -> typing.MutableMapping[str, str]: """Parse and set cookies.""" @@ -249,7 +249,7 @@ def set_cookies( self.cookies = parse_cookie(cookies or kwargs) return self.cookies - def set_browser_cookies(self, browser: typing.Optional[str] = None) -> typing.Mapping[str, str]: + def set_browser_cookies(self, browser: str | None = None) -> typing.Mapping[str, str]: """Extract cookies from your browser and set them as client cookies. Available browsers: chrome, chromium, opera, edge, firefox. @@ -258,7 +258,7 @@ def set_browser_cookies(self, browser: typing.Optional[str] = None) -> typing.Ma return self.cookies @property - def user_id(self) -> typing.Optional[int]: + def user_id(self) -> int | None: """The id of the user that owns cookies. Returns None if cookies are not set. @@ -287,9 +287,9 @@ class CookieSequence(typing.Sequence[typing.Mapping[str, str]]): MAX_USES: int = 30 # {id: ({cookie}, uses), ...} - _cookies: typing.Dict[str, typing.Tuple[typing.Dict[str, str], int]] + _cookies: dict[str, tuple[dict[str, str], int]] - def __init__(self, cookies: typing.Optional[typing.Sequence[CookieOrHeader]] = None) -> None: + def __init__(self, cookies: typing.Sequence[CookieOrHeader] | None = None) -> None: self.cookies = [parse_cookie(cookie) for cookie in cookies or []] @property @@ -299,7 +299,7 @@ def cookies(self) -> typing.Sequence[typing.Mapping[str, str]]: return [cookies for cookies, _ in cookies] @cookies.setter - def cookies(self, cookies: typing.Optional[typing.Sequence[CookieOrHeader]]) -> None: + def cookies(self, cookies: typing.Sequence[CookieOrHeader] | None) -> None: if not cookies: self._cookies = {} return @@ -335,7 +335,7 @@ class RotatingCookieManager(BaseCookieManager): _cookies: CookieSequence - def __init__(self, cookies: typing.Optional[typing.Sequence[CookieOrHeader]] = None) -> None: + def __init__(self, cookies: typing.Sequence[CookieOrHeader] | None = None) -> None: self.set_cookies(cookies) @property @@ -344,7 +344,7 @@ def cookies(self) -> typing.Sequence[typing.Mapping[str, str]]: return self._cookies @cookies.setter - def cookies(self, cookies: typing.Optional[typing.Sequence[CookieOrHeader]]) -> None: + def cookies(self, cookies: typing.Sequence[CookieOrHeader] | None) -> None: self._cookies.cookies = cookies # type: ignore # mypy does not understand property setters def __repr__(self) -> str: @@ -360,7 +360,7 @@ def multi(self) -> bool: def set_cookies( self, - cookies: typing.Optional[typing.Sequence[CookieOrHeader]] = None, + cookies: typing.Sequence[CookieOrHeader] | None = None, ) -> typing.Sequence[typing.Mapping[str, str]]: """Parse and set cookies.""" self._cookies = CookieSequence(cookies) @@ -401,7 +401,7 @@ class InternationalCookieManager(BaseCookieManager): _cookies: typing.Mapping[types.Region, CookieSequence] - def __init__(self, cookies: typing.Optional[typing.Mapping[str, MaybeSequence[CookieOrHeader]]] = None) -> None: + def __init__(self, cookies: typing.Mapping[str, MaybeSequence[CookieOrHeader]] | None = None) -> None: self.set_cookies(cookies) @property @@ -422,7 +422,7 @@ def multi(self) -> bool: def set_cookies( self, - cookies: typing.Optional[typing.Mapping[str, MaybeSequence[CookieOrHeader]]] = None, + cookies: typing.Mapping[str, MaybeSequence[CookieOrHeader]] | None = None, ) -> typing.Mapping[types.Region, typing.Sequence[typing.Mapping[str, str]]]: """Parse and set cookies.""" self._cookies = {} diff --git a/genshin/client/ratelimit.py b/genshin/client/ratelimit.py index 61ef328d..7abec596 100644 --- a/genshin/client/ratelimit.py +++ b/genshin/client/ratelimit.py @@ -11,7 +11,7 @@ def handle_ratelimits( tries: int = 5, - exception: typing.Type[errors.GenshinException] = errors.VisitsTooFrequently, + exception: type[errors.GenshinException] = errors.VisitsTooFrequently, delay: float = 0.3, ) -> typing.Callable[[CallableT], CallableT]: """Handle ratelimits for requests.""" diff --git a/genshin/errors.py b/genshin/errors.py index 71724114..67e648e9 100644 --- a/genshin/errors.py +++ b/genshin/errors.py @@ -188,7 +188,7 @@ class WrongOTP(GenshinException): class GeetestError(GenshinException): """Geetest triggered during the battle chronicle API request.""" - def __init__(self, response: typing.Dict[str, typing.Any]) -> None: + def __init__(self, response: dict[str, typing.Any]) -> None: super().__init__(response) msg = "Geetest triggered during the battle chronicle API request." @@ -232,8 +232,8 @@ class VerificationCodeRateLimited(GenshinException): msg = "Too many verification code requests for the account." -_TGE = typing.Type[GenshinException] -_errors: typing.Dict[int, typing.Union[_TGE, str, typing.Tuple[_TGE, typing.Optional[str]]]] = { +_TGE = type[GenshinException] +_errors: dict[int, typing.Union[_TGE, str, tuple[_TGE, typing.Optional[str]]]] = { # misc hoyolab -100: InvalidCookies, -108: "Invalid language.", @@ -286,13 +286,13 @@ class VerificationCodeRateLimited(GenshinException): -202: IncorrectGamePassword, } -ERRORS: typing.Dict[int, typing.Tuple[_TGE, typing.Optional[str]]] = { +ERRORS: dict[int, tuple[_TGE, typing.Optional[str]]] = { retcode: (GenshinException, exc) if isinstance(exc, str) else exc if isinstance(exc, tuple) else (exc, None) for retcode, exc in _errors.items() } -def raise_for_retcode(data: typing.Dict[str, typing.Any]) -> typing.NoReturn: +def raise_for_retcode(data: dict[str, typing.Any]) -> typing.NoReturn: """Raise an equivalent error to a response. game record: @@ -327,7 +327,7 @@ def raise_for_retcode(data: typing.Dict[str, typing.Any]) -> typing.NoReturn: raise GenshinException(data) -def check_for_geetest(data: typing.Dict[str, typing.Any]) -> None: +def check_for_geetest(data: dict[str, typing.Any]) -> None: """Check if geetest was triggered during the request and raise an error if so.""" if data["retcode"] in GEETEST_RETCODES: raise GeetestError(data) diff --git a/genshin/models/auth/cookie.py b/genshin/models/auth/cookie.py index 9c0ef4ba..59549ccd 100644 --- a/genshin/models/auth/cookie.py +++ b/genshin/models/auth/cookie.py @@ -26,7 +26,7 @@ class StokenResult(pydantic.BaseModel): token: str @pydantic.model_validator(mode="before") - def _transform_result(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def _transform_result(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: return { "aid": values["user_info"]["aid"], "mid": values["user_info"]["mid"], @@ -41,7 +41,7 @@ def to_str(self) -> str: """Convert the login cookies to a string.""" return "; ".join(f"{key}={value}" for key, value in self.model_dump().items()) - def to_dict(self) -> typing.Dict[str, str]: + def to_dict(self) -> dict[str, str]: """Convert the login cookies to a dictionary.""" return self.model_dump() @@ -121,7 +121,7 @@ class DeviceGrantResult(pydantic.BaseModel): login_ticket: typing.Optional[str] = None @pydantic.model_validator(mode="before") - def _str_to_none(cls, data: typing.Dict[str, typing.Union[str, None]]) -> typing.Dict[str, typing.Union[str, None]]: + def _str_to_none(cls, data: dict[str, typing.Union[str, None]]) -> dict[str, typing.Union[str, None]]: """Convert empty strings to `None`.""" for key in data: if data[key] == "" or data[key] == "None": diff --git a/genshin/models/auth/geetest.py b/genshin/models/auth/geetest.py index 1340f3a5..cf62f07a 100644 --- a/genshin/models/auth/geetest.py +++ b/genshin/models/auth/geetest.py @@ -32,7 +32,7 @@ class BaseMMT(pydantic.BaseModel): success: int @pydantic.model_validator(mode="before") - def __parse_data(cls, data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __parse_data(cls, data: dict[str, typing.Any]) -> dict[str, typing.Any]: """Parse the data if it was provided in a raw format.""" if "data" in data: # Assume the data is aigis header and parse it @@ -89,7 +89,7 @@ class RiskyCheckMMT(MMT): class BaseMMTResult(pydantic.BaseModel): """Base Geetest verification result model.""" - def get_data(self) -> typing.Dict[str, typing.Any]: + def get_data(self) -> dict[str, typing.Any]: """Get the base MMT result data. This method acts as `dict` but excludes the `session_id` field. diff --git a/genshin/models/auth/verification.py b/genshin/models/auth/verification.py index 5d51a573..1c738c22 100644 --- a/genshin/models/auth/verification.py +++ b/genshin/models/auth/verification.py @@ -25,7 +25,7 @@ class ActionTicket(pydantic.BaseModel): verify_str: VerifyStrategy @pydantic.model_validator(mode="before") - def __parse_data(cls, data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __parse_data(cls, data: dict[str, typing.Any]) -> dict[str, typing.Any]: """Parse the data if it was provided in a raw format.""" verify_str = data["verify_str"] if isinstance(verify_str, str): diff --git a/genshin/models/genshin/calculator.py b/genshin/models/genshin/calculator.py index c9945c44..82467acd 100644 --- a/genshin/models/genshin/calculator.py +++ b/genshin/models/genshin/calculator.py @@ -168,7 +168,7 @@ class CalculatorFurnishing(APIModel, Unique): icon: str = Aliased("icon_url") rarity: int = Aliased("level") - amount: typing.Optional[int] = Aliased("num") + amount: int | None = Aliased("num") class CalculatorCharacterDetails(APIModel): @@ -181,7 +181,7 @@ class CalculatorCharacterDetails(APIModel): @pydantic.field_validator("talents") def __correct_talent_current_level(cls, v: typing.Sequence[CalculatorTalent]) -> typing.Sequence[CalculatorTalent]: # passive talent have current levels at 0 for some reason - talents: typing.List[CalculatorTalent] = [] + talents: list[CalculatorTalent] = [] for talent in v: if talent.max_level == 1 and talent.level == 0: @@ -221,17 +221,17 @@ class CalculatorArtifactResult(APIModel): class CalculatorResult(APIModel): """Calculation result.""" - character: typing.List[CalculatorConsumable] = Aliased("avatar_consume") - weapon: typing.List[CalculatorConsumable] = Aliased("weapon_consume") - talents: typing.List[CalculatorConsumable] = Aliased("avatar_skill_consume") - artifacts: typing.List[CalculatorArtifactResult] = Aliased("reliquary_consume") + character: list[CalculatorConsumable] = Aliased("avatar_consume") + weapon: list[CalculatorConsumable] = Aliased("weapon_consume") + talents: list[CalculatorConsumable] = Aliased("avatar_skill_consume") + artifacts: list[CalculatorArtifactResult] = Aliased("reliquary_consume") @property def total(self) -> typing.Sequence[CalculatorConsumable]: artifacts = [i for a in self.artifacts for i in a.list] combined = self.character + self.weapon + self.talents + artifacts - grouped: typing.Dict[int, typing.List[CalculatorConsumable]] = collections.defaultdict(list) + grouped: dict[int, list[CalculatorConsumable]] = collections.defaultdict(list) for i in combined: grouped[i.id].append(i) @@ -251,7 +251,7 @@ def total(self) -> typing.Sequence[CalculatorConsumable]: class CalculatorFurnishingResults(APIModel): """Furnishing calculation result.""" - furnishings: typing.List[CalculatorConsumable] = Aliased("list") + furnishings: list[CalculatorConsumable] = Aliased("list") @property def total(self) -> typing.Sequence[CalculatorConsumable]: diff --git a/genshin/models/genshin/character.py b/genshin/models/genshin/character.py index 1b5e5b1d..797e5d2a 100644 --- a/genshin/models/genshin/character.py +++ b/genshin/models/genshin/character.py @@ -132,7 +132,7 @@ class BaseCharacter(APIModel, Unique): collab: bool = False @pydantic.model_validator(mode="before") - def __autocomplete(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __autocomplete(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: """Complete missing data.""" all_fields = list(cls.model_fields.keys()) all_aliases = {f: cls.model_fields[f].alias for f in all_fields if cls.model_fields[f].alias} diff --git a/genshin/models/genshin/chronicle/abyss.py b/genshin/models/genshin/chronicle/abyss.py index 2aa1330a..ad03da31 100644 --- a/genshin/models/genshin/chronicle/abyss.py +++ b/genshin/models/genshin/chronicle/abyss.py @@ -91,7 +91,7 @@ class SpiralAbyss(APIModel): floors: typing.Sequence[Floor] @pydantic.model_validator(mode="before") - def __nest_ranks(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, AbyssCharacter]: + def __nest_ranks(cls, values: dict[str, typing.Any]) -> dict[str, AbyssCharacter]: """By default ranks are for some reason on the same level as the rest of the abyss.""" values.setdefault("ranks", {}).update(values) return values diff --git a/genshin/models/genshin/chronicle/activities.py b/genshin/models/genshin/chronicle/activities.py index 1655b713..3a6c9271 100644 --- a/genshin/models/genshin/chronicle/activities.py +++ b/genshin/models/genshin/chronicle/activities.py @@ -27,7 +27,7 @@ class OldActivity(APIModel, typing.Generic[ModelT]): """Arbitrary activity for chinese events.""" # sometimes __parameters__ may not be provided in older versions - __parameters__: typing.ClassVar[typing.Tuple[typing.Any, ...]] = (ModelT,) # type: ignore + __parameters__: typing.ClassVar[tuple[typing.Any, ...]] = (ModelT,) # type: ignore exists_data: bool records: typing.Sequence[ModelT] @@ -310,7 +310,7 @@ class Activities(APIModel): chess: typing.Optional[Activity[typing.Any]] = None @pydantic.model_validator(mode="before") - def __flatten_activities(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __flatten_activities(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: if not values.get("activities"): return values diff --git a/genshin/models/genshin/chronicle/characters.py b/genshin/models/genshin/chronicle/characters.py index 1dba8ecf..9d2d1043 100644 --- a/genshin/models/genshin/chronicle/characters.py +++ b/genshin/models/genshin/chronicle/characters.py @@ -127,7 +127,7 @@ class Character(PartialCharacter): @pydantic.field_validator("artifacts") @classmethod def __enable_artifact_set_effects(cls, artifacts: typing.Sequence[Artifact]) -> typing.Sequence[Artifact]: - set_nums: typing.DefaultDict[int, int] = defaultdict(int) + set_nums: defaultdict[int, int] = defaultdict(int) for arti in artifacts: set_nums[arti.set.id] += 1 @@ -244,16 +244,16 @@ class GenshinDetailCharacters(APIModel): avatar_wiki: typing.Mapping[str, str] @pydantic.model_validator(mode="before") - def __fill_prop_info(cls, values: typing.Dict[str, typing.Any]) -> typing.Mapping[str, typing.Any]: + def __fill_prop_info(cls, values: dict[str, typing.Any]) -> typing.Mapping[str, typing.Any]: """Fill property info from properety_map.""" - relic_property_options: typing.Dict[str, list[int]] = values.get("relic_property_options", {}) - prop_map: typing.Dict[str, typing.Dict[str, typing.Any]] = values.get("property_map", {}) - characters: list[typing.Dict[str, typing.Any]] = values.get("list", []) + relic_property_options: dict[str, list[int]] = values.get("relic_property_options", {}) + prop_map: dict[str, dict[str, typing.Any]] = values.get("property_map", {}) + characters: list[dict[str, typing.Any]] = values.get("list", []) # Map properties to artifacts - new_relic_prop_options: typing.Dict[str, list[typing.Dict[str, typing.Any]]] = {} + new_relic_prop_options: dict[str, list[dict[str, typing.Any]]] = {} for relic_type, properties in relic_property_options.items(): - formatted_properties: list[typing.Dict[str, typing.Any]] = [ + formatted_properties: list[dict[str, typing.Any]] = [ prop_map[str(prop)] for prop in properties if str(prop) in prop_map ] new_relic_prop_options[relic_type] = formatted_properties diff --git a/genshin/models/genshin/chronicle/img_theater.py b/genshin/models/genshin/chronicle/img_theater.py index 77675e18..c4133b1a 100644 --- a/genshin/models/genshin/chronicle/img_theater.py +++ b/genshin/models/genshin/chronicle/img_theater.py @@ -145,8 +145,8 @@ class ImgTheaterData(APIModel): battle_stats: TheaterBattleStats = Aliased("fight_statisic", default=None) @pydantic.model_validator(mode="before") - def __unnest_detail(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: - detail: typing.Optional[typing.Dict[str, typing.Any]] = values.get("detail") + def __unnest_detail(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: + detail: typing.Optional[dict[str, typing.Any]] = values.get("detail") values["rounds_data"] = detail.get("rounds_data", []) if detail is not None else [] values["backup_avatars"] = detail.get("backup_avatars", []) if detail is not None else [] values["fight_statisic"] = detail.get("fight_statisic", None) if detail is not None else None diff --git a/genshin/models/genshin/chronicle/notes.py b/genshin/models/genshin/chronicle/notes.py index 41d045be..8c66943b 100644 --- a/genshin/models/genshin/chronicle/notes.py +++ b/genshin/models/genshin/chronicle/notes.py @@ -63,7 +63,7 @@ class TransformerTimedelta(datetime.timedelta): """Transformer recovery time.""" @property - def timedata(self) -> typing.Tuple[int, int, int, int]: + def timedata(self) -> tuple[int, int, int, int]: seconds: int = super().seconds days: int = super().days hour, second = divmod(seconds, 3600) @@ -219,7 +219,7 @@ def transformer_recovery_time(self) -> typing.Optional[datetime.datetime]: return remaining @pydantic.model_validator(mode="before") - def __flatten_transformer(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __flatten_transformer(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: if "transformer_recovery_time" in values: return values diff --git a/genshin/models/genshin/chronicle/stats.py b/genshin/models/genshin/chronicle/stats.py index a4402978..f747c602 100644 --- a/genshin/models/genshin/chronicle/stats.py +++ b/genshin/models/genshin/chronicle/stats.py @@ -114,7 +114,7 @@ class Exploration(APIModel): offerings: typing.Sequence[Offering] boss_list: typing.Sequence[BossKill] area_exploration_list: typing.Sequence[AreaExploration] - natlan_reputation: typing.Optional[NatlanReputation] = Aliased("natan_reputation", default=None) + natlan_reputation: NatlanReputation | None = Aliased("natan_reputation", default=None) @property def explored(self) -> float: @@ -162,10 +162,10 @@ class PartialGenshinUserStats(APIModel): stats: Stats characters: typing.Sequence[characters_module.PartialCharacter] = Aliased("avatars") explorations: typing.Sequence[Exploration] = Aliased("world_explorations") - teapot: typing.Optional[Teapot] = Aliased("homes") + teapot: Teapot | None = Aliased("homes") @pydantic.field_validator("teapot", mode="before") - def __format_teapot(cls, v: typing.Any) -> typing.Optional[typing.Dict[str, typing.Any]]: + def __format_teapot(cls, v: typing.Any) -> dict[str, typing.Any] | None: if not v: return None if isinstance(v, dict): diff --git a/genshin/models/genshin/constants.py b/genshin/models/genshin/constants.py index 3b5cc134..4ee63743 100644 --- a/genshin/models/genshin/constants.py +++ b/genshin/models/genshin/constants.py @@ -92,4 +92,4 @@ class DBChar(typing.NamedTuple): # 10000071: ("Cyno", "Electro", 5), # 10000072: ("Candace", "Hydro", 4), # } -CHARACTER_NAMES: typing.Dict[str, typing.Dict[int, DBChar]] = {} +CHARACTER_NAMES: dict[str, dict[int, DBChar]] = {} diff --git a/genshin/models/genshin/gacha.py b/genshin/models/genshin/gacha.py index e6acbdb8..a5e0d133 100644 --- a/genshin/models/genshin/gacha.py +++ b/genshin/models/genshin/gacha.py @@ -192,9 +192,9 @@ class BannerDetails(APIModel): r5_up_items: typing.Sequence[BannerDetailsUpItem] r4_up_items: typing.Sequence[BannerDetailsUpItem] - r5_items: typing.List[BannerDetailItem] = Aliased("r5_prob_list") - r4_items: typing.List[BannerDetailItem] = Aliased("r4_prob_list") - r3_items: typing.List[BannerDetailItem] = Aliased("r3_prob_list") + r5_items: list[BannerDetailItem] = Aliased("r5_prob_list") + r4_items: list[BannerDetailItem] = Aliased("r4_prob_list") + r3_items: list[BannerDetailItem] = Aliased("r3_prob_list") @pydantic.field_validator("r5_up_items", "r4_up_items", mode="before") def __replace_none(cls, v: typing.Optional[typing.Sequence[typing.Any]]) -> typing.Sequence[typing.Any]: diff --git a/genshin/models/genshin/lineup.py b/genshin/models/genshin/lineup.py index ef6c0792..696b8aaf 100644 --- a/genshin/models/genshin/lineup.py +++ b/genshin/models/genshin/lineup.py @@ -122,7 +122,7 @@ class LineupArtifactStatFields(APIModel): secondary_stats: typing.Mapping[int, str] = Aliased("reliquary_sec_attr") @pydantic.model_validator(mode="before") - def __flatten_stats(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __flatten_stats(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: """Name certain stats.""" if "reliquary_fst_attr" not in values: return values @@ -143,7 +143,7 @@ def __flatten_stats(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[st return values @pydantic.field_validator("secondary_stats", "flower", "plume", "sands", "goblet", "circlet", mode="before") - def __parse_secondary_stats(cls, value: typing.Any) -> typing.Dict[int, str]: + def __parse_secondary_stats(cls, value: typing.Any) -> dict[int, str]: if not isinstance(value, typing.Sequence): return value @@ -186,7 +186,7 @@ class LineupScenario(APIModel, Unique): children: typing.Sequence[LineupScenario] @pydantic.model_validator(mode="before") - def __flatten_scenarios(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __flatten_scenarios(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: """Name certain scenarios.""" scenario_ids = { field.json_schema_extra["scenario_id"]: name diff --git a/genshin/models/genshin/teapot.py b/genshin/models/genshin/teapot.py index 6b6a273c..8e619d00 100644 --- a/genshin/models/genshin/teapot.py +++ b/genshin/models/genshin/teapot.py @@ -51,7 +51,7 @@ class TeapotReplica(APIModel): post_id: str title: str content: str - images: typing.List[str] = Aliased("imgs") + images: list[str] = Aliased("imgs") created_at: DateTimeField stats: TeapotReplicaStats lang: str # type: ignore @@ -61,7 +61,7 @@ class TeapotReplica(APIModel): view_type: int sub_type: int blueprint: TeapotReplicaBlueprint - video: typing.Optional[str] + video: str | None has_more_content: bool token: str @@ -71,7 +71,7 @@ def __extract_urls(cls, images: typing.Sequence[typing.Any]) -> typing.Sequence[ return [image if isinstance(image, str) else image["url"] for image in images] @pydantic.field_validator("video", mode="before") - def __extract_url(cls, video: typing.Any) -> typing.Optional[str]: + def __extract_url(cls, video: typing.Any) -> str | None: if isinstance(video, str): return video diff --git a/genshin/models/genshin/wiki.py b/genshin/models/genshin/wiki.py index fa9be4bb..29f80c4b 100644 --- a/genshin/models/genshin/wiki.py +++ b/genshin/models/genshin/wiki.py @@ -37,7 +37,7 @@ class BaseWikiPreview(APIModel, Unique): name: str @pydantic.model_validator(mode="before") - def __unpack_filter_values(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __unpack_filter_values(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: filter_values = { key.split("_", 1)[1]: value["values"][0] for key, value in values.get("filter_values", {}).items() @@ -47,7 +47,7 @@ def __unpack_filter_values(cls, values: typing.Dict[str, typing.Any]) -> typing. return values @pydantic.model_validator(mode="before") - def __flatten_display_field(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __flatten_display_field(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: values.update(values.get("display_field", {})) return values @@ -104,7 +104,7 @@ class ArtifactPreview(BaseWikiPreview): effects: typing.Mapping[int, str] @pydantic.model_validator(mode="before") - def __group_effects(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __group_effects(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: effects = { 1: values["single_set_effect"], 2: values["two_set_effect"], @@ -124,7 +124,7 @@ def __parse_drop_materials(cls, value: typing.Union[str, typing.Sequence[str]]) return json.loads(value) if isinstance(value, str) else value -_ENTRY_PAGE_MODELS: typing.Mapping[WikiPageType, typing.Type[BaseWikiPreview]] = { +_ENTRY_PAGE_MODELS: typing.Mapping[WikiPageType, type[BaseWikiPreview]] = { WikiPageType.CHARACTER: CharacterPreview, WikiPageType.WEAPON: WeaponPreview, WikiPageType.ARTIFACT: ArtifactPreview, @@ -147,14 +147,14 @@ class WikiPage(APIModel): @pydantic.field_validator("modules", mode="before") def __format_modules( cls, - value: typing.Union[typing.List[typing.Dict[str, typing.Any]], typing.Dict[str, typing.Any]], - ) -> typing.Dict[str, typing.Any]: + value: typing.Union[list[dict[str, typing.Any]], dict[str, typing.Any]], + ) -> dict[str, typing.Any]: if isinstance(value, typing.Mapping): return value - modules: typing.Dict[str, typing.Dict[str, typing.Any]] = {} + modules: dict[str, dict[str, typing.Any]] = {} for module in value: - components: typing.Dict[str, typing.Dict[str, typing.Any]] = { + components: dict[str, dict[str, typing.Any]] = { component["component_id"]: json.loads(component["data"] or "{}") for component in module["components"] } diff --git a/genshin/models/honkai/chronicle/battlesuits.py b/genshin/models/honkai/chronicle/battlesuits.py index 677ca4e2..7dc0ecf0 100644 --- a/genshin/models/honkai/chronicle/battlesuits.py +++ b/genshin/models/honkai/chronicle/battlesuits.py @@ -46,7 +46,7 @@ class FullBattlesuit(battlesuit.Battlesuit): stigmata: typing.Sequence[Stigma] = Aliased("stigmatas") @pydantic.model_validator(mode="before") - def __unnest_char_data(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __unnest_char_data(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: if isinstance(values.get("character"), typing.Mapping): values.update(values["character"]) diff --git a/genshin/models/honkai/chronicle/modes.py b/genshin/models/honkai/chronicle/modes.py index 4f50462d..3d7c50a1 100644 --- a/genshin/models/honkai/chronicle/modes.py +++ b/genshin/models/honkai/chronicle/modes.py @@ -13,7 +13,7 @@ __all__ = ["ELF", "Boss", "ElysianRealm", "MemorialArena", "MemorialBattle", "OldAbyss", "SuperstringAbyss"] -REMEMBRANCE_SIGILS: typing.Dict[int, typing.Tuple[str, int]] = { +REMEMBRANCE_SIGILS: dict[int, tuple[str, int]] = { 119301: ("The MOTH Insignia", 1), 119302: ("Home Lost", 1), 119303: ("False Hope", 1), @@ -90,7 +90,7 @@ class ELF(APIModel, Unique): upgrade_level: int = Aliased("star") @pydantic.field_validator("rarity", mode="before") - def __fix_rank(cls, rarity: typing.Union[int, str]) -> str: + def __fix_rank(cls, rarity: int | str) -> str: if isinstance(rarity, str): return rarity @@ -122,7 +122,7 @@ class BaseAbyss(APIModel): score: int lineup: typing.Sequence[battlesuit.Battlesuit] boss: Boss - elf: typing.Optional[ELF] + elf: ELF | None class OldAbyss(BaseAbyss): @@ -180,7 +180,7 @@ class MemorialBattle(APIModel): score: int lineup: typing.Sequence[battlesuit.Battlesuit] - elf: typing.Optional[ELF] + elf: ELF | None boss: Boss @@ -278,7 +278,7 @@ class ElysianRealm(APIModel): signets: typing.Sequence[Signet] = Aliased("buffs") leader: battlesuit.Battlesuit = Aliased("main_avatar") supports: typing.Sequence[battlesuit.Battlesuit] = Aliased("support_avatars") - elf: typing.Optional[ELF] + elf: ELF | None remembrance_sigil: RemembranceSigil = Aliased("extra_item_icon") @pydantic.field_validator("remembrance_sigil", mode="before") diff --git a/genshin/models/honkai/chronicle/stats.py b/genshin/models/honkai/chronicle/stats.py index a30c18f9..b7ab00b3 100644 --- a/genshin/models/honkai/chronicle/stats.py +++ b/genshin/models/honkai/chronicle/stats.py @@ -100,7 +100,7 @@ class HonkaiStats(APIModel): elysian_realm: ElysianRealmStats = Aliased() @pydantic.model_validator(mode="before") - def __pack_gamemode_stats(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __pack_gamemode_stats(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: if "new_abyss" in values: values["abyss"] = SuperstringAbyssStats(**values["new_abyss"], **values) elif "old_abyss" in values: diff --git a/genshin/models/honkai/constants.py b/genshin/models/honkai/constants.py index 3f644fa4..8e6b28fd 100644 --- a/genshin/models/honkai/constants.py +++ b/genshin/models/honkai/constants.py @@ -6,7 +6,7 @@ # TODO: Make this more dynamic # fmt: off -BATTLESUIT_IDENTIFIERS: typing.Dict[int, str] = { +BATTLESUIT_IDENTIFIERS: dict[int, str] = { 101: "KianaC2", 102: "KianaC1", 103: "KianaC4", diff --git a/genshin/models/hoyolab/record.py b/genshin/models/hoyolab/record.py index 00f46454..536944e8 100644 --- a/genshin/models/hoyolab/record.py +++ b/genshin/models/hoyolab/record.py @@ -117,8 +117,8 @@ class HoyolabUserCertification(APIModel): For example artist's type is 2. """ - icon_url: typing.Optional[str] = None - description: typing.Optional[str] = Aliased("desc", default=None) + icon_url: str | None = None + description: str | None = Aliased("desc", default=None) type: int @@ -138,11 +138,11 @@ class FullHoyolabUser(PartialHoyolabUser): Not actually full, but most of the data is useless. """ - certification: typing.Optional[HoyolabUserCertification] = None - level: typing.Optional[HoyolabUserLevel] = None + certification: HoyolabUserCertification | None = None + level: HoyolabUserLevel | None = None pendant_url: str = Aliased("pendant") - bg_url: typing.Optional[str] = None - pc_bg_url: typing.Optional[str] = None + bg_url: str | None = None + pc_bg_url: str | None = None class RecordCard(GenshinAccount): @@ -176,7 +176,7 @@ def __new__(cls, **kwargs: typing.Any) -> RecordCard: has_uid: bool = Aliased("has_role") url: str - def as_dict(self) -> typing.Dict[str, typing.Any]: + def as_dict(self) -> dict[str, typing.Any]: """Return data as a dictionary.""" return {d.name: (int(d.value) if d.value.isdigit() else d.value) for d in self.data} diff --git a/genshin/models/model.py b/genshin/models/model.py index b2742263..a60fb4f6 100644 --- a/genshin/models/model.py +++ b/genshin/models/model.py @@ -33,7 +33,7 @@ def __hash__(self) -> int: def Aliased( - alias: typing.Optional[str] = None, + alias: str | None = None, default: typing.Any = None, **kwargs: typing.Any, ) -> typing.Any: diff --git a/genshin/models/starrail/chronicle/challenge.py b/genshin/models/starrail/chronicle/challenge.py index 81ac637b..b9678a5c 100644 --- a/genshin/models/starrail/chronicle/challenge.py +++ b/genshin/models/starrail/chronicle/challenge.py @@ -1,6 +1,6 @@ """Starrail chronicle challenge.""" -from typing import Any, Dict, List, Optional +from typing import Any, Optional import pydantic @@ -30,7 +30,7 @@ class FloorNode(APIModel): """Node for a memory of chaos floor.""" challenge_time: PartialTime - avatars: List[FloorCharacter] + avatars: list[FloorCharacter] class StarRailChallengeFloor(APIModel): @@ -74,13 +74,13 @@ class StarRailChallenge(APIModel): total_battles: int = Aliased("battle_num") has_data: bool - floors: List[StarRailFloor] = Aliased("all_floor_detail") - seasons: List[StarRailChallengeSeason] = Aliased("groups") + floors: list[StarRailFloor] = Aliased("all_floor_detail") + seasons: list[StarRailChallengeSeason] = Aliased("groups") @pydantic.model_validator(mode="before") - def __extract_name(cls, values: Dict[str, Any]) -> Dict[str, Any]: - if "groups" in values and isinstance(values["groups"], List): - seasons: List[Dict[str, Any]] = values["groups"] + def __extract_name(cls, values: dict[str, Any]) -> dict[str, Any]: + if "groups" in values and isinstance(values["groups"], list): + seasons: list[dict[str, Any]] = values["groups"] if len(seasons) > 0: values["name"] = seasons[0]["name_mi18n"] @@ -129,14 +129,14 @@ class StarRailPureFiction(APIModel): total_battles: int = Aliased("battle_num") has_data: bool - floors: List[FictionFloor] = Aliased("all_floor_detail") - seasons: List[StarRailChallengeSeason] = Aliased("groups") + floors: list[FictionFloor] = Aliased("all_floor_detail") + seasons: list[StarRailChallengeSeason] = Aliased("groups") max_floor_id: int @pydantic.model_validator(mode="before") - def __unnest_groups(cls, values: Dict[str, Any]) -> Dict[str, Any]: - if "groups" in values and isinstance(values["groups"], List): - seasons: List[Dict[str, Any]] = values["groups"] + def __unnest_groups(cls, values: dict[str, Any]) -> dict[str, Any]: + if "groups" in values and isinstance(values["groups"], list): + seasons: list[dict[str, Any]] = values["groups"] if len(seasons) > 0: values["name"] = seasons[0]["name_mi18n"] values["season_id"] = seasons[0]["schedule_id"] @@ -196,6 +196,6 @@ class StarRailAPCShadow(APIModel): total_battles: int = Aliased("battle_num") has_data: bool - floors: List[APCShadowFloor] = Aliased("all_floor_detail") - seasons: List[APCShadowSeason] = Aliased("groups") + floors: list[APCShadowFloor] = Aliased("all_floor_detail") + seasons: list[APCShadowSeason] = Aliased("groups") max_floor_id: int diff --git a/genshin/models/starrail/chronicle/notes.py b/genshin/models/starrail/chronicle/notes.py index 08ef7dfd..cfc26d04 100644 --- a/genshin/models/starrail/chronicle/notes.py +++ b/genshin/models/starrail/chronicle/notes.py @@ -11,7 +11,7 @@ class StarRailExpedition(APIModel): """StarRail expedition.""" - avatars: typing.List[str] + avatars: list[str] status: typing.Literal["Ongoing", "Finished"] remaining_time: datetime.timedelta name: str diff --git a/genshin/models/starrail/chronicle/rogue.py b/genshin/models/starrail/chronicle/rogue.py index ea2b5329..a3882b36 100644 --- a/genshin/models/starrail/chronicle/rogue.py +++ b/genshin/models/starrail/chronicle/rogue.py @@ -1,6 +1,5 @@ """Starrail Rogue models.""" -from typing import List from genshin.models.model import APIModel @@ -54,7 +53,7 @@ class RogueBuff(APIModel): """Rogue buff info.""" base_type: RogueBuffType - items: List[RogueBuffItem] + items: list[RogueBuffItem] class RogueMiracle(APIModel): @@ -71,11 +70,11 @@ class RogueRecordDetail(APIModel): name: str finish_time: PartialTime score: int - final_lineup: List[RogueCharacter] - base_type_list: List[RogueBuffType] - cached_avatars: List[RogueCharacter] - buffs: List[RogueBuff] - miracles: List[RogueMiracle] + final_lineup: list[RogueCharacter] + base_type_list: list[RogueBuffType] + cached_avatars: list[RogueCharacter] + buffs: list[RogueBuff] + miracles: list[RogueMiracle] difficulty: int progress: int @@ -84,7 +83,7 @@ class RogueRecord(APIModel): """generic record data.""" basic: RogueRecordBasic - records: List[RogueRecordDetail] + records: list[RogueRecordDetail] has_data: bool diff --git a/genshin/models/zzz/chronicle/challenge.py b/genshin/models/zzz/chronicle/challenge.py index 9c47926e..445c4d29 100644 --- a/genshin/models/zzz/chronicle/challenge.py +++ b/genshin/models/zzz/chronicle/challenge.py @@ -70,18 +70,18 @@ def __parse_weakness(cls, value: int) -> typing.Optional[ZZZElementType]: class ShiyuDefenseNode(APIModel): """Shiyu Defense node model.""" - characters: typing.List[ShiyuDefenseCharacter] = Aliased("avatars") + characters: list[ShiyuDefenseCharacter] = Aliased("avatars") bangboo: ShiyuDefenseBangboo = Aliased("buddy") - recommended_elements: typing.List[ZZZElementType] = Aliased("element_type_list") - enemies: typing.List[ShiyuDefenseMonster] = Aliased("monster_info") + recommended_elements: list[ZZZElementType] = Aliased("element_type_list") + enemies: list[ShiyuDefenseMonster] = Aliased("monster_info") @pydantic.field_validator("enemies", mode="before") @classmethod def __convert_enemies( - cls, value: typing.Dict[typing.Literal["level", "list"], typing.Any] - ) -> typing.List[ShiyuDefenseMonster]: + cls, value: dict[typing.Literal["level", "list"], typing.Any] + ) -> list[ShiyuDefenseMonster]: level = value["level"] - result: typing.List[ShiyuDefenseMonster] = [] + result: list[ShiyuDefenseMonster] = [] for monster in value["list"]: monster["level"] = level result.append(ShiyuDefenseMonster(**monster)) @@ -94,7 +94,7 @@ class ShiyuDefenseFloor(APIModel): index: int = Aliased("layer_index") rating: typing.Literal["S", "A", "B"] id: int = Aliased("layer_id") - buffs: typing.List[ShiyuDefenseBuff] + buffs: list[ShiyuDefenseBuff] node_1: ShiyuDefenseNode node_2: ShiyuDefenseNode challenge_time: DateTimeField = Aliased("floor_challenge_time") @@ -116,7 +116,7 @@ class ShiyuDefense(APIModel): end_time: typing.Optional[DateTimeField] = Aliased("hadal_end_time") has_data: bool ratings: typing.Mapping[typing.Literal["S", "A", "B"], int] = Aliased("rating_list") - floors: typing.List[ShiyuDefenseFloor] = Aliased("all_floor_detail") + floors: list[ShiyuDefenseFloor] = Aliased("all_floor_detail") fastest_clear_time: int = Aliased("fast_layer_time") """Fastest clear time this season in seconds.""" max_floor: int = Aliased("max_layer") @@ -131,6 +131,6 @@ def __parse_datetime(cls, value: typing.Mapping[str, typing.Any]) -> typing.Opti @pydantic.field_validator("ratings", mode="before") @classmethod def __convert_ratings( - cls, v: typing.List[typing.Dict[typing.Literal["times", "rating"], typing.Any]] + cls, v: list[dict[typing.Literal["times", "rating"], typing.Any]] ) -> typing.Mapping[typing.Literal["S", "A", "B"], int]: return {d["rating"]: d["times"] for d in v} diff --git a/genshin/models/zzz/chronicle/notes.py b/genshin/models/zzz/chronicle/notes.py index d2dafb88..090159bc 100644 --- a/genshin/models/zzz/chronicle/notes.py +++ b/genshin/models/zzz/chronicle/notes.py @@ -37,7 +37,7 @@ def full_datetime(self) -> datetime.datetime: return datetime.datetime.now().astimezone() + datetime.timedelta(seconds=self.seconds_till_full) @pydantic.model_validator(mode="before") - def __unnest_progress(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __unnest_progress(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: return {**values, **values.pop("progress")} @@ -61,6 +61,6 @@ def __transform_value(cls, v: typing.Literal["CardSignDone", "CardSignNotDone"]) return v == "CardSignDone" @pydantic.model_validator(mode="before") - def __unnest_value(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __unnest_value(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: values["video_store_state"] = values["vhs_sale"]["sale_state"] return values diff --git a/genshin/paginators/api.py b/genshin/paginators/api.py index 8201a84f..580b29bd 100644 --- a/genshin/paginators/api.py +++ b/genshin/paginators/api.py @@ -30,7 +30,7 @@ async def __call__(self, page: int, /) -> typing.Sequence[T_co]: class TokenGetterCallback(typing.Protocol[T_co]): """Callback for returning resources based on a page or cursor.""" - async def __call__(self, token: str, /) -> typing.Tuple[str, typing.Sequence[T_co]]: + async def __call__(self, token: str, /) -> tuple[str, typing.Sequence[T_co]]: """Return a sequence of resources.""" ... @@ -55,18 +55,18 @@ class PagedPaginator(typing.Generic[T], APIPaginator[T]): getter: GetterCallback[T] """Underlying getter that yields the next page.""" - _page_size: typing.Optional[int] + _page_size: int | None """Expected non-zero page size to be able to tell the end.""" - current_page: typing.Optional[int] + current_page: int | None """Current page counter..""" def __init__( self, getter: GetterCallback[T], *, - limit: typing.Optional[int] = None, - page_size: typing.Optional[int] = None, + limit: int | None = None, + page_size: int | None = None, ) -> None: super().__init__(limit=limit) self.getter = getter @@ -74,7 +74,7 @@ def __init__( self.current_page = 1 - async def next_page(self) -> typing.Optional[typing.Iterable[T]]: + async def next_page(self) -> typing.Iterable[T] | None: """Get the next page of the paginator.""" if self.current_page is None: return None @@ -101,17 +101,17 @@ class TokenPaginator(typing.Generic[T], APIPaginator[T]): getter: TokenGetterCallback[T] """Underlying getter that yields the next page.""" - _page_size: typing.Optional[int] + _page_size: int | None """Expected non-zero page size to be able to tell the end.""" - token: typing.Optional[str] + token: str | None def __init__( self, getter: TokenGetterCallback[T], *, - limit: typing.Optional[int] = None, - page_size: typing.Optional[int] = None, + limit: int | None = None, + page_size: int | None = None, ) -> None: super().__init__(limit=limit) self.getter = getter @@ -119,7 +119,7 @@ def __init__( self.token = "" - async def next_page(self) -> typing.Optional[typing.Iterable[T]]: + async def next_page(self) -> typing.Iterable[T] | None: """Get the next page of the paginator.""" if self.token is None: return None @@ -145,19 +145,19 @@ class CursorPaginator(typing.Generic[UniqueT], APIPaginator[UniqueT]): getter: GetterCallback[UniqueT] """Underlying getter that yields the next page.""" - _page_size: typing.Optional[int] + _page_size: int | None """Expected non-zero page size to be able to tell the end.""" - end_id: typing.Optional[int] + end_id: int | None """Current end id. If none then exhausted.""" def __init__( self, getter: GetterCallback[UniqueT], *, - limit: typing.Optional[int] = None, + limit: int | None = None, end_id: int = 0, - page_size: typing.Optional[int] = 20, + page_size: int | None = 20, ) -> None: super().__init__(limit=limit) self.getter = getter @@ -165,7 +165,7 @@ def __init__( self._page_size = page_size - async def next_page(self) -> typing.Optional[typing.Iterable[UniqueT]]: + async def next_page(self) -> typing.Iterable[UniqueT] | None: """Get the next page of the paginator.""" if self.end_id is None: return None diff --git a/genshin/paginators/base.py b/genshin/paginators/base.py index a466c59a..f9cb8591 100644 --- a/genshin/paginators/base.py +++ b/genshin/paginators/base.py @@ -102,7 +102,7 @@ class BasicPaginator(typing.Generic[T], Paginator[T], abc.ABC): iterator: typing.AsyncIterator[T] """Underlying iterator.""" - def __init__(self, iterable: typing.Union[typing.Iterable[T], typing.AsyncIterable[T]]) -> None: + def __init__(self, iterable: typing.Iterable[T] | typing.AsyncIterable[T]) -> None: if isinstance(iterable, typing.AsyncIterable): self.iterator = iterable.__aiter__() else: @@ -120,16 +120,16 @@ class BufferedPaginator(typing.Generic[T], Paginator[T], abc.ABC): __slots__ = ("limit", "_buffer", "_counter") - limit: typing.Optional[int] + limit: int | None """Limit of items to be yielded.""" - _buffer: typing.Optional[typing.Iterator[T]] + _buffer: typing.Iterator[T] | None """Item buffer. If none then exhausted.""" _counter: int """Amount of yielded items so far. No guarantee to be synchronized.""" - def __init__(self, *, limit: typing.Optional[int] = None) -> None: + def __init__(self, *, limit: int | None = None) -> None: self.limit = limit self._buffer = iter(()) @@ -147,7 +147,7 @@ def _complete(self) -> typing.NoReturn: raise # pyright bug @abc.abstractmethod - async def next_page(self) -> typing.Optional[typing.Iterable[T]]: + async def next_page(self) -> typing.Iterable[T] | None: """Get the next page of the paginator.""" async def __anext__(self) -> T: @@ -185,16 +185,16 @@ class MergedPaginator(typing.Generic[T], Paginator[T]): Only used as pointers to a heap. """ - _heap: typing.List[typing.Tuple[typing.Any, int, T, typing.AsyncIterator[T]]] + _heap: list[tuple[typing.Any, int, T, typing.AsyncIterator[T]]] """Underlying heap queue. List of (comparable, unique order id, value, iterator) """ - limit: typing.Optional[int] + limit: int | None """Limit of items to be yielded""" - _key: typing.Optional[typing.Callable[[T], typing.Any]] + _key: typing.Callable[[T], typing.Any] | None """Sorting key.""" _prepared: bool @@ -207,8 +207,8 @@ def __init__( self, iterables: typing.Collection[typing.AsyncIterable[T]], *, - key: typing.Optional[typing.Callable[[T], typing.Any]] = None, - limit: typing.Optional[int] = None, + key: typing.Callable[[T], typing.Any] | None = None, + limit: int | None = None, ) -> None: self.iterators = [iterable.__aiter__() for iterable in iterables] self._key = key @@ -230,8 +230,8 @@ def _create_heap_item( self, value: T, iterator: typing.AsyncIterator[T], - order: typing.Optional[int] = None, - ) -> typing.Tuple[typing.Any, int, T, typing.AsyncIterator[T]]: + order: int | None = None, + ) -> tuple[typing.Any, int, T, typing.AsyncIterator[T]]: """Create a new item for the heap queue.""" sort_value = self._key(value) if self._key else value if order is None: diff --git a/genshin/utility/auth.py b/genshin/utility/auth.py index c0de46e4..5634f725 100644 --- a/genshin/utility/auth.py +++ b/genshin/utility/auth.py @@ -152,12 +152,12 @@ def encrypt_credentials(text: str, key_type: typing.Literal[1, 2]) -> str: return base64.b64encode(crypto).decode("utf-8") -def get_aigis_header(session_id: str, mmt_data: typing.Dict[str, typing.Any]) -> str: +def get_aigis_header(session_id: str, mmt_data: dict[str, typing.Any]) -> str: """Get aigis header.""" return f"{session_id};{base64.b64encode(json.dumps(mmt_data).encode()).decode()}" -def generate_sign(data: typing.Dict[str, typing.Any], key: str) -> str: +def generate_sign(data: dict[str, typing.Any], key: str) -> str: """Generate a sign for the given `data` and `app_key`.""" string = "" for k in sorted(data.keys()): diff --git a/genshin/utility/concurrency.py b/genshin/utility/concurrency.py index 681a5cf5..6b1894b6 100644 --- a/genshin/utility/concurrency.py +++ b/genshin/utility/concurrency.py @@ -20,7 +20,7 @@ def prevent_concurrency(func: CallableT) -> CallableT: """ def wrapper(func: AnyCallable) -> AnyCallable: - lock: typing.Optional[asyncio.Lock] = None + lock: asyncio.Lock | None = None @functools.wraps(func) async def inner(*args: typing.Any, **kwargs: typing.Any) -> typing.Any: @@ -48,7 +48,7 @@ def __init__( method: AnyCallable, decorator: typing.Callable[[AnyCallable], AnyCallable], *, - name: typing.Optional[str] = None, + name: str | None = None, ) -> None: self.method = method # type: ignore # mypy doesn't understand methods self.decorator = decorator # type: ignore # mypy doesn't understand methods @@ -57,7 +57,7 @@ def __init__( def __set_name__(self, owner: type, name: str) -> None: self.name = name - def __get__(self, instance: typing.Optional[T], owner: typing.Type[T]) -> AnyCallable: + def __get__(self, instance: T | None, owner: type[T]) -> AnyCallable: if instance is None: return self.method diff --git a/genshin/utility/ds.py b/genshin/utility/ds.py index d9a47951..7a95a3aa 100644 --- a/genshin/utility/ds.py +++ b/genshin/utility/ds.py @@ -47,7 +47,7 @@ def get_ds_headers( data: typing.Any = None, params: typing.Optional[typing.Mapping[str, typing.Any]] = None, lang: typing.Optional[str] = None, -) -> typing.Dict[str, typing.Any]: +) -> dict[str, typing.Any]: """Get ds http headers.""" if region == types.Region.OVERSEAS: ds_headers = { diff --git a/genshin/utility/logfile.py b/genshin/utility/logfile.py index f0b6719b..50f0e8c8 100644 --- a/genshin/utility/logfile.py +++ b/genshin/utility/logfile.py @@ -49,7 +49,7 @@ def get_output_log(*, game: typing.Optional[types.Game] = None) -> pathlib.Path: """Get output_log.txt for a game.""" locallow = pathlib.Path("~/AppData/LocalLow").expanduser() - game_name: typing.List[str] = [] + game_name: list[str] = [] if game is None or game == types.Game.GENSHIN: game_name += ["Genshin Impact", "原神"] if game is None or game == types.Game.STARRAIL: @@ -70,7 +70,7 @@ def get_output_log(*, game: typing.Optional[types.Game] = None) -> pathlib.Path: def _expand_game_location(game_location: pathlib.Path, *, game: typing.Optional[types.Game] = None) -> pathlib.Path: """Expand a game location folder to data_2.""" - data_location: typing.List[pathlib.Path] = [] + data_location: list[pathlib.Path] = [] if "Data" in str(game_location): while "Data" not in game_location.name: game_location = game_location.parent diff --git a/tests/conftest.py b/tests/conftest.py index 3b2b502b..66c6d9a4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -209,7 +209,7 @@ def pytest_addoption(parser: pytest.Parser): parser.addoption("--cooperative", action="store_true") -def pytest_collection_modifyitems(items: typing.List[pytest.Item], config: pytest.Config): +def pytest_collection_modifyitems(items: list[pytest.Item], config: pytest.Config): if config.option.cooperative: for item in items: if isinstance(item, pytest.Function) and asyncio.iscoroutinefunction(item.obj): From 406770f3c9343d7a0a87295bf18fe7e0a14106df Mon Sep 17 00:00:00 2001 From: seriaati Date: Sun, 22 Sep 2024 08:44:18 +0800 Subject: [PATCH 18/21] Revert "PyUpgrade unsafe fixes" This reverts commit 39597d685513e2099984648760e63991a87111ae. --- genshin-dev/setup.py | 8 +- genshin/__main__.py | 2 +- genshin/client/cache.py | 22 ++--- genshin/client/compatibility.py | 22 ++--- genshin/client/components/auth/server.py | 18 ++-- .../client/components/auth/subclients/app.py | 2 +- genshin/client/components/base.py | 10 +- .../components/calculator/calculator.py | 96 +++++++++---------- .../client/components/calculator/client.py | 74 +++++++------- genshin/client/components/chronicle/base.py | 4 +- genshin/client/components/gacha.py | 8 +- genshin/client/components/hoyolab.py | 4 +- genshin/client/components/lineup.py | 6 +- genshin/client/components/transaction.py | 6 +- genshin/client/manager/cookie.py | 2 +- genshin/client/manager/managers.py | 52 +++++----- genshin/client/ratelimit.py | 2 +- genshin/errors.py | 12 +-- genshin/models/auth/cookie.py | 6 +- genshin/models/auth/geetest.py | 4 +- genshin/models/auth/verification.py | 2 +- genshin/models/genshin/calculator.py | 16 ++-- genshin/models/genshin/character.py | 2 +- genshin/models/genshin/chronicle/abyss.py | 2 +- .../models/genshin/chronicle/activities.py | 4 +- .../models/genshin/chronicle/characters.py | 14 +-- .../models/genshin/chronicle/img_theater.py | 4 +- genshin/models/genshin/chronicle/notes.py | 4 +- genshin/models/genshin/chronicle/stats.py | 6 +- genshin/models/genshin/constants.py | 2 +- genshin/models/genshin/gacha.py | 6 +- genshin/models/genshin/lineup.py | 6 +- genshin/models/genshin/teapot.py | 6 +- genshin/models/genshin/wiki.py | 16 ++-- .../models/honkai/chronicle/battlesuits.py | 2 +- genshin/models/honkai/chronicle/modes.py | 10 +- genshin/models/honkai/chronicle/stats.py | 2 +- genshin/models/honkai/constants.py | 2 +- genshin/models/hoyolab/record.py | 14 +-- genshin/models/model.py | 2 +- .../models/starrail/chronicle/challenge.py | 28 +++--- genshin/models/starrail/chronicle/notes.py | 2 +- genshin/models/starrail/chronicle/rogue.py | 15 +-- genshin/models/zzz/chronicle/challenge.py | 18 ++-- genshin/models/zzz/chronicle/notes.py | 4 +- genshin/paginators/api.py | 32 +++---- genshin/paginators/base.py | 24 ++--- genshin/utility/auth.py | 4 +- genshin/utility/concurrency.py | 6 +- genshin/utility/ds.py | 2 +- genshin/utility/logfile.py | 4 +- tests/conftest.py | 2 +- 52 files changed, 312 insertions(+), 311 deletions(-) diff --git a/genshin-dev/setup.py b/genshin-dev/setup.py index ee204edd..712ff131 100644 --- a/genshin-dev/setup.py +++ b/genshin-dev/setup.py @@ -6,12 +6,12 @@ import setuptools -def parse_requirements_file(path: pathlib.Path) -> list[str]: +def parse_requirements_file(path: pathlib.Path) -> typing.List[str]: """Parse a requirements file into a list of requirements.""" with open(path) as fp: raw_dependencies = fp.readlines() - dependencies: list[str] = [] + dependencies: typing.List[str] = [] for dependency in raw_dependencies: comment_index = dependency.find("#") if comment_index == 0: @@ -30,8 +30,8 @@ def parse_requirements_file(path: pathlib.Path) -> list[str]: normal_requirements = parse_requirements_file(dev_directory / ".." / "requirements.txt") -all_extras: set[str] = set() -extras: dict[str, typing.Sequence[str]] = {} +all_extras: typing.Set[str] = set() +extras: typing.Dict[str, typing.Sequence[str]] = {} for path in dev_directory.glob("*-requirements.txt"): name = path.name.split("-")[0] diff --git a/genshin/__main__.py b/genshin/__main__.py index 158f830c..7a422299 100644 --- a/genshin/__main__.py +++ b/genshin/__main__.py @@ -85,7 +85,7 @@ async def honkai_stats(client: genshin.Client, uid: int) -> None: for k, v in data.stats.model_dump().items(): if isinstance(v, dict): click.echo(f"{k}:") - for nested_k, nested_v in typing.cast("dict[str, object]", v).items(): + for nested_k, nested_v in typing.cast("typing.Dict[str, object]", v).items(): click.echo(f" {nested_k}: {click.style(str(nested_v), bold=True)}") else: click.echo(f"{k}: {click.style(str(v), bold=True)}") diff --git a/genshin/client/cache.py b/genshin/client/cache.py index 1a7729ba..e31743fe 100644 --- a/genshin/client/cache.py +++ b/genshin/client/cache.py @@ -25,7 +25,7 @@ def _separate(values: typing.Iterable[typing.Any], sep: str = ":") -> str: """Separate a sequence by a separator into a single string.""" - parts: list[str] = [] + parts: typing.List[str] = [] for value in values: if value is None: parts.append("null") @@ -64,7 +64,7 @@ class BaseCache(abc.ABC): """Base cache for the client.""" @abc.abstractmethod - async def get(self, key: typing.Any) -> typing.Any | None: + async def get(self, key: typing.Any) -> typing.Optional[typing.Any]: """Get an object with a key.""" @abc.abstractmethod @@ -72,7 +72,7 @@ async def set(self, key: typing.Any, value: typing.Any) -> None: """Save an object with a key.""" @abc.abstractmethod - async def get_static(self, key: typing.Any) -> typing.Any | None: + async def get_static(self, key: typing.Any) -> typing.Optional[typing.Any]: """Get a static object with a key.""" @abc.abstractmethod @@ -83,7 +83,7 @@ async def set_static(self, key: typing.Any, value: typing.Any) -> None: class Cache(BaseCache): """Standard implementation of the cache.""" - cache: dict[typing.Any, tuple[float, typing.Any]] + cache: typing.Dict[typing.Any, typing.Tuple[float, typing.Any]] maxsize: int ttl: float static_ttl: float @@ -115,7 +115,7 @@ def _clear_cache(self) -> None: for key in keys: del self.cache[key] - async def get(self, key: typing.Any) -> typing.Any | None: + async def get(self, key: typing.Any) -> typing.Optional[typing.Any]: """Get an object with a key.""" self._clear_cache() @@ -130,7 +130,7 @@ async def set(self, key: typing.Any, value: typing.Any) -> None: self._clear_cache() - async def get_static(self, key: typing.Any) -> typing.Any | None: + async def get_static(self, key: typing.Any) -> typing.Optional[typing.Any]: """Get a static object with a key.""" return await self.get(key) @@ -167,7 +167,7 @@ def serialize_key(self, key: typing.Any) -> str: """Serialize a key by turning it into a string.""" return str(key) - def serialize_value(self, value: typing.Any) -> str | bytes: + def serialize_value(self, value: typing.Any) -> typing.Union[str, bytes]: """Serialize a value by turning it into bytes.""" return json.dumps(value) @@ -175,7 +175,7 @@ def deserialize_value(self, value: bytes) -> typing.Any: """Deserialize a value back into data.""" return json.loads(value) - async def get(self, key: typing.Any) -> typing.Any | None: + async def get(self, key: typing.Any) -> typing.Optional[typing.Any]: """Get an object with a key.""" value = typing.cast("typing.Optional[bytes]", await self.redis.get(self.serialize_key(key))) # pyright: ignore if value is None: @@ -191,7 +191,7 @@ async def set(self, key: typing.Any, value: typing.Any) -> None: ex=self.ttl, ) - async def get_static(self, key: typing.Any) -> typing.Any | None: + async def get_static(self, key: typing.Any) -> typing.Optional[typing.Any]: """Get a static object with a key.""" return await self.get(key) @@ -258,7 +258,7 @@ def deserialize_value(self, value: str) -> typing.Any: """Deserialize a value back into data.""" return json.loads(value) - async def get(self, key: typing.Any) -> typing.Any | None: + async def get(self, key: typing.Any) -> typing.Optional[typing.Any]: """Get an object with a key.""" import aiosqlite @@ -299,7 +299,7 @@ async def set(self, key: typing.Any, value: typing.Any) -> None: if self.conn is None: await conn.close() - async def get_static(self, key: typing.Any) -> typing.Any | None: + async def get_static(self, key: typing.Any) -> typing.Optional[typing.Any]: """Get a static object with a key.""" return await self.get(key) diff --git a/genshin/client/compatibility.py b/genshin/client/compatibility.py index 2f96ecce..ce4d8fc2 100644 --- a/genshin/client/compatibility.py +++ b/genshin/client/compatibility.py @@ -24,8 +24,8 @@ class GenshinClient(clients.Client): def __init__( self, - cookies: typing.Any | None = None, - authkey: str | None = None, + cookies: typing.Optional[typing.Any] = None, + authkey: typing.Optional[str] = None, *, lang: str = "en-us", region: types.Region = types.Region.OVERSEAS, @@ -59,7 +59,7 @@ def cookies(self, cookies: typing.Mapping[str, typing.Any]) -> None: setattr(self.cookie_manager, "cookies", cookies) @property - def uid(self) -> int | None: + def uid(self) -> typing.Optional[int]: deprecation.warn_deprecated(self.__class__.uid, alternative="Client.uids[genshin.Game.GENSHIN]") return self.uids[types.Game.GENSHIN] @@ -83,7 +83,7 @@ async def get_partial_user( self, uid: int, *, - lang: str | None = None, + lang: typing.Optional[str] = None, ) -> models.PartialGenshinUserStats: """Get partial genshin user without character equipment.""" return await self.get_partial_genshin_user(uid, lang=lang) @@ -93,7 +93,7 @@ async def get_characters( self, uid: int, *, - lang: str | None = None, + lang: typing.Optional[str] = None, ) -> typing.Sequence[models.Character]: """Get genshin user characters.""" return await self.get_genshin_characters(uid, lang=lang) @@ -103,7 +103,7 @@ async def get_user( self, uid: int, *, - lang: str | None = None, + lang: typing.Optional[str] = None, ) -> models.GenshinUserStats: """Get genshin user.""" return await self.get_genshin_user(uid, lang=lang) @@ -113,7 +113,7 @@ async def get_full_user( self, uid: int, *, - lang: str | None = None, + lang: typing.Optional[str] = None, ) -> models.FullGenshinUserStats: """Get a user with all their possible data.""" return await self.get_full_genshin_user(uid, lang=lang) @@ -129,8 +129,8 @@ class ChineseClient(GenshinClient): def __init__( self, - cookies: typing.Mapping[str, str] | None = None, - authkey: str | None = None, + cookies: typing.Optional[typing.Mapping[str, str]] = None, + authkey: typing.Optional[str] = None, *, lang: str = "zh-cn", debug: bool = False, @@ -154,7 +154,7 @@ class MultiCookieClient(GenshinClient): def __init__( self, - cookie_list: typing.Sequence[typing.Mapping[str, str]] | None = None, + cookie_list: typing.Optional[typing.Sequence[typing.Mapping[str, str]]] = None, *, lang: str = "en-us", debug: bool = False, @@ -176,7 +176,7 @@ class ChineseMultiCookieClient(GenshinClient): def __init__( self, - cookie_list: typing.Sequence[typing.Mapping[str, str]] | None = None, + cookie_list: typing.Optional[typing.Sequence[typing.Mapping[str, str]]] = None, *, lang: str = "en-us", debug: bool = False, diff --git a/genshin/client/components/auth/server.py b/genshin/client/components/auth/server.py index f69a3a2f..7baf267d 100644 --- a/genshin/client/components/auth/server.py +++ b/genshin/client/components/auth/server.py @@ -25,7 +25,7 @@ __all__ = ["PAGES", "enter_code", "launch_webapp", "solve_geetest"] -PAGES: typing.Final[dict[typing.Literal["captcha", "enter-code"], str]] = { +PAGES: typing.Final[typing.Dict[typing.Literal["captcha", "enter-code"], str]] = { "captcha": """ @@ -112,11 +112,11 @@ async def launch_webapp( page: typing.Literal["captcha"], *, - mmt: MMT | MMTv4 | SessionMMT | SessionMMTv4 | RiskyCheckMMT, + mmt: typing.Union[MMT, MMTv4, SessionMMT, SessionMMTv4, RiskyCheckMMT], lang: str = ..., api_server: str = ..., port: int = ..., -) -> MMTResult | MMTv4Result | SessionMMTResult | SessionMMTv4Result | RiskyCheckMMTResult: ... +) -> typing.Union[MMTResult, MMTv4Result, SessionMMTResult, SessionMMTv4Result, RiskyCheckMMTResult]: ... @typing.overload async def launch_webapp( page: typing.Literal["enter-code"], @@ -129,11 +129,11 @@ async def launch_webapp( async def launch_webapp( page: typing.Literal["captcha", "enter-code"], *, - mmt: MMT | MMTv4 | SessionMMT | SessionMMTv4 | RiskyCheckMMT | None = None, - lang: str | None = None, - api_server: str | None = None, + mmt: typing.Optional[typing.Union[MMT, MMTv4, SessionMMT, SessionMMTv4, RiskyCheckMMT]] = None, + lang: typing.Optional[str] = None, + api_server: typing.Optional[str] = None, port: int = 5000, -) -> MMTResult | MMTv4Result | SessionMMTResult | SessionMMTv4Result | RiskyCheckMMTResult | str: +) -> typing.Union[MMTResult, MMTv4Result, SessionMMTResult, SessionMMTv4Result, RiskyCheckMMTResult, str]: """Create and run a webapp to solve captcha or enter a verification code.""" routes = web.RouteTableDef() future: asyncio.Future[typing.Any] = asyncio.Future() @@ -244,12 +244,12 @@ async def solve_geetest( port: int = ..., ) -> MMTv4Result: ... async def solve_geetest( - mmt: MMT | MMTv4 | SessionMMT | SessionMMTv4 | RiskyCheckMMT, + mmt: typing.Union[MMT, MMTv4, SessionMMT, SessionMMTv4, RiskyCheckMMT], *, lang: str = "en-us", api_server: str = "api-na.geetest.com", port: int = 5000, -) -> MMTResult | MMTv4Result | SessionMMTResult | SessionMMTv4Result | RiskyCheckMMTResult: +) -> typing.Union[MMTResult, MMTv4Result, SessionMMTResult, SessionMMTv4Result, RiskyCheckMMTResult]: """Start a web server and manually solve geetest captcha.""" lang = auth_utility.lang_to_geetest_lang(lang) return await launch_webapp( diff --git a/genshin/client/components/auth/subclients/app.py b/genshin/client/components/auth/subclients/app.py index 6f0d599a..0af38edb 100644 --- a/genshin/client/components/auth/subclients/app.py +++ b/genshin/client/components/auth/subclients/app.py @@ -194,7 +194,7 @@ async def _create_qrcode(self) -> QRCodeCreationResult: url=data["data"]["url"], ) - async def _check_qrcode(self, ticket: str) -> tuple[QRCodeStatus, SimpleCookie]: + async def _check_qrcode(self, ticket: str) -> typing.Tuple[QRCodeStatus, SimpleCookie]: """Check the status of a QR code login.""" payload = {"ticket": ticket} diff --git a/genshin/client/components/base.py b/genshin/client/components/base.py index b7956c4e..851080b0 100644 --- a/genshin/client/components/base.py +++ b/genshin/client/components/base.py @@ -62,10 +62,10 @@ class BaseClient(abc.ABC): _region: types.Region _default_game: typing.Optional[types.Game] - uids: dict[types.Game, int] - authkeys: dict[types.Game, str] + uids: typing.Dict[types.Game, int] + authkeys: typing.Dict[types.Game, str] _hoyolab_id: typing.Optional[int] - _accounts: dict[types.Game, hoyolab_models.GenshinAccount] + _accounts: typing.Dict[types.Game, hoyolab_models.GenshinAccount] custom_headers: multidict.CIMultiDict[str] def __init__( @@ -500,7 +500,7 @@ async def _update_cached_uids(self) -> None: """Update cached fallback uids.""" mixed_accounts = await self.get_game_accounts() - game_accounts: dict[types.Game, list[hoyolab_models.GenshinAccount]] = {} + game_accounts: typing.Dict[types.Game, typing.List[hoyolab_models.GenshinAccount]] = {} for account in mixed_accounts: if not isinstance(account.game, types.Game): # pyright: ignore[reportUnnecessaryIsInstance] continue @@ -533,7 +533,7 @@ async def _update_cached_accounts(self) -> None: """Update cached fallback accounts.""" mixed_accounts = await self.get_game_accounts() - game_accounts: dict[types.Game, list[hoyolab_models.GenshinAccount]] = {} + game_accounts: typing.Dict[types.Game, typing.List[hoyolab_models.GenshinAccount]] = {} for account in mixed_accounts: if not isinstance(account.game, types.Game): # pyright: ignore[reportUnnecessaryIsInstance] continue diff --git a/genshin/client/components/calculator/calculator.py b/genshin/client/components/calculator/calculator.py index b08f61ca..435bd755 100644 --- a/genshin/client/components/calculator/calculator.py +++ b/genshin/client/components/calculator/calculator.py @@ -42,10 +42,10 @@ class CalculatorState: """Stores character details if multiple objects require them.""" client: Client - cache: dict[str, typing.Any] + cache: typing.Dict[str, typing.Any] lock: asyncio.Lock - character_id: int | None = None + character_id: typing.Optional[int] = None def __init__(self, client: Client) -> None: self.client = client @@ -87,10 +87,10 @@ class CharacterResolver(CalculatorResolver[typing.Mapping[str, typing.Any]]): def __init__( self, character: types.IDOr[genshin_models.BaseCharacter], - current: int | None = None, - target: int | None = None, + current: typing.Optional[int] = None, + target: typing.Optional[int] = None, *, - element: int | None = None, + element: typing.Optional[int] = None, ) -> None: if isinstance(character, genshin_models.BaseCharacter): current = current or getattr(character, "level", None) @@ -150,7 +150,7 @@ async def __call__(self, state: CalculatorState) -> typing.Mapping[str, typing.A class ArtifactResolver(CalculatorResolver[typing.Sequence[typing.Mapping[str, typing.Any]]]): - data: list[typing.Mapping[str, typing.Any]] + data: typing.List[typing.Mapping[str, typing.Any]] def __init__(self) -> None: self.data = [] @@ -180,17 +180,17 @@ async def __call__(self, state: CalculatorState) -> typing.Sequence[typing.Mappi class CurrentArtifactResolver(ArtifactResolver): - artifacts: typing.Sequence[int | None] + artifacts: typing.Sequence[typing.Optional[int]] def __init__( self, - target: int | None = None, + target: typing.Optional[int] = None, *, - flower: int | None = None, - feather: int | None = None, - sands: int | None = None, - goblet: int | None = None, - circlet: int | None = None, + flower: typing.Optional[int] = None, + feather: typing.Optional[int] = None, + sands: typing.Optional[int] = None, + goblet: typing.Optional[int] = None, + circlet: typing.Optional[int] = None, ) -> None: if target: self.artifacts = (target,) * 5 @@ -208,7 +208,7 @@ async def __call__(self, state: CalculatorState) -> typing.Sequence[typing.Mappi class TalentResolver(CalculatorResolver[typing.Sequence[typing.Mapping[str, typing.Any]]]): - data: list[typing.Mapping[str, typing.Any]] + data: typing.List[typing.Mapping[str, typing.Any]] def __init__(self) -> None: self.data = [] @@ -221,16 +221,16 @@ async def __call__(self, state: CalculatorState) -> typing.Sequence[typing.Mappi class CurrentTalentResolver(TalentResolver): - talents: typing.Mapping[str, int | None] + talents: typing.Mapping[str, typing.Optional[int]] def __init__( self, - target: int | None = None, - current: int | None = None, + target: typing.Optional[int] = None, + current: typing.Optional[int] = None, *, - attack: int | None = None, - skill: int | None = None, - burst: int | None = None, + attack: typing.Optional[int] = None, + skill: typing.Optional[int] = None, + burst: typing.Optional[int] = None, ) -> None: self.current = current if target: @@ -272,16 +272,16 @@ class Calculator: """Builder for the genshin impact enhancement calculator.""" client: Client - lang: str | None + lang: typing.Optional[str] - character: CharacterResolver | None - weapon: WeaponResolver | None - artifacts: ArtifactResolver | None - talents: TalentResolver | None + character: typing.Optional[CharacterResolver] + weapon: typing.Optional[WeaponResolver] + artifacts: typing.Optional[ArtifactResolver] + talents: typing.Optional[TalentResolver] _state: CalculatorState - def __init__(self, client: Client, *, lang: str | None = None) -> None: + def __init__(self, client: Client, *, lang: typing.Optional[str] = None) -> None: self.client = client self.lang = lang @@ -295,10 +295,10 @@ def __init__(self, client: Client, *, lang: str | None = None) -> None: def set_character( self, character: types.IDOr[genshin_models.BaseCharacter], - current: int | None = None, - target: int | None = None, + current: typing.Optional[int] = None, + target: typing.Optional[int] = None, *, - element: int | None = None, + element: typing.Optional[int] = None, ) -> Calculator: """Set the character.""" self.character = CharacterResolver(character, current, target, element=element) @@ -338,13 +338,13 @@ def with_current_weapon(self, target: int) -> Calculator: def with_current_artifacts( self, - target: int | None = None, + target: typing.Optional[int] = None, *, - flower: int | None = None, - feather: int | None = None, - sands: int | None = None, - goblet: int | None = None, - circlet: int | None = None, + flower: typing.Optional[int] = None, + feather: typing.Optional[int] = None, + sands: typing.Optional[int] = None, + goblet: typing.Optional[int] = None, + circlet: typing.Optional[int] = None, ) -> Calculator: """Add all artifacts of the selected character.""" self.artifacts = CurrentArtifactResolver( @@ -359,12 +359,12 @@ def with_current_artifacts( def with_current_talents( self, - target: int | None = None, - current: int | None = None, + target: typing.Optional[int] = None, + current: typing.Optional[int] = None, *, - attack: int | None = None, - skill: int | None = None, - burst: int | None = None, + attack: typing.Optional[int] = None, + skill: typing.Optional[int] = None, + burst: typing.Optional[int] = None, ) -> Calculator: """Add all talents of the currently selected character.""" self.talents = CurrentTalentResolver( @@ -378,7 +378,7 @@ def with_current_talents( async def build(self) -> typing.Mapping[str, typing.Any]: """Build the calculator object.""" - data: dict[str, typing.Any] = {} + data: typing.Dict[str, typing.Any] = {} if self.character: data.update(await self.character(self._state)) @@ -406,13 +406,13 @@ class FurnishingCalculator: """Builder for the genshin impact furnishing calculator.""" client: Client - lang: str | None + lang: typing.Optional[str] - furnishings: dict[int, int] - replica_code: int | None = None - replica_region: str | None = None + furnishings: typing.Dict[int, int] + replica_code: typing.Optional[int] = None + replica_region: typing.Optional[str] = None - def __init__(self, client: Client, *, lang: str | None = None) -> None: + def __init__(self, client: Client, *, lang: typing.Optional[str] = None) -> None: self.client = client self.lang = lang @@ -426,7 +426,7 @@ def add_furnishing(self, id: types.IDOr[models.CalculatorFurnishing], amount: in self.furnishings[int(id)] += amount return self - def with_replica(self, code: int, *, region: str | None = None) -> FurnishingCalculator: + def with_replica(self, code: int, *, region: typing.Optional[str] = None) -> FurnishingCalculator: """Set the replica code.""" self.replica_code = code self.replica_region = region @@ -434,7 +434,7 @@ def with_replica(self, code: int, *, region: str | None = None) -> FurnishingCal async def build(self) -> typing.Mapping[str, typing.Any]: """Build the calculator object.""" - data: dict[str, typing.Any] = {} + data: typing.Dict[str, typing.Any] = {} if self.replica_code: furnishings = await self.client.get_teapot_replica_blueprint(self.replica_code, region=self.replica_region) diff --git a/genshin/client/components/calculator/client.py b/genshin/client/components/calculator/client.py index 67017af9..2ba59a4d 100644 --- a/genshin/client/components/calculator/client.py +++ b/genshin/client/components/calculator/client.py @@ -33,10 +33,10 @@ async def request_calculator( endpoint: str, *, method: str = "POST", - lang: str | None = None, - params: typing.Mapping[str, typing.Any] | None = None, - data: typing.Mapping[str, typing.Any] | None = None, - headers: aiohttp.typedefs.LooseHeaders | None = None, + lang: typing.Optional[str] = None, + params: typing.Optional[typing.Mapping[str, typing.Any]] = None, + data: typing.Optional[typing.Mapping[str, typing.Any]] = None, + headers: typing.Optional[aiohttp.typedefs.LooseHeaders] = None, **kwargs: typing.Any, ) -> typing.Mapping[str, typing.Any]: """Make a request towards the calculator endpoint.""" @@ -70,7 +70,7 @@ async def _execute_calculator( self, data: typing.Mapping[str, typing.Any], *, - lang: str | None = None, + lang: typing.Optional[str] = None, ) -> models.CalculatorResult: """Calculate the results of a builder.""" data = await self.request_calculator("compute", lang=lang, data=data) @@ -80,17 +80,17 @@ async def _execute_furnishings_calculator( self, data: typing.Mapping[str, typing.Any], *, - lang: str | None = None, + lang: typing.Optional[str] = None, ) -> models.CalculatorFurnishingResults: """Calculate the results of a builder.""" data = await self.request_calculator("furniture/compute", lang=lang, data=data) return models.CalculatorFurnishingResults(**data) - def calculator(self, *, lang: str | None = None) -> Calculator: + def calculator(self, *, lang: typing.Optional[str] = None) -> Calculator: """Create a calculator builder object.""" return Calculator(self, lang=lang) - def furnishings_calculator(self, *, lang: str | None = None) -> FurnishingCalculator: + def furnishings_calculator(self, *, lang: typing.Optional[str] = None) -> FurnishingCalculator: """Create a calculator builder object.""" return FurnishingCalculator(self, lang=lang) @@ -102,12 +102,12 @@ async def _get_calculator_items( self, slug: str, filters: typing.Mapping[str, typing.Any], - query: str | None = None, + query: typing.Optional[str] = None, *, - uid: int | None = None, + uid: typing.Optional[int] = None, is_all: bool = False, sync: bool = False, - lang: str | None = None, + lang: typing.Optional[str] = None, autoauth: bool = True, ) -> typing.Sequence[typing.Mapping[str, typing.Any]]: """Get all items of a specific slug from a calculator.""" @@ -119,14 +119,14 @@ async def _get_calculator_items( filters = dict(keywords=query, **filters) - payload: dict[str, typing.Any] = dict(page=1, size=69420, is_all=is_all, **filters) + payload: typing.Dict[str, typing.Any] = dict(page=1, size=69420, is_all=is_all, **filters) if sync: uid = uid or await self._get_uid(types.Game.GENSHIN) payload["uid"] = uid payload["region"] = utility.recognize_genshin_server(uid) - cache: client_cache.CacheKey | None = None + cache: typing.Optional[client_cache.CacheKey] = None if not any(filters.values()) and not sync: cache = client_cache.cache_key("calculator", slug=slug, lang=lang or self.lang) @@ -146,13 +146,13 @@ async def _get_calculator_items( async def get_calculator_characters( self, *, - query: str | None = None, - elements: typing.Sequence[int] | None = None, - weapon_types: typing.Sequence[int] | None = None, + query: typing.Optional[str] = None, + elements: typing.Optional[typing.Sequence[int]] = None, + weapon_types: typing.Optional[typing.Sequence[int]] = None, include_traveler: bool = False, sync: bool = False, - uid: int | None = None, - lang: str | None = None, + uid: typing.Optional[int] = None, + lang: typing.Optional[str] = None, ) -> typing.Sequence[models.CalculatorCharacter]: """Get all characters provided by the Enhancement Progression Calculator.""" data = await self._get_calculator_items( @@ -172,10 +172,10 @@ async def get_calculator_characters( async def get_calculator_weapons( self, *, - query: str | None = None, - types: typing.Sequence[int] | None = None, - rarities: typing.Sequence[int] | None = None, - lang: str | None = None, + query: typing.Optional[str] = None, + types: typing.Optional[typing.Sequence[int]] = None, + rarities: typing.Optional[typing.Sequence[int]] = None, + lang: typing.Optional[str] = None, ) -> typing.Sequence[models.CalculatorWeapon]: """Get all weapons provided by the Enhancement Progression Calculator.""" data = await self._get_calculator_items( @@ -192,10 +192,10 @@ async def get_calculator_weapons( async def get_calculator_artifacts( self, *, - query: str | None = None, + query: typing.Optional[str] = None, pos: int = 1, - rarities: typing.Sequence[int] | None = None, - lang: str | None = None, + rarities: typing.Optional[typing.Sequence[int]] = None, + lang: typing.Optional[str] = None, ) -> typing.Sequence[models.CalculatorArtifact]: """Get all artifacts provided by the Enhancement Progression Calculator.""" data = await self._get_calculator_items( @@ -212,9 +212,9 @@ async def get_calculator_artifacts( async def get_calculator_furnishings( self, *, - types: int | None = None, - rarities: int | None = None, - lang: str | None = None, + types: typing.Optional[int] = None, + rarities: typing.Optional[int] = None, + lang: typing.Optional[str] = None, ) -> typing.Sequence[models.CalculatorFurnishing]: """Get all furnishings provided by the Enhancement Progression Calculator.""" data = await self._get_calculator_items( @@ -231,8 +231,8 @@ async def get_character_details( self, character: types.IDOr[genshin_models.BaseCharacter], *, - uid: int | None = None, - lang: str | None = None, + uid: typing.Optional[int] = None, + lang: typing.Optional[str] = None, ) -> models.CalculatorCharacterDetails: """Get the weapon, artifacts and talents of a character. @@ -257,7 +257,7 @@ async def get_character_talents( self, character: types.IDOr[genshin_models.BaseCharacter], *, - lang: str | None = None, + lang: typing.Optional[str] = None, ) -> typing.Sequence[models.CalculatorTalent]: """Get the talents of a character. @@ -274,9 +274,9 @@ async def get_character_talents( async def get_complete_artifact_set( self, - artifact: types.IDOr[genshin_models.Artifact | genshin_models.CalculatorArtifact], + artifact: types.IDOr[typing.Union[genshin_models.Artifact, genshin_models.CalculatorArtifact]], *, - lang: str | None = None, + lang: typing.Optional[str] = None, ) -> typing.Sequence[models.CalculatorArtifact]: """Get all other artifacts that share a set with any given artifact. @@ -300,9 +300,9 @@ async def get_teapot_replica_blueprint( self, share_code: int, *, - region: str | None = None, - uid: int | None = None, - lang: str | None = None, + region: typing.Optional[str] = None, + uid: typing.Optional[int] = None, + lang: typing.Optional[str] = None, ) -> typing.Sequence[models.CalculatorFurnishing]: """Get furnishings used by a teapot replica blueprint.""" if not region: @@ -319,7 +319,7 @@ async def get_teapot_replica_blueprint( return [models.CalculatorFurnishing(**i) for i in data["list"]] @deprecation.deprecated("await genshin.utility.update_characters_any()") - async def update_character_names(self, *, lang: str | None = None) -> None: + async def update_character_names(self, *, lang: typing.Optional[str] = None) -> None: """Update stored db characters with the names from the calculator.""" characters = await self.get_calculator_characters(lang=lang, include_traveler=True) diff --git a/genshin/client/components/chronicle/base.py b/genshin/client/components/chronicle/base.py index a9c34e01..fd6d9802 100644 --- a/genshin/client/components/chronicle/base.py +++ b/genshin/client/components/chronicle/base.py @@ -31,7 +31,7 @@ def __str__(self) -> str: endpoint: str uid: int lang: str - params: tuple[typing.Any, ...] = () + params: typing.Tuple[typing.Any, ...] = () class BaseBattleChronicleClient(base.BaseClient): @@ -71,7 +71,7 @@ async def request_game_record( async def get_record_cards( self, hoyolab_id: typing.Optional[int] = None, *, lang: typing.Optional[str] = None - ) -> list[models.hoyolab.RecordCard]: + ) -> typing.List[models.hoyolab.RecordCard]: """Get a user's record cards.""" hoyolab_id = hoyolab_id or self._get_hoyolab_id() diff --git a/genshin/client/components/gacha.py b/genshin/client/components/gacha.py index bb9f3a08..b45af6a4 100644 --- a/genshin/client/components/gacha.py +++ b/genshin/client/components/gacha.py @@ -57,7 +57,7 @@ async def _get_gacha_page( game: typing.Optional[types.Game] = None, lang: typing.Optional[str] = None, authkey: typing.Optional[str] = None, - ) -> tuple[typing.Sequence[typing.Any], int]: + ) -> typing.Tuple[typing.Sequence[typing.Any], int]: """Get a single page of wishes.""" data = await self.request_gacha_info( "getGachaLog", @@ -150,7 +150,7 @@ def wish_history( if not isinstance(banner_types, typing.Sequence): banner_types = [banner_types] - iterators: list[paginators.Paginator[models.Wish]] = [] + iterators: typing.List[paginators.Paginator[models.Wish]] = [] for banner in banner_types: iterators.append( paginators.CursorPaginator( @@ -185,7 +185,7 @@ def warp_history( if not isinstance(banner_types, typing.Sequence): banner_types = [banner_types] - iterators: list[paginators.Paginator[models.Warp]] = [] + iterators: typing.List[paginators.Paginator[models.Warp]] = [] for banner in banner_types: iterators.append( paginators.CursorPaginator( @@ -220,7 +220,7 @@ def signal_history( if not isinstance(banner_types, typing.Sequence): banner_types = [banner_types] - iterators: list[paginators.Paginator[models.SignalSearch]] = [] + iterators: typing.List[paginators.Paginator[models.SignalSearch]] = [] for banner in banner_types: iterators.append( paginators.CursorPaginator( diff --git a/genshin/client/components/hoyolab.py b/genshin/client/components/hoyolab.py index b781e49f..3df6e2e4 100644 --- a/genshin/client/components/hoyolab.py +++ b/genshin/client/components/hoyolab.py @@ -94,8 +94,8 @@ async def _request_announcements( ), ) - announcements: list[typing.Mapping[str, typing.Any]] = [] - extra_list: list[typing.Mapping[str, typing.Any]] = ( + announcements: typing.List[typing.Mapping[str, typing.Any]] = [] + extra_list: typing.List[typing.Mapping[str, typing.Any]] = ( info["pic_list"][0]["type_list"] if "pic_list" in info and info["pic_list"] else [] ) for sublist in info["list"] + extra_list: diff --git a/genshin/client/components/lineup.py b/genshin/client/components/lineup.py index a9dc5f4e..2ca3c8c9 100644 --- a/genshin/client/components/lineup.py +++ b/genshin/client/components/lineup.py @@ -56,7 +56,7 @@ async def get_lineup_scenarios( lang=lang, static_cache=cache.cache_key("lineup", endpoint="tags", lang=lang or self.lang), ) - dummy: dict[str, typing.Any] = dict(id=0, name="", children=data["tree"]) + dummy: typing.Dict[str, typing.Any] = dict(id=0, name="", children=data["tree"]) return models.LineupScenarios(**dummy) @@ -70,9 +70,9 @@ async def _get_lineup_page( order: typing.Optional[str] = None, uid: typing.Optional[int] = None, lang: typing.Optional[str] = None, - ) -> tuple[str, typing.Sequence[models.LineupPreview]]: + ) -> typing.Tuple[str, typing.Sequence[models.LineupPreview]]: """Get a single page of lineups.""" - params: dict[str, typing.Any] = dict( + params: typing.Dict[str, typing.Any] = dict( next_page_token=token, limit=limit or "", tag_id=tag_id or "", diff --git a/genshin/client/components/transaction.py b/genshin/client/components/transaction.py index 61c8e00d..d74099fe 100644 --- a/genshin/client/components/transaction.py +++ b/genshin/client/components/transaction.py @@ -61,10 +61,10 @@ async def _get_transaction_page( params=dict(end_id=end_id, size=20), ) - transactions: list[models.BaseTransaction] = [] + transactions: typing.List[models.BaseTransaction] = [] for trans in data["list"]: model = models.ItemTransaction if "name" in trans else models.Transaction - model = typing.cast("type[models.BaseTransaction]", model) + model = typing.cast("typing.Type[models.BaseTransaction]", model) transactions.append(model(**trans, kind=kind)) return transactions @@ -84,7 +84,7 @@ def transaction_log( if isinstance(kinds, str): kinds = [kinds] - iterators: list[paginators.Paginator[models.BaseTransaction]] = [] + iterators: typing.List[paginators.Paginator[models.BaseTransaction]] = [] for kind in kinds: iterators.append( paginators.CursorPaginator( diff --git a/genshin/client/manager/cookie.py b/genshin/client/manager/cookie.py index c70b2537..2ee60d5b 100644 --- a/genshin/client/manager/cookie.py +++ b/genshin/client/manager/cookie.py @@ -86,7 +86,7 @@ async def fetch_cookie_with_cookie( async def fetch_cookie_with_stoken_v2( cookies: managers.CookieOrHeader, *, - token_types: list[typing.Literal[2, 4]], + token_types: typing.List[typing.Literal[2, 4]], ) -> typing.Mapping[str, str]: """Fetch cookie (v2) with an stoken (v2) and mid.""" cookies = managers.parse_cookie(cookies) diff --git a/genshin/client/manager/managers.py b/genshin/client/manager/managers.py index e0b407b5..9f7c1004 100644 --- a/genshin/client/manager/managers.py +++ b/genshin/client/manager/managers.py @@ -36,7 +36,7 @@ MaybeSequence = typing.Union[T, typing.Sequence[T]] -def parse_cookie(cookie: CookieOrHeader | None) -> dict[str, str]: +def parse_cookie(cookie: typing.Optional[CookieOrHeader]) -> typing.Dict[str, str]: """Parse a cookie or header into a cookie mapping.""" if cookie is None: return {} @@ -47,7 +47,7 @@ def parse_cookie(cookie: CookieOrHeader | None) -> dict[str, str]: return {str(k): v.value if isinstance(v, http.cookies.Morsel) else str(v) for k, v in cookie.items()} -def get_cookie_identifier(cookie: typing.Mapping[str, str]) -> str | None: +def get_cookie_identifier(cookie: typing.Mapping[str, str]) -> typing.Optional[str]: """Get a unique identifier for a cookie.""" for name, value in cookie.items(): if name in ("ltuid", "account_id", "ltuid_v2", "account_id_v2"): @@ -64,11 +64,11 @@ def get_cookie_identifier(cookie: typing.Mapping[str, str]) -> str | None: class BaseCookieManager(abc.ABC): """A cookie manager for making requests.""" - _proxy: yarl.URL | None = None - _socks_proxy: str | None = None + _proxy: typing.Optional[yarl.URL] = None + _socks_proxy: typing.Optional[str] = None @classmethod - def from_cookies(cls, cookies: AnyCookieOrHeader | None = None) -> BaseCookieManager: + def from_cookies(cls, cookies: typing.Optional[AnyCookieOrHeader] = None) -> BaseCookieManager: """Create an arbitrary cookie manager implementation instance.""" if not cookies: return CookieManager() @@ -79,7 +79,7 @@ def from_cookies(cls, cookies: AnyCookieOrHeader | None = None) -> BaseCookieMan return CookieManager(cookies) @classmethod - def from_browser_cookies(cls, browser: str | None = None) -> CookieManager: + def from_browser_cookies(cls, browser: typing.Optional[str] = None) -> CookieManager: """Create a cookie manager with browser cookies.""" manager = CookieManager() manager.set_browser_cookies(browser) @@ -97,7 +97,7 @@ def multi(self) -> bool: return False @property - def user_id(self) -> int | None: + def user_id(self) -> typing.Optional[int]: """The id of the user that owns cookies. Returns None if not found or not applicable. @@ -105,12 +105,12 @@ def user_id(self) -> int | None: return None @property - def proxy(self) -> yarl.URL | None: + def proxy(self) -> typing.Optional[yarl.URL]: """Proxy for http(s) requests.""" return self._proxy @proxy.setter - def proxy(self, proxy: aiohttp.typedefs.StrOrURL | None) -> None: + def proxy(self, proxy: typing.Optional[aiohttp.typedefs.StrOrURL]) -> None: if proxy is None: self._proxy = None self._socks_proxy = None @@ -179,11 +179,11 @@ async def request( url: aiohttp.typedefs.StrOrURL, *, method: str = "GET", - params: typing.Mapping[str, typing.Any] | None = None, + params: typing.Optional[typing.Mapping[str, typing.Any]] = None, data: typing.Any = None, json: typing.Any = None, - cookies: aiohttp.typedefs.LooseCookies | None = None, - headers: aiohttp.typedefs.LooseHeaders | None = None, + cookies: typing.Optional[aiohttp.typedefs.LooseCookies] = None, + headers: typing.Optional[aiohttp.typedefs.LooseHeaders] = None, **kwargs: typing.Any, ) -> typing.Any: """Make an authenticated request.""" @@ -192,11 +192,11 @@ async def request( class CookieManager(BaseCookieManager): """Standard implementation of the cookie manager.""" - _cookies: dict[str, str] + _cookies: typing.Dict[str, str] def __init__( self, - cookies: CookieOrHeader | None = None, + cookies: typing.Optional[CookieOrHeader] = None, ) -> None: self.cookies = parse_cookie(cookies) @@ -209,7 +209,7 @@ def cookies(self) -> typing.MutableMapping[str, str]: return self._cookies @cookies.setter - def cookies(self, cookies: CookieOrHeader | None) -> None: + def cookies(self, cookies: typing.Optional[CookieOrHeader]) -> None: if not cookies: self._cookies = {} return @@ -239,7 +239,7 @@ def header(self) -> str: def set_cookies( self, - cookies: CookieOrHeader | None = None, + cookies: typing.Optional[CookieOrHeader] = None, **kwargs: typing.Any, ) -> typing.MutableMapping[str, str]: """Parse and set cookies.""" @@ -249,7 +249,7 @@ def set_cookies( self.cookies = parse_cookie(cookies or kwargs) return self.cookies - def set_browser_cookies(self, browser: str | None = None) -> typing.Mapping[str, str]: + def set_browser_cookies(self, browser: typing.Optional[str] = None) -> typing.Mapping[str, str]: """Extract cookies from your browser and set them as client cookies. Available browsers: chrome, chromium, opera, edge, firefox. @@ -258,7 +258,7 @@ def set_browser_cookies(self, browser: str | None = None) -> typing.Mapping[str, return self.cookies @property - def user_id(self) -> int | None: + def user_id(self) -> typing.Optional[int]: """The id of the user that owns cookies. Returns None if cookies are not set. @@ -287,9 +287,9 @@ class CookieSequence(typing.Sequence[typing.Mapping[str, str]]): MAX_USES: int = 30 # {id: ({cookie}, uses), ...} - _cookies: dict[str, tuple[dict[str, str], int]] + _cookies: typing.Dict[str, typing.Tuple[typing.Dict[str, str], int]] - def __init__(self, cookies: typing.Sequence[CookieOrHeader] | None = None) -> None: + def __init__(self, cookies: typing.Optional[typing.Sequence[CookieOrHeader]] = None) -> None: self.cookies = [parse_cookie(cookie) for cookie in cookies or []] @property @@ -299,7 +299,7 @@ def cookies(self) -> typing.Sequence[typing.Mapping[str, str]]: return [cookies for cookies, _ in cookies] @cookies.setter - def cookies(self, cookies: typing.Sequence[CookieOrHeader] | None) -> None: + def cookies(self, cookies: typing.Optional[typing.Sequence[CookieOrHeader]]) -> None: if not cookies: self._cookies = {} return @@ -335,7 +335,7 @@ class RotatingCookieManager(BaseCookieManager): _cookies: CookieSequence - def __init__(self, cookies: typing.Sequence[CookieOrHeader] | None = None) -> None: + def __init__(self, cookies: typing.Optional[typing.Sequence[CookieOrHeader]] = None) -> None: self.set_cookies(cookies) @property @@ -344,7 +344,7 @@ def cookies(self) -> typing.Sequence[typing.Mapping[str, str]]: return self._cookies @cookies.setter - def cookies(self, cookies: typing.Sequence[CookieOrHeader] | None) -> None: + def cookies(self, cookies: typing.Optional[typing.Sequence[CookieOrHeader]]) -> None: self._cookies.cookies = cookies # type: ignore # mypy does not understand property setters def __repr__(self) -> str: @@ -360,7 +360,7 @@ def multi(self) -> bool: def set_cookies( self, - cookies: typing.Sequence[CookieOrHeader] | None = None, + cookies: typing.Optional[typing.Sequence[CookieOrHeader]] = None, ) -> typing.Sequence[typing.Mapping[str, str]]: """Parse and set cookies.""" self._cookies = CookieSequence(cookies) @@ -401,7 +401,7 @@ class InternationalCookieManager(BaseCookieManager): _cookies: typing.Mapping[types.Region, CookieSequence] - def __init__(self, cookies: typing.Mapping[str, MaybeSequence[CookieOrHeader]] | None = None) -> None: + def __init__(self, cookies: typing.Optional[typing.Mapping[str, MaybeSequence[CookieOrHeader]]] = None) -> None: self.set_cookies(cookies) @property @@ -422,7 +422,7 @@ def multi(self) -> bool: def set_cookies( self, - cookies: typing.Mapping[str, MaybeSequence[CookieOrHeader]] | None = None, + cookies: typing.Optional[typing.Mapping[str, MaybeSequence[CookieOrHeader]]] = None, ) -> typing.Mapping[types.Region, typing.Sequence[typing.Mapping[str, str]]]: """Parse and set cookies.""" self._cookies = {} diff --git a/genshin/client/ratelimit.py b/genshin/client/ratelimit.py index 7abec596..61ef328d 100644 --- a/genshin/client/ratelimit.py +++ b/genshin/client/ratelimit.py @@ -11,7 +11,7 @@ def handle_ratelimits( tries: int = 5, - exception: type[errors.GenshinException] = errors.VisitsTooFrequently, + exception: typing.Type[errors.GenshinException] = errors.VisitsTooFrequently, delay: float = 0.3, ) -> typing.Callable[[CallableT], CallableT]: """Handle ratelimits for requests.""" diff --git a/genshin/errors.py b/genshin/errors.py index 67e648e9..71724114 100644 --- a/genshin/errors.py +++ b/genshin/errors.py @@ -188,7 +188,7 @@ class WrongOTP(GenshinException): class GeetestError(GenshinException): """Geetest triggered during the battle chronicle API request.""" - def __init__(self, response: dict[str, typing.Any]) -> None: + def __init__(self, response: typing.Dict[str, typing.Any]) -> None: super().__init__(response) msg = "Geetest triggered during the battle chronicle API request." @@ -232,8 +232,8 @@ class VerificationCodeRateLimited(GenshinException): msg = "Too many verification code requests for the account." -_TGE = type[GenshinException] -_errors: dict[int, typing.Union[_TGE, str, tuple[_TGE, typing.Optional[str]]]] = { +_TGE = typing.Type[GenshinException] +_errors: typing.Dict[int, typing.Union[_TGE, str, typing.Tuple[_TGE, typing.Optional[str]]]] = { # misc hoyolab -100: InvalidCookies, -108: "Invalid language.", @@ -286,13 +286,13 @@ class VerificationCodeRateLimited(GenshinException): -202: IncorrectGamePassword, } -ERRORS: dict[int, tuple[_TGE, typing.Optional[str]]] = { +ERRORS: typing.Dict[int, typing.Tuple[_TGE, typing.Optional[str]]] = { retcode: (GenshinException, exc) if isinstance(exc, str) else exc if isinstance(exc, tuple) else (exc, None) for retcode, exc in _errors.items() } -def raise_for_retcode(data: dict[str, typing.Any]) -> typing.NoReturn: +def raise_for_retcode(data: typing.Dict[str, typing.Any]) -> typing.NoReturn: """Raise an equivalent error to a response. game record: @@ -327,7 +327,7 @@ def raise_for_retcode(data: dict[str, typing.Any]) -> typing.NoReturn: raise GenshinException(data) -def check_for_geetest(data: dict[str, typing.Any]) -> None: +def check_for_geetest(data: typing.Dict[str, typing.Any]) -> None: """Check if geetest was triggered during the request and raise an error if so.""" if data["retcode"] in GEETEST_RETCODES: raise GeetestError(data) diff --git a/genshin/models/auth/cookie.py b/genshin/models/auth/cookie.py index 59549ccd..9c0ef4ba 100644 --- a/genshin/models/auth/cookie.py +++ b/genshin/models/auth/cookie.py @@ -26,7 +26,7 @@ class StokenResult(pydantic.BaseModel): token: str @pydantic.model_validator(mode="before") - def _transform_result(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: + def _transform_result(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: return { "aid": values["user_info"]["aid"], "mid": values["user_info"]["mid"], @@ -41,7 +41,7 @@ def to_str(self) -> str: """Convert the login cookies to a string.""" return "; ".join(f"{key}={value}" for key, value in self.model_dump().items()) - def to_dict(self) -> dict[str, str]: + def to_dict(self) -> typing.Dict[str, str]: """Convert the login cookies to a dictionary.""" return self.model_dump() @@ -121,7 +121,7 @@ class DeviceGrantResult(pydantic.BaseModel): login_ticket: typing.Optional[str] = None @pydantic.model_validator(mode="before") - def _str_to_none(cls, data: dict[str, typing.Union[str, None]]) -> dict[str, typing.Union[str, None]]: + def _str_to_none(cls, data: typing.Dict[str, typing.Union[str, None]]) -> typing.Dict[str, typing.Union[str, None]]: """Convert empty strings to `None`.""" for key in data: if data[key] == "" or data[key] == "None": diff --git a/genshin/models/auth/geetest.py b/genshin/models/auth/geetest.py index cf62f07a..1340f3a5 100644 --- a/genshin/models/auth/geetest.py +++ b/genshin/models/auth/geetest.py @@ -32,7 +32,7 @@ class BaseMMT(pydantic.BaseModel): success: int @pydantic.model_validator(mode="before") - def __parse_data(cls, data: dict[str, typing.Any]) -> dict[str, typing.Any]: + def __parse_data(cls, data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: """Parse the data if it was provided in a raw format.""" if "data" in data: # Assume the data is aigis header and parse it @@ -89,7 +89,7 @@ class RiskyCheckMMT(MMT): class BaseMMTResult(pydantic.BaseModel): """Base Geetest verification result model.""" - def get_data(self) -> dict[str, typing.Any]: + def get_data(self) -> typing.Dict[str, typing.Any]: """Get the base MMT result data. This method acts as `dict` but excludes the `session_id` field. diff --git a/genshin/models/auth/verification.py b/genshin/models/auth/verification.py index 1c738c22..5d51a573 100644 --- a/genshin/models/auth/verification.py +++ b/genshin/models/auth/verification.py @@ -25,7 +25,7 @@ class ActionTicket(pydantic.BaseModel): verify_str: VerifyStrategy @pydantic.model_validator(mode="before") - def __parse_data(cls, data: dict[str, typing.Any]) -> dict[str, typing.Any]: + def __parse_data(cls, data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: """Parse the data if it was provided in a raw format.""" verify_str = data["verify_str"] if isinstance(verify_str, str): diff --git a/genshin/models/genshin/calculator.py b/genshin/models/genshin/calculator.py index 82467acd..c9945c44 100644 --- a/genshin/models/genshin/calculator.py +++ b/genshin/models/genshin/calculator.py @@ -168,7 +168,7 @@ class CalculatorFurnishing(APIModel, Unique): icon: str = Aliased("icon_url") rarity: int = Aliased("level") - amount: int | None = Aliased("num") + amount: typing.Optional[int] = Aliased("num") class CalculatorCharacterDetails(APIModel): @@ -181,7 +181,7 @@ class CalculatorCharacterDetails(APIModel): @pydantic.field_validator("talents") def __correct_talent_current_level(cls, v: typing.Sequence[CalculatorTalent]) -> typing.Sequence[CalculatorTalent]: # passive talent have current levels at 0 for some reason - talents: list[CalculatorTalent] = [] + talents: typing.List[CalculatorTalent] = [] for talent in v: if talent.max_level == 1 and talent.level == 0: @@ -221,17 +221,17 @@ class CalculatorArtifactResult(APIModel): class CalculatorResult(APIModel): """Calculation result.""" - character: list[CalculatorConsumable] = Aliased("avatar_consume") - weapon: list[CalculatorConsumable] = Aliased("weapon_consume") - talents: list[CalculatorConsumable] = Aliased("avatar_skill_consume") - artifacts: list[CalculatorArtifactResult] = Aliased("reliquary_consume") + character: typing.List[CalculatorConsumable] = Aliased("avatar_consume") + weapon: typing.List[CalculatorConsumable] = Aliased("weapon_consume") + talents: typing.List[CalculatorConsumable] = Aliased("avatar_skill_consume") + artifacts: typing.List[CalculatorArtifactResult] = Aliased("reliquary_consume") @property def total(self) -> typing.Sequence[CalculatorConsumable]: artifacts = [i for a in self.artifacts for i in a.list] combined = self.character + self.weapon + self.talents + artifacts - grouped: dict[int, list[CalculatorConsumable]] = collections.defaultdict(list) + grouped: typing.Dict[int, typing.List[CalculatorConsumable]] = collections.defaultdict(list) for i in combined: grouped[i.id].append(i) @@ -251,7 +251,7 @@ def total(self) -> typing.Sequence[CalculatorConsumable]: class CalculatorFurnishingResults(APIModel): """Furnishing calculation result.""" - furnishings: list[CalculatorConsumable] = Aliased("list") + furnishings: typing.List[CalculatorConsumable] = Aliased("list") @property def total(self) -> typing.Sequence[CalculatorConsumable]: diff --git a/genshin/models/genshin/character.py b/genshin/models/genshin/character.py index 797e5d2a..1b5e5b1d 100644 --- a/genshin/models/genshin/character.py +++ b/genshin/models/genshin/character.py @@ -132,7 +132,7 @@ class BaseCharacter(APIModel, Unique): collab: bool = False @pydantic.model_validator(mode="before") - def __autocomplete(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: + def __autocomplete(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: """Complete missing data.""" all_fields = list(cls.model_fields.keys()) all_aliases = {f: cls.model_fields[f].alias for f in all_fields if cls.model_fields[f].alias} diff --git a/genshin/models/genshin/chronicle/abyss.py b/genshin/models/genshin/chronicle/abyss.py index ad03da31..2aa1330a 100644 --- a/genshin/models/genshin/chronicle/abyss.py +++ b/genshin/models/genshin/chronicle/abyss.py @@ -91,7 +91,7 @@ class SpiralAbyss(APIModel): floors: typing.Sequence[Floor] @pydantic.model_validator(mode="before") - def __nest_ranks(cls, values: dict[str, typing.Any]) -> dict[str, AbyssCharacter]: + def __nest_ranks(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, AbyssCharacter]: """By default ranks are for some reason on the same level as the rest of the abyss.""" values.setdefault("ranks", {}).update(values) return values diff --git a/genshin/models/genshin/chronicle/activities.py b/genshin/models/genshin/chronicle/activities.py index 3a6c9271..1655b713 100644 --- a/genshin/models/genshin/chronicle/activities.py +++ b/genshin/models/genshin/chronicle/activities.py @@ -27,7 +27,7 @@ class OldActivity(APIModel, typing.Generic[ModelT]): """Arbitrary activity for chinese events.""" # sometimes __parameters__ may not be provided in older versions - __parameters__: typing.ClassVar[tuple[typing.Any, ...]] = (ModelT,) # type: ignore + __parameters__: typing.ClassVar[typing.Tuple[typing.Any, ...]] = (ModelT,) # type: ignore exists_data: bool records: typing.Sequence[ModelT] @@ -310,7 +310,7 @@ class Activities(APIModel): chess: typing.Optional[Activity[typing.Any]] = None @pydantic.model_validator(mode="before") - def __flatten_activities(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: + def __flatten_activities(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: if not values.get("activities"): return values diff --git a/genshin/models/genshin/chronicle/characters.py b/genshin/models/genshin/chronicle/characters.py index 9d2d1043..1dba8ecf 100644 --- a/genshin/models/genshin/chronicle/characters.py +++ b/genshin/models/genshin/chronicle/characters.py @@ -127,7 +127,7 @@ class Character(PartialCharacter): @pydantic.field_validator("artifacts") @classmethod def __enable_artifact_set_effects(cls, artifacts: typing.Sequence[Artifact]) -> typing.Sequence[Artifact]: - set_nums: defaultdict[int, int] = defaultdict(int) + set_nums: typing.DefaultDict[int, int] = defaultdict(int) for arti in artifacts: set_nums[arti.set.id] += 1 @@ -244,16 +244,16 @@ class GenshinDetailCharacters(APIModel): avatar_wiki: typing.Mapping[str, str] @pydantic.model_validator(mode="before") - def __fill_prop_info(cls, values: dict[str, typing.Any]) -> typing.Mapping[str, typing.Any]: + def __fill_prop_info(cls, values: typing.Dict[str, typing.Any]) -> typing.Mapping[str, typing.Any]: """Fill property info from properety_map.""" - relic_property_options: dict[str, list[int]] = values.get("relic_property_options", {}) - prop_map: dict[str, dict[str, typing.Any]] = values.get("property_map", {}) - characters: list[dict[str, typing.Any]] = values.get("list", []) + relic_property_options: typing.Dict[str, list[int]] = values.get("relic_property_options", {}) + prop_map: typing.Dict[str, typing.Dict[str, typing.Any]] = values.get("property_map", {}) + characters: list[typing.Dict[str, typing.Any]] = values.get("list", []) # Map properties to artifacts - new_relic_prop_options: dict[str, list[dict[str, typing.Any]]] = {} + new_relic_prop_options: typing.Dict[str, list[typing.Dict[str, typing.Any]]] = {} for relic_type, properties in relic_property_options.items(): - formatted_properties: list[dict[str, typing.Any]] = [ + formatted_properties: list[typing.Dict[str, typing.Any]] = [ prop_map[str(prop)] for prop in properties if str(prop) in prop_map ] new_relic_prop_options[relic_type] = formatted_properties diff --git a/genshin/models/genshin/chronicle/img_theater.py b/genshin/models/genshin/chronicle/img_theater.py index c4133b1a..77675e18 100644 --- a/genshin/models/genshin/chronicle/img_theater.py +++ b/genshin/models/genshin/chronicle/img_theater.py @@ -145,8 +145,8 @@ class ImgTheaterData(APIModel): battle_stats: TheaterBattleStats = Aliased("fight_statisic", default=None) @pydantic.model_validator(mode="before") - def __unnest_detail(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: - detail: typing.Optional[dict[str, typing.Any]] = values.get("detail") + def __unnest_detail(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + detail: typing.Optional[typing.Dict[str, typing.Any]] = values.get("detail") values["rounds_data"] = detail.get("rounds_data", []) if detail is not None else [] values["backup_avatars"] = detail.get("backup_avatars", []) if detail is not None else [] values["fight_statisic"] = detail.get("fight_statisic", None) if detail is not None else None diff --git a/genshin/models/genshin/chronicle/notes.py b/genshin/models/genshin/chronicle/notes.py index 8c66943b..41d045be 100644 --- a/genshin/models/genshin/chronicle/notes.py +++ b/genshin/models/genshin/chronicle/notes.py @@ -63,7 +63,7 @@ class TransformerTimedelta(datetime.timedelta): """Transformer recovery time.""" @property - def timedata(self) -> tuple[int, int, int, int]: + def timedata(self) -> typing.Tuple[int, int, int, int]: seconds: int = super().seconds days: int = super().days hour, second = divmod(seconds, 3600) @@ -219,7 +219,7 @@ def transformer_recovery_time(self) -> typing.Optional[datetime.datetime]: return remaining @pydantic.model_validator(mode="before") - def __flatten_transformer(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: + def __flatten_transformer(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: if "transformer_recovery_time" in values: return values diff --git a/genshin/models/genshin/chronicle/stats.py b/genshin/models/genshin/chronicle/stats.py index f747c602..a4402978 100644 --- a/genshin/models/genshin/chronicle/stats.py +++ b/genshin/models/genshin/chronicle/stats.py @@ -114,7 +114,7 @@ class Exploration(APIModel): offerings: typing.Sequence[Offering] boss_list: typing.Sequence[BossKill] area_exploration_list: typing.Sequence[AreaExploration] - natlan_reputation: NatlanReputation | None = Aliased("natan_reputation", default=None) + natlan_reputation: typing.Optional[NatlanReputation] = Aliased("natan_reputation", default=None) @property def explored(self) -> float: @@ -162,10 +162,10 @@ class PartialGenshinUserStats(APIModel): stats: Stats characters: typing.Sequence[characters_module.PartialCharacter] = Aliased("avatars") explorations: typing.Sequence[Exploration] = Aliased("world_explorations") - teapot: Teapot | None = Aliased("homes") + teapot: typing.Optional[Teapot] = Aliased("homes") @pydantic.field_validator("teapot", mode="before") - def __format_teapot(cls, v: typing.Any) -> dict[str, typing.Any] | None: + def __format_teapot(cls, v: typing.Any) -> typing.Optional[typing.Dict[str, typing.Any]]: if not v: return None if isinstance(v, dict): diff --git a/genshin/models/genshin/constants.py b/genshin/models/genshin/constants.py index 4ee63743..3b5cc134 100644 --- a/genshin/models/genshin/constants.py +++ b/genshin/models/genshin/constants.py @@ -92,4 +92,4 @@ class DBChar(typing.NamedTuple): # 10000071: ("Cyno", "Electro", 5), # 10000072: ("Candace", "Hydro", 4), # } -CHARACTER_NAMES: dict[str, dict[int, DBChar]] = {} +CHARACTER_NAMES: typing.Dict[str, typing.Dict[int, DBChar]] = {} diff --git a/genshin/models/genshin/gacha.py b/genshin/models/genshin/gacha.py index a5e0d133..e6acbdb8 100644 --- a/genshin/models/genshin/gacha.py +++ b/genshin/models/genshin/gacha.py @@ -192,9 +192,9 @@ class BannerDetails(APIModel): r5_up_items: typing.Sequence[BannerDetailsUpItem] r4_up_items: typing.Sequence[BannerDetailsUpItem] - r5_items: list[BannerDetailItem] = Aliased("r5_prob_list") - r4_items: list[BannerDetailItem] = Aliased("r4_prob_list") - r3_items: list[BannerDetailItem] = Aliased("r3_prob_list") + r5_items: typing.List[BannerDetailItem] = Aliased("r5_prob_list") + r4_items: typing.List[BannerDetailItem] = Aliased("r4_prob_list") + r3_items: typing.List[BannerDetailItem] = Aliased("r3_prob_list") @pydantic.field_validator("r5_up_items", "r4_up_items", mode="before") def __replace_none(cls, v: typing.Optional[typing.Sequence[typing.Any]]) -> typing.Sequence[typing.Any]: diff --git a/genshin/models/genshin/lineup.py b/genshin/models/genshin/lineup.py index 696b8aaf..ef6c0792 100644 --- a/genshin/models/genshin/lineup.py +++ b/genshin/models/genshin/lineup.py @@ -122,7 +122,7 @@ class LineupArtifactStatFields(APIModel): secondary_stats: typing.Mapping[int, str] = Aliased("reliquary_sec_attr") @pydantic.model_validator(mode="before") - def __flatten_stats(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: + def __flatten_stats(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: """Name certain stats.""" if "reliquary_fst_attr" not in values: return values @@ -143,7 +143,7 @@ def __flatten_stats(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any] return values @pydantic.field_validator("secondary_stats", "flower", "plume", "sands", "goblet", "circlet", mode="before") - def __parse_secondary_stats(cls, value: typing.Any) -> dict[int, str]: + def __parse_secondary_stats(cls, value: typing.Any) -> typing.Dict[int, str]: if not isinstance(value, typing.Sequence): return value @@ -186,7 +186,7 @@ class LineupScenario(APIModel, Unique): children: typing.Sequence[LineupScenario] @pydantic.model_validator(mode="before") - def __flatten_scenarios(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: + def __flatten_scenarios(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: """Name certain scenarios.""" scenario_ids = { field.json_schema_extra["scenario_id"]: name diff --git a/genshin/models/genshin/teapot.py b/genshin/models/genshin/teapot.py index 8e619d00..6b6a273c 100644 --- a/genshin/models/genshin/teapot.py +++ b/genshin/models/genshin/teapot.py @@ -51,7 +51,7 @@ class TeapotReplica(APIModel): post_id: str title: str content: str - images: list[str] = Aliased("imgs") + images: typing.List[str] = Aliased("imgs") created_at: DateTimeField stats: TeapotReplicaStats lang: str # type: ignore @@ -61,7 +61,7 @@ class TeapotReplica(APIModel): view_type: int sub_type: int blueprint: TeapotReplicaBlueprint - video: str | None + video: typing.Optional[str] has_more_content: bool token: str @@ -71,7 +71,7 @@ def __extract_urls(cls, images: typing.Sequence[typing.Any]) -> typing.Sequence[ return [image if isinstance(image, str) else image["url"] for image in images] @pydantic.field_validator("video", mode="before") - def __extract_url(cls, video: typing.Any) -> str | None: + def __extract_url(cls, video: typing.Any) -> typing.Optional[str]: if isinstance(video, str): return video diff --git a/genshin/models/genshin/wiki.py b/genshin/models/genshin/wiki.py index 29f80c4b..fa9be4bb 100644 --- a/genshin/models/genshin/wiki.py +++ b/genshin/models/genshin/wiki.py @@ -37,7 +37,7 @@ class BaseWikiPreview(APIModel, Unique): name: str @pydantic.model_validator(mode="before") - def __unpack_filter_values(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: + def __unpack_filter_values(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: filter_values = { key.split("_", 1)[1]: value["values"][0] for key, value in values.get("filter_values", {}).items() @@ -47,7 +47,7 @@ def __unpack_filter_values(cls, values: dict[str, typing.Any]) -> dict[str, typi return values @pydantic.model_validator(mode="before") - def __flatten_display_field(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: + def __flatten_display_field(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: values.update(values.get("display_field", {})) return values @@ -104,7 +104,7 @@ class ArtifactPreview(BaseWikiPreview): effects: typing.Mapping[int, str] @pydantic.model_validator(mode="before") - def __group_effects(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: + def __group_effects(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: effects = { 1: values["single_set_effect"], 2: values["two_set_effect"], @@ -124,7 +124,7 @@ def __parse_drop_materials(cls, value: typing.Union[str, typing.Sequence[str]]) return json.loads(value) if isinstance(value, str) else value -_ENTRY_PAGE_MODELS: typing.Mapping[WikiPageType, type[BaseWikiPreview]] = { +_ENTRY_PAGE_MODELS: typing.Mapping[WikiPageType, typing.Type[BaseWikiPreview]] = { WikiPageType.CHARACTER: CharacterPreview, WikiPageType.WEAPON: WeaponPreview, WikiPageType.ARTIFACT: ArtifactPreview, @@ -147,14 +147,14 @@ class WikiPage(APIModel): @pydantic.field_validator("modules", mode="before") def __format_modules( cls, - value: typing.Union[list[dict[str, typing.Any]], dict[str, typing.Any]], - ) -> dict[str, typing.Any]: + value: typing.Union[typing.List[typing.Dict[str, typing.Any]], typing.Dict[str, typing.Any]], + ) -> typing.Dict[str, typing.Any]: if isinstance(value, typing.Mapping): return value - modules: dict[str, dict[str, typing.Any]] = {} + modules: typing.Dict[str, typing.Dict[str, typing.Any]] = {} for module in value: - components: dict[str, dict[str, typing.Any]] = { + components: typing.Dict[str, typing.Dict[str, typing.Any]] = { component["component_id"]: json.loads(component["data"] or "{}") for component in module["components"] } diff --git a/genshin/models/honkai/chronicle/battlesuits.py b/genshin/models/honkai/chronicle/battlesuits.py index 7dc0ecf0..677ca4e2 100644 --- a/genshin/models/honkai/chronicle/battlesuits.py +++ b/genshin/models/honkai/chronicle/battlesuits.py @@ -46,7 +46,7 @@ class FullBattlesuit(battlesuit.Battlesuit): stigmata: typing.Sequence[Stigma] = Aliased("stigmatas") @pydantic.model_validator(mode="before") - def __unnest_char_data(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: + def __unnest_char_data(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: if isinstance(values.get("character"), typing.Mapping): values.update(values["character"]) diff --git a/genshin/models/honkai/chronicle/modes.py b/genshin/models/honkai/chronicle/modes.py index 3d7c50a1..4f50462d 100644 --- a/genshin/models/honkai/chronicle/modes.py +++ b/genshin/models/honkai/chronicle/modes.py @@ -13,7 +13,7 @@ __all__ = ["ELF", "Boss", "ElysianRealm", "MemorialArena", "MemorialBattle", "OldAbyss", "SuperstringAbyss"] -REMEMBRANCE_SIGILS: dict[int, tuple[str, int]] = { +REMEMBRANCE_SIGILS: typing.Dict[int, typing.Tuple[str, int]] = { 119301: ("The MOTH Insignia", 1), 119302: ("Home Lost", 1), 119303: ("False Hope", 1), @@ -90,7 +90,7 @@ class ELF(APIModel, Unique): upgrade_level: int = Aliased("star") @pydantic.field_validator("rarity", mode="before") - def __fix_rank(cls, rarity: int | str) -> str: + def __fix_rank(cls, rarity: typing.Union[int, str]) -> str: if isinstance(rarity, str): return rarity @@ -122,7 +122,7 @@ class BaseAbyss(APIModel): score: int lineup: typing.Sequence[battlesuit.Battlesuit] boss: Boss - elf: ELF | None + elf: typing.Optional[ELF] class OldAbyss(BaseAbyss): @@ -180,7 +180,7 @@ class MemorialBattle(APIModel): score: int lineup: typing.Sequence[battlesuit.Battlesuit] - elf: ELF | None + elf: typing.Optional[ELF] boss: Boss @@ -278,7 +278,7 @@ class ElysianRealm(APIModel): signets: typing.Sequence[Signet] = Aliased("buffs") leader: battlesuit.Battlesuit = Aliased("main_avatar") supports: typing.Sequence[battlesuit.Battlesuit] = Aliased("support_avatars") - elf: ELF | None + elf: typing.Optional[ELF] remembrance_sigil: RemembranceSigil = Aliased("extra_item_icon") @pydantic.field_validator("remembrance_sigil", mode="before") diff --git a/genshin/models/honkai/chronicle/stats.py b/genshin/models/honkai/chronicle/stats.py index b7ab00b3..a30c18f9 100644 --- a/genshin/models/honkai/chronicle/stats.py +++ b/genshin/models/honkai/chronicle/stats.py @@ -100,7 +100,7 @@ class HonkaiStats(APIModel): elysian_realm: ElysianRealmStats = Aliased() @pydantic.model_validator(mode="before") - def __pack_gamemode_stats(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: + def __pack_gamemode_stats(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: if "new_abyss" in values: values["abyss"] = SuperstringAbyssStats(**values["new_abyss"], **values) elif "old_abyss" in values: diff --git a/genshin/models/honkai/constants.py b/genshin/models/honkai/constants.py index 8e6b28fd..3f644fa4 100644 --- a/genshin/models/honkai/constants.py +++ b/genshin/models/honkai/constants.py @@ -6,7 +6,7 @@ # TODO: Make this more dynamic # fmt: off -BATTLESUIT_IDENTIFIERS: dict[int, str] = { +BATTLESUIT_IDENTIFIERS: typing.Dict[int, str] = { 101: "KianaC2", 102: "KianaC1", 103: "KianaC4", diff --git a/genshin/models/hoyolab/record.py b/genshin/models/hoyolab/record.py index 536944e8..00f46454 100644 --- a/genshin/models/hoyolab/record.py +++ b/genshin/models/hoyolab/record.py @@ -117,8 +117,8 @@ class HoyolabUserCertification(APIModel): For example artist's type is 2. """ - icon_url: str | None = None - description: str | None = Aliased("desc", default=None) + icon_url: typing.Optional[str] = None + description: typing.Optional[str] = Aliased("desc", default=None) type: int @@ -138,11 +138,11 @@ class FullHoyolabUser(PartialHoyolabUser): Not actually full, but most of the data is useless. """ - certification: HoyolabUserCertification | None = None - level: HoyolabUserLevel | None = None + certification: typing.Optional[HoyolabUserCertification] = None + level: typing.Optional[HoyolabUserLevel] = None pendant_url: str = Aliased("pendant") - bg_url: str | None = None - pc_bg_url: str | None = None + bg_url: typing.Optional[str] = None + pc_bg_url: typing.Optional[str] = None class RecordCard(GenshinAccount): @@ -176,7 +176,7 @@ def __new__(cls, **kwargs: typing.Any) -> RecordCard: has_uid: bool = Aliased("has_role") url: str - def as_dict(self) -> dict[str, typing.Any]: + def as_dict(self) -> typing.Dict[str, typing.Any]: """Return data as a dictionary.""" return {d.name: (int(d.value) if d.value.isdigit() else d.value) for d in self.data} diff --git a/genshin/models/model.py b/genshin/models/model.py index a60fb4f6..b2742263 100644 --- a/genshin/models/model.py +++ b/genshin/models/model.py @@ -33,7 +33,7 @@ def __hash__(self) -> int: def Aliased( - alias: str | None = None, + alias: typing.Optional[str] = None, default: typing.Any = None, **kwargs: typing.Any, ) -> typing.Any: diff --git a/genshin/models/starrail/chronicle/challenge.py b/genshin/models/starrail/chronicle/challenge.py index b9678a5c..81ac637b 100644 --- a/genshin/models/starrail/chronicle/challenge.py +++ b/genshin/models/starrail/chronicle/challenge.py @@ -1,6 +1,6 @@ """Starrail chronicle challenge.""" -from typing import Any, Optional +from typing import Any, Dict, List, Optional import pydantic @@ -30,7 +30,7 @@ class FloorNode(APIModel): """Node for a memory of chaos floor.""" challenge_time: PartialTime - avatars: list[FloorCharacter] + avatars: List[FloorCharacter] class StarRailChallengeFloor(APIModel): @@ -74,13 +74,13 @@ class StarRailChallenge(APIModel): total_battles: int = Aliased("battle_num") has_data: bool - floors: list[StarRailFloor] = Aliased("all_floor_detail") - seasons: list[StarRailChallengeSeason] = Aliased("groups") + floors: List[StarRailFloor] = Aliased("all_floor_detail") + seasons: List[StarRailChallengeSeason] = Aliased("groups") @pydantic.model_validator(mode="before") - def __extract_name(cls, values: dict[str, Any]) -> dict[str, Any]: - if "groups" in values and isinstance(values["groups"], list): - seasons: list[dict[str, Any]] = values["groups"] + def __extract_name(cls, values: Dict[str, Any]) -> Dict[str, Any]: + if "groups" in values and isinstance(values["groups"], List): + seasons: List[Dict[str, Any]] = values["groups"] if len(seasons) > 0: values["name"] = seasons[0]["name_mi18n"] @@ -129,14 +129,14 @@ class StarRailPureFiction(APIModel): total_battles: int = Aliased("battle_num") has_data: bool - floors: list[FictionFloor] = Aliased("all_floor_detail") - seasons: list[StarRailChallengeSeason] = Aliased("groups") + floors: List[FictionFloor] = Aliased("all_floor_detail") + seasons: List[StarRailChallengeSeason] = Aliased("groups") max_floor_id: int @pydantic.model_validator(mode="before") - def __unnest_groups(cls, values: dict[str, Any]) -> dict[str, Any]: - if "groups" in values and isinstance(values["groups"], list): - seasons: list[dict[str, Any]] = values["groups"] + def __unnest_groups(cls, values: Dict[str, Any]) -> Dict[str, Any]: + if "groups" in values and isinstance(values["groups"], List): + seasons: List[Dict[str, Any]] = values["groups"] if len(seasons) > 0: values["name"] = seasons[0]["name_mi18n"] values["season_id"] = seasons[0]["schedule_id"] @@ -196,6 +196,6 @@ class StarRailAPCShadow(APIModel): total_battles: int = Aliased("battle_num") has_data: bool - floors: list[APCShadowFloor] = Aliased("all_floor_detail") - seasons: list[APCShadowSeason] = Aliased("groups") + floors: List[APCShadowFloor] = Aliased("all_floor_detail") + seasons: List[APCShadowSeason] = Aliased("groups") max_floor_id: int diff --git a/genshin/models/starrail/chronicle/notes.py b/genshin/models/starrail/chronicle/notes.py index cfc26d04..08ef7dfd 100644 --- a/genshin/models/starrail/chronicle/notes.py +++ b/genshin/models/starrail/chronicle/notes.py @@ -11,7 +11,7 @@ class StarRailExpedition(APIModel): """StarRail expedition.""" - avatars: list[str] + avatars: typing.List[str] status: typing.Literal["Ongoing", "Finished"] remaining_time: datetime.timedelta name: str diff --git a/genshin/models/starrail/chronicle/rogue.py b/genshin/models/starrail/chronicle/rogue.py index a3882b36..ea2b5329 100644 --- a/genshin/models/starrail/chronicle/rogue.py +++ b/genshin/models/starrail/chronicle/rogue.py @@ -1,5 +1,6 @@ """Starrail Rogue models.""" +from typing import List from genshin.models.model import APIModel @@ -53,7 +54,7 @@ class RogueBuff(APIModel): """Rogue buff info.""" base_type: RogueBuffType - items: list[RogueBuffItem] + items: List[RogueBuffItem] class RogueMiracle(APIModel): @@ -70,11 +71,11 @@ class RogueRecordDetail(APIModel): name: str finish_time: PartialTime score: int - final_lineup: list[RogueCharacter] - base_type_list: list[RogueBuffType] - cached_avatars: list[RogueCharacter] - buffs: list[RogueBuff] - miracles: list[RogueMiracle] + final_lineup: List[RogueCharacter] + base_type_list: List[RogueBuffType] + cached_avatars: List[RogueCharacter] + buffs: List[RogueBuff] + miracles: List[RogueMiracle] difficulty: int progress: int @@ -83,7 +84,7 @@ class RogueRecord(APIModel): """generic record data.""" basic: RogueRecordBasic - records: list[RogueRecordDetail] + records: List[RogueRecordDetail] has_data: bool diff --git a/genshin/models/zzz/chronicle/challenge.py b/genshin/models/zzz/chronicle/challenge.py index 5acf8e19..4ccaab9c 100644 --- a/genshin/models/zzz/chronicle/challenge.py +++ b/genshin/models/zzz/chronicle/challenge.py @@ -70,18 +70,18 @@ def __convert_weakness(cls, v: int) -> typing.Union[ZZZElementType, int]: class ShiyuDefenseNode(APIModel): """Shiyu Defense node model.""" - characters: list[ShiyuDefenseCharacter] = Aliased("avatars") + characters: typing.List[ShiyuDefenseCharacter] = Aliased("avatars") bangboo: ShiyuDefenseBangboo = Aliased("buddy") - recommended_elements: list[ZZZElementType] = Aliased("element_type_list") - enemies: list[ShiyuDefenseMonster] = Aliased("monster_info") + recommended_elements: typing.List[ZZZElementType] = Aliased("element_type_list") + enemies: typing.List[ShiyuDefenseMonster] = Aliased("monster_info") @pydantic.field_validator("enemies", mode="before") @classmethod def __convert_enemies( - cls, value: dict[typing.Literal["level", "list"], typing.Any] - ) -> list[ShiyuDefenseMonster]: + cls, value: typing.Dict[typing.Literal["level", "list"], typing.Any] + ) -> typing.List[ShiyuDefenseMonster]: level = value["level"] - result: list[ShiyuDefenseMonster] = [] + result: typing.List[ShiyuDefenseMonster] = [] for monster in value["list"]: monster["level"] = level result.append(ShiyuDefenseMonster(**monster)) @@ -94,7 +94,7 @@ class ShiyuDefenseFloor(APIModel): index: int = Aliased("layer_index") rating: typing.Literal["S", "A", "B"] id: int = Aliased("layer_id") - buffs: list[ShiyuDefenseBuff] + buffs: typing.List[ShiyuDefenseBuff] node_1: ShiyuDefenseNode node_2: ShiyuDefenseNode challenge_time: DateTimeField = Aliased("floor_challenge_time") @@ -116,7 +116,7 @@ class ShiyuDefense(APIModel): end_time: typing.Optional[DateTimeField] = Aliased("hadal_end_time") has_data: bool ratings: typing.Mapping[typing.Literal["S", "A", "B"], int] = Aliased("rating_list") - floors: list[ShiyuDefenseFloor] = Aliased("all_floor_detail") + floors: typing.List[ShiyuDefenseFloor] = Aliased("all_floor_detail") fastest_clear_time: int = Aliased("fast_layer_time") """Fastest clear time this season in seconds.""" max_floor: int = Aliased("max_layer") @@ -131,6 +131,6 @@ def __parse_datetime(cls, value: typing.Mapping[str, typing.Any]) -> typing.Opti @pydantic.field_validator("ratings", mode="before") @classmethod def __convert_ratings( - cls, v: list[dict[typing.Literal["times", "rating"], typing.Any]] + cls, v: typing.List[typing.Dict[typing.Literal["times", "rating"], typing.Any]] ) -> typing.Mapping[typing.Literal["S", "A", "B"], int]: return {d["rating"]: d["times"] for d in v} diff --git a/genshin/models/zzz/chronicle/notes.py b/genshin/models/zzz/chronicle/notes.py index 090159bc..d2dafb88 100644 --- a/genshin/models/zzz/chronicle/notes.py +++ b/genshin/models/zzz/chronicle/notes.py @@ -37,7 +37,7 @@ def full_datetime(self) -> datetime.datetime: return datetime.datetime.now().astimezone() + datetime.timedelta(seconds=self.seconds_till_full) @pydantic.model_validator(mode="before") - def __unnest_progress(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: + def __unnest_progress(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: return {**values, **values.pop("progress")} @@ -61,6 +61,6 @@ def __transform_value(cls, v: typing.Literal["CardSignDone", "CardSignNotDone"]) return v == "CardSignDone" @pydantic.model_validator(mode="before") - def __unnest_value(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: + def __unnest_value(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: values["video_store_state"] = values["vhs_sale"]["sale_state"] return values diff --git a/genshin/paginators/api.py b/genshin/paginators/api.py index 580b29bd..8201a84f 100644 --- a/genshin/paginators/api.py +++ b/genshin/paginators/api.py @@ -30,7 +30,7 @@ async def __call__(self, page: int, /) -> typing.Sequence[T_co]: class TokenGetterCallback(typing.Protocol[T_co]): """Callback for returning resources based on a page or cursor.""" - async def __call__(self, token: str, /) -> tuple[str, typing.Sequence[T_co]]: + async def __call__(self, token: str, /) -> typing.Tuple[str, typing.Sequence[T_co]]: """Return a sequence of resources.""" ... @@ -55,18 +55,18 @@ class PagedPaginator(typing.Generic[T], APIPaginator[T]): getter: GetterCallback[T] """Underlying getter that yields the next page.""" - _page_size: int | None + _page_size: typing.Optional[int] """Expected non-zero page size to be able to tell the end.""" - current_page: int | None + current_page: typing.Optional[int] """Current page counter..""" def __init__( self, getter: GetterCallback[T], *, - limit: int | None = None, - page_size: int | None = None, + limit: typing.Optional[int] = None, + page_size: typing.Optional[int] = None, ) -> None: super().__init__(limit=limit) self.getter = getter @@ -74,7 +74,7 @@ def __init__( self.current_page = 1 - async def next_page(self) -> typing.Iterable[T] | None: + async def next_page(self) -> typing.Optional[typing.Iterable[T]]: """Get the next page of the paginator.""" if self.current_page is None: return None @@ -101,17 +101,17 @@ class TokenPaginator(typing.Generic[T], APIPaginator[T]): getter: TokenGetterCallback[T] """Underlying getter that yields the next page.""" - _page_size: int | None + _page_size: typing.Optional[int] """Expected non-zero page size to be able to tell the end.""" - token: str | None + token: typing.Optional[str] def __init__( self, getter: TokenGetterCallback[T], *, - limit: int | None = None, - page_size: int | None = None, + limit: typing.Optional[int] = None, + page_size: typing.Optional[int] = None, ) -> None: super().__init__(limit=limit) self.getter = getter @@ -119,7 +119,7 @@ def __init__( self.token = "" - async def next_page(self) -> typing.Iterable[T] | None: + async def next_page(self) -> typing.Optional[typing.Iterable[T]]: """Get the next page of the paginator.""" if self.token is None: return None @@ -145,19 +145,19 @@ class CursorPaginator(typing.Generic[UniqueT], APIPaginator[UniqueT]): getter: GetterCallback[UniqueT] """Underlying getter that yields the next page.""" - _page_size: int | None + _page_size: typing.Optional[int] """Expected non-zero page size to be able to tell the end.""" - end_id: int | None + end_id: typing.Optional[int] """Current end id. If none then exhausted.""" def __init__( self, getter: GetterCallback[UniqueT], *, - limit: int | None = None, + limit: typing.Optional[int] = None, end_id: int = 0, - page_size: int | None = 20, + page_size: typing.Optional[int] = 20, ) -> None: super().__init__(limit=limit) self.getter = getter @@ -165,7 +165,7 @@ def __init__( self._page_size = page_size - async def next_page(self) -> typing.Iterable[UniqueT] | None: + async def next_page(self) -> typing.Optional[typing.Iterable[UniqueT]]: """Get the next page of the paginator.""" if self.end_id is None: return None diff --git a/genshin/paginators/base.py b/genshin/paginators/base.py index f9cb8591..a466c59a 100644 --- a/genshin/paginators/base.py +++ b/genshin/paginators/base.py @@ -102,7 +102,7 @@ class BasicPaginator(typing.Generic[T], Paginator[T], abc.ABC): iterator: typing.AsyncIterator[T] """Underlying iterator.""" - def __init__(self, iterable: typing.Iterable[T] | typing.AsyncIterable[T]) -> None: + def __init__(self, iterable: typing.Union[typing.Iterable[T], typing.AsyncIterable[T]]) -> None: if isinstance(iterable, typing.AsyncIterable): self.iterator = iterable.__aiter__() else: @@ -120,16 +120,16 @@ class BufferedPaginator(typing.Generic[T], Paginator[T], abc.ABC): __slots__ = ("limit", "_buffer", "_counter") - limit: int | None + limit: typing.Optional[int] """Limit of items to be yielded.""" - _buffer: typing.Iterator[T] | None + _buffer: typing.Optional[typing.Iterator[T]] """Item buffer. If none then exhausted.""" _counter: int """Amount of yielded items so far. No guarantee to be synchronized.""" - def __init__(self, *, limit: int | None = None) -> None: + def __init__(self, *, limit: typing.Optional[int] = None) -> None: self.limit = limit self._buffer = iter(()) @@ -147,7 +147,7 @@ def _complete(self) -> typing.NoReturn: raise # pyright bug @abc.abstractmethod - async def next_page(self) -> typing.Iterable[T] | None: + async def next_page(self) -> typing.Optional[typing.Iterable[T]]: """Get the next page of the paginator.""" async def __anext__(self) -> T: @@ -185,16 +185,16 @@ class MergedPaginator(typing.Generic[T], Paginator[T]): Only used as pointers to a heap. """ - _heap: list[tuple[typing.Any, int, T, typing.AsyncIterator[T]]] + _heap: typing.List[typing.Tuple[typing.Any, int, T, typing.AsyncIterator[T]]] """Underlying heap queue. List of (comparable, unique order id, value, iterator) """ - limit: int | None + limit: typing.Optional[int] """Limit of items to be yielded""" - _key: typing.Callable[[T], typing.Any] | None + _key: typing.Optional[typing.Callable[[T], typing.Any]] """Sorting key.""" _prepared: bool @@ -207,8 +207,8 @@ def __init__( self, iterables: typing.Collection[typing.AsyncIterable[T]], *, - key: typing.Callable[[T], typing.Any] | None = None, - limit: int | None = None, + key: typing.Optional[typing.Callable[[T], typing.Any]] = None, + limit: typing.Optional[int] = None, ) -> None: self.iterators = [iterable.__aiter__() for iterable in iterables] self._key = key @@ -230,8 +230,8 @@ def _create_heap_item( self, value: T, iterator: typing.AsyncIterator[T], - order: int | None = None, - ) -> tuple[typing.Any, int, T, typing.AsyncIterator[T]]: + order: typing.Optional[int] = None, + ) -> typing.Tuple[typing.Any, int, T, typing.AsyncIterator[T]]: """Create a new item for the heap queue.""" sort_value = self._key(value) if self._key else value if order is None: diff --git a/genshin/utility/auth.py b/genshin/utility/auth.py index 5634f725..c0de46e4 100644 --- a/genshin/utility/auth.py +++ b/genshin/utility/auth.py @@ -152,12 +152,12 @@ def encrypt_credentials(text: str, key_type: typing.Literal[1, 2]) -> str: return base64.b64encode(crypto).decode("utf-8") -def get_aigis_header(session_id: str, mmt_data: dict[str, typing.Any]) -> str: +def get_aigis_header(session_id: str, mmt_data: typing.Dict[str, typing.Any]) -> str: """Get aigis header.""" return f"{session_id};{base64.b64encode(json.dumps(mmt_data).encode()).decode()}" -def generate_sign(data: dict[str, typing.Any], key: str) -> str: +def generate_sign(data: typing.Dict[str, typing.Any], key: str) -> str: """Generate a sign for the given `data` and `app_key`.""" string = "" for k in sorted(data.keys()): diff --git a/genshin/utility/concurrency.py b/genshin/utility/concurrency.py index 6b1894b6..681a5cf5 100644 --- a/genshin/utility/concurrency.py +++ b/genshin/utility/concurrency.py @@ -20,7 +20,7 @@ def prevent_concurrency(func: CallableT) -> CallableT: """ def wrapper(func: AnyCallable) -> AnyCallable: - lock: asyncio.Lock | None = None + lock: typing.Optional[asyncio.Lock] = None @functools.wraps(func) async def inner(*args: typing.Any, **kwargs: typing.Any) -> typing.Any: @@ -48,7 +48,7 @@ def __init__( method: AnyCallable, decorator: typing.Callable[[AnyCallable], AnyCallable], *, - name: str | None = None, + name: typing.Optional[str] = None, ) -> None: self.method = method # type: ignore # mypy doesn't understand methods self.decorator = decorator # type: ignore # mypy doesn't understand methods @@ -57,7 +57,7 @@ def __init__( def __set_name__(self, owner: type, name: str) -> None: self.name = name - def __get__(self, instance: T | None, owner: type[T]) -> AnyCallable: + def __get__(self, instance: typing.Optional[T], owner: typing.Type[T]) -> AnyCallable: if instance is None: return self.method diff --git a/genshin/utility/ds.py b/genshin/utility/ds.py index 7a95a3aa..d9a47951 100644 --- a/genshin/utility/ds.py +++ b/genshin/utility/ds.py @@ -47,7 +47,7 @@ def get_ds_headers( data: typing.Any = None, params: typing.Optional[typing.Mapping[str, typing.Any]] = None, lang: typing.Optional[str] = None, -) -> dict[str, typing.Any]: +) -> typing.Dict[str, typing.Any]: """Get ds http headers.""" if region == types.Region.OVERSEAS: ds_headers = { diff --git a/genshin/utility/logfile.py b/genshin/utility/logfile.py index 50f0e8c8..f0b6719b 100644 --- a/genshin/utility/logfile.py +++ b/genshin/utility/logfile.py @@ -49,7 +49,7 @@ def get_output_log(*, game: typing.Optional[types.Game] = None) -> pathlib.Path: """Get output_log.txt for a game.""" locallow = pathlib.Path("~/AppData/LocalLow").expanduser() - game_name: list[str] = [] + game_name: typing.List[str] = [] if game is None or game == types.Game.GENSHIN: game_name += ["Genshin Impact", "原神"] if game is None or game == types.Game.STARRAIL: @@ -70,7 +70,7 @@ def get_output_log(*, game: typing.Optional[types.Game] = None) -> pathlib.Path: def _expand_game_location(game_location: pathlib.Path, *, game: typing.Optional[types.Game] = None) -> pathlib.Path: """Expand a game location folder to data_2.""" - data_location: list[pathlib.Path] = [] + data_location: typing.List[pathlib.Path] = [] if "Data" in str(game_location): while "Data" not in game_location.name: game_location = game_location.parent diff --git a/tests/conftest.py b/tests/conftest.py index 66c6d9a4..3b2b502b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -209,7 +209,7 @@ def pytest_addoption(parser: pytest.Parser): parser.addoption("--cooperative", action="store_true") -def pytest_collection_modifyitems(items: list[pytest.Item], config: pytest.Config): +def pytest_collection_modifyitems(items: typing.List[pytest.Item], config: pytest.Config): if config.option.cooperative: for item in items: if isinstance(item, pytest.Function) and asyncio.iscoroutinefunction(item.obj): From 41df8b057e4e59a428e5cf2b79e74ed89590a58b Mon Sep 17 00:00:00 2001 From: seriaati Date: Sun, 22 Sep 2024 08:47:59 +0800 Subject: [PATCH 19/21] Only apply typing.Dict and typing.List fixes --- genshin-dev/setup.py | 8 +++--- genshin/__main__.py | 2 +- genshin/client/cache.py | 4 +-- genshin/client/components/auth/client.py | 7 +++-- genshin/client/components/auth/server.py | 2 +- .../client/components/auth/subclients/app.py | 2 +- genshin/client/components/base.py | 10 +++---- .../components/calculator/calculator.py | 12 ++++---- .../client/components/calculator/client.py | 2 +- genshin/client/components/chronicle/base.py | 4 +-- genshin/client/components/gacha.py | 8 +++--- genshin/client/components/hoyolab.py | 4 +-- genshin/client/components/lineup.py | 6 ++-- genshin/client/components/transaction.py | 6 ++-- genshin/client/manager/cookie.py | 2 +- genshin/client/manager/managers.py | 6 ++-- genshin/client/ratelimit.py | 2 +- genshin/errors.py | 12 ++++---- genshin/models/auth/cookie.py | 6 ++-- genshin/models/auth/geetest.py | 4 +-- genshin/models/auth/verification.py | 2 +- genshin/models/genshin/calculator.py | 14 +++++----- genshin/models/genshin/character.py | 2 +- genshin/models/genshin/chronicle/abyss.py | 2 +- .../models/genshin/chronicle/activities.py | 4 +-- .../models/genshin/chronicle/characters.py | 14 +++++----- .../models/genshin/chronicle/img_theater.py | 4 +-- genshin/models/genshin/chronicle/notes.py | 4 +-- genshin/models/genshin/chronicle/stats.py | 2 +- genshin/models/genshin/constants.py | 2 +- genshin/models/genshin/gacha.py | 6 ++-- genshin/models/genshin/lineup.py | 6 ++-- genshin/models/genshin/teapot.py | 2 +- genshin/models/genshin/wiki.py | 16 +++++------ .../models/honkai/chronicle/battlesuits.py | 2 +- genshin/models/honkai/chronicle/modes.py | 2 +- genshin/models/honkai/chronicle/stats.py | 2 +- genshin/models/honkai/constants.py | 2 +- genshin/models/hoyolab/record.py | 2 +- .../models/starrail/chronicle/challenge.py | 28 +++++++++---------- genshin/models/starrail/chronicle/notes.py | 2 +- genshin/models/starrail/chronicle/rogue.py | 16 +++++------ genshin/models/zzz/chronicle/challenge.py | 18 ++++++------ genshin/models/zzz/chronicle/notes.py | 4 +-- genshin/paginators/api.py | 2 +- genshin/paginators/base.py | 4 +-- genshin/utility/auth.py | 4 +-- genshin/utility/concurrency.py | 2 +- genshin/utility/ds.py | 2 +- genshin/utility/logfile.py | 4 +-- tests/conftest.py | 2 +- 51 files changed, 142 insertions(+), 145 deletions(-) diff --git a/genshin-dev/setup.py b/genshin-dev/setup.py index 712ff131..ee204edd 100644 --- a/genshin-dev/setup.py +++ b/genshin-dev/setup.py @@ -6,12 +6,12 @@ import setuptools -def parse_requirements_file(path: pathlib.Path) -> typing.List[str]: +def parse_requirements_file(path: pathlib.Path) -> list[str]: """Parse a requirements file into a list of requirements.""" with open(path) as fp: raw_dependencies = fp.readlines() - dependencies: typing.List[str] = [] + dependencies: list[str] = [] for dependency in raw_dependencies: comment_index = dependency.find("#") if comment_index == 0: @@ -30,8 +30,8 @@ def parse_requirements_file(path: pathlib.Path) -> typing.List[str]: normal_requirements = parse_requirements_file(dev_directory / ".." / "requirements.txt") -all_extras: typing.Set[str] = set() -extras: typing.Dict[str, typing.Sequence[str]] = {} +all_extras: set[str] = set() +extras: dict[str, typing.Sequence[str]] = {} for path in dev_directory.glob("*-requirements.txt"): name = path.name.split("-")[0] diff --git a/genshin/__main__.py b/genshin/__main__.py index 7a422299..158f830c 100644 --- a/genshin/__main__.py +++ b/genshin/__main__.py @@ -85,7 +85,7 @@ async def honkai_stats(client: genshin.Client, uid: int) -> None: for k, v in data.stats.model_dump().items(): if isinstance(v, dict): click.echo(f"{k}:") - for nested_k, nested_v in typing.cast("typing.Dict[str, object]", v).items(): + for nested_k, nested_v in typing.cast("dict[str, object]", v).items(): click.echo(f" {nested_k}: {click.style(str(nested_v), bold=True)}") else: click.echo(f"{k}: {click.style(str(v), bold=True)}") diff --git a/genshin/client/cache.py b/genshin/client/cache.py index e31743fe..03c4215e 100644 --- a/genshin/client/cache.py +++ b/genshin/client/cache.py @@ -25,7 +25,7 @@ def _separate(values: typing.Iterable[typing.Any], sep: str = ":") -> str: """Separate a sequence by a separator into a single string.""" - parts: typing.List[str] = [] + parts: list[str] = [] for value in values: if value is None: parts.append("null") @@ -83,7 +83,7 @@ async def set_static(self, key: typing.Any, value: typing.Any) -> None: class Cache(BaseCache): """Standard implementation of the cache.""" - cache: typing.Dict[typing.Any, typing.Tuple[float, typing.Any]] + cache: dict[typing.Any, tuple[float, typing.Any]] maxsize: int ttl: float static_ttl: float diff --git a/genshin/client/components/auth/client.py b/genshin/client/components/auth/client.py index 2454f045..67994caf 100644 --- a/genshin/client/components/auth/client.py +++ b/genshin/client/components/auth/client.py @@ -385,9 +385,10 @@ async def generate_fp( device_id_key: str(uuid.uuid4()).lower(), } - async with aiohttp.ClientSession() as session, session.post( - routes.GET_FP_URL.get_url(self.region), json=payload - ) as r: + async with ( + aiohttp.ClientSession() as session, + session.post(routes.GET_FP_URL.get_url(self.region), json=payload) as r, + ): data = await r.json() if data["data"]["code"] != 200: diff --git a/genshin/client/components/auth/server.py b/genshin/client/components/auth/server.py index 7baf267d..61e08bc6 100644 --- a/genshin/client/components/auth/server.py +++ b/genshin/client/components/auth/server.py @@ -25,7 +25,7 @@ __all__ = ["PAGES", "enter_code", "launch_webapp", "solve_geetest"] -PAGES: typing.Final[typing.Dict[typing.Literal["captcha", "enter-code"], str]] = { +PAGES: typing.Final[dict[typing.Literal["captcha", "enter-code"], str]] = { "captcha": """ diff --git a/genshin/client/components/auth/subclients/app.py b/genshin/client/components/auth/subclients/app.py index 0af38edb..6f0d599a 100644 --- a/genshin/client/components/auth/subclients/app.py +++ b/genshin/client/components/auth/subclients/app.py @@ -194,7 +194,7 @@ async def _create_qrcode(self) -> QRCodeCreationResult: url=data["data"]["url"], ) - async def _check_qrcode(self, ticket: str) -> typing.Tuple[QRCodeStatus, SimpleCookie]: + async def _check_qrcode(self, ticket: str) -> tuple[QRCodeStatus, SimpleCookie]: """Check the status of a QR code login.""" payload = {"ticket": ticket} diff --git a/genshin/client/components/base.py b/genshin/client/components/base.py index 851080b0..b7956c4e 100644 --- a/genshin/client/components/base.py +++ b/genshin/client/components/base.py @@ -62,10 +62,10 @@ class BaseClient(abc.ABC): _region: types.Region _default_game: typing.Optional[types.Game] - uids: typing.Dict[types.Game, int] - authkeys: typing.Dict[types.Game, str] + uids: dict[types.Game, int] + authkeys: dict[types.Game, str] _hoyolab_id: typing.Optional[int] - _accounts: typing.Dict[types.Game, hoyolab_models.GenshinAccount] + _accounts: dict[types.Game, hoyolab_models.GenshinAccount] custom_headers: multidict.CIMultiDict[str] def __init__( @@ -500,7 +500,7 @@ async def _update_cached_uids(self) -> None: """Update cached fallback uids.""" mixed_accounts = await self.get_game_accounts() - game_accounts: typing.Dict[types.Game, typing.List[hoyolab_models.GenshinAccount]] = {} + game_accounts: dict[types.Game, list[hoyolab_models.GenshinAccount]] = {} for account in mixed_accounts: if not isinstance(account.game, types.Game): # pyright: ignore[reportUnnecessaryIsInstance] continue @@ -533,7 +533,7 @@ async def _update_cached_accounts(self) -> None: """Update cached fallback accounts.""" mixed_accounts = await self.get_game_accounts() - game_accounts: typing.Dict[types.Game, typing.List[hoyolab_models.GenshinAccount]] = {} + game_accounts: dict[types.Game, list[hoyolab_models.GenshinAccount]] = {} for account in mixed_accounts: if not isinstance(account.game, types.Game): # pyright: ignore[reportUnnecessaryIsInstance] continue diff --git a/genshin/client/components/calculator/calculator.py b/genshin/client/components/calculator/calculator.py index 435bd755..675360d9 100644 --- a/genshin/client/components/calculator/calculator.py +++ b/genshin/client/components/calculator/calculator.py @@ -42,7 +42,7 @@ class CalculatorState: """Stores character details if multiple objects require them.""" client: Client - cache: typing.Dict[str, typing.Any] + cache: dict[str, typing.Any] lock: asyncio.Lock character_id: typing.Optional[int] = None @@ -150,7 +150,7 @@ async def __call__(self, state: CalculatorState) -> typing.Mapping[str, typing.A class ArtifactResolver(CalculatorResolver[typing.Sequence[typing.Mapping[str, typing.Any]]]): - data: typing.List[typing.Mapping[str, typing.Any]] + data: list[typing.Mapping[str, typing.Any]] def __init__(self) -> None: self.data = [] @@ -208,7 +208,7 @@ async def __call__(self, state: CalculatorState) -> typing.Sequence[typing.Mappi class TalentResolver(CalculatorResolver[typing.Sequence[typing.Mapping[str, typing.Any]]]): - data: typing.List[typing.Mapping[str, typing.Any]] + data: list[typing.Mapping[str, typing.Any]] def __init__(self) -> None: self.data = [] @@ -378,7 +378,7 @@ def with_current_talents( async def build(self) -> typing.Mapping[str, typing.Any]: """Build the calculator object.""" - data: typing.Dict[str, typing.Any] = {} + data: dict[str, typing.Any] = {} if self.character: data.update(await self.character(self._state)) @@ -408,7 +408,7 @@ class FurnishingCalculator: client: Client lang: typing.Optional[str] - furnishings: typing.Dict[int, int] + furnishings: dict[int, int] replica_code: typing.Optional[int] = None replica_region: typing.Optional[str] = None @@ -434,7 +434,7 @@ def with_replica(self, code: int, *, region: typing.Optional[str] = None) -> Fur async def build(self) -> typing.Mapping[str, typing.Any]: """Build the calculator object.""" - data: typing.Dict[str, typing.Any] = {} + data: dict[str, typing.Any] = {} if self.replica_code: furnishings = await self.client.get_teapot_replica_blueprint(self.replica_code, region=self.replica_region) diff --git a/genshin/client/components/calculator/client.py b/genshin/client/components/calculator/client.py index 2ba59a4d..95e789e2 100644 --- a/genshin/client/components/calculator/client.py +++ b/genshin/client/components/calculator/client.py @@ -119,7 +119,7 @@ async def _get_calculator_items( filters = dict(keywords=query, **filters) - payload: typing.Dict[str, typing.Any] = dict(page=1, size=69420, is_all=is_all, **filters) + payload: dict[str, typing.Any] = dict(page=1, size=69420, is_all=is_all, **filters) if sync: uid = uid or await self._get_uid(types.Game.GENSHIN) diff --git a/genshin/client/components/chronicle/base.py b/genshin/client/components/chronicle/base.py index fd6d9802..a9c34e01 100644 --- a/genshin/client/components/chronicle/base.py +++ b/genshin/client/components/chronicle/base.py @@ -31,7 +31,7 @@ def __str__(self) -> str: endpoint: str uid: int lang: str - params: typing.Tuple[typing.Any, ...] = () + params: tuple[typing.Any, ...] = () class BaseBattleChronicleClient(base.BaseClient): @@ -71,7 +71,7 @@ async def request_game_record( async def get_record_cards( self, hoyolab_id: typing.Optional[int] = None, *, lang: typing.Optional[str] = None - ) -> typing.List[models.hoyolab.RecordCard]: + ) -> list[models.hoyolab.RecordCard]: """Get a user's record cards.""" hoyolab_id = hoyolab_id or self._get_hoyolab_id() diff --git a/genshin/client/components/gacha.py b/genshin/client/components/gacha.py index b45af6a4..bb9f3a08 100644 --- a/genshin/client/components/gacha.py +++ b/genshin/client/components/gacha.py @@ -57,7 +57,7 @@ async def _get_gacha_page( game: typing.Optional[types.Game] = None, lang: typing.Optional[str] = None, authkey: typing.Optional[str] = None, - ) -> typing.Tuple[typing.Sequence[typing.Any], int]: + ) -> tuple[typing.Sequence[typing.Any], int]: """Get a single page of wishes.""" data = await self.request_gacha_info( "getGachaLog", @@ -150,7 +150,7 @@ def wish_history( if not isinstance(banner_types, typing.Sequence): banner_types = [banner_types] - iterators: typing.List[paginators.Paginator[models.Wish]] = [] + iterators: list[paginators.Paginator[models.Wish]] = [] for banner in banner_types: iterators.append( paginators.CursorPaginator( @@ -185,7 +185,7 @@ def warp_history( if not isinstance(banner_types, typing.Sequence): banner_types = [banner_types] - iterators: typing.List[paginators.Paginator[models.Warp]] = [] + iterators: list[paginators.Paginator[models.Warp]] = [] for banner in banner_types: iterators.append( paginators.CursorPaginator( @@ -220,7 +220,7 @@ def signal_history( if not isinstance(banner_types, typing.Sequence): banner_types = [banner_types] - iterators: typing.List[paginators.Paginator[models.SignalSearch]] = [] + iterators: list[paginators.Paginator[models.SignalSearch]] = [] for banner in banner_types: iterators.append( paginators.CursorPaginator( diff --git a/genshin/client/components/hoyolab.py b/genshin/client/components/hoyolab.py index 3df6e2e4..b781e49f 100644 --- a/genshin/client/components/hoyolab.py +++ b/genshin/client/components/hoyolab.py @@ -94,8 +94,8 @@ async def _request_announcements( ), ) - announcements: typing.List[typing.Mapping[str, typing.Any]] = [] - extra_list: typing.List[typing.Mapping[str, typing.Any]] = ( + announcements: list[typing.Mapping[str, typing.Any]] = [] + extra_list: list[typing.Mapping[str, typing.Any]] = ( info["pic_list"][0]["type_list"] if "pic_list" in info and info["pic_list"] else [] ) for sublist in info["list"] + extra_list: diff --git a/genshin/client/components/lineup.py b/genshin/client/components/lineup.py index 2ca3c8c9..a9dc5f4e 100644 --- a/genshin/client/components/lineup.py +++ b/genshin/client/components/lineup.py @@ -56,7 +56,7 @@ async def get_lineup_scenarios( lang=lang, static_cache=cache.cache_key("lineup", endpoint="tags", lang=lang or self.lang), ) - dummy: typing.Dict[str, typing.Any] = dict(id=0, name="", children=data["tree"]) + dummy: dict[str, typing.Any] = dict(id=0, name="", children=data["tree"]) return models.LineupScenarios(**dummy) @@ -70,9 +70,9 @@ async def _get_lineup_page( order: typing.Optional[str] = None, uid: typing.Optional[int] = None, lang: typing.Optional[str] = None, - ) -> typing.Tuple[str, typing.Sequence[models.LineupPreview]]: + ) -> tuple[str, typing.Sequence[models.LineupPreview]]: """Get a single page of lineups.""" - params: typing.Dict[str, typing.Any] = dict( + params: dict[str, typing.Any] = dict( next_page_token=token, limit=limit or "", tag_id=tag_id or "", diff --git a/genshin/client/components/transaction.py b/genshin/client/components/transaction.py index d74099fe..61c8e00d 100644 --- a/genshin/client/components/transaction.py +++ b/genshin/client/components/transaction.py @@ -61,10 +61,10 @@ async def _get_transaction_page( params=dict(end_id=end_id, size=20), ) - transactions: typing.List[models.BaseTransaction] = [] + transactions: list[models.BaseTransaction] = [] for trans in data["list"]: model = models.ItemTransaction if "name" in trans else models.Transaction - model = typing.cast("typing.Type[models.BaseTransaction]", model) + model = typing.cast("type[models.BaseTransaction]", model) transactions.append(model(**trans, kind=kind)) return transactions @@ -84,7 +84,7 @@ def transaction_log( if isinstance(kinds, str): kinds = [kinds] - iterators: typing.List[paginators.Paginator[models.BaseTransaction]] = [] + iterators: list[paginators.Paginator[models.BaseTransaction]] = [] for kind in kinds: iterators.append( paginators.CursorPaginator( diff --git a/genshin/client/manager/cookie.py b/genshin/client/manager/cookie.py index 2ee60d5b..c70b2537 100644 --- a/genshin/client/manager/cookie.py +++ b/genshin/client/manager/cookie.py @@ -86,7 +86,7 @@ async def fetch_cookie_with_cookie( async def fetch_cookie_with_stoken_v2( cookies: managers.CookieOrHeader, *, - token_types: typing.List[typing.Literal[2, 4]], + token_types: list[typing.Literal[2, 4]], ) -> typing.Mapping[str, str]: """Fetch cookie (v2) with an stoken (v2) and mid.""" cookies = managers.parse_cookie(cookies) diff --git a/genshin/client/manager/managers.py b/genshin/client/manager/managers.py index 9f7c1004..b79ed1f0 100644 --- a/genshin/client/manager/managers.py +++ b/genshin/client/manager/managers.py @@ -36,7 +36,7 @@ MaybeSequence = typing.Union[T, typing.Sequence[T]] -def parse_cookie(cookie: typing.Optional[CookieOrHeader]) -> typing.Dict[str, str]: +def parse_cookie(cookie: typing.Optional[CookieOrHeader]) -> dict[str, str]: """Parse a cookie or header into a cookie mapping.""" if cookie is None: return {} @@ -192,7 +192,7 @@ async def request( class CookieManager(BaseCookieManager): """Standard implementation of the cookie manager.""" - _cookies: typing.Dict[str, str] + _cookies: dict[str, str] def __init__( self, @@ -287,7 +287,7 @@ class CookieSequence(typing.Sequence[typing.Mapping[str, str]]): MAX_USES: int = 30 # {id: ({cookie}, uses), ...} - _cookies: typing.Dict[str, typing.Tuple[typing.Dict[str, str], int]] + _cookies: dict[str, tuple[dict[str, str], int]] def __init__(self, cookies: typing.Optional[typing.Sequence[CookieOrHeader]] = None) -> None: self.cookies = [parse_cookie(cookie) for cookie in cookies or []] diff --git a/genshin/client/ratelimit.py b/genshin/client/ratelimit.py index 61ef328d..7abec596 100644 --- a/genshin/client/ratelimit.py +++ b/genshin/client/ratelimit.py @@ -11,7 +11,7 @@ def handle_ratelimits( tries: int = 5, - exception: typing.Type[errors.GenshinException] = errors.VisitsTooFrequently, + exception: type[errors.GenshinException] = errors.VisitsTooFrequently, delay: float = 0.3, ) -> typing.Callable[[CallableT], CallableT]: """Handle ratelimits for requests.""" diff --git a/genshin/errors.py b/genshin/errors.py index 71724114..67e648e9 100644 --- a/genshin/errors.py +++ b/genshin/errors.py @@ -188,7 +188,7 @@ class WrongOTP(GenshinException): class GeetestError(GenshinException): """Geetest triggered during the battle chronicle API request.""" - def __init__(self, response: typing.Dict[str, typing.Any]) -> None: + def __init__(self, response: dict[str, typing.Any]) -> None: super().__init__(response) msg = "Geetest triggered during the battle chronicle API request." @@ -232,8 +232,8 @@ class VerificationCodeRateLimited(GenshinException): msg = "Too many verification code requests for the account." -_TGE = typing.Type[GenshinException] -_errors: typing.Dict[int, typing.Union[_TGE, str, typing.Tuple[_TGE, typing.Optional[str]]]] = { +_TGE = type[GenshinException] +_errors: dict[int, typing.Union[_TGE, str, tuple[_TGE, typing.Optional[str]]]] = { # misc hoyolab -100: InvalidCookies, -108: "Invalid language.", @@ -286,13 +286,13 @@ class VerificationCodeRateLimited(GenshinException): -202: IncorrectGamePassword, } -ERRORS: typing.Dict[int, typing.Tuple[_TGE, typing.Optional[str]]] = { +ERRORS: dict[int, tuple[_TGE, typing.Optional[str]]] = { retcode: (GenshinException, exc) if isinstance(exc, str) else exc if isinstance(exc, tuple) else (exc, None) for retcode, exc in _errors.items() } -def raise_for_retcode(data: typing.Dict[str, typing.Any]) -> typing.NoReturn: +def raise_for_retcode(data: dict[str, typing.Any]) -> typing.NoReturn: """Raise an equivalent error to a response. game record: @@ -327,7 +327,7 @@ def raise_for_retcode(data: typing.Dict[str, typing.Any]) -> typing.NoReturn: raise GenshinException(data) -def check_for_geetest(data: typing.Dict[str, typing.Any]) -> None: +def check_for_geetest(data: dict[str, typing.Any]) -> None: """Check if geetest was triggered during the request and raise an error if so.""" if data["retcode"] in GEETEST_RETCODES: raise GeetestError(data) diff --git a/genshin/models/auth/cookie.py b/genshin/models/auth/cookie.py index 9c0ef4ba..59549ccd 100644 --- a/genshin/models/auth/cookie.py +++ b/genshin/models/auth/cookie.py @@ -26,7 +26,7 @@ class StokenResult(pydantic.BaseModel): token: str @pydantic.model_validator(mode="before") - def _transform_result(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def _transform_result(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: return { "aid": values["user_info"]["aid"], "mid": values["user_info"]["mid"], @@ -41,7 +41,7 @@ def to_str(self) -> str: """Convert the login cookies to a string.""" return "; ".join(f"{key}={value}" for key, value in self.model_dump().items()) - def to_dict(self) -> typing.Dict[str, str]: + def to_dict(self) -> dict[str, str]: """Convert the login cookies to a dictionary.""" return self.model_dump() @@ -121,7 +121,7 @@ class DeviceGrantResult(pydantic.BaseModel): login_ticket: typing.Optional[str] = None @pydantic.model_validator(mode="before") - def _str_to_none(cls, data: typing.Dict[str, typing.Union[str, None]]) -> typing.Dict[str, typing.Union[str, None]]: + def _str_to_none(cls, data: dict[str, typing.Union[str, None]]) -> dict[str, typing.Union[str, None]]: """Convert empty strings to `None`.""" for key in data: if data[key] == "" or data[key] == "None": diff --git a/genshin/models/auth/geetest.py b/genshin/models/auth/geetest.py index 1340f3a5..cf62f07a 100644 --- a/genshin/models/auth/geetest.py +++ b/genshin/models/auth/geetest.py @@ -32,7 +32,7 @@ class BaseMMT(pydantic.BaseModel): success: int @pydantic.model_validator(mode="before") - def __parse_data(cls, data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __parse_data(cls, data: dict[str, typing.Any]) -> dict[str, typing.Any]: """Parse the data if it was provided in a raw format.""" if "data" in data: # Assume the data is aigis header and parse it @@ -89,7 +89,7 @@ class RiskyCheckMMT(MMT): class BaseMMTResult(pydantic.BaseModel): """Base Geetest verification result model.""" - def get_data(self) -> typing.Dict[str, typing.Any]: + def get_data(self) -> dict[str, typing.Any]: """Get the base MMT result data. This method acts as `dict` but excludes the `session_id` field. diff --git a/genshin/models/auth/verification.py b/genshin/models/auth/verification.py index 5d51a573..1c738c22 100644 --- a/genshin/models/auth/verification.py +++ b/genshin/models/auth/verification.py @@ -25,7 +25,7 @@ class ActionTicket(pydantic.BaseModel): verify_str: VerifyStrategy @pydantic.model_validator(mode="before") - def __parse_data(cls, data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __parse_data(cls, data: dict[str, typing.Any]) -> dict[str, typing.Any]: """Parse the data if it was provided in a raw format.""" verify_str = data["verify_str"] if isinstance(verify_str, str): diff --git a/genshin/models/genshin/calculator.py b/genshin/models/genshin/calculator.py index c9945c44..85302866 100644 --- a/genshin/models/genshin/calculator.py +++ b/genshin/models/genshin/calculator.py @@ -181,7 +181,7 @@ class CalculatorCharacterDetails(APIModel): @pydantic.field_validator("talents") def __correct_talent_current_level(cls, v: typing.Sequence[CalculatorTalent]) -> typing.Sequence[CalculatorTalent]: # passive talent have current levels at 0 for some reason - talents: typing.List[CalculatorTalent] = [] + talents: list[CalculatorTalent] = [] for talent in v: if talent.max_level == 1 and talent.level == 0: @@ -221,17 +221,17 @@ class CalculatorArtifactResult(APIModel): class CalculatorResult(APIModel): """Calculation result.""" - character: typing.List[CalculatorConsumable] = Aliased("avatar_consume") - weapon: typing.List[CalculatorConsumable] = Aliased("weapon_consume") - talents: typing.List[CalculatorConsumable] = Aliased("avatar_skill_consume") - artifacts: typing.List[CalculatorArtifactResult] = Aliased("reliquary_consume") + character: list[CalculatorConsumable] = Aliased("avatar_consume") + weapon: list[CalculatorConsumable] = Aliased("weapon_consume") + talents: list[CalculatorConsumable] = Aliased("avatar_skill_consume") + artifacts: list[CalculatorArtifactResult] = Aliased("reliquary_consume") @property def total(self) -> typing.Sequence[CalculatorConsumable]: artifacts = [i for a in self.artifacts for i in a.list] combined = self.character + self.weapon + self.talents + artifacts - grouped: typing.Dict[int, typing.List[CalculatorConsumable]] = collections.defaultdict(list) + grouped: dict[int, list[CalculatorConsumable]] = collections.defaultdict(list) for i in combined: grouped[i.id].append(i) @@ -251,7 +251,7 @@ def total(self) -> typing.Sequence[CalculatorConsumable]: class CalculatorFurnishingResults(APIModel): """Furnishing calculation result.""" - furnishings: typing.List[CalculatorConsumable] = Aliased("list") + furnishings: list[CalculatorConsumable] = Aliased("list") @property def total(self) -> typing.Sequence[CalculatorConsumable]: diff --git a/genshin/models/genshin/character.py b/genshin/models/genshin/character.py index 1b5e5b1d..797e5d2a 100644 --- a/genshin/models/genshin/character.py +++ b/genshin/models/genshin/character.py @@ -132,7 +132,7 @@ class BaseCharacter(APIModel, Unique): collab: bool = False @pydantic.model_validator(mode="before") - def __autocomplete(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __autocomplete(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: """Complete missing data.""" all_fields = list(cls.model_fields.keys()) all_aliases = {f: cls.model_fields[f].alias for f in all_fields if cls.model_fields[f].alias} diff --git a/genshin/models/genshin/chronicle/abyss.py b/genshin/models/genshin/chronicle/abyss.py index 2aa1330a..ad03da31 100644 --- a/genshin/models/genshin/chronicle/abyss.py +++ b/genshin/models/genshin/chronicle/abyss.py @@ -91,7 +91,7 @@ class SpiralAbyss(APIModel): floors: typing.Sequence[Floor] @pydantic.model_validator(mode="before") - def __nest_ranks(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, AbyssCharacter]: + def __nest_ranks(cls, values: dict[str, typing.Any]) -> dict[str, AbyssCharacter]: """By default ranks are for some reason on the same level as the rest of the abyss.""" values.setdefault("ranks", {}).update(values) return values diff --git a/genshin/models/genshin/chronicle/activities.py b/genshin/models/genshin/chronicle/activities.py index 1655b713..3a6c9271 100644 --- a/genshin/models/genshin/chronicle/activities.py +++ b/genshin/models/genshin/chronicle/activities.py @@ -27,7 +27,7 @@ class OldActivity(APIModel, typing.Generic[ModelT]): """Arbitrary activity for chinese events.""" # sometimes __parameters__ may not be provided in older versions - __parameters__: typing.ClassVar[typing.Tuple[typing.Any, ...]] = (ModelT,) # type: ignore + __parameters__: typing.ClassVar[tuple[typing.Any, ...]] = (ModelT,) # type: ignore exists_data: bool records: typing.Sequence[ModelT] @@ -310,7 +310,7 @@ class Activities(APIModel): chess: typing.Optional[Activity[typing.Any]] = None @pydantic.model_validator(mode="before") - def __flatten_activities(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __flatten_activities(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: if not values.get("activities"): return values diff --git a/genshin/models/genshin/chronicle/characters.py b/genshin/models/genshin/chronicle/characters.py index 1dba8ecf..9d2d1043 100644 --- a/genshin/models/genshin/chronicle/characters.py +++ b/genshin/models/genshin/chronicle/characters.py @@ -127,7 +127,7 @@ class Character(PartialCharacter): @pydantic.field_validator("artifacts") @classmethod def __enable_artifact_set_effects(cls, artifacts: typing.Sequence[Artifact]) -> typing.Sequence[Artifact]: - set_nums: typing.DefaultDict[int, int] = defaultdict(int) + set_nums: defaultdict[int, int] = defaultdict(int) for arti in artifacts: set_nums[arti.set.id] += 1 @@ -244,16 +244,16 @@ class GenshinDetailCharacters(APIModel): avatar_wiki: typing.Mapping[str, str] @pydantic.model_validator(mode="before") - def __fill_prop_info(cls, values: typing.Dict[str, typing.Any]) -> typing.Mapping[str, typing.Any]: + def __fill_prop_info(cls, values: dict[str, typing.Any]) -> typing.Mapping[str, typing.Any]: """Fill property info from properety_map.""" - relic_property_options: typing.Dict[str, list[int]] = values.get("relic_property_options", {}) - prop_map: typing.Dict[str, typing.Dict[str, typing.Any]] = values.get("property_map", {}) - characters: list[typing.Dict[str, typing.Any]] = values.get("list", []) + relic_property_options: dict[str, list[int]] = values.get("relic_property_options", {}) + prop_map: dict[str, dict[str, typing.Any]] = values.get("property_map", {}) + characters: list[dict[str, typing.Any]] = values.get("list", []) # Map properties to artifacts - new_relic_prop_options: typing.Dict[str, list[typing.Dict[str, typing.Any]]] = {} + new_relic_prop_options: dict[str, list[dict[str, typing.Any]]] = {} for relic_type, properties in relic_property_options.items(): - formatted_properties: list[typing.Dict[str, typing.Any]] = [ + formatted_properties: list[dict[str, typing.Any]] = [ prop_map[str(prop)] for prop in properties if str(prop) in prop_map ] new_relic_prop_options[relic_type] = formatted_properties diff --git a/genshin/models/genshin/chronicle/img_theater.py b/genshin/models/genshin/chronicle/img_theater.py index 77675e18..c4133b1a 100644 --- a/genshin/models/genshin/chronicle/img_theater.py +++ b/genshin/models/genshin/chronicle/img_theater.py @@ -145,8 +145,8 @@ class ImgTheaterData(APIModel): battle_stats: TheaterBattleStats = Aliased("fight_statisic", default=None) @pydantic.model_validator(mode="before") - def __unnest_detail(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: - detail: typing.Optional[typing.Dict[str, typing.Any]] = values.get("detail") + def __unnest_detail(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: + detail: typing.Optional[dict[str, typing.Any]] = values.get("detail") values["rounds_data"] = detail.get("rounds_data", []) if detail is not None else [] values["backup_avatars"] = detail.get("backup_avatars", []) if detail is not None else [] values["fight_statisic"] = detail.get("fight_statisic", None) if detail is not None else None diff --git a/genshin/models/genshin/chronicle/notes.py b/genshin/models/genshin/chronicle/notes.py index 41d045be..8c66943b 100644 --- a/genshin/models/genshin/chronicle/notes.py +++ b/genshin/models/genshin/chronicle/notes.py @@ -63,7 +63,7 @@ class TransformerTimedelta(datetime.timedelta): """Transformer recovery time.""" @property - def timedata(self) -> typing.Tuple[int, int, int, int]: + def timedata(self) -> tuple[int, int, int, int]: seconds: int = super().seconds days: int = super().days hour, second = divmod(seconds, 3600) @@ -219,7 +219,7 @@ def transformer_recovery_time(self) -> typing.Optional[datetime.datetime]: return remaining @pydantic.model_validator(mode="before") - def __flatten_transformer(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __flatten_transformer(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: if "transformer_recovery_time" in values: return values diff --git a/genshin/models/genshin/chronicle/stats.py b/genshin/models/genshin/chronicle/stats.py index a4402978..4a03e641 100644 --- a/genshin/models/genshin/chronicle/stats.py +++ b/genshin/models/genshin/chronicle/stats.py @@ -165,7 +165,7 @@ class PartialGenshinUserStats(APIModel): teapot: typing.Optional[Teapot] = Aliased("homes") @pydantic.field_validator("teapot", mode="before") - def __format_teapot(cls, v: typing.Any) -> typing.Optional[typing.Dict[str, typing.Any]]: + def __format_teapot(cls, v: typing.Any) -> typing.Optional[dict[str, typing.Any]]: if not v: return None if isinstance(v, dict): diff --git a/genshin/models/genshin/constants.py b/genshin/models/genshin/constants.py index 3b5cc134..4ee63743 100644 --- a/genshin/models/genshin/constants.py +++ b/genshin/models/genshin/constants.py @@ -92,4 +92,4 @@ class DBChar(typing.NamedTuple): # 10000071: ("Cyno", "Electro", 5), # 10000072: ("Candace", "Hydro", 4), # } -CHARACTER_NAMES: typing.Dict[str, typing.Dict[int, DBChar]] = {} +CHARACTER_NAMES: dict[str, dict[int, DBChar]] = {} diff --git a/genshin/models/genshin/gacha.py b/genshin/models/genshin/gacha.py index e6acbdb8..a5e0d133 100644 --- a/genshin/models/genshin/gacha.py +++ b/genshin/models/genshin/gacha.py @@ -192,9 +192,9 @@ class BannerDetails(APIModel): r5_up_items: typing.Sequence[BannerDetailsUpItem] r4_up_items: typing.Sequence[BannerDetailsUpItem] - r5_items: typing.List[BannerDetailItem] = Aliased("r5_prob_list") - r4_items: typing.List[BannerDetailItem] = Aliased("r4_prob_list") - r3_items: typing.List[BannerDetailItem] = Aliased("r3_prob_list") + r5_items: list[BannerDetailItem] = Aliased("r5_prob_list") + r4_items: list[BannerDetailItem] = Aliased("r4_prob_list") + r3_items: list[BannerDetailItem] = Aliased("r3_prob_list") @pydantic.field_validator("r5_up_items", "r4_up_items", mode="before") def __replace_none(cls, v: typing.Optional[typing.Sequence[typing.Any]]) -> typing.Sequence[typing.Any]: diff --git a/genshin/models/genshin/lineup.py b/genshin/models/genshin/lineup.py index ef6c0792..696b8aaf 100644 --- a/genshin/models/genshin/lineup.py +++ b/genshin/models/genshin/lineup.py @@ -122,7 +122,7 @@ class LineupArtifactStatFields(APIModel): secondary_stats: typing.Mapping[int, str] = Aliased("reliquary_sec_attr") @pydantic.model_validator(mode="before") - def __flatten_stats(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __flatten_stats(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: """Name certain stats.""" if "reliquary_fst_attr" not in values: return values @@ -143,7 +143,7 @@ def __flatten_stats(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[st return values @pydantic.field_validator("secondary_stats", "flower", "plume", "sands", "goblet", "circlet", mode="before") - def __parse_secondary_stats(cls, value: typing.Any) -> typing.Dict[int, str]: + def __parse_secondary_stats(cls, value: typing.Any) -> dict[int, str]: if not isinstance(value, typing.Sequence): return value @@ -186,7 +186,7 @@ class LineupScenario(APIModel, Unique): children: typing.Sequence[LineupScenario] @pydantic.model_validator(mode="before") - def __flatten_scenarios(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __flatten_scenarios(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: """Name certain scenarios.""" scenario_ids = { field.json_schema_extra["scenario_id"]: name diff --git a/genshin/models/genshin/teapot.py b/genshin/models/genshin/teapot.py index 6b6a273c..c18601ad 100644 --- a/genshin/models/genshin/teapot.py +++ b/genshin/models/genshin/teapot.py @@ -51,7 +51,7 @@ class TeapotReplica(APIModel): post_id: str title: str content: str - images: typing.List[str] = Aliased("imgs") + images: list[str] = Aliased("imgs") created_at: DateTimeField stats: TeapotReplicaStats lang: str # type: ignore diff --git a/genshin/models/genshin/wiki.py b/genshin/models/genshin/wiki.py index fa9be4bb..29f80c4b 100644 --- a/genshin/models/genshin/wiki.py +++ b/genshin/models/genshin/wiki.py @@ -37,7 +37,7 @@ class BaseWikiPreview(APIModel, Unique): name: str @pydantic.model_validator(mode="before") - def __unpack_filter_values(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __unpack_filter_values(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: filter_values = { key.split("_", 1)[1]: value["values"][0] for key, value in values.get("filter_values", {}).items() @@ -47,7 +47,7 @@ def __unpack_filter_values(cls, values: typing.Dict[str, typing.Any]) -> typing. return values @pydantic.model_validator(mode="before") - def __flatten_display_field(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __flatten_display_field(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: values.update(values.get("display_field", {})) return values @@ -104,7 +104,7 @@ class ArtifactPreview(BaseWikiPreview): effects: typing.Mapping[int, str] @pydantic.model_validator(mode="before") - def __group_effects(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __group_effects(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: effects = { 1: values["single_set_effect"], 2: values["two_set_effect"], @@ -124,7 +124,7 @@ def __parse_drop_materials(cls, value: typing.Union[str, typing.Sequence[str]]) return json.loads(value) if isinstance(value, str) else value -_ENTRY_PAGE_MODELS: typing.Mapping[WikiPageType, typing.Type[BaseWikiPreview]] = { +_ENTRY_PAGE_MODELS: typing.Mapping[WikiPageType, type[BaseWikiPreview]] = { WikiPageType.CHARACTER: CharacterPreview, WikiPageType.WEAPON: WeaponPreview, WikiPageType.ARTIFACT: ArtifactPreview, @@ -147,14 +147,14 @@ class WikiPage(APIModel): @pydantic.field_validator("modules", mode="before") def __format_modules( cls, - value: typing.Union[typing.List[typing.Dict[str, typing.Any]], typing.Dict[str, typing.Any]], - ) -> typing.Dict[str, typing.Any]: + value: typing.Union[list[dict[str, typing.Any]], dict[str, typing.Any]], + ) -> dict[str, typing.Any]: if isinstance(value, typing.Mapping): return value - modules: typing.Dict[str, typing.Dict[str, typing.Any]] = {} + modules: dict[str, dict[str, typing.Any]] = {} for module in value: - components: typing.Dict[str, typing.Dict[str, typing.Any]] = { + components: dict[str, dict[str, typing.Any]] = { component["component_id"]: json.loads(component["data"] or "{}") for component in module["components"] } diff --git a/genshin/models/honkai/chronicle/battlesuits.py b/genshin/models/honkai/chronicle/battlesuits.py index 677ca4e2..7dc0ecf0 100644 --- a/genshin/models/honkai/chronicle/battlesuits.py +++ b/genshin/models/honkai/chronicle/battlesuits.py @@ -46,7 +46,7 @@ class FullBattlesuit(battlesuit.Battlesuit): stigmata: typing.Sequence[Stigma] = Aliased("stigmatas") @pydantic.model_validator(mode="before") - def __unnest_char_data(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __unnest_char_data(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: if isinstance(values.get("character"), typing.Mapping): values.update(values["character"]) diff --git a/genshin/models/honkai/chronicle/modes.py b/genshin/models/honkai/chronicle/modes.py index 4f50462d..67535290 100644 --- a/genshin/models/honkai/chronicle/modes.py +++ b/genshin/models/honkai/chronicle/modes.py @@ -13,7 +13,7 @@ __all__ = ["ELF", "Boss", "ElysianRealm", "MemorialArena", "MemorialBattle", "OldAbyss", "SuperstringAbyss"] -REMEMBRANCE_SIGILS: typing.Dict[int, typing.Tuple[str, int]] = { +REMEMBRANCE_SIGILS: dict[int, tuple[str, int]] = { 119301: ("The MOTH Insignia", 1), 119302: ("Home Lost", 1), 119303: ("False Hope", 1), diff --git a/genshin/models/honkai/chronicle/stats.py b/genshin/models/honkai/chronicle/stats.py index a30c18f9..b7ab00b3 100644 --- a/genshin/models/honkai/chronicle/stats.py +++ b/genshin/models/honkai/chronicle/stats.py @@ -100,7 +100,7 @@ class HonkaiStats(APIModel): elysian_realm: ElysianRealmStats = Aliased() @pydantic.model_validator(mode="before") - def __pack_gamemode_stats(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __pack_gamemode_stats(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: if "new_abyss" in values: values["abyss"] = SuperstringAbyssStats(**values["new_abyss"], **values) elif "old_abyss" in values: diff --git a/genshin/models/honkai/constants.py b/genshin/models/honkai/constants.py index 3f644fa4..8e6b28fd 100644 --- a/genshin/models/honkai/constants.py +++ b/genshin/models/honkai/constants.py @@ -6,7 +6,7 @@ # TODO: Make this more dynamic # fmt: off -BATTLESUIT_IDENTIFIERS: typing.Dict[int, str] = { +BATTLESUIT_IDENTIFIERS: dict[int, str] = { 101: "KianaC2", 102: "KianaC1", 103: "KianaC4", diff --git a/genshin/models/hoyolab/record.py b/genshin/models/hoyolab/record.py index 00f46454..7607105d 100644 --- a/genshin/models/hoyolab/record.py +++ b/genshin/models/hoyolab/record.py @@ -176,7 +176,7 @@ def __new__(cls, **kwargs: typing.Any) -> RecordCard: has_uid: bool = Aliased("has_role") url: str - def as_dict(self) -> typing.Dict[str, typing.Any]: + def as_dict(self) -> dict[str, typing.Any]: """Return data as a dictionary.""" return {d.name: (int(d.value) if d.value.isdigit() else d.value) for d in self.data} diff --git a/genshin/models/starrail/chronicle/challenge.py b/genshin/models/starrail/chronicle/challenge.py index 81ac637b..b9678a5c 100644 --- a/genshin/models/starrail/chronicle/challenge.py +++ b/genshin/models/starrail/chronicle/challenge.py @@ -1,6 +1,6 @@ """Starrail chronicle challenge.""" -from typing import Any, Dict, List, Optional +from typing import Any, Optional import pydantic @@ -30,7 +30,7 @@ class FloorNode(APIModel): """Node for a memory of chaos floor.""" challenge_time: PartialTime - avatars: List[FloorCharacter] + avatars: list[FloorCharacter] class StarRailChallengeFloor(APIModel): @@ -74,13 +74,13 @@ class StarRailChallenge(APIModel): total_battles: int = Aliased("battle_num") has_data: bool - floors: List[StarRailFloor] = Aliased("all_floor_detail") - seasons: List[StarRailChallengeSeason] = Aliased("groups") + floors: list[StarRailFloor] = Aliased("all_floor_detail") + seasons: list[StarRailChallengeSeason] = Aliased("groups") @pydantic.model_validator(mode="before") - def __extract_name(cls, values: Dict[str, Any]) -> Dict[str, Any]: - if "groups" in values and isinstance(values["groups"], List): - seasons: List[Dict[str, Any]] = values["groups"] + def __extract_name(cls, values: dict[str, Any]) -> dict[str, Any]: + if "groups" in values and isinstance(values["groups"], list): + seasons: list[dict[str, Any]] = values["groups"] if len(seasons) > 0: values["name"] = seasons[0]["name_mi18n"] @@ -129,14 +129,14 @@ class StarRailPureFiction(APIModel): total_battles: int = Aliased("battle_num") has_data: bool - floors: List[FictionFloor] = Aliased("all_floor_detail") - seasons: List[StarRailChallengeSeason] = Aliased("groups") + floors: list[FictionFloor] = Aliased("all_floor_detail") + seasons: list[StarRailChallengeSeason] = Aliased("groups") max_floor_id: int @pydantic.model_validator(mode="before") - def __unnest_groups(cls, values: Dict[str, Any]) -> Dict[str, Any]: - if "groups" in values and isinstance(values["groups"], List): - seasons: List[Dict[str, Any]] = values["groups"] + def __unnest_groups(cls, values: dict[str, Any]) -> dict[str, Any]: + if "groups" in values and isinstance(values["groups"], list): + seasons: list[dict[str, Any]] = values["groups"] if len(seasons) > 0: values["name"] = seasons[0]["name_mi18n"] values["season_id"] = seasons[0]["schedule_id"] @@ -196,6 +196,6 @@ class StarRailAPCShadow(APIModel): total_battles: int = Aliased("battle_num") has_data: bool - floors: List[APCShadowFloor] = Aliased("all_floor_detail") - seasons: List[APCShadowSeason] = Aliased("groups") + floors: list[APCShadowFloor] = Aliased("all_floor_detail") + seasons: list[APCShadowSeason] = Aliased("groups") max_floor_id: int diff --git a/genshin/models/starrail/chronicle/notes.py b/genshin/models/starrail/chronicle/notes.py index 08ef7dfd..cfc26d04 100644 --- a/genshin/models/starrail/chronicle/notes.py +++ b/genshin/models/starrail/chronicle/notes.py @@ -11,7 +11,7 @@ class StarRailExpedition(APIModel): """StarRail expedition.""" - avatars: typing.List[str] + avatars: list[str] status: typing.Literal["Ongoing", "Finished"] remaining_time: datetime.timedelta name: str diff --git a/genshin/models/starrail/chronicle/rogue.py b/genshin/models/starrail/chronicle/rogue.py index ea2b5329..f44cebd1 100644 --- a/genshin/models/starrail/chronicle/rogue.py +++ b/genshin/models/starrail/chronicle/rogue.py @@ -1,7 +1,5 @@ """Starrail Rogue models.""" -from typing import List - from genshin.models.model import APIModel from ..character import RogueCharacter @@ -54,7 +52,7 @@ class RogueBuff(APIModel): """Rogue buff info.""" base_type: RogueBuffType - items: List[RogueBuffItem] + items: list[RogueBuffItem] class RogueMiracle(APIModel): @@ -71,11 +69,11 @@ class RogueRecordDetail(APIModel): name: str finish_time: PartialTime score: int - final_lineup: List[RogueCharacter] - base_type_list: List[RogueBuffType] - cached_avatars: List[RogueCharacter] - buffs: List[RogueBuff] - miracles: List[RogueMiracle] + final_lineup: list[RogueCharacter] + base_type_list: list[RogueBuffType] + cached_avatars: list[RogueCharacter] + buffs: list[RogueBuff] + miracles: list[RogueMiracle] difficulty: int progress: int @@ -84,7 +82,7 @@ class RogueRecord(APIModel): """generic record data.""" basic: RogueRecordBasic - records: List[RogueRecordDetail] + records: list[RogueRecordDetail] has_data: bool diff --git a/genshin/models/zzz/chronicle/challenge.py b/genshin/models/zzz/chronicle/challenge.py index 4ccaab9c..daad140c 100644 --- a/genshin/models/zzz/chronicle/challenge.py +++ b/genshin/models/zzz/chronicle/challenge.py @@ -70,18 +70,16 @@ def __convert_weakness(cls, v: int) -> typing.Union[ZZZElementType, int]: class ShiyuDefenseNode(APIModel): """Shiyu Defense node model.""" - characters: typing.List[ShiyuDefenseCharacter] = Aliased("avatars") + characters: list[ShiyuDefenseCharacter] = Aliased("avatars") bangboo: ShiyuDefenseBangboo = Aliased("buddy") - recommended_elements: typing.List[ZZZElementType] = Aliased("element_type_list") - enemies: typing.List[ShiyuDefenseMonster] = Aliased("monster_info") + recommended_elements: list[ZZZElementType] = Aliased("element_type_list") + enemies: list[ShiyuDefenseMonster] = Aliased("monster_info") @pydantic.field_validator("enemies", mode="before") @classmethod - def __convert_enemies( - cls, value: typing.Dict[typing.Literal["level", "list"], typing.Any] - ) -> typing.List[ShiyuDefenseMonster]: + def __convert_enemies(cls, value: dict[typing.Literal["level", "list"], typing.Any]) -> list[ShiyuDefenseMonster]: level = value["level"] - result: typing.List[ShiyuDefenseMonster] = [] + result: list[ShiyuDefenseMonster] = [] for monster in value["list"]: monster["level"] = level result.append(ShiyuDefenseMonster(**monster)) @@ -94,7 +92,7 @@ class ShiyuDefenseFloor(APIModel): index: int = Aliased("layer_index") rating: typing.Literal["S", "A", "B"] id: int = Aliased("layer_id") - buffs: typing.List[ShiyuDefenseBuff] + buffs: list[ShiyuDefenseBuff] node_1: ShiyuDefenseNode node_2: ShiyuDefenseNode challenge_time: DateTimeField = Aliased("floor_challenge_time") @@ -116,7 +114,7 @@ class ShiyuDefense(APIModel): end_time: typing.Optional[DateTimeField] = Aliased("hadal_end_time") has_data: bool ratings: typing.Mapping[typing.Literal["S", "A", "B"], int] = Aliased("rating_list") - floors: typing.List[ShiyuDefenseFloor] = Aliased("all_floor_detail") + floors: list[ShiyuDefenseFloor] = Aliased("all_floor_detail") fastest_clear_time: int = Aliased("fast_layer_time") """Fastest clear time this season in seconds.""" max_floor: int = Aliased("max_layer") @@ -131,6 +129,6 @@ def __parse_datetime(cls, value: typing.Mapping[str, typing.Any]) -> typing.Opti @pydantic.field_validator("ratings", mode="before") @classmethod def __convert_ratings( - cls, v: typing.List[typing.Dict[typing.Literal["times", "rating"], typing.Any]] + cls, v: list[dict[typing.Literal["times", "rating"], typing.Any]] ) -> typing.Mapping[typing.Literal["S", "A", "B"], int]: return {d["rating"]: d["times"] for d in v} diff --git a/genshin/models/zzz/chronicle/notes.py b/genshin/models/zzz/chronicle/notes.py index d2dafb88..090159bc 100644 --- a/genshin/models/zzz/chronicle/notes.py +++ b/genshin/models/zzz/chronicle/notes.py @@ -37,7 +37,7 @@ def full_datetime(self) -> datetime.datetime: return datetime.datetime.now().astimezone() + datetime.timedelta(seconds=self.seconds_till_full) @pydantic.model_validator(mode="before") - def __unnest_progress(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __unnest_progress(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: return {**values, **values.pop("progress")} @@ -61,6 +61,6 @@ def __transform_value(cls, v: typing.Literal["CardSignDone", "CardSignNotDone"]) return v == "CardSignDone" @pydantic.model_validator(mode="before") - def __unnest_value(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def __unnest_value(cls, values: dict[str, typing.Any]) -> dict[str, typing.Any]: values["video_store_state"] = values["vhs_sale"]["sale_state"] return values diff --git a/genshin/paginators/api.py b/genshin/paginators/api.py index 8201a84f..7d99ff03 100644 --- a/genshin/paginators/api.py +++ b/genshin/paginators/api.py @@ -30,7 +30,7 @@ async def __call__(self, page: int, /) -> typing.Sequence[T_co]: class TokenGetterCallback(typing.Protocol[T_co]): """Callback for returning resources based on a page or cursor.""" - async def __call__(self, token: str, /) -> typing.Tuple[str, typing.Sequence[T_co]]: + async def __call__(self, token: str, /) -> tuple[str, typing.Sequence[T_co]]: """Return a sequence of resources.""" ... diff --git a/genshin/paginators/base.py b/genshin/paginators/base.py index a466c59a..2ec595e5 100644 --- a/genshin/paginators/base.py +++ b/genshin/paginators/base.py @@ -185,7 +185,7 @@ class MergedPaginator(typing.Generic[T], Paginator[T]): Only used as pointers to a heap. """ - _heap: typing.List[typing.Tuple[typing.Any, int, T, typing.AsyncIterator[T]]] + _heap: list[tuple[typing.Any, int, T, typing.AsyncIterator[T]]] """Underlying heap queue. List of (comparable, unique order id, value, iterator) @@ -231,7 +231,7 @@ def _create_heap_item( value: T, iterator: typing.AsyncIterator[T], order: typing.Optional[int] = None, - ) -> typing.Tuple[typing.Any, int, T, typing.AsyncIterator[T]]: + ) -> tuple[typing.Any, int, T, typing.AsyncIterator[T]]: """Create a new item for the heap queue.""" sort_value = self._key(value) if self._key else value if order is None: diff --git a/genshin/utility/auth.py b/genshin/utility/auth.py index c0de46e4..5634f725 100644 --- a/genshin/utility/auth.py +++ b/genshin/utility/auth.py @@ -152,12 +152,12 @@ def encrypt_credentials(text: str, key_type: typing.Literal[1, 2]) -> str: return base64.b64encode(crypto).decode("utf-8") -def get_aigis_header(session_id: str, mmt_data: typing.Dict[str, typing.Any]) -> str: +def get_aigis_header(session_id: str, mmt_data: dict[str, typing.Any]) -> str: """Get aigis header.""" return f"{session_id};{base64.b64encode(json.dumps(mmt_data).encode()).decode()}" -def generate_sign(data: typing.Dict[str, typing.Any], key: str) -> str: +def generate_sign(data: dict[str, typing.Any], key: str) -> str: """Generate a sign for the given `data` and `app_key`.""" string = "" for k in sorted(data.keys()): diff --git a/genshin/utility/concurrency.py b/genshin/utility/concurrency.py index 681a5cf5..7fa64fe4 100644 --- a/genshin/utility/concurrency.py +++ b/genshin/utility/concurrency.py @@ -57,7 +57,7 @@ def __init__( def __set_name__(self, owner: type, name: str) -> None: self.name = name - def __get__(self, instance: typing.Optional[T], owner: typing.Type[T]) -> AnyCallable: + def __get__(self, instance: typing.Optional[T], owner: type[T]) -> AnyCallable: if instance is None: return self.method diff --git a/genshin/utility/ds.py b/genshin/utility/ds.py index d9a47951..7a95a3aa 100644 --- a/genshin/utility/ds.py +++ b/genshin/utility/ds.py @@ -47,7 +47,7 @@ def get_ds_headers( data: typing.Any = None, params: typing.Optional[typing.Mapping[str, typing.Any]] = None, lang: typing.Optional[str] = None, -) -> typing.Dict[str, typing.Any]: +) -> dict[str, typing.Any]: """Get ds http headers.""" if region == types.Region.OVERSEAS: ds_headers = { diff --git a/genshin/utility/logfile.py b/genshin/utility/logfile.py index f0b6719b..50f0e8c8 100644 --- a/genshin/utility/logfile.py +++ b/genshin/utility/logfile.py @@ -49,7 +49,7 @@ def get_output_log(*, game: typing.Optional[types.Game] = None) -> pathlib.Path: """Get output_log.txt for a game.""" locallow = pathlib.Path("~/AppData/LocalLow").expanduser() - game_name: typing.List[str] = [] + game_name: list[str] = [] if game is None or game == types.Game.GENSHIN: game_name += ["Genshin Impact", "原神"] if game is None or game == types.Game.STARRAIL: @@ -70,7 +70,7 @@ def get_output_log(*, game: typing.Optional[types.Game] = None) -> pathlib.Path: def _expand_game_location(game_location: pathlib.Path, *, game: typing.Optional[types.Game] = None) -> pathlib.Path: """Expand a game location folder to data_2.""" - data_location: typing.List[pathlib.Path] = [] + data_location: list[pathlib.Path] = [] if "Data" in str(game_location): while "Data" not in game_location.name: game_location = game_location.parent diff --git a/tests/conftest.py b/tests/conftest.py index 3b2b502b..66c6d9a4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -209,7 +209,7 @@ def pytest_addoption(parser: pytest.Parser): parser.addoption("--cooperative", action="store_true") -def pytest_collection_modifyitems(items: typing.List[pytest.Item], config: pytest.Config): +def pytest_collection_modifyitems(items: list[pytest.Item], config: pytest.Config): if config.option.cooperative: for item in items: if isinstance(item, pytest.Function) and asyncio.iscoroutinefunction(item.obj): From 70796d69f271a33f57426813ede1d4ba5f068ef3 Mon Sep 17 00:00:00 2001 From: seriaati Date: Sun, 22 Sep 2024 08:49:52 +0800 Subject: [PATCH 20/21] Remove unused import --- genshin/models/honkai/constants.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/genshin/models/honkai/constants.py b/genshin/models/honkai/constants.py index 8e6b28fd..765d702d 100644 --- a/genshin/models/honkai/constants.py +++ b/genshin/models/honkai/constants.py @@ -1,7 +1,5 @@ """Honkai model constants.""" -import typing - __all__ = ["BATTLESUIT_IDENTIFIERS"] # TODO: Make this more dynamic From e3eddfa4e67f235dba5e85c0af930ef7a41ee5c2 Mon Sep 17 00:00:00 2001 From: seriaati Date: Sun, 22 Sep 2024 08:50:34 +0800 Subject: [PATCH 21/21] Remove use of pydantic v1 validator --- genshin/models/zzz/chronicle/challenge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/genshin/models/zzz/chronicle/challenge.py b/genshin/models/zzz/chronicle/challenge.py index daad140c..547d0d8e 100644 --- a/genshin/models/zzz/chronicle/challenge.py +++ b/genshin/models/zzz/chronicle/challenge.py @@ -59,7 +59,7 @@ class ShiyuDefenseMonster(APIModel): weakness: typing.Union[ZZZElementType, int] = Aliased("weak_element_type") level: int - @pydantic.validator("weakness", pre=True) + @pydantic.field_validator("weakness", mode="before") def __convert_weakness(cls, v: int) -> typing.Union[ZZZElementType, int]: try: return ZZZElementType(v)