From 7481a2d97b732f3c29c2566861dff39a905fcb4b Mon Sep 17 00:00:00 2001 From: ashlen Date: Sun, 11 Aug 2024 17:55:19 +0200 Subject: [PATCH] Implement easier raw and tmpl handling --- .github/workflows/checks.yml | 32 +++++++++---------- arkprts/__init__.py | 3 +- arkprts/__main__.py | 11 +++---- arkprts/assets/bundle.py | 3 +- arkprts/assets/git.py | 9 ++++-- arkprts/auth.py | 62 +++++++++++++++++++++++++----------- arkprts/errors.py | 10 +++--- arkprts/models/base.py | 8 +++-- arkprts/models/battle.py | 2 ++ arkprts/models/data.py | 6 ++++ arkprts/models/social.py | 2 ++ arkprts/network.py | 16 ++++++---- dev-requirements/pytest.txt | 2 +- pyproject.toml | 2 +- setup.py | 2 +- 15 files changed, 107 insertions(+), 63 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 00569b4..5aa51c7 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -8,12 +8,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - name: Setup Python 3.11 - uses: actions/setup-python@v4 + - name: Setup Python 3.12 + uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.12" - name: install nox run: python -m pip install nox @@ -25,14 +25,14 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12"] fail-fast: false steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -53,12 +53,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - name: Setup Python 3.11 - uses: actions/setup-python@v4 + - name: Setup Python 3.12 + uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.12" - name: install nox run: python -m pip install nox @@ -70,12 +70,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - name: Setup Python 3.11 - uses: actions/setup-python@v4 + - name: Setup Python 3.12 + uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.12" - name: install nox run: python -m pip install nox @@ -87,7 +87,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Run prettier run: npx prettier --check *.md diff --git a/arkprts/__init__.py b/arkprts/__init__.py index b99c413..076c69a 100644 --- a/arkprts/__init__.py +++ b/arkprts/__init__.py @@ -1,7 +1,8 @@ """Arknights python wrapper.""" -from . import errors, models +from . import models from .assets import * from .auth import * from .client import * +from .errors import * from .network import * diff --git a/arkprts/__main__.py b/arkprts/__main__.py index 5a155c7..2b58bbf 100644 --- a/arkprts/__main__.py +++ b/arkprts/__main__.py @@ -16,7 +16,7 @@ parser.add_argument("--server", type=str, default="en", help="Server to use") parser.add_argument("--guest", action="store_true", help="Whether to use a guest account.") -subparsers: argparse._SubParsersAction[argparse.ArgumentParser] = parser.add_subparsers(dest="command", required=True) +subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]" = parser.add_subparsers(dest="command", required=True) parser_search: argparse.ArgumentParser = subparsers.add_parser("search", description="Get user info.") parser_search.add_argument( @@ -29,7 +29,7 @@ parser_api: argparse.ArgumentParser = subparsers.add_parser("api", description="Make a request towards the API.") parser_api.add_argument("endpoint", type=str, nargs="?", help="Endpoint path, not full url") -parser_api.add_argument("payload", type=str, nargs="?", default="{}", help="JSON payload") +parser_api.add_argument("payload", type=str, nargs="?", default=r"{}", help="JSON payload") async def search(client: arkprts.Client, nickname: typing.Optional[str] = None) -> None: @@ -70,14 +70,11 @@ async def search(client: arkprts.Client, nickname: typing.Optional[str] = None) async def api(client: arkprts.Client, endpoint: str, payload: typing.Optional[str] = None) -> None: """Make a request.""" - client.assets.loaded = True try: - data = await client.request(endpoint, json=payload and json.loads(payload)) + data = await client.auth.auth_request(endpoint, json=payload and json.loads(payload), handle_errors=False) json.dump(data, sys.stdout, indent=4, ensure_ascii=False) - except arkprts.errors.GameServerError as e: - json.dump(e.data, sys.stdout, indent=4, ensure_ascii=False) finally: - await client.network.close() + await client.auth.network.close() sys.stdout.write("\n") diff --git a/arkprts/assets/bundle.py b/arkprts/assets/bundle.py index 20b4f0b..222a780 100644 --- a/arkprts/assets/bundle.py +++ b/arkprts/assets/bundle.py @@ -343,7 +343,8 @@ def __init__( except ImportError as e: raise ImportError("Cannot use BundleAssets without arkprts[assets]") from e try: - subprocess.run(["flatc", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False) # noqa: S603 + cmd = ["flatc", "--version"] + subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False) # noqa: S603 except OSError as e: raise ImportError("Cannot use BundleAssets without a flatc executable") from e diff --git a/arkprts/assets/git.py b/arkprts/assets/git.py index 6e4df89..d78c283 100644 --- a/arkprts/assets/git.py +++ b/arkprts/assets/git.py @@ -80,7 +80,7 @@ async def download_github_tarball( async with aiohttp.ClientSession(auto_decompress=False) as session, session.get(url) as response: response.raise_for_status() - with destination.open("wb") as file: # noqa: ASYNC101 # would need another dependency + with destination.open("wb") as file: # noqa: ASYNC230 async for chunk in response.content.iter_any(): file.write(chunk) @@ -124,8 +124,11 @@ async def download_repository( try: commit = await get_github_repository_commit(repository, branch=branch) except aiohttp.ClientResponseError: - LOGGER.warning("Failed to get %s commit, skipping download", repository, exc_info=True) - return + if commit_file.exists(): + LOGGER.warning("Failed to get %s commit, skipping download", repository, exc_info=True) + return + + commit = "null" if not force and commit_file.exists() and commit_file.read_text() == commit: LOGGER.debug("%s is up to date [%s]", repository, commit) diff --git a/arkprts/auth.py b/arkprts/auth.py index ee85f61..8ad28cc 100644 --- a/arkprts/auth.py +++ b/arkprts/auth.py @@ -180,7 +180,7 @@ async def auth_request( """Send an authenticated request to the arkights game server.""" -class Auth(abc.ABC, CoreAuth): +class Auth(CoreAuth): """Authentication client for single sessions.""" server: netn.ArknightsServer @@ -188,7 +188,7 @@ class Auth(abc.ABC, CoreAuth): network: netn.NetworkSession """Network session.""" device_ids: tuple[str, str, str] - """Device ids.""" + """Random device ids.""" session: AuthSession """Authentication session.""" @@ -206,6 +206,25 @@ def __init__( self.session = AuthSession(self.server, "", "") self.device_ids = create_random_device_ids() + @classmethod + def create( + cls, + server: netn.ArknightsServer | None = None, + *, + network: netn.NetworkSession | None = None, + ) -> Auth: + """Find and create an appropriate auth client.""" + if server in ("en", "jp", "kr"): + return YostarAuth(server, network=network) + if server == "cn": + return HypergryphAuth(server, network=network) + if server == "bili": + return BilibiliAuth(server, network=network) + if server == "tw": + return LongchengAuth(server, network=network) + + return cls(server, network=network) + @property def uid(self) -> str: """Arknights user UID.""" @@ -216,6 +235,11 @@ def secret(self) -> str: """Arknights session token.""" return self.session.secret + @property + def seqnum(self) -> int: + """Last sent seqnum.""" + return self.session.seqnum + async def request( self, domain: netn.ArknightsDomain, @@ -329,21 +353,23 @@ async def from_token( network: netn.NetworkSession | None = None, ) -> Auth: """Create a client from a token.""" - if server in ("en", "jp", "kr"): - auth = YostarAuth(server, network=network) - await auth.login_with_token(channel_uid, token) - elif server == "cn": - auth = HypergryphAuth(server, network=network) - await auth.login_with_token(channel_uid, token) - elif server == "bili": - auth = BilibiliAuth(server, network=network) - await auth.login_with_token(channel_uid, token) - elif server == "tw": - auth = LongchengAuth(server, network=network) - await auth.login_with_token(channel_uid, token) - else: - raise ValueError(f"Cannot create a generic auth client for server {server!r}") + auth = cls.create(server, network=network) + await auth.login_with_token(channel_uid, token) + return auth + @classmethod + async def from_session( + cls, + server: netn.ArknightsServer, + *, + uid: str, + secret: str, + seqnum: str | int, + network: netn.NetworkSession, + ) -> Auth: + """Create an auth from an already ongoing session.""" + auth = cls.create(server, network=network) + auth.session = AuthSession(server, uid=uid, secret=secret, seqnum=int(seqnum)) return auth @@ -491,14 +517,14 @@ async def _get_hypergryph_access_token(self, username: str, password: str) -> st "platform": 1, } data["sign"] = generate_u8_sign(data) - data = await self.network.request("as", "user/login", json=data) + data = await self.request("as", "user/login", json=data) return data["token"] async def _get_hypergryph_uid(self, token: str) -> str: """Get a channel uid from a hypergryph access token.""" data = {"token": token} data["sign"] = generate_u8_sign(data) - data = await self.network.request("as", "user/auth", json=data) + data = await self.request("as", "user/auth", json=data) return data["uid"] async def login_with_token(self, channel_uid: str, access_token: str) -> None: diff --git a/arkprts/errors.py b/arkprts/errors.py index e83e0b7..14b1fce 100644 --- a/arkprts/errors.py +++ b/arkprts/errors.py @@ -28,10 +28,10 @@ class ArkPrtsError(BaseArkprtsError): def __init__(self, data: typing.Mapping[str, typing.Any]) -> None: self.data = data - super().__init__(f"[{data.get('result')}] {self.message} {data}") + super().__init__(f"[{data.get('result')}] {self.message} {json.dumps(data)}") -class GameServerError(BaseArkprtsError): +class GameServerError(ArkPrtsError): """Game server error.""" data: typing.Mapping[str, typing.Any] @@ -49,7 +49,7 @@ def __init__(self, data: typing.Mapping[str, typing.Any]) -> None: self.msg = data.get("msg", "") self.info = json.loads(data.get("info", "{}")) - super().__init__(str(data)) + BaseArkprtsError.__init__(self, json.dumps(data)) class GeetestError(ArkPrtsError): @@ -65,7 +65,7 @@ def __init__(self, data: typing.Mapping[str, typing.Any]) -> None: super().__init__(data) -class InvalidStatusError(BaseArkprtsError): +class InvalidStatusError(ArkPrtsError): """Raised when a response has an invalid status code.""" status: int @@ -75,7 +75,7 @@ def __init__(self, status: int, data: typing.Mapping[str, typing.Any]) -> None: self.status = status self.data = data - super().__init__(f"[{status}] {data}") + BaseArkprtsError.__init__(self, f"[{status}] {json.dumps(data)}") class InvalidContentTypeError(BaseArkprtsError): diff --git a/arkprts/models/base.py b/arkprts/models/base.py index a08e284..2a5045a 100644 --- a/arkprts/models/base.py +++ b/arkprts/models/base.py @@ -62,11 +62,15 @@ def __init__(self, client: CoreClient | None = None, **kwargs: typing.Any) -> No if client: _set_recursively(self, "client", client) - @pydantic.model_validator(mode="before") # pyright: ignore + @pydantic.model_validator(mode="before") + @classmethod def _fix_amiya(cls, value: typing.Any, info: pydantic.ValidationInfo) -> typing.Any: """Flatten Amiya to only keep her selected form if applicable.""" if value and value.get("tmpl"): - # tmplId present in battle replays + value["variations"] = { + tmplid: cls(value["client"], **{**value, **tmpl}) for tmplid, tmpl in value["tmpl"].items() + } + # tmplId present in battle replays, sometimes the tmpl for amiya guard is not actually present current_tmpl = value["currentTmpl"] if "currentTmpl" in value else value["tmplId"] current = value["tmpl"].get(current_tmpl, next(iter(value["tmpl"].values()))) value.update(current) diff --git a/arkprts/models/battle.py b/arkprts/models/battle.py index e1e1a55..fe2cb26 100644 --- a/arkprts/models/battle.py +++ b/arkprts/models/battle.py @@ -68,6 +68,8 @@ class Character(base.BaseModel): tmpl: typing.Mapping[str, base.DDict] = pydantic.Field(default_factory=base.DDict, repr=False) """Alternative operator class data. Only for Amiya.""" + variations: typing.Mapping[str, "Character"] = pydantic.Field(default_factory=dict, repr=False) + """All representations of amiya.""" class Signature(base.BaseModel): diff --git a/arkprts/models/data.py b/arkprts/models/data.py index db7c61d..32c5a58 100644 --- a/arkprts/models/data.py +++ b/arkprts/models/data.py @@ -145,6 +145,8 @@ class SquadSlot(base.BaseModel): """Currently equipped module ID.""" tmpl: typing.Mapping[str, base.DDict] = pydantic.Field(default_factory=base.DDict, repr=False) """Alternative operator class data. Only for Amiya.""" + variations: typing.Mapping[str, "SquadSlot"] = pydantic.Field(default_factory=dict, repr=False) + """All representations of amiya.""" class Squads(base.BaseModel): @@ -224,6 +226,8 @@ class Character(base.BaseModel): """Whether the operator is marked as favorite.""" tmpl: typing.Mapping[str, base.DDict] = pydantic.Field(default_factory=base.DDict, repr=False) """Alternative operator class data. Only for Amiya.""" + variations: typing.Mapping[str, "Character"] = pydantic.Field(default_factory=dict, repr=False) + """All representations of amiya.""" @property def static(self) -> base.DDict: @@ -287,6 +291,8 @@ class AssistChar(base.BaseModel): """Currently equipped module.""" tmpl: typing.Mapping[str, base.DDict] = pydantic.Field(default_factory=base.DDict, repr=False) """Alternative operator class data. Only for Amiya.""" + variations: typing.Mapping[str, "AssistChar"] = pydantic.Field(default_factory=dict, repr=False) + """All representations of amiya.""" class Social(base.BaseModel): diff --git a/arkprts/models/social.py b/arkprts/models/social.py index a4d4e17..07ce60d 100644 --- a/arkprts/models/social.py +++ b/arkprts/models/social.py @@ -81,6 +81,8 @@ class AssistChar(base.BaseModel): """Equipped modules. Module ID to module info.""" tmpl: typing.Mapping[str, base.DDict] = pydantic.Field(default_factory=base.DDict, repr=False) """Alternative operator class data. Only for Amiya.""" + variations: typing.Mapping[str, "AssistChar"] = pydantic.Field(default_factory=dict, repr=False) + """All representations of amiya.""" @property def static(self) -> base.DDict: diff --git a/arkprts/network.py b/arkprts/network.py index 972bc8a..8f9ea19 100644 --- a/arkprts/network.py +++ b/arkprts/network.py @@ -116,6 +116,7 @@ async def raw_request( url: str, *, headers: typing.Mapping[str, str] | None = None, + handle_errors: bool = True, **kwargs: typing.Any, ) -> typing.Any: """Send a request to an arbitrary endpoint.""" @@ -128,11 +129,11 @@ async def raw_request( resp.raise_for_status() raise errors.InvalidContentTypeError(await resp.text()) from e - if data.get("error"): - raise errors.GameServerError(data) - - if resp.status != 200: - raise errors.InvalidStatusError(resp.status, data) + if handle_errors: + if data.get("error"): + raise errors.GameServerError(data) + if resp.status != 200: + raise errors.InvalidStatusError(resp.status, data) return data @@ -143,6 +144,7 @@ async def request( *, server: ArknightsServer | None = None, method: str | None = None, + handle_errors: bool = True, **kwargs: typing.Any, ) -> typing.Any: """Send a request to an arknights server.""" @@ -170,9 +172,9 @@ async def request( if method is None: method = "POST" if kwargs.get("json") is not None else "GET" - data = await self.raw_request(method, url, **kwargs) + data = await self.raw_request(method, url, handle_errors=handle_errors, **kwargs) - if "result" in data and isinstance(data["result"], int) and data["result"] != 0: + if handle_errors and "result" in data and isinstance(data["result"], int) and data["result"] != 0: if "captcha" in data: raise errors.GeetestError(data) diff --git a/dev-requirements/pytest.txt b/dev-requirements/pytest.txt index ffa1dfb..347d8fe 100644 --- a/dev-requirements/pytest.txt +++ b/dev-requirements/pytest.txt @@ -1,5 +1,5 @@ pytest -pytest-asyncio +pytest-asyncio~=0.21.2 pytest-dotenv pytest-cov coverage[toml] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e8b6527..f2ac96d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "arkprts" requires-python = ">=3.9" -version = "0.3.8" +version = "0.3.9" dynamic = [ "dependencies", "description", diff --git a/setup.py b/setup.py index d7104ea..a42ddfb 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="arkprts", - version="0.3.8", + version="0.3.9", description="Arknights python wrapper.", url="https://github.com/thesadru/arkprts", packages=find_packages(exclude=["tests", "tests.*"]),