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 all 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
2 changes: 1 addition & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
if: github.event_name == 'push'
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10"]
python-version: ["3.9", "3.10", "3.11"]

steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ good references for how projects should be type-hinted to be type-complete.

- This project deviates from the common convention of importing types from the typing module and instead
imports the typing module itself to use generics and types in it like `typing.Union` and `typing.Optional`.
- Since this project supports python 3.8+, the `typing` module takes priority over `collections.abc`.
- Since this project supports python 3.9+, the `typing` module takes priority over `collections.abc`.
- All exported symbols should have docstrings.

---
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Key features:
## Requirements

- Python 3.8+
- Python 3.9+
- aiohttp
- Pydantic

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pip install git+https://github.com/thesadru/genshin.py

### Requirements:

- Python 3.8+
- Python 3.9+
- aiohttp
- Pydantic

Expand Down
8 changes: 4 additions & 4 deletions genshin-dev/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
import setuptools


def parse_requirements_file(path: pathlib.Path) -> typing.List[str]:
def parse_requirements_file(path: pathlib.Path) -> list[str]:
"""Parse a requirements file into a list of requirements."""
with open(path) as fp:
raw_dependencies = fp.readlines()

dependencies: typing.List[str] = []
dependencies: list[str] = []
for dependency in raw_dependencies:
comment_index = dependency.find("#")
if comment_index == 0:
Expand All @@ -30,8 +30,8 @@ def parse_requirements_file(path: pathlib.Path) -> typing.List[str]:

normal_requirements = parse_requirements_file(dev_directory / ".." / "requirements.txt")

all_extras: typing.Set[str] = set()
extras: typing.Dict[str, typing.Sequence[str]] = {}
all_extras: set[str] = set()
extras: dict[str, typing.Sequence[str]] = {}

for path in dev_directory.glob("*-requirements.txt"):
name = path.name.split("-")[0]
Expand Down
16 changes: 6 additions & 10 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,10 +82,10 @@ 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.model_dump().items():
if isinstance(v, dict):
click.echo(f"{k}:")
for nested_k, nested_v in typing.cast("typing.Dict[str, object]", v).items():
for nested_k, nested_v in typing.cast("dict[str, object]", v).items():
click.echo(f" {nested_k}: {click.style(str(nested_v), bold=True)}")
else:
click.echo(f"{k}: {click.style(str(v), bold=True)}")
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.model_dump().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 Expand Up @@ -339,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)}")
Expand Down
6 changes: 3 additions & 3 deletions 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 All @@ -25,7 +25,7 @@

def _separate(values: typing.Iterable[typing.Any], sep: str = ":") -> str:
"""Separate a sequence by a separator into a single string."""
parts: typing.List[str] = []
parts: list[str] = []
for value in values:
if value is None:
parts.append("null")
Expand Down Expand Up @@ -83,7 +83,7 @@ async def set_static(self, key: typing.Any, value: typing.Any) -> None:
class Cache(BaseCache):
"""Standard implementation of the cache."""

cache: typing.Dict[typing.Any, typing.Tuple[float, typing.Any]]
cache: dict[typing.Any, tuple[float, typing.Any]]
maxsize: int
ttl: float
static_ttl: float
Expand Down
9 changes: 5 additions & 4 deletions genshin/client/components/auth/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -385,9 +385,10 @@ async def generate_fp(
device_id_key: str(uuid.uuid4()).lower(),
}

async with aiohttp.ClientSession() as session, session.post(
routes.GET_FP_URL.get_url(self.region), json=payload
) as r:
async with (
aiohttp.ClientSession() as session,
session.post(routes.GET_FP_URL.get_url(self.region), json=payload) as r,
):
data = await r.json()

if data["data"]["code"] != 200:
Expand Down
4 changes: 2 additions & 2 deletions genshin/client/components/auth/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

__all__ = ["PAGES", "enter_code", "launch_webapp", "solve_geetest"]

PAGES: typing.Final[typing.Dict[typing.Literal["captcha", "enter-code"], str]] = {
PAGES: typing.Final[dict[typing.Literal["captcha", "enter-code"], str]] = {
"captcha": """
<!DOCTYPE html>
<head>
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion genshin/client/components/auth/subclients/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ async def _create_qrcode(self) -> QRCodeCreationResult:
url=data["data"]["url"],
)

async def _check_qrcode(self, ticket: str) -> typing.Tuple[QRCodeStatus, SimpleCookie]:
async def _check_qrcode(self, ticket: str) -> tuple[QRCodeStatus, SimpleCookie]:
"""Check the status of a QR code login."""
payload = {"ticket": ticket}

Expand Down
71 changes: 10 additions & 61 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 @@ -64,10 +62,10 @@ class BaseClient(abc.ABC):
_region: types.Region
_default_game: typing.Optional[types.Game]

uids: typing.Dict[types.Game, int]
authkeys: typing.Dict[types.Game, str]
uids: dict[types.Game, int]
authkeys: dict[types.Game, str]
_hoyolab_id: typing.Optional[int]
_accounts: typing.Dict[types.Game, hoyolab_models.GenshinAccount]
_accounts: dict[types.Game, hoyolab_models.GenshinAccount]
custom_headers: multidict.CIMultiDict[str]

def __init__(
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 All @@ -520,7 +500,7 @@ async def _update_cached_uids(self) -> None:
"""Update cached fallback uids."""
mixed_accounts = await self.get_game_accounts()

game_accounts: typing.Dict[types.Game, typing.List[hoyolab_models.GenshinAccount]] = {}
game_accounts: dict[types.Game, list[hoyolab_models.GenshinAccount]] = {}
for account in mixed_accounts:
if not isinstance(account.game, types.Game): # pyright: ignore[reportUnnecessaryIsInstance]
continue
Expand Down Expand Up @@ -553,7 +533,7 @@ async def _update_cached_accounts(self) -> None:
"""Update cached fallback accounts."""
mixed_accounts = await self.get_game_accounts()

game_accounts: typing.Dict[types.Game, typing.List[hoyolab_models.GenshinAccount]] = {}
game_accounts: dict[types.Game, list[hoyolab_models.GenshinAccount]] = {}
for account in mixed_accounts:
if not isinstance(account.game, types.Game): # pyright: ignore[reportUnnecessaryIsInstance]
continue
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
12 changes: 6 additions & 6 deletions genshin/client/components/calculator/calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class CalculatorState:
"""Stores character details if multiple objects require them."""

client: Client
cache: typing.Dict[str, typing.Any]
cache: dict[str, typing.Any]
lock: asyncio.Lock

character_id: typing.Optional[int] = None
Expand Down Expand Up @@ -150,7 +150,7 @@ async def __call__(self, state: CalculatorState) -> typing.Mapping[str, typing.A


class ArtifactResolver(CalculatorResolver[typing.Sequence[typing.Mapping[str, typing.Any]]]):
data: typing.List[typing.Mapping[str, typing.Any]]
data: list[typing.Mapping[str, typing.Any]]

def __init__(self) -> None:
self.data = []
Expand Down Expand Up @@ -208,7 +208,7 @@ async def __call__(self, state: CalculatorState) -> typing.Sequence[typing.Mappi


class TalentResolver(CalculatorResolver[typing.Sequence[typing.Mapping[str, typing.Any]]]):
data: typing.List[typing.Mapping[str, typing.Any]]
data: list[typing.Mapping[str, typing.Any]]

def __init__(self) -> None:
self.data = []
Expand Down Expand Up @@ -378,7 +378,7 @@ def with_current_talents(

async def build(self) -> typing.Mapping[str, typing.Any]:
"""Build the calculator object."""
data: typing.Dict[str, typing.Any] = {}
data: dict[str, typing.Any] = {}

if self.character:
data.update(await self.character(self._state))
Expand Down Expand Up @@ -408,7 +408,7 @@ class FurnishingCalculator:
client: Client
lang: typing.Optional[str]

furnishings: typing.Dict[int, int]
furnishings: dict[int, int]
replica_code: typing.Optional[int] = None
replica_region: typing.Optional[str] = None

Expand All @@ -434,7 +434,7 @@ def with_replica(self, code: int, *, region: typing.Optional[str] = None) -> Fur

async def build(self) -> typing.Mapping[str, typing.Any]:
"""Build the calculator object."""
data: typing.Dict[str, typing.Any] = {}
data: dict[str, typing.Any] = {}

if self.replica_code:
furnishings = await self.client.get_teapot_replica_blueprint(self.replica_code, region=self.replica_region)
Expand Down
Loading
Loading