Support starrail calculator

This commit is contained in:
omg-xtao 2024-01-23 17:51:22 +08:00 committed by GitHub
parent 2d54ab5ca1
commit f15dcce603
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 384 additions and 5 deletions

View File

@ -42,7 +42,7 @@ class CalculatorClient(BaseClient):
"""
params = dict(params or {})
base_url = CALCULATOR_URL.get_url(self.region)
base_url = CALCULATOR_URL.get_url(self.region, self.game)
url = base_url / endpoint
if method == "GET":

View File

@ -0,0 +1,122 @@
from typing import Optional, Any, Dict, List, Literal
from simnet.client.base import BaseClient
from simnet.client.routes import CALCULATOR_URL
from simnet.models.starrail.calculator import (
StarrailCalculatorCharacter,
StarrailCalculatorCharacterDetails,
)
from simnet.utils.enums import Region
from simnet.utils.player import recognize_starrail_server
class StarrailCalculatorClient(BaseClient):
"""A client for retrieving data from star rail's calculator component."""
async def request_calculator(
self,
endpoint: str,
*,
method: str = "POST",
lang: Optional[str] = None,
params: Optional[Dict[str, Any]] = None,
data: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Make a request towards the calculator endpoint.
Args:
endpoint (str): The calculator endpoint to send the request to.
method (str): The HTTP method to use for the request (default "POST").
lang (str): The language to use for the request (default None).
params (dict): The parameters to include in the request URL (default None).
data (dict): The data to include in the request body (default None).
Returns:
dict: The data returned by the calculator endpoint.
"""
params = dict(params or {})
base_url = CALCULATOR_URL.get_url(self.region, self.game)
url = base_url / endpoint
if method == "GET":
params["game"] = self.game.value
params["lang"] = lang or self.lang
data = None
else:
data = dict(data or {})
data["lang"] = lang or self.lang
headers = {}
if self.region == Region.CHINESE:
headers["Referer"] = "https://webstatic.mihoyo.com/"
data = await self.request_lab(url, method=method, params=params, data=data, headers=headers)
return data
async def get_calculator_characters(
self,
tab_from: Literal["TabOwned", "TabAll"] = "TabOwned",
page: int = 1,
size: int = 100,
player_id: Optional[int] = None,
lang: Optional[str] = None,
) -> List[StarrailCalculatorCharacter]:
"""Get all characters provided by the Enhancement Progression Calculator.
Args:
tab_from (Literal["TabOwned", "TabAll"], optional): The tab to get characters from. Defaults to "TabOwned".
page (int, optional): The page to get characters from. Defaults to 1.
size (int, optional): The number of characters to get per page. Defaults to 100.
player_id (int): The player ID to use for syncing (default None).
lang (str): The language to use for the request (default None).
Returns:
list: A list of CalculatorCharacter objects representing the characters retrieved from the calculator.
"""
player_id = player_id or self.player_id
params = {
"tab_from": tab_from,
"page": page,
"size": size,
"uid": player_id,
"region": recognize_starrail_server(player_id),
}
data = await self.request_calculator("avatar/list", method="GET", params=params, lang=lang)
return [StarrailCalculatorCharacter(**i) for i in data.get("list", [])]
async def get_character_details(
self,
character: int,
tab_from: Literal["TabOwned", "TabAll"] = "TabOwned",
player_id: Optional[int] = None,
lang: Optional[str] = None,
) -> StarrailCalculatorCharacterDetails:
"""
Get the weapon, artifacts and talents of a character.
Args:
character (int): The ID of the character to retrieve details for.
tab_from (Literal["TabOwned", "TabAll"], optional): The tab to get characters from. Defaults to "TabOwned".
player_id (Optional[int], optional): The player ID to use for the request. Defaults to None.
lang (Optional[str], optional): The language to use for the calculator. Defaults to None.
Returns:
StarrailCalculatorCharacterDetails: The details of the character.
"""
player_id = player_id or self.player_id
params = dict(
item_id=int(character),
uid=player_id,
region=recognize_starrail_server(player_id),
tab_from=tab_from,
change_target_level=0,
)
data = await self.request_calculator(
"avatar/detail",
method="GET",
lang=lang,
params=params,
)
return StarrailCalculatorCharacterDetails(**data)

View File

@ -274,9 +274,15 @@ TAKUMI_URL = InternationalRoute(
chinese="https://api-takumi.mihoyo.com/",
)
CALCULATOR_URL = InternationalRoute(
overseas="https://sg-public-api.hoyoverse.com/event/calculateos/",
chinese="https://api-takumi.mihoyo.com/event/e20200928calculate/v1/",
CALCULATOR_URL = GameRoute(
overseas=dict(
genshin="https://sg-public-api.hoyoverse.com/event/calculateos/",
hkrpg="https://sg-public-api.hoyolab.com/event/rpgcalc/",
),
chinese=dict(
genshin="https://api-takumi.mihoyo.com/event/e20200928calculate/v1/",
hkrpg="https://api-takumi.mihoyo.com/event/rpgcalc/",
),
)
DETAIL_LEDGER_URL = GameRoute(

View File

@ -1,6 +1,7 @@
from typing import Optional
from simnet.client.components.auth import AuthClient
from simnet.client.components.calculator.starrail import StarrailCalculatorClient
from simnet.client.components.chronicle.starrail import StarRailBattleChronicleClient
from simnet.client.components.daily import DailyRewardClient
from simnet.client.components.diary.starrail import StarrailDiaryClient
@ -13,6 +14,7 @@ __all__ = ("StarRailClient",)
class StarRailClient(
StarrailCalculatorClient,
StarRailBattleChronicleClient,
StarRailWishClient,
StarrailDiaryClient,

View File

@ -1,6 +1,7 @@
from typing import Optional
from simnet.client.components.auth import AuthClient
from simnet.client.components.calculator.starrail import StarrailCalculatorClient
from simnet.client.components.chronicle.starrail import StarRailBattleChronicleClient
from simnet.client.components.daily import DailyRewardClient
from simnet.client.components.diary.starrail import StarrailDiaryClient
@ -11,6 +12,7 @@ from simnet.utils.enums import Region
from simnet.utils.types import CookieTypes, HeaderTypes, TimeoutTypes
class StarRailClient(
StarrailCalculatorClient,
StarRailBattleChronicleClient,
StarRailWishClient,
StarrailDiaryClient,

View File

@ -0,0 +1,180 @@
"""Starrail calculator models."""
from typing import Dict, Any, Optional, List
from pydantic import Field, validator
from simnet.models.base import APIModel
from simnet.models.starrail.character import StarRailBaseCharacter, StarRailElement, StarRailDestiny
__all__ = (
"CALCULATOR_ELEMENTS",
"CALCULATOR_DESTINIES",
"StarrailCalculatorCharacter",
"StarrailCalculatorWeapon",
"StarrailCalculatorSkill",
"StarrailCalculatorCharacterDetails",
)
CALCULATOR_ELEMENTS: Dict[int, StarRailElement] = {
1: StarRailElement.Physical,
2: StarRailElement.Pyro,
4: StarRailElement.Cryo,
8: StarRailElement.Electro,
16: StarRailElement.Anemo,
32: StarRailElement.Quantum,
64: StarRailElement.Nombre,
}
CALCULATOR_DESTINIES: Dict[int, StarRailDestiny] = {
1: StarRailDestiny.HuiMie,
2: StarRailDestiny.XunLie,
3: StarRailDestiny.ZhiShi,
4: StarRailDestiny.TongXie,
5: StarRailDestiny.XuWu,
6: StarRailDestiny.CunHu,
7: StarRailDestiny.FengRao,
}
class StarrailCalculatorCharacter(StarRailBaseCharacter):
"""Character meant to be used with calculators.
Attributes:
id (int): The ID of the character.
element (str): The element of the character.
icon (str): The icon of the character.
name (str): The name of the character.
path (str): The path type of the character.
max_level (int): The maximum level of the character.
cur_level (int): The current level of the character.
target_level (int): The target level of the character.
is_forward (bool): Whether the character is forward.
"""
id: int = Field(alias="item_id")
element: StarRailElement = Field(alias="damage_type")
icon: str = Field(alias="icon_url")
name: str = Field(alias="item_name")
path: StarRailDestiny = Field(alias="avatar_base_type")
max_level: int
cur_level: int
target_level: int
is_forward: bool
@validator("element", pre=True)
def parse_element(cls, v: Any) -> str:
"""Parse the element of a character.
Args:
v (Any): The value of the element.
Returns:
str: The parsed element.
"""
if isinstance(v, str) and v.isnumeric():
return CALCULATOR_ELEMENTS[int(v)].value
return v
@validator("path", pre=True)
def parse_path(cls, v: Any) -> str:
"""Parse the path type of character.
Args:
v (Any): The value of the path type.
Returns:
str: The parsed path type.
"""
if isinstance(v, str) and v.isnumeric():
return CALCULATOR_DESTINIES[int(v)].value
return v
class StarrailCalculatorWeapon(APIModel):
"""Weapon meant to be used with calculators.
Attributes:
id (int): The ID of the weapon.
name (str): The name of the weapon.
icon (str): The icon of the weapon.
rarity (int): The rarity of the weapon.
path (str): The path type of the weapon.
max_level (int): The maximum level of the weapon.
cur_level (int): The current level of the weapon.
target_level (int): The target level of the weapon.
is_forward (bool): Whether the weapon is forward.
"""
id: int = Field(alias="item_id")
name: str = Field(alias="item_name")
icon: str = Field(alias="item_url")
rarity: int
path: StarRailDestiny = Field(alias="avatar_base_type")
max_level: int
cur_level: int
target_level: int
is_forward: bool
@validator("path", pre=True)
def parse_path(cls, v: Any) -> str:
"""Parse the path type of weapon.
Args:
v (Any): The value of the path type.
Returns:
str: The parsed path type.
"""
if isinstance(v, str) and v.isnumeric():
return CALCULATOR_DESTINIES[int(v)].value
return v
class StarrailCalculatorSkill(APIModel):
"""Talent of a character meant to be used with calculators.
Attributes:
id (int): The ID of the talent.
pre_point (int): The previous point of the talent.
point_type (int): The type of the talent.
anchor (str): The anchor of the talent.
icon (str): The icon of the talent.
max_level (int): The maximum level of the talent.
cur_level (int): The current level of the talent.
target_level (int): The target level of the talent.
progress (str): The progress of the talent.
min_level_limit (int): The minimum level limit of the talent.
"""
id: int = Field(alias="point_id")
pre_point: int
point_type: int
anchor: str
icon: str = Field(alias="item_url")
max_level: int
cur_level: int
target_level: int
progress: str
min_level_limit: int
@property
def learned(self) -> bool:
return self.progress == "Learned"
class StarrailCalculatorCharacterDetails(APIModel):
"""Details of a synced calculator
Attributes:
avatar (StarrailCalculatorCharacter): The character of the calculator.
equipment (Optional[StarrailCalculatorWeapon]): The weapon of the calculator.
skills (List[StarrailCalculatorSkill]): The skills of the calculator.
skills_other (List[StarrailCalculatorSkill]): The other skills of the calculator.
"""
avatar: StarrailCalculatorCharacter
equipment: Optional[StarrailCalculatorWeapon] = None
skills: List[StarrailCalculatorSkill]
skills_other: List[StarrailCalculatorSkill]

View File

@ -1,7 +1,35 @@
"""Starrail base character model."""
from enum import Enum
from simnet.models.base import APIModel
class StarRailDestiny(str, Enum):
"""命途"""
HuiMie = "毁灭"
ZhiShi = "智识"
XunLie = "巡猎"
CunHu = "存护"
FengRao = "丰饶"
TongXie = "同谐"
XuWu = "虚无"
class StarRailElement(str, Enum):
"""属性"""
Physical = "物理"
Pyro = ""
Anemo = ""
Electro = ""
Cryo = ""
Nombre = "虚数"
Quantum = "量子"
Null = "NULL"
""""""
class StarRailBaseCharacter(APIModel):
"""Base character model."""

View File

@ -5,6 +5,7 @@ import pytest_asyncio
from simnet.client.components.calculator.genshin import CalculatorClient
from simnet.client.components.chronicle.genshin import GenshinBattleChronicleClient
from simnet.utils.enums import Game
if TYPE_CHECKING:
from simnet.client.cookies import Cookies
@ -14,13 +15,14 @@ if TYPE_CHECKING:
@pytest_asyncio.fixture
async def calculator_client(genshin_player_id: int, account_id: int, region: "Region", cookies: "Cookies"):
if genshin_player_id is None:
pytest.skip("Test case test_starrail_battle_chronicle_client skipped: No genshin player id set.")
pytest.skip("Test case test_genshin_calculator_client skipped: No genshin player id set.")
async with CalculatorClient(
player_id=genshin_player_id,
cookies=cookies,
account_id=account_id,
region=region,
) as client_instance:
client_instance.game = Game.GENSHIN
yield client_instance

View File

@ -0,0 +1,37 @@
from typing import TYPE_CHECKING
import pytest
import pytest_asyncio
from simnet.client.components.calculator.starrail import StarrailCalculatorClient
from simnet.utils.enums import Game
if TYPE_CHECKING:
from simnet.client.cookies import Cookies
from simnet.utils.enums import Region
@pytest_asyncio.fixture
async def calculator_client(starrail_player_id: int, account_id: int, region: "Region", cookies: "Cookies"):
if starrail_player_id is None:
pytest.skip("Test case test_starrail_calculator_client skipped: No starrail player id set.")
async with StarrailCalculatorClient(
player_id=starrail_player_id,
cookies=cookies,
account_id=account_id,
region=region,
lang="zh-cn",
) as client_instance:
client_instance.game = Game.STARRAIL
yield client_instance
@pytest.mark.asyncio
class TestCalculatorClient:
@staticmethod
async def test_character_details(calculator_client: "StarrailCalculatorClient"):
characters = await calculator_client.get_calculator_characters()
character_details = await calculator_client.get_character_details(characters[-1].id)
assert len(character_details.skills) == 4
for talent in character_details.skills:
assert talent.cur_level > -1