mirror of
https://github.com/PaiGramTeam/SIMNet.git
synced 2024-11-22 06:17:57 +00:00
✨ Add Star Rail Client
This commit is contained in:
parent
7294850914
commit
cd97a751b9
3
simnet/__init__.py
Normal file
3
simnet/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from simnet.client.starrail import StarRailClient
|
||||
|
||||
__all__ = ("StarRailClient",)
|
56
simnet/client/chronicle/base.py
Normal file
56
simnet/client/chronicle/base.py
Normal file
@ -0,0 +1,56 @@
|
||||
from typing import Optional, Any
|
||||
from simnet.client.base import BaseClient
|
||||
from simnet.client.routes import RECORD_URL
|
||||
from simnet.utils.enum_ import Region, Game
|
||||
from simnet.utils.types import QueryParamTypes
|
||||
|
||||
|
||||
class BaseChronicleClient(BaseClient):
|
||||
"""The base class for the Chronicle API client.
|
||||
|
||||
This class provides the basic functionality for making requests to the
|
||||
Chronicle API endpoints. It is meant to be subclassed by other clients
|
||||
that provide a more specific interface to the Chronicle API.
|
||||
|
||||
Attributes:
|
||||
region (Region): The region associated with the API client.
|
||||
"""
|
||||
|
||||
async def request_game_record(
|
||||
self,
|
||||
endpoint: str,
|
||||
data: Optional[Any] = None,
|
||||
params: Optional[QueryParamTypes] = None,
|
||||
lang: Optional[str] = None,
|
||||
region: Optional[Region] = None,
|
||||
game: Optional[Game] = None,
|
||||
):
|
||||
"""Make a request towards the game record endpoint.
|
||||
|
||||
Args:
|
||||
endpoint (str): The endpoint to send the request to.
|
||||
data (Optional[Any], optional): The request payload.
|
||||
params (Optional[QueryParamTypes], optional): The query parameters for the request.
|
||||
lang (Optional[str], optional): The language for the response.
|
||||
region (Optional[Region], optional): The region associated with the request.
|
||||
game (Optional[Game], optional): The game associated with the request.
|
||||
|
||||
Returns:
|
||||
The response from the server.
|
||||
|
||||
Raises:
|
||||
NetworkError: If an HTTP error occurs while making the request.
|
||||
TimedOut: If the request times out.
|
||||
BadRequest: If the response contains an error.
|
||||
"""
|
||||
base_url = RECORD_URL.get_url(region or self.region)
|
||||
|
||||
if game:
|
||||
base_url = base_url / game.value / "api"
|
||||
|
||||
url = base_url / endpoint
|
||||
new_ds = self.region == Region.CHINESE
|
||||
|
||||
return await self.request_lab(
|
||||
url, data=data, params=params, lang=lang, new_ds=new_ds
|
||||
)
|
143
simnet/client/chronicle/starrail.py
Normal file
143
simnet/client/chronicle/starrail.py
Normal file
@ -0,0 +1,143 @@
|
||||
from typing import Optional, Mapping, Dict, Any
|
||||
|
||||
from simnet.client.chronicle.base import BaseChronicleClient
|
||||
from simnet.errors import BadRequest, DataNotPublic
|
||||
from simnet.models.starrail.chronicle.characters import StarShipDetailCharacters
|
||||
from simnet.models.starrail.chronicle.notes import StarRailNote
|
||||
from simnet.models.starrail.chronicle.stats import StarRailUserStats
|
||||
from simnet.utils.enum_ import Game
|
||||
from simnet.utils.player import recognize_starrail_server, recognize_region
|
||||
|
||||
|
||||
class StarRailBattleChronicleClient(BaseChronicleClient):
|
||||
"""A client for retrieving data from StarRail's battle chronicle component.
|
||||
|
||||
This class is used to retrieve various data objects from StarRail's battle chronicle component,
|
||||
including real-time notes, user statistics, and character information.
|
||||
"""
|
||||
|
||||
async def _request_starrail_record(
|
||||
self,
|
||||
endpoint: str,
|
||||
player_id: Optional[int] = None,
|
||||
method: str = "GET",
|
||||
lang: Optional[str] = None,
|
||||
payload: Optional[Dict[str, Any]] = None,
|
||||
) -> Mapping[str, Any]:
|
||||
"""Get an arbitrary object from StarRail's battle chronicle.
|
||||
|
||||
Args:
|
||||
endpoint (str): The endpoint of the object to retrieve.
|
||||
player_id (Optional[int], optional): The player ID. Defaults to None.
|
||||
method (str, optional): The HTTP method to use. Defaults to "GET".
|
||||
lang (Optional[str], optional): The language of the data. Defaults to None.
|
||||
payload (Optional[Dict[str, Any]], optional): The request payload. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Mapping[str, Any]: The requested object.
|
||||
|
||||
Raises:
|
||||
BadRequest: If the request is invalid.
|
||||
DataNotPublic: If the requested data is not public.
|
||||
"""
|
||||
payload = dict(payload or {})
|
||||
|
||||
player_id = player_id or self.player_id
|
||||
payload = dict(
|
||||
role_id=player_id, server=recognize_starrail_server(player_id), **payload
|
||||
)
|
||||
|
||||
data, params = None, None
|
||||
if method == "POST":
|
||||
data = payload
|
||||
else:
|
||||
params = payload
|
||||
|
||||
return await self.request_game_record(
|
||||
endpoint,
|
||||
lang=lang,
|
||||
game=Game.STARRAIL,
|
||||
region=recognize_region(player_id, game=Game.STARRAIL),
|
||||
params=params,
|
||||
data=data,
|
||||
)
|
||||
|
||||
async def get_starrail_notes(
|
||||
self,
|
||||
player_id: Optional[int] = None,
|
||||
lang: Optional[str] = None,
|
||||
autoauth: bool = True,
|
||||
) -> StarRailNote:
|
||||
"""Get StarRail's real-time notes.
|
||||
|
||||
Args:
|
||||
player_id (Optional[int], optional): The player ID. Defaults to None.
|
||||
lang (Optional[str], optional): The language of the data. Defaults to None.
|
||||
autoauth (bool, optional): Whether to automatically authenticate the user. Defaults to True.
|
||||
|
||||
Returns:
|
||||
StarRailNote: The requested real-time notes.
|
||||
|
||||
Raises:
|
||||
BadRequest: If the request is invalid.
|
||||
DataNotPublic: If the requested data is not public.
|
||||
"""
|
||||
try:
|
||||
data = await self._request_starrail_record("note", player_id, lang=lang)
|
||||
except DataNotPublic as e:
|
||||
# error raised only when real-time notes are not enabled
|
||||
if player_id and self.player_id != player_id:
|
||||
raise BadRequest(
|
||||
e.response, "Cannot view real-time notes of other users."
|
||||
) from e
|
||||
if not autoauth:
|
||||
raise BadRequest(e.response, "Real-time notes are not enabled.") from e
|
||||
data = await self._request_starrail_record("note", player_id, lang=lang)
|
||||
|
||||
return StarRailNote(**data)
|
||||
|
||||
async def get_starrail_user(
|
||||
self,
|
||||
player_id: Optional[int] = None,
|
||||
*,
|
||||
lang: Optional[str] = None,
|
||||
) -> StarRailUserStats:
|
||||
"""Get StarRail user statistics.
|
||||
|
||||
Args:
|
||||
player_id (Optional[int], optional): The player ID. Defaults to None.
|
||||
lang (Optional[str], optional): The language of the data. Defaults to None.
|
||||
|
||||
Returns:
|
||||
StarRailUserStats: The requested user statistics.
|
||||
|
||||
Raises:
|
||||
BadRequest: If the request is invalid.
|
||||
DataNotPublic: If the requested data is not public.
|
||||
"""
|
||||
data = await self._request_starrail_record("index", player_id, lang=lang)
|
||||
return StarRailUserStats(**data)
|
||||
|
||||
async def get_starrail_characters(
|
||||
self,
|
||||
player_id: Optional[int] = None,
|
||||
lang: Optional[str] = None,
|
||||
) -> StarShipDetailCharacters:
|
||||
"""Get StarRail character information.
|
||||
|
||||
Args:
|
||||
player_id (Optional[int], optional): The player ID. Defaults to None.
|
||||
lang (Optional[str], optional): The language of the data. Defaults to None.
|
||||
|
||||
Returns:
|
||||
StarShipDetailCharacters: The requested character information.
|
||||
|
||||
Raises:
|
||||
BadRequest: If the request is invalid.
|
||||
DataNotPublic: If the requested data is not public.
|
||||
"""
|
||||
payload = {"need_wiki": "true"}
|
||||
data = await self._request_starrail_record(
|
||||
"avatar/info", player_id, lang=lang, payload=payload
|
||||
)
|
||||
return StarShipDetailCharacters(**data)
|
@ -154,3 +154,20 @@ class GameRoute(BaseRoute):
|
||||
)
|
||||
|
||||
return self.urls[region][game]
|
||||
|
||||
|
||||
RECORD_URL = InternationalRoute(
|
||||
overseas="https://bbs-api-os.hoyolab.com/game_record",
|
||||
chinese="https://api-takumi-record.mihoyo.com/game_record/app",
|
||||
)
|
||||
|
||||
GACHA_INFO_URL = GameRoute(
|
||||
overseas=dict(
|
||||
genshin="https://hk4e-api-os.hoyoverse.com/event/gacha_info/api",
|
||||
hkrpg="",
|
||||
),
|
||||
chinese=dict(
|
||||
genshin="https://hk4e-api.mihoyo.com/event/gacha_info/api",
|
||||
hkrpg="https://api-takumi.mihoyo.com/common/gacha_record/api",
|
||||
),
|
||||
)
|
||||
|
8
simnet/client/starrail.py
Normal file
8
simnet/client/starrail.py
Normal file
@ -0,0 +1,8 @@
|
||||
from simnet.client.chronicle.starrail import StarRailBattleChronicleClient
|
||||
from simnet.client.wish.starrail import WishClient
|
||||
|
||||
__all__ = ("StarRailClient",)
|
||||
|
||||
|
||||
class StarRailClient(StarRailBattleChronicleClient, WishClient):
|
||||
"""A simple http client for StarRail endpoints."""
|
18
simnet/client/starrail.pyi
Normal file
18
simnet/client/starrail.pyi
Normal file
@ -0,0 +1,18 @@
|
||||
from typing import Optional
|
||||
|
||||
from simnet.client.chronicle.starrail import StarRailBattleChronicleClient
|
||||
from simnet.client.wish.starrail import WishClient
|
||||
from simnet.utils.enum_ import Region
|
||||
from simnet.utils.types import CookieTypes, HeaderTypes
|
||||
|
||||
|
||||
class StarRailClient(StarRailBattleChronicleClient, WishClient):
|
||||
def __init__(
|
||||
self,
|
||||
cookies: Optional[CookieTypes] = None,
|
||||
headers: Optional[HeaderTypes] = None,
|
||||
account_id: Optional[int] = None,
|
||||
player_id: Optional[int] = None,
|
||||
region: Region = Region.OVERSEAS,
|
||||
lang: str = "en-us",
|
||||
): ...
|
128
simnet/client/wish/base.py
Normal file
128
simnet/client/wish/base.py
Normal file
@ -0,0 +1,128 @@
|
||||
from typing import Optional, Any, List, Dict
|
||||
from urllib.parse import unquote
|
||||
|
||||
from simnet.client.base import BaseClient
|
||||
from simnet.client.routes import GACHA_INFO_URL
|
||||
from simnet.utils.enum_ import Game
|
||||
from simnet.utils.lang import create_short_lang_code
|
||||
|
||||
|
||||
class BaseWishClient(BaseClient):
|
||||
"""The base class for the Wish API client."""
|
||||
|
||||
async def request_gacha_info(
|
||||
self,
|
||||
endpoint: str,
|
||||
game: Game,
|
||||
lang: Optional[str] = None,
|
||||
authkey: Optional[str] = None,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Make a request towards the gacha info endpoint.
|
||||
|
||||
Args:
|
||||
endpoint (str): The endpoint to request data from.
|
||||
game (Game): The game to make the request for.
|
||||
lang (Optional[str] , optional): The language code to use for the request.
|
||||
If not provided, the class default will be used.
|
||||
authkey (Optional[str] , optional): The authorization key for making the request.
|
||||
params (Optional[Dict[str, Any]], optional): The query parameters for the request.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]
|
||||
The response data as a dictionary.
|
||||
"""
|
||||
params = dict(params or {})
|
||||
|
||||
if authkey is None:
|
||||
raise RuntimeError("No authkey provided")
|
||||
|
||||
base_url = GACHA_INFO_URL.get_url(self.region, game)
|
||||
url = base_url / endpoint
|
||||
|
||||
params["authkey_ver"] = 1
|
||||
params["authkey"] = unquote(authkey)
|
||||
params["lang"] = create_short_lang_code(lang or self.lang)
|
||||
|
||||
return await self.request_api("GET", url, params=params)
|
||||
|
||||
async def wish_history(
|
||||
self,
|
||||
banner_types: List[int],
|
||||
limit: Optional[int] = None,
|
||||
lang: Optional[str] = None,
|
||||
authkey: Optional[str] = None,
|
||||
end_id: int = 0,
|
||||
) -> List[object]:
|
||||
"""
|
||||
Get the wish history for a list of banner types.
|
||||
|
||||
Args:
|
||||
banner_types (List[int]): The list of banner types to get the wish history for.
|
||||
limit (Optional[int] , optional): The maximum number of wishes to retrieve.
|
||||
If not provided, all available wishes will be returned.
|
||||
lang (Optional[str], optional): The language code to use for the request.
|
||||
If not provided, the class default will be used.
|
||||
authkey (Optional[str], optional): The authorization key for making the request.
|
||||
end_id (int, optional): The ending ID of the last wish to retrieve.
|
||||
|
||||
Returns:
|
||||
List[Any]: A list of Wish objects representing the retrieved wishes.
|
||||
"""
|
||||
|
||||
async def get_wish_page(
|
||||
self,
|
||||
end_id: int,
|
||||
banner_type: int,
|
||||
game: Game,
|
||||
lang: Optional[str] = None,
|
||||
authkey: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get a single page of wishes.
|
||||
|
||||
Args:
|
||||
end_id (int): The ending ID of the last wish to retrieve.
|
||||
banner_type (int): The type of banner to retrieve wishes from.
|
||||
game (Game): The game to make the request for.
|
||||
lang (Optional[str], optional): The language code to use for the request.
|
||||
If not provided, the class default will be used.
|
||||
authkey (Optional[str], optional): The authorization key for making the request.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The response data as a dictionary.
|
||||
"""
|
||||
return await self.request_gacha_info(
|
||||
"getGachaLog",
|
||||
game=game,
|
||||
lang=lang,
|
||||
authkey=authkey,
|
||||
params=dict(gacha_type=banner_type, size=20, end_id=end_id),
|
||||
)
|
||||
|
||||
async def get_banner_names(
|
||||
self,
|
||||
game: Game,
|
||||
lang: Optional[str] = None,
|
||||
authkey: Optional[str] = None,
|
||||
) -> Dict[int, str]:
|
||||
"""
|
||||
Get a list of banner names.
|
||||
|
||||
Args:
|
||||
game (Game): The game to make the request for.
|
||||
lang (Optional[str], optional): The language code to use for the request.
|
||||
If not provided, the class default will be used.
|
||||
authkey (Optional[str], optional): The authorization key for making the request.
|
||||
|
||||
Returns:
|
||||
Dict[int, str]: A dictionary mapping banner type IDs to their corresponding names.
|
||||
"""
|
||||
data = await self.request_gacha_info(
|
||||
"getConfigList",
|
||||
game=game,
|
||||
lang=lang,
|
||||
authkey=authkey,
|
||||
)
|
||||
return {int(i["key"]): i["name"] for i in data["gacha_type_list"]}
|
41
simnet/client/wish/starrail.py
Normal file
41
simnet/client/wish/starrail.py
Normal file
@ -0,0 +1,41 @@
|
||||
from typing import Optional, List
|
||||
from simnet.client.wish.base import BaseWishClient
|
||||
from simnet.models.starrail.wish import StarRailWish
|
||||
from simnet.utils.enum_ import Game
|
||||
|
||||
|
||||
class WishClient(BaseWishClient):
|
||||
"""The WishClient class for making requests towards the Wish API."""
|
||||
|
||||
async def wish_history(
|
||||
self,
|
||||
banner_types: List[int],
|
||||
limit: Optional[int] = None,
|
||||
lang: Optional[str] = None,
|
||||
authkey: Optional[str] = None,
|
||||
end_id: int = 0,
|
||||
) -> List[StarRailWish]:
|
||||
"""
|
||||
Get the wish history for a list of banner types.
|
||||
|
||||
Args:
|
||||
banner_types (List[int], optional): The list of banner types to get the wish history for.
|
||||
limit (Optional[int] , optional): The maximum number of wishes to retrieve.
|
||||
If not provided, all available wishes will be returned.
|
||||
lang (Optional[str], optional): The language code to use for the request.
|
||||
If not provided, the class default will be used.
|
||||
authkey (Optional[str], optional): The authorization key for making the request.
|
||||
end_id (int, optional): The ending ID of the last wish to retrieve.
|
||||
|
||||
Returns:
|
||||
List[StarRailWish]: A list of StarRailWish objects representing the retrieved wishes.
|
||||
"""
|
||||
wish: List[StarRailWish] = []
|
||||
banner_names = await self.get_banner_names(
|
||||
game=Game.STARRAIL, lang=lang, authkey=authkey
|
||||
)
|
||||
for banner_type in banner_types:
|
||||
data = await self.get_wish_page(end_id, banner_type, Game.STARRAIL)
|
||||
banner_name = banner_names[banner_type]
|
||||
wish = [StarRailWish(**i, banner_name=banner_name) for i in data["list"]]
|
||||
return wish
|
19
simnet/models/starrail/character.py
Normal file
19
simnet/models/starrail/character.py
Normal file
@ -0,0 +1,19 @@
|
||||
from simnet.models.base import APIModel
|
||||
|
||||
|
||||
class BaseCharacter(APIModel):
|
||||
"""
|
||||
A class representing a character in a game.
|
||||
|
||||
Attributes:
|
||||
id (:obj:`int`): The unique identifier of the character.
|
||||
name (:obj:`str`): The name of the character.
|
||||
element (:obj:`str`): The element that the character represents (e.g. fire, water, etc.).
|
||||
rarity (:obj:`int`): The rarity of the character (e.g. 1-5 stars).
|
||||
"""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
element: str
|
||||
rarity: int
|
||||
icon: str
|
106
simnet/models/starrail/chronicle/characters.py
Normal file
106
simnet/models/starrail/chronicle/characters.py
Normal file
@ -0,0 +1,106 @@
|
||||
from typing import Optional, List
|
||||
|
||||
from simnet.models.base import APIModel
|
||||
from simnet.models.starrail.character import BaseCharacter
|
||||
|
||||
|
||||
class PartialCharacter(BaseCharacter):
|
||||
"""A character without any equipment.
|
||||
|
||||
Attributes:
|
||||
level (int): The level of the character.
|
||||
rank (int): The rank of the character.
|
||||
"""
|
||||
|
||||
level: int
|
||||
rank: int
|
||||
|
||||
|
||||
class Equipment(APIModel):
|
||||
"""An equipment model used in StarRailDetailCharacter.
|
||||
|
||||
Attributes:
|
||||
id (int): The ID of the equipment.
|
||||
level (int): The level of the equipment.
|
||||
rank (int): The rank of the equipment.
|
||||
name (str): The name of the equipment.
|
||||
desc (str): The description of the equipment.
|
||||
icon (str): The icon of the equipment.
|
||||
"""
|
||||
|
||||
id: int
|
||||
level: int
|
||||
rank: int
|
||||
name: str
|
||||
desc: str
|
||||
icon: str
|
||||
|
||||
|
||||
class Relic(APIModel):
|
||||
"""A relic model used in StarRailDetailCharacter.
|
||||
|
||||
Attributes:
|
||||
id (int): The ID of the relic.
|
||||
level (int): The level of the relic.
|
||||
pos (int): The position of the relic.
|
||||
name (str): The name of the relic.
|
||||
desc (str): The description of the relic.
|
||||
icon (str): The icon of the relic.
|
||||
rarity (int): The rarity of the relic.
|
||||
"""
|
||||
|
||||
id: int
|
||||
level: int
|
||||
pos: int
|
||||
name: str
|
||||
desc: str
|
||||
icon: str
|
||||
rarity: int
|
||||
|
||||
|
||||
class Rank(APIModel):
|
||||
"""A rank model used in StarRailDetailCharacter.
|
||||
|
||||
Attributes:
|
||||
id (int): The ID of the rank.
|
||||
pos (int): The position of the rank.
|
||||
name (str): The name of the rank.
|
||||
icon (str): The icon of the rank.
|
||||
desc (str): The description of the rank.
|
||||
is_unlocked (bool): Whether the rank is unlocked.
|
||||
"""
|
||||
|
||||
id: int
|
||||
pos: int
|
||||
name: str
|
||||
icon: str
|
||||
desc: str
|
||||
is_unlocked: bool
|
||||
|
||||
|
||||
class StarRailDetailCharacter(PartialCharacter):
|
||||
"""A detailed character model used in StarShipDetailCharacters.
|
||||
|
||||
Attributes:
|
||||
image (str): The image of the character.
|
||||
equip (Optional[Equipment]): The equipment of the character, if any.
|
||||
relics (List[Relic]): The relics of the character.
|
||||
ornaments (List[Relic]): The ornaments of the character.
|
||||
ranks (List[Rank]): The ranks of the character.
|
||||
"""
|
||||
|
||||
image: str
|
||||
equip: Optional[Equipment]
|
||||
relics: List[Relic]
|
||||
ornaments: List[Relic]
|
||||
ranks: List[Rank]
|
||||
|
||||
|
||||
class StarShipDetailCharacters(APIModel):
|
||||
"""A model containing a list of detailed characters used in the StarShipDetail API.
|
||||
|
||||
Attributes:
|
||||
avatar_list (List[StarRailDetailCharacter]): The list of detailed characters.
|
||||
"""
|
||||
|
||||
avatar_list: List[StarRailDetailCharacter]
|
52
simnet/models/starrail/chronicle/notes.py
Normal file
52
simnet/models/starrail/chronicle/notes.py
Normal file
@ -0,0 +1,52 @@
|
||||
from datetime import timedelta, datetime
|
||||
from typing import List, Literal, Sequence
|
||||
|
||||
from simnet.models.base import APIModel
|
||||
|
||||
|
||||
class StarRailExpedition(APIModel):
|
||||
"""Represents a StarRail Expedition.
|
||||
|
||||
Attributes:
|
||||
avatars (List[str]): A list of avatar names participating in the expedition.
|
||||
status (Literal["Ongoing", "Finished"]): The status of the expedition.
|
||||
remaining_time (timedelta): The time remaining for the expedition to finish.
|
||||
name (str): The name of the expedition.
|
||||
|
||||
"""
|
||||
|
||||
avatars: List[str]
|
||||
status: Literal["Ongoing", "Finished"]
|
||||
remaining_time: timedelta
|
||||
name: str
|
||||
|
||||
@property
|
||||
def finished(self) -> bool:
|
||||
"""Returns whether the expedition has finished."""
|
||||
return self.remaining_time <= timedelta(0)
|
||||
|
||||
@property
|
||||
def completion_time(self) -> datetime:
|
||||
"""Returns the time at which the expedition will be completed."""
|
||||
return datetime.now().astimezone() + self.remaining_time
|
||||
|
||||
|
||||
class StarRailNote(APIModel):
|
||||
"""Represents a StarRail Note.
|
||||
|
||||
Attributes:
|
||||
current_stamina (int): The current stamina of the user.
|
||||
max_stamina (int): The maximum stamina of the user.
|
||||
stamina_recover_time (timedelta): The time it takes for one stamina to recover.
|
||||
accepted_epedition_num (int): The number of expeditions the user has accepted.
|
||||
total_expedition_num (int): The total number of expeditions the user has participated in.
|
||||
expeditions (Sequence[StarRailExpedition]): A list of expeditions the user has participated in.
|
||||
|
||||
"""
|
||||
|
||||
current_stamina: int
|
||||
max_stamina: int
|
||||
stamina_recover_time: timedelta
|
||||
accepted_epedition_num: int
|
||||
total_expedition_num: int
|
||||
expeditions: Sequence[StarRailExpedition]
|
40
simnet/models/starrail/chronicle/stats.py
Normal file
40
simnet/models/starrail/chronicle/stats.py
Normal file
@ -0,0 +1,40 @@
|
||||
from typing import List
|
||||
from pydantic import Field
|
||||
from simnet.models.base import APIModel
|
||||
from simnet.models.starrail.chronicle.characters import PartialCharacter
|
||||
|
||||
|
||||
class Stats(APIModel):
|
||||
"""
|
||||
Statistics of user data.
|
||||
|
||||
Attributes:
|
||||
active_days (int): Number of days the user has been active.
|
||||
avatar_num (int): Number of avatars the user has.
|
||||
achievement_num (int): Number of achievements the user has earned.
|
||||
chest_num (int): Number of chests the user has opened.
|
||||
abyss_process (str): Progress of the user in the abyss mode.
|
||||
"""
|
||||
|
||||
active_days: int
|
||||
avatar_num: int
|
||||
achievement_num: int
|
||||
chest_num: int
|
||||
abyss_process: str
|
||||
|
||||
|
||||
class PartialStarRailUserStats(APIModel):
|
||||
"""
|
||||
Partial data of StarRail user, containing statistics and character information.
|
||||
|
||||
Attributes:
|
||||
stats (Stats): Statistics of user data.
|
||||
characters (List[PartialCharacter]): List of user's avatars/characters.
|
||||
"""
|
||||
|
||||
stats: Stats
|
||||
characters: List[PartialCharacter] = Field(alias="avatar_list")
|
||||
|
||||
|
||||
class StarRailUserStats(PartialStarRailUserStats):
|
||||
"""Complete data of StarRail user, containing statistics and character information."""
|
56
simnet/models/starrail/wish.py
Normal file
56
simnet/models/starrail/wish.py
Normal file
@ -0,0 +1,56 @@
|
||||
from datetime import datetime
|
||||
from enum import IntEnum
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field, validator
|
||||
|
||||
from simnet.models.base import APIModel
|
||||
|
||||
|
||||
class StarRailBannerType(IntEnum):
|
||||
"""Banner types in wish histories."""
|
||||
|
||||
NOVICE = 2
|
||||
"""Temporary novice banner."""
|
||||
|
||||
STANDARD = PERMANENT = 1
|
||||
"""Permanent standard banner."""
|
||||
|
||||
CHARACTER = 11
|
||||
"""Rotating character banner."""
|
||||
|
||||
WEAPON = 12
|
||||
"""Rotating weapon banner."""
|
||||
|
||||
|
||||
class StarRailWish(APIModel):
|
||||
"""Wish made on any banner."""
|
||||
|
||||
uid: int
|
||||
"""User ID of the wish maker."""
|
||||
|
||||
id: int
|
||||
"""ID of the wished item."""
|
||||
|
||||
type: str = Field(alias="item_type")
|
||||
"""Type of the wished item."""
|
||||
|
||||
name: str
|
||||
"""Name of the wished item."""
|
||||
|
||||
rarity: int = Field(alias="rank_type")
|
||||
"""Rarity of the wished item."""
|
||||
|
||||
time: datetime
|
||||
"""Time when the wish was made."""
|
||||
|
||||
banner_type: StarRailBannerType = Field(alias="gacha_type")
|
||||
"""Type of the banner the wish was made on."""
|
||||
|
||||
banner_name: str
|
||||
"""Name of the banner the wish was made on."""
|
||||
|
||||
@validator("banner_type", pre=True)
|
||||
def cast_banner_type(cls, v: Any) -> int:
|
||||
"""Converts the banner type from any type to int."""
|
||||
return int(v)
|
62
tests/conftest.py
Normal file
62
tests/conftest.py
Normal file
@ -0,0 +1,62 @@
|
||||
import asyncio
|
||||
import os
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from simnet.client.cookies import Cookies
|
||||
from simnet.client.starrail import StarRailClient
|
||||
from simnet.utils.cookies import parse_cookie
|
||||
from simnet.utils.enum_ import Region
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop(): # skipcq: PY-D0003
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def cookies() -> "Cookies": # skipcq: PY-D0003
|
||||
cookies_str = os.environ.get("COOKIES")
|
||||
if not cookies_str:
|
||||
pytest.exit("No cookies set", 1)
|
||||
|
||||
_cookies = Cookies(parse_cookie(cookies_str))
|
||||
|
||||
return _cookies
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def player_id() -> int: # skipcq: PY-D0003
|
||||
_player_id = os.environ.get("PLAYER_ID")
|
||||
if not _player_id:
|
||||
pytest.exit("No player id set", 1)
|
||||
return int(_player_id)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def account_id() -> int: # skipcq: PY-D0003
|
||||
_account_id = os.environ.get("ACCOUNT_ID")
|
||||
if not _account_id:
|
||||
pytest.exit("No player id set", 1)
|
||||
return int(_account_id)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def starrail_client( # skipcq: PY-D0003
|
||||
player_id: int, account_id: int, cookies: "Cookies" # skipcq: PYL-W0621
|
||||
):
|
||||
async with StarRailClient(
|
||||
player_id=player_id,
|
||||
cookies=cookies,
|
||||
account_id=account_id,
|
||||
region=Region.CHINESE,
|
||||
) as client_instance:
|
||||
yield client_instance
|
30
tests/test_starrail.py
Normal file
30
tests/test_starrail.py
Normal file
@ -0,0 +1,30 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from simnet.client.starrail import StarRailClient
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestStarRailClient:
|
||||
@staticmethod
|
||||
async def test_get_starrail_user(starrail_client: "StarRailClient"):
|
||||
user = await starrail_client.get_starrail_user()
|
||||
assert user is not None
|
||||
assert user.stats.chest_num > 0
|
||||
assert len(user.characters) > 0
|
||||
character = user.characters[-1]
|
||||
assert character.id > 0
|
||||
|
||||
@staticmethod
|
||||
async def test_get_starrail_notes(starrail_client: "StarRailClient"):
|
||||
notes = await starrail_client.get_starrail_notes()
|
||||
assert notes is not None
|
||||
|
||||
@staticmethod
|
||||
async def test_get_starrail_characters(starrail_client: "StarRailClient"):
|
||||
characters = await starrail_client.get_starrail_characters()
|
||||
assert len(characters.avatar_list) > 0
|
||||
character = characters.avatar_list[-1]
|
||||
assert character.id > 0
|
Loading…
Reference in New Issue
Block a user