Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate the Library to Pydantic V2 and Drop Support for Python 3.8 #225

Merged
merged 22 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions genshin/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,7 @@ def client_command(func: typing.Callable[..., typing.Awaitable[typing.Any]]) ->
@functools.wraps(func)
@asynchronous
async def command(
cookies: typing.Optional[str] = None,
lang: str = "en-us",
debug: bool = False,
**kwargs: typing.Any,
cookies: typing.Optional[str] = None, lang: str = "en-us", debug: bool = False, **kwargs: typing.Any
) -> typing.Any:
client = genshin.Client(cookies, lang=lang, debug=debug)
if cookies is None:
Expand Down Expand Up @@ -85,7 +82,7 @@ async def honkai_stats(client: genshin.Client, uid: int) -> None:
data = await client.get_honkai_user(uid)

click.secho("Stats:", fg="yellow")
for k, v in data.stats.as_dict(lang=client.lang).items():
for k, v in data.stats.dict().items():
if isinstance(v, dict):
click.echo(f"{k}:")
for nested_k, nested_v in typing.cast("typing.Dict[str, object]", v).items():
Expand All @@ -105,7 +102,7 @@ async def genshin_stats(client: genshin.Client, uid: int) -> None:
data = await client.get_partial_genshin_user(uid)

click.secho("Stats:", fg="yellow")
for k, v in data.stats.as_dict(lang=client.lang).items():
for k, v in data.stats.dict().items():
value = click.style(str(v), bold=True)
click.echo(f"{k}: {value}")

Expand Down Expand Up @@ -178,8 +175,7 @@ async def genshin_notes(client: genshin.Client, uid: typing.Optional[int]) -> No
click.echo(f"{click.style('Resin:', bold=True)} {data.current_resin}/{data.max_resin}")
click.echo(f"{click.style('Realm currency:', bold=True)} {data.current_realm_currency}/{data.max_realm_currency}")
click.echo(
f"{click.style('Commissions:', bold=True)} " f"{data.completed_commissions}/{data.max_commissions}",
nl=False,
f"{click.style('Commissions:', bold=True)} " f"{data.completed_commissions}/{data.max_commissions}", nl=False
)
if data.completed_commissions == data.max_commissions and not data.claimed_commission_reward:
click.echo(f" | [{click.style('X', fg='red')}] Haven't claimed rewards")
Expand Down
2 changes: 1 addition & 1 deletion genshin/client/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import aiosqlite


__all__ = ["BaseCache", "Cache", "RedisCache", "StaticCache", "SQLiteCache"]
__all__ = ["BaseCache", "Cache", "RedisCache", "SQLiteCache", "StaticCache"]

MINUTE = 60
HOUR = MINUTE * 60
Expand Down
61 changes: 5 additions & 56 deletions genshin/client/components/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Base ABC Client."""

import abc
import asyncio
import base64
import functools
import json
Expand All @@ -20,7 +19,6 @@
from genshin.client import routes
from genshin.client.manager import managers
from genshin.models import hoyolab as hoyolab_models
from genshin.models import model as base_model
from genshin.utility import concurrency, deprecation, ds

__all__ = ["BaseClient"]
Expand Down Expand Up @@ -289,22 +287,13 @@ def set_authkey(self, authkey: typing.Optional[str] = None, *, game: typing.Opti
self.authkeys[game] = authkey

def set_cache(
self,
maxsize: int = 1024,
*,
ttl: int = client_cache.HOUR,
static_ttl: int = client_cache.DAY,
self, maxsize: int = 1024, *, ttl: int = client_cache.HOUR, static_ttl: int = client_cache.DAY
) -> None:
"""Create and set a new cache."""
self.cache = client_cache.Cache(maxsize, ttl=ttl, static_ttl=static_ttl)

def set_redis_cache(
self,
url: str,
*,
ttl: int = client_cache.HOUR,
static_ttl: int = client_cache.DAY,
**redis_kwargs: typing.Any,
self, url: str, *, ttl: int = client_cache.HOUR, static_ttl: int = client_cache.DAY, **redis_kwargs: typing.Any
) -> None:
"""Create and set a new redis cache."""
import aioredis
Expand Down Expand Up @@ -384,12 +373,7 @@ async def request(
await self._request_hook(method, url, params=params, data=data, headers=headers, **kwargs)

response = await self.cookie_manager.request(
url,
method=method,
params=params,
json=data,
headers=headers,
**kwargs,
url, method=method, params=params, json=data, headers=headers, **kwargs
)

# cache
Expand Down Expand Up @@ -491,9 +475,7 @@ async def request_hoyolab(

@managers.no_multi
async def get_game_accounts(
self,
*,
lang: typing.Optional[str] = None,
self, *, lang: typing.Optional[str] = None
) -> typing.Sequence[hoyolab_models.GenshinAccount]:
"""Get the game accounts of the currently logged-in user."""
if self.hoyolab_id is None:
Expand All @@ -508,9 +490,7 @@ async def get_game_accounts(

@deprecation.deprecated("get_game_accounts")
async def genshin_accounts(
self,
*,
lang: typing.Optional[str] = None,
self, *, lang: typing.Optional[str] = None
) -> typing.Sequence[hoyolab_models.GenshinAccount]:
"""Get the genshin accounts of the currently logged-in user."""
accounts = await self.get_game_accounts(lang=lang)
Expand Down Expand Up @@ -592,37 +572,6 @@ def _get_hoyolab_id(self) -> int:

raise RuntimeError("No default hoyolab ID provided.")

async def _fetch_mi18n(self, key: str, lang: str, *, force: bool = False) -> None:
"""Update mi18n for a single url."""
if not force:
if key in base_model.APIModel._mi18n:
return

base_model.APIModel._mi18n[key] = {}

url = routes.MI18N[key]
cache_key = client_cache.cache_key("mi18n", mi18n=key, lang=lang)

data = await self.request_webstatic(url.format(lang=lang), cache=cache_key)
for k, v in data.items():
actual_key = str.lower(key + "/" + k)
base_model.APIModel._mi18n.setdefault(actual_key, {})[lang] = v

async def update_mi18n(self, langs: typing.Iterable[str] = constants.LANGS, *, force: bool = False) -> None:
"""Fetch mi18n for partially localized endpoints."""
if not force:
if base_model.APIModel._mi18n:
return

langs = tuple(langs)

coros: typing.List[typing.Awaitable[None]] = []
for key in routes.MI18N:
for lang in langs:
coros.append(self._fetch_mi18n(key, lang, force=force))

await asyncio.gather(*coros)


def region_specific(region: types.Region) -> typing.Callable[[AsyncCallableT], AsyncCallableT]:
"""Prevent function to be ran with unsupported regions."""
Expand Down
17 changes: 3 additions & 14 deletions genshin/client/components/chronicle/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,10 @@ async def request_game_record(

url = base_url / endpoint

mi18n_task = asyncio.create_task(self._fetch_mi18n("bbs", lang=lang or self.lang))
update_task = asyncio.create_task(utility.update_characters_any(lang or self.lang, lenient=True))

data = await self.request_hoyolab(url, lang=lang, region=region, **kwargs)

await mi18n_task
try:
await update_task
except Exception as e:
Expand All @@ -72,21 +70,15 @@ async def request_game_record(
return data

async def get_record_cards(
self,
hoyolab_id: typing.Optional[int] = None,
*,
lang: typing.Optional[str] = None,
self, hoyolab_id: typing.Optional[int] = None, *, lang: typing.Optional[str] = None
) -> typing.List[models.hoyolab.RecordCard]:
"""Get a user's record cards."""
hoyolab_id = hoyolab_id or self._get_hoyolab_id()

cache_key = cache.cache_key("records", hoyolab_id=hoyolab_id, lang=lang or self.lang)
if not (data := await self.cache.get(cache_key)):
data = await self.request_game_record(
"getGameRecordCard",
lang=lang,
params=dict(uid=hoyolab_id),
is_card_wapi=True,
"getGameRecordCard", lang=lang, params=dict(uid=hoyolab_id), is_card_wapi=True
)

if data["list"]:
Expand All @@ -98,10 +90,7 @@ async def get_record_cards(

@deprecation.deprecated("get_record_cards")
async def get_record_card(
self,
hoyolab_id: typing.Optional[int] = None,
*,
lang: typing.Optional[str] = None,
self, hoyolab_id: typing.Optional[int] = None, *, lang: typing.Optional[str] = None
) -> models.hoyolab.RecordCard:
"""Get a user's record card."""
cards = await self.get_record_cards(hoyolab_id, lang=lang)
Expand Down
2 changes: 1 addition & 1 deletion genshin/client/components/hoyolab.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ async def redeem_code(
game_biz=utility.get_prod_game_biz(self.region, game),
lang=utility.create_short_lang_code(lang or self.lang),
),
method="POST" if game is types.Game.STARRAIL else "GET"
method="POST" if game is types.Game.STARRAIL else "GET",
)

@managers.no_multi
Expand Down
5 changes: 0 additions & 5 deletions genshin/client/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
"HKRPG_URL",
"INFO_LEDGER_URL",
"LINEUP_URL",
"MI18N",
"NAP_URL",
"RECORD_URL",
"REWARD_URL",
Expand Down Expand Up @@ -244,10 +243,6 @@ def get_url(self, region: types.Region, game: types.Game) -> yarl.URL:
chinese="https://hk4e-api.mihoyo.com/common/hk4e_self_help_query/User/",
)

MI18N = dict(
bbs="https://fastcdn.hoyoverse.com/mi18n/bbs_oversea/m11241040191111/m11241040191111-{lang}.json",
inquiry="https://mi18n-os.hoyoverse.com/webstatic/admin/mi18n/hk4e_global/m02251421001311/m02251421001311-{lang}.json",
)

COOKIE_V2_REFRESH_URL = Route("https://sg-public-api.hoyoverse.com/account/ma-passport/token/getBySToken")
GET_COOKIE_TOKEN_BY_GAME_TOKEN_URL = Route("https://api-takumi.mihoyo.com/auth/api/getCookieAccountInfoByGameToken")
Expand Down
18 changes: 4 additions & 14 deletions genshin/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,7 @@ class GenshinException(Exception):
original: str = ""
msg: str = ""

def __init__(
self,
response: typing.Mapping[str, typing.Any] = {},
msg: typing.Optional[str] = None,
) -> None:
def __init__(self, response: typing.Mapping[str, typing.Any] = {}, msg: typing.Optional[str] = None) -> None:
self.retcode = response.get("retcode", self.retcode)
self.original = response.get("message", "")
self.msg = msg or self.msg or self.original
Expand Down Expand Up @@ -249,10 +245,7 @@ class VerificationCodeRateLimited(GenshinException):
# database game record
10101: TooManyRequests,
10102: DataNotPublic,
10103: (
InvalidCookies,
"Cookies are valid but do not have a hoyolab account bound to them.",
),
10103: (InvalidCookies, "Cookies are valid but do not have a hoyolab account bound to them."),
10104: "Cannot view real-time notes of other users.",
# calculator
-500001: "Invalid fields in calculation.",
Expand All @@ -273,10 +266,7 @@ class VerificationCodeRateLimited(GenshinException):
-2016: RedemptionCooldown,
-2017: RedemptionClaimed,
-2018: RedemptionClaimed,
-2021: (
RedemptionException,
"Cannot claim codes for accounts with adventure rank lower than 10.",
),
-2021: (RedemptionException, "Cannot claim codes for accounts with adventure rank lower than 10."),
# rewards
-5003: AlreadyClaimed,
# chinese
Expand All @@ -297,7 +287,7 @@ class VerificationCodeRateLimited(GenshinException):
}

ERRORS: typing.Dict[int, typing.Tuple[_TGE, typing.Optional[str]]] = {
retcode: ((exc, None) if isinstance(exc, type) else (GenshinException, exc) if isinstance(exc, str) else exc)
retcode: (GenshinException, exc) if isinstance(exc, str) else exc if isinstance(exc, tuple) else (exc, None)
for retcode, exc in _errors.items()
}

Expand Down
22 changes: 6 additions & 16 deletions genshin/models/genshin/chronicle/abyss.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,12 @@ class AbyssCharacter(character.BaseCharacter):
class CharacterRanks(APIModel):
"""Collection of rankings achieved during spiral abyss runs."""

# fmt: off
most_played: typing.Sequence[AbyssRankCharacter] = Aliased("reveal_rank", default=[], mi18n="bbs/go_fight_count")
most_kills: typing.Sequence[AbyssRankCharacter] = Aliased("defeat_rank", default=[], mi18n="bbs/max_rout_count")
strongest_strike: typing.Sequence[AbyssRankCharacter] = Aliased("damage_rank", default=[], mi18n="bbs/powerful_attack")
most_damage_taken: typing.Sequence[AbyssRankCharacter] = Aliased("take_damage_rank", default=[], mi18n="bbs/receive_max_damage") # noqa: E501
most_bursts_used: typing.Sequence[AbyssRankCharacter] = Aliased("energy_skill_rank", default=[], mi18n="bbs/element_break_count") # noqa: E501
most_skills_used: typing.Sequence[AbyssRankCharacter] = Aliased("normal_skill_rank", default=[], mi18n="bbs/element_skill_use_count") # noqa: E501
# fmt: on

def as_dict(self, lang: str = "en-us") -> typing.Mapping[str, typing.Any]:
"""Turn fields into properly named ones."""
return {
self._get_mi18n(field, lang): getattr(self, field.name)
for field in self.__fields__.values()
if field.name != "lang"
}
most_played: typing.Sequence[AbyssRankCharacter] = Aliased("reveal_rank", default=[])
most_kills: typing.Sequence[AbyssRankCharacter] = Aliased("defeat_rank", default=[])
strongest_strike: typing.Sequence[AbyssRankCharacter] = Aliased("damage_rank", default=[])
most_damage_taken: typing.Sequence[AbyssRankCharacter] = Aliased("take_damage_rank", default=[]) # noqa: E501
most_bursts_used: typing.Sequence[AbyssRankCharacter] = Aliased("energy_skill_rank", default=[]) # noqa: E501
most_skills_used: typing.Sequence[AbyssRankCharacter] = Aliased("normal_skill_rank", default=[]) # noqa: E501


class Battle(APIModel):
Expand Down
4 changes: 2 additions & 2 deletions genshin/models/genshin/chronicle/img_theater.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@
__all__ = (
"Act",
"ActCharacter",
"BattleStatCharacter",
"ImgTheater",
"ImgTheaterData",
"TheaterBattleStats",
"TheaterBuff",
"TheaterCharaType",
"TheaterDifficulty",
"TheaterSchedule",
"TheaterStats",
"TheaterBattleStats",
"BattleStatCharacter",
)


Expand Down
Loading
Loading