diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 4581c3a..09c6d9e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.11"] + python-version: ["3.11", "3.10", "3.9", "3.8"] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 2ca1c84..90db390 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.11"] + python-version: ["3.11", "3.10", "3.9", "3.8"] steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index 7947f2e..fc5f792 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,8 @@ good as I can. ### Planned features - more bulk requests -- pytests for every request - pytests for the models -- possibility to download files (images, etc) from `api-assets.clashofclans.com` +- events ### Planned utils @@ -74,6 +73,7 @@ good as I can. - attributes `king`, `queen`, `warden`, `royal_champion` for the `Player.heroes` attribute - autosort for the `ClanCurrentWarRequest` members of the `member_list` attribute (sorted by the map position) - events and an `EventClient` + --- If you find a bug, an error or want custom functionality, please tell diff --git a/pyclasher/__init__.py b/pyclasher/__init__.py index c628983..25728f8 100644 --- a/pyclasher/__init__.py +++ b/pyclasher/__init__.py @@ -22,7 +22,7 @@ # exceptions.py from .exceptions import ( - Missing, MISSING, PyClasherException, ApiCode, RequestNotDone, NoneToken, + Missing, MISSING, PyClasherException, RequestNotDone, NoneToken, InvalidLoginData, InvalidType, LoginNotDone, ClientIsRunning, ClientIsNotRunning, ClientAlreadyInitialised, NoClient, InvalidTimeFormat, ClientRunningOverwrite, InvalidSeasonFormat, diff --git a/pyclasher/api/models/__init__.py b/pyclasher/api/models/__init__.py index 771db0f..a3a74e8 100644 --- a/pyclasher/api/models/__init__.py +++ b/pyclasher/api/models/__init__.py @@ -40,7 +40,7 @@ ) from .clan_war_log import ClanWarLogEntry, ClanWarLog from .enums import ( - ApiCodes, ClanType, WarFrequency, Locations, Leagues, CapitalLeagues, + ClanType, WarFrequency, Locations, Leagues, CapitalLeagues, BuilderBaseLeagues, WarLeagues, Labels, Languages, ClanWarState, ClanWarLeagueGroupState, ClanWarResult, WarPreference, PlayerHouseElementType, Village, TokenStatus, ClanRole diff --git a/pyclasher/api/models/base_models.py b/pyclasher/api/models/base_models.py index 6ab8327..4a00721 100644 --- a/pyclasher/api/models/base_models.py +++ b/pyclasher/api/models/base_models.py @@ -1,18 +1,27 @@ from datetime import datetime +from aiohttp import ClientSession + from .abc import BaseModel -from ...exceptions import InvalidTimeFormat +from ...exceptions import InvalidTimeFormat, MISSING class ImageUrl: - __url = None - def __init__(self, url): self.__url = url return - async def get_image(self): - raise NotImplementedError + async def get_image(self, logger=MISSING) -> bytes: + async with ClientSession() as session: + async with session.get(self.url) as request: + if request.status == 200: + logger.info(f"Successfully downloaded {self.url}") + return await request.read() + + async def save_image(self, filename, logger=MISSING): + image = await self.get_image(logger) + with open(filename, "wb") as file: + file.write(image) @property def url(self): diff --git a/pyclasher/api/models/base_models.pyi b/pyclasher/api/models/base_models.pyi index 3b5a2a5..d9d4369 100644 --- a/pyclasher/api/models/base_models.pyi +++ b/pyclasher/api/models/base_models.pyi @@ -1,6 +1,7 @@ """ base models for this API wrapper client """ +from logging import Logger from .abc import BaseModel from ...exceptions import MISSING, Missing @@ -10,19 +11,20 @@ class ImageUrl: """ image URL model - :cvar __url: URL of the image + :ivar __url: URL of the image :type __url: str """ - __url: str = None - def __init__(self, url: str) -> None: """ initialisation of the image url model """ self.__url = url - async def get_image(self): + async def get_image(self, logger: Logger = MISSING) -> bytes: + ... + + async def save_image(self, logger: Logger): """ NOT IMPLEMENTED YET diff --git a/pyclasher/api/models/clan.py b/pyclasher/api/models/clan.py index 8bcb356..9c78528 100644 --- a/pyclasher/api/models/clan.py +++ b/pyclasher/api/models/clan.py @@ -103,6 +103,10 @@ def war_ties(self): def war_losses(self): return self._get_data('warLosses') + @property + def total_wars(self): + return self.war_wins + self.war_losses + self.war_ties + @property def clan_points(self): return self._get_data('clanPoints') diff --git a/pyclasher/api/models/clan.pyi b/pyclasher/api/models/clan.pyi index 21c225a..bad0932 100644 --- a/pyclasher/api/models/clan.pyi +++ b/pyclasher/api/models/clan.pyi @@ -240,6 +240,10 @@ class Clan(BaseClan): """ ... + @property + def total_wars(self) -> int: + ... + @property def clan_points(self) -> int: """ diff --git a/pyclasher/api/models/clan_member_list.py b/pyclasher/api/models/clan_member_list.py index 4f006cf..e5ea1cc 100644 --- a/pyclasher/api/models/clan_member_list.py +++ b/pyclasher/api/models/clan_member_list.py @@ -5,7 +5,7 @@ class ClanMemberList(IterBaseModel): _iter_rtype = ClanMember - def __getitem__(self, item: int | str): + def __getitem__(self, item): return super().__getitem__(item) def __next__(self): diff --git a/pyclasher/api/models/enums.py b/pyclasher/api/models/enums.py index 83674fe..733ef11 100644 --- a/pyclasher/api/models/enums.py +++ b/pyclasher/api/models/enums.py @@ -3,31 +3,6 @@ from .labels import Label from .leagues import League, CapitalLeague, BuilderBaseLeague, WarLeague from .location import Location -from ...exceptions import ApiCode - - -class ApiCodes(Enum): - SUCCESS = ApiCode(200, "Successful response") - BAD_REQUEST = ApiCode(400, "Client provided incorrect parameters for the request.") - ACCESS_DENIED = ApiCode(403, - "Access denied, either because of missing/incorrect credentials or used API token does not grant access to the requested resource.") - NOT_FOUND = ApiCode(404, "Resource was not found.") - THROTTLED = ApiCode(429, "Request was throttled, because amount of requests was above the threshold defined for the used API token.") - UNKNOWN = ApiCode(500, "Unknown error happened when handling the request.") - MAINTENANCE = ApiCode(503, "Service is temporarily unavailable because of maintenance.") - - @classmethod - def from_code(cls, code): - for exception in cls: - if exception.value.code == code: - return exception - raise ValueError - - @classmethod - def from_exception(cls, code, response_json): - self = cls.from_code(code) - self.value.response_json = response_json - return self class ClanType(Enum): diff --git a/pyclasher/api/models/enums.pyi b/pyclasher/api/models/enums.pyi index 74e6456..f58b5df 100644 --- a/pyclasher/api/models/enums.pyi +++ b/pyclasher/api/models/enums.pyi @@ -3,47 +3,6 @@ from enum import Enum from .labels import Label from .leagues import League, CapitalLeague, BuilderBaseLeague, WarLeague from .location import Location -from ...exceptions import ApiCode - - -class ApiCodes(Enum): - SUCCESS = ApiCode(..., ...) - BAD_REQUEST = ApiCode(..., ...) - ACCESS_DENIED = ApiCode(..., ..., - "Access denied, either because of missing/incorrect credentials or used API token does not grant access to the requested resource.") - NOT_FOUND = ApiCode(..., ...) - THROTTLED = ApiCode(..., ...) - UNKNOWN = ApiCode(..., ...) - MAINTENANCE = ApiCode(..., ...) - - @classmethod - def from_code(cls, code: int) -> ApiCodes: - """ - class method that allows to initialise ApiCodes with the error code - - :param code: error or success code - :type code: int - :return: the corresponding ApiCode - :rtype: ApiCodes - """ - ... - - @classmethod - def from_exception(cls, - code: int, - response_json: dict - ) -> ApiCodes: - """ - class method that allows to initialise ApiCodes with a failed request response - - :param code: error code - :type code: int - :param response_json: failed request response json - :type response_json: dict - :return: the corresponding ApiCode - :rtype: ApiCodes - """ - ... class ClanType(Enum): diff --git a/pyclasher/api/models/location.pyi b/pyclasher/api/models/location.pyi index b217915..8daa1f6 100644 --- a/pyclasher/api/models/location.pyi +++ b/pyclasher/api/models/location.pyi @@ -43,7 +43,7 @@ class Location(BaseModel): ... @property - def country_code(self) -> Missing | int: + def country_code(self) -> Missing | str: """ location country code diff --git a/pyclasher/api/models/season.py b/pyclasher/api/models/season.py index 5900616..c38ca5f 100644 --- a/pyclasher/api/models/season.py +++ b/pyclasher/api/models/season.py @@ -21,3 +21,13 @@ def from_str(cls, season): year = int(season[0]) month = int(season[1]) return cls(year, month) + + def to_str(self): + return f"{self.year}-{self.month}" + + def __eq__(self, other): + if isinstance(other, Season): + return self.year == other.year and self.month == other.month + if isinstance(other, str): + return self == Season.from_str(other) + raise NotImplementedError diff --git a/pyclasher/api/models/season.pyi b/pyclasher/api/models/season.pyi index e9de6a0..b21b05e 100644 --- a/pyclasher/api/models/season.pyi +++ b/pyclasher/api/models/season.pyi @@ -13,3 +13,6 @@ class Season: @classmethod def from_str(cls, season: str): ... + + def to_str(self) -> str: + ... diff --git a/pyclasher/api/requests/__init__.py b/pyclasher/api/requests/__init__.py index 0d6055b..c0b520f 100644 --- a/pyclasher/api/requests/__init__.py +++ b/pyclasher/api/requests/__init__.py @@ -8,12 +8,10 @@ from .capital_league_seasons import CapitalLeaguesRequest # clan from .clan import ClanRequest -from .clan_builder_base_rankings import ClanBuilderBaseRankingsRequest from .clan_capital_raid_seasons import ClanCapitalRaidSeasonsRequest from .clan_current_war import ClanCurrentWarRequest from .clan_labels import ClanLabelsRequest from .clan_members import ClanMembersRequest -from .clan_rankings import ClanRankingsRequest from .clan_search import ClanSearchRequest from .clan_war_log import ClanWarLogRequest from .clan_currentwar_leaguegroup import ClanCurrentwarLeaguegroupRequest @@ -24,15 +22,21 @@ # leagues from .league_season import LeagueSeasonRequest from .leagues import LeaguesRequest -from .location import LocationRequest +from .league_seasons import LeagueSeasonsRequest # locations +from .location import LocationRequest from .locations import LocationsRequest # player from .player import PlayerRequest -from .player_builder_base_rankings import PlayerBuilderBaseRankingsRequest # labels from .player_labels import PlayerLabelsRequest -from .player_rankings import PlayerRankingsRequest from .abc import RequestModel, IterRequestModel, request_id from .war_league import WarLeagueRequest from .war_leagues import WarLeaguesRequest +# rankings +from .clan_builder_base_rankings import ClanBuilderBaseRankingsRequest +from .clan_rankings import ClanRankingsRequest +from .player_builder_base_rankings import PlayerBuilderBaseRankingsRequest +from .player_rankings import PlayerRankingsRequest +from .capital_rankings import CapitalRankingsRequest + diff --git a/pyclasher/api/requests/abc.py b/pyclasher/api/requests/abc.py index 5d96497..31463fc 100644 --- a/pyclasher/api/requests/abc.py +++ b/pyclasher/api/requests/abc.py @@ -121,7 +121,7 @@ async def request(self, client_id=None): await error) if req_status != 200: - raise req_error.value + raise req_error self.client.logger.debug(f"request {self._request_id} done") diff --git a/pyclasher/api/requests/capital_rankings.py b/pyclasher/api/requests/capital_rankings.py new file mode 100644 index 0000000..ed5ffcc --- /dev/null +++ b/pyclasher/api/requests/capital_rankings.py @@ -0,0 +1,20 @@ +from .abc import IterRequestModel +from ..models import ClanCapitalRanking, ClanCapitalRankingList, Location + + +class CapitalRankingsRequest(IterRequestModel): + _iter_rtype = ClanCapitalRanking + _list_rtype = ClanCapitalRankingList + + def __init__(self, location_id, limit=None, after=None, before=None): + self.location_id = (location_id.id if isinstance(location_id, Location) + else location_id) + super().__init__("locations/{location_id}/rankings/capitals", + location_id=self.location_id, + kwargs={ + 'limit': limit, + 'after': after, + 'before': before + }) + return + diff --git a/pyclasher/api/requests/capital_rankings.pyi b/pyclasher/api/requests/capital_rankings.pyi new file mode 100644 index 0000000..8b13a7e --- /dev/null +++ b/pyclasher/api/requests/capital_rankings.pyi @@ -0,0 +1,27 @@ +from typing import Iterator + +from .abc import IterRequestModel +from ..models import ClanCapitalRanking, ClanCapitalRankingList, Location + + +class CapitalRankingsRequest(IterRequestModel): + _iter_rtype = ClanCapitalRanking + _list_rtype = ClanCapitalRankingList + + def __init__(self, location_id: int | Location, + limit: int = None, after: str = None, before: str = None): + self.location_id: int = ... + ... + + @property + def items(self) -> _list_rtype: + ... + + def __getitem__(self, item: int) -> _iter_rtype: + ... + + def __iter__(self) -> Iterator[_iter_rtype]: + ... + + def __next__(self) -> _iter_rtype: + ... diff --git a/pyclasher/api/requests/league_season.pyi b/pyclasher/api/requests/league_season.pyi index 23a53e5..abb0804 100644 --- a/pyclasher/api/requests/league_season.pyi +++ b/pyclasher/api/requests/league_season.pyi @@ -16,6 +16,7 @@ class LeagueSeasonRequest(IterRequestModel): async def _async_request(self) -> LeagueSeasonRequest: ... + @property def items(self) -> _list_rtype: ... diff --git a/pyclasher/api/requests/league_seasons.py b/pyclasher/api/requests/league_seasons.py new file mode 100644 index 0000000..f35902b --- /dev/null +++ b/pyclasher/api/requests/league_seasons.py @@ -0,0 +1,12 @@ +from .abc import IterRequestModel +from ..models import LeagueSeason, LeagueSeasonList + + +class LeagueSeasonsRequest(IterRequestModel): + _iter_rtype = LeagueSeason + _list_rtype = LeagueSeasonList + + def __init__(self, league_id): + super().__init__("leagues/{league_id}/seasons", + league_id=league_id) + return diff --git a/pyclasher/api/requests/league_seasons.pyi b/pyclasher/api/requests/league_seasons.pyi new file mode 100644 index 0000000..f0b1d6b --- /dev/null +++ b/pyclasher/api/requests/league_seasons.pyi @@ -0,0 +1,25 @@ +from typing import Iterator + +from .abc import IterRequestModel +from ..models import LeagueSeason, LeagueSeasonList + + +class LeagueSeasonsRequest(IterRequestModel): + _iter_rtype = LeagueSeason + _list_rtype = LeagueSeasonList + + def __init__(self, league_id: int): + ... + + @property + def items(self) -> _list_rtype: + ... + + def __getitem__(self, item: int) -> _iter_rtype: + ... + + def __iter__(self) -> Iterator[_iter_rtype]: + ... + + def __next__(self) -> _iter_rtype: + ... diff --git a/pyclasher/api/requests/locations.pyi b/pyclasher/api/requests/locations.pyi index 6cdb202..9ddf17a 100644 --- a/pyclasher/api/requests/locations.pyi +++ b/pyclasher/api/requests/locations.pyi @@ -1,4 +1,4 @@ -from typing import Self, Iterator +from typing import Iterator from .abc import IterRequestModel from ..models import Location, LocationList @@ -11,9 +11,7 @@ class LocationsRequest(IterRequestModel): def __init__(self, limit: int = None, after: str = None, before: str = None): ... - async def _async_request(self) -> Self: - ... - + @property def items(self) -> _list_rtype: ... diff --git a/pyclasher/api/requests/player_rankings.pyi b/pyclasher/api/requests/player_rankings.pyi index cd1a569..0532046 100644 --- a/pyclasher/api/requests/player_rankings.pyi +++ b/pyclasher/api/requests/player_rankings.pyi @@ -16,6 +16,7 @@ class PlayerRankingsRequest(IterRequestModel): def _async_request(self) -> PlayerRankingsRequest: ... + @property def items(self) -> _list_rtype: ... diff --git a/pyclasher/client.pyi b/pyclasher/client.pyi index 6ceb4de..a079680 100644 --- a/pyclasher/client.pyi +++ b/pyclasher/client.pyi @@ -50,7 +50,7 @@ class Client: self, tokens: str | Iterable[str] = None, requests_per_second: int = 5, - request_timeout: float = 30, + request_timeout: float | None = 30, logger: Logger = MISSING, swagger_url: str = None ) -> None: @@ -78,7 +78,7 @@ class Client: self.__tokens: list[str] = ... self.requests_per_second: int = ... self.queue: PQueue = ... - self.request_timeout: float = ... + self.request_timeout: float | None = ... self.__client_running: bool = ... self.__temporary_session: bool = ... self.__consumers: list = ... @@ -92,7 +92,7 @@ class Client: email: str, password: str, requests_per_second: int = 5, - request_timeout: float = 30, + request_timeout: float | None = 30, logger: Logger = MISSING, login_count: int = 1 ) -> Client: diff --git a/pyclasher/exceptions.py b/pyclasher/exceptions.py index 43650d0..0271374 100644 --- a/pyclasher/exceptions.py +++ b/pyclasher/exceptions.py @@ -8,6 +8,11 @@ def __getitem__(self, item): def __getattr__(self, item): return self + def __add__(self, other): + if isinstance(other, Missing): + return 0 + return other + def __str__(self): return "MISSING" @@ -22,30 +27,93 @@ class PyClasherException(Exception): pass -class ApiCode(PyClasherException): - """ - exception class to handle ClashOfClans API client errors - """ +class ApiException(PyClasherException): + def __init__(self, api_code, client_error=None, *args, **kwargs): + self.api_code = api_code + self.client_error = client_error + super.__init__(*args, **kwargs) + return + + def __repr__(self): + return f"{self.__class__.__name__}({self.api_code})" + + def __str__(self): + return f"an API error occurred" + + +class BadRequest(ApiException): + def __init__(self, client_error=None): + super().__init__(400, client_error) + return + + def __str__(self): + return "Client provided incorrect parameters for the request." + - def __init__(self, code, description, response_json=None): - self.code = code - self.description = description - self.response_json = response_json +class AccessDenied(ApiException): + def __init__(self, client_error=None): + super().__init__(400, client_error) return - def _dict_to_str(self): - return "\n".join( - (f" - {key}: {val}" for key, val in self.response_json.items()) - ) + def __str__(self): + return ("Access denied, either because of missing/incorrect " + "credentials or used API token does not grant access to the " + "requested resource.") + + +class NotFound(ApiException): + def __init__(self, client_error=None): + super().__init__(400, client_error) + return def __str__(self): - if self.response_json is None: - return f"ApiException({self.code})" - return (f"ApiException:\n - Code: {self.code}\n - Description: " - f"{self.description}\n{self._dict_to_str()}") + return "Resource was not found." + + +class Throttled(ApiException): + def __init__(self, client_error=None): + super().__init__(400, client_error) + return + + def __str__(self): + return ("Request was throttled, because amount of requests was above " + "the threshold defined for the used API token.") + + +class UnknownApiException(ApiException): + def __init__(self, client_error=None): + super().__init__(400, client_error) + return + + def __str__(self): + return "Unknown error happened when handling the request." - def __repr__(self): - return f"ApiException(code={self.code})" + +class Maintenance(ApiException): + def __init__(self, client_error=None): + super().__init__(400, client_error) + return + + def __str__(self): + return "Service is temporarily unavailable because of maintenance." + + +class ApiExceptions: + BadRequest = BadRequest + AccessDenied = AccessDenied + NotFound = NotFound + Throttled = Throttled + UnknownApiException = UnknownApiException + Maintenance = Maintenance + + @classmethod + def from_api_code(cls, api_code, client_error=None): + for key, value in cls.__dict__.items(): + if value is ApiException: + if value().api_code == api_code: + return value(client_error) + raise PyClasherException(f"could not find {api_code} in the API " + f"exceptions") class RequestNotDone(PyClasherException): @@ -128,8 +196,14 @@ def __str__(self): class RequestTimeout(PyClasherException): + def __init__(self, allowed_time, *args): + self.allowed_time = allowed_time + super().__init__(*args) + return + def __str__(self): - return "The request took to much time and was cancelled." + return (f"The request took longer than {self.allowed_time}s and was " + f"cancelled.") class InvalidClientId(PyClasherException): diff --git a/pyclasher/exceptions.pyi b/pyclasher/exceptions.pyi index 6b2a035..5d7b2c7 100644 --- a/pyclasher/exceptions.pyi +++ b/pyclasher/exceptions.pyi @@ -1,5 +1,7 @@ from typing import Any +from .api.models import ClientError + class Missing: """ @@ -15,6 +17,9 @@ class Missing: def __getattr__(self, item) -> Missing: ... + def __add__(self, other) -> int | float: + ... + def __str__(self) -> str: ... @@ -30,56 +35,71 @@ class PyClasherException(Exception): pass -class ApiCode(Exception): - """ - exception class to handle ClashOfClans API exceptions +class ApiException(PyClasherException): + def __init__(self, api_code: int, client_error: ClientError = None, *args, + **kwargs) -> None: + self.api_code: int = ... + self.client_error: ClientError = ... + ... - :ivar code: error code of the exception - :type code: int - :ivar description: description of the exception - :type description: str - :ivar response_json: the raw dictionary object of the exception - :type response_json: dict - """ + def __repr__(self) -> str: + ... - def __init__(self, - code: int, - description: str, - response_json: dict = None - ) -> None: - """ - initialisation of the API exception model - - :param code: error code of the exception - :type code: int - :param description: description of the exception - :type description: str - :param response_json: the raw dictionary object of the exception - :type response_json: dict - :return: None - :rtype: None - """ - self.code = code - self.description = description - self.response_json = response_json + def __str__(self) -> str: + ... - def _dict_to_str(self) -> str: - """ - protected method that converts the response json to a string - :return: the response json as a string - :rtype: str - """ +class BadRequest(ApiException): + def __init__(self, client_error: ClientError = None): ... - def __str__(self) -> str: + +class AccessDenied(ApiException): + def __init__(self, client_error: ClientError = None): ... - def __repr__(self) -> str: + +class NotFound(ApiException): + def __init__(self, client_error: ClientError = None): + ... + + +class Throttled(ApiException): + def __init__(self, client_error: ClientError = None): + ... + + +class UnknownApiException(ApiException): + def __init__(self, client_error: ClientError = None): ... -class RequestNotDone(Exception): +class Maintenance(ApiException): + def __init__(self, client_error: ClientError = None): + ... + + +class ApiExceptions: + BadRequest = BadRequest + AccessDenied = AccessDenied + NotFound = NotFound + Throttled = Throttled + UnknownApiException = UnknownApiException + Maintenance = Maintenance + + @classmethod + def from_api_code(cls, + api_code: int, + client_error: ClientError = None) -> ApiException: + for key, value in cls.__dict__.items(): + if value is ApiException: + if value().api_code == api_code: + return value(client_error) + raise PyClasherException(f"could not find {api_code} in the API " + f"exceptions") + + +class RequestNotDone(PyClasherException): """ exception class to handle the case if a request was not done but data was retrieved """ @@ -88,7 +108,7 @@ class RequestNotDone(Exception): ... -class NoneToken(Exception): +class NoneToken(PyClasherException): """ exception class to handle the case if no ClashOfClans API token was entered to the client """ @@ -97,7 +117,7 @@ class NoneToken(Exception): ... -class InvalidLoginData(Exception): +class InvalidLoginData(PyClasherException): """ exception class to handle invalid login data to log in to the ClashOfClans developer portal """ @@ -106,7 +126,7 @@ class InvalidLoginData(Exception): ... -class InvalidType(Exception): +class InvalidType(PyClasherException): """ exception class to handle type errors for the pyclasher package """ @@ -132,7 +152,7 @@ class InvalidType(Exception): ... -class LoginNotDone(Exception): +class LoginNotDone(PyClasherException): """ exception class to handle the error if the login was not done but data was retrieved """ @@ -141,7 +161,7 @@ class LoginNotDone(Exception): ... -class ClientIsRunning(Exception): +class ClientIsRunning(PyClasherException): """ exception class that handles errors if a not permitted action while the client was running was done """ @@ -150,7 +170,7 @@ class ClientIsRunning(Exception): ... -class ClientIsNotRunning(Exception): +class ClientIsNotRunning(PyClasherException): """ exception class that handles errors if a not permitted action while the client was not running was done """ @@ -159,7 +179,7 @@ class ClientIsNotRunning(Exception): ... -class ClientAlreadyInitialised(Exception): +class ClientAlreadyInitialised(PyClasherException): """ exception class to handle multiple client initialisations """ @@ -168,7 +188,7 @@ class ClientAlreadyInitialised(Exception): ... -class NoClient(Exception): +class NoClient(PyClasherException): """ exception class to handle the error if no client was initialised """ @@ -177,7 +197,7 @@ class NoClient(Exception): ... -class InvalidTimeFormat(Exception): +class InvalidTimeFormat(PyClasherException): """ exception class to handle errors while converting a time string to a Time class @@ -203,7 +223,7 @@ class InvalidTimeFormat(Exception): ... -class ClientRunningOverwrite(Exception): +class ClientRunningOverwrite(PyClasherException): """ exception class that handles the error if a client was running and a client variable was overwritten """ @@ -212,7 +232,7 @@ class ClientRunningOverwrite(Exception): ... -class InvalidSeasonFormat(Exception): +class InvalidSeasonFormat(PyClasherException): """ exception class that handles an invalid season format """ @@ -221,7 +241,12 @@ class InvalidSeasonFormat(Exception): ... -class RequestTimeout(Exception): +class RequestTimeout(PyClasherException): + def __init__(self, allowed_time: float, *args): + self.allowed_time: float = ... + ... + + def __str__(self) -> str: ... diff --git a/pyclasher/request_queue/request_consumer.py b/pyclasher/request_queue/request_consumer.py index 7e7737f..8953f61 100644 --- a/pyclasher/request_queue/request_consumer.py +++ b/pyclasher/request_queue/request_consumer.py @@ -1,10 +1,11 @@ -from asyncio import timeout, create_task +from asyncio import create_task, TimeoutError as aTimeoutError from json import dumps -from aiohttp import ClientSession +from aiohttp import ClientSession, ClientTimeout -from ..api.models import ApiCodes from ..utils import ExecutionTimer +from ..exceptions import ApiExceptions, MISSING, RequestTimeout +from ..api.models import ClientError class PConsumer: @@ -19,23 +20,35 @@ def __init__(self, queue, token, requests_per_s, request_timeout, url): self.url = url self.session = ClientSession( base_url=url, - headers=self.header + headers=self.header, + timeout=ClientTimeout(total=self.timeout) ) return async def _request(self, future, url, method, body, status, error): - async with self.session.request( - method=method, url=url, - data=None if body is None else dumps(body) - ) as response, timeout(self.timeout): - response_json = await response.json() - - future.set_result(response_json) - status.set_result(response.status) - error.set_result(None if response.status == 200 - else ApiCodes.from_exception(response.status, - response_json)) - return + try: + async with self.session.request( + method=method, + url=url, + data=None if body is None else dumps(body) + ) as response: + response_json = await response.json() + + if response.status == 200: + error.set_result(None) + else: + error.set_result(ApiExceptions.from_api_code( + response.status, ClientError(response_json) + )) + + future.set_result(response_json) + status.set_result(response.status) + return + + except aTimeoutError: + future.set_result(MISSING) + status.set_result(None) + error.set_result(RequestTimeout(self.timeout)) async def consume(self): while True: diff --git a/tests/requests/test_clan.py b/tests/requests/test_clan.py index 49f17c3..cf0e41e 100644 --- a/tests/requests/test_clan.py +++ b/tests/requests/test_clan.py @@ -34,6 +34,8 @@ async def test_clan(event_loop, pyclasher_client): assert isinstance(clan.war_losses, int) assert isinstance(clan.war_wins, int) assert isinstance(clan.war_win_streak, int) + assert isinstance(clan.total_wars, int) + assert clan.total_wars == clan.war_wins + clan.war_ties + clan.war_losses assert isinstance(clan.war_frequency, WarFrequency) assert isinstance(clan.badge_urls, BadgeUrls) assert isinstance(clan.war_league, WarLeague) diff --git a/tests/requests/test_leagues.py b/tests/requests/test_leagues.py index dd49bd9..64a367f 100644 --- a/tests/requests/test_leagues.py +++ b/tests/requests/test_leagues.py @@ -1,11 +1,43 @@ import pytest +from random import randint, choices -from pyclasher.api.models import LeagueList, Paging, IconUrls -from pyclasher import LeaguesRequest +from pyclasher import MISSING +from pyclasher.api.models import ( + LeagueList, CapitalLeagueList, BuilderBaseLeagueList, WarLeagueList, + Paging, IconUrls, LeagueSeasonList, LeagueSeason, PlayerRankingList, + PlayerRankingClan, Season +) +from pyclasher.api.requests import ( + LeaguesRequest, CapitalLeaguesRequest, BuilderBaseLeaguesRequest, + WarLeaguesRequest, LeagueRequest, CapitalLeagueRequest, + BuilderBaseLeagueRequest, WarLeagueRequest, LeagueSeasonRequest, + LeagueSeasonsRequest +) + + +Seasons = ( + "2015-07", "2015-08", "2015-09", "2015-10", "2015-11", "2015-12", + "2016-01", "2016-02", "2016-03", "2016-04", "2016-05", "2016-06", + "2016-07", "2016-08", "2016-09", "2016-10", "2016-11", "2016-12", + "2017-01", "2017-02", "2017-03", "2017-04", "2017-05", "2017-06", + "2017-07", "2017-08", "2017-09", "2017-10", "2017-11", "2017-12", + "2018-01", "2018-02", "2018-03", "2018-04", "2018-05", "2018-06", + "2018-07", "2018-08", "2018-09", "2018-10", "2018-11", "2018-12", + "2019-01", "2019-02", "2019-03", "2019-04", "2019-05", "2019-06", + "2019-07", "2019-08", "2019-09", "2019-10", "2019-11", "2019-12", + "2020-01", "2020-02", "2020-03", "2020-04", "2020-05", "2020-06", + "2020-07", "2020-08", "2020-09", "2020-10", "2020-11", "2020-12", + "2021-01", "2021-02", "2021-03", "2021-04", "2021-05", "2021-06", + "2021-07", "2021-08", "2021-09", "2021-10", "2021-11", "2021-12", + "2022-01", "2022-02", "2022-03", "2022-04", "2022-05", "2022-06", + "2022-07", "2022-08", "2022-09", "2022-10", "2022-11", "2022-12", + "2023-01", "2023-02", "2023-03", "2023-04", "2023-05", "2023-06", + "2023-07", +) @pytest.mark.asyncio -async def test_leagues(event_loop, pyclasher_client): +async def test_leagues(pyclasher_client): leagues = LeaguesRequest() await leagues.request("test_client") @@ -18,3 +50,164 @@ async def test_leagues(event_loop, pyclasher_client): assert isinstance(league.name, str) assert isinstance(league.id, int) assert isinstance(league.icon_urls, IconUrls) + + +@pytest.mark.asyncio +async def test_capital_leagues(pyclasher_client): + capital_leagues = CapitalLeaguesRequest() + + await capital_leagues.request("test_client") + + assert isinstance(capital_leagues.to_dict(), dict) + assert isinstance(capital_leagues.items, CapitalLeagueList) + assert isinstance(capital_leagues.paging, Paging) + + for capital_league in capital_leagues: + assert isinstance(capital_league.name, str) + assert isinstance(capital_league.id, int) + + +@pytest.mark.asyncio +async def test_builder_base_leagues(pyclasher_client): + builder_base_leagues = BuilderBaseLeaguesRequest() + + await builder_base_leagues.request("test_client") + + assert isinstance(builder_base_leagues.to_dict(), dict) + assert isinstance(builder_base_leagues.items, BuilderBaseLeagueList) + assert isinstance(builder_base_leagues.paging, Paging) + + for builder_base_league in builder_base_leagues: + assert isinstance(builder_base_league.name, str) + assert isinstance(builder_base_league.id, int) + + +@pytest.mark.asyncio +async def test_war_leagues(pyclasher_client): + war_leagues = WarLeaguesRequest() + + await war_leagues.request("test_client") + + assert isinstance(war_leagues.to_dict(), dict) + assert isinstance(war_leagues.items, WarLeagueList) + assert isinstance(war_leagues.paging, Paging) + + for war_league in war_leagues: + assert isinstance(war_league.name, str) + assert isinstance(war_league.id, int) + + +@pytest.mark.parametrize( + "league_id", + [randint(29000000, 29000022) for _ in range(10)] +) +@pytest.mark.asyncio +async def test_league(pyclasher_client, league_id): + league = LeagueRequest(league_id) + + await league.request("test_client") + + assert isinstance(league.to_dict(), dict) + assert league.league_id == league_id + assert league.id == league_id + assert isinstance(league.id, int) + assert isinstance(league.name, str) + assert isinstance(league.icon_urls, IconUrls) + + +@pytest.mark.parametrize( + "league_id", + [randint(85000000, 85000022) for _ in range(5)] +) +@pytest.mark.asyncio +async def test_capital_league(pyclasher_client, league_id): + capital_league = CapitalLeagueRequest(league_id) + + await capital_league.request("test_client") + + assert isinstance(capital_league.to_dict(), dict) + assert capital_league.league_id == league_id + assert capital_league.id == league_id + assert isinstance(capital_league.id, int) + assert isinstance(capital_league.name, str) + + +@pytest.mark.parametrize( + "league_id", + [randint(44000000, 44000041) for _ in range(5)] +) +@pytest.mark.asyncio +async def test_builder_base_league(pyclasher_client, league_id): + builder_base_league = BuilderBaseLeagueRequest(league_id) + + await builder_base_league.request("test_client") + + assert isinstance(builder_base_league.to_dict(), dict) + assert builder_base_league.league_id == league_id + assert builder_base_league.id == league_id + assert isinstance(builder_base_league.id, int) + assert isinstance(builder_base_league.name, str) + + +@pytest.mark.parametrize( + "league_id", + [randint(48000000, 48000018) for _ in range(5)] +) +@pytest.mark.asyncio +async def test_war_league(pyclasher_client, league_id): + war_league = WarLeagueRequest(league_id) + + await war_league.request("test_client") + + assert isinstance(war_league.to_dict(), dict) + assert war_league.league_id == league_id + assert war_league.id == league_id + assert isinstance(war_league.id, int) + assert isinstance(war_league.name, str) + + +@pytest.mark.asyncio +async def test_league_seasons(pyclasher_client): + seasons = LeagueSeasonsRequest(29000022) + + await seasons.request("test_client") + + assert isinstance(seasons.to_dict(), dict) + assert isinstance(seasons.items, LeagueSeasonList) + assert isinstance(seasons.paging, Paging) + + for season in seasons: + assert isinstance(season, LeagueSeason) + assert isinstance(season.to_dict(), dict) + assert isinstance(season.id, str) + + +@pytest.mark.parametrize( + "season", + choices(Seasons, k=5) +) +@pytest.mark.asyncio +async def test_league_season(pyclasher_client, season): + league_season = LeagueSeasonRequest(29000022, season, limit=20) + + await league_season.request("test_client") + + assert isinstance(league_season.to_dict(), dict) + assert league_season.league_id == 29000022 + assert league_season.season_id == Season.from_str(season) + assert isinstance(league_season.items, PlayerRankingList) + assert isinstance(league_season.paging, Paging) + + for ranking in league_season: + assert isinstance(ranking.to_dict(), dict) + assert isinstance(ranking.name, str) + assert ranking.league == MISSING + assert isinstance(ranking.rank, int) + assert isinstance(ranking.tag, str) + assert isinstance(ranking.exp_level, int) + assert isinstance(ranking.defense_wins, int) + assert isinstance(ranking.trophies, int) + assert isinstance(ranking.attack_wins, int) + assert (isinstance(ranking.clan, PlayerRankingClan) + or ranking.clan == MISSING) + assert ranking.previous_rank == MISSING diff --git a/tests/requests/test_locations.py b/tests/requests/test_locations.py index 776b9f6..6e85e12 100644 --- a/tests/requests/test_locations.py +++ b/tests/requests/test_locations.py @@ -1,7 +1,19 @@ +from random import randint + import pytest -from pyclasher import LocationsRequest -from pyclasher.api.models import LocationList, Paging +from pyclasher import MISSING +from pyclasher.api.requests import ( + LocationsRequest, LocationRequest, ClanRankingsRequest, + ClanBuilderBaseRankingsRequest, PlayerRankingsRequest, + PlayerBuilderBaseRankingsRequest, CapitalRankingsRequest +) +from pyclasher.api.models import ( + LocationList, Paging, PlayerRankingList, ClanRankingList, + ClanCapitalRankingList, PlayerBuilderBaseRankingList, + ClanBuilderBaseRankingList, League, PlayerRankingClan, Location, + BadgeUrls, BuilderBaseLeague +) @pytest.mark.asyncio @@ -18,3 +30,152 @@ async def test_locations(event_loop, pyclasher_client): assert isinstance(location.to_dict(), dict) assert isinstance(location.id, int) assert isinstance(location.name, str) + + +@pytest.mark.parametrize( + "location_id", + [randint(32000007, 32000260) for _ in range(5)] +) +@pytest.mark.asyncio +async def test_location(pyclasher_client, location_id): + location = LocationRequest(location_id) + + await location.request("test_client") + + assert isinstance(location.to_dict(), dict) + assert location.location_id == location_id + assert location.id == location_id + assert isinstance(location.name, str) + assert isinstance(location.id, int) + assert (isinstance(location.country_code, str) + or location.country_code is MISSING) + assert location.localized_name is MISSING + + +@pytest.mark.parametrize( + "location_id", + [randint(32000007, 32000260) for _ in range(5)] +) +@pytest.mark.asyncio +async def test_player_rankings(pyclasher_client, location_id): + rankings = PlayerRankingsRequest(location_id, limit=20) + + await rankings.request("test_client") + + assert isinstance(rankings.to_dict(), dict) + assert rankings.location_id == location_id + + assert isinstance(rankings.items, PlayerRankingList) + assert isinstance(rankings.paging, Paging) + + for rank in rankings: + assert isinstance(rank.rank, int) + assert isinstance(rank.previous_rank, int) + assert isinstance(rank.name, str) + assert (isinstance(rank.clan, PlayerRankingClan) + or rank.clan is MISSING) + assert isinstance(rank.league, League) + assert isinstance(rank.attack_wins, int) + assert isinstance(rank.trophies, int) + assert isinstance(rank.defense_wins, int) + assert isinstance(rank.exp_level, int) + assert isinstance(rank.tag, str) + + +@pytest.mark.parametrize( + "location_id", + [randint(32000007, 32000260) for _ in range(5)] +) +@pytest.mark.asyncio +async def test_clan_rankings(pyclasher_client, location_id): + rankings = ClanRankingsRequest(location_id, limit=20) + + await rankings.request("test_client") + + assert isinstance(rankings.to_dict(), dict) + assert rankings.location_id == location_id + + assert isinstance(rankings.items, ClanRankingList) + assert isinstance(rankings.paging, Paging) + + for rank in rankings: + assert isinstance(rank.rank, int) + assert isinstance(rank.previous_rank, int) + assert isinstance(rank.name, str) + assert isinstance(rank.tag, str) + assert isinstance(rank.location, Location) + assert isinstance(rank.members, int) + assert isinstance(rank.clan_points, int) + assert isinstance(rank.clan_level, int) + assert isinstance(rank.badge_urls, BadgeUrls) + + +@pytest.mark.parametrize( + "location_id", + [randint(32000007, 32000260) for _ in range(5)] +) +@pytest.mark.asyncio +async def test_player_builder_base_rankings(pyclasher_client, location_id): + rankings = PlayerBuilderBaseRankingsRequest(location_id, limit=20) + + await rankings.request("test_client") + + assert isinstance(rankings.to_dict(), dict) + assert rankings.location_id == location_id + + assert isinstance(rankings.items, PlayerBuilderBaseRankingList) + assert isinstance(rankings.paging, Paging) + + for rank in rankings: + assert isinstance(rank.rank, int) + assert isinstance(rank.previous_rank, int) + assert isinstance(rank.name, str) + assert isinstance(rank.tag, str) + assert (isinstance(rank.clan, PlayerRankingClan) + or rank.clan is MISSING) + assert isinstance(rank.exp_level, int) + assert isinstance(rank.versus_battle_wins, int) + assert isinstance(rank.builder_base_trophies, int) + assert isinstance(rank.builder_base_league, BuilderBaseLeague) + + +@pytest.mark.parametrize( + "location_id", + [randint(32000000, 32000260) for _ in range(5)] +) +@pytest.mark.asyncio +async def test_clan_builder_base_rankings(pyclasher_client, location_id): + rankings = ClanBuilderBaseRankingsRequest(location_id, limit=20) + + await rankings.request("test_client") + + assert isinstance(rankings.to_dict(), dict) + assert rankings.location_id == location_id + + assert isinstance(rankings.items, ClanBuilderBaseRankingList) + assert isinstance(rankings.paging, Paging) + + for rank in rankings: + assert rank.clan_points is MISSING + assert isinstance(rank.clan_builder_base_points, int) + + +@pytest.mark.parametrize( + "location_id", + [randint(32000000, 32000260) for _ in range(5)] +) +@pytest.mark.asyncio +async def test_capital_rankings(pyclasher_client, location_id): + rankings = CapitalRankingsRequest(location_id) + + await rankings.request("test_client") + + assert isinstance(rankings.to_dict(), dict) + assert rankings.location_id == location_id + + assert isinstance(rankings.items, ClanCapitalRankingList) + assert isinstance(rankings.paging, Paging) + + for rank in rankings: + assert rank.clan_points is MISSING + assert isinstance(rank.clan_capital_points, int)