Skip to content

Commit

Permalink
⬆️ upgrade Pydantic to V2 and drop python 3.8
Browse files Browse the repository at this point in the history
  • Loading branch information
omg-xtao authored Nov 30, 2024
1 parent 461e7eb commit df0a451
Show file tree
Hide file tree
Showing 37 changed files with 278 additions and 253 deletions.
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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']
target-version = ['py39', 'py310', 'py311', 'py312', 'py313']
107 changes: 91 additions & 16 deletions simnet/models/base.py
Original file line number Diff line number Diff line change
@@ -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),
]
4 changes: 1 addition & 3 deletions simnet/models/diary.py
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
22 changes: 13 additions & 9 deletions simnet/models/genshin/calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = (
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Expand All @@ -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.
Expand All @@ -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)

Expand Down
19 changes: 8 additions & 11 deletions simnet/models/genshin/chronicle/abyss.py
Original file line number Diff line number Diff line change
@@ -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__ = (
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -90,7 +86,7 @@ class Battle(APIModel):
"""

half: int = Field(alias="index")
timestamp: datetime.datetime
timestamp: DateTimeField
characters: List[AbyssCharacter] = Field(alias="avatars")


Expand Down Expand Up @@ -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")
Expand All @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions simnet/models/genshin/chronicle/act_calendar.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand Down
9 changes: 4 additions & 5 deletions simnet/models/genshin/chronicle/character_detail.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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."""
Expand Down
7 changes: 4 additions & 3 deletions simnet/models/genshin/chronicle/characters.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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.
Expand Down
Loading

0 comments on commit df0a451

Please sign in to comment.