2022-08-04 13:56:23 +00:00
|
|
|
from __future__ import annotations
|
2022-07-05 06:41:16 +00:00
|
|
|
|
2022-08-05 01:56:35 +00:00
|
|
|
import os
|
|
|
|
import json
|
2022-08-04 13:56:23 +00:00
|
|
|
import logging
|
2022-08-05 01:56:35 +00:00
|
|
|
|
2022-08-04 13:56:23 +00:00
|
|
|
from .http import HTTPClient
|
2023-02-06 14:58:58 +00:00
|
|
|
from .model.base import (
|
|
|
|
EnkaNetworkResponse,
|
|
|
|
EnkaNetworkProfileResponse
|
|
|
|
)
|
|
|
|
from .model.hoyos import PlayerHoyos
|
|
|
|
from .model.build import Builds
|
|
|
|
|
2022-07-16 19:35:08 +00:00
|
|
|
from .assets import Assets
|
2022-07-18 08:54:24 +00:00
|
|
|
from .enum import Language
|
2022-08-02 17:37:24 +00:00
|
|
|
from .cache import Cache
|
2023-02-06 14:58:58 +00:00
|
|
|
from .config import Config
|
2022-07-12 19:55:54 +00:00
|
|
|
|
2023-02-16 16:16:53 +00:00
|
|
|
from typing import Union, Optional, Type, TYPE_CHECKING, List, Any, Callable, Dict
|
2022-08-04 13:56:23 +00:00
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
from typing_extensions import Self
|
|
|
|
from types import TracebackType
|
|
|
|
|
|
|
|
__all__ = ("EnkaNetworkAPI",)
|
2022-07-29 04:31:21 +00:00
|
|
|
|
2022-08-04 13:56:23 +00:00
|
|
|
|
2023-02-06 14:58:58 +00:00
|
|
|
class EnkaNetworkAPI:
|
2023-02-07 08:22:05 +00:00
|
|
|
""" A library for API wrapper player by UID / Username from https://enka.network
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
------------
|
|
|
|
lang: :class:`str`
|
|
|
|
Init default language
|
|
|
|
debug: :class:`bool`
|
|
|
|
If set to `True`. In request data or get assets. It's will be shown log processing
|
|
|
|
key: :class:`str`
|
|
|
|
Depercated
|
|
|
|
cache: :class:`bool`
|
|
|
|
If set to `True`. In response data will be cache data
|
|
|
|
user_agent: :class:`str`
|
|
|
|
User-Agent for speical to request Enka.Network
|
|
|
|
timeout: :class:`int`
|
|
|
|
Request timeout to Enka.Network
|
|
|
|
|
|
|
|
Attributes
|
|
|
|
------------
|
|
|
|
assets: :class:`Assets`
|
|
|
|
Assets character / artifact / namecards / language / etc. data
|
|
|
|
http: :class:`HTTPClient`
|
|
|
|
HTTP for request and handle data
|
|
|
|
lang: :class:`Language`
|
|
|
|
A default language
|
|
|
|
|
|
|
|
Example
|
|
|
|
------------
|
|
|
|
```py
|
|
|
|
import asyncio
|
|
|
|
|
|
|
|
from enkanetwork import EnkaNetworkAPI
|
|
|
|
|
|
|
|
client = EnkaNetworkAPI(lang="th",user_agent="SpeicalAgent/1.0")
|
|
|
|
|
|
|
|
async def main():
|
|
|
|
async with client:
|
|
|
|
data = await client.fetch_user(843715177)
|
|
|
|
print(data.player.nickname)
|
|
|
|
|
|
|
|
asyncio.run(main())
|
|
|
|
```
|
|
|
|
"""
|
2022-07-05 08:46:47 +00:00
|
|
|
LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2023-02-07 08:22:05 +00:00
|
|
|
def __init__(self, *, lang: str = "en", debug: bool = False, key: str = "", cache: bool = True, user_agent: str = "", timeout: int = 10) -> None: # noqa: E501
|
2022-07-05 08:46:47 +00:00
|
|
|
# Logging
|
|
|
|
logging.basicConfig()
|
2022-07-29 04:31:21 +00:00
|
|
|
logging.getLogger("enkanetwork").setLevel(logging.DEBUG if debug else logging.ERROR) # noqa: E501
|
2022-06-22 06:14:31 +00:00
|
|
|
|
2022-07-05 06:41:16 +00:00
|
|
|
# Set language and load config
|
2022-07-16 19:35:08 +00:00
|
|
|
self.assets = Assets(lang)
|
2022-07-12 19:55:54 +00:00
|
|
|
|
2022-08-02 17:37:24 +00:00
|
|
|
# Cache
|
|
|
|
self._enable_cache = cache
|
2023-02-06 14:58:58 +00:00
|
|
|
if self._enable_cache:
|
2023-02-07 08:22:05 +00:00
|
|
|
Config.init_cache(Cache(1024, 60 * 1))
|
2022-08-02 17:37:24 +00:00
|
|
|
|
2022-08-04 13:56:23 +00:00
|
|
|
# http client
|
2023-02-06 14:58:58 +00:00
|
|
|
self.__http = HTTPClient(key=key, agent=user_agent, timeout=timeout)
|
2022-08-04 13:56:23 +00:00
|
|
|
self._closed = False
|
2022-08-03 07:50:43 +00:00
|
|
|
|
2022-08-04 13:56:23 +00:00
|
|
|
async def __aenter__(self) -> Self:
|
|
|
|
return self
|
|
|
|
|
|
|
|
async def __aexit__(
|
|
|
|
self,
|
|
|
|
exc_type: Optional[Type[BaseException]],
|
|
|
|
exc_value: Optional[BaseException],
|
|
|
|
traceback: Optional[TracebackType],
|
|
|
|
) -> None:
|
|
|
|
self._close = True
|
|
|
|
if self._close:
|
|
|
|
await self.__http.close()
|
|
|
|
|
|
|
|
def is_closed(self) -> bool:
|
|
|
|
return self._closed
|
2022-06-22 06:14:31 +00:00
|
|
|
|
2022-08-22 09:39:55 +00:00
|
|
|
@property
|
|
|
|
def http(self) -> HTTPClient:
|
|
|
|
return self.__http
|
|
|
|
|
|
|
|
@http.setter
|
|
|
|
def http(self, http: HTTPClient) -> None:
|
|
|
|
self.__http = http
|
|
|
|
|
2022-07-18 08:54:24 +00:00
|
|
|
@property
|
|
|
|
def lang(self) -> Language:
|
|
|
|
return self.assets.LANGS
|
|
|
|
|
|
|
|
@lang.setter
|
|
|
|
def lang(self, lang: Language) -> None:
|
|
|
|
self.assets._set_language(lang)
|
|
|
|
|
2022-08-02 17:37:24 +00:00
|
|
|
def set_cache(self, cache: Cache) -> None:
|
2023-02-06 14:58:58 +00:00
|
|
|
Config.init_cache(cache)
|
2022-08-02 17:37:24 +00:00
|
|
|
|
2022-07-18 08:54:24 +00:00
|
|
|
async def set_language(self, lang: Language) -> None:
|
|
|
|
self.lang = lang
|
|
|
|
|
2023-02-06 14:58:58 +00:00
|
|
|
async def fetch_user_by_uid(
|
|
|
|
self,
|
|
|
|
uid: Union[str, int],
|
|
|
|
*,
|
|
|
|
info: bool = False
|
|
|
|
) -> EnkaNetworkResponse:
|
2023-02-07 08:22:05 +00:00
|
|
|
""" Fetch user profile by UID
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
------------
|
|
|
|
uid: Union[:class:`str`,:class:`int`]
|
|
|
|
UID player in-game
|
|
|
|
info: :class:`bool`
|
|
|
|
If set to `True`. It's will response player info only
|
|
|
|
|
|
|
|
Raises
|
|
|
|
------------
|
|
|
|
VaildateUIDError
|
|
|
|
Player UID empty/format has incorrect.
|
|
|
|
EnkaPlayerNotFound
|
|
|
|
Player UID doesn't not exists in-game
|
|
|
|
EnkaServerRateLimit
|
|
|
|
Enka.Network has been rate limit
|
|
|
|
EnkaServerMaintanance
|
|
|
|
Enka.Network has maintenance server
|
|
|
|
EnkaServerError
|
|
|
|
Enka.Network has server error (The reason normal is `general`)
|
|
|
|
EnkaServerUnknown
|
|
|
|
Enka.Network has error another
|
|
|
|
|
|
|
|
Returns
|
|
|
|
------------
|
|
|
|
:class:`EnkaNetworkResponse`
|
|
|
|
The response player data
|
|
|
|
"""
|
|
|
|
func = self.__http.fetch_user_by_uid(uid, info=info)
|
|
|
|
data = await self.request_enka(func, uid)
|
2022-07-12 19:55:54 +00:00
|
|
|
# Return data
|
|
|
|
self.LOGGER.debug("Parsing data...")
|
2023-02-06 14:58:58 +00:00
|
|
|
if "owner" in data:
|
|
|
|
data["owner"] = {
|
|
|
|
**data["owner"],
|
|
|
|
"builds": await self.fetch_builds(
|
2023-02-07 08:22:05 +00:00
|
|
|
profile_id=data["owner"]["username"],
|
2023-02-06 14:58:58 +00:00
|
|
|
metaname=data["owner"]["hash"]
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-07-12 19:55:54 +00:00
|
|
|
return EnkaNetworkResponse.parse_obj(data)
|
2022-06-22 06:14:31 +00:00
|
|
|
|
2023-02-06 14:58:58 +00:00
|
|
|
async def fetch_user_by_username(
|
|
|
|
self,
|
2023-02-07 08:22:05 +00:00
|
|
|
profile_id: Optional[str]
|
2023-02-06 14:58:58 +00:00
|
|
|
) -> EnkaNetworkProfileResponse:
|
2023-02-07 08:22:05 +00:00
|
|
|
""" Fetch user profile by Username / patreon ID
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
------------
|
|
|
|
profile_id: Optional[:class:`str`]
|
|
|
|
Username / patreon ID has subscriptions in Enka.Network
|
|
|
|
|
|
|
|
Raises
|
|
|
|
------------
|
|
|
|
EnkaPlayerNotFound
|
|
|
|
Player UID doesn't not exists in-game
|
|
|
|
EnkaServerRateLimit
|
|
|
|
Enka.Network has been rate limit
|
|
|
|
EnkaServerMaintanance
|
|
|
|
Enka.Network has maintenance server
|
|
|
|
EnkaServerError
|
|
|
|
Enka.Network has server error (The reason normal is `general`)
|
|
|
|
EnkaServerUnknown
|
|
|
|
Enka.Network has error another
|
|
|
|
|
|
|
|
Returns
|
|
|
|
------------
|
|
|
|
:class:`EnkaNetworkProfileResponse`
|
|
|
|
The response profile / hoyos and builds data
|
|
|
|
"""
|
|
|
|
func = self.__http.fetch_user_by_username(profile_id)
|
|
|
|
data = await self.request_enka(func, profile_id)
|
2023-02-06 14:58:58 +00:00
|
|
|
self.LOGGER.debug("Parsing data...")
|
2023-02-07 08:22:05 +00:00
|
|
|
|
2023-02-06 14:58:58 +00:00
|
|
|
return EnkaNetworkProfileResponse.parse_obj({
|
|
|
|
**data,
|
|
|
|
"hoyos": await self.fetch_hoyos_by_username(profile_id)
|
|
|
|
})
|
|
|
|
|
|
|
|
async def fetch_hoyos_by_username(
|
|
|
|
self,
|
2023-02-07 08:22:05 +00:00
|
|
|
profile_id: Optional[str]
|
2023-02-06 14:58:58 +00:00
|
|
|
) -> List[PlayerHoyos]:
|
2023-02-07 08:22:05 +00:00
|
|
|
""" Fetch hoyos user data by Username / patreon ID
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
------------
|
|
|
|
profile_id: Optional[:class:`str`]
|
|
|
|
Username / patreon ID has subscriptions in Enka.Network
|
|
|
|
|
|
|
|
Raises
|
|
|
|
------------
|
|
|
|
EnkaPlayerNotFound
|
|
|
|
Player UID doesn't not exists in-game
|
|
|
|
EnkaServerRateLimit
|
|
|
|
Enka.Network has been rate limit
|
|
|
|
EnkaServerMaintanance
|
|
|
|
Enka.Network has maintenance server
|
|
|
|
EnkaServerError
|
|
|
|
Enka.Network has server error (The reason normal is `general`)
|
|
|
|
EnkaServerUnknown
|
|
|
|
Enka.Network has error another
|
|
|
|
|
|
|
|
Returns
|
|
|
|
------------
|
2023-02-16 05:51:58 +00:00
|
|
|
List[:class:`PlayerHoyos`
|
2023-02-07 08:22:05 +00:00
|
|
|
A response hoyos player data
|
|
|
|
"""
|
2023-02-06 14:58:58 +00:00
|
|
|
key = profile_id + ":hoyos"
|
2023-02-07 09:30:36 +00:00
|
|
|
func = self.__http.fetch_hoyos_by_username(profile_id)
|
|
|
|
data = await self.request_enka(func, key)
|
2023-02-06 14:58:58 +00:00
|
|
|
self.LOGGER.debug("Parsing data...")
|
2023-02-07 08:22:05 +00:00
|
|
|
|
2023-02-06 14:58:58 +00:00
|
|
|
return await self.__format_hoyos(profile_id, data)
|
|
|
|
|
|
|
|
async def fetch_builds(
|
|
|
|
self,
|
|
|
|
*,
|
2023-02-07 08:22:05 +00:00
|
|
|
profile_id: Optional[str],
|
|
|
|
metaname: Optional[str]
|
|
|
|
) -> Builds:
|
|
|
|
""" Fetch hoyos build(s) data
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
------------
|
|
|
|
profile_id: Optional[:class:`str`]
|
|
|
|
Username / patreon ID has subscriptions in Enka.Network
|
|
|
|
metaname: Optional[:class:`str`]
|
|
|
|
Metaname from hoyos data or owner tag in hash field
|
|
|
|
|
|
|
|
Raises
|
|
|
|
------------
|
|
|
|
EnkaPlayerNotFound
|
|
|
|
Player UID doesn't not exists in-game
|
|
|
|
EnkaServerRateLimit
|
|
|
|
Enka.Network has been rate limit
|
|
|
|
EnkaServerMaintanance
|
|
|
|
Enka.Network has maintenance server
|
|
|
|
EnkaServerError
|
|
|
|
Enka.Network has server error (The reason normal is `general`)
|
|
|
|
EnkaServerUnknown
|
|
|
|
Enka.Network has error another
|
|
|
|
|
|
|
|
Returns
|
|
|
|
------------
|
|
|
|
:class:`Builds`
|
|
|
|
A response builds data
|
|
|
|
"""
|
2023-02-06 14:58:58 +00:00
|
|
|
key = profile_id + ":hoyos:" + metaname + ":builds"
|
2023-02-07 08:22:05 +00:00
|
|
|
func = self.__http.fetch_hoyos_by_username(profile_id, metaname, True)
|
|
|
|
data = await self.request_enka(func, key)
|
|
|
|
self.LOGGER.debug("Parsing data...")
|
|
|
|
|
|
|
|
return Builds.parse_obj(data)
|
|
|
|
|
|
|
|
async def request_enka(
|
|
|
|
self,
|
|
|
|
func: Callable,
|
|
|
|
cache_key: str,
|
|
|
|
):
|
|
|
|
key = cache_key
|
2023-02-06 14:58:58 +00:00
|
|
|
# Check config
|
|
|
|
if Config.CACHE_ENABLED:
|
2023-02-07 08:22:05 +00:00
|
|
|
self.LOGGER.warning(f"Getting data {key} from cache...")
|
2023-02-06 14:58:58 +00:00
|
|
|
data = await Config.CACHE.get(key)
|
|
|
|
|
|
|
|
if data is not None:
|
|
|
|
self.LOGGER.debug("Parsing data...")
|
2023-02-07 08:22:05 +00:00
|
|
|
return data
|
2023-02-06 14:58:58 +00:00
|
|
|
|
2023-02-07 08:22:05 +00:00
|
|
|
user = await func
|
2023-02-06 14:58:58 +00:00
|
|
|
data = user["content"]
|
|
|
|
data = json.loads(data)
|
|
|
|
|
|
|
|
if Config.CACHE_ENABLED:
|
2023-02-07 08:22:05 +00:00
|
|
|
self.LOGGER.debug(f"Caching data {key}...")
|
2023-02-06 14:58:58 +00:00
|
|
|
await Config.CACHE.set(key, data)
|
|
|
|
|
2023-02-07 08:22:05 +00:00
|
|
|
return data
|
2023-02-06 14:58:58 +00:00
|
|
|
|
2022-07-17 06:08:16 +00:00
|
|
|
async def update_assets(self) -> None:
|
2022-08-04 13:56:23 +00:00
|
|
|
print("Updating assets...")
|
2022-07-12 19:55:54 +00:00
|
|
|
self.LOGGER.debug("Downloading new content...")
|
2022-08-04 13:56:23 +00:00
|
|
|
|
|
|
|
path = Assets._get_path_assets()
|
2022-08-05 01:56:35 +00:00
|
|
|
for folder in path:
|
|
|
|
for filename in os.listdir(path[folder]):
|
|
|
|
self.LOGGER.debug(f"Downloading {folder} file {filename}...")
|
|
|
|
|
2022-08-05 02:21:21 +00:00
|
|
|
data = await self.__http.fetch_asset(folder, filename)
|
2022-08-05 01:56:35 +00:00
|
|
|
|
|
|
|
self.LOGGER.debug(f"Writing {folder} file {filename}...")
|
2022-08-04 13:56:23 +00:00
|
|
|
|
2022-08-05 01:56:35 +00:00
|
|
|
# dumps to json file
|
|
|
|
with open(os.path.join(path[folder], filename), "w", encoding="utf-8") as f:
|
2023-02-06 14:58:58 +00:00
|
|
|
json.dump(json.loads(data["content"]),
|
|
|
|
f, ensure_ascii=False, indent=4)
|
2022-06-22 06:14:31 +00:00
|
|
|
|
2022-07-17 06:08:16 +00:00
|
|
|
# Reload config
|
2022-07-29 04:31:21 +00:00
|
|
|
self.assets.reload_assets()
|
2023-02-07 08:22:05 +00:00
|
|
|
|
2023-02-06 14:58:58 +00:00
|
|
|
async def __format_hoyos(self, username: str, data: List[Any]) -> List[PlayerHoyos]:
|
|
|
|
return [PlayerHoyos.parse_obj({
|
|
|
|
"builds": await self.fetch_builds(profile_id=username, metaname=data[key]["hash"]),
|
|
|
|
**data[key]
|
|
|
|
}) for key in data]
|
|
|
|
|
2023-02-16 16:16:53 +00:00
|
|
|
async def fetch_raw_data(self, uid: int) -> Dict[str, Any]:
|
|
|
|
"""Fetches raw data for a user with the given UID.
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
uid: The UID of the user to fetch data for.
|
|
|
|
|
|
|
|
Returns
|
|
|
|
------
|
|
|
|
A dictionary containing the raw data for the user.
|
|
|
|
"""
|
|
|
|
func = self.__http.fetch_user_by_uid(uid)
|
|
|
|
data = await self.request_enka(func, uid)
|
|
|
|
return data
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
async def combineCacheData(
|
|
|
|
new_data: Dict[str, Any], cache_data: Dict[str, Any]
|
|
|
|
) -> Dict[str, Any]:
|
|
|
|
"""
|
|
|
|
Merge cached data into newly fetched data.
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
new_data: The newly fetched data as a dictionary.
|
|
|
|
cache_data: The cached data as a dictionary.
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
A dictionary containing the merged data.
|
|
|
|
"""
|
|
|
|
|
|
|
|
async def combine_lists(
|
|
|
|
new_list: List[Dict[str, Any]], cache_list: List[Dict[str, Any]]
|
|
|
|
):
|
|
|
|
new_ids = {item["avatarId"] for item in new_list}
|
|
|
|
unique_cache_items = [
|
|
|
|
item for item in cache_list if item["avatarId"] not in new_ids
|
|
|
|
]
|
|
|
|
new_list.extend(unique_cache_items)
|
|
|
|
|
|
|
|
if "showAvatarInfoList" in cache_data["playerInfo"]:
|
|
|
|
new_data.setdefault("playerInfo", {}).setdefault("showAvatarInfoList", [])
|
|
|
|
await combine_lists(
|
|
|
|
new_data["playerInfo"]["showAvatarInfoList"],
|
|
|
|
cache_data["playerInfo"]["showAvatarInfoList"],
|
|
|
|
)
|
|
|
|
|
|
|
|
if "avatarInfoList" in cache_data:
|
|
|
|
new_data.setdefault("avatarInfoList", [])
|
|
|
|
await combine_lists(
|
|
|
|
new_data["avatarInfoList"], cache_data["avatarInfoList"]
|
|
|
|
)
|
|
|
|
|
|
|
|
return new_data
|
|
|
|
|
2023-02-06 14:58:58 +00:00
|
|
|
# Concept by genshin.py python library
|
|
|
|
fetch_user = fetch_user_by_uid
|
|
|
|
fetch_profile = fetch_user_by_username
|