Add diary client

This commit is contained in:
luoshuijs 2023-06-09 11:48:56 +08:00 committed by GitHub
parent 6f9cee697b
commit 40d55ea0ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 489 additions and 2 deletions

View File

@ -0,0 +1,99 @@
from datetime import timedelta, timezone, datetime
from typing import Optional, Any, Dict
from simnet.client.base import BaseClient
from simnet.client.routes import DETAIL_LEDGER_URL, INFO_LEDGER_URL
from simnet.models.diary import DiaryType
from simnet.models.genshin.diary import DiaryPage
from simnet.utils.enum_ import Region, Game
from simnet.utils.player import recognize_server
__all__ = ("BaseDiaryClient",)
CN_TIMEZONE = timezone(timedelta(hours=8))
class BaseDiaryClient(BaseClient):
"""Base diary component."""
async def request_ledger(
self,
player_id: Optional[int] = None,
*,
game: Optional[Game] = None,
detail: bool = False,
month: Optional[int] = None,
lang: Optional[str] = None,
params: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Make a request towards the ys ledger endpoint.
Args:
player_id (Optional[int], optional): The player ID to get the ledger for.
game (Optional[Game], optional): The game to get the ledger for.
detail (bool, optional): Whether to get the detailed ledger.
month (Optional[int], optional): The month to get the ledger for.
lang (Optional[str], optional): The language code to use for the request.
params (Optional[Dict[str, Any]], optional): The query parameters to use for the request.
Returns:
Dict[str, Any]: The response data.
"""
game = game or self.game
player_id = player_id or self.player_id
params = params or {}
url = (
DETAIL_LEDGER_URL.get_url(self.region)
if detail
else INFO_LEDGER_URL.get_url(
self.region,
game,
)
)
if self.region == Region.OVERSEAS or game == Game.STARRAIL:
params["uid"] = player_id
params["region"] = recognize_server(player_id, game)
elif self.region == Region.CHINESE:
params["bind_uid"] = player_id
params["bind_region"] = recognize_server(player_id, game)
else:
raise TypeError(f"{self.region!r} is not a valid region.")
params["month"] = month or (datetime.now().strftime("%Y%m") if game == Game.STARRAIL else datetime.now().month)
params["lang"] = lang or self.lang
return await self.request_lab(url, params=params)
async def _get_diary_page(
self,
page: int,
*,
game: Optional[Game] = None,
player_id: Optional[int] = None,
diary_type: int = DiaryType.PRIMOGEMS,
month: Optional[int] = None,
lang: Optional[str] = None,
) -> DiaryPage:
"""Get a diary page.
Args:
page (int): The page number to get.
game (Optional[Game], optional): The game to get the diary page for.
player_id (Optional[int], optional): The player ID to get the diary page for.
diary_type (int, optional): The diary type to get the diary page for.
month (Optional[int], optional): The month to get the diary page for.
lang (Optional[str], optional): The language code to use for the request.
Returns:
DiaryPage: The diary page.
"""
data = await self.request_ledger(
player_id,
game=game,
detail=True,
month=month,
lang=lang,
params=dict(type=diary_type, current_page=page, page_size=100),
)
return DiaryPage(**data)

View File

@ -0,0 +1,29 @@
from typing import Optional
from simnet.client.components.diary.base import BaseDiaryClient
from simnet.models.genshin.diary import Diary
from simnet.utils.enum_ import Game
class GenshinDiaryClient(BaseDiaryClient):
"""Genshin diary component."""
async def get_genshin_diary(
self,
player_id: Optional[int] = None,
*,
month: Optional[int] = None,
lang: Optional[str] = None,
) -> Diary:
"""Get a traveler's diary with earning details for the month.
Args:
player_id (int, optional): The player's ID. Defaults to None.
month (int, optional): The month to get the diary for. Defaults to None.
lang (str, optional): The language to get the diary in. Defaults to None.
Returns:
Diary: The diary for the month.
"""
data = await self.request_ledger(player_id, game=Game.GENSHIN, month=month, lang=lang)
return Diary(**data)

View File

@ -0,0 +1,29 @@
from typing import Optional
from simnet.client.components.diary.base import BaseDiaryClient
from simnet.models.starrail.diary import StarRailDiary
from simnet.utils.enum_ import Game
class StarrailDiaryClient(BaseDiaryClient):
"""Starrail diary component."""
async def get_starrail_diary(
self,
player_id: Optional[int] = None,
*,
month: Optional[int] = None,
lang: Optional[str] = None,
) -> StarRailDiary:
"""Get a traveler's diary with earning details for the month.
Args:
player_id (int, optional): The player's ID. Defaults to None.
month (int, optional): The month to get the diary for. Defaults to None.
lang (str, optional): The language to get the diary in. Defaults to None.
Returns:
Diary: The diary for the month.
"""
data = await self.request_ledger(player_id, game=Game.STARRAIL, month=month, lang=lang)
return StarRailDiary(**data)

View File

@ -3,6 +3,7 @@ from typing import Optional
from simnet.client.components.auth import AuthClient from simnet.client.components.auth import AuthClient
from simnet.client.components.chronicle.genshin import GenshinBattleChronicleClient from simnet.client.components.chronicle.genshin import GenshinBattleChronicleClient
from simnet.client.components.daily import DailyRewardClient from simnet.client.components.daily import DailyRewardClient
from simnet.client.components.diary.genshin import GenshinDiaryClient
from simnet.client.components.lab import LabClient from simnet.client.components.lab import LabClient
from simnet.client.components.wish.genshin import GenshinWishClient from simnet.client.components.wish.genshin import GenshinWishClient
from simnet.utils.enum_ import Game from simnet.utils.enum_ import Game
@ -13,6 +14,7 @@ __all__ = ("GenshinClient",)
class GenshinClient( class GenshinClient(
GenshinBattleChronicleClient, GenshinBattleChronicleClient,
GenshinWishClient, GenshinWishClient,
GenshinDiaryClient,
AuthClient, AuthClient,
DailyRewardClient, DailyRewardClient,
LabClient, LabClient,

View File

@ -3,13 +3,16 @@ from typing import Optional
from simnet.client.components.auth import AuthClient from simnet.client.components.auth import AuthClient
from simnet.client.components.chronicle.genshin import GenshinBattleChronicleClient from simnet.client.components.chronicle.genshin import GenshinBattleChronicleClient
from simnet.client.components.daily import DailyRewardClient from simnet.client.components.daily import DailyRewardClient
from simnet.client.components.diary.genshin import GenshinDiaryClient
from simnet.client.components.lab import LabClient from simnet.client.components.lab import LabClient
from simnet.client.components.wish.genshin import GenshinWishClient from simnet.client.components.wish.genshin import GenshinWishClient
from simnet.utils.enum_ import Region from simnet.utils.enum_ import Region
from simnet.utils.types import CookieTypes, HeaderTypes, TimeoutTypes from simnet.utils.types import CookieTypes, HeaderTypes, TimeoutTypes
class GenshinClient(GenshinBattleChronicleClient, GenshinWishClient, AuthClient, DailyRewardClient, LabClient): class GenshinClient(
GenshinBattleChronicleClient, GenshinWishClient, GenshinDiaryClient, AuthClient, DailyRewardClient, LabClient
):
def __init__( def __init__(
self, self,
cookies: Optional[CookieTypes] = None, cookies: Optional[CookieTypes] = None,

View File

@ -23,6 +23,8 @@ __all__ = (
"REWARD_URL", "REWARD_URL",
"TAKUMI_URL", "TAKUMI_URL",
"CALCULATOR_URL", "CALCULATOR_URL",
"DETAIL_LEDGER_URL",
"INFO_LEDGER_URL",
"HK4E_URL", "HK4E_URL",
"CODE_URL", "CODE_URL",
) )
@ -237,6 +239,22 @@ CALCULATOR_URL = InternationalRoute(
chinese="https://api-takumi.mihoyo.com/event/e20200928calculate/v1/", chinese="https://api-takumi.mihoyo.com/event/e20200928calculate/v1/",
) )
DETAIL_LEDGER_URL = InternationalRoute(
overseas="https://sg-hk4e-api.hoyolab.com/event/ysledgeros/month_detail",
chinese="https://hk4e-api.mihoyo.com/event/ys_ledger/monthDetail",
)
INFO_LEDGER_URL = GameRoute(
overseas=dict(
genshin="https://sg-hk4e-api.hoyolab.com/event/ysledgeros/month_info",
hkrpg="",
),
chinese=dict(
genshin="https://hk4e-api.mihoyo.com/event/ys_ledger/monthInfo",
hkrpg="https://api-takumi.mihoyo.com/event/srledger/month_info",
),
)
HK4E_URL = Route("https://sg-hk4e-api.hoyoverse.com/common/hk4e_global/") HK4E_URL = Route("https://sg-hk4e-api.hoyoverse.com/common/hk4e_global/")

View File

@ -3,6 +3,7 @@ from typing import Optional
from simnet.client.components.auth import AuthClient from simnet.client.components.auth import AuthClient
from simnet.client.components.chronicle.starrail import StarRailBattleChronicleClient from simnet.client.components.chronicle.starrail import StarRailBattleChronicleClient
from simnet.client.components.daily import DailyRewardClient from simnet.client.components.daily import DailyRewardClient
from simnet.client.components.diary.starrail import StarrailDiaryClient
from simnet.client.components.lab import LabClient from simnet.client.components.lab import LabClient
from simnet.client.components.wish.starrail import StarRailWishClient from simnet.client.components.wish.starrail import StarRailWishClient
from simnet.utils.enum_ import Game from simnet.utils.enum_ import Game
@ -13,6 +14,7 @@ __all__ = ("StarRailClient",)
class StarRailClient( class StarRailClient(
StarRailBattleChronicleClient, StarRailBattleChronicleClient,
StarRailWishClient, StarRailWishClient,
StarrailDiaryClient,
DailyRewardClient, DailyRewardClient,
AuthClient, AuthClient,
LabClient, LabClient,

View File

@ -3,13 +3,16 @@ from typing import Optional
from simnet.client.components.auth import AuthClient from simnet.client.components.auth import AuthClient
from simnet.client.components.chronicle.starrail import StarRailBattleChronicleClient from simnet.client.components.chronicle.starrail import StarRailBattleChronicleClient
from simnet.client.components.daily import DailyRewardClient from simnet.client.components.daily import DailyRewardClient
from simnet.client.components.diary.starrail import StarrailDiaryClient
from simnet.client.components.lab import LabClient from simnet.client.components.lab import LabClient
from simnet.client.components.wish.starrail import StarRailWishClient from simnet.client.components.wish.starrail import StarRailWishClient
from simnet.utils.enum_ import Region from simnet.utils.enum_ import Region
from simnet.utils.types import CookieTypes, HeaderTypes, TimeoutTypes from simnet.utils.types import CookieTypes, HeaderTypes, TimeoutTypes
class StarRailClient(StarRailBattleChronicleClient, StarRailWishClient,DailyRewardClient, AuthClient, LabClient): class StarRailClient(
StarRailBattleChronicleClient, StarRailWishClient, StarrailDiaryClient, DailyRewardClient, AuthClient, LabClient
):
def __init__( def __init__(
self, self,
cookies: Optional[CookieTypes] = None, cookies: Optional[CookieTypes] = None,

43
simnet/models/diary.py Normal file
View File

@ -0,0 +1,43 @@
from enum import IntEnum
from pydantic import Field
from simnet.models.base import APIModel
__all__ = (
"DiaryType",
"BaseDiary",
)
class DiaryType(IntEnum):
"""Type of diary pages.
0: Unknown
1: Primogems
2: Mora
"""
UNKNOWN = 0
PRIMOGEMS = 1
"""Primogems."""
MORA = 2
"""Mora."""
class BaseDiary(APIModel):
"""Base model for diary and diary page.
Attributes:
uid: User ID.
server: Server name.
nickname: User nickname.
month: Month of the diary page.
"""
uid: int
server: str = Field(alias="region")
nickname: str = ""
month: int = Field(alias="data_month")

View File

@ -0,0 +1,113 @@
from datetime import datetime
from typing import List
from pydantic import Field
from simnet.models.base import APIModel
from simnet.models.diary import BaseDiary
__all__ = (
"DayDiaryData",
"Diary",
"DiaryAction",
"DiaryActionCategory",
"DiaryPage",
"MonthDiaryData",
)
class DiaryActionCategory(APIModel):
"""Diary category for primogems.
Attributes:
id: Category ID.
name: Category name.
amount: Amount of primogems.
percentage: Percentage of primogems.
"""
id: int = Field(alias="action_id")
name: str = Field(alias="action")
amount: int = Field(alias="num")
percentage: int = Field(alias="percent")
class MonthDiaryData(APIModel):
"""Diary data for a month.
Attributes:
current_primogems: Current amount of primogems.
current_mora: Current amount of mora.
last_primogems: Last amount of primogems.
last_mora: Last amount of mora.
primogems_rate: Primogems rate.
mora_rate: Mora rate.
categories: List of diary categories.
"""
def __init__(self, **data):
if data.get("primogem_rate"):
data["primogems_rate"] = data.pop("primogem_rate")
super().__init__(**data)
current_primogems: int
current_mora: int
last_primogems: int
last_mora: int
primogems_rate: int
mora_rate: int
categories: List[DiaryActionCategory] = Field(alias="group_by")
class DayDiaryData(APIModel):
"""Diary data for a day.
Attributes:
current_primogems: Current amount of primogems.
current_mora: Current amount of mora.
"""
current_primogems: int
current_mora: int
class Diary(BaseDiary):
"""Traveler's diary.
Attributes:
data: Diary data for a month.
day_data: Diary data for a day.
"""
data: MonthDiaryData = Field(alias="month_data")
day_data: DayDiaryData
@property
def month_data(self) -> MonthDiaryData:
return self.data
class DiaryAction(APIModel):
"""Action which earned currency.
Attributes:
action_id: Action ID.
action: Action name.
time: Time of the action.
amount: Amount of the action.
"""
action_id: int
action: str
time: datetime = Field(timezone=8)
amount: int = Field(alias="num")
class DiaryPage(BaseDiary):
"""Page of a diary.
Attributes:
actions: List of diary actions.
"""
actions: List[DiaryAction] = Field(alias="list")

View File

@ -0,0 +1,84 @@
from typing import List
from pydantic import Field
from simnet.models.base import APIModel
from simnet.models.diary import BaseDiary
__all__ = (
"DiaryActionCategory",
"MonthDiaryData",
"DayDiaryData",
"StarRailDiary",
)
class DiaryActionCategory(APIModel):
"""Diary category for rails_pass .
Attributes:
id: Category ID.
name: Category name.
amount: Amount of rails_pass.
percentage: Percentage of rails_pass.
"""
id: str = Field(alias="action")
name: str = Field(alias="action_name")
amount: int = Field(alias="num")
percentage: int = Field(alias="percent")
class MonthDiaryData(APIModel):
"""Diary data for a month.
Attributes:
current_hcoin: Current amount of hcoin.
current_rails_pass: Current amount of rails_pass.
last_hcoin: Last amount of hcoin.
last_rails_pass: Last amount of rails_pass.
hcoin_rate: hcoin rate.
rails_rate: rails_pass rate.
categories: List of diary categories.
"""
current_hcoin: int
current_rails_pass: int
last_hcoin: int
last_rails_pass: int
hcoin_rate: int
rails_rate: int
categories: List[DiaryActionCategory] = Field(alias="group_by")
class DayDiaryData(APIModel):
"""Diary data for a day.
Attributes:
current_hcoin: Current amount of hcoin.
current_rails_pass: Current amount of rails_pass.
last_hcoin: Last amount of hcoin.
last_rails_pass: Last amount of rails_pass.
"""
current_hcoin: int
current_rails_pass: int
last_hcoin: int
last_rails_pass: int
class StarRailDiary(BaseDiary):
"""Traveler's diary.
Attributes:
data: Diary data for a month.
day_data: Diary data for a day.
"""
data: MonthDiaryData = Field(alias="month_data")
day_data: DayDiaryData
@property
def month_data(self) -> MonthDiaryData:
"""Diary data for a month."""
return self.data

View File

@ -0,0 +1,31 @@
from typing import TYPE_CHECKING
import pytest
import pytest_asyncio
from simnet.client.components.diary.genshin import GenshinDiaryClient
if TYPE_CHECKING:
from simnet.client.cookies import Cookies
from simnet.utils.enum_ import Region
@pytest_asyncio.fixture
async def diary_client(genshin_player_id: int, account_id: int, region: "Region", cookies: "Cookies"):
if genshin_player_id is None:
pytest.skip("Test case test_genshin_diary_client skipped: No genshin player id set.")
async with GenshinDiaryClient(
player_id=genshin_player_id,
cookies=cookies,
account_id=account_id,
region=region,
) as client_instance:
yield client_instance
@pytest.mark.asyncio
class TestGenshinDiaryClient:
@staticmethod
async def test_get_genshin_diary(diary_client: "GenshinDiaryClient"):
genshin_diary = await diary_client.get_genshin_diary()
assert genshin_diary is not None

View File

@ -0,0 +1,31 @@
from typing import TYPE_CHECKING
import pytest
import pytest_asyncio
from simnet.client.components.diary.starrail import StarrailDiaryClient
if TYPE_CHECKING:
from simnet.client.cookies import Cookies
from simnet.utils.enum_ import Region
@pytest_asyncio.fixture
async def diary_client(starrail_player_id: int, account_id: int, region: "Region", cookies: "Cookies"):
if starrail_player_id is None:
pytest.skip("Test case test_starrail_diary_client skipped: No starrail player id set.")
async with StarrailDiaryClient(
player_id=starrail_player_id,
cookies=cookies,
account_id=account_id,
region=region,
) as client_instance:
yield client_instance
@pytest.mark.asyncio
class TestStarrailDiaryClient:
@staticmethod
async def test_get_starrail_diary(diary_client: "StarrailDiaryClient"):
genshin_diary = await diary_client.get_starrail_diary()
assert genshin_diary is not None