From df0a451c1e44afa7a08abe246358e03a25fac4c0 Mon Sep 17 00:00:00 2001 From: omg-xtao <100690902+omg-xtao@users.noreply.github.com> Date: Sat, 30 Nov 2024 20:45:19 +0800 Subject: [PATCH] :arrow_up: upgrade Pydantic to V2 and drop python 3.8 --- pyproject.toml | 8 +- simnet/models/base.py | 107 +++++++++++++++--- simnet/models/diary.py | 4 +- simnet/models/genshin/calculator.py | 22 ++-- simnet/models/genshin/chronicle/abyss.py | 19 ++-- .../models/genshin/chronicle/act_calendar.py | 8 +- .../genshin/chronicle/character_detail.py | 9 +- simnet/models/genshin/chronicle/characters.py | 7 +- .../models/genshin/chronicle/img_theater.py | 19 ++-- simnet/models/genshin/chronicle/notes.py | 56 +++------ simnet/models/genshin/chronicle/stats.py | 58 +++++----- simnet/models/genshin/diary.py | 8 +- simnet/models/genshin/transaction.py | 7 +- simnet/models/genshin/wish.py | 37 +++--- simnet/models/lab/announcement.py | 4 +- simnet/models/lab/daily.py | 11 +- simnet/models/lab/record.py | 9 +- simnet/models/starrail/calculator.py | 13 ++- .../models/starrail/chronicle/act_calendar.py | 11 +- simnet/models/starrail/chronicle/challenge.py | 4 +- .../starrail/chronicle/challenge_boss.py | 4 +- .../starrail/chronicle/challenge_story.py | 4 +- .../models/starrail/chronicle/characters.py | 13 ++- simnet/models/starrail/chronicle/notes.py | 14 +-- .../models/starrail/chronicle/rogue_tourn.py | 2 +- simnet/models/starrail/chronicle/stats.py | 4 +- simnet/models/starrail/diary.py | 4 +- simnet/models/starrail/wish.py | 7 +- simnet/models/zzz/calculator.py | 6 +- simnet/models/zzz/character.py | 4 +- simnet/models/zzz/chronicle/abyss_abstract.py | 5 +- simnet/models/zzz/chronicle/challenge.py | 10 +- simnet/models/zzz/chronicle/notes.py | 8 +- simnet/models/zzz/chronicle/stats.py | 4 +- simnet/models/zzz/diary.py | 4 +- simnet/models/zzz/self_help.py | 7 +- simnet/models/zzz/wish.py | 10 +- 37 files changed, 278 insertions(+), 253 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 59ea2fd..5a84601 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,15 @@ [tool.poetry] name = "SIMNet" -version = "0.1.24" +version = "0.2.0" description = "Modern API wrapper for Genshin Impact & Honkai: Star Rail built on asyncio and pydantic." authors = ["PaiGramTeam"] license = "MIT license" readme = "README.md" [tool.poetry.dependencies] -python = "^3.8" +python = "^3.9" httpx = ">=0.25.0" -pydantic = "<2.0.0,>=1.10.7" +pydantic = "<3.0.0,>=2.0.0" [tool.poetry.group.dev.dependencies] pytest = "^7.4.0" @@ -33,4 +33,4 @@ log_cli_date_format = "%Y-%m-%d %H:%M:%S" [tool.black] include = '\.pyi?$' line-length = 120 -target-version = ['py38', 'py39', 'py310', 'py311', 'py312'] \ No newline at end of file +target-version = ['py39', 'py310', 'py311', 'py312', 'py313'] diff --git a/simnet/models/base.py b/simnet/models/base.py index a773ac0..23c79ea 100644 --- a/simnet/models/base.py +++ b/simnet/models/base.py @@ -1,25 +1,100 @@ -from typing import Any +import datetime +import typing -from pydantic import BaseModel +from pydantic import ConfigDict, BaseModel, Field as PydanticField, AfterValidator, BeforeValidator, WrapSerializer -try: - import ujson as jsonlib -except ImportError: - import json as jsonlib +if typing.TYPE_CHECKING: + from pydantic import SerializerFunctionWrapHandler, SerializationInfo + +CN_TIMEZONE = datetime.timezone(datetime.timedelta(hours=8)) class APIModel(BaseModel): """A Pydantic BaseModel class used for modeling JSON data returned by an API.""" - def __init__(self, **data: Any) -> None: - for field_name, field in self.__fields__.items(): - aliases = field.field_info.extra.get("aliases") - if aliases and aliases in data: - data[field_name] = data.pop(aliases) - super().__init__(**data) + model_config = ConfigDict(coerce_numbers_to_str=True, arbitrary_types_allowed=True) + + +def Field( + default: typing.Any = None, + alias: typing.Optional[str] = None, + **kwargs: typing.Any, +): + """Create an aliased field.""" + return PydanticField(default, alias=alias, **kwargs) + + +def add_timezone(value: datetime.datetime) -> datetime.datetime: + """ + Adds the CN_TIMEZONE to a datetime object. + + Args: + value (datetime.datetime): The datetime object to which the timezone will be added. + + Returns: + datetime.datetime: The datetime object with the CN_TIMEZONE applied. + """ + return value.astimezone(CN_TIMEZONE) + + +def str_time_date_plain( + value: datetime.datetime, handler: "SerializerFunctionWrapHandler", info: "SerializationInfo" +) -> typing.Union[str, datetime.datetime]: + """ + Converts a datetime object to its ISO 8601 string representation if the mode is JSON, otherwise uses the handler. + + Args: + value (datetime.datetime): The datetime object to convert. + handler (SerializerFunctionWrapHandler): The handler function to use if the mode is not JSON. + info (SerializationInfo): Information about the serialization context. + + Returns: + typing.Union[str, datetime.datetime]: The ISO 8601 string representation if the mode is JSON, otherwise the result of the handler. + """ + if info.mode_is_json(): + return value.isoformat() + return handler(value) + + +def str_time_delta_parsing(v: str) -> datetime.timedelta: + """ + Parses a string representing seconds into a timedelta object. + + Args: + v (str): The string representing the number of seconds. + + Returns: + datetime.timedelta: The resulting timedelta object. + """ + return datetime.timedelta(seconds=int(v)) + + +def str_time_delta_plain( + value: datetime.timedelta, handler: "SerializerFunctionWrapHandler", info: "SerializationInfo" +) -> typing.Union[float, datetime.timedelta]: + """ + Converts a timedelta object to its total seconds as a float if the mode is JSON, otherwise uses the handler. + + Args: + value (datetime.timedelta): The timedelta object to convert. + handler (SerializerFunctionWrapHandler): The handler function to use if the mode is not JSON. + info (SerializationInfo): Information about the serialization context. + + Returns: + typing.Union[float, datetime.timedelta]: The total seconds as a float if the mode is JSON, otherwise the result of the handler. + """ + if info.mode_is_json(): + return value.total_seconds() + return handler(value) - class Config: - """A nested class defining configuration options for the APIModel.""" - json_dumps = jsonlib.dumps - json_loads = jsonlib.loads +DateTimeField = typing.Annotated[ + datetime.datetime, + AfterValidator(add_timezone), + WrapSerializer(str_time_date_plain), +] +TimeDeltaField = typing.Annotated[ + datetime.timedelta, + BeforeValidator(str_time_delta_parsing), + WrapSerializer(str_time_delta_plain), +] diff --git a/simnet/models/diary.py b/simnet/models/diary.py index 0c558a8..82fa4ce 100644 --- a/simnet/models/diary.py +++ b/simnet/models/diary.py @@ -1,8 +1,6 @@ from enum import IntEnum -from pydantic import Field - -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field __all__ = ( "DiaryType", diff --git a/simnet/models/genshin/calculator.py b/simnet/models/genshin/calculator.py index b6629dd..f44d710 100644 --- a/simnet/models/genshin/calculator.py +++ b/simnet/models/genshin/calculator.py @@ -3,9 +3,9 @@ import collections from typing import Dict, Any, Literal, Optional, List -from pydantic import Field, validator +from pydantic import field_validator -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field from simnet.models.genshin.character import BaseCharacter __all__ = ( @@ -65,7 +65,8 @@ class CalculatorCharacter(BaseCharacter): level: int = Field(alias="level_current", default=0) max_level: int - @validator("element", pre=True) + @field_validator("element", mode="before") + @classmethod def parse_element(cls, v: Any) -> str: """Parse the element of a character. @@ -80,7 +81,8 @@ def parse_element(cls, v: Any) -> str: return CALCULATOR_ELEMENTS[int(v)] - @validator("weapon_type", pre=True) + @field_validator("weapon_type", mode="before") + @classmethod def parse_weapon_type(cls, v: Any) -> str: """Parse the weapon type of character. @@ -117,7 +119,8 @@ class CalculatorWeapon(APIModel): level: int = Field(alias="level_current", default=0) max_level: int - @validator("type", pre=True) + @field_validator("type", mode="before") + @classmethod def parse_weapon_type(cls, v: Any) -> str: """ Parse the type of weapon. @@ -241,7 +244,7 @@ class CalculatorFurnishing(APIModel): icon: str = Field(alias="icon_url") rarity: int = Field(alias="level") - amount: Optional[int] = Field(alias="num") + amount: Optional[int] = Field(None, alias="num") class CalculatorCharacterDetails(APIModel): @@ -253,11 +256,12 @@ class CalculatorCharacterDetails(APIModel): artifacts (List[CalculatorArtifact]): A list of calculator artifacts. """ - weapon: Optional[CalculatorWeapon] = Field(alias="weapon") + weapon: Optional[CalculatorWeapon] = Field(None, alias="weapon") talents: List[CalculatorTalent] = Field(alias="skill_list") artifacts: List[CalculatorArtifact] = Field(alias="reliquary_list") - @validator("talents") + @field_validator("talents") + @classmethod def correct_talent_current_level(cls, v: List[CalculatorTalent]) -> List[CalculatorTalent]: """Validates the current level of each calculator talent in the talents list and sets it to 1 if it is 0. @@ -273,7 +277,7 @@ def correct_talent_current_level(cls, v: List[CalculatorTalent]) -> List[Calcula 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/simnet/models/genshin/chronicle/abyss.py b/simnet/models/genshin/chronicle/abyss.py index 77f04cc..324b492 100644 --- a/simnet/models/genshin/chronicle/abyss.py +++ b/simnet/models/genshin/chronicle/abyss.py @@ -1,9 +1,8 @@ -import datetime from typing import List, Dict, Any, Literal -from pydantic import Field, root_validator +from pydantic import model_validator -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field, DateTimeField from simnet.models.genshin.character import BaseCharacter __all__ = ( @@ -54,10 +53,7 @@ class CharacterRanks(APIModel): most_skills_used (List[AbyssRankCharacter]): The characters that have used their elemental skill the most. """ - most_played: List[AbyssRankCharacter] = Field( - default=[], - alias="reveal_rank", - ) + most_played: List[AbyssRankCharacter] = Field(default=[], alias="reveal_rank") most_kills: List[AbyssRankCharacter] = Field( default=[], alias="defeat_rank", @@ -90,7 +86,7 @@ class Battle(APIModel): """ half: int = Field(alias="index") - timestamp: datetime.datetime + timestamp: DateTimeField characters: List[AbyssCharacter] = Field(alias="avatars") @@ -146,8 +142,8 @@ class SpiralAbyss(APIModel): unlocked: bool = Field(alias="is_unlock") season: int = Field(alias="schedule_id") - start_time: datetime.datetime - end_time: datetime.datetime + start_time: DateTimeField + end_time: DateTimeField total_battles: int = Field(alias="total_battle_times") total_wins: str = Field(alias="total_win_times") @@ -158,7 +154,8 @@ class SpiralAbyss(APIModel): floors: List[Floor] - @root_validator(pre=True) + @model_validator(mode="before") + @classmethod def nest_ranks(cls, values: Dict[str, 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) diff --git a/simnet/models/genshin/chronicle/act_calendar.py b/simnet/models/genshin/chronicle/act_calendar.py index 2186797..b82c57e 100644 --- a/simnet/models/genshin/chronicle/act_calendar.py +++ b/simnet/models/genshin/chronicle/act_calendar.py @@ -1,7 +1,7 @@ -from datetime import datetime, timedelta +from datetime import datetime from typing import List -from simnet.models.base import APIModel +from simnet.models.base import APIModel, TimeDeltaField from simnet.models.genshin.character import BaseCharacter @@ -30,7 +30,7 @@ class CardPoolListItem(APIModel): end_timestamp: datetime jump_url: str pool_status: int - countdown_seconds: timedelta + countdown_seconds: TimeDeltaField class RewardItem(APIModel): @@ -55,7 +55,7 @@ class ActListItem(APIModel): end_timestamp: datetime desc: str strategy: str - countdown_seconds: timedelta + countdown_seconds: TimeDeltaField status: int reward_list: List[RewardItem] is_finished: bool diff --git a/simnet/models/genshin/chronicle/character_detail.py b/simnet/models/genshin/chronicle/character_detail.py index efbd2f1..bdc9e87 100644 --- a/simnet/models/genshin/chronicle/character_detail.py +++ b/simnet/models/genshin/chronicle/character_detail.py @@ -1,10 +1,9 @@ import enum import typing -import pydantic -from pydantic import Field +from pydantic import field_validator -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field from simnet.models.genshin.chronicle.characters import ( PartialCharacter, CharacterWeapon, @@ -51,10 +50,10 @@ class PropInfo(APIModel): type: int = Field(alias="property_type") name: str - icon: typing.Optional[str] + icon: typing.Optional[str] = None filter_name: str - @pydantic.validator("name", "filter_name") + @field_validator("name", "filter_name") @classmethod def __fix_names(cls, value: str) -> str: # skipcq: PTC-W0038 r"""Fix "\xa0" in Crit Damage + Crit Rate names.""" diff --git a/simnet/models/genshin/chronicle/characters.py b/simnet/models/genshin/chronicle/characters.py index 4061e7b..bec6aff 100644 --- a/simnet/models/genshin/chronicle/characters.py +++ b/simnet/models/genshin/chronicle/characters.py @@ -1,8 +1,8 @@ from typing import List, Dict, TYPE_CHECKING -from pydantic import Field, validator +from pydantic import field_validator -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field from simnet.models.genshin.character import BaseCharacter if TYPE_CHECKING: @@ -176,7 +176,8 @@ class Character(PartialCharacter): constellations: List[Constellation] outfits: List[Outfit] = Field(alias="costumes") - @validator("artifacts") + @field_validator("artifacts") + @classmethod def add_artifact_effect_enabled(cls, artifacts: List[Artifact]) -> List[Artifact]: """ Determines which artifact set effects are enabled for the character's equipped artifacts. diff --git a/simnet/models/genshin/chronicle/img_theater.py b/simnet/models/genshin/chronicle/img_theater.py index 5c75a0a..1f20fe8 100644 --- a/simnet/models/genshin/chronicle/img_theater.py +++ b/simnet/models/genshin/chronicle/img_theater.py @@ -1,11 +1,8 @@ -import datetime import enum import typing from typing import Optional -from pydantic import Field - -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field, DateTimeField from simnet.models.genshin.character import BaseCharacter from simnet.models.starrail.chronicle.base import PartialTime from simnet.models.zzz.calculator import desc_to_html @@ -128,7 +125,7 @@ class Act(APIModel): buffs: typing.Sequence[TheaterBuff] is_get_medal: bool round_id: int - finish_time: datetime.datetime + finish_time: DateTimeField finish_date_time: PartialTime enemies: typing.Optional[typing.Sequence[TheaterEnemy]] = None splendour_buff: typing.Optional[TheaterSplendourBuff] = None @@ -157,8 +154,8 @@ class TheaterStats(APIModel): class TheaterSchedule(APIModel): """Imaginarium theater schedule.""" - start_time: datetime.datetime - end_time: datetime.datetime + start_time: DateTimeField + end_time: DateTimeField schedule_type: int # Not sure what this is id: int = Field(alias="schedule_id") start_date_time: PartialTime @@ -172,10 +169,10 @@ class ImgTheaterFightStaticAvatar(BaseCharacter): class ImgTheaterFightStatic(APIModel): - max_defeat_avatar: Optional[ImgTheaterFightStaticAvatar] - max_damage_avatar: Optional[ImgTheaterFightStaticAvatar] - max_take_damage_avatar: Optional[ImgTheaterFightStaticAvatar] - total_coin_consumed: Optional[ImgTheaterFightStaticAvatar] + max_defeat_avatar: Optional[ImgTheaterFightStaticAvatar] = None + max_damage_avatar: Optional[ImgTheaterFightStaticAvatar] = None + max_take_damage_avatar: Optional[ImgTheaterFightStaticAvatar] = None + total_coin_consumed: Optional[ImgTheaterFightStaticAvatar] = None shortest_avatar_list: typing.Sequence[ImgTheaterFightStaticAvatar] total_use_time: int is_show_battle_stats: bool diff --git a/simnet/models/genshin/chronicle/notes.py b/simnet/models/genshin/chronicle/notes.py index ccbcd79..f90c8c8 100644 --- a/simnet/models/genshin/chronicle/notes.py +++ b/simnet/models/genshin/chronicle/notes.py @@ -1,10 +1,10 @@ -from datetime import timedelta, datetime +from datetime import datetime, timedelta from enum import Enum -from typing import Union, Literal, Tuple, List, Optional, Dict, Any +from typing import Literal, Tuple, List, Optional, Dict, Any -from pydantic import Field, root_validator +from pydantic import model_validator -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field, TimeDeltaField __all__ = [ "TaskRewardStatus", @@ -22,33 +22,6 @@ ] -def _process_timedelta(time: Union[int, timedelta, datetime]) -> datetime: - """A helper function that processes time inputs. - - Args: - time (Union[int, timedelta, datetime]): The time input to process. - - Returns: - datetime: The processed datetime. - - Raises: - ValueError: If the input time is less than January 1, 2000. - """ - if isinstance(time, int): - time = datetime.fromtimestamp(time).astimezone() - - if isinstance(time, timedelta): - time = datetime.now().astimezone() + time - - if time < datetime(2000, 1, 1).astimezone(): - delta = timedelta(seconds=int(time.timestamp())) - time = datetime.now().astimezone() + delta - - time = time.replace(second=0, microsecond=0) - - return time - - class Expedition(APIModel): """The model for a real-time expedition. @@ -60,7 +33,7 @@ class Expedition(APIModel): character: str = Field(alias="avatar_side_icon") status: Literal["Ongoing", "Finished"] - remaining_time: timedelta = Field(alias="remained_time") + remaining_time: TimeDeltaField = Field(alias="remained_time") @property def finished(self) -> bool: @@ -73,7 +46,7 @@ def completion_time(self) -> datetime: return datetime.now().astimezone() + self.remaining_time -class TransformerTimedelta(timedelta): +class TransformerTimedelta(TimeDeltaField): """The model for a transformer recovery time.""" @property @@ -163,7 +136,7 @@ class DailyTask(APIModel): attendance_rewards: List[AttendanceReward] attendance_visible: bool stored_attendance: float - stored_attendance_refresh_countdown: timedelta + stored_attendance_refresh_countdown: TimeDeltaField class ArchonStatusEnum(str, Enum): @@ -239,11 +212,11 @@ class Notes(APIModel): current_resin: int max_resin: int - remaining_resin_recovery_time: timedelta = Field(alias="resin_recovery_time") + remaining_resin_recovery_time: TimeDeltaField = Field(alias="resin_recovery_time") current_realm_currency: int = Field(alias="current_home_coin") max_realm_currency: int = Field(alias="max_home_coin") - remaining_realm_currency_recovery_time: timedelta = Field(alias="home_coin_recovery_time") + remaining_realm_currency_recovery_time: TimeDeltaField = Field(alias="home_coin_recovery_time") completed_commissions: int = Field(alias="finished_task_num") max_commissions: int = Field(alias="total_task_num") @@ -252,7 +225,7 @@ class Notes(APIModel): remaining_resin_discounts: int = Field(alias="remain_resin_discount_num") max_resin_discounts: int = Field(alias="resin_discount_num_limit") - remaining_transformer_recovery_time: Optional[TransformerTimedelta] + remaining_transformer_recovery_time: Optional[TransformerTimedelta] = None expeditions: List[Expedition] max_expeditions: int = Field(alias="max_expedition_num") @@ -279,7 +252,8 @@ def transformer_recovery_time(self) -> Optional[datetime]: remaining = datetime.now().astimezone() + self.remaining_transformer_recovery_time return remaining - @root_validator(pre=True) + @model_validator(mode="before") + @classmethod def flatten_transformer(cls, values: Dict[str, Any]) -> Dict[str, Any]: """A validator that flattens the transformer recovery time. @@ -347,7 +321,7 @@ class NotesWidget(APIModel): current_resin: int max_resin: int - remaining_resin_recovery_time: timedelta = Field(alias="resin_recovery_time") + remaining_resin_recovery_time: TimeDeltaField = Field(alias="resin_recovery_time") current_realm_currency: int = Field(alias="current_home_coin") max_realm_currency: int = Field(alias="max_home_coin") @@ -376,7 +350,7 @@ class NotesOverseaWidgetResin(APIModel): current_val: int max_val: int - remaining_resin_recovery_time: timedelta = Field(alias="recovery_time") + remaining_resin_recovery_time: TimeDeltaField = Field(alias="recovery_time") @property def resin_recovery_time(self) -> datetime: @@ -395,7 +369,7 @@ class NotesOverseaWidgetRealm(APIModel): current_val: int max_val: int - remaining_realm_currency_recovery_time: timedelta = Field(alias="recovery_time") + remaining_realm_currency_recovery_time: TimeDeltaField = Field(alias="recovery_time") @property def realm_currency_recovery_time(self) -> datetime: diff --git a/simnet/models/genshin/chronicle/stats.py b/simnet/models/genshin/chronicle/stats.py index 9358891..4ec34d9 100644 --- a/simnet/models/genshin/chronicle/stats.py +++ b/simnet/models/genshin/chronicle/stats.py @@ -1,9 +1,9 @@ import re from typing import Any, Optional, cast, List, Dict -from pydantic import validator, Field +from pydantic import field_validator, ValidationInfo -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field from simnet.models.genshin.chronicle.abyss import SpiralAbyssPair from simnet.models.genshin.chronicle.characters import PartialCharacter from simnet.models.lab.record import UserInfo @@ -42,26 +42,26 @@ class Stats(APIModel): unlocked_domains (int): Number of domains unlocked by the user. """ - achievements: int = Field(aliases="achievement_number") - days_active: int = Field(aliases="active_day_number") - characters: int = Field(aliases="avatar_number") + achievements: int = Field(alias="achievement_number") + days_active: int = Field(alias="active_day_number") + characters: int = Field(alias="avatar_number") full_fetter_avatar_num: int - spiral_abyss: str = Field(aliases="spiral_abyss") + spiral_abyss: str = Field(alias="spiral_abyss") - anemoculi: int = Field(aliases="anemoculus_number") - geoculi: int = Field(aliases="geoculus_number") - dendroculi: int = Field(aliases="dendroculus_number") - electroculi: int = Field(aliases="electroculus_number") - hydroculi: int = Field(aliases="hydroculus_number") + anemoculi: int = Field(alias="anemoculus_number") + geoculi: int = Field(alias="geoculus_number") + dendroculi: int = Field(alias="dendroculus_number") + electroculi: int = Field(alias="electroculus_number") + hydroculi: int = Field(alias="hydroculus_number") pyroculi: int = Field(alias="pyroculus_number") - common_chests: int = Field(aliases="common_chest_number") - exquisite_chests: int = Field(aliases="exquisite_chest_number") - precious_chests: int = Field(aliases="precious_chest_number") - luxurious_chests: int = Field(aliases="luxurious_chest_number") - remarkable_chests: int = Field(aliases="magic_chest_number") - unlocked_waypoints: int = Field(aliases="way_point_number") - unlocked_domains: int = Field(aliases="domain_number") + common_chests: int = Field(alias="common_chest_number") + exquisite_chests: int = Field(alias="exquisite_chest_number") + precious_chests: int = Field(alias="precious_chest_number") + luxurious_chests: int = Field(alias="luxurious_chest_number") + remarkable_chests: int = Field(alias="magic_chest_number") + unlocked_waypoints: int = Field(alias="way_point_number") + unlocked_domains: int = Field(alias="domain_number") role_combat: StatsRoleCombat @@ -123,19 +123,20 @@ def explored(self) -> float: """ return self.raw_explored / 10 - @validator("offerings", pre=True) - def add_base_offering(cls, offerings: List[Any], values: Dict[str, Any]) -> List[Any]: + @field_validator("offerings", mode="before") + @classmethod + def add_base_offering(cls, offerings: List[Any], info: ValidationInfo) -> List[Any]: """Add a base offering if the exploration type is Reputation. Args: offerings (List[Any]): The list of offerings. - values (Dict[str, Any]): The dict of attribute values. + info (ValidationInfo): The validation info. Returns: The updated list of offerings. """ - 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 @@ -195,13 +196,14 @@ class PartialGenshinUserStats(APIModel): teapot (Optional[Teapot]): The user's Serenitea Teapot. """ - info: UserInfo = Field(aliases="role") + info: UserInfo = Field(alias="role") stats: Stats - characters: List[PartialCharacter] = Field(aliases="avatars") - explorations: List[Exploration] = Field(aliases="world_explorations") - teapot: Optional[Teapot] = Field(aliases="homes") + characters: List[PartialCharacter] = Field(alias="avatars") + explorations: List[Exploration] = Field(alias="world_explorations") + teapot: Optional[Teapot] = Field(None, alias="homes") - @validator("teapot", pre=True) + @field_validator("teapot", mode="before") + @classmethod def format_teapot(cls, v: Any) -> Optional[Dict[str, Any]]: """Format the user's Serenitea Teapot. diff --git a/simnet/models/genshin/diary.py b/simnet/models/genshin/diary.py index 9e58eff..9054f11 100644 --- a/simnet/models/genshin/diary.py +++ b/simnet/models/genshin/diary.py @@ -1,9 +1,7 @@ from datetime import datetime from typing import List, Optional -from pydantic import Field - -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field, DateTimeField from simnet.models.diary import BaseDiary __all__ = ( @@ -49,7 +47,7 @@ class MonthDiaryData(APIModel): current_mora: int last_primogems: int last_mora: int - primogems_rate: int = Field(aliases="primogem_rate") + primogems_rate: int = Field(alias="primogem_rate") mora_rate: int categories: List[DiaryActionCategory] = Field(alias="group_by") @@ -105,7 +103,7 @@ class DiaryAction(APIModel): action_id: int action: str - time: datetime = Field(timezone=8) + time: DateTimeField amount: int = Field(alias="num") diff --git a/simnet/models/genshin/transaction.py b/simnet/models/genshin/transaction.py index 9c74e21..2f29512 100644 --- a/simnet/models/genshin/transaction.py +++ b/simnet/models/genshin/transaction.py @@ -1,10 +1,7 @@ -from datetime import datetime from enum import Enum from typing import Literal -from pydantic import Field - -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field, DateTimeField __all__ = ("BaseTransaction", "ItemTransaction", "Transaction", "TransactionKind") @@ -33,7 +30,7 @@ class BaseTransaction(APIModel): kind: TransactionKind id: int - time: datetime = Field(alias="datetime") + time: DateTimeField = Field(alias="datetime") amount: int = Field(alias="add_num") reason: str diff --git a/simnet/models/genshin/wish.py b/simnet/models/genshin/wish.py index 148b1c9..845d51d 100644 --- a/simnet/models/genshin/wish.py +++ b/simnet/models/genshin/wish.py @@ -3,9 +3,9 @@ from enum import IntEnum from typing import Any, Optional, List -from pydantic import Field, validator +from pydantic import field_validator -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field class BannerType(IntEnum): @@ -73,7 +73,8 @@ class Wish(APIModel): banner_type: BannerType = Field(alias="gacha_type") banner_name: str - @validator("banner_type", pre=True) + @field_validator("banner_type", mode="before") + @classmethod def cast_banner_type(cls, v: Any) -> int: """Converts the banner type to an integer. @@ -120,7 +121,8 @@ class BannerDetailsUpItem(APIModel): element: str = Field(alias="item_attr") icon: str = Field(alias="item_img") - @validator("element", pre=True) + @field_validator("element", mode="before") + @classmethod def parse_element(cls, v: str) -> str: """Converts the element string to a standardized format. @@ -181,14 +183,14 @@ class BannerDetails(APIModel): content: str date_range: str - r5_up_prob: Optional[float] - r4_up_prob: Optional[float] - r5_prob: Optional[float] - r4_prob: Optional[float] - r3_prob: Optional[float] - r5_guarantee_prob: Optional[float] = Field(alias="r5_baodi_prob") - r4_guarantee_prob: Optional[float] = Field(alias="r4_baodi_prob") - r3_guarantee_prob: Optional[float] = Field(alias="r3_baodi_prob") + r5_up_prob: Optional[float] = None + r4_up_prob: Optional[float] = None + r5_prob: Optional[float] = None + r4_prob: Optional[float] = None + r3_prob: Optional[float] = None + r5_guarantee_prob: Optional[float] = Field(None, alias="r5_baodi_prob") + r4_guarantee_prob: Optional[float] = Field(None, alias="r4_baodi_prob") + r3_guarantee_prob: Optional[float] = Field(None, alias="r3_baodi_prob") r5_up_items: List[BannerDetailsUpItem] r4_up_items: List[BannerDetailsUpItem] @@ -197,7 +199,8 @@ class BannerDetails(APIModel): r4_items: List[BannerDetailItem] = Field(alias="r4_prob_list") r3_items: List[BannerDetailItem] = Field(alias="r3_prob_list") - @validator("r5_up_items", "r4_up_items", pre=True) + @field_validator("r5_up_items", "r4_up_items", mode="before") + @classmethod def replace_none(cls, v: Optional[List[Any]]) -> List[Any]: """Replaces NoneType attributes with an empty list. @@ -210,7 +213,7 @@ def replace_none(cls, v: Optional[List[Any]]) -> List[Any]: """ return v or [] - @validator( + @field_validator( "r5_up_prob", "r4_up_prob", "r5_prob", @@ -219,8 +222,9 @@ def replace_none(cls, v: Optional[List[Any]]) -> List[Any]: "r5_guarantee_prob", "r4_guarantee_prob", "r3_guarantee_prob", - pre=True, + mode="before", ) + @classmethod def parse_percentage(cls, v: Optional[str]) -> Optional[float]: """Parses percentage strings into float values. @@ -290,7 +294,8 @@ class GachaItem(APIModel): rarity: int = Field(alias="rank_type") id: int = Field(alias="item_id") - @validator("id") + @field_validator("id") + @classmethod def format_id(cls, v: int) -> int: """Formats the `id` attribute to a standardized 8-digit format. diff --git a/simnet/models/lab/announcement.py b/simnet/models/lab/announcement.py index 4cf8583..2517999 100644 --- a/simnet/models/lab/announcement.py +++ b/simnet/models/lab/announcement.py @@ -1,8 +1,6 @@ from datetime import datetime -from pydantic import Field - -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field __all__ = ("Announcement",) diff --git a/simnet/models/lab/daily.py b/simnet/models/lab/daily.py index 26b4688..e7d09aa 100644 --- a/simnet/models/lab/daily.py +++ b/simnet/models/lab/daily.py @@ -1,9 +1,7 @@ -from datetime import timezone, timedelta, datetime +import datetime from typing import NamedTuple -from pydantic import Field - -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field, DateTimeField, CN_TIMEZONE __all__ = ("ClaimedDailyReward", "DailyReward", "DailyRewardInfo") @@ -22,8 +20,7 @@ class DailyRewardInfo(NamedTuple): @property def missed_rewards(self) -> int: """The number of rewards that the user has missed since the start of the month.""" - cn_timezone = timezone(timedelta(hours=8)) - now = datetime.now(cn_timezone) + now = datetime.datetime.now(CN_TIMEZONE) return now.day - self.claimed_rewards @@ -56,4 +53,4 @@ class ClaimedDailyReward(APIModel): name: str amount: int = Field(alias="cnt") icon: str = Field(alias="img") - time: datetime = Field(alias="created_at", tzinfo=timezone(timedelta(hours=8))) + time: DateTimeField = Field(alias="created_at") diff --git a/simnet/models/lab/record.py b/simnet/models/lab/record.py index a02f660..539dd68 100644 --- a/simnet/models/lab/record.py +++ b/simnet/models/lab/record.py @@ -2,9 +2,9 @@ import re from typing import Optional, Any, Dict, List, Union, Type -from pydantic import Field, validator +from pydantic import field_validator -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field from simnet.utils.enums import Game __all__ = ( @@ -141,12 +141,13 @@ class PartialUser(APIModel): accident_id: int = Field(alias="uid") nickname: str introduction: str = Field(alias="introduce") - avatar_id: Optional[str] = Field(alias="avatar") + avatar_id: Optional[str] = Field(None, alias="avatar") avatar_url: Optional[str] = "" gender: Gender icon: str = Field(alias="avatar_url") - @validator("nickname") + @field_validator("nickname") + @classmethod def remove_highlight(cls, v: str) -> str: """Removes the highlight tags from the user's nickname. diff --git a/simnet/models/starrail/calculator.py b/simnet/models/starrail/calculator.py index cd5c56e..f07a8ab 100644 --- a/simnet/models/starrail/calculator.py +++ b/simnet/models/starrail/calculator.py @@ -2,9 +2,9 @@ from typing import Dict, Any, Optional, List -from pydantic import Field, validator +from pydantic import field_validator -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field from simnet.models.starrail.character import StarRailBaseCharacter, StarRailElement, StarRailDestiny __all__ = ( @@ -62,7 +62,8 @@ class StarrailCalculatorCharacter(StarRailBaseCharacter): target_level: int is_forward: bool - @validator("element", pre=True) + @field_validator("element", mode="before") + @classmethod def parse_element(cls, v: Any) -> str: """Parse the element of a character. @@ -76,7 +77,8 @@ def parse_element(cls, v: Any) -> str: return CALCULATOR_ELEMENTS[int(v)].value return v - @validator("path", pre=True) + @field_validator("path", mode="before") + @classmethod def parse_path(cls, v: Any) -> str: """Parse the path type of character. @@ -117,7 +119,8 @@ class StarrailCalculatorWeapon(APIModel): target_level: int is_forward: bool - @validator("path", pre=True) + @field_validator("path", mode="before") + @classmethod def parse_path(cls, v: Any) -> str: """Parse the path type of weapon. diff --git a/simnet/models/starrail/chronicle/act_calendar.py b/simnet/models/starrail/chronicle/act_calendar.py index 1c8e1de..8b52a29 100644 --- a/simnet/models/starrail/chronicle/act_calendar.py +++ b/simnet/models/starrail/chronicle/act_calendar.py @@ -1,8 +1,7 @@ -import datetime from enum import Enum from typing import List, Optional -from simnet.models.base import APIModel +from simnet.models.base import APIModel, DateTimeField class EquipListItem(APIModel): @@ -29,11 +28,11 @@ class AvatarListItem(EquipListItem): class TimeInfo(APIModel): """A model representing time information in the StarRail act calendar.""" - start_ts: datetime.datetime - end_ts: datetime.datetime + start_ts: DateTimeField + end_ts: DateTimeField start_time: str end_time: str - now: datetime.datetime + now: DateTimeField class CardPoolTypeEnum(str, Enum): @@ -171,5 +170,5 @@ class StarRailActCalendar(APIModel): equip_card_pool_list: List[CardPoolListItem] act_list: List[ActListItem] challenge_list: List[ChallengeListItem] - now: datetime.datetime + now: DateTimeField cur_game_version: str diff --git a/simnet/models/starrail/chronicle/challenge.py b/simnet/models/starrail/chronicle/challenge.py index 6b8cdfa..2b2e54a 100644 --- a/simnet/models/starrail/chronicle/challenge.py +++ b/simnet/models/starrail/chronicle/challenge.py @@ -2,9 +2,7 @@ from typing import List -from pydantic import Field - -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field from simnet.models.starrail.character import RogueCharacter from .base import PartialTime diff --git a/simnet/models/starrail/chronicle/challenge_boss.py b/simnet/models/starrail/chronicle/challenge_boss.py index 30a6ecd..e381655 100644 --- a/simnet/models/starrail/chronicle/challenge_boss.py +++ b/simnet/models/starrail/chronicle/challenge_boss.py @@ -2,9 +2,7 @@ from typing import List, Optional -from pydantic import Field - -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field from simnet.models.starrail.character import RogueCharacter from .base import PartialTime diff --git a/simnet/models/starrail/chronicle/challenge_story.py b/simnet/models/starrail/chronicle/challenge_story.py index ce4cae7..2053237 100644 --- a/simnet/models/starrail/chronicle/challenge_story.py +++ b/simnet/models/starrail/chronicle/challenge_story.py @@ -2,9 +2,7 @@ from typing import List, Optional -from pydantic import Field - -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field from simnet.models.starrail.character import RogueCharacter from .base import PartialTime diff --git a/simnet/models/starrail/chronicle/characters.py b/simnet/models/starrail/chronicle/characters.py index dd5b74d..6035b1c 100644 --- a/simnet/models/starrail/chronicle/characters.py +++ b/simnet/models/starrail/chronicle/characters.py @@ -2,7 +2,7 @@ from typing import List, Optional, Any, Dict -from pydantic import validator +from pydantic import field_validator from simnet.models.base import APIModel @@ -114,7 +114,7 @@ class StarRailDetailCharacter(character.StarRailPartialCharacter): """StarRail character with equipment and relics.""" image: str - equip: Optional[StarRailEquipment] + equip: Optional[StarRailEquipment] = None relics: List[Relic] ornaments: List[Relic] ranks: List[Rank] @@ -218,17 +218,20 @@ def _parse(v: Dict[str, Any], key: str = None, value_key: str = None) -> List[Di new_list.append(v_) return new_list - @validator("equip_wiki", pre=True) + @field_validator("equip_wiki", mode="before") + @classmethod def parse_equip_wiki(cls, v: Dict[str, str]) -> List[Dict[str, str]]: """Parse equip wiki.""" return cls._parse(v, "id", "url") - @validator("property_info", pre=True) + @field_validator("property_info", mode="before") + @classmethod def parse_property_info(cls, v: Dict[str, Any]) -> List[Dict[str, Any]]: """Parse property info.""" return cls._parse(v) - @validator("recommend_property", pre=True) + @field_validator("recommend_property", mode="before") + @classmethod def parse_recommend_property(cls, v: Dict[str, Any]) -> List[Dict[str, Any]]: """Parse recommend property.""" return cls._parse(v, "id") diff --git a/simnet/models/starrail/chronicle/notes.py b/simnet/models/starrail/chronicle/notes.py index 68473f3..a7f75c3 100644 --- a/simnet/models/starrail/chronicle/notes.py +++ b/simnet/models/starrail/chronicle/notes.py @@ -1,9 +1,7 @@ -from datetime import timedelta, datetime +from datetime import datetime, timedelta from typing import List, Literal, Sequence -from pydantic import Field - -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field, TimeDeltaField class StarRailExpedition(APIModel): @@ -19,7 +17,7 @@ class StarRailExpedition(APIModel): avatars: List[str] status: Literal["Ongoing", "Finished"] - remaining_time: timedelta + remaining_time: TimeDeltaField name: str @property @@ -55,7 +53,7 @@ class StarRailNote(APIModel): current_stamina: int max_stamina: int - stamina_recover_time: timedelta + stamina_recover_time: TimeDeltaField accepted_epedition_num: int total_expedition_num: int expeditions: Sequence[StarRailExpedition] @@ -109,7 +107,7 @@ class StarRailNoteWidget(APIModel): current_stamina: int max_stamina: int - stamina_recover_time: timedelta + stamina_recover_time: TimeDeltaField current_reserve_stamina: int is_reserve_stamina_full: bool accepted_expedition_num: int @@ -198,7 +196,7 @@ class StarRailNoteOverseaWidget(APIModel): current_stamina: int max_stamina: int - stamina_recover_time: timedelta + stamina_recover_time: TimeDeltaField accepted_expedition_num: int total_expedition_num: int expeditions: Sequence[StarRailExpedition] diff --git a/simnet/models/starrail/chronicle/rogue_tourn.py b/simnet/models/starrail/chronicle/rogue_tourn.py index 17249e3..355f115 100644 --- a/simnet/models/starrail/chronicle/rogue_tourn.py +++ b/simnet/models/starrail/chronicle/rogue_tourn.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List from simnet.models.base import APIModel from simnet.models.starrail.character import RogueCharacter diff --git a/simnet/models/starrail/chronicle/stats.py b/simnet/models/starrail/chronicle/stats.py index 448f1e2..d77b301 100644 --- a/simnet/models/starrail/chronicle/stats.py +++ b/simnet/models/starrail/chronicle/stats.py @@ -2,9 +2,7 @@ import typing -from pydantic import Field - -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field from .. import character diff --git a/simnet/models/starrail/diary.py b/simnet/models/starrail/diary.py index 8c8b16b..f861dfd 100644 --- a/simnet/models/starrail/diary.py +++ b/simnet/models/starrail/diary.py @@ -1,8 +1,6 @@ from typing import List -from pydantic import Field - -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field from simnet.models.diary import BaseDiary from simnet.models.starrail.chronicle.base import PartialTime diff --git a/simnet/models/starrail/wish.py b/simnet/models/starrail/wish.py index 4b9a0d4..e0e7330 100644 --- a/simnet/models/starrail/wish.py +++ b/simnet/models/starrail/wish.py @@ -2,9 +2,9 @@ from enum import IntEnum from typing import Any -from pydantic import Field, validator +from pydantic import field_validator -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field class StarRailBannerType(IntEnum): @@ -53,7 +53,8 @@ class StarRailWish(APIModel): banner_type: StarRailBannerType = Field(alias="gacha_type") """Type of the banner the wish was made on.""" - @validator("banner_type", pre=True) + @field_validator("banner_type", mode="before") + @classmethod def cast_banner_type(cls, v: Any) -> int: """Converts the banner type from any type to int.""" return int(v) diff --git a/simnet/models/zzz/calculator.py b/simnet/models/zzz/calculator.py index 485548c..3db8e00 100644 --- a/simnet/models/zzz/calculator.py +++ b/simnet/models/zzz/calculator.py @@ -1,9 +1,7 @@ import re from typing import List, Optional, Dict -from pydantic import Field - -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field from simnet.models.zzz.character import ZZZPartialCharacter @@ -99,7 +97,7 @@ class ZZZCalculatorEquipment(APIModel): class ZZZCalculatorCharacter(ZZZPartialCharacter): equip: List[ZZZCalculatorEquipment] - weapon: Optional[ZZZCalculatorWeapon] + weapon: Optional[ZZZCalculatorWeapon] = None properties: List[ZZZCalculatorAvatarProperty] skills: List[ZZZCalculatorAvatarSkill] ranks: List[ZZZCalculatorAvatarRank] diff --git a/simnet/models/zzz/character.py b/simnet/models/zzz/character.py index 80cd64c..000b3ee 100644 --- a/simnet/models/zzz/character.py +++ b/simnet/models/zzz/character.py @@ -1,8 +1,6 @@ """Starrail base character model.""" -from pydantic import Field - -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field class ZZZBaseCharacter(APIModel): diff --git a/simnet/models/zzz/chronicle/abyss_abstract.py b/simnet/models/zzz/chronicle/abyss_abstract.py index c111c74..8f48aea 100644 --- a/simnet/models/zzz/chronicle/abyss_abstract.py +++ b/simnet/models/zzz/chronicle/abyss_abstract.py @@ -1,7 +1,6 @@ -import datetime from typing import List, Optional -from simnet.models.base import APIModel +from simnet.models.base import APIModel, TimeDeltaField class ZZZAbyssLevel(APIModel): @@ -116,7 +115,7 @@ class ZZZAbyssAbstract(APIModel): abyss_point: Optional[ZZZAbyssPoint] = None abyss_duty: Optional[ZZZAbyssDuty] = None abyss_talent: Optional[ZZZAbyssTalent] = None - refresh_time: datetime.timedelta + refresh_time: TimeDeltaField abyss_collect: List[ZZZAbyssCollectItem] abyss_nest: Optional[ZZZAbyssNest] = None abyss_throne: Optional[ZZZAbyssThrone] = None diff --git a/simnet/models/zzz/chronicle/challenge.py b/simnet/models/zzz/chronicle/challenge.py index deaec95..65b18dd 100644 --- a/simnet/models/zzz/chronicle/challenge.py +++ b/simnet/models/zzz/chronicle/challenge.py @@ -1,8 +1,6 @@ from typing import List, Optional -from pydantic import Field - -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field from simnet.models.starrail.chronicle.base import PartialTime from simnet.models.zzz.character import ZZZBaseBuddy, ZZZBaseCharacter @@ -39,7 +37,7 @@ class ZZZFloorNode(APIModel): """A node""" avatars: List[ZZZChallengeCharacter] - buddy: Optional[ZZZBaseBuddy] + buddy: Optional[ZZZBaseBuddy] = None element_type_list: List[int] monster_info: ZZZFloorMonster @@ -69,8 +67,8 @@ class ZZZChallenge(APIModel): """Challenge in a season.""" season: int = Field(alias="schedule_id") - begin_time: Optional[PartialTime] = Field(alias="hadal_begin_time") - end_time: Optional[PartialTime] = Field(alias="hadal_end_time") + begin_time: Optional[PartialTime] = Field(None, alias="hadal_begin_time") + end_time: Optional[PartialTime] = Field(None, alias="hadal_end_time") fast_layer_time: int max_layer: int diff --git a/simnet/models/zzz/chronicle/notes.py b/simnet/models/zzz/chronicle/notes.py index 7101960..2ad90a9 100644 --- a/simnet/models/zzz/chronicle/notes.py +++ b/simnet/models/zzz/chronicle/notes.py @@ -2,7 +2,7 @@ import enum from typing import Optional -from simnet.models.base import APIModel +from simnet.models.base import APIModel, TimeDeltaField class ZZZNoteProgress(APIModel): @@ -28,7 +28,7 @@ class ZZZNoteEnergy(APIModel): """ progress: ZZZNoteProgress - restore: datetime.timedelta + restore: TimeDeltaField class ZZZNoteVitality(APIModel): @@ -114,7 +114,7 @@ class ZZZNoteWeeklyTask(APIModel): max_point: int cur_point: int - refresh_time: datetime.timedelta + refresh_time: TimeDeltaField class ZZZNote(APIModel): @@ -136,7 +136,7 @@ class ZZZNote(APIModel): bounty_commission: Optional[ZZZNoteBountyCommission] = None survey_points: Optional[ZZZNoteSurveyPoints] = None weekly_task: Optional[ZZZNoteWeeklyTask] = None - abyss_refresh: datetime.timedelta + abyss_refresh: TimeDeltaField @property def current_stamina(self) -> int: diff --git a/simnet/models/zzz/chronicle/stats.py b/simnet/models/zzz/chronicle/stats.py index facee05..71e57ca 100644 --- a/simnet/models/zzz/chronicle/stats.py +++ b/simnet/models/zzz/chronicle/stats.py @@ -2,9 +2,7 @@ import typing -from pydantic import Field - -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field from simnet.models.zzz import character diff --git a/simnet/models/zzz/diary.py b/simnet/models/zzz/diary.py index 403641e..5f45d0e 100644 --- a/simnet/models/zzz/diary.py +++ b/simnet/models/zzz/diary.py @@ -1,8 +1,6 @@ from typing import List -from pydantic import Field - -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field from simnet.models.diary import BaseDiary __all__ = ( diff --git a/simnet/models/zzz/self_help.py b/simnet/models/zzz/self_help.py index cef4ed0..b469070 100644 --- a/simnet/models/zzz/self_help.py +++ b/simnet/models/zzz/self_help.py @@ -1,10 +1,7 @@ -from datetime import datetime from enum import Enum from typing import Optional -from pydantic import Field - -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field, DateTimeField class ZZZSelfHelpActionLogReason(str, Enum): @@ -34,7 +31,7 @@ class ZZZSelfHelpActionLog(APIModel): id: int uid: int - time: datetime = Field(alias="datetime") + time: DateTimeField = Field(alias="datetime") reason: ZZZSelfHelpActionLogReason = Field(alias="action_name") client_ip: Optional[str] = "" diff --git a/simnet/models/zzz/wish.py b/simnet/models/zzz/wish.py index 8703091..c6fc050 100644 --- a/simnet/models/zzz/wish.py +++ b/simnet/models/zzz/wish.py @@ -2,9 +2,9 @@ from enum import IntEnum from typing import Any -from pydantic import Field, validator +from pydantic import field_validator -from simnet.models.base import APIModel +from simnet.models.base import APIModel, Field class ZZZBannerType(IntEnum): @@ -53,12 +53,14 @@ class ZZZWish(APIModel): banner_type: ZZZBannerType = Field(alias="gacha_type") """Type of the banner the wish was made on.""" - @validator("banner_type", pre=True) + @field_validator("banner_type", mode="before") + @classmethod def cast_banner_type(cls, v: Any) -> int: """Converts the banner type from any type to int.""" return int(v) - @validator("rarity") + @field_validator("rarity") + @classmethod def add_rarity(cls, v: int) -> int: """Add rarity 1.""" return v + 1