Skip to content

Commit

Permalink
feat: added support for multiple clients
Browse files Browse the repository at this point in the history
  • Loading branch information
201st-Luka committed Aug 23, 2023
1 parent e0e7135 commit bae4821
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 72 deletions.
2 changes: 1 addition & 1 deletion pyclasher/api/requests/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
150 changes: 99 additions & 51 deletions pyclasher/client.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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

Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -143,23 +166,48 @@ 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

@property
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)
32 changes: 23 additions & 9 deletions pyclasher/client.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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):
...
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
...
3 changes: 2 additions & 1 deletion pyclasher/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions pyclasher/request_queue/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from .request_queue import PcQueue
from .request_consumer import PcConsumer
from .request_queue import PQueue
from .request_consumer import PConsumer
2 changes: 1 addition & 1 deletion pyclasher/request_queue/request_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
6 changes: 3 additions & 3 deletions pyclasher/request_queue/request_consumer.pyi
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -17,7 +17,7 @@ class PcConsumer:
"""

def __init__(self,
queue: PcQueue,
queue: PQueue,
token: str,
requests_per_s: int,
request_timeout: float | None,
Expand Down
2 changes: 1 addition & 1 deletion pyclasher/request_queue/request_queue.py
Original file line number Diff line number Diff line change
@@ -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))
2 changes: 1 addition & 1 deletion pyclasher/request_queue/request_queue.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down

0 comments on commit bae4821

Please sign in to comment.