diff --git a/enkanetwork/client.py b/enkanetwork/client.py index 82e2f2d..e48f51f 100644 --- a/enkanetwork/client.py +++ b/enkanetwork/client.py @@ -17,7 +17,7 @@ from .enum import Language from .cache import Cache from .config import Config -from typing import Union, Optional, Type, TYPE_CHECKING, List, Any +from typing import Union, Optional, Type, TYPE_CHECKING, List, Any, Callable if TYPE_CHECKING: from typing_extensions import Self @@ -27,9 +27,52 @@ __all__ = ("EnkaNetworkAPI",) class EnkaNetworkAPI: + """ 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()) + ``` + """ LOGGER = logging.getLogger(__name__) - def __init__(self, lang: str = "en", *, debug: bool = False, key: str = "", cache: bool = True, user_agent: str = "", timeout: int = 10) -> None: # noqa: E501 + def __init__(self, *, lang: str = "en", debug: bool = False, key: str = "", cache: bool = True, user_agent: str = "", timeout: int = 10) -> None: # noqa: E501 # Logging logging.basicConfig() logging.getLogger("enkanetwork").setLevel(logging.DEBUG if debug else logging.ERROR) # noqa: E501 @@ -40,7 +83,7 @@ class EnkaNetworkAPI: # Cache self._enable_cache = cache if self._enable_cache: - Config.init_cache(Cache(1024, 60 * 3)) + Config.init_cache(Cache(1024, 60 * 1)) # http client self.__http = HTTPClient(key=key, agent=user_agent, timeout=timeout) @@ -90,32 +133,45 @@ class EnkaNetworkAPI: *, info: bool = False ) -> EnkaNetworkResponse: - if self._enable_cache: - self.LOGGER.warning("Getting data from cache...") - data = await Config.CACHE.get(uid) + """ Fetch user profile by UID - if data is not None: - # Return data - self.LOGGER.debug("Parsing data...") - return EnkaNetworkResponse.parse_obj(data) + 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 - self.LOGGER.debug(f"Fetching user with UID {uid}...") - user = await self.__http.fetch_user_by_uid(uid, info=info) - - data = user["content"] - data = json.loads(data) - - if self._enable_cache: - self.LOGGER.debug("Caching data...") - await Config.CACHE.set(uid, data) + 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) + self.LOGGER.debug("Parsing data...") # Return data self.LOGGER.debug("Parsing data...") if "owner" in data: data["owner"] = { **data["owner"], "builds": await self.fetch_builds( - profile_id=data["owner"]["username"], + profile_id=data["owner"]["username"], metaname=data["owner"]["hash"] ) } @@ -124,29 +180,37 @@ class EnkaNetworkAPI: async def fetch_user_by_username( self, - profile_id: Union[str, int] + profile_id: Optional[str] ) -> EnkaNetworkProfileResponse: - if self._enable_cache: - self.LOGGER.warning("Getting data from cache...") - data = await Config.CACHE.get(profile_id) + """ Fetch user profile by Username / patreon ID - if data is not None: - # Return data - self.LOGGER.debug("Parsing data...") - return EnkaNetworkProfileResponse.parse_obj(data) + Parameters + ------------ + profile_id: Optional[:class:`str`] + Username / patreon ID has subscriptions in Enka.Network - self.LOGGER.debug(f"Fetching user with profile {profile_id}...") + 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 - user = await self.__http.fetch_user_by_username(profile_id) - data = user["content"] - data = json.loads(data) - - if self._enable_cache: - self.LOGGER.debug("Caching data...") - await Config.CACHE.set(profile_id, data) - - # Return data + 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) self.LOGGER.debug("Parsing data...") + return EnkaNetworkProfileResponse.parse_obj({ **data, "hoyos": await self.fetch_hoyos_by_username(profile_id) @@ -154,60 +218,104 @@ class EnkaNetworkAPI: async def fetch_hoyos_by_username( self, - profile_id: Union[str, int] + profile_id: Optional[str] ) -> List[PlayerHoyos]: + """ 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 + ------------ + List[:class:`PlayerHoyos`] + A response hoyos player data + """ key = profile_id + ":hoyos" - # Check config - if Config.CACHE_ENABLED: - self.LOGGER.warning("Getting data from cache...") - data = await Config.CACHE.get(key) - - if data is not None: - self.LOGGER.debug("Parsing data...") - return await self.__format_hoyos(profile_id, data) - - self.LOGGER.debug(f"Fetching user hoyos with profile {profile_id}...") - user = await self.__http.fetch_hoyos_by_username(profile_id) - data = user["content"] - data = json.loads(data) - - if Config.CACHE_ENABLED: - self.LOGGER.debug("Caching data...") - await Config.CACHE.set(key, data) - + func = self.__http.fetch_hoyos_by_username(key) + data = await self.request_enka(func, profile_id) self.LOGGER.debug("Parsing data...") + return await self.__format_hoyos(profile_id, data) async def fetch_builds( self, *, - profile_id: Union[str, int], - metaname: Union[str, int] - ) -> PlayerHoyos: + 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 + """ key = profile_id + ":hoyos:" + metaname + ":builds" + 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 # Check config if Config.CACHE_ENABLED: - self.LOGGER.warning("Getting data from cache...") + self.LOGGER.warning(f"Getting data {key} from cache...") data = await Config.CACHE.get(key) if data is not None: self.LOGGER.debug("Parsing data...") - return await self.__format_hoyos(profile_id, data) + return data - # Request data first - user = await self.__http.fetch_hoyos_by_username(profile_id,metaname,True) + user = await func data = user["content"] data = json.loads(data) if Config.CACHE_ENABLED: - self.LOGGER.debug("Caching data...") + self.LOGGER.debug(f"Caching data {key}...") await Config.CACHE.set(key, data) - self.LOGGER.debug("Parsing data...") - return Builds.parse_obj(data) - - async def request_enka(): - pass + return data async def update_assets(self) -> None: print("Updating assets...") @@ -229,7 +337,7 @@ class EnkaNetworkAPI: # Reload config self.assets.reload_assets() - + 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"]), diff --git a/enkanetwork/config.py b/enkanetwork/config.py index 18cc1f2..6c7f5af 100644 --- a/enkanetwork/config.py +++ b/enkanetwork/config.py @@ -7,7 +7,7 @@ from .cache import Cache class Config: # HTTP Config ENKA_PROTOCOL: ClassVar[str] = "https" - ENKA_URL: ClassVar[str] = "dev.enka.network" + ENKA_URL: ClassVar[str] = "enka.network" # Assets ASSETS_PROTOCOL: ClassVar[str] = "https" ASSETS_URL: ClassVar[str] = "raw.githubusercontent.com" diff --git a/enkanetwork/exception.py b/enkanetwork/exception.py index a883174..cb26663 100644 --- a/enkanetwork/exception.py +++ b/enkanetwork/exception.py @@ -13,7 +13,7 @@ class HTTPException(Exception): class EnkaValidateFailed(HTTPException): """ Exception that's raised for when status code 400 occurs.""" -class EnkaUIDNotFound(Exception): +class EnkaPlayerNotFound(Exception): """ Raised when the UID is not found. """ class EnkaServerError(HTTPException): @@ -30,7 +30,7 @@ class EnkaServerUnknown(HTTPException): ERROR_ENKA = { 400: [VaildateUIDError, "Validate UID {uid} failed."], - 404: [EnkaUIDNotFound, "UID {uid} not found. Please check your UID"], + 404: [EnkaPlayerNotFound, "Player ID {uid} not found. Please check your UID / Username"], 429: [EnkaServerRateLimit, "Enka.network has been rate limit this path"], 424: [EnkaServerMaintanance, "Enka.Network doing maintenance server. Please wait took 5-8 hours or 1 day"], 500: [EnkaServerError, "Enka.network server has down or Genshin server broken."], diff --git a/enkanetwork/http.py b/enkanetwork/http.py index 95df854..d6dbca5 100644 --- a/enkanetwork/http.py +++ b/enkanetwork/http.py @@ -124,28 +124,13 @@ class HTTPClient: _host = response.host if 300 > response.status >= 200: data = await utils.to_data(response) - - # if not data['content'] or response.status != 200: - # raise UIDNotFounded(f"UID {username} not found.") - + self.LOGGER.debug('%s %s has received %s', method, url, data) return data if _host == Config.ENKA_URL: err = ERROR_ENKA[response.status] raise err[0](err[1].format(uid=username)) - # if response.status == 500: # UID not found or Genshin broken - # raise UIDNotFounded(f"UID {username} not found or Genshin server broken.") - - # if response.status == 404: # Profile UID not found - # raise ProfileNotFounded(f"Profile {username} not found. Please check username has subscription?") - - # if response.status == 424: - # raise EnkaServerMaintanance("Enka.Network doing maintenance server. Please wait took 5-8 hours or 1 day") - - # we are being rate limited - # if response.status == 429: - # Banned by Cloudflare more than likely. if response.status >= 400: self.LOGGER.warning(f"Failure to fetch {url} ({response.status}) Retry {tries} / {RETRY_MAX}") @@ -221,7 +206,7 @@ class HTTPClient: def fetch_asset(self, folder: str, filename: str) -> Response[DefaultPayload]: r = Route( 'GET', - f'/master/exports/{folder}/{filename}', + f'/mrwan200/enkanetwork.py-data/master/exports/{folder}/{filename}', endpoint='assets' ) return self.request(r) diff --git a/enkanetwork/model/assets.py b/enkanetwork/model/assets.py index 9685ed9..1c4bcf9 100644 --- a/enkanetwork/model/assets.py +++ b/enkanetwork/model/assets.py @@ -13,7 +13,24 @@ __all__ = ( 'CharacterAsset' ) + class NamecardAsset(BaseModel): + """ Namecards (Assets) + + Attributes + ------------ + id: :class:`int` + Namecard ID + hash_id: :class:`str` + Namecard hash id + icon: :class:`IconAsset` + A icon assets. Please refers in `IconAsset` class + banner: :class:`IconAsset` + A banner assets. Please refers in `IconAsset` class + navbar: :class:`IconAsset` + A navbar assets. Please refers in `IconAsset` class + """ + id: int = 0 hash_id: str = Field("", alias="nameTextMapHash") icon: IconAsset @@ -22,13 +39,39 @@ class NamecardAsset(BaseModel): class CharacterIconAsset(BaseModel): + """ Character Icon (Assets) + + Attributes + ------------ + icon: :class:`IconAsset` + A icon assets. Please refers in `IconAsset` class + side: :class:`IconAsset` + A navbar assets. Please refers in `IconAsset` class + banner: :class:`IconAsset` + A banner assets. Please refers in `IconAsset` class + card: :class:`IconAsset` + A navbar assets. Please refers in `IconAsset` class + """ icon: IconAsset - side: IconAsset + side: IconAsset banner: IconAsset card: IconAsset class CharacterSkillAsset(BaseModel): + """ Character Skill(s) (Assets) + + Attributes + ------------ + id: :class:`int` + Character skill(s) ID + pround_map: :class:`int` + pround map for a booest skill by constellation + hash_id: :class:`str` + Skill(s) hash id + icon: :class:`IconAsset` + A icon assets. Please refers in `IconAsset` class + """ id: int = 0, pround_map: int = 0, hash_id: str = Field("", alias="nameTextMapHash") @@ -36,16 +79,60 @@ class CharacterSkillAsset(BaseModel): class CharacterConstellationsAsset(BaseModel): + """ Character Constellations (Assets) + + Attributes + ------------ + id: :class:`int` + Character constellations ID + hash_id: :class:`str` + Constellations hash id + icon: :class:`IconAsset` + A icon assets. Please refers in `IconAsset` class + """ id: int = 0 hash_id: str = Field("", alias="nameTextMapHash") icon: IconAsset = None + class CharacterCostume(BaseModel): + """ Character Costume (Assets) + + Attributes + ------------ + id: :class:`int` + Costume ID + hash_id: :class:`str` + Costume hash id + icon: :class:`IconAsset` + A icon assets. Please refers in `IconAsset` class + """ id: int = 0 images: CharacterIconAsset = None class CharacterAsset(BaseModel): + """ Character (Assets) + + Attributes + ------------ + id: :class:`int` + Avatar ID + rarity: :class:`int` + Character rarity (5 stars or 4stars) + hash_id: :class:`str` + Character hash id + element: :class:`ElementType` + Character element type + images: :class:`CharacterIconAsset` + Character image data. Please refers in `CharacterIconAsset` + skill_id: :class:`int` + Character skill ID + skills: List[:class:`int`] + Character skill data list + constellations: List[:class:`int`] + Character constellations data list + """ id: int = 0 rarity: int = 0 hash_id: str = Field("", alias="nameTextMapHash") @@ -61,5 +148,6 @@ class CharacterAsset(BaseModel): def __init__(self, **data: Any) -> None: super().__init__(**data) - self.element = ElementType(data["costElemType"]) if data["costElemType"] != "" else ElementType.Unknown + self.element = ElementType( + data["costElemType"]) if data["costElemType"] != "" else ElementType.Unknown self.rarity = 5 if data["qualityType"].endswith("_ORANGE") else 4