From bae482191751e86ef398a648f9a182d9d2d63456 Mon Sep 17 00:00:00 2001 From: 201st-Luka Date: Wed, 23 Aug 2023 09:38:56 +0200 Subject: [PATCH] feat: added support for multiple clients --- pyclasher/api/requests/abc.py | 2 +- pyclasher/client.py | 150 ++++++++++++------- pyclasher/client.pyi | 32 ++-- pyclasher/exceptions.py | 3 +- pyclasher/request_queue/__init__.py | 4 +- pyclasher/request_queue/request_consumer.py | 2 +- pyclasher/request_queue/request_consumer.pyi | 6 +- pyclasher/request_queue/request_queue.py | 2 +- pyclasher/request_queue/request_queue.pyi | 2 +- tests/test_client.py | 4 +- 10 files changed, 135 insertions(+), 72 deletions(-) diff --git a/pyclasher/api/requests/abc.py b/pyclasher/api/requests/abc.py index 97e3c77..c121c2a 100644 --- a/pyclasher/api/requests/abc.py +++ b/pyclasher/api/requests/abc.py @@ -34,7 +34,7 @@ def __init__(self, :param url_kwargs: the url kwargs that are to replace in raw_url """ - if Client.initialised: + if Client.initialized(): global request_id self._request_id = request_id diff --git a/pyclasher/client.py b/pyclasher/client.py index 8b704a2..bd5c505 100644 --- a/pyclasher/client.py +++ b/pyclasher/client.py @@ -1,28 +1,45 @@ -import sys +from sys import stderr from asyncio import create_task, run from typing import Iterable from urllib.parse import urlparse -from .request_queue import PcConsumer, PcQueue +from .request_queue import PConsumer, PQueue from .utils.login import Login from .exceptions import (InvalidType, ClientIsRunning, ClientIsNotRunning, NoneToken, MISSING, ClientAlreadyInitialised) +client_id = 0 + + class Client: - __instance = None + __instances = None base_url = "https://api.clashofclans.com" endpoint = "/v1" requests_per_second = 5 logger = MISSING - initialised = False def __new__(cls, *args, **kwargs): - if cls.__instance is None: - cls.__instance = super().__new__(cls) - return cls.__instance - raise ClientAlreadyInitialised + if cls.__instances is None: + cls.__instances = [super().__new__(cls)] + return cls.__instances[0] + if 'tokens' in kwargs: + if isinstance(kwargs['tokens'], str): + tokens = [kwargs['tokens']] + elif isinstance(kwargs['tokens'], Iterable): + tokens = list(kwargs['tokens']) + else: + raise InvalidType(kwargs['tokens'], (str, Iterable[str])) + for token in tokens: + for client in Client.__instances: + if client.__tokens is not None: + if token in client.__tokens: + raise ClientAlreadyInitialised + continue + + cls.__instances.append(super().__new__(cls)) + return cls.__instances[-1] def __init__( self, @@ -32,36 +49,42 @@ def __init__( logger=MISSING, swagger_url=None ): - if not Client.initialised: - if logger is None: - logger = MISSING - self.logger = logger - self.logger.info("initialising pyclasher client") - if tokens is not None: - if isinstance(tokens, str): - self.__tokens = [tokens] - elif isinstance(tokens, Iterable): - self.__tokens = list(tokens) - else: - raise InvalidType(tokens, (str, Iterable[str])) - - if swagger_url is not None: - parsed_url = urlparse(swagger_url) - self.base_url = f"{parsed_url.scheme}://{parsed_url.netloc}" - self.endpoint = parsed_url.path[:-1] - - self.requests_per_second = requests_per_second - - self.logger.debug("pyclasher client initialised") - - self.queue = PcQueue() - self.request_timeout = request_timeout - - Client.initialised = True - self.__client_running = False - self.__temporary_session = False - self.__consumers = None - self.__consume_tasks = None + global client_id + + if logger is None: + logger = MISSING + self.logger = logger + self.logger.info("initialising client") + if tokens is not None: + if isinstance(tokens, str): + self.__tokens = [tokens] + elif isinstance(tokens, Iterable): + self.__tokens = list(tokens) + else: + raise TypeError(f"Expected types str, list got {type(tokens)} " + f"instead") + + if swagger_url is not None: + parsed_url = urlparse(swagger_url) + self.base_url = f"{parsed_url.scheme}://{parsed_url.netloc}" + self.endpoint = parsed_url.path[:-1] + + self.requests_per_second = requests_per_second + + self.logger.debug("client initialised") + + self.queue = PQueue() + self.request_timeout = request_timeout + + self.__client_running = False + self.__temporary_session = False + self.__consumers = None + self.__consume_tasks = None + self._client_id = client_id + + client_id += 1 + + self._event_client = False return @@ -75,7 +98,7 @@ async def from_login(cls, email, password, requests_per_second=5, await Login(email, password).login() for _ in range(login_count) ] - logger.info("initialising pyclasher client via login") + logger.info("initialising client via login") self = cls([login.temporary_api_token for login in logins], requests_per_second, @@ -103,22 +126,22 @@ async def start(self, tokens=None): raise ClientIsRunning self.__client_running = True - self.logger.info("starting pychlasher client") + self.logger.info("starting client") self.__consumers = [ - PcConsumer(self.queue, token, self.requests_per_second, - self.request_timeout, self.base_url) + PConsumer(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") + self.logger.debug("client started") return self async def close(self): - self.logger.info("closing pyclasher client") + self.logger.info("closing client") if not self.__client_running: self.logger.error("the client is not running") raise ClientIsNotRunning @@ -132,7 +155,7 @@ async def close(self): await consumer.close() self.__consumers = None - self.logger.debug("pyclasher client closed") + self.logger.debug("client closed") return self async def __aenter__(self): @@ -143,16 +166,16 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): return def __del__(self): - Client.__instance = None - Client.initialised = False + Client.__instances.remove(self) + if not len(Client.__instances): + Client.__instances = None if self.__client_running: run(self.close()) if self.logger is not MISSING: self.logger.warning("The client was still running, closed now.") else: - print("The client was still running, closed now.", - file=sys.stderr) + print("The client was still running, closed now.", file=stderr) return @@ -160,6 +183,31 @@ def __del__(self): def is_running(self) -> bool: return self.__client_running + @property + def client_id(self): + return self._client_id + + @client_id.setter + def client_id(self, new_id): + if not isinstance(new_id, (int, str)): + raise TypeError(f"Expected types int, str got {type(new_id)} " + f"instead.") + self._client_id = new_id + return + + @classmethod + def get_instance(cls, client_id=None): + if cls.__instances is None: + return None + clients = [client + for client in cls.__instances + if not client._event_client] + if len(clients): + if client_id is None: + return clients[0] + return clients[client_id] + return None + @classmethod - def get_instance(cls): - return Client.__instance + def initialized(cls): + return isinstance(cls.__instances, list) diff --git a/pyclasher/client.pyi b/pyclasher/client.pyi index a56fd44..bff0e46 100644 --- a/pyclasher/client.pyi +++ b/pyclasher/client.pyi @@ -2,15 +2,18 @@ from logging import Logger from typing import Iterable from .exceptions import MISSING -from .request_queue import PcQueue +from .request_queue import PQueue + + +client_id: int = ... class Client: """ this is the class for the ClashOfClans API client - :cvar __instance: the private instance of the client - :type __instance: Client + :cvar __instances: the private instance of the client + :type __instances: Client :cvar base_url: the public base URL for the requests (usually https://api.clashofclans.com) :type base_url: str :cvar endpoint: the public endpoint URL for the requests (usually /v1) @@ -19,8 +22,6 @@ class Client: :type requests_per_second: int :cvar logger: public logger to log the requests, ... (usually MISSING) :type logger: Logger - :cvar initialised: public boolean that indicates if the - :type initialised: bool :ivar queue: the public request_queue where the requests are enqueued :type queue: RequestQueue :ivar __consumers: private list of consumers of the request_queue and requests @@ -35,13 +36,12 @@ class Client: :type __client_running: bool """ - __instance: Client = None + __instances: list[Client] = None base_url: str = "https://api.clashofclans.com" endpoint: str = "/v1" requests_per_second: int = 5 logger: Logger = MISSING - initialised = False def __new__(cls, *args, **kwargs): ... @@ -77,12 +77,14 @@ class Client: self.logger: Logger = ... self.__tokens: list[str] = ... self.requests_per_second: int = ... - self.queue: PcQueue = ... + self.queue: PQueue = ... self.request_timeout: float = ... self.__client_running: bool = ... self.__temporary_session: bool = ... self.__consumers: list = ... self.__consume_tasks: list = ... + self._client_id: int | str = ... + self._event_client: bool = ... ... @classmethod @@ -153,6 +155,18 @@ class Client: """ ... + @property + def client_id(self): + ... + + @client_id.setter + def client_id(self, new_id: int | str): + self._client_id: int | str = ... + + @classmethod + def get_instance(cls, client_id: int | str = None) -> None | Client: + ... + @classmethod - def get_instance(cls) -> None | Client: + def initialized(cls) -> bool: ... diff --git a/pyclasher/exceptions.py b/pyclasher/exceptions.py index bc08846..661d46b 100644 --- a/pyclasher/exceptions.py +++ b/pyclasher/exceptions.py @@ -89,7 +89,8 @@ def __str__(self): class ClientAlreadyInitialised(PyClasherException): def __str__(self): - return "The PyClasherClient has already been initialised." + return ("It is not possible to create multiple clients with the same " + "tokens.") class NoClient(PyClasherException): diff --git a/pyclasher/request_queue/__init__.py b/pyclasher/request_queue/__init__.py index 9764cb9..8713b0a 100644 --- a/pyclasher/request_queue/__init__.py +++ b/pyclasher/request_queue/__init__.py @@ -1,2 +1,2 @@ -from .request_queue import PcQueue -from .request_consumer import PcConsumer +from .request_queue import PQueue +from .request_consumer import PConsumer diff --git a/pyclasher/request_queue/request_consumer.py b/pyclasher/request_queue/request_consumer.py index 5c3126f..8a25d34 100644 --- a/pyclasher/request_queue/request_consumer.py +++ b/pyclasher/request_queue/request_consumer.py @@ -7,7 +7,7 @@ from ..utils import ExecutionTimer -class PcConsumer: +class PConsumer: def __init__(self, queue, token, requests_per_s, request_timeout, url): self.queue = queue self.header = { diff --git a/pyclasher/request_queue/request_consumer.pyi b/pyclasher/request_queue/request_consumer.pyi index 41d0159..7468242 100644 --- a/pyclasher/request_queue/request_consumer.pyi +++ b/pyclasher/request_queue/request_consumer.pyi @@ -1,10 +1,10 @@ from asyncio import Future from aiohttp import ClientSession -from .request_queue import PcQueue +from .request_queue import PQueue -class PcConsumer: +class PConsumer: """ consumer class that consumes the requests and returns the responses of the ClashOfClans API @@ -17,7 +17,7 @@ class PcConsumer: """ def __init__(self, - queue: PcQueue, + queue: PQueue, token: str, requests_per_s: int, request_timeout: float | None, diff --git a/pyclasher/request_queue/request_queue.py b/pyclasher/request_queue/request_queue.py index d638256..c90fd88 100644 --- a/pyclasher/request_queue/request_queue.py +++ b/pyclasher/request_queue/request_queue.py @@ -1,6 +1,6 @@ from asyncio import Queue -class PcQueue(Queue): +class PQueue(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 index 0fd4fe7..1f40656 100644 --- a/pyclasher/request_queue/request_queue.pyi +++ b/pyclasher/request_queue/request_queue.pyi @@ -3,7 +3,7 @@ from asyncio import Queue, Future from ..utils.request_methods import RequestMethods -class PcQueue(Queue): +class PQueue(Queue): async def put(self, future: Future, request_url: str, diff --git a/tests/test_client.py b/tests/test_client.py index 185ee38..c1dfbad 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -10,12 +10,12 @@ @pytest.mark.tryfirst @pytest.mark.asyncio async def test_client(): - assert not Client.initialised + assert not Client.initialized() client = await Client.from_login(CLASH_OF_CLANS_LOGIN_EMAIL, CLASH_OF_CLANS_LOGIN_PASSWORD) - assert Client.initialised + assert Client.initialized() assert not client.is_running assert isinstance(client.queue, Queue)