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")()