diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8ec19e4..0a05df6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -21,4 +21,5 @@ jobs: restore-keys: | mkdocs-material- - run: pip install -r requirements-docs.txt + - run: python doc_gen.py - run: mkdocs gh-deploy --force diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1a6afd3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,81 @@ +# Contributing + +Contributing to this open-source project is appreciated. To contribute please +visit the Discord server as well. + +--- + +## Where to start + +The contribution starts with an issue. The created issue should explain what to +do. Based on this issue you can start contributing. If so, refer to the issue in +the pull request. + +### Issues + +The issues must follow these guidelines: +- An issue must not be a duplicate of an existing issue. +- A bug issue must provide clear information about the bug. +- A feature request should contain information about what the feature should do +and what it serves. + +Irrelevant issues, duplicates or issues failing to comply to these guidelines +will be closed. + +### Pull requests + +After solving the issue it is possible to create a pull request. This pull +request must target the `unstable` branch. From there it will be part of the bot +in the next release. + +Requirements for a pull request are: +- Commits must be clear +- The pull request must be up-to-date with the `stable` branch +- All checks (if applicable) must pass +- A review must be requested from at least one developer (201st-Luka) + +### Recognising the contribution + +When the PR is merged in the `stable` branch. GitHub will automatically add the +user to the repository's contributor list. It is also possible to earn a role +on the Discord server for contribution. + +--- + +## Installation + +1. Fork the repository and copy it to your local machine. +2. Install the requirements: + - for developing: + - [aiohttp](https://pypi.org/project/aiohttp/) (`pip install aiohttp`) + + You can simply do + ```bash + pip install -r requirements.txt + ``` + - for testing: + - [aiohttp](https://pypi.org/project/aiohttp/) (`pip install aiohttp`) + - [pytest](https://pypi.org/project/pytest/) (`pip install pytest`) + - [pytest-asyncio](https://pypi.org/project/pytest-asyncio/) +(`pip install pytest-asyncio`) + You can simply do + ```bash + pip install -r requirements-tests.txt + ``` + - for creating the documentation: + - [mkdocs](https://pypi.org/project/mkdocs/) (`pip install mkdocs`) + - [mkdocs-material](https://pypi.org/project/mkdocs-material/) +(`pip install mkdocs-material`) + - [mkdocstrings[python]](https://pypi.org/project/mkdocstrings/) +(`pip install mkdocstrings[python]`) + - [mkdocs-awesome-pages-plugin](https://pypi.org/project/mkdocs-awesome-pages-plugin/) +(`pip install mkdocs-awesome-pages-plugin`) + You can simply do + ```bash + pip install -r requirements-docs.txt + ``` + In total, you should have installed 7 packages. + +--- + +You're done! Happy coding. diff --git a/README.md b/README.md index 05be1e0..27eff19 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,8 @@ Feel free to contribute to the repository. You can fork the repository and commit your changes in a pull request. Please consider to check out the [Discord server][discord_url] if so. +For more information, please see the [CONTRIBUTING.md](CONTRIBUTING.md) file + --- ## Future diff --git a/doc_gen.py b/doc_gen.py index a3999b8..aa3fc35 100644 --- a/doc_gen.py +++ b/doc_gen.py @@ -13,10 +13,11 @@ def create_markdown_structure(package_path, output_path): Args: package_path (str): The path to the Python package. - output_path (str): The path where the directory structure and markdown files will be generated. + output_path (str): The path where the directory structure and markdown + files will be generated. """ - for dirpath, dirnames, filenames in os.walk(package_path): - relative_dir = os.path.relpath(dirpath, package_path) + for dir_path, _, filenames in os.walk(package_path): + relative_dir = os.path.relpath(dir_path, package_path) output_dir = os.path.join(output_path, relative_dir) os.makedirs(output_dir, exist_ok=True) @@ -35,16 +36,29 @@ def create_markdown_structure(package_path, output_path): f"{module_name}.md") with open(markdown_file_path, 'w') as markdown_file: - print(f"Creating docs for {import_path}") + print(f"Creating markdown file for {import_path}") markdown_file.write(markdown_content) +def copy_contributing(package_path, docs_path): + source_file = os.path.join(package_path, "CONTRIBUTING.md") + dest_file = os.path.join(docs_path, "CONTRIBUTING.md") + + with open(source_file, 'r') as file: + print(f"Reading {source_file}...") + contributing = file.read() + + with open(dest_file, 'w') as copy: + print(f"Writing {dest_file}...") + copy.write(contributing) + + if __name__ == '__main__': - project_dir = get_project_dir() + project_dir = os.getcwd() doc_dir = os.path.join(project_dir, "docs") - api_ref_dir = os.path.join(doc_dir, "docs/API Reference") + api_ref_dir = os.path.join(doc_dir, "API Reference") pyclasher_dir = os.path.join(project_dir, "pyclasher") shutil.rmtree(api_ref_dir) @@ -52,3 +66,4 @@ def create_markdown_structure(package_path, output_path): os.mkdir(api_ref_dir) create_markdown_structure(pyclasher_dir, api_ref_dir) + copy_contributing(project_dir, doc_dir) diff --git a/docs/.pages b/docs/.pages new file mode 100644 index 0000000..1a8d97f --- /dev/null +++ b/docs/.pages @@ -0,0 +1,7 @@ +nav: + - Home: index.md + - API Reference: API Reference + - Contributing: + - CONTRIBUTING.md + - contributors.md + - Tutorials: Tutorials diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..1a6afd3 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,81 @@ +# Contributing + +Contributing to this open-source project is appreciated. To contribute please +visit the Discord server as well. + +--- + +## Where to start + +The contribution starts with an issue. The created issue should explain what to +do. Based on this issue you can start contributing. If so, refer to the issue in +the pull request. + +### Issues + +The issues must follow these guidelines: +- An issue must not be a duplicate of an existing issue. +- A bug issue must provide clear information about the bug. +- A feature request should contain information about what the feature should do +and what it serves. + +Irrelevant issues, duplicates or issues failing to comply to these guidelines +will be closed. + +### Pull requests + +After solving the issue it is possible to create a pull request. This pull +request must target the `unstable` branch. From there it will be part of the bot +in the next release. + +Requirements for a pull request are: +- Commits must be clear +- The pull request must be up-to-date with the `stable` branch +- All checks (if applicable) must pass +- A review must be requested from at least one developer (201st-Luka) + +### Recognising the contribution + +When the PR is merged in the `stable` branch. GitHub will automatically add the +user to the repository's contributor list. It is also possible to earn a role +on the Discord server for contribution. + +--- + +## Installation + +1. Fork the repository and copy it to your local machine. +2. Install the requirements: + - for developing: + - [aiohttp](https://pypi.org/project/aiohttp/) (`pip install aiohttp`) + + You can simply do + ```bash + pip install -r requirements.txt + ``` + - for testing: + - [aiohttp](https://pypi.org/project/aiohttp/) (`pip install aiohttp`) + - [pytest](https://pypi.org/project/pytest/) (`pip install pytest`) + - [pytest-asyncio](https://pypi.org/project/pytest-asyncio/) +(`pip install pytest-asyncio`) + You can simply do + ```bash + pip install -r requirements-tests.txt + ``` + - for creating the documentation: + - [mkdocs](https://pypi.org/project/mkdocs/) (`pip install mkdocs`) + - [mkdocs-material](https://pypi.org/project/mkdocs-material/) +(`pip install mkdocs-material`) + - [mkdocstrings[python]](https://pypi.org/project/mkdocstrings/) +(`pip install mkdocstrings[python]`) + - [mkdocs-awesome-pages-plugin](https://pypi.org/project/mkdocs-awesome-pages-plugin/) +(`pip install mkdocs-awesome-pages-plugin`) + You can simply do + ```bash + pip install -r requirements-docs.txt + ``` + In total, you should have installed 7 packages. + +--- + +You're done! Happy coding. diff --git a/docs/Tutorials/01_installation.md b/docs/Tutorials/01_installation.md new file mode 100644 index 0000000..8a18491 --- /dev/null +++ b/docs/Tutorials/01_installation.md @@ -0,0 +1,13 @@ +# Installation + +## Requirements + +To use the [PyClasher](../index.md)-package you need Python 3.8 or higher. + +## Installation + +The package is accessible on [PyPi.org](https://pypi.org) so you can install the package +using +```bash +pip install pyclasher +``` \ No newline at end of file diff --git a/docs/Tutorials/11_setting_up_a_Client.md b/docs/Tutorials/11_setting_up_a_Client.md new file mode 100644 index 0000000..1a3f987 --- /dev/null +++ b/docs/Tutorials/11_setting_up_a_Client.md @@ -0,0 +1,94 @@ +# Setting up a Client + +The [Client][client_ref] +is the heard of this package. It allows to make the requests. + +## The client + +The [Client][client_ref] uses API tokens that you can obtain from the official +[ClashOfClans developer portal](https://developer.clashofclans.com/#/). Those tokens serve as a verification to +the ClashOfClans API. It is sufficient to create an account and create one or +multiple tokens. + +## Setting up + +### With tokens + +To set up a client, you need to import the [Client][client_ref] from +`pyclasher.client` and adding the tokens to it: + +```python +import asyncio + +from pyclasher.client import Client + + +my_tokens = [ + "enter your first token here", + "enter the other tokens here separated as strings in a list" +] + +my_client = Client(tokens=my_tokens) + +async def main(): + # starting the client + await my_client.start() + + # doing requests here + ... + + # stopping the client + await my_client.close() + +asyncio.run(main()) +``` + +!!! note + It is recommended to use environment variables for the tokens. So you can + make sure that the tokens are never leaked on the Internet or on GitHub, ... + +### With ClashOfClans developer account + +It is also possible to set up a client using the login data from +https://developer.clashofclans.com/#/login. + +```python +import asyncio + +from pyclasher.client import Client + + +my_email = "email@example.com" +my_password = "examplePassword1234" + +async def login(): + return await Client.from_login( + my_email, my_password, + login_count=1 # this parameter is used to log in multiple times to the + # ClashOfClans developer portal and create multiple tokens + ) + +async def main(): + my_client = await login() + + # starting the client + await my_client.start() + + # requests + ... + + # stopping the client + await my_client.close() +``` + +!!! warning + This is an alternative method to get tokens. Those tokens are temporarily + and expire after one hour. It is intended for testing purpose only. + An implementation to renew the tokens after one hour will not ever happen. + Use [tokens](#with-tokens) for production purpose. + + + + +[client_ref]: ../API%20Reference/client/client.md#pyclasher.client.client.Client + diff --git a/docs/Tutorials/12_Requests.md b/docs/Tutorials/12_Requests.md new file mode 100644 index 0000000..50e0d9b --- /dev/null +++ b/docs/Tutorials/12_Requests.md @@ -0,0 +1,7 @@ +# Requests + +The requests defined in this package are a user-friendly implementation of +requesting and getting data of the official ClashOfClans-API. The requests +return models that are some kind of wrapper for the data that hides behind the +models. Those models have error handling and are easier to use than raw JSON +data. \ No newline at end of file diff --git a/docs/contributors.md b/docs/contributors.md new file mode 100644 index 0000000..256b94c --- /dev/null +++ b/docs/contributors.md @@ -0,0 +1,9 @@ +# Contributors + +## Project owner + +- [201st-Luka](https://github.com/201st-Luka) + +## Contributors + +No contributors yet. In the future, contributors are going to be added here. diff --git a/mkdocs.yml b/mkdocs.yml index eabc871..95bc011 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -42,10 +42,6 @@ theme: plugins: - search: lang: en - - git-committers: - repository: 201st-Luka/PyClasher - branch: master - token: !!python/object/apply:os.getenv ["MKDOCS_GIT_COMMITTERS_APIKEY"] - mkdocstrings: enabled: true custom_templates: templates @@ -56,6 +52,9 @@ plugins: show_source: true - awesome-pages +markdown_extensions: + - admonition + extra: version: provider: mike @@ -68,4 +67,4 @@ extra: name: PyClasher on GitHub watch: - - pyclasher \ No newline at end of file + - pyclasher diff --git a/pyclasher/client/client.py b/pyclasher/client/client.py index 7832f06..6800b73 100644 --- a/pyclasher/client/client.py +++ b/pyclasher/client/client.py @@ -58,15 +58,17 @@ def __new__(cls, *, tokens=None, **kwargs): Args: tokens (str | list[str] | None): the Bearer tokens for the authentication of the ClashOfClans API - **kwargs: other key word arguments + **kwargs (Any): other key word arguments Notes: This function checks if all initialised clients do not share a token. If so the ecxeption ``ClientAlreadyInitialised`` is raised. Raises: - InvalidType - ClientAlreadyInitialised + InvalidType: provided tokens are not of type ``str`` + or ``Iterable[str]`` + ClientAlreadyInitialised: at least one of the provided tokens is + equal to a token that is already in use """ if cls.__instances is None: cls.__instances = [super().__new__(cls)] @@ -100,18 +102,26 @@ def __init__( initialisation method for the client Args: - tokens (str | list[str] | None): the Bearer tokens for the authentication of the ClashOfClans API - requests_per_second (int): This integer limits the number of requests done per second (per token). - This value is important to bypass the rate limit of the ClashOfClans API. - More tokens allow more requests per second because each token can do - as many requests per second as specified. - Defaults to 5. + tokens (str | list[str] | None): the Bearer tokens for the + authentication of the + ClashOfClans API + requests_per_second (int): This integer limits the number + of requests done per second + (per token). + This value is important to + bypass the rate limit of the + ClashOfClans API. + More tokens allow more requests + per second because each token + can do as many requests per + second as specified. + request_timeout (float): timeout in seconds for one + request logger (Logger): logger for detailed logging - Defaults to None swagger_url (str): swagger url for requests - Defaults to None - Returns: - None + Raises: + InvalidType: provided tokens are not of type ``str`` + or ``Iterable[str]`` """ global global_client_id @@ -126,8 +136,8 @@ def __init__( elif isinstance(tokens, Iterable): self.__tokens = list(tokens) else: - raise TypeError(f"Expected types str, list got {type(tokens)} " - f"instead") + raise InvalidType(tokens, + (str, Iterable[str])) if swagger_url is not None: parsed_url = urlparse(swagger_url) @@ -156,6 +166,29 @@ def __init__( @classmethod async def from_login(cls, email, password, requests_per_second=5, request_timeout=30, logger=MISSING, login_count=1): + """ + Class method to initialise a client using the authentication of the + ClashOfClans API and create tokens using this API. + + Args: + email (str): user email address to log in to the + ClashOfClans developer portal + password (str): user password for the email + requests_per_second (int): number of requests per token per second + request_timeout (float): seconds until the request is cancelled + due to a timeout + logger (Logger): logger + login_count (int): number of logins that should be done + (having more logins results more tokens + and this leads to more requests that can + be executed in parallel) + Notes: + Do not set the ``login_count`` to high, otherwise the account + could be banned. 5 works fine. + + Returns: + Client: an instance of the pyclasher client + """ if logger is None: logger = MISSING @@ -174,6 +207,28 @@ async def from_login(cls, email, password, requests_per_second=5, return self async def start(self, tokens=None): + """ + coroutine method to start the client + + Args: + tokens (str | list[str] | None): the Bearer tokens for the + authentication of the + ClashOfClans API + + Notes: + The tokens passed to this function have priority so if tokens are + set in the client initialisation and also passed to this function, + the tokens passed to this function will be used to start the + client and the consumer tasks. + + If it is needed to create multiple clients with the same tokens, + it is possible to use this function and pass the tokens directly to + the different clients. + + Returns: + Client: returns itself + + """ if tokens is None: tokens = self.__tokens @@ -206,6 +261,12 @@ async def start(self, tokens=None): return self async def close(self): + """ + coroutine method to stop the client + + Returns: + Client: returns itself + """ self.logger.info("closing client") if not self.__client_running: self.logger.error("the client is not running") @@ -224,13 +285,36 @@ async def close(self): return self async def __aenter__(self): + """ + asynchronous context manager (starting) + + Returns: + Client: returns itself + """ return await self.start() async def __aexit__(self, exc_type, exc_val, exc_tb): + """ + asynchronous context manager (stopping) + + Args: + exc_type (type[BaseException]): type of the exception or ``None`` + exc_val (BaseException): the raised exception or ``None`` + exc_tb (TracebackType): the traceback or ``None`` + """ await self.close() return def __del__(self): + """ + del method of the client + + Notes: + Calling ``client_instance.__del__()`` will instantly delete the + client but ``del client_instance`` will initiate the deleting + process of the client instance and the client may be accessible + for a short time after the call. + """ Client.__instances.remove(self) if not len(Client.__instances): Client.__instances = None @@ -245,37 +329,81 @@ def __del__(self): return @property - def is_running(self) -> bool: + def is_running(self): + """ + Returns: + bool: ``True`` if the client is running + bool: ``False`` if the client is not running + """ return self.__client_running @property def client_id(self): + """ + Getter of the client ID + + Returns: + int: the integer value of the client ID (only if the client ID is + an integer) + str: the string value of the client ID (only if the client ID is + a string) + """ return self._client_id @client_id.setter def client_id(self, new_id): - global global_client_id - if isinstance(new_id, str) and new_id.isdigit(): - new_id = int(new_id) + """ + Setter of the client ID - if not isinstance(new_id, (int, str)): - raise TypeError(f"Expected types int, str got {type(new_id)} " - f"instead.") - for client in Client.__instances: - if client.client_id == new_id: - raise ValueError(f"`new_id` {new_id} has already been taken " - f"and must be different") + Args: + new_id (str): new custom ID of the client + + Raises: + PyClasherException: the new custom ID must be a string and must not + contain a string value that is a digit + PyClasherException: `new_id` must not contain spaces + PyClasherException: `new_id` {new_id} has already been + taken and must be different + """ + global global_client_id + if not isinstance(new_id, str) or new_id.isdigit(): + raise PyClasherException("The new custom ID must be a string and " + "must not contain a string value that is a" + " digit") if isinstance(new_id, str): if " " in new_id: raise PyClasherException("`new_id` must not contain spaces") + for client in Client.__instances: + if client.client_id == new_id: + raise PyClasherException(f"`new_id` {new_id} has already been " + f"taken and must be different") + self._client_id = new_id return @classmethod def get_instance(cls, client_id=None): + """ + Getter of a client + + Args: + client_id (int | str): ID of a specific client or ``None`` + + Returns: + None: no client initialised + Client: the first client if ``client_id`` is ``None`` + Client: the client with the same ID as ``client_id`` + MISSING: no client with the same ID as ``client_id`` was found + + Notes: + If ``client_id`` is left empty, the method is going to return the + first initialised client instance. Otherwise, the method is going to + return the client that has the same client ID as specified in + ``client_id``. + """ if cls.__instances is None: return None clients = [client @@ -292,4 +420,12 @@ def get_instance(cls, client_id=None): @classmethod def initialized(cls): + """ + Class method that returns a bool indicating if the ``Client``-class has + been initialised on or multiple times + + Returns: + bool: ``True`` if a client has been initialised, + ``False`` otherwise + """ return isinstance(cls.__instances, list) diff --git a/pyclasher/client/client.pyi b/pyclasher/client/client.pyi index d996575..610f6ed 100644 --- a/pyclasher/client/client.pyi +++ b/pyclasher/client/client.pyi @@ -1,4 +1,5 @@ from logging import Logger +from types import TracebackType from typing import Iterable from ..exceptions import MISSING @@ -128,7 +129,10 @@ class Client: async def __aenter__(self) -> Client: ... - async def __aexit__(self, exc_type, exc_val, exc_tb) -> Client: + async def __aexit__(self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None) -> Client: ... def __del__(self) -> None: diff --git a/pyclasher/exceptions.py b/pyclasher/exceptions.py index d03933c..cd61ebe 100644 --- a/pyclasher/exceptions.py +++ b/pyclasher/exceptions.py @@ -1,4 +1,26 @@ +""" +This file contains the exception classes for the `PyClasher` package. + +Authors: + 201st-Luka +""" + + class Missing: + """ + Class of the ``MISSING`` object + + Notes: + This class always returns itself. One time received in a response there + is no way back to an object different from ``MISSING``. + + Attributes: + return_string (str): the string that is returned using + ``str(MISSING)`` + """ + + return_string = "MISSING" + def __call__(self, *args, **kwargs): return self @@ -14,24 +36,50 @@ def __add__(self, other): return other def __str__(self): - return "MISSING" + return self.return_string def __repr__(self): return "Missing()" MISSING = Missing() +""" +``MISSING`` object + +This Missing-instance is used as a reference in many parts of the package. + +instance of the ``Missing`` class +""" class PyClasherException(Exception): + """ + Exception class that is subclassed by every exception to the ``pyclasher`` + package + """ pass class ApiException(PyClasherException): - def __init__(self, api_code, client_error=None, *args, **kwargs): + """ + Exception class that is subclassed by every API exception + + Attributes: + api_code (int): API status code of the request + client_error (ClientError): optional ``ClientError`` information that is + provided by the request + """ + + def __init__(self, api_code, client_error=None): + """ + Args: + api_code (int): API status code of the request + client_error (ClientError): optional ``ClientError`` information + that is provided by the request + """ self.api_code = api_code self.client_error = client_error - super().__init__(*args, **kwargs) + super().__init__() return def __repr__(self): @@ -42,7 +90,16 @@ def __str__(self): class BadRequest(ApiException): + """ + Client provided incorrect parameters for the request. + """ + def __init__(self, client_error=None): + """ + Args: + client_error (ClientError): optional ``ClientError`` information + that is provided by the request + """ super().__init__(400, client_error) return @@ -51,7 +108,17 @@ def __str__(self): class AccessDenied(ApiException): + """ + Access denied, either because of missing/incorrect credentials or used API + token does not grant access to the requested resource. + """ + def __init__(self, client_error=None): + """ + Args: + client_error (ClientError): optional ``ClientError`` information + that is provided by the request + """ super().__init__(403, client_error) return @@ -62,7 +129,16 @@ def __str__(self): class NotFound(ApiException): + """ + Resource was not found. + """ + def __init__(self, client_error=None): + """ + Args: + client_error (ClientError): optional ``ClientError`` information + that is provided by the request + """ super().__init__(404, client_error) return @@ -71,7 +147,17 @@ def __str__(self): class Throttled(ApiException): + """ + Request was throttled, because amount of requests was above the threshold + defined for the used API token. + """ + def __init__(self, client_error=None): + """ + Args: + client_error (ClientError): optional ``ClientError`` information + that is provided by the request + """ super().__init__(429, client_error) return @@ -81,7 +167,16 @@ def __str__(self): class UnknownApiException(ApiException): + """ + Unknown error happened when handling the request. + """ + def __init__(self, client_error=None): + """ + Args: + client_error (ClientError): optional ``ClientError`` information + that is provided by the request + """ super().__init__(500, client_error) return @@ -90,7 +185,16 @@ def __str__(self): class Maintenance(ApiException): + """ + Service is temporarily unavailable because of maintenance. + """ + def __init__(self, client_error=None): + """ + Args: + client_error (ClientError): optional ``ClientError`` information + that is provided by the request + """ super().__init__(503, client_error) return @@ -99,6 +203,19 @@ def __str__(self): class ApiExceptions: + """ + Collection of the ApiExceptions + + Attributes: + BadRequest (BadRequest): ``BadRequest`` instance + AccessDenied (AccessDenied): ``AccessDenied`` instance + NotFound (NotFound): ``NotFound`` instance + Throttled (Throttled): ``Throttled`` instance + UnknownApiException (UnknownApiException): ``UnknownApiException`` + instance + Maintenance (Maintenance): ``Maintenance`` instance + """ + BadRequest = BadRequest() AccessDenied = AccessDenied() NotFound = NotFound() @@ -108,6 +225,26 @@ class ApiExceptions: @classmethod def from_api_code(cls, api_code, client_error=None): + """ + Class method to create a subclass of ``ApiException`` using the API + code and the optional client error information that is provided by the + request itself. + + Args: + api_code (int): API status code of the request + client_error (ClientError): optional ``ClientError`` information + that is provided by the request + + Returns: + returns a subclass of ``ApiException`` + + Raises: + PyClasherException: ``api_code`` is not 400, 403, 404, 429, + 500, 503 + """ + + # cannot use a `match ...: case ...:` here because it is not + # supported for Python version 3.9 and below if api_code == 400: return BadRequest(client_error) elif api_code == 403: @@ -126,11 +263,20 @@ def from_api_code(cls, api_code, client_error=None): class RequestNotDone(PyClasherException): + """ + Exception that is raised if a request attribute, property, ... was + accessed but could not be loaded because the request was not done. + """ + def __str__(self): return "The request was not done." class NoneToken(PyClasherException): + """ + Exception that is raised if a client is started without any tokens. + """ + def __str__(self): return ("The token must be passed to the client. " "You can do this in the initialisation process" @@ -138,53 +284,112 @@ def __str__(self): class InvalidLoginData(PyClasherException): + """ + Exception that is raised if the provided login data using + `Client.from_login(..., ...)` is not valid. + """ + def __str__(self): return "The login data is invalid." class InvalidType(PyClasherException): + """ + Exception that is raised if a type is incorrect (similar to `TypeError`) + + Attributes: + element (Any): the element whose type is not correct + types (type, tuple[type, ...): correct type or types + """ + def __init__(self, element, allowed_types): + """ + Args: + element (Any): the element whose type is + not correct + allowed_types (type, tuple[type, ...): correct type or types + """ super().__init__() self.element = element self.types = allowed_types return def __str__(self): - return (f"{self.element} is of invalid type\nallowed types are " + return (f"{self.element} is of invalid type, allowed types are " f"{self.types}.") class LoginNotDone(PyClasherException): + """ + Exception that is raised of raised if the login is not done but tokens + were tried to retrieve. (similar to ``RequestNotDone``) + """ + def __str__(self): return "The login was not done. You need to login first." class ClientIsRunning(PyClasherException): + """ + Exception that is raised if the client is started multiple times without + stopping the client between those calls. + """ + def __str__(self): - return "The client is already running." + return ("The client is already running. Stop it first before starting " + "again.") class ClientIsNotRunning(PyClasherException): + """ + Exception that is raised if the client is not running but an action that + requires the client to run was done. + """ + def __str__(self): return "The client is not running." class ClientAlreadyInitialised(PyClasherException): + """ + Exception that is raised if a new client was created but there is another + client that has at least one equal token. + """ + def __str__(self): return ("It is not possible to create multiple clients with the same " "tokens.") class NoClient(PyClasherException): + """ + Exception that is raised if a request was started but there is no client + that can execute the request. + """ + def __str__(self): return "No client has been initialised." class InvalidTimeFormat(PyClasherException): + """ + Exception that is raised if the provided time format is not recognized by + the API. + + Attributes: + value (str): value string of the invalid time + time_format (str): format of a valid time string + """ + def __init__(self, value, time_format): - super().__init__() + """ + Args: + value (str): value string of the invalid time + time_format (str): format of a valid time string + """ self.value = value self.time_format = time_format + super().__init__() return def __str__(self): @@ -193,11 +398,20 @@ def __str__(self): class ClientRunningOverwrite(PyClasherException): + """ + Exception that is raised if the client is running but a client parameter + was tried to edit but requires a client that is not running. + """ + def __str__(self): return "You cannot overwrite the parameter of a running client." class InvalidSeasonFormat(PyClasherException): + """ + Exception that is raised if the season format is not valid. + """ + def __str__(self): return ("The season string is not valid. It must be follow the " "following format: where is the year" @@ -205,9 +419,20 @@ def __str__(self): class RequestTimeout(PyClasherException): - def __init__(self, allowed_time, *args): + """ + Exception that is raised if a request takes longer than allowed. + + Attributes: + allowed_time (float): maximal time a request is allowed to take + """ + + def __init__(self, allowed_time): + """ + Args: + allowed_time (float): maximal time a request is allowed to take + """ self.allowed_time = allowed_time - super().__init__(*args) + super().__init__() return def __str__(self): @@ -216,4 +441,9 @@ def __str__(self): class InvalidClientId(PyClasherException): + """ + Exception that is raised if a client ID is not valid. It can already been + taken, or it can be equal to an ID that is in the range of 0 to + ``global_client_id``. + """ pass diff --git a/pyclasher/exceptions.pyi b/pyclasher/exceptions.pyi index 5d7b2c7..6f98c1e 100644 --- a/pyclasher/exceptions.pyi +++ b/pyclasher/exceptions.pyi @@ -1,3 +1,11 @@ +""" +This file contains the exception classes stub for the `PyClasher` package. + +Authors: + 201st-Luka +""" + + from typing import Any from .api.models import ClientError @@ -5,9 +13,19 @@ from .api.models import ClientError class Missing: """ - this class represents the absence of a value + Class of the ``MISSING`` object + + Notes: + This class always returns itself. One time received in a response there + is no way back to an object different from ``MISSING``. + + Attributes: + return_string (str): the string that is returned using + ``str(MISSING)`` """ + return_string: str = ... + def __call__(self, *args, **kwargs) -> Missing: ... @@ -27,17 +45,40 @@ class Missing: ... -# this Missing-instance is used as a reference in many parts of the package MISSING = Missing() +""" +``MISSING`` object + +This Missing-instance is used as a reference in many parts of the package. + +instance of the ``Missing`` class +""" class PyClasherException(Exception): + """ + Exception class that is subclassed by every exception to the ``pyclasher`` + package + """ pass class ApiException(PyClasherException): - def __init__(self, api_code: int, client_error: ClientError = None, *args, - **kwargs) -> None: + """ + Exception class that is subclassed by every API exception + + Attributes: + api_code (int): API status code of the request + client_error (ClientError): optional ``ClientError`` information that is + provided by the request + """ + def __init__(self, api_code: int, client_error: ClientError = None) -> None: + """ + Args: + api_code (int): API status code of the request + client_error (ClientError): optional ``ClientError`` information + that is provided by the request + """ self.api_code: int = ... self.client_error: ClientError = ... ... @@ -50,58 +91,135 @@ class ApiException(PyClasherException): class BadRequest(ApiException): + """ + Client provided incorrect parameters for the request. + """ + def __init__(self, client_error: ClientError = None): ... class AccessDenied(ApiException): + """ + Access denied, either because of missing/incorrect credentials or used API + token does not grant access to the requested resource. + """ + def __init__(self, client_error: ClientError = None): + """ + Args: + client_error (ClientError): optional ``ClientError`` information + that is provided by the request + """ ... class NotFound(ApiException): + """ + Resource was not found. + """ + def __init__(self, client_error: ClientError = None): + """ + Args: + client_error (ClientError): optional ``ClientError`` information + that is provided by the request + """ ... class Throttled(ApiException): + """ + Request was throttled, because amount of requests was above the threshold + defined for the used API token. + """ + def __init__(self, client_error: ClientError = None): + """ + Args: + client_error (ClientError): optional ``ClientError`` information + that is provided by the request + """ ... class UnknownApiException(ApiException): + """ + Unknown error happened when handling the request. + """ + def __init__(self, client_error: ClientError = None): + """ + Args: + client_error (ClientError): optional ``ClientError`` information + that is provided by the request + """ ... class Maintenance(ApiException): + """ + Service is temporarily unavailable because of maintenance. + """ + def __init__(self, client_error: ClientError = None): + """ + Args: + client_error (ClientError): optional ``ClientError`` information + that is provided by the request + """ ... class ApiExceptions: - BadRequest = BadRequest - AccessDenied = AccessDenied - NotFound = NotFound - Throttled = Throttled - UnknownApiException = UnknownApiException - Maintenance = Maintenance + """ + Collection of the ApiExceptions + + Attributes: + BadRequest (BadRequest): ``BadRequest`` instance + AccessDenied (AccessDenied): ``AccessDenied`` instance + NotFound (NotFound): ``NotFound`` instance + Throttled (Throttled): ``Throttled`` instance + UnknownApiException (UnknownApiException): ``UnknownApiException`` + instance + Maintenance (Maintenance): ``Maintenance`` instance + """ + + 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 method to create a subclass of ``ApiException`` using the API + code and the optional client error information that is provided by the + request itself. + + Args: + api_code (int): API status code of the request + client_error (ClientError): optional ``ClientError`` information + that is provided by the request + + Returns: + returns a subclass of ``ApiException`` + + Raises: + PyClasherException: ``api_code`` is not 400, 403, 404, 429, + 500, 503 + """ + ... class RequestNotDone(PyClasherException): """ - exception class to handle the case if a request was not done but data was retrieved + Exception that is raised if a request attribute, property, ... was + accessed but could not be loaded because the request was not done. """ def __str__(self) -> str: @@ -110,7 +228,7 @@ class RequestNotDone(PyClasherException): class NoneToken(PyClasherException): """ - exception class to handle the case if no ClashOfClans API token was entered to the client + Exception that is raised if a client is started without any tokens. """ def __str__(self) -> str: @@ -119,7 +237,8 @@ class NoneToken(PyClasherException): class InvalidLoginData(PyClasherException): """ - exception class to handle invalid login data to log in to the ClashOfClans developer portal + Exception that is raised if the provided login data using + `Client.from_login(..., ...)` is not valid. """ def __str__(self) -> str: @@ -128,22 +247,21 @@ class InvalidLoginData(PyClasherException): class InvalidType(PyClasherException): """ - exception class to handle type errors for the pyclasher package + Exception that is raised if a type is incorrect (similar to `TypeError`) + + Attributes: + element (Any): the element whose type is not correct + types (type, tuple[type, ...): correct type or types """ def __init__(self, element: Any, - allowed_types: type | tuple[type, ...] - ) -> None: + allowed_types: type | tuple[type, ...]) -> None: """ - initialisation of the invalid type exception - - :param element: the element that does not match the allowed types - :type element Any - :param allowed_types: a type or a tuple of the allowed types - :type allowed_types: type | tuple[type, ...] - :return: None - :rtype: None + Args: + element (Any): the element whose type is + not correct + allowed_types (type, tuple[type, ...): correct type or types """ self.element = element self.types = allowed_types @@ -154,7 +272,8 @@ class InvalidType(PyClasherException): class LoginNotDone(PyClasherException): """ - exception class to handle the error if the login was not done but data was retrieved + Exception that is raised of raised if the login is not done but tokens + were tried to retrieve. (similar to ``RequestNotDone``) """ def __str__(self) -> str: @@ -163,7 +282,8 @@ class LoginNotDone(PyClasherException): class ClientIsRunning(PyClasherException): """ - exception class that handles errors if a not permitted action while the client was running was done + Exception that is raised if the client is started multiple times without + stopping the client between those calls. """ def __str__(self) -> str: @@ -172,7 +292,8 @@ class ClientIsRunning(PyClasherException): class ClientIsNotRunning(PyClasherException): """ - exception class that handles errors if a not permitted action while the client was not running was done + Exception that is raised if the client is not running but an action that + requires the client to run was done. """ def __str__(self) -> str: @@ -181,7 +302,8 @@ class ClientIsNotRunning(PyClasherException): class ClientAlreadyInitialised(PyClasherException): """ - exception class to handle multiple client initialisations + Exception that is raised if a new client was created but there is another + client that has at least one equal token. """ def __str__(self) -> str: @@ -190,7 +312,8 @@ class ClientAlreadyInitialised(PyClasherException): class NoClient(PyClasherException): """ - exception class to handle the error if no client was initialised + Exception that is raised if a request was started but there is no client + that can execute the request. """ def __str__(self) -> str: @@ -199,25 +322,22 @@ class NoClient(PyClasherException): class InvalidTimeFormat(PyClasherException): """ - exception class to handle errors while converting a time string to a Time class + Exception that is raised if the provided time format is not recognized by + the API. - :ivar value: the current value that does not match the allowed tme format - :type value: str - :ivar time_format: the allowed time format - :type time_format: str + Attributes: + value (str): value string of the invalid time + time_format (str): format of a valid time string """ def __init__(self, value: str, time_format: str) -> None: """ - initialisation of the invalid time format exception - - :param value: the current value that does not match the allowed tme format - :type value: str - :param time_format: the allowed time format - :type time_format: str + Args: + value (str): value string of the invalid time + time_format (str): format of a valid time string """ - self.value = value - self.time_format = time_format + self.value: str = ... + self.time_format: str = ... def __str__(self) -> str: ... @@ -225,7 +345,8 @@ class InvalidTimeFormat(PyClasherException): class ClientRunningOverwrite(PyClasherException): """ - exception class that handles the error if a client was running and a client variable was overwritten + Exception that is raised if the client is running but a client parameter + was tried to edit but requires a client that is not running. """ def __str__(self) -> str: @@ -234,7 +355,7 @@ class ClientRunningOverwrite(PyClasherException): class InvalidSeasonFormat(PyClasherException): """ - exception class that handles an invalid season format + Exception that is raised if the season format is not valid. """ def __str__(self) -> str: @@ -242,7 +363,18 @@ class InvalidSeasonFormat(PyClasherException): class RequestTimeout(PyClasherException): + """ + Exception that is raised if a request takes longer than allowed. + + Attributes: + allowed_time (float): maximal time a request is allowed to take + """ + def __init__(self, allowed_time: float, *args): + """ + Args: + allowed_time (float): maximal time a request is allowed to take + """ self.allowed_time: float = ... ... @@ -252,4 +384,9 @@ class RequestTimeout(PyClasherException): class InvalidClientId(PyClasherException): + """ + Exception that is raised if a client ID is not valid. It can already been + taken, or it can be equal to an ID that is in the range of 0 to + ``global_client_id``. + """ pass diff --git a/requirements-docs.txt b/requirements-docs.txt index 23d4344..4d4a097 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,4 @@ mkdocs mkdocs-material -mkdocs-git-committers-plugin mkdocstrings[python] mkdocs-awesome-pages-plugin \ No newline at end of file