diff --git a/pyclasher/__init__.py b/pyclasher/__init__.py index d7150c8..ec4c49c 100644 --- a/pyclasher/__init__.py +++ b/pyclasher/__init__.py @@ -18,7 +18,7 @@ from .utils import * # client.py -from .client import RequestMethods, Status, Auth, Developer, Login, RequestQueue, Consumer, PyClasherClient +from .client import Client # exceptions.py from .exceptions import (Missing, MISSING, PyClasherException, ApiCode, RequestNotDone, NoneToken, InvalidLoginData, diff --git a/pyclasher/api/models/__init__.py b/pyclasher/api/models/__init__.py index da7a405..366fcc3 100644 --- a/pyclasher/api/models/__init__.py +++ b/pyclasher/api/models/__init__.py @@ -23,16 +23,22 @@ from .clan_war_league_group import ClanWarLeagueRound, ClanWarLeagueRoundList, ClanWarLeagueClanMember, \ ClanWarLeagueClanMemberList, ClanWarLeagueClan, ClanWarLeagueClanList, ClanWarLeagueGroup from .clan_war_log import ClanWarLogEntry, ClanWarLog -from .war_clan import ClanWarAttack, ClanWarAttackList, ClanWarMember, ClanWarMemberList, WarClan +from .enums import ApiCodes, ClanType, WarFrequency, Locations, Leagues, CapitalLeagues, \ + BuilderBaseLeagues, WarLeagues, Labels, Languages, ClanWarState, ClanWarLeagueGroupState, \ + ClanWarResult, WarPreference, PlayerHouseElementType, Village, TokenStatus, ClanRole # gold pass season from .gold_pass_season import GoldPassSeason # labels from .labels import Label, LabelList +from .language import Language # league models from .leagues import League, BuilderBaseLeague, CapitalLeague, WarLeague, LeagueList, \ BuilderBaseLeagueList, CapitalLeagueList, WarLeagueList, LeagueSeason, LeagueSeasonList # locations from .location import Location, LocationList +# login +from .login import * +from .misc import * # player models from .player import PlayerClan, LegendLeagueTournamentSeasonResult, PlayerLegendStatistics, \ PlayerItemLevel, PlayerItemLevelList, PlayerAchievementProgress, PlayerAchievementProgressList, Player @@ -40,12 +46,6 @@ from .player_house import PlayerHouseElement, PlayerHouseElementList, PlayerHouse from .player_ranking_clan import PlayerRankingClan from .player_ranking_list import PlayerRanking, PlayerRankingList - # misc from .season import Season -from .language import Language -from .misc import * -from .enums import ApiCodes, ClanType, WarFrequency, Locations, Leagues, CapitalLeagues, \ - BuilderBaseLeagues, WarLeagues, Labels, Languages, ClanWarState, ClanWarLeagueGroupState, \ - ClanWarResult, WarPreference, PlayerHouseElementType, Village, TokenStatus, ClanRole - +from .war_clan import ClanWarAttack, ClanWarAttackList, ClanWarMember, ClanWarMemberList, WarClan diff --git a/pyclasher/api/models/clan.py b/pyclasher/api/models/clan.py index 1d94e5e..8bcb356 100644 --- a/pyclasher/api/models/clan.py +++ b/pyclasher/api/models/clan.py @@ -3,9 +3,9 @@ from .clan_member_list import ClanMemberList from .enums import WarFrequency, ClanType from .labels import LabelList +from .language import Language from .leagues import WarLeague, CapitalLeague from .location import Location -from .language import Language class ClanDistrictData(BaseModel): diff --git a/pyclasher/api/models/clan.pyi b/pyclasher/api/models/clan.pyi index 3303eaa..21c225a 100644 --- a/pyclasher/api/models/clan.pyi +++ b/pyclasher/api/models/clan.pyi @@ -8,9 +8,9 @@ from .base_models import BaseClan from .clan_member_list import ClanMemberList from .enums import WarFrequency, ClanType from .labels import LabelList +from .language import Language from .leagues import WarLeague, CapitalLeague from .location import Location -from .language import Language from ...exceptions import Missing diff --git a/pyclasher/api/models/gold_pass_season.py b/pyclasher/api/models/gold_pass_season.py index 31e888d..a104814 100644 --- a/pyclasher/api/models/gold_pass_season.py +++ b/pyclasher/api/models/gold_pass_season.py @@ -1,5 +1,5 @@ -from .base_models import Time from .abc import BaseModel +from .base_models import Time class GoldPassSeason(BaseModel): diff --git a/pyclasher/api/models/gold_pass_season.pyi b/pyclasher/api/models/gold_pass_season.pyi index 5f14839..d270a6d 100644 --- a/pyclasher/api/models/gold_pass_season.pyi +++ b/pyclasher/api/models/gold_pass_season.pyi @@ -1,5 +1,5 @@ -from .base_models import Time from .abc import BaseModel +from .base_models import Time class GoldPassSeason(BaseModel): diff --git a/pyclasher/api/models/login/__init__.py b/pyclasher/api/models/login/__init__.py index e69de29..fe4412a 100644 --- a/pyclasher/api/models/login/__init__.py +++ b/pyclasher/api/models/login/__init__.py @@ -0,0 +1 @@ +from .login_models import Auth, Developer, LoginModel, Status diff --git a/pyclasher/api/models/login/login_models.py b/pyclasher/api/models/login/login_models.py new file mode 100644 index 0000000..df05cc3 --- /dev/null +++ b/pyclasher/api/models/login/login_models.py @@ -0,0 +1,110 @@ +from ..abc import BaseModel + + +class Status(BaseModel): + """ + class representing the status of the ClashOfClans API login + """ + + @property + def code(self): + return self._get_data('code') + + @property + def message(self): + return self._get_data('message') + + @property + def detail(self): + return self._get_data('detail') + + +class Auth(BaseModel): + def __init__(self, data): + super().__init__(data) + self._main_attribute = self.uid + return + + @property + def uid(self): + return self._get_data('uid') + + @property + def token(self): + return self._get_data('token') + + @property + def ua(self): + return self._get_data('ua') + + @property + def ip(self): + return self._get_data('ip') + + +class Developer(BaseModel): + @property + def id(self): + return self._get_data('id') + + @property + def name(self): + return self._get_data('name') + + @property + def game(self): + return self._get_data('game') + + @property + def email(self): + return self._get_data('email') + + @property + def tier(self): + return self._get_data('tier') + + @property + def allowed_scopes(self): + return self._get_data('allowedScopes') + + @property + def max_cidrs(self): + return self._get_data('maxCidrs') + + @property + def prev_login_ts(self): + return self._get_data('prevLoginTs') + + @property + def prev_login_ip(self): + return self._get_data('prevLoginIp') + + @property + def prev_login_ua(self): + return self._get_data('prevLoginUa') + + +class LoginModel(BaseModel): + @property + def status(self): + return Status(self._get_data('status')) + + @property + def session_expires_in_seconds(self): + return self._get_data('sessionExpiresInSeconds') + + @property + def auth(self): + return Auth(self._get_data('auth')) + + @property + def developer(self): + return Developer(self._get_data('developer')) + + @property + def temporary_api_token(self): + return self._get_data('temporaryAPIToken') + + @property + def swagger_url(self): + return self._get_data('swaggerUrl') diff --git a/pyclasher/api/models/login/login_models.pyi b/pyclasher/api/models/login/login_models.pyi new file mode 100644 index 0000000..68b61cf --- /dev/null +++ b/pyclasher/api/models/login/login_models.pyi @@ -0,0 +1,240 @@ +from ..abc import BaseModel + + +class Status(BaseModel): + """ + class representing the status of the ClashOfClans API login + """ + + @property + def code(self) -> int: + """ + status code + + :return: the status code + :rtype: int + """ + ... + + @property + def message(self) -> str: + """ + status message + + :return: the status message + :rtype: str + """ + ... + + @property + def detail(self): + ... + + +class Auth(BaseModel): + """ + class representing the authentication of the ClashOfClans API login + """ + + @property + def uid(self) -> str: + """ + user id of the authentication + + :return: the user id + :rtype: str + """ + ... + + @property + def token(self) -> str: + """ + user token + + :return: the user token + :rtype: str + """ + ... + + @property + def ua(self): + """ + user agent of the authentication + + :return: the user agent of the authentication + """ + ... + + @property + def ip(self): + ... + + +class Developer(BaseModel): + """ + class representing the developer that logged in via the ClashOfClans login API + """ + + @property + def id(self) -> str: + """ + id of the developer + + :return: the developer's id + :rtype: str + """ + ... + + @property + def name(self) -> str: + """ + name of the developer + + :return: the developer's name + :rtype: str + """ + ... + + @property + def game(self) -> str: + """ + game of the developer + + :return: the developer's name + :rtype: str + """ + ... + + @property + def email(self) -> str: + """ + email address of the developer + + :return: the developer's email address + :rtype: str + """ + ... + + @property + def tier(self) -> str: + """ + tier of the developer + + :return: the developer's tier + :rtype: str + """ + ... + + @property + def allowed_scopes(self): + """ + allowed scopes of the developer + + :return: the developer's allowed scopes + """ + ... + + @property + def max_cidrs(self): + """ + max cidrs of the developer + + :return: the developer's max cidrs + """ + ... + + @property + def prev_login_ts(self) -> str: + """ + previous login timestamp of the developer + + :return: the developer's previous login timestamp + :rtype: str + """ + ... + + @property + def prev_login_ip(self) -> str: + """ + previous login ip address of the developer + + :return: the developer's previous login ip address + :rtype: str + """ + ... + + @property + def prev_login_ua(self) -> str: + """ + previous login user agent of the developer + + :return: the developer's previous login user agent + :rtype: str + """ + ... + + +class LoginModel(BaseModel): + """ + login model class + """ + + @property + def status(self) -> Status: + """ + login status + + :return: the login status + :rtype: Status + """ + ... + + @property + def session_expires_in_seconds(self) -> int: + """ + expiration duration of the login in seconds + + :return: the login's expiration duration in seconds + :rtype: int + """ + ... + + @property + def auth(self) -> Auth: + """ + login authentication + + :return: the login's authentication + :rtype: Auth + """ + ... + + @property + def developer(self) -> Developer: + """ + developer of the login + + :return: the developer that logged in + :rtype: Developer + """ + ... + + @property + def temporary_api_token(self) -> str: + """ + returned temporary API token + + :return: the returned temporary API token + :rtype: str + """ + ... + + @property + def swagger_url(self) -> str: + """ + swagger URL (usually https://api.clashofclans.com/v1) + + :return: the swagger URL + :rtype: str + """ + ... diff --git a/pyclasher/api/requests/player.py b/pyclasher/api/requests/player.py index bf04f73..f0bb31c 100644 --- a/pyclasher/api/requests/player.py +++ b/pyclasher/api/requests/player.py @@ -3,7 +3,7 @@ from .request_models import RequestModel from ..models import Player, VerifyTokenRequest, VerifyTokenResponse -from ...client import RequestMethods +from ...utils.request_methods import RequestMethods from ...exceptions import ClientIsNotRunning, ApiCode diff --git a/pyclasher/api/requests/request_models.py b/pyclasher/api/requests/request_models.py index 2370ed7..c140b8c 100644 --- a/pyclasher/api/requests/request_models.py +++ b/pyclasher/api/requests/request_models.py @@ -3,7 +3,8 @@ from urllib.parse import quote, urlencode from ..models import Paging -from ...client import PyClasherClient, RequestMethods +from ...client import Client +from ...utils.request_methods import RequestMethods from ...exceptions import NoClient, ClientIsNotRunning, RequestNotDone, MISSING request_id = 0 @@ -29,11 +30,11 @@ def __init__(self, raw_url, kwargs=None, request_method=RequestMethods.REQUEST, :param url_kwargs: the url kwargs that are to replace in raw_url """ - if PyClasherClient.initialised: + if Client.initialised: global request_id self._request_id = request_id - self.client = PyClasherClient() + self.client = Client() self.client.logger.info(f"request {self._request_id} initialised") self._url = raw_url.format(**url_kwargs) diff --git a/pyclasher/client.py b/pyclasher/client.py index 3e2cbb4..8b432f1 100644 --- a/pyclasher/client.py +++ b/pyclasher/client.py @@ -1,228 +1,14 @@ -from asyncio import run, Queue, create_task, get_running_loop, new_event_loop, timeout -from enum import Enum -from json import dumps +from asyncio import run, create_task, get_running_loop, new_event_loop from typing import Iterable from urllib.parse import urlparse -from aiohttp import ClientSession, request +from .request_queue import PcConsumer, PcQueue +from .utils.login import Login +from .exceptions import (InvalidType, ClientIsRunning, ClientIsNotRunning, + NoneToken, MISSING) -from .api.models import ApiCodes -from .api.models import BaseModel -from .exceptions import InvalidLoginData, InvalidType, LoginNotDone, ClientIsRunning, ClientIsNotRunning, \ - NoneToken, MISSING -from .utils import ExecutionTimer - -class RequestMethods(Enum): - REQUEST = "get" - POST = "post" - - -class Status(BaseModel): - """ - class representing the status of the ClashOfClans API login - """ - - def __init__(self, data): - super().__init__(data) - self._main_attribute = self.code - return - - @property - def code(self): - return self._get_data('code') - - @property - def message(self): - return self._get_data('message') - - @property - def detail(self): - return self._get_data('detail') - - -class Auth(BaseModel): - def __init__(self, data): - super().__init__(data) - self._main_attribute = self.uid - return - - @property - def uid(self): - return self._get_data('uid') - - @property - def token(self): - return self._get_data('token') - - @property - def ua(self): - return self._get_data('ua') - - @property - def ip(self): - return self._get_data('ip') - - -class Developer(BaseModel): - def __init__(self, data): - super().__init__(data) - self._main_attribute = self.email - return - - @property - def id(self): - return self._get_data('id') - - @property - def name(self): - return self._get_data('name') - - @property - def game(self): - return self._get_data('game') - - @property - def email(self): - return self._get_data('email') - - @property - def tier(self): - return self._get_data('tier') - - @property - def allowed_scopes(self): - return self._get_data('allowedScopes') - - @property - def max_cidrs(self): - return self._get_data('maxCidrs') - - @property - def prev_login_ts(self): - return self._get_data('prevLoginTs') - - @property - def prev_login_ip(self): - return self._get_data('prevLoginIp') - - @property - def prev_login_ua(self): - return self._get_data('prevLoginUa') - - -class Login: - login_url = "https://developer.clashofclans.com/api/login" - __response = None - - def __init__(self, email, password): - self.email = email - self.__password = password - - return - - @property - def status(self): - if self.__response is None: - raise LoginNotDone - return Status(self.__response['status']) - - @property - def session_expires_in_seconds(self): - return self.__response['sessionExpiresInSeconds'] - - @property - def auth(self): - return Auth(self.__response['auth']) - - @property - def developer(self): - return Developer(self.__response['developer']) - - @property - def temporary_api_token(self): - return self.__response['temporaryAPIToken'] - - @property - def swagger_url(self): - return self.__response['swaggerUrl'] - - def login(self): - async def async_login(): - async with request("post", self.login_url, json={ - "email": self.email, - "password": self.__password - }) as response: - if response.status == 200: - self.__response = await response.json() - return self - else: - raise InvalidLoginData - - try: - get_running_loop() - except RuntimeError: - return run(async_login()) - else: - return async_login() - - def __repr__(self): - return f"Login(email={self.email}, password={'*' * len(self.__password)}, " \ - f"status={self.status}, session_expires_in_seconds={self.session_expires_in_seconds}, auth={self.auth}, " \ - f"developer={self.developer}, temporary_api_token={self.temporary_api_token}, swagger_url={self.swagger_url})" - - def __str__(self): - return f"Login({self.email})" - - -class RequestQueue(Queue): - async def put(self, future, request_url, request_method, body, status, error): - return await super().put((future, request_url, request_method, body, status, error)) - - -class Consumer: - def __init__(self, queue, token, requests_per_s, request_timeout, url): - self.queue = queue - self.header = { - 'Authorization': f'Bearer {token}' - } - self.r_p_s = requests_per_s - self.timeout = request_timeout - self.wait = 1 / self.r_p_s - self.url = url - self.session = ClientSession( - base_url=url, - headers=self.header - ) - 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 - - async def consume(self): - while True: - future, url, method, body, status, error = await self.queue.get() - - async with ExecutionTimer(self.wait): - create_task(self._request(future, url, method.value, body, status, error)) - - self.queue.task_done() - - async def close(self): - await self.session.close() - return - - -class PyClasherClient: +class Client: __instance = None base_url = "https://api.clashofclans.com" @@ -251,7 +37,7 @@ def __init__( logger=MISSING, swagger_url=None ): - if not PyClasherClient.initialised: + if not Client.initialised: if logger is None: logger = MISSING self.logger = logger @@ -273,11 +59,11 @@ def __init__( self.logger.debug("pyclasher client initialised") - self.queue = RequestQueue() + self.queue = PcQueue() self.__loop = new_event_loop() self.request_timeout = request_timeout - PyClasherClient.initialised = True + Client.initialised = True return @classmethod @@ -290,7 +76,10 @@ async def from_async_login(): logger.info("initialising pyclasher client via login") - self = cls([login.temporary_api_token for login in logins], requests_per_second, request_timeout, swagger_url=logins[0].swagger_url) + self = cls([login.temporary_api_token for login in logins], + requests_per_second, + request_timeout, + swagger_url=logins[0].swagger_url) self.logger = logger self.__temporary_session = True return self @@ -308,7 +97,7 @@ def is_running(self) -> bool: def start(self, tokens=None): async def async_consumer_start(tokens_): - self.__consumers = [Consumer(self.queue, token, self.requests_per_second, self.request_timeout, self.base_url) for token in tokens_] + self.__consumers = [PcConsumer(self.queue, token, self.requests_per_second, self.request_timeout, self.base_url) for token in tokens_] self.__consume_tasks = [create_task(consumer.consume()) for consumer in self.__consumers] self.logger.debug("pyclasher client started") return self @@ -338,7 +127,7 @@ async def async_consumer_start(tokens_): return self.__loop.run_until_complete(async_consumer_start(tokens)) else: self.__consumers = [ - Consumer( + PcConsumer( self.queue, token, self.requests_per_second, self.request_timeout, self.base_url) for token in tokens ] @@ -387,8 +176,8 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): return def __del__(self): - PyClasherClient.__instance = None - PyClasherClient.initialised = False + Client.__instance = None + Client.initialised = False if self.__client_running: self.close() @@ -407,7 +196,7 @@ def reset_client(self, reset_queue=True, reset_loop=True, reset_tokens=True): if not self.is_running: if reset_queue: del self.queue - self.queue = RequestQueue() + self.queue = PcQueue() if reset_loop: self.__loop.stop() diff --git a/pyclasher/client.pyi b/pyclasher/client.pyi index d2a6bd0..eb3994b 100644 --- a/pyclasher/client.pyi +++ b/pyclasher/client.pyi @@ -9,378 +9,10 @@ from .api.models import BaseModel from .exceptions import MISSING -class RequestMethods(Enum): - REQUEST = "get" - POST = "post" -class Status(BaseModel): - """ - class representing the status of the ClashOfClans API login - """ - - @property - def code(self) -> int: - """ - status code - - :return: the status code - :rtype: int - """ - ... - - @property - def message(self) -> str: - """ - status message - - :return: the status message - :rtype: str - """ - ... - - @property - def detail(self): - ... - - -class Auth(BaseModel): - """ - class representing the authentication of the ClashOfClans API login - """ - - @property - def uid(self) -> str: - """ - user id of the authentication - - :return: the user id - :rtype: str - """ - ... - - @property - def token(self) -> str: - """ - user token - - :return: the user token - :rtype: str - """ - ... - - @property - def ua(self): - """ - user agent of the authentication - - :return: the user agent of the authentication - """ - ... - - @property - def ip(self): - ... - - -class Developer(BaseModel): - """ - class representing the developer that logged in via the ClashOfClans login API - """ - - @property - def id(self) -> str: - """ - id of the developer - - :return: the developer's id - :rtype: str - """ - ... - - @property - def name(self) -> str: - """ - name of the developer - - :return: the developer's name - :rtype: str - """ - ... - - @property - def game(self) -> str: - """ - game of the developer - - :return: the developer's name - :rtype: str - """ - ... - - @property - def email(self) -> str: - """ - email address of the developer - - :return: the developer's email address - :rtype: str - """ - ... - - @property - def tier(self) -> str: - """ - tier of the developer - - :return: the developer's tier - :rtype: str - """ - ... - - @property - def allowed_scopes(self): - """ - allowed scopes of the developer - - :return: the developer's allowed scopes - """ - ... - - @property - def max_cidrs(self): - """ - max cidrs of the developer - - :return: the developer's max cidrs - """ - ... - - @property - def prev_login_ts(self) -> str: - """ - previous login timestamp of the developer - :return: the developer's previous login timestamp - :rtype: str - """ - ... - - @property - def prev_login_ip(self) -> str: - """ - previous login ip address of the developer - - :return: the developer's previous login ip address - :rtype: str - """ - ... - - @property - def prev_login_ua(self) -> str: - """ - previous login user agent of the developer - - :return: the developer's previous login user agent - :rtype: str - """ - ... - - -class Login: - """ - class to log in via the ClashOfClans login API - - to execute the login use ``Login(...).login()`` or ``await Login(...).login()`` depending on the context - """ - - login_url = "https://developer.clashofclans.com/api/login" - __response: dict - - def __init__(self, email: str, password: str) -> None: - self.email = email - self.__password = password - - @property - def status(self) -> Status: - """ - login status - - :return: the login status - :rtype: Status - """ - ... - - @property - def session_expires_in_seconds(self) -> int: - """ - expiration duration of the login in seconds - - :return: the login's expiration duration in seconds - :rtype: int - """ - ... - @property - def auth(self) -> Auth: - """ - login authentication - - :return: the login's authentication - :rtype: Auth - """ - ... - - @property - def developer(self) -> Developer: - """ - developer of the login - - :return: the developer that logged in - :rtype: Developer - """ - ... - - @property - def temporary_api_token(self) -> str: - """ - returned temporary API token - - :return: the returned temporary API token - :rtype: str - """ - ... - - @property - def swagger_url(self) -> str: - """ - swagger URL (usually https://api.clashofclans.com/v1) - - :return: the swagger URL - :rtype: str - """ - ... - - def login(self) -> Login | Coroutine[Any, Any, Login]: - """ - method to execute the login process - - This method can be called in an asynchronous context using - the ``await`` keyword in an asynchronous definition or used - as a traditional method without awaiting it. - - :return: the login - :rtype: Login | Coroutine[Any, Any, Login] - """ - ... - - def __repr__(self) -> str: - ... - - def __str__(self) -> str: - ... - - -class RequestQueue(Queue): - async def put(self, - future: Future, - request_url: str, - request_method: RequestMethods, - body: dict | None, - status: Future, - error: Future) -> None: - ... - - async def get(self) -> tuple[Future, str, RequestMethods, dict | None, Future, Future]: - ... - - -class Consumer: - """ - consumer class that consumes the requests and returns the responses of the ClashOfClans API - - :ivar queue: the queue where the requests are enqueued - :type queue: Queue - :ivar r_p_s: allowed number of requests that can be done with one consumer in one second - :type r_p_s: int - :ivar url: the base URL for the requests - :type url: str - """ - - def __init__(self, - queue: RequestQueue, - token: str, - requests_per_s: int, - request_timeout: float | None, - url: str - ) -> None: - """ - initialisation of the request consumer - - :param queue: the queue where the requests are enqueued - :type queue: Queue - :param token: one ClashOfClans API token - :type token: str - :param requests_per_s: allowed number of requests that can be done with one consumer in one second - :type requests_per_s: int - :param request_timeout: seconds until the request is cancelled due to a timeout - :type request_timeout: float - :param url: the base URL for the requests - :type url: str - :return: None - :rtype: None - """ - self.queue = queue - self.header = { - 'Authorization': f'Bearer {token}' - } - self.r_p_s = requests_per_s - self.timeout = request_timeout - self.wait = 1 / self.r_p_s - self.url = url - self.session = ClientSession( - base_url=url, - headers=self.header - ) - - async def _request(self, - future: Future, - url: str, method: str, - body: dict | None, - status: Future, - error: Future - ) -> None: - """ - asynchronous method that executes one request - - :param future: the future object of the response - :param url: the request's parsed url - :param method: the request method (post or get) - :param body: optional body (for post requests) - :return: None - :rtype: None - :raise ApiException: if the request fails - """ - ... - - async def consume(self) -> None: - """ - asynchronous method that is used as a consuming task that consumes requests forever until stopped - - :return: None - :rtype: None - .. note:: uses an infinite while loop, only run it as an asyncio task - """ - ... - - async def close(self) -> None: - """ - asynchronous method that closed the consumer - - :return: None - :rtype: None - """ - ... class PyClasherClient: @@ -393,7 +25,7 @@ class PyClasherClient: :type base_url: str :cvar endpoint: the public endpoint URL for the requests (usually /v1) :type endpoint: str - :cvar queue: the public queue where the requests are enqueued + :cvar queue: the public request_queue where the requests are enqueued :type queue: RequestQueue :cvar requests_per_second: the public number of requests done per consumer/token per second (usually 5) :type requests_per_second: int @@ -403,7 +35,7 @@ class PyClasherClient: :type initialised: bool :cvar __loop: abstract event loop that is used for making requests if no loop is running :type __loop: AbstractEventLoop - :cvar __consumers: private list of consumers of the queue and requests + :cvar __consumers: private list of consumers of the request_queue and requests :type __consumers: list[Consumer] :cvar __consume_tasks: private list of tasks of the consumer :type __consume_tasks: list[Task] diff --git a/pyclasher/request_queue/__init__.py b/pyclasher/request_queue/__init__.py new file mode 100644 index 0000000..9764cb9 --- /dev/null +++ b/pyclasher/request_queue/__init__.py @@ -0,0 +1,2 @@ +from .request_queue import PcQueue +from .request_consumer import PcConsumer diff --git a/pyclasher/request_queue/request_consumer.py b/pyclasher/request_queue/request_consumer.py new file mode 100644 index 0000000..5c3126f --- /dev/null +++ b/pyclasher/request_queue/request_consumer.py @@ -0,0 +1,49 @@ +from asyncio import timeout, create_task +from json import dumps + +from aiohttp import ClientSession + +from ..api.models import ApiCodes +from ..utils import ExecutionTimer + + +class PcConsumer: + def __init__(self, queue, token, requests_per_s, request_timeout, url): + self.queue = queue + self.header = { + 'Authorization': f'Bearer {token}' + } + self.r_p_s = requests_per_s + self.timeout = request_timeout + self.wait = 1 / self.r_p_s + self.url = url + self.session = ClientSession( + base_url=url, + headers=self.header + ) + 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 + + async def consume(self): + while True: + future, url, method, body, status, error = await self.queue.get() + + async with ExecutionTimer(self.wait): + create_task(self._request(future, url, method.value, body, status, error)) + + self.queue.task_done() + + async def close(self): + await self.session.close() + return diff --git a/pyclasher/request_queue/request_consumer.pyi b/pyclasher/request_queue/request_consumer.pyi new file mode 100644 index 0000000..41d0159 --- /dev/null +++ b/pyclasher/request_queue/request_consumer.pyi @@ -0,0 +1,92 @@ +from asyncio import Future +from aiohttp import ClientSession + +from .request_queue import PcQueue + + +class PcConsumer: + """ + consumer class that consumes the requests and returns the responses of the ClashOfClans API + + :ivar queue: the request_queue where the requests are enqueued + :type queue: Queue + :ivar r_p_s: allowed number of requests that can be done with one consumer in one second + :type r_p_s: int + :ivar url: the base URL for the requests + :type url: str + """ + + def __init__(self, + queue: PcQueue, + token: str, + requests_per_s: int, + request_timeout: float | None, + url: str + ) -> None: + """ + initialisation of the request consumer + + :param queue: the request_queue where the requests are enqueued + :type queue: Queue + :param token: one ClashOfClans API token + :type token: str + :param requests_per_s: allowed number of requests that can be done with one consumer in one second + :type requests_per_s: int + :param request_timeout: seconds until the request is cancelled due to a timeout + :type request_timeout: float + :param url: the base URL for the requests + :type url: str + :return: None + :rtype: None + """ + self.queue = queue + self.header = { + 'Authorization': f'Bearer {token}' + } + self.r_p_s = requests_per_s + self.timeout = request_timeout + self.wait = 1 / self.r_p_s + self.url = url + self.session = ClientSession( + base_url=url, + headers=self.header + ) + + async def _request(self, + future: Future, + url: str, method: str, + body: dict | None, + status: Future, + error: Future + ) -> None: + """ + asynchronous method that executes one request + + :param future: the future object of the response + :param url: the request's parsed url + :param method: the request method (post or get) + :param body: optional body (for post requests) + :return: None + :rtype: None + :raise ApiException: if the request fails + """ + ... + + async def consume(self) -> None: + """ + asynchronous method that is used as a consuming task that consumes requests forever until stopped + + :return: None + :rtype: None + .. note:: uses an infinite while loop, only run it as an asyncio task + """ + ... + + async def close(self) -> None: + """ + asynchronous method that closed the consumer + + :return: None + :rtype: None + """ + ... \ No newline at end of file diff --git a/pyclasher/request_queue/request_queue.py b/pyclasher/request_queue/request_queue.py new file mode 100644 index 0000000..d638256 --- /dev/null +++ b/pyclasher/request_queue/request_queue.py @@ -0,0 +1,6 @@ +from asyncio import Queue + + +class PcQueue(Queue): + async def put(self, future, request_url, request_method, body, status, error): + return await super().put((future, request_url, request_method, body, status, error)) diff --git a/pyclasher/request_queue/request_queue.pyi b/pyclasher/request_queue/request_queue.pyi new file mode 100644 index 0000000..0fd4fe7 --- /dev/null +++ b/pyclasher/request_queue/request_queue.pyi @@ -0,0 +1,17 @@ +from asyncio import Queue, Future + +from ..utils.request_methods import RequestMethods + + +class PcQueue(Queue): + async def put(self, + future: Future, + request_url: str, + request_method: RequestMethods, + body: dict | None, + status: Future, + error: Future) -> None: + ... + + async def get(self) -> tuple[Future, str, RequestMethods, dict | None, Future, Future]: + ... \ No newline at end of file diff --git a/pyclasher/utils/login.py b/pyclasher/utils/login.py new file mode 100644 index 0000000..9a441d1 --- /dev/null +++ b/pyclasher/utils/login.py @@ -0,0 +1,50 @@ +from asyncio import get_running_loop, run + +from aiohttp import request + +from ..api.models.login import LoginModel +from ..exceptions import Missing, MISSING, LoginNotDone, InvalidLoginData + + +class Login(LoginModel): + login_url = "https://developer.clashofclans.com/api/login" + __response = None + + def __new__(cls, *args, **kwargs): + return super().__new__(cls) + + def __init__(self, email, password): + super().__init__(data=None) + self.email = email + self.__password = password + + return + + def _get_data(self, item: str) -> None | Missing | dict | list | int | str | float | bool: + if self._data is None: + return None + if self._data is MISSING: + raise LoginNotDone + if item in self._data: + return self._data[item] + else: + return MISSING + + def login(self): + async def async_login(): + async with request("post", self.login_url, json={ + "email": self.email, + "password": self.__password + }) as response: + if response.status == 200: + self._data = await response.json() + return self + else: + raise InvalidLoginData + + try: + get_running_loop() + except RuntimeError: + return run(async_login()) + else: + return async_login() diff --git a/pyclasher/utils/login.pyi b/pyclasher/utils/login.pyi new file mode 100644 index 0000000..f2cc60c --- /dev/null +++ b/pyclasher/utils/login.pyi @@ -0,0 +1,39 @@ +from asyncio import get_running_loop, run +from typing import Coroutine + +from aiohttp import request + +from ..api.models.login import LoginModel +from ..exceptions import Missing, MISSING, LoginNotDone, InvalidLoginData + + +class Login(LoginModel): + """ + class to log in via the ClashOfClans login API + + to execute the login use ``Login(...).login()`` or ``await Login(...).login()`` depending on the context + """ + + login_url = "https://developer.clashofclans.com/api/login" + __response: dict + + def __new__(cls, *args, **kwargs) -> Login: + ... + + def __init__(self, email: str, password: str) -> None: + self.email = email + self.__password = password + ... + + def login(self) -> Login | Coroutine[Any, Any, Login]: + """ + method to execute the login process + + This method can be called in an asynchronous context using + the ``await`` keyword in an asynchronous definition or used + as a traditional method without awaiting it. + + :return: the login + :rtype: Login | Coroutine[Any, Any, Login] + """ + ... diff --git a/pyclasher/utils/request_methods.py b/pyclasher/utils/request_methods.py new file mode 100644 index 0000000..9679a2c --- /dev/null +++ b/pyclasher/utils/request_methods.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class RequestMethods(Enum): + REQUEST = "get" + POST = "post" diff --git a/pyclasher/utils/request_methods.pyi b/pyclasher/utils/request_methods.pyi new file mode 100644 index 0000000..9679a2c --- /dev/null +++ b/pyclasher/utils/request_methods.pyi @@ -0,0 +1,6 @@ +from enum import Enum + + +class RequestMethods(Enum): + REQUEST = "get" + POST = "post" diff --git a/tests/requests/async_tests/conftest.py b/tests/requests/async_tests/conftest.py index f1aaa2d..2a4ed7f 100644 --- a/tests/requests/async_tests/conftest.py +++ b/tests/requests/async_tests/conftest.py @@ -2,7 +2,7 @@ import pytest_asyncio from asyncio import new_event_loop -from pyclasher import PyClasherClient +from pyclasher import Client from ...constants import CLASH_OF_CLANS_LOGIN_EMAIL, CLASH_OF_CLANS_LOGIN_PASSWORD @@ -21,7 +21,7 @@ def event_loop(): @pytest_asyncio.fixture(scope="package") async def pyclasher_client(event_loop): print("Setting PyClasherClient ...") - client = await PyClasherClient.from_login(CLASH_OF_CLANS_LOGIN_EMAIL, CLASH_OF_CLANS_LOGIN_PASSWORD) + client = await Client.from_login(CLASH_OF_CLANS_LOGIN_EMAIL, CLASH_OF_CLANS_LOGIN_PASSWORD) client.start() yield client diff --git a/tests/test_client.py b/tests/test_client.py index 1df1c59..9953b6e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -2,18 +2,18 @@ from asyncio import Queue, AbstractEventLoop -from pyclasher import PyClasherClient +from pyclasher import Client from .constants import CLASH_OF_CLANS_LOGIN_EMAIL, CLASH_OF_CLANS_LOGIN_PASSWORD @pytest.mark.asyncio async def test_client(): - assert not PyClasherClient.initialised + assert not Client.initialised - client = await PyClasherClient.from_login(CLASH_OF_CLANS_LOGIN_EMAIL, CLASH_OF_CLANS_LOGIN_PASSWORD) + client = await Client.from_login(CLASH_OF_CLANS_LOGIN_EMAIL, CLASH_OF_CLANS_LOGIN_PASSWORD) - assert PyClasherClient.initialised + assert Client.initialised assert not client.is_running assert isinstance(client.queue, Queue)