From 4f5b543940971ae82ab111c63ec3e0b95a6fb70f Mon Sep 17 00:00:00 2001 From: Bogdan Evstratenko Date: Sun, 8 May 2022 23:24:53 +0300 Subject: [PATCH 01/55] init commit --- .flake8 | 8 + .gitignore | 161 ++++++++++++++++- cent/__init__.py | 5 +- cent/async_core.py | 146 +++++++++++++++ cent/core.py | 316 ++++++++++---------------------- cent/exceptions.py | 33 ++++ cent/mixins.py | 73 ++++++++ cent/utils.py | 2 + poetry.lock | 441 +++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 27 +++ setup.py | 66 +++---- 11 files changed, 1014 insertions(+), 264 deletions(-) create mode 100644 .flake8 create mode 100644 cent/async_core.py create mode 100644 cent/exceptions.py create mode 100644 cent/mixins.py create mode 100644 cent/utils.py create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..fce6463 --- /dev/null +++ b/.flake8 @@ -0,0 +1,8 @@ +[flake8] +exclude = + .git, + __pycache__, + .venv, +max-line-length = 120 +format = '%(path)s:%(row)d Column:%(col)d: %(code)s %(text)s' + diff --git a/.gitignore b/.gitignore index e76a1c7..5a8005f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,158 @@ -*.pyc -build/* -dist/* -cent.egg-info/* +# Created by .ignore support plugin (hsz.mobi) +### JupyterNotebooks template +# gitignore template for Jupyter Notebooks +# website: http://jupyter.org/ + +.ipynb_checkpoints +*/.ipynb_checkpoints/* + +# Remove previous ipynb_checkpoints +# git rm -r .ipynb_checkpoints/ +# + +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pgdata/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +tags + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +### VirtualEnv template +# Virtualenv +# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +[Bb]in +[Ii]nclude +[Ll]ib +[Ll]ib64 +[Ll]ocal +[Ss]cripts +pyvenv.cfg +pip-selfcheck.json +.idea +.DS_Store +coverage +coverage.* + +# Docker +docker-compose.yml + +# IDE's +.vim/ .vscode/ -.idea/ diff --git a/cent/__init__.py b/cent/__init__.py index 422cfb5..e6422e5 100644 --- a/cent/__init__.py +++ b/cent/__init__.py @@ -1,2 +1,5 @@ # coding: utf-8 -from .core import Client, CentException, RequestException, ResponseError, ClientNotEmpty +from .core import Client +from .exceptions import CentException, ClientNotEmpty, RequestException, ResponseError + +__all__ = ["Client", "CentException", "ClientNotEmpty", "RequestException", "ResponseError"] diff --git a/cent/async_core.py b/cent/async_core.py new file mode 100644 index 0000000..649756a --- /dev/null +++ b/cent/async_core.py @@ -0,0 +1,146 @@ +from typing import Callable, List, Optional + +import httpx + +from cent import RequestException, ResponseError +from cent.mixins import ParamsMixin + + +class Client(ParamsMixin): + def __init__( + self, + address: str, + api_key: str = "", + timeout: int = 1, + json_encoder: Callable = None, + session: Callable = None, + **kwargs + ): + """ + :param address: Centrifugo address + :param api_key: Centrifugo API key + :param timeout: timeout for HTTP requests to Centrifugo + :param json_encoder: custom JSON encoder + :param verify: boolean flag, when set to False no certificate check will be done during requests. + :param session: custom requests.Session instance + """ + + self.address = address + self.api_key = api_key + self.timeout = timeout + self.json_encoder = json_encoder + self.session = session or httpx.AsyncClient() + self._prepare_session() + self.kwargs = kwargs + + def _prepare_session(self): + self.session.headers["Authorization"] = "apikey " + self.api_key + self.session.timeout = self.timeout + + async def send(self, method, payload): + payload["method"] = method + try: + resp: httpx.Response = await self.session.post( + url=self.address, + json=payload, + timeout=self.timeout, + ) + except RequestException as err: + raise RequestException(err) + if resp.status_code != 200: + raise RequestException("wrong status code: %d" % resp.status_code) + return resp + + async def _send_one(self, method: str, payload: dict) -> Optional[dict]: + resp = await self.send(method, payload) + resp_json = resp.json() + if "error" in resp_json: + raise ResponseError(resp_json["error"]) + return resp_json.get("result", {}) + + async def publish(self, channel: str, data: dict, skip_history: bool = False) -> Optional[dict]: + result = await self._send_one( + method="publish", + payload=self.get_publish_params(channel, data, skip_history=skip_history), + ) + return result + + async def broadcast(self, channels: List[str], data: dict, skip_history: bool = False) -> Optional[dict]: + result = await self._send_one( + method="broadcast", + payload=self.get_broadcast_params(channels, data, skip_history=skip_history), + ) + return result + + async def subscribe(self, user: str, channel: str, client: Optional[str] = None) -> None: + await self._send_one( + method="subscribe", + payload=self.get_subscribe_params(user, channel, client=client), + ) + return + + async def unsubscribe(self, user: str, channel: str, client: Optional[str] = None) -> None: + await self._send_one( + method="unsubscribe", + payload=self.get_unsubscribe_params(user, channel, client=client), + ) + return + + async def disconnect(self, user: str, client: Optional[str] = None) -> None: + await self._send_one( + method="disconnect", + payload=self.get_disconnect_params(user, client=client), + ) + return + + async def presence(self, channel: str) -> dict: + result = await self._send_one( + method="presence", + payload=self.get_presence_params(channel), + ) + return result["presence"] + + async def presence_stats(self, channel: str) -> dict[str, int]: + result = await self._send_one( + method="presence_stats", + payload=self.get_presence_stats_params(channel), + ) + return { + "num_clients": result["num_clients"], + "num_users": result["num_users"], + } + + async def history(self, channel: str, limit: int = 0, since: dict = None, reverse: bool = False) -> dict: + result = await self._send_one( + method="history", + payload=self.get_history_params(channel, limit=limit, since=since, reverse=reverse), + ) + return { + "publications": result.get("publications", []), + "offset": result.get("offset", 0), + "epoch": result.get("epoch", ""), + } + + def history_remove(self, channel: str) -> None: + await self._send_one( + method="history_remove", + payload=self.get_history_remove_params(channel), + ) + return + + async def channels(self, pattern="") -> List[Optional[str]]: + result = await self._send_one( + method="channels", + payload=self.get_channels_params(pattern=pattern), + ) + return result["channels"] + + async def info(self) -> dict[str, list]: + result = await self._send_one( + method="info", + payload=self.get_info_params(), + ) + return result + + async def close(self): + await self.session.aclose() diff --git a/cent/core.py b/cent/core.py index 5df8e2b..bbbff5c 100644 --- a/cent/core.py +++ b/cent/core.py @@ -1,53 +1,25 @@ -# coding: utf-8 -import urllib.parse as urlparse -import sys -import json -import requests +from typing import Callable, List, Optional +import httpx -def to_bytes(s): - return s.encode("latin-1") +from cent import RequestException, ResponseError +from cent.mixins import ParamsMixin -class CentException(Exception): - """ - Wrapper for all exceptions coming from this library. - """ - pass - - -class RequestException(CentException): - """ - RequestException means that request to Centrifugo API failed in some way. - This is just a wrapper over RequestException from requests library. - """ - pass - - -class ClientNotEmpty(CentException): - """ - ClientNotEmpty raised when attempting to call single method but internal - client command buffer is not empty. - """ - pass - - -class ResponseError(CentException): - """ - Raised when response from Centrifugo contains any error as result of API - command execution. - """ - pass - - -class Client(object): +class Client(ParamsMixin): """ Core class to communicate with Centrifugo. """ - def __init__(self, address, api_key="", timeout=1, - json_encoder=None, verify=True, - session=None, **kwargs): + def __init__( + self, + address: str, + api_key: str = "", + timeout: int = 1, + json_encoder: Callable = None, + session: Callable = None, + **kwargs + ): """ :param address: Centrifugo address :param api_key: Centrifugo API key @@ -61,224 +33,118 @@ def __init__(self, address, api_key="", timeout=1, self.api_key = api_key self.timeout = timeout self.json_encoder = json_encoder - self.verify = verify - self.session = session or requests.Session() + self.session = session or httpx.Client() + self._prepare_session() self.kwargs = kwargs - self._messages = [] - def add(self, method, params): - data = { - "method": method, - "params": params - } - self._messages.append(data) - - def send(self, method=None, params=None): - if method and params is not None: - self.add(method, params) - messages = self._messages[:] - self._messages = [] - data = to_bytes( - "\n".join([json.dumps(x, cls=self.json_encoder) for x in messages])) - response = self._send(self.address, data) - return [json.loads(x) for x in response.split("\n") if x] + def _prepare_session(self): + self.session.headers["Authorization"] = "apikey " + self.api_key + self.session.timeout = self.timeout - def _send(self, url, data): - """ - Send a request to a remote web server using HTTP POST. - """ - headers = { - 'Content-type': 'application/json' - } - if self.api_key: - headers['Authorization'] = 'apikey ' + self.api_key + def send(self, method, payload): + payload["method"] = method try: resp = self.session.post( - url, data=data, headers=headers, timeout=self.timeout, verify=self.verify) - except requests.RequestException as err: + url=self.address, + json=payload, + timeout=self.timeout, + ) + except RequestException as err: raise RequestException(err) if resp.status_code != 200: raise RequestException("wrong status code: %d" % resp.status_code) - return resp.content.decode('utf-8') + return resp def reset(self): - self._messages = [] - - @staticmethod - def get_publish_params(channel, data, skip_history=False): - params = { - "channel": channel, - "data": data, - "skip_history": skip_history, - } - return params - - @staticmethod - def get_broadcast_params(channels, data, skip_history=False): - params = { - "channels": channels, - "data": data, - "skip_history": skip_history, - } - return params - - @staticmethod - def get_subscribe_params(user, channel, client=None): - params = { - "user": user, - "channel": channel - } - if client: - params["client"] = client - return params - - @staticmethod - def get_unsubscribe_params(user, channel, client=None): - params = { - "user": user, - "channel": channel - } - if client: - params["client"] = client - return params - - @staticmethod - def get_disconnect_params(user, client=None): - params = { - "user": user - } - if client: - params["client"] = client - return params - - @staticmethod - def get_presence_params(channel): - return { - "channel": channel - } - - @staticmethod - def get_presence_stats_params(channel): - return { - "channel": channel - } - - @staticmethod - def get_history_params(channel, limit=0, since=None, reverse=False): - params = { - "channel": channel, - "limit": limit, - "reverse": reverse, - } - if since: - params["since"] = { - "offset": since["offset"], - "epoch": since["epoch"] - } - return params - - @staticmethod - def get_history_remove_params(channel): - return { - "channel": channel - } - - @staticmethod - def get_channels_params(pattern=""): - return { - "pattern": pattern - } - - @staticmethod - def get_info_params(): - return {} - - def _check_empty(self): - if self._messages: - raise ClientNotEmpty( - "client command buffer not empty, send commands or reset client") - - def _send_one(self): - res = self.send() - data = res[0] - if "error" in data and data["error"]: - raise ResponseError(data["error"]) - return data.get("result") - - def publish(self, channel, data, skip_history=False): - self._check_empty() - self.add("publish", self.get_publish_params( - channel, data, skip_history=skip_history)) - result = self._send_one() + pass + + def _send_one(self, method: str, payload: dict) -> Optional[dict]: + resp = self.send(method, payload) + resp_json = resp.json() + if "error" in resp_json: + raise ResponseError(resp_json["error"]) + return resp_json.get("result", {}) + + def publish(self, channel: str, data: dict, skip_history: bool = False) -> Optional[dict]: + result = self._send_one( + method="publish", + payload=self.get_publish_params(channel, data, skip_history=skip_history), + ) return result - def broadcast(self, channels, data, skip_history=False): - self._check_empty() - self.add("broadcast", self.get_broadcast_params( - channels, data, skip_history=skip_history)) - result = self._send_one() + def broadcast(self, channels: List[str], data: dict, skip_history: bool = False) -> Optional[dict]: + result = self._send_one( + method="broadcast", + payload=self.get_broadcast_params(channels, data, skip_history=skip_history), + ) return result - def subscribe(self, user, channel, client=None): - self._check_empty() - self.add("subscribe", self.get_subscribe_params( - user, channel, client=client)) - self._send_one() + def subscribe(self, user: str, channel: str, client: Optional[str] = None) -> None: + self._send_one( + method="subscribe", + payload=self.get_subscribe_params(user, channel, client=client), + ) return - def unsubscribe(self, user, channel, client=None): - self._check_empty() - self.add("unsubscribe", self.get_unsubscribe_params( - user, channel, client=client)) - self._send_one() + def unsubscribe(self, user: str, channel: str, client: Optional[str] = None) -> None: + self._send_one( + method="unsubscribe", + payload=self.get_unsubscribe_params(user, channel, client=client), + ) return - def disconnect(self, user, client=None): - self._check_empty() - self.add("disconnect", self.get_disconnect_params(user, client=client)) - self._send_one() + def disconnect(self, user: str, client: Optional[str] = None) -> None: + self._send_one( + method="disconnect", + payload=self.get_disconnect_params(user, client=client), + ) return - def presence(self, channel): - self._check_empty() - self.add("presence", self.get_presence_params(channel)) - result = self._send_one() + def presence(self, channel: str) -> dict: + result = self._send_one( + method="presence", + payload=self.get_presence_params(channel), + ) return result["presence"] - def presence_stats(self, channel): - self._check_empty() - self.add("presence_stats", self.get_presence_stats_params(channel)) - result = self._send_one() + def presence_stats(self, channel: str) -> dict[str, int]: + result = self._send_one( + method="presence_stats", + payload=self.get_presence_stats_params(channel), + ) return { "num_clients": result["num_clients"], "num_users": result["num_users"], } - def history(self, channel, limit=0, since=None, reverse=False): - self._check_empty() - self.add("history", self.get_history_params( - channel, limit=limit, since=since, reverse=reverse)) - result = self._send_one() + def history(self, channel: str, limit: int = 0, since: dict = None, reverse: bool = False) -> dict: + result = self._send_one( + method="history", + payload=self.get_history_params(channel, limit=limit, since=since, reverse=reverse), + ) return { "publications": result.get("publications", []), "offset": result.get("offset", 0), "epoch": result.get("epoch", ""), } - def history_remove(self, channel): - self._check_empty() - self.add("history_remove", self.get_history_remove_params(channel)) - self._send_one() + def history_remove(self, channel: str) -> None: + self._send_one( + method="history_remove", + payload=self.get_history_remove_params(channel), + ) return - def channels(self, pattern=""): - self._check_empty() - self.add("channels", params=self.get_channels_params(pattern=pattern)) - result = self._send_one() + def channels(self, pattern="") -> List[Optional[str]]: + result = self._send_one( + method="channels", + payload=self.get_channels_params(pattern=pattern), + ) return result["channels"] - def info(self): - self._check_empty() - self.add("info", self.get_info_params()) - result = self._send_one() + def info(self) -> dict[str, list]: + result = self._send_one( + method="info", + payload=self.get_info_params(), + ) return result diff --git a/cent/exceptions.py b/cent/exceptions.py new file mode 100644 index 0000000..49c137c --- /dev/null +++ b/cent/exceptions.py @@ -0,0 +1,33 @@ +class CentException(Exception): + """ + Wrapper for all exceptions coming from this library. + """ + + pass + + +class RequestException(CentException): + """ + RequestException means that request to Centrifugo API failed in some way. + This is just a wrapper over RequestException from requests library. + """ + + pass + + +class ClientNotEmpty(CentException): + """ + ClientNotEmpty raised when attempting to call single method but internal + client command buffer is not empty. + """ + + pass + + +class ResponseError(CentException): + """ + Raised when response from Centrifugo contains any error as result of API + command execution. + """ + + pass diff --git a/cent/mixins.py b/cent/mixins.py new file mode 100644 index 0000000..170c583 --- /dev/null +++ b/cent/mixins.py @@ -0,0 +1,73 @@ +from typing import List, Optional + + +class ParamsMixin: + @staticmethod + def get_publish_params(channel: str, data: dict, skip_history: bool = False) -> dict: + params = { + "channel": channel, + "data": data, + "skip_history": skip_history, + } + return params + + @staticmethod + def get_broadcast_params(channels: List[str], data: dict, skip_history: bool = False) -> dict: + params = { + "channels": channels, + "data": data, + "skip_history": skip_history, + } + return params + + @staticmethod + def get_subscribe_params(user: str, channel: str, client: Optional[str] = None) -> dict: + params = {"user": user, "channel": channel} + if client: + params["client"] = client + return params + + @staticmethod + def get_unsubscribe_params(user: str, channel: str, client: Optional[str] = None) -> dict: + params = {"user": user, "channel": channel} + if client: + params["client"] = client + return params + + @staticmethod + def get_disconnect_params(user: str, client: Optional[str] = None) -> dict: + params = {"user": user} + if client: + params["client"] = client + return params + + @staticmethod + def get_presence_params(channel: str) -> dict: + return {"channel": channel} + + @staticmethod + def get_presence_stats_params(channel: str) -> dict: + return {"channel": channel} + + @staticmethod + def get_history_params(channel: str, limit: int = 0, since: Optional[dict] = None, reverse: bool = False) -> dict: + params = { + "channel": channel, + "limit": limit, + "reverse": reverse, + } + if since: + params["since"] = {"offset": since["offset"], "epoch": since["epoch"]} + return params + + @staticmethod + def get_history_remove_params(channel: str) -> dict: + return {"channel": channel} + + @staticmethod + def get_channels_params(pattern: str = "") -> dict: + return {"pattern": pattern} + + @staticmethod + def get_info_params() -> dict: + return {} diff --git a/cent/utils.py b/cent/utils.py new file mode 100644 index 0000000..370634f --- /dev/null +++ b/cent/utils.py @@ -0,0 +1,2 @@ +def to_bytes(s): + return s.encode("latin-1") diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..f046d2a --- /dev/null +++ b/poetry.lock @@ -0,0 +1,441 @@ +[[package]] +name = "anyio" +version = "3.5.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=6.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] +trio = ["trio (>=0.16)"] + +[[package]] +name = "black" +version = "22.3.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2021.10.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "charset-normalizer" +version = "2.0.12" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "flake8" +version = "4.0.1" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.8.0,<2.9.0" +pyflakes = ">=2.4.0,<2.5.0" + +[[package]] +name = "h11" +version = "0.12.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "httpcore" +version = "0.14.7" +description = "A minimal low-level HTTP client." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +anyio = ">=3.0.0,<4.0.0" +certifi = "*" +h11 = ">=0.11,<0.13" +sniffio = ">=1.0.0,<2.0.0" + +[package.extras] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "httpx" +version = "0.22.0" +description = "The next generation HTTP client." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +certifi = "*" +charset-normalizer = "*" +httpcore = ">=0.14.5,<0.15.0" +rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +sniffio = "*" + +[package.extras] +brotli = ["brotlicffi", "brotli"] +cli = ["click (>=8.0.0,<9.0.0)", "rich (>=10.0.0,<11.0.0)", "pygments (>=2.0.0,<3.0.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "importlib-metadata" +version = "4.2.0" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] + +[[package]] +name = "isort" +version = "5.10.1" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "platformdirs" +version = "2.5.2" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] +test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] + +[[package]] +name = "pycodestyle" +version = "2.8.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pyflakes" +version = "2.4.0" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "sniffio" +version = "1.2.0" +description = "Sniff out which async library your code is running under" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "typed-ast" +version = "1.5.3" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "typing-extensions" +version = "4.1.1" +description = "Backported and Experimental Type Hints for Python 3.6+" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "zipp" +version = "3.8.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] + +[extras] +async = ["httpx"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.7" +content-hash = "780516b11991f41b3a2cefb69502b39df2c4c86b39b0231dd881ebcbdec04dc6" + +[metadata.files] +anyio = [ + {file = "anyio-3.5.0-py3-none-any.whl", hash = "sha256:b5fa16c5ff93fa1046f2eeb5bbff2dad4d3514d6cda61d02816dba34fa8c3c2e"}, + {file = "anyio-3.5.0.tar.gz", hash = "sha256:a0aeffe2fb1fdf374a8e4b471444f0f3ac4fb9f5a5b542b48824475e0042a5a6"}, +] +black = [ + {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, + {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, + {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, + {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, + {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, + {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, + {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, + {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, + {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, + {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, + {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, + {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, + {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, + {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, + {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, + {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, + {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, + {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, + {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, +] +certifi = [ + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, +] +click = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +flake8 = [ + {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, + {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, +] +h11 = [ + {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, + {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, +] +httpcore = [ + {file = "httpcore-0.14.7-py3-none-any.whl", hash = "sha256:47d772f754359e56dd9d892d9593b6f9870a37aeb8ba51e9a88b09b3d68cfade"}, + {file = "httpcore-0.14.7.tar.gz", hash = "sha256:7503ec1c0f559066e7e39bc4003fd2ce023d01cf51793e3c173b864eb456ead1"}, +] +httpx = [ + {file = "httpx-0.22.0-py3-none-any.whl", hash = "sha256:e35e83d1d2b9b2a609ef367cc4c1e66fd80b750348b20cc9e19d1952fc2ca3f6"}, + {file = "httpx-0.22.0.tar.gz", hash = "sha256:d8e778f76d9bbd46af49e7f062467e3157a5a3d2ae4876a4bbfd8a51ed9c9cb4"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +importlib-metadata = [ + {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, + {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, +] +isort = [ + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +pathspec = [ + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, +] +platformdirs = [ + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, +] +pycodestyle = [ + {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, + {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, +] +pyflakes = [ + {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, + {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, +] +rfc3986 = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, +] +sniffio = [ + {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, + {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, +] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +typed-ast = [ + {file = "typed_ast-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ad3b48cf2b487be140072fb86feff36801487d4abb7382bb1929aaac80638ea"}, + {file = "typed_ast-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:542cd732351ba8235f20faa0fc7398946fe1a57f2cdb289e5497e1e7f48cfedb"}, + {file = "typed_ast-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc2c11ae59003d4a26dda637222d9ae924387f96acae9492df663843aefad55"}, + {file = "typed_ast-1.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd5df1313915dbd70eaaa88c19030b441742e8b05e6103c631c83b75e0435ccc"}, + {file = "typed_ast-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:e34f9b9e61333ecb0f7d79c21c28aa5cd63bec15cb7e1310d7d3da6ce886bc9b"}, + {file = "typed_ast-1.5.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f818c5b81966d4728fec14caa338e30a70dfc3da577984d38f97816c4b3071ec"}, + {file = "typed_ast-1.5.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3042bfc9ca118712c9809201f55355479cfcdc17449f9f8db5e744e9625c6805"}, + {file = "typed_ast-1.5.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4fff9fdcce59dc61ec1b317bdb319f8f4e6b69ebbe61193ae0a60c5f9333dc49"}, + {file = "typed_ast-1.5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8e0b8528838ffd426fea8d18bde4c73bcb4167218998cc8b9ee0a0f2bfe678a6"}, + {file = "typed_ast-1.5.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ef1d96ad05a291f5c36895d86d1375c0ee70595b90f6bb5f5fdbee749b146db"}, + {file = "typed_ast-1.5.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed44e81517364cb5ba367e4f68fca01fba42a7a4690d40c07886586ac267d9b9"}, + {file = "typed_ast-1.5.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f60d9de0d087454c91b3999a296d0c4558c1666771e3460621875021bf899af9"}, + {file = "typed_ast-1.5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9e237e74fd321a55c90eee9bc5d44be976979ad38a29bbd734148295c1ce7617"}, + {file = "typed_ast-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ee852185964744987609b40aee1d2eb81502ae63ee8eef614558f96a56c1902d"}, + {file = "typed_ast-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:27e46cdd01d6c3a0dd8f728b6a938a6751f7bd324817501c15fb056307f918c6"}, + {file = "typed_ast-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d64dabc6336ddc10373922a146fa2256043b3b43e61f28961caec2a5207c56d5"}, + {file = "typed_ast-1.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8cdf91b0c466a6c43f36c1964772918a2c04cfa83df8001ff32a89e357f8eb06"}, + {file = "typed_ast-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:9cc9e1457e1feb06b075c8ef8aeb046a28ec351b1958b42c7c31c989c841403a"}, + {file = "typed_ast-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e20d196815eeffb3d76b75223e8ffed124e65ee62097e4e73afb5fec6b993e7a"}, + {file = "typed_ast-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:37e5349d1d5de2f4763d534ccb26809d1c24b180a477659a12c4bde9dd677d74"}, + {file = "typed_ast-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f1a27592fac87daa4e3f16538713d705599b0a27dfe25518b80b6b017f0a6d"}, + {file = "typed_ast-1.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8831479695eadc8b5ffed06fdfb3e424adc37962a75925668deeb503f446c0a3"}, + {file = "typed_ast-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:20d5118e494478ef2d3a2702d964dae830aedd7b4d3b626d003eea526be18718"}, + {file = "typed_ast-1.5.3.tar.gz", hash = "sha256:27f25232e2dd0edfe1f019d6bfaaf11e86e657d9bdb7b0956db95f560cceb2b3"}, +] +typing-extensions = [ + {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, + {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, +] +zipp = [ + {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, + {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e6e0ebe --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[tool.poetry] +name = "cent" +version = "4.2.0" +description = "Python library to communicate with Centrifugo v3 HTTP API" +authors = ["Alexandr Emelin", "Bogdan Evstratenko"] +license = "MIT" +readme = 'README.md' + +[tool.poetry.dependencies] +python = "^3.7" +httpx = "^0.22.0" + +[tool.poetry.dev-dependencies] +black = "^22.3.0" +isort = "^5.10.1" +flake8 = "^4.0.1" + +[tool.poetry.extras] +async = ["httpx"] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.isort] +profile = "black" +multi_line_output = 3 diff --git a/setup.py b/setup.py index 4798733..03e0ea3 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,14 @@ import os import sys -from setuptools import setup +from setuptools import setup -if sys.argv[-1] == 'test': - status = os.system('python tests/tests.py') +if sys.argv[-1] == "test": + status = os.system("python tests/tests.py") sys.exit(1 if status > 127 else status) -requirements = ['requests'] +requirements = ["requests"] def long_description(): @@ -16,41 +16,41 @@ def long_description(): setup( - name='cent', - version='4.1.0', + name="cent", + version="4.1.0", description="Python library to communicate with Centrifugo v3 HTTP API", long_description=long_description(), - url='https://github.com/centrifugal/cent', - download_url='https://github.com/centrifugal/cent', + url="https://github.com/centrifugal/cent", + download_url="https://github.com/centrifugal/cent", author="Alexandr Emelin", - author_email='frvzmb@gmail.com', - license='MIT', - packages=['cent'], + author_email="frvzmb@gmail.com", + license="MIT", + packages=["cent"], entry_points={ - 'console_scripts': [ - 'cent = cent.console:run', + "console_scripts": [ + "cent = cent.console:run", ], }, install_requires=requirements, classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: Apache Software License', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Software Development', - 'Topic :: System :: Networking', - 'Topic :: Terminals', - 'Topic :: Text Processing', - 'Topic :: Utilities' - ] + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: Apache Software License", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Software Development", + "Topic :: System :: Networking", + "Topic :: Terminals", + "Topic :: Text Processing", + "Topic :: Utilities", + ], ) From 10d993ffaff9b86096db26beb392753b14997926 Mon Sep 17 00:00:00 2001 From: Bogdan Evstratenko Date: Sun, 15 May 2022 17:50:18 +0300 Subject: [PATCH 02/55] add tests --- cent/async_core.py | 4 +- cent/core.py | 118 +++++++++++++++++-------- cent/utils.py | 13 +++ poetry.lock | 208 +++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 30 ++++++- tests/__init__.py | 0 tests/conftest.py | 14 +++ tests/test_async.py | 0 tests/test_sync.py | 78 +++++++++++++++++ 9 files changed, 420 insertions(+), 45 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_async.py create mode 100644 tests/test_sync.py diff --git a/cent/async_core.py b/cent/async_core.py index 649756a..38b50d9 100644 --- a/cent/async_core.py +++ b/cent/async_core.py @@ -6,7 +6,7 @@ from cent.mixins import ParamsMixin -class Client(ParamsMixin): +class AsyncClient(ParamsMixin): def __init__( self, address: str, @@ -121,7 +121,7 @@ async def history(self, channel: str, limit: int = 0, since: dict = None, revers "epoch": result.get("epoch", ""), } - def history_remove(self, channel: str) -> None: + async def history_remove(self, channel: str) -> None: await self._send_one( method="history_remove", payload=self.get_history_remove_params(channel), diff --git a/cent/core.py b/cent/core.py index bbbff5c..05dda7c 100644 --- a/cent/core.py +++ b/cent/core.py @@ -1,9 +1,11 @@ +import json from typing import Callable, List, Optional import httpx -from cent import RequestException, ResponseError +from cent.exceptions import ClientNotEmpty, RequestException, ResponseError from cent.mixins import ParamsMixin +from cent.utils import check_not_empty_pipeline, to_bytes class Client(ParamsMixin): @@ -16,7 +18,10 @@ def __init__( address: str, api_key: str = "", timeout: int = 1, + verify: bool = True, json_encoder: Callable = None, + json_dumps: Callable = None, + json_loads: Callable = None, session: Callable = None, **kwargs ): @@ -32,119 +37,160 @@ def __init__( self.address = address self.api_key = api_key self.timeout = timeout - self.json_encoder = json_encoder - self.session = session or httpx.Client() + self.session = session or httpx.Client(verify=verify) self._prepare_session() self.kwargs = kwargs + self.json_encoder = json_encoder + self.json_dumps = json_dumps or json.dumps + self.json_loads = json_loads or json.loads + self._messages = [] + + def _check_empty(self): + if self._messages: + raise ClientNotEmpty("client command buffer not empty, send commands or reset client") def _prepare_session(self): - self.session.headers["Authorization"] = "apikey " + self.api_key + if self.api_key: + self.session.headers["Authorization"] = "apikey " + self.api_key self.session.timeout = self.timeout - def send(self, method, payload): - payload["method"] = method + def add(self, method, params): + data = {"method": method, "params": params} + self._messages.append(data) + + def send(self, method=None, params=None): + if method and params is not None: + self.add(method, params) + messages = self._messages[:] + self.reset() + data = to_bytes("\n".join([self.json_dumps(x, cls=self.json_encoder) for x in messages])) + response = self._send(self.address, data) + return [self.json_loads(x) for x in response.split("\n") if x] + + def _send(self, url, data) -> str: try: - resp = self.session.post( - url=self.address, - json=payload, + resp: httpx.Response = self.session.post( + url=url, + data=data, timeout=self.timeout, ) except RequestException as err: raise RequestException(err) if resp.status_code != 200: raise RequestException("wrong status code: %d" % resp.status_code) - return resp + return resp.text def reset(self): - pass + self._messages = [] - def _send_one(self, method: str, payload: dict) -> Optional[dict]: - resp = self.send(method, payload) - resp_json = resp.json() + def _send_one(self) -> Optional[dict]: + resp_json = self.send()[0] if "error" in resp_json: raise ResponseError(resp_json["error"]) return resp_json.get("result", {}) + @check_not_empty_pipeline def publish(self, channel: str, data: dict, skip_history: bool = False) -> Optional[dict]: - result = self._send_one( + self.add( method="publish", - payload=self.get_publish_params(channel, data, skip_history=skip_history), + params=self.get_publish_params(channel, data, skip_history=skip_history), ) + result = self._send_one() return result + @check_not_empty_pipeline def broadcast(self, channels: List[str], data: dict, skip_history: bool = False) -> Optional[dict]: - result = self._send_one( + self.add( method="broadcast", - payload=self.get_broadcast_params(channels, data, skip_history=skip_history), + params=self.get_broadcast_params(channels, data, skip_history=skip_history), ) + result = self._send_one() return result + @check_not_empty_pipeline def subscribe(self, user: str, channel: str, client: Optional[str] = None) -> None: - self._send_one( + self.add( method="subscribe", - payload=self.get_subscribe_params(user, channel, client=client), + params=self.get_subscribe_params(user, channel, client=client), ) + self._send_one() return + @check_not_empty_pipeline def unsubscribe(self, user: str, channel: str, client: Optional[str] = None) -> None: - self._send_one( + self.add( method="unsubscribe", - payload=self.get_unsubscribe_params(user, channel, client=client), + params=self.get_unsubscribe_params(user, channel, client=client), ) + self._send_one() return + @check_not_empty_pipeline def disconnect(self, user: str, client: Optional[str] = None) -> None: - self._send_one( + self.add( method="disconnect", - payload=self.get_disconnect_params(user, client=client), + params=self.get_disconnect_params(user, client=client), ) + self._send_one() return + @check_not_empty_pipeline def presence(self, channel: str) -> dict: - result = self._send_one( + self.add( method="presence", - payload=self.get_presence_params(channel), + params=self.get_presence_params(channel), ) + result = self._send_one() return result["presence"] + @check_not_empty_pipeline def presence_stats(self, channel: str) -> dict[str, int]: - result = self._send_one( + self.add( method="presence_stats", - payload=self.get_presence_stats_params(channel), + params=self.get_presence_stats_params(channel), ) + result = self._send_one() return { "num_clients": result["num_clients"], "num_users": result["num_users"], } + @check_not_empty_pipeline def history(self, channel: str, limit: int = 0, since: dict = None, reverse: bool = False) -> dict: - result = self._send_one( + self.add( method="history", - payload=self.get_history_params(channel, limit=limit, since=since, reverse=reverse), + params=self.get_history_params(channel, limit=limit, since=since, reverse=reverse), ) + result = self._send_one() return { "publications": result.get("publications", []), "offset": result.get("offset", 0), "epoch": result.get("epoch", ""), } + @check_not_empty_pipeline def history_remove(self, channel: str) -> None: - self._send_one( + self.add( method="history_remove", - payload=self.get_history_remove_params(channel), + params=self.get_history_remove_params(channel), ) + self._send_one() return + @check_not_empty_pipeline def channels(self, pattern="") -> List[Optional[str]]: - result = self._send_one( + self.add( method="channels", - payload=self.get_channels_params(pattern=pattern), + params=self.get_channels_params(pattern=pattern), ) + result = self._send_one() return result["channels"] + @check_not_empty_pipeline def info(self) -> dict[str, list]: - result = self._send_one( + self.add( method="info", - payload=self.get_info_params(), + params=self.get_info_params(), ) + result = self._send_one() return result diff --git a/cent/utils.py b/cent/utils.py index 370634f..3a09355 100644 --- a/cent/utils.py +++ b/cent/utils.py @@ -1,2 +1,15 @@ +from .exceptions import ClientNotEmpty + + def to_bytes(s): return s.encode("latin-1") + + +def check_not_empty_pipeline(func): + def wrapper(self, *args, **kwargs): + if self._messages: + raise ClientNotEmpty("client command buffer not empty, send commands or reset client") + res = func(self, *args, **kwargs) + return res + + return wrapper diff --git a/poetry.lock b/poetry.lock index f046d2a..c48ed01 100644 --- a/poetry.lock +++ b/poetry.lock @@ -16,6 +16,28 @@ doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=6.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] trio = ["trio (>=0.16)"] +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.4.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] + [[package]] name = "black" version = "22.3.0" @@ -78,6 +100,17 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "coverage" +version = "6.3.3" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +toml = ["tomli"] + [[package]] name = "flake8" version = "4.0.1" @@ -163,6 +196,14 @@ zipp = ">=0.5" docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "isort" version = "5.10.1" @@ -193,6 +234,17 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + [[package]] name = "pathspec" version = "0.9.0" @@ -213,6 +265,29 @@ python-versions = ">=3.7" docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "pycodestyle" version = "2.8.0" @@ -229,6 +304,54 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "dev" +optional = false +python-versions = ">=3.6.8" + +[package.extras] +diagrams = ["railroad-diagrams", "jinja2"] + +[[package]] +name = "pytest" +version = "7.1.2" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +tomli = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.18.3" +description = "Pytest support for asyncio" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pytest = ">=6.1.0" +typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} + +[package.extras] +testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)", "pytest-trio (>=0.7.0)"] + [[package]] name = "rfc3986" version = "1.5.0" @@ -287,19 +410,24 @@ python-versions = ">=3.7" docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] -[extras] -async = ["httpx"] - [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "780516b11991f41b3a2cefb69502b39df2c4c86b39b0231dd881ebcbdec04dc6" +content-hash = "c40b319fcff373d71ccc2dc820df977f5494f56583c44f1567f2db3a56c049f1" [metadata.files] anyio = [ {file = "anyio-3.5.0-py3-none-any.whl", hash = "sha256:b5fa16c5ff93fa1046f2eeb5bbff2dad4d3514d6cda61d02816dba34fa8c3c2e"}, {file = "anyio-3.5.0.tar.gz", hash = "sha256:a0aeffe2fb1fdf374a8e4b471444f0f3ac4fb9f5a5b542b48824475e0042a5a6"}, ] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, +] black = [ {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, @@ -341,6 +469,49 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] +coverage = [ + {file = "coverage-6.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df32ee0f4935a101e4b9a5f07b617d884a531ed5666671ff6ac66d2e8e8246d8"}, + {file = "coverage-6.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75b5dbffc334e0beb4f6c503fb95e6d422770fd2d1b40a64898ea26d6c02742d"}, + {file = "coverage-6.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:114944e6061b68a801c5da5427b9173a0dd9d32cd5fcc18a13de90352843737d"}, + {file = "coverage-6.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ab88a01cd180b5640ccc9c47232e31924d5f9967ab7edd7e5c91c68eee47a69"}, + {file = "coverage-6.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad8f9068f5972a46d50fe5f32c09d6ee11da69c560fcb1b4c3baea246ca4109b"}, + {file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4cd696aa712e6cd16898d63cf66139dc70d998f8121ab558f0e1936396dbc579"}, + {file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c1a9942e282cc9d3ed522cd3e3cab081149b27ea3bda72d6f61f84eaf88c1a63"}, + {file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c06455121a089252b5943ea682187a4e0a5cf0a3fb980eb8e7ce394b144430a9"}, + {file = "coverage-6.3.3-cp310-cp310-win32.whl", hash = "sha256:cb5311d6ccbd22578c80028c5e292a7ab9adb91bd62c1982087fad75abe2e63d"}, + {file = "coverage-6.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:6d4a6f30f611e657495cc81a07ff7aa8cd949144e7667c5d3e680d73ba7a70e4"}, + {file = "coverage-6.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:79bf405432428e989cad7b8bc60581963238f7645ae8a404f5dce90236cc0293"}, + {file = "coverage-6.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:338c417613f15596af9eb7a39353b60abec9d8ce1080aedba5ecee6a5d85f8d3"}, + {file = "coverage-6.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db094a6a4ae6329ed322a8973f83630b12715654c197dd392410400a5bfa1a73"}, + {file = "coverage-6.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1414e8b124611bf4df8d77215bd32cba6e3425da8ce9c1f1046149615e3a9a31"}, + {file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:93b16b08f94c92cab88073ffd185070cdcb29f1b98df8b28e6649145b7f2c90d"}, + {file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fbc86ae8cc129c801e7baaafe3addf3c8d49c9c1597c44bdf2d78139707c3c62"}, + {file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b5ba058610e8289a07db2a57bce45a1793ec0d3d11db28c047aae2aa1a832572"}, + {file = "coverage-6.3.3-cp37-cp37m-win32.whl", hash = "sha256:8329635c0781927a2c6ae068461e19674c564e05b86736ab8eb29c420ee7dc20"}, + {file = "coverage-6.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:e5af1feee71099ae2e3b086ec04f57f9950e1be9ecf6c420696fea7977b84738"}, + {file = "coverage-6.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e814a4a5a1d95223b08cdb0f4f57029e8eab22ffdbae2f97107aeef28554517e"}, + {file = "coverage-6.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:61f4fbf3633cb0713437291b8848634ea97f89c7e849c2be17a665611e433f53"}, + {file = "coverage-6.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3401b0d2ed9f726fadbfa35102e00d1b3547b73772a1de5508ef3bdbcb36afe7"}, + {file = "coverage-6.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8586b177b4407f988731eb7f41967415b2197f35e2a6ee1a9b9b561f6323c8e9"}, + {file = "coverage-6.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:892e7fe32191960da559a14536768a62e83e87bbb867e1b9c643e7e0fbce2579"}, + {file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:afb03f981fadb5aed1ac6e3dd34f0488e1a0875623d557b6fad09b97a942b38a"}, + {file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cbe91bc84be4e5ef0b1480d15c7b18e29c73bdfa33e07d3725da7d18e1b0aff2"}, + {file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:91502bf27cbd5c83c95cfea291ef387469f2387508645602e1ca0fd8a4ba7548"}, + {file = "coverage-6.3.3-cp38-cp38-win32.whl", hash = "sha256:c488db059848702aff30aa1d90ef87928d4e72e4f00717343800546fdbff0a94"}, + {file = "coverage-6.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6534fcdfb5c503affb6b1130db7b5bfc8a0f77fa34880146f7a5c117987d0"}, + {file = "coverage-6.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cc692c9ee18f0dd3214843779ba6b275ee4bb9b9a5745ba64265bce911aefd1a"}, + {file = "coverage-6.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:462105283de203df8de58a68c1bb4ba2a8a164097c2379f664fa81d6baf94b81"}, + {file = "coverage-6.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc972d829ad5ef4d4c5fcabd2bbe2add84ce8236f64ba1c0c72185da3a273130"}, + {file = "coverage-6.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:06f54765cdbce99901871d50fe9f41d58213f18e98b170a30ca34f47de7dd5e8"}, + {file = "coverage-6.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7835f76a081787f0ca62a53504361b3869840a1620049b56d803a8cb3a9eeea3"}, + {file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6f5fee77ec3384b934797f1873758f796dfb4f167e1296dc00f8b2e023ce6ee9"}, + {file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:baa8be8aba3dd1e976e68677be68a960a633a6d44c325757aefaa4d66175050f"}, + {file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4d06380e777dd6b35ee936f333d55b53dc4a8271036ff884c909cf6e94be8b6c"}, + {file = "coverage-6.3.3-cp39-cp39-win32.whl", hash = "sha256:f8cabc5fd0091976ab7b020f5708335033e422de25e20ddf9416bdce2b7e07d8"}, + {file = "coverage-6.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c9441d57b0963cf8340268ad62fc83de61f1613034b79c2b1053046af0c5284"}, + {file = "coverage-6.3.3-pp36.pp37.pp38-none-any.whl", hash = "sha256:d522f1dc49127eab0bfbba4e90fa068ecff0899bbf61bf4065c790ddd6c177fe"}, + {file = "coverage-6.3.3.tar.gz", hash = "sha256:2781c43bffbbec2b8867376d4d61916f5e9c4cc168232528562a61d1b4b01879"}, +] flake8 = [ {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, @@ -365,6 +536,10 @@ importlib-metadata = [ {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, ] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] isort = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, @@ -377,6 +552,10 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] pathspec = [ {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, @@ -385,6 +564,14 @@ platformdirs = [ {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, ] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] pycodestyle = [ {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, @@ -393,6 +580,19 @@ pyflakes = [ {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, ] +pyparsing = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +] +pytest = [ + {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, + {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, +] +pytest-asyncio = [ + {file = "pytest-asyncio-0.18.3.tar.gz", hash = "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91"}, + {file = "pytest_asyncio-0.18.3-1-py3-none-any.whl", hash = "sha256:16cf40bdf2b4fb7fc8e4b82bd05ce3fbcd454cbf7b92afc445fe299dabb88213"}, + {file = "pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84"}, +] rfc3986 = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, diff --git a/pyproject.toml b/pyproject.toml index e6e0ebe..bd22289 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,27 @@ description = "Python library to communicate with Centrifugo v3 HTTP API" authors = ["Alexandr Emelin", "Bogdan Evstratenko"] license = "MIT" readme = 'README.md' +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: Apache Software License", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Software Development", + "Topic :: System :: Networking", + "Topic :: Terminals", + "Topic :: Text Processing", + "Topic :: Utilities", + ] + +[tool.poetry.scripts] +cent = "cent.console:run" [tool.poetry.dependencies] python = "^3.7" @@ -14,9 +35,9 @@ httpx = "^0.22.0" black = "^22.3.0" isort = "^5.10.1" flake8 = "^4.0.1" - -[tool.poetry.extras] -async = ["httpx"] +pytest-asyncio = "^0.18.3" +pytest = "^7.1.2" +coverage = "^6.3.3" [build-system] requires = ["poetry-core>=1.0.0"] @@ -25,3 +46,6 @@ build-backend = "poetry.core.masonry.api" [tool.isort] profile = "black" multi_line_output = 3 + +[tool.pytest.ini_options] +asyncio_mode = "auto" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..8524ef4 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,14 @@ +import pytest + +from cent.async_core import AsyncClient +from cent.core import Client + + +@pytest.fixture +def sync_client(): + return Client(address="http://localhost:8000/api") + + +@pytest.fixture +def async_client(): + return AsyncClient(address="http://localhost:8000/api") diff --git a/tests/test_async.py b/tests/test_async.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_sync.py b/tests/test_sync.py new file mode 100644 index 0000000..60aa1cc --- /dev/null +++ b/tests/test_sync.py @@ -0,0 +1,78 @@ +import pytest + +from cent.core import Client, ClientNotEmpty + + +def test_add(sync_client: Client): + sync_client.add("info", {}) + assert len(sync_client._messages) == 1 + + +def test_reset(sync_client: Client): + sync_client.add("info", {}) + assert len(sync_client._messages) == 1 + sync_client.reset() + assert len(sync_client._messages) == 0 + + +def test_info(sync_client: Client): + res = sync_client.info() + assert len(res["nodes"]) == 1 + + +def test_check_empty(sync_client: Client): + sync_client.add("info", {}) + + with pytest.raises(ClientNotEmpty): + sync_client.info("info", {}) + + +def test_publish(sync_client: Client): + res = sync_client.publish(channel="public", data={"hello": 1}) + assert res == {} + + +def test_broadcast(sync_client: Client): + res = sync_client.broadcast(channels=["public", "public2"], data={"hello": 1}, skip_history=True) + assert res == {"responses": [{"result": {}}, {"result": {}}]} + + res = sync_client.broadcast(channels=["public", "public2"], data={"hello": 1}, skip_history=False) + assert res == {"responses": [{"result": {}}, {"result": {}}]} + + +def test_subscribe(sync_client: Client): + res = sync_client.subscribe(user="1", channel="public") + assert res is None + + +def test_unsubscribe(sync_client: Client): + res = sync_client.unsubscribe(user="1", channel="public") + assert res is None + + +def test_disconnect(sync_client: Client): + res = sync_client.disconnect(user="1") + assert res is None + + +def test_presence(sync_client: Client): + res = sync_client.presence(channel="public") + assert res is None + + +def test_presence_stats(sync_client: Client): + sync_client.presence_stats(channel="public") + + +def test_history(sync_client: Client): + sync_client.history(channel="public") + sync_client.history(channel="public", reverse=True) + + +def test_history_remove(sync_client: Client): + sync_client.history_remove(channel="public") + + +def test_channels(sync_client: Client): + res = sync_client.channels() + assert res == {} From 81a5e6d3ee8cc14170b866a41bc5633f13c5bdb9 Mon Sep 17 00:00:00 2001 From: KatantDev Date: Tue, 30 Jan 2024 15:40:34 +1000 Subject: [PATCH 03/55] refactor/feat: new base client with first two features and --- .editorconfig | 24 + .flake8 | 8 - .pre-commit-config.yaml | 30 + README.md | 121 +-- cent/__init__.py | 14 +- cent/__meta__.py | 1 + cent/async_core.py | 146 ---- cent/client/__init__.py | 4 + cent/client/cent_client.py | 79 ++ cent/client/session/__init__.py | 4 + cent/client/session/aiohttp.py | 69 ++ cent/client/session/base.py | 108 +++ cent/context_controller.py | 32 + cent/core.py | 196 ----- cent/exceptions.py | 37 +- cent/methods/__init__.py | 4 + cent/methods/base.py | 53 ++ cent/methods/broadcast.py | 24 + cent/methods/publish.py | 24 + cent/mixins.py | 73 -- tests/__init__.py => cent/py.typed | 0 cent/types/__init__.py | 4 + cent/types/base.py | 15 + cent/types/broadcast.py | 13 + cent/types/publish.py | 12 + cent/utils.py | 15 - poetry.lock | 1287 ++++++++++++++++------------ pyproject.toml | 126 ++- setup.py | 56 -- tests/conftest.py | 14 - tests/test_async.py | 0 tests/test_sync.py | 78 -- 32 files changed, 1400 insertions(+), 1271 deletions(-) create mode 100644 .editorconfig delete mode 100644 .flake8 create mode 100644 .pre-commit-config.yaml create mode 100644 cent/__meta__.py delete mode 100644 cent/async_core.py create mode 100644 cent/client/__init__.py create mode 100644 cent/client/cent_client.py create mode 100644 cent/client/session/__init__.py create mode 100644 cent/client/session/aiohttp.py create mode 100644 cent/client/session/base.py create mode 100644 cent/context_controller.py delete mode 100644 cent/core.py create mode 100644 cent/methods/__init__.py create mode 100644 cent/methods/base.py create mode 100644 cent/methods/broadcast.py create mode 100644 cent/methods/publish.py delete mode 100644 cent/mixins.py rename tests/__init__.py => cent/py.typed (100%) create mode 100644 cent/types/__init__.py create mode 100644 cent/types/base.py create mode 100644 cent/types/broadcast.py create mode 100644 cent/types/publish.py delete mode 100644 cent/utils.py delete mode 100644 setup.py delete mode 100644 tests/conftest.py delete mode 100644 tests/test_async.py delete mode 100644 tests/test_sync.py diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..376731a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,24 @@ +root = true + +[*] +tab_width = 4 +end_of_line = lf +max_line_length = 88 +ij_visual_guides = 88 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{js,py,html}] +charset = utf-8 + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.py] +indent_style = space +indent_size = 4 +ij_python_from_import_parentheses_force_if_multiline = true diff --git a/.flake8 b/.flake8 deleted file mode 100644 index fce6463..0000000 --- a/.flake8 +++ /dev/null @@ -1,8 +0,0 @@ -[flake8] -exclude = - .git, - __pycache__, - .venv, -max-line-length = 120 -format = '%(path)s:%(row)d Column:%(col)d: %(code)s %(text)s' - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..e7ded36 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,30 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.2.0 + hooks: + - id: "trailing-whitespace" + - id: "check-case-conflict" + - id: "check-merge-conflict" + - id: "debug-statements" + - id: "end-of-file-fixer" + - id: "mixed-line-ending" + - id: "detect-private-key" + - id: "check-yaml" + - id: "check-toml" + + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: 'v0.1.15' + hooks: + - id: ruff + args: [ "--fix" ] + - id: ruff-format + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.8.0 + hooks: + - id: mypy + name: Validate types with MyPy + language: system + types: [ python ] + args: + - "cent" diff --git a/README.md b/README.md index 1aa5f74..f9a613c 100644 --- a/README.md +++ b/README.md @@ -1,121 +1,72 @@ CENT ==== -Python tools to communicate with Centrifugo HTTP API. Python >= 3.3 supported. +Python tools to communicate with Centrifugo v5 HTTP API. Python >= 3.9 supported. To install run: ```bash pip install cent ``` +--- +### Centrifugo compatibility +**Cent v5 and higher works only with Centrifugo v5**. + +If you need to work with Centrifugo v3 then use Cent v4 +If you need to work with Centrifugo v2 then use Cent v3 +--- ### High-level library API -First see [available API methods in documentation](https://centrifugal.dev/docs/server/server_api#http-api). +First +see [available API methods in documentation](https://centrifugal.dev/docs/server/server_api#api-methods). -This library contains `Client` class to send messages to Centrifugo from your python-powered backend: +This library contains `CentClient` class to send messages to Centrifugo from your +python-powered backend: ```python -from cent import Client +import asyncio +from cent import CentClient url = "http://localhost:8000/api" api_key = "XXX" -# initialize client instance. -client = Client(url, api_key=api_key, timeout=1) - -# publish data into channel -channel = "public:chat" -data = {"input": "test"} -client.publish(channel, data) - -# other available methods -client.unsubscribe("user_id", "channel") -client.disconnect("user_id") -history = client.history("public:chat") -presence = client.presence("public:chat") -channels = client.channels() -info = client.info() -client.history_remove("public:chat") -``` +# Initialize client +client = CentClient(url, api_key=api_key) -`publish`, `disconnect`, `unsubscribe`, `history_remove` return `None` in case of success. Each of this commands can raise an instance of `CentException`. -I.e.: +async def main(): + response = await client.publish( + "example:2", + {"input": "Hello world!"}, + ) + print(response) -```python -from cent import Client, CentException -client = Client("http://localhost:8000/api", api_key="XXX", timeout=1) -try: - client.publish("public:chat", {"input": "test"}) -except CentException: - # handle exception +if __name__ == "__main__": + asyncio.run(main()) ``` +--- +### CentClient init arguments -Depending on problem occurred exceptions can be: - -* RequestException – HTTP request to Centrifugo failed -* ResponseError - Centrifugo returned some error on request - -Both exceptions inherited from `CentException`. - -### Low-level library API: - -To send lots of commands in one request: - -```python -from cent import Client, CentException - -client = Client("http://localhost:8000/api", api_key="XXX", timeout=1) - -params = { - "channel": "python", - "data": "hello world" -} - -client.add("publish", params) - -try: - result = client.send() -except CentException: - # handle exception -else: - print(result) -``` +Required: -You can use `add` method to add several messages which will be sent. +* base_url - Centrifugo HTTP API endpoint address +* api_key - Centrifugo HTTP API key -You'll get something like this in response: +Optional: -```bash -[{}] -``` +* session (`BaseSession`) - session to use -I.e. list of single response to each command sent. So you need to inspect response on errors (if any) yourself. +You can use `AiohttpSession` or create custom from `BaseSession` class. -### Client initialization arguments +Arguments for default session: Required: -* address - Centrifugo HTTP API endpoint address +* base_url - Centrifugo HTTP API endpoint address Optional: -* `api_key` - HTTP API key of Centrifugo -* `timeout` (default: `1`) - timeout for HTTP requests to Centrifugo -* `json_encoder` (default: `None`) - set custom JSON encoder -* `send_func` (default: `None`) - set custom send function -* `verify` (default: `True`) - when set to `False` no certificate check will be done during requests. - -## For maintainer - -To release: - -1. Bump version in `setup.py` -1. Changelog, push and create new tag -1. `pip install twine` -1. `pip install wheel` -1. `python setup.py sdist bdist_wheel` -1. `twine check dist/*` -1. `twine upload dist/*` +* json_loads — function to load JSON from response body +* timeout - timeout for requests diff --git a/cent/__init__.py b/cent/__init__.py index e6422e5..66d5eff 100644 --- a/cent/__init__.py +++ b/cent/__init__.py @@ -1,5 +1,11 @@ -# coding: utf-8 -from .core import Client -from .exceptions import CentException, ClientNotEmpty, RequestException, ResponseError +from .client import CentClient, BaseSession, AiohttpSession +from .__meta__ import __version__ -__all__ = ["Client", "CentException", "ClientNotEmpty", "RequestException", "ResponseError"] +__all__ = ( + "__version__", + "CentClient", + "BaseSession", + "AiohttpSession", + "types", + "methods", +) diff --git a/cent/__meta__.py b/cent/__meta__.py new file mode 100644 index 0000000..9a5effc --- /dev/null +++ b/cent/__meta__.py @@ -0,0 +1 @@ +__version__ = "5.0.0b1" diff --git a/cent/async_core.py b/cent/async_core.py deleted file mode 100644 index 38b50d9..0000000 --- a/cent/async_core.py +++ /dev/null @@ -1,146 +0,0 @@ -from typing import Callable, List, Optional - -import httpx - -from cent import RequestException, ResponseError -from cent.mixins import ParamsMixin - - -class AsyncClient(ParamsMixin): - def __init__( - self, - address: str, - api_key: str = "", - timeout: int = 1, - json_encoder: Callable = None, - session: Callable = None, - **kwargs - ): - """ - :param address: Centrifugo address - :param api_key: Centrifugo API key - :param timeout: timeout for HTTP requests to Centrifugo - :param json_encoder: custom JSON encoder - :param verify: boolean flag, when set to False no certificate check will be done during requests. - :param session: custom requests.Session instance - """ - - self.address = address - self.api_key = api_key - self.timeout = timeout - self.json_encoder = json_encoder - self.session = session or httpx.AsyncClient() - self._prepare_session() - self.kwargs = kwargs - - def _prepare_session(self): - self.session.headers["Authorization"] = "apikey " + self.api_key - self.session.timeout = self.timeout - - async def send(self, method, payload): - payload["method"] = method - try: - resp: httpx.Response = await self.session.post( - url=self.address, - json=payload, - timeout=self.timeout, - ) - except RequestException as err: - raise RequestException(err) - if resp.status_code != 200: - raise RequestException("wrong status code: %d" % resp.status_code) - return resp - - async def _send_one(self, method: str, payload: dict) -> Optional[dict]: - resp = await self.send(method, payload) - resp_json = resp.json() - if "error" in resp_json: - raise ResponseError(resp_json["error"]) - return resp_json.get("result", {}) - - async def publish(self, channel: str, data: dict, skip_history: bool = False) -> Optional[dict]: - result = await self._send_one( - method="publish", - payload=self.get_publish_params(channel, data, skip_history=skip_history), - ) - return result - - async def broadcast(self, channels: List[str], data: dict, skip_history: bool = False) -> Optional[dict]: - result = await self._send_one( - method="broadcast", - payload=self.get_broadcast_params(channels, data, skip_history=skip_history), - ) - return result - - async def subscribe(self, user: str, channel: str, client: Optional[str] = None) -> None: - await self._send_one( - method="subscribe", - payload=self.get_subscribe_params(user, channel, client=client), - ) - return - - async def unsubscribe(self, user: str, channel: str, client: Optional[str] = None) -> None: - await self._send_one( - method="unsubscribe", - payload=self.get_unsubscribe_params(user, channel, client=client), - ) - return - - async def disconnect(self, user: str, client: Optional[str] = None) -> None: - await self._send_one( - method="disconnect", - payload=self.get_disconnect_params(user, client=client), - ) - return - - async def presence(self, channel: str) -> dict: - result = await self._send_one( - method="presence", - payload=self.get_presence_params(channel), - ) - return result["presence"] - - async def presence_stats(self, channel: str) -> dict[str, int]: - result = await self._send_one( - method="presence_stats", - payload=self.get_presence_stats_params(channel), - ) - return { - "num_clients": result["num_clients"], - "num_users": result["num_users"], - } - - async def history(self, channel: str, limit: int = 0, since: dict = None, reverse: bool = False) -> dict: - result = await self._send_one( - method="history", - payload=self.get_history_params(channel, limit=limit, since=since, reverse=reverse), - ) - return { - "publications": result.get("publications", []), - "offset": result.get("offset", 0), - "epoch": result.get("epoch", ""), - } - - async def history_remove(self, channel: str) -> None: - await self._send_one( - method="history_remove", - payload=self.get_history_remove_params(channel), - ) - return - - async def channels(self, pattern="") -> List[Optional[str]]: - result = await self._send_one( - method="channels", - payload=self.get_channels_params(pattern=pattern), - ) - return result["channels"] - - async def info(self) -> dict[str, list]: - result = await self._send_one( - method="info", - payload=self.get_info_params(), - ) - return result - - async def close(self): - await self.session.aclose() diff --git a/cent/client/__init__.py b/cent/client/__init__.py new file mode 100644 index 0000000..6a5d086 --- /dev/null +++ b/cent/client/__init__.py @@ -0,0 +1,4 @@ +from .cent_client import CentClient +from .session import BaseSession, AiohttpSession + +__all__ = ("CentClient", "BaseSession", "AiohttpSession") diff --git a/cent/client/cent_client.py b/cent/client/cent_client.py new file mode 100644 index 0000000..681ced3 --- /dev/null +++ b/cent/client/cent_client.py @@ -0,0 +1,79 @@ +from typing import List, Optional, Any, Dict, TypeVar + +from cent.client.session import BaseSession, AiohttpSession +from cent.methods.base import CentMethod +from cent.methods.broadcast import BroadcastMethod +from cent.methods.publish import PublishMethod +from cent.types.broadcast import BroadcastObject +from cent.types.publish import PublishObject + +T = TypeVar("T") + + +class CentClient: + def __init__( + self, + base_url: str, + api_key: str, + session: Optional[BaseSession] = None, + ) -> None: + """ + :param base_url: Centrifuge base_url + :param api_key: Centrifuge API key + :param session: Custom Session instance + """ + + self._base_url = base_url + self.api_key = api_key + self.session = session or AiohttpSession(base_url=base_url) + + async def publish( + self, + channel: str, + data: Dict[str, Any], + skip_history: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + b64data: Optional[str] = None, + idempotency_key: Optional[str] = None, + request_timeout: Optional[int] = None, + ) -> PublishObject: + call = PublishMethod( + channel=channel, + data=data, + skip_history=skip_history, + tags=tags, + b64data=b64data, + idempotency_key=idempotency_key, + ) + return await self(call, request_timeout=request_timeout) + + async def broadcast( + self, + channels: List[str], + data: Dict[str, Any], + skip_history: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + b64data: Optional[str] = None, + idempotency_key: Optional[str] = None, + request_timeout: Optional[int] = None, + ) -> BroadcastObject: + call = BroadcastMethod( + channels=channels, + data=data, + skip_history=skip_history, + tags=tags, + b64data=b64data, + idempotency_key=idempotency_key, + ) + return await self(call, request_timeout=request_timeout) + + async def __call__( + self, method: CentMethod[T], request_timeout: Optional[int] = None + ) -> T: + """ + Call API method + + :param method: Centrifugo method + :return: Centrifugo response + """ + return await self.session(self, method, timeout=request_timeout) diff --git a/cent/client/session/__init__.py b/cent/client/session/__init__.py new file mode 100644 index 0000000..62703e6 --- /dev/null +++ b/cent/client/session/__init__.py @@ -0,0 +1,4 @@ +from .base import BaseSession +from .aiohttp import AiohttpSession + +__all__ = ("BaseSession", "AiohttpSession") diff --git a/cent/client/session/aiohttp.py b/cent/client/session/aiohttp.py new file mode 100644 index 0000000..0106837 --- /dev/null +++ b/cent/client/session/aiohttp.py @@ -0,0 +1,69 @@ +import asyncio +from typing import Optional, TYPE_CHECKING, cast, Any + +from aiohttp import ClientSession +from aiohttp.hdrs import USER_AGENT, CONTENT_TYPE +from aiohttp.http import SERVER_SOFTWARE + +from cent.__meta__ import __version__ +from cent.client.session.base import BaseSession +from cent.methods.base import CentMethod, CentType + +if TYPE_CHECKING: + from cent.client.cent_client import CentClient + + +class AiohttpSession(BaseSession): + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self._session: Optional[ClientSession] = None + + async def _create_session(self) -> ClientSession: + if self._session is None or self._session.closed: + self._session = ClientSession( + headers={ + USER_AGENT: f"{SERVER_SOFTWARE} pycent/{__version__}", + CONTENT_TYPE: "application/json", + "X-Centrifugo-Error-Mode": "transport", + }, + ) + + return self._session + + async def close(self) -> None: + if self._session is not None and not self._session.closed: + await self._session.close() + + # https://docs.aiohttp.org/en/stable/client_advanced.html#graceful-shutdown + await asyncio.sleep(0) + + async def make_request( + self, + client: "CentClient", + method: CentMethod[CentType], + timeout: Optional[float] = None, + ) -> CentType: + session = await self._create_session() + session.headers["X-API-Key"] = client.api_key + json_data = method.model_dump(exclude_none=True) + + url = f"{self._base_url}/{method.__api_method__}" + + async with session.post( + url=url, + json=json_data, + timeout=timeout or self._timeout, + ) as resp: + raw_result = await resp.text() + response = self.check_response( + client=client, + method=method, + content=raw_result, + ) + return cast(CentType, response.result) + + def __del__(self) -> None: + if self._session and not self._session.closed: + if self._session.connector is not None and self._session.connector_owner: + self._session.connector.close() + self._session._connector = None diff --git a/cent/client/session/base.py b/cent/client/session/base.py new file mode 100644 index 0000000..0fd8692 --- /dev/null +++ b/cent/client/session/base.py @@ -0,0 +1,108 @@ +import json +from abc import ABC, abstractmethod +from types import TracebackType +from typing import Final, TYPE_CHECKING, Callable, Any, Optional, cast, Type + +from pydantic import ValidationError + +from cent.exceptions import ClientDecodeError, DetailedAPIError +from cent.methods.base import CentMethod, CentType, Response + +if TYPE_CHECKING: + from cent.client.cent_client import CentClient + +DEFAULT_TIMEOUT: Final[float] = 60.0 +_JsonLoads = Callable[..., Any] +_JsonDumps = Callable[..., str] + + +class BaseSession(ABC): + """Base class for all sessions.""" + + def __init__( + self, + base_url: str, + json_loads: _JsonLoads = json.loads, + timeout: float = DEFAULT_TIMEOUT, + ) -> None: + """ + Initialize session. + + :param base_url: Centrifuge base url. + :param json_loads: JSON loader. + :param timeout: Default request timeout. + """ + self._base_url = base_url + self.json_loads = json_loads + self._timeout = timeout + + def check_response( + self, + client: "CentClient", + method: CentMethod[CentType], + content: str, + ) -> Response[CentType]: + """Validate response.""" + try: + json_data = self.json_loads(content) + except Exception as err: + raise ClientDecodeError from err + + try: + response_type = Response[method.__returning__] # type: ignore + response = response_type.model_validate( + json_data, + context={"client": client}, + ) + except ValidationError as err: + raise ClientDecodeError from err + + if response.error is None: + return response + + raise DetailedAPIError( + method=method, + code=response.error.code, + message=response.error.message, + ) + + @abstractmethod + async def close(self) -> None: # pragma: no cover + """ + Close client session + """ + + @abstractmethod + async def make_request( + self, + client: "CentClient", + method: CentMethod[CentType], + timeout: Optional[float] = None, + ) -> CentType: # pragma: no cover + """ + Make request to centrifuge API. + + :param client: Centrifuge client. + :param method: Centrifuge method. + :param timeout: Request timeout. + """ + ... + + async def __call__( + self, + client: "CentClient", + method: CentMethod[CentType], + timeout: Optional[float] = None, + ) -> CentType: + return cast(CentType, await self.make_request(client, method, timeout)) + + async def __aenter__(self) -> "BaseSession": + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + await self.close() diff --git a/cent/context_controller.py b/cent/context_controller.py new file mode 100644 index 0000000..ecc6a98 --- /dev/null +++ b/cent/context_controller.py @@ -0,0 +1,32 @@ +from typing import Optional, TYPE_CHECKING, Any + +from pydantic import BaseModel, PrivateAttr + +if TYPE_CHECKING: + from cent.client import CentClient + + +class ClientContextController(BaseModel): + _client: Optional["CentClient"] = PrivateAttr() + + def model_post_init(self, __context: Any) -> None: + self._client = __context.get("client") if __context else None + + def as_(self, client: Optional["CentClient"]) -> "ClientContextController": + """ + Bind an object to a client instance. + + :param client: Client instance + :return: self + """ + self._client = client + return self + + @property + def client(self) -> Optional["CentClient"]: + """ + Get client instance. + + :return: Client instance + """ + return self._client diff --git a/cent/core.py b/cent/core.py deleted file mode 100644 index 05dda7c..0000000 --- a/cent/core.py +++ /dev/null @@ -1,196 +0,0 @@ -import json -from typing import Callable, List, Optional - -import httpx - -from cent.exceptions import ClientNotEmpty, RequestException, ResponseError -from cent.mixins import ParamsMixin -from cent.utils import check_not_empty_pipeline, to_bytes - - -class Client(ParamsMixin): - """ - Core class to communicate with Centrifugo. - """ - - def __init__( - self, - address: str, - api_key: str = "", - timeout: int = 1, - verify: bool = True, - json_encoder: Callable = None, - json_dumps: Callable = None, - json_loads: Callable = None, - session: Callable = None, - **kwargs - ): - """ - :param address: Centrifugo address - :param api_key: Centrifugo API key - :param timeout: timeout for HTTP requests to Centrifugo - :param json_encoder: custom JSON encoder - :param verify: boolean flag, when set to False no certificate check will be done during requests. - :param session: custom requests.Session instance - """ - - self.address = address - self.api_key = api_key - self.timeout = timeout - self.session = session or httpx.Client(verify=verify) - self._prepare_session() - self.kwargs = kwargs - self.json_encoder = json_encoder - self.json_dumps = json_dumps or json.dumps - self.json_loads = json_loads or json.loads - self._messages = [] - - def _check_empty(self): - if self._messages: - raise ClientNotEmpty("client command buffer not empty, send commands or reset client") - - def _prepare_session(self): - if self.api_key: - self.session.headers["Authorization"] = "apikey " + self.api_key - self.session.timeout = self.timeout - - def add(self, method, params): - data = {"method": method, "params": params} - self._messages.append(data) - - def send(self, method=None, params=None): - if method and params is not None: - self.add(method, params) - messages = self._messages[:] - self.reset() - data = to_bytes("\n".join([self.json_dumps(x, cls=self.json_encoder) for x in messages])) - response = self._send(self.address, data) - return [self.json_loads(x) for x in response.split("\n") if x] - - def _send(self, url, data) -> str: - try: - resp: httpx.Response = self.session.post( - url=url, - data=data, - timeout=self.timeout, - ) - except RequestException as err: - raise RequestException(err) - if resp.status_code != 200: - raise RequestException("wrong status code: %d" % resp.status_code) - return resp.text - - def reset(self): - self._messages = [] - - def _send_one(self) -> Optional[dict]: - resp_json = self.send()[0] - if "error" in resp_json: - raise ResponseError(resp_json["error"]) - return resp_json.get("result", {}) - - @check_not_empty_pipeline - def publish(self, channel: str, data: dict, skip_history: bool = False) -> Optional[dict]: - self.add( - method="publish", - params=self.get_publish_params(channel, data, skip_history=skip_history), - ) - result = self._send_one() - return result - - @check_not_empty_pipeline - def broadcast(self, channels: List[str], data: dict, skip_history: bool = False) -> Optional[dict]: - self.add( - method="broadcast", - params=self.get_broadcast_params(channels, data, skip_history=skip_history), - ) - result = self._send_one() - return result - - @check_not_empty_pipeline - def subscribe(self, user: str, channel: str, client: Optional[str] = None) -> None: - self.add( - method="subscribe", - params=self.get_subscribe_params(user, channel, client=client), - ) - self._send_one() - return - - @check_not_empty_pipeline - def unsubscribe(self, user: str, channel: str, client: Optional[str] = None) -> None: - self.add( - method="unsubscribe", - params=self.get_unsubscribe_params(user, channel, client=client), - ) - self._send_one() - return - - @check_not_empty_pipeline - def disconnect(self, user: str, client: Optional[str] = None) -> None: - self.add( - method="disconnect", - params=self.get_disconnect_params(user, client=client), - ) - self._send_one() - return - - @check_not_empty_pipeline - def presence(self, channel: str) -> dict: - self.add( - method="presence", - params=self.get_presence_params(channel), - ) - result = self._send_one() - return result["presence"] - - @check_not_empty_pipeline - def presence_stats(self, channel: str) -> dict[str, int]: - self.add( - method="presence_stats", - params=self.get_presence_stats_params(channel), - ) - result = self._send_one() - return { - "num_clients": result["num_clients"], - "num_users": result["num_users"], - } - - @check_not_empty_pipeline - def history(self, channel: str, limit: int = 0, since: dict = None, reverse: bool = False) -> dict: - self.add( - method="history", - params=self.get_history_params(channel, limit=limit, since=since, reverse=reverse), - ) - result = self._send_one() - return { - "publications": result.get("publications", []), - "offset": result.get("offset", 0), - "epoch": result.get("epoch", ""), - } - - @check_not_empty_pipeline - def history_remove(self, channel: str) -> None: - self.add( - method="history_remove", - params=self.get_history_remove_params(channel), - ) - self._send_one() - return - - @check_not_empty_pipeline - def channels(self, pattern="") -> List[Optional[str]]: - self.add( - method="channels", - params=self.get_channels_params(pattern=pattern), - ) - result = self._send_one() - return result["channels"] - - @check_not_empty_pipeline - def info(self) -> dict[str, list]: - self.add( - method="info", - params=self.get_info_params(), - ) - result = self._send_one() - return result diff --git a/cent/exceptions.py b/cent/exceptions.py index 49c137c..972ca56 100644 --- a/cent/exceptions.py +++ b/cent/exceptions.py @@ -1,33 +1,32 @@ -class CentException(Exception): +from cent.methods.base import CentMethod, CentType + + +class CentError(Exception): """ Wrapper for all exceptions coming from this library. """ - pass - -class RequestException(CentException): +class ClientDecodeError(CentError): """ - RequestException means that request to Centrifugo API failed in some way. - This is just a wrapper over RequestException from requests library. + ClientDecodeError raised when response from Centrifugo can't be decoded + from JSON. """ - pass - -class ClientNotEmpty(CentException): +class DetailedAPIError(CentError): """ - ClientNotEmpty raised when attempting to call single method but internal - client command buffer is not empty. + DetailedAPIError raised when response from Centrifugo contains any error + as a result of API command execution. """ - pass + def __init__(self, method: CentMethod[CentType], code: int, message: str) -> None: + self.method = method + self.code = code + self.message = message + def __str__(self) -> str: + return f"Centrifuge error #{self.code}: {self.message}" -class ResponseError(CentException): - """ - Raised when response from Centrifugo contains any error as result of API - command execution. - """ - - pass + def __repr__(self) -> str: + return f"{type(self).__name__}('{self}')" diff --git a/cent/methods/__init__.py b/cent/methods/__init__.py new file mode 100644 index 0000000..ef3c2ff --- /dev/null +++ b/cent/methods/__init__.py @@ -0,0 +1,4 @@ +from .broadcast import BroadcastMethod +from .publish import PublishMethod + +__all__ = ("BroadcastMethod", "PublishMethod") diff --git a/cent/methods/base.py b/cent/methods/base.py new file mode 100644 index 0000000..c843ea1 --- /dev/null +++ b/cent/methods/base.py @@ -0,0 +1,53 @@ +from abc import ABC, abstractmethod +from typing import TypeVar, Any, Generic, TYPE_CHECKING, ClassVar, Generator, Optional + +from pydantic import BaseModel, ConfigDict + +from cent.context_controller import ClientContextController + +if TYPE_CHECKING: + from cent.client.cent_client import CentClient + +CentType = TypeVar("CentType", bound=Any) + + +class Error(BaseModel): + code: int + message: str + + +class Response(BaseModel, Generic[CentType]): + error: Optional[Error] = None + result: Optional[CentType] = None + + +class CentMethod(ClientContextController, BaseModel, Generic[CentType], ABC): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + arbitrary_types_allowed=True, + ) + + if TYPE_CHECKING: + __returning__: ClassVar[type] + __api_method__: ClassVar[str] + else: + + @property + @abstractmethod + def __returning__(self) -> type: + pass + + @property + @abstractmethod + def __api_method__(self) -> str: + pass + + async def emit(self, client: "CentClient") -> CentType: + return await client(self) + + def __await__(self) -> Generator[Any, None, CentType]: + client = self._client + if not client: + raise RuntimeError("CentMethod is not bound to a client") + return self.emit(client).__await__() diff --git a/cent/methods/broadcast.py b/cent/methods/broadcast.py new file mode 100644 index 0000000..1ede13f --- /dev/null +++ b/cent/methods/broadcast.py @@ -0,0 +1,24 @@ +from typing import Any, Dict, Optional, List + +from cent.methods.base import CentMethod +from cent.types.broadcast import BroadcastObject + + +class BroadcastMethod(CentMethod[BroadcastObject]): + """Broadcast request.""" + + __returning__ = BroadcastObject + __api_method__ = "broadcast" + + channels: List[str] + """List of channels to publish data to.""" + data: Dict[Any, Any] + """Custom JSON data to publish into a channel.""" + skip_history: Optional[bool] = None + """Skip adding publications to channels' history for this request.""" + tags: Optional[Dict[str, str]] = None + """Publication tags - map with arbitrary string keys and values which is attached to publication and will be delivered to clients.""" + b64data: Optional[str] = None + """Custom binary data to publish into a channel encoded to base64 so it's possible to use HTTP API to send binary to clients. Centrifugo will decode it from base64 before publishing. In case of GRPC you can publish binary using data field.""" + idempotency_key: Optional[str] = None + """Optional idempotency key to drop duplicate publications upon retries. It acts per channel. Centrifugo currently keeps the cache of idempotent publish results during 5 minutes window. Available since Centrifugo v5.2.0""" diff --git a/cent/methods/publish.py b/cent/methods/publish.py new file mode 100644 index 0000000..1c2f07b --- /dev/null +++ b/cent/methods/publish.py @@ -0,0 +1,24 @@ +from typing import Any, Dict, Optional + +from cent.methods.base import CentMethod +from cent.types.publish import PublishObject + + +class PublishMethod(CentMethod[PublishObject]): + """Publish request.""" + + __returning__ = PublishObject + __api_method__ = "publish" + + channel: str + """Name of channel to publish.""" + data: Dict[Any, Any] + """Custom JSON data to publish into a channel.""" + skip_history: Optional[bool] = None + """Skip adding publication to history for this request.""" + tags: Optional[Dict[str, str]] = None + """Publication tags - map with arbitrary string keys and values which is attached to publication and will be delivered to clients.""" + b64data: Optional[str] = None + """Custom binary data to publish into a channel encoded to base64 so it's possible to use HTTP API to send binary to clients. Centrifugo will decode it from base64 before publishing. In case of GRPC you can publish binary using data field.""" + idempotency_key: Optional[str] = None + """Optional idempotency key to drop duplicate publications upon retries. It acts per channel. Centrifugo currently keeps the cache of idempotent publish results during 5 minutes window. Available since Centrifugo v5.2.0""" diff --git a/cent/mixins.py b/cent/mixins.py deleted file mode 100644 index 170c583..0000000 --- a/cent/mixins.py +++ /dev/null @@ -1,73 +0,0 @@ -from typing import List, Optional - - -class ParamsMixin: - @staticmethod - def get_publish_params(channel: str, data: dict, skip_history: bool = False) -> dict: - params = { - "channel": channel, - "data": data, - "skip_history": skip_history, - } - return params - - @staticmethod - def get_broadcast_params(channels: List[str], data: dict, skip_history: bool = False) -> dict: - params = { - "channels": channels, - "data": data, - "skip_history": skip_history, - } - return params - - @staticmethod - def get_subscribe_params(user: str, channel: str, client: Optional[str] = None) -> dict: - params = {"user": user, "channel": channel} - if client: - params["client"] = client - return params - - @staticmethod - def get_unsubscribe_params(user: str, channel: str, client: Optional[str] = None) -> dict: - params = {"user": user, "channel": channel} - if client: - params["client"] = client - return params - - @staticmethod - def get_disconnect_params(user: str, client: Optional[str] = None) -> dict: - params = {"user": user} - if client: - params["client"] = client - return params - - @staticmethod - def get_presence_params(channel: str) -> dict: - return {"channel": channel} - - @staticmethod - def get_presence_stats_params(channel: str) -> dict: - return {"channel": channel} - - @staticmethod - def get_history_params(channel: str, limit: int = 0, since: Optional[dict] = None, reverse: bool = False) -> dict: - params = { - "channel": channel, - "limit": limit, - "reverse": reverse, - } - if since: - params["since"] = {"offset": since["offset"], "epoch": since["epoch"]} - return params - - @staticmethod - def get_history_remove_params(channel: str) -> dict: - return {"channel": channel} - - @staticmethod - def get_channels_params(pattern: str = "") -> dict: - return {"pattern": pattern} - - @staticmethod - def get_info_params() -> dict: - return {} diff --git a/tests/__init__.py b/cent/py.typed similarity index 100% rename from tests/__init__.py rename to cent/py.typed diff --git a/cent/types/__init__.py b/cent/types/__init__.py new file mode 100644 index 0000000..6ddf73e --- /dev/null +++ b/cent/types/__init__.py @@ -0,0 +1,4 @@ +from .broadcast import BroadcastObject +from .publish import PublishObject + +__all__ = ("BroadcastObject", "PublishObject") diff --git a/cent/types/base.py b/cent/types/base.py new file mode 100644 index 0000000..cbfbf46 --- /dev/null +++ b/cent/types/base.py @@ -0,0 +1,15 @@ +from pydantic import BaseModel, ConfigDict + +from cent.context_controller import ClientContextController + + +class CentObject(ClientContextController, BaseModel): + model_config = ConfigDict( + use_enum_values=True, + extra="allow", + validate_assignment=True, + frozen=True, + populate_by_name=True, + arbitrary_types_allowed=True, + defer_build=True, + ) diff --git a/cent/types/broadcast.py b/cent/types/broadcast.py new file mode 100644 index 0000000..9069685 --- /dev/null +++ b/cent/types/broadcast.py @@ -0,0 +1,13 @@ +from typing import List + +from pydantic import Field + +from cent.methods.base import Response +from cent.types.base import CentObject +from cent.types.publish import PublishObject + + +class BroadcastObject(CentObject): + """Publish result.""" + + responses: List[Response[PublishObject]] = Field(default_factory=list) diff --git a/cent/types/publish.py b/cent/types/publish.py new file mode 100644 index 0000000..e9a0c20 --- /dev/null +++ b/cent/types/publish.py @@ -0,0 +1,12 @@ +from typing import Optional + +from cent.types.base import CentObject + + +class PublishObject(CentObject): + """Publish result.""" + + offset: Optional[str] = None + """Offset of publication in history stream.""" + epoch: Optional[str] = None + """Epoch of current stream.""" diff --git a/cent/utils.py b/cent/utils.py deleted file mode 100644 index 3a09355..0000000 --- a/cent/utils.py +++ /dev/null @@ -1,15 +0,0 @@ -from .exceptions import ClientNotEmpty - - -def to_bytes(s): - return s.encode("latin-1") - - -def check_not_empty_pipeline(func): - def wrapper(self, *args, **kwargs): - if self._messages: - raise ClientNotEmpty("client command buffer not empty, send commands or reset client") - res = func(self, *args, **kwargs) - return res - - return wrapper diff --git a/poetry.lock b/poetry.lock index c48ed01..ff13369 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,641 +1,850 @@ -[[package]] -name = "anyio" -version = "3.5.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "main" -optional = false -python-versions = ">=3.6.2" +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. + +[[package]] +name = "aiohttp" +version = "3.9.3" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54"}, + {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc"}, + {file = "aiohttp-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82e6aa28dd46374f72093eda8bcd142f7771ee1eb9d1e223ff0fa7177a96b4a5"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f56455b0c2c7cc3b0c584815264461d07b177f903a04481dfc33e08a89f0c26b"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bca77a198bb6e69795ef2f09a5f4c12758487f83f33d63acde5f0d4919815768"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e083c285857b78ee21a96ba1eb1b5339733c3563f72980728ca2b08b53826ca5"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab40e6251c3873d86ea9b30a1ac6d7478c09277b32e14745d0d3c6e76e3c7e29"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df822ee7feaaeffb99c1a9e5e608800bd8eda6e5f18f5cfb0dc7eeb2eaa6bbec"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:acef0899fea7492145d2bbaaaec7b345c87753168589cc7faf0afec9afe9b747"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cd73265a9e5ea618014802ab01babf1940cecb90c9762d8b9e7d2cc1e1969ec6"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a78ed8a53a1221393d9637c01870248a6f4ea5b214a59a92a36f18151739452c"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6b0e029353361f1746bac2e4cc19b32f972ec03f0f943b390c4ab3371840aabf"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7cf5c9458e1e90e3c390c2639f1017a0379a99a94fdfad3a1fd966a2874bba52"}, + {file = "aiohttp-3.9.3-cp310-cp310-win32.whl", hash = "sha256:3e59c23c52765951b69ec45ddbbc9403a8761ee6f57253250c6e1536cacc758b"}, + {file = "aiohttp-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:055ce4f74b82551678291473f66dc9fb9048a50d8324278751926ff0ae7715e5"}, + {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b88f9386ff1ad91ace19d2a1c0225896e28815ee09fc6a8932fded8cda97c3d"}, + {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c46956ed82961e31557b6857a5ca153c67e5476972e5f7190015018760938da2"}, + {file = "aiohttp-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07b837ef0d2f252f96009e9b8435ec1fef68ef8b1461933253d318748ec1acdc"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad46e6f620574b3b4801c68255492e0159d1712271cc99d8bdf35f2043ec266"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ed3e046ea7b14938112ccd53d91c1539af3e6679b222f9469981e3dac7ba1ce"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:039df344b45ae0b34ac885ab5b53940b174530d4dd8a14ed8b0e2155b9dddccb"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7943c414d3a8d9235f5f15c22ace69787c140c80b718dcd57caaade95f7cd93b"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84871a243359bb42c12728f04d181a389718710129b36b6aad0fc4655a7647d4"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5eafe2c065df5401ba06821b9a054d9cb2848867f3c59801b5d07a0be3a380ae"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9d3c9b50f19704552f23b4eaea1fc082fdd82c63429a6506446cbd8737823da3"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:f033d80bc6283092613882dfe40419c6a6a1527e04fc69350e87a9df02bbc283"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2c895a656dd7e061b2fd6bb77d971cc38f2afc277229ce7dd3552de8313a483e"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1f5a71d25cd8106eab05f8704cd9167b6e5187bcdf8f090a66c6d88b634802b4"}, + {file = "aiohttp-3.9.3-cp311-cp311-win32.whl", hash = "sha256:50fca156d718f8ced687a373f9e140c1bb765ca16e3d6f4fe116e3df7c05b2c5"}, + {file = "aiohttp-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:5fe9ce6c09668063b8447f85d43b8d1c4e5d3d7e92c63173e6180b2ac5d46dd8"}, + {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:38a19bc3b686ad55804ae931012f78f7a534cce165d089a2059f658f6c91fa60"}, + {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:770d015888c2a598b377bd2f663adfd947d78c0124cfe7b959e1ef39f5b13869"}, + {file = "aiohttp-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee43080e75fc92bf36219926c8e6de497f9b247301bbf88c5c7593d931426679"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52df73f14ed99cee84865b95a3d9e044f226320a87af208f068ecc33e0c35b96"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9b311743a78043b26ffaeeb9715dc360335e5517832f5a8e339f8a43581e4d"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b955ed993491f1a5da7f92e98d5dad3c1e14dc175f74517c4e610b1f2456fb11"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504b6981675ace64c28bf4a05a508af5cde526e36492c98916127f5a02354d53"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fe5571784af92b6bc2fda8d1925cccdf24642d49546d3144948a6a1ed58ca5"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ba39e9c8627edc56544c8628cc180d88605df3892beeb2b94c9bc857774848ca"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e5e46b578c0e9db71d04c4b506a2121c0cb371dd89af17a0586ff6769d4c58c1"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:938a9653e1e0c592053f815f7028e41a3062e902095e5a7dc84617c87267ebd5"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:c3452ea726c76e92f3b9fae4b34a151981a9ec0a4847a627c43d71a15ac32aa6"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff30218887e62209942f91ac1be902cc80cddb86bf00fbc6783b7a43b2bea26f"}, + {file = "aiohttp-3.9.3-cp312-cp312-win32.whl", hash = "sha256:38f307b41e0bea3294a9a2a87833191e4bcf89bb0365e83a8be3a58b31fb7f38"}, + {file = "aiohttp-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:b791a3143681a520c0a17e26ae7465f1b6f99461a28019d1a2f425236e6eedb5"}, + {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ed621426d961df79aa3b963ac7af0d40392956ffa9be022024cd16297b30c8c"}, + {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f46acd6a194287b7e41e87957bfe2ad1ad88318d447caf5b090012f2c5bb528"}, + {file = "aiohttp-3.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feeb18a801aacb098220e2c3eea59a512362eb408d4afd0c242044c33ad6d542"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f734e38fd8666f53da904c52a23ce517f1b07722118d750405af7e4123933511"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b40670ec7e2156d8e57f70aec34a7216407848dfe6c693ef131ddf6e76feb672"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdd215b7b7fd4a53994f238d0f46b7ba4ac4c0adb12452beee724ddd0743ae5d"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:017a21b0df49039c8f46ca0971b3a7fdc1f56741ab1240cb90ca408049766168"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99abf0bba688259a496f966211c49a514e65afa9b3073a1fcee08856e04425b"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:648056db9a9fa565d3fa851880f99f45e3f9a771dd3ff3bb0c048ea83fb28194"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8aacb477dc26797ee089721536a292a664846489c49d3ef9725f992449eda5a8"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:522a11c934ea660ff8953eda090dcd2154d367dec1ae3c540aff9f8a5c109ab4"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5bce0dc147ca85caa5d33debc4f4d65e8e8b5c97c7f9f660f215fa74fc49a321"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b4af9f25b49a7be47c0972139e59ec0e8285c371049df1a63b6ca81fdd216a2"}, + {file = "aiohttp-3.9.3-cp38-cp38-win32.whl", hash = "sha256:298abd678033b8571995650ccee753d9458dfa0377be4dba91e4491da3f2be63"}, + {file = "aiohttp-3.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:69361bfdca5468c0488d7017b9b1e5ce769d40b46a9f4a2eed26b78619e9396c"}, + {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0fa43c32d1643f518491d9d3a730f85f5bbaedcbd7fbcae27435bb8b7a061b29"}, + {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:835a55b7ca49468aaaac0b217092dfdff370e6c215c9224c52f30daaa735c1c1"}, + {file = "aiohttp-3.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06a9b2c8837d9a94fae16c6223acc14b4dfdff216ab9b7202e07a9a09541168f"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abf151955990d23f84205286938796c55ff11bbfb4ccfada8c9c83ae6b3c89a3"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59c26c95975f26e662ca78fdf543d4eeaef70e533a672b4113dd888bd2423caa"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f95511dd5d0e05fd9728bac4096319f80615aaef4acbecb35a990afebe953b0e"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:595f105710293e76b9dc09f52e0dd896bd064a79346234b521f6b968ffdd8e58"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7c8b816c2b5af5c8a436df44ca08258fc1a13b449393a91484225fcb7545533"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f1088fa100bf46e7b398ffd9904f4808a0612e1d966b4aa43baa535d1b6341eb"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f59dfe57bb1ec82ac0698ebfcdb7bcd0e99c255bd637ff613760d5f33e7c81b3"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:361a1026c9dd4aba0109e4040e2aecf9884f5cfe1b1b1bd3d09419c205e2e53d"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:363afe77cfcbe3a36353d8ea133e904b108feea505aa4792dad6585a8192c55a"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e2c45c208c62e955e8256949eb225bd8b66a4c9b6865729a786f2aa79b72e9d"}, + {file = "aiohttp-3.9.3-cp39-cp39-win32.whl", hash = "sha256:f7217af2e14da0856e082e96ff637f14ae45c10a5714b63c77f26d8884cf1051"}, + {file = "aiohttp-3.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:27468897f628c627230dba07ec65dc8d0db566923c48f29e084ce382119802bc"}, + {file = "aiohttp-3.9.3.tar.gz", hash = "sha256:90842933e5d1ff760fae6caca4b2b3edba53ba8f4b71e95dacf2818a2aca06f7"}, +] [package.dependencies] -idna = ">=2.8" -sniffio = ">=1.1" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - -[package.extras] -doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] -test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=6.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] -trio = ["trio (>=0.16)"] - -[[package]] -name = "atomicwrites" -version = "1.4.0" -description = "Atomic file writes." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "attrs" -version = "21.4.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +aiosignal = ">=1.1.2" +async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.0,<2.0" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] +speedups = ["Brotli", "aiodns", "brotlicffi"] [[package]] -name = "black" -version = "22.3.0" -description = "The uncompromising code formatter." -category = "dev" +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] [package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "certifi" -version = "2021.10.8" -description = "Python package for providing Mozilla's CA Bundle." -category = "main" -optional = false -python-versions = "*" +frozenlist = ">=1.1.0" [[package]] -name = "charset-normalizer" -version = "2.0.12" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" optional = false -python-versions = ">=3.5.0" - -[package.extras] -unicode_backport = ["unicodedata2"] +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] [[package]] -name = "click" -version = "8.1.3" -description = "Composable command line interface toolkit" -category = "dev" +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.7" - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} - -[[package]] -name = "colorama" -version = "0.4.4" -description = "Cross-platform colored terminal text." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] [[package]] -name = "coverage" -version = "6.3.3" -description = "Code coverage measurement for Python" -category = "dev" +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] [package.extras] -toml = ["tomli"] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] [[package]] -name = "flake8" -version = "4.0.1" -description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." optional = false -python-versions = ">=3.6" - -[package.dependencies] -importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.8.0,<2.9.0" -pyflakes = ">=2.4.0,<2.5.0" +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] [[package]] -name = "h11" -version = "0.12.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "main" +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" optional = false -python-versions = ">=3.6" +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] [[package]] -name = "httpcore" -version = "0.14.7" -description = "A minimal low-level HTTP client." -category = "main" +name = "filelock" +version = "3.13.1" +description = "A platform independent file lock." optional = false -python-versions = ">=3.6" - -[package.dependencies] -anyio = ">=3.0.0,<4.0.0" -certifi = "*" -h11 = ">=0.11,<0.13" -sniffio = ">=1.0.0,<2.0.0" +python-versions = ">=3.8" +files = [ + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, +] [package.extras] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] - -[[package]] -name = "httpx" -version = "0.22.0" -description = "The next generation HTTP client." -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -certifi = "*" -charset-normalizer = "*" -httpcore = ">=0.14.5,<0.15.0" -rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} -sniffio = "*" +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "frozenlist" +version = "1.4.1" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, + {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, + {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, + {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, + {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, + {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, + {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, + {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, + {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, + {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, + {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, + {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, + {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, +] + +[[package]] +name = "identify" +version = "2.5.33" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, + {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, +] [package.extras] -brotli = ["brotlicffi", "brotli"] -cli = ["click (>=8.0.0,<9.0.0)", "rich (>=10.0.0,<11.0.0)", "pygments (>=2.0.0,<3.0.0)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +license = ["ukkonen"] [[package]] name = "idna" -version = "3.3" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" +files = [ + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] [[package]] -name = "importlib-metadata" -version = "4.2.0" -description = "Read metadata from Python packages" -category = "dev" +name = "multidict" +version = "6.0.4" +description = "multidict implementation" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +files = [ + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, + {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, + {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, + {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, + {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, + {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, + {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, + {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, + {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, + {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, + {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, + {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, + {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, +] + +[[package]] +name = "mypy" +version = "1.8.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, + {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, + {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, + {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, + {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, + {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, + {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, + {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, + {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, + {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, + {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, + {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, + {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, + {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, + {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, + {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, + {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, + {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, + {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, +] [package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] - -[[package]] -name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "isort" -version = "5.10.1" -description = "A Python utility / library to sort Python imports." -category = "dev" -optional = false -python-versions = ">=3.6.1,<4.0" +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" [package.extras] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] -requirements_deprecated_finder = ["pipreqs", "pip-api"] -colors = ["colorama (>=0.4.3,<0.5.0)"] -plugins = ["setuptools"] - -[[package]] -name = "mccabe" -version = "0.6.1" -description = "McCabe checker, plugin for flake8" -category = "dev" -optional = false -python-versions = "*" +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] [[package]] name = "mypy-extensions" -version = "0.4.3" -description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." optional = false -python-versions = "*" +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] [[package]] -name = "packaging" -version = "21.3" -description = "Core utilities for Python packages" -category = "dev" +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" optional = false -python-versions = ">=3.6" +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] [package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" - -[[package]] -name = "pathspec" -version = "0.9.0" -description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +setuptools = "*" [[package]] name = "platformdirs" -version = "2.5.2" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" +version = "4.1.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, + {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, +] [package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] -test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] [[package]] -name = "pluggy" -version = "1.0.0" -description = "plugin and hook calling mechanisms for python" -category = "dev" +name = "pre-commit" +version = "3.6.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.6.0-py2.py3-none-any.whl", hash = "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376"}, + {file = "pre_commit-3.6.0.tar.gz", hash = "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"}, +] [package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "pycodestyle" -version = "2.8.0" -description = "Python style guide checker" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "pyflakes" -version = "2.4.0" -description = "passive checker of Python programs" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "dev" -optional = false -python-versions = ">=3.6.8" - -[package.extras] -diagrams = ["railroad-diagrams", "jinja2"] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" [[package]] -name = "pytest" -version = "7.1.2" -description = "pytest: simple powerful testing with Python" -category = "dev" +name = "pydantic" +version = "2.6.0" +description = "Data validation using Python type hints" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.6.0-py3-none-any.whl", hash = "sha256:1440966574e1b5b99cf75a13bec7b20e3512e8a61b894ae252f56275e2c465ae"}, + {file = "pydantic-2.6.0.tar.gz", hash = "sha256:ae887bd94eb404b09d86e4d12f93893bdca79d766e738528c6fa1c849f3c6bcf"}, +] [package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=19.2.0" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" +annotated-types = ">=0.4.0" +pydantic-core = "2.16.1" +typing-extensions = ">=4.6.1" [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] - -[[package]] -name = "pytest-asyncio" -version = "0.18.3" -description = "Pytest support for asyncio" -category = "dev" -optional = false -python-versions = ">=3.7" +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.16.1" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.16.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:300616102fb71241ff477a2cbbc847321dbec49428434a2f17f37528721c4948"}, + {file = "pydantic_core-2.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5511f962dd1b9b553e9534c3b9c6a4b0c9ded3d8c2be96e61d56f933feef9e1f"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98f0edee7ee9cc7f9221af2e1b95bd02810e1c7a6d115cfd82698803d385b28f"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9795f56aa6b2296f05ac79d8a424e94056730c0b860a62b0fdcfe6340b658cc8"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c45f62e4107ebd05166717ac58f6feb44471ed450d07fecd90e5f69d9bf03c48"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:462d599299c5971f03c676e2b63aa80fec5ebc572d89ce766cd11ca8bcb56f3f"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ebaa4bf6386a3b22eec518da7d679c8363fb7fb70cf6972161e5542f470798"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:99f9a50b56713a598d33bc23a9912224fc5d7f9f292444e6664236ae471ddf17"}, + {file = "pydantic_core-2.16.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8ec364e280db4235389b5e1e6ee924723c693cbc98e9d28dc1767041ff9bc388"}, + {file = "pydantic_core-2.16.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:653a5dfd00f601a0ed6654a8b877b18d65ac32c9d9997456e0ab240807be6cf7"}, + {file = "pydantic_core-2.16.1-cp310-none-win32.whl", hash = "sha256:1661c668c1bb67b7cec96914329d9ab66755911d093bb9063c4c8914188af6d4"}, + {file = "pydantic_core-2.16.1-cp310-none-win_amd64.whl", hash = "sha256:561be4e3e952c2f9056fba5267b99be4ec2afadc27261505d4992c50b33c513c"}, + {file = "pydantic_core-2.16.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:102569d371fadc40d8f8598a59379c37ec60164315884467052830b28cc4e9da"}, + {file = "pydantic_core-2.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:735dceec50fa907a3c314b84ed609dec54b76a814aa14eb90da31d1d36873a5e"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e83ebbf020be727d6e0991c1b192a5c2e7113eb66e3def0cd0c62f9f266247e4"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:30a8259569fbeec49cfac7fda3ec8123486ef1b729225222f0d41d5f840b476f"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:920c4897e55e2881db6a6da151198e5001552c3777cd42b8a4c2f72eedc2ee91"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5247a3d74355f8b1d780d0f3b32a23dd9f6d3ff43ef2037c6dcd249f35ecf4c"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5bea8012df5bb6dda1e67d0563ac50b7f64a5d5858348b5c8cb5043811c19d"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ed3025a8a7e5a59817b7494686d449ebfbe301f3e757b852c8d0d1961d6be864"}, + {file = "pydantic_core-2.16.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06f0d5a1d9e1b7932477c172cc720b3b23c18762ed7a8efa8398298a59d177c7"}, + {file = "pydantic_core-2.16.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:150ba5c86f502c040b822777e2e519b5625b47813bd05f9273a8ed169c97d9ae"}, + {file = "pydantic_core-2.16.1-cp311-none-win32.whl", hash = "sha256:d6cbdf12ef967a6aa401cf5cdf47850559e59eedad10e781471c960583f25aa1"}, + {file = "pydantic_core-2.16.1-cp311-none-win_amd64.whl", hash = "sha256:afa01d25769af33a8dac0d905d5c7bb2d73c7c3d5161b2dd6f8b5b5eea6a3c4c"}, + {file = "pydantic_core-2.16.1-cp311-none-win_arm64.whl", hash = "sha256:1a2fe7b00a49b51047334d84aafd7e39f80b7675cad0083678c58983662da89b"}, + {file = "pydantic_core-2.16.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f478ec204772a5c8218e30eb813ca43e34005dff2eafa03931b3d8caef87d51"}, + {file = "pydantic_core-2.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1936ef138bed2165dd8573aa65e3095ef7c2b6247faccd0e15186aabdda7f66"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99d3a433ef5dc3021c9534a58a3686c88363c591974c16c54a01af7efd741f13"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd88f40f2294440d3f3c6308e50d96a0d3d0973d6f1a5732875d10f569acef49"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fac641bbfa43d5a1bed99d28aa1fded1984d31c670a95aac1bf1d36ac6ce137"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72bf9308a82b75039b8c8edd2be2924c352eda5da14a920551a8b65d5ee89253"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb4363e6c9fc87365c2bc777a1f585a22f2f56642501885ffc7942138499bf54"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:20f724a023042588d0f4396bbbcf4cffd0ddd0ad3ed4f0d8e6d4ac4264bae81e"}, + {file = "pydantic_core-2.16.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fb4370b15111905bf8b5ba2129b926af9470f014cb0493a67d23e9d7a48348e8"}, + {file = "pydantic_core-2.16.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23632132f1fd608034f1a56cc3e484be00854db845b3a4a508834be5a6435a6f"}, + {file = "pydantic_core-2.16.1-cp312-none-win32.whl", hash = "sha256:b9f3e0bffad6e238f7acc20c393c1ed8fab4371e3b3bc311020dfa6020d99212"}, + {file = "pydantic_core-2.16.1-cp312-none-win_amd64.whl", hash = "sha256:a0b4cfe408cd84c53bab7d83e4209458de676a6ec5e9c623ae914ce1cb79b96f"}, + {file = "pydantic_core-2.16.1-cp312-none-win_arm64.whl", hash = "sha256:d195add190abccefc70ad0f9a0141ad7da53e16183048380e688b466702195dd"}, + {file = "pydantic_core-2.16.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:502c062a18d84452858f8aea1e520e12a4d5228fc3621ea5061409d666ea1706"}, + {file = "pydantic_core-2.16.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8c032ccee90b37b44e05948b449a2d6baed7e614df3d3f47fe432c952c21b60"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:920f4633bee43d7a2818e1a1a788906df5a17b7ab6fe411220ed92b42940f818"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f5d37ff01edcbace53a402e80793640c25798fb7208f105d87a25e6fcc9ea06"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:399166f24c33a0c5759ecc4801f040dbc87d412c1a6d6292b2349b4c505effc9"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac89ccc39cd1d556cc72d6752f252dc869dde41c7c936e86beac5eb555041b66"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73802194f10c394c2bedce7a135ba1d8ba6cff23adf4217612bfc5cf060de34c"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8fa00fa24ffd8c31fac081bf7be7eb495be6d248db127f8776575a746fa55c95"}, + {file = "pydantic_core-2.16.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:601d3e42452cd4f2891c13fa8c70366d71851c1593ed42f57bf37f40f7dca3c8"}, + {file = "pydantic_core-2.16.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07982b82d121ed3fc1c51faf6e8f57ff09b1325d2efccaa257dd8c0dd937acca"}, + {file = "pydantic_core-2.16.1-cp38-none-win32.whl", hash = "sha256:d0bf6f93a55d3fa7a079d811b29100b019784e2ee6bc06b0bb839538272a5610"}, + {file = "pydantic_core-2.16.1-cp38-none-win_amd64.whl", hash = "sha256:fbec2af0ebafa57eb82c18c304b37c86a8abddf7022955d1742b3d5471a6339e"}, + {file = "pydantic_core-2.16.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a497be217818c318d93f07e14502ef93d44e6a20c72b04c530611e45e54c2196"}, + {file = "pydantic_core-2.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:694a5e9f1f2c124a17ff2d0be613fd53ba0c26de588eb4bdab8bca855e550d95"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d4dfc66abea3ec6d9f83e837a8f8a7d9d3a76d25c9911735c76d6745950e62c"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8655f55fe68c4685673265a650ef71beb2d31871c049c8b80262026f23605ee3"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21e3298486c4ea4e4d5cc6fb69e06fb02a4e22089304308817035ac006a7f506"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71b4a48a7427f14679f0015b13c712863d28bb1ab700bd11776a5368135c7d60"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dca874e35bb60ce4f9f6665bfbfad050dd7573596608aeb9e098621ac331dc"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa496cd45cda0165d597e9d6f01e36c33c9508f75cf03c0a650018c5048f578e"}, + {file = "pydantic_core-2.16.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5317c04349472e683803da262c781c42c5628a9be73f4750ac7d13040efb5d2d"}, + {file = "pydantic_core-2.16.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:42c29d54ed4501a30cd71015bf982fa95e4a60117b44e1a200290ce687d3e640"}, + {file = "pydantic_core-2.16.1-cp39-none-win32.whl", hash = "sha256:ba07646f35e4e49376c9831130039d1b478fbfa1215ae62ad62d2ee63cf9c18f"}, + {file = "pydantic_core-2.16.1-cp39-none-win_amd64.whl", hash = "sha256:2133b0e412a47868a358713287ff9f9a328879da547dc88be67481cdac529118"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d25ef0c33f22649b7a088035fd65ac1ce6464fa2876578df1adad9472f918a76"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:99c095457eea8550c9fa9a7a992e842aeae1429dab6b6b378710f62bfb70b394"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b49c604ace7a7aa8af31196abbf8f2193be605db6739ed905ecaf62af31ccae0"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c56da23034fe66221f2208c813d8aa509eea34d97328ce2add56e219c3a9f41c"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cebf8d56fee3b08ad40d332a807ecccd4153d3f1ba8231e111d9759f02edfd05"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1ae8048cba95f382dba56766525abca438328455e35c283bb202964f41a780b0"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:780daad9e35b18d10d7219d24bfb30148ca2afc309928e1d4d53de86822593dc"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c94b5537bf6ce66e4d7830c6993152940a188600f6ae044435287753044a8fe2"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:adf28099d061a25fbcc6531febb7a091e027605385de9fe14dd6a97319d614cf"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:644904600c15816a1f9a1bafa6aab0d21db2788abcdf4e2a77951280473f33e1"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87bce04f09f0552b66fca0c4e10da78d17cb0e71c205864bab4e9595122cb9d9"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:877045a7969ace04d59516d5d6a7dee13106822f99a5d8df5e6822941f7bedc8"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9c46e556ee266ed3fb7b7a882b53df3c76b45e872fdab8d9cf49ae5e91147fd7"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4eebbd049008eb800f519578e944b8dc8e0f7d59a5abb5924cc2d4ed3a1834ff"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c0be58529d43d38ae849a91932391eb93275a06b93b79a8ab828b012e916a206"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b1fc07896fc1851558f532dffc8987e526b682ec73140886c831d773cef44b76"}, + {file = "pydantic_core-2.16.1.tar.gz", hash = "sha256:daff04257b49ab7f4b3f73f98283d3dbb1a65bf3500d55c7beac3c66c310fe34"}, +] [package.dependencies] -pytest = ">=6.1.0" -typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} - -[package.extras] -testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)", "pytest-trio (>=0.7.0)"] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] -name = "rfc3986" -version = "1.5.0" -description = "Validating URI References per RFC 3986" -category = "main" +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" optional = false -python-versions = "*" - -[package.dependencies] -idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "ruff" +version = "0.1.15" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, + {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, + {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, + {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, + {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, +] + +[[package]] +name = "setuptools" +version = "69.0.3" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, + {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, +] [package.extras] -idna2008 = ["idna"] - -[[package]] -name = "sniffio" -version = "1.2.0" -description = "Sniff out which async library your code is running under" -category = "main" -optional = false -python-versions = ">=3.5" +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] [[package]] -name = "typed-ast" -version = "1.5.3" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" +name = "typing-extensions" +version = "4.9.0" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, +] [[package]] -name = "typing-extensions" -version = "4.1.1" -description = "Backported and Experimental Type Hints for Python 3.6+" -category = "main" +name = "virtualenv" +version = "20.25.0" +description = "Virtual Python Environment builder" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, + {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [[package]] -name = "zipp" -version = "3.8.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" +name = "yarl" +version = "1.9.4" +description = "Yet another URL library" optional = false python-versions = ">=3.7" +files = [ + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, + {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, + {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, + {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, + {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, + {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, + {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, + {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, + {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, + {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, + {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, + {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, + {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, + {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, + {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, + {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, +] -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" [metadata] -lock-version = "1.1" -python-versions = "^3.7" -content-hash = "c40b319fcff373d71ccc2dc820df977f5494f56583c44f1567f2db3a56c049f1" - -[metadata.files] -anyio = [ - {file = "anyio-3.5.0-py3-none-any.whl", hash = "sha256:b5fa16c5ff93fa1046f2eeb5bbff2dad4d3514d6cda61d02816dba34fa8c3c2e"}, - {file = "anyio-3.5.0.tar.gz", hash = "sha256:a0aeffe2fb1fdf374a8e4b471444f0f3ac4fb9f5a5b542b48824475e0042a5a6"}, -] -atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, -] -attrs = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, -] -black = [ - {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, - {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, - {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, - {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, - {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, - {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, - {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, - {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, - {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, - {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, - {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, - {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, - {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, - {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, - {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, - {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, - {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, - {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, - {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, - {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, - {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, - {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, - {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, -] -certifi = [ - {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, - {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, -] -charset-normalizer = [ - {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, - {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, -] -click = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] -colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, -] -coverage = [ - {file = "coverage-6.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df32ee0f4935a101e4b9a5f07b617d884a531ed5666671ff6ac66d2e8e8246d8"}, - {file = "coverage-6.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75b5dbffc334e0beb4f6c503fb95e6d422770fd2d1b40a64898ea26d6c02742d"}, - {file = "coverage-6.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:114944e6061b68a801c5da5427b9173a0dd9d32cd5fcc18a13de90352843737d"}, - {file = "coverage-6.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ab88a01cd180b5640ccc9c47232e31924d5f9967ab7edd7e5c91c68eee47a69"}, - {file = "coverage-6.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad8f9068f5972a46d50fe5f32c09d6ee11da69c560fcb1b4c3baea246ca4109b"}, - {file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4cd696aa712e6cd16898d63cf66139dc70d998f8121ab558f0e1936396dbc579"}, - {file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c1a9942e282cc9d3ed522cd3e3cab081149b27ea3bda72d6f61f84eaf88c1a63"}, - {file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c06455121a089252b5943ea682187a4e0a5cf0a3fb980eb8e7ce394b144430a9"}, - {file = "coverage-6.3.3-cp310-cp310-win32.whl", hash = "sha256:cb5311d6ccbd22578c80028c5e292a7ab9adb91bd62c1982087fad75abe2e63d"}, - {file = "coverage-6.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:6d4a6f30f611e657495cc81a07ff7aa8cd949144e7667c5d3e680d73ba7a70e4"}, - {file = "coverage-6.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:79bf405432428e989cad7b8bc60581963238f7645ae8a404f5dce90236cc0293"}, - {file = "coverage-6.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:338c417613f15596af9eb7a39353b60abec9d8ce1080aedba5ecee6a5d85f8d3"}, - {file = "coverage-6.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db094a6a4ae6329ed322a8973f83630b12715654c197dd392410400a5bfa1a73"}, - {file = "coverage-6.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1414e8b124611bf4df8d77215bd32cba6e3425da8ce9c1f1046149615e3a9a31"}, - {file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:93b16b08f94c92cab88073ffd185070cdcb29f1b98df8b28e6649145b7f2c90d"}, - {file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fbc86ae8cc129c801e7baaafe3addf3c8d49c9c1597c44bdf2d78139707c3c62"}, - {file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b5ba058610e8289a07db2a57bce45a1793ec0d3d11db28c047aae2aa1a832572"}, - {file = "coverage-6.3.3-cp37-cp37m-win32.whl", hash = "sha256:8329635c0781927a2c6ae068461e19674c564e05b86736ab8eb29c420ee7dc20"}, - {file = "coverage-6.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:e5af1feee71099ae2e3b086ec04f57f9950e1be9ecf6c420696fea7977b84738"}, - {file = "coverage-6.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e814a4a5a1d95223b08cdb0f4f57029e8eab22ffdbae2f97107aeef28554517e"}, - {file = "coverage-6.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:61f4fbf3633cb0713437291b8848634ea97f89c7e849c2be17a665611e433f53"}, - {file = "coverage-6.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3401b0d2ed9f726fadbfa35102e00d1b3547b73772a1de5508ef3bdbcb36afe7"}, - {file = "coverage-6.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8586b177b4407f988731eb7f41967415b2197f35e2a6ee1a9b9b561f6323c8e9"}, - {file = "coverage-6.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:892e7fe32191960da559a14536768a62e83e87bbb867e1b9c643e7e0fbce2579"}, - {file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:afb03f981fadb5aed1ac6e3dd34f0488e1a0875623d557b6fad09b97a942b38a"}, - {file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cbe91bc84be4e5ef0b1480d15c7b18e29c73bdfa33e07d3725da7d18e1b0aff2"}, - {file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:91502bf27cbd5c83c95cfea291ef387469f2387508645602e1ca0fd8a4ba7548"}, - {file = "coverage-6.3.3-cp38-cp38-win32.whl", hash = "sha256:c488db059848702aff30aa1d90ef87928d4e72e4f00717343800546fdbff0a94"}, - {file = "coverage-6.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6534fcdfb5c503affb6b1130db7b5bfc8a0f77fa34880146f7a5c117987d0"}, - {file = "coverage-6.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cc692c9ee18f0dd3214843779ba6b275ee4bb9b9a5745ba64265bce911aefd1a"}, - {file = "coverage-6.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:462105283de203df8de58a68c1bb4ba2a8a164097c2379f664fa81d6baf94b81"}, - {file = "coverage-6.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc972d829ad5ef4d4c5fcabd2bbe2add84ce8236f64ba1c0c72185da3a273130"}, - {file = "coverage-6.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:06f54765cdbce99901871d50fe9f41d58213f18e98b170a30ca34f47de7dd5e8"}, - {file = "coverage-6.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7835f76a081787f0ca62a53504361b3869840a1620049b56d803a8cb3a9eeea3"}, - {file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6f5fee77ec3384b934797f1873758f796dfb4f167e1296dc00f8b2e023ce6ee9"}, - {file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:baa8be8aba3dd1e976e68677be68a960a633a6d44c325757aefaa4d66175050f"}, - {file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4d06380e777dd6b35ee936f333d55b53dc4a8271036ff884c909cf6e94be8b6c"}, - {file = "coverage-6.3.3-cp39-cp39-win32.whl", hash = "sha256:f8cabc5fd0091976ab7b020f5708335033e422de25e20ddf9416bdce2b7e07d8"}, - {file = "coverage-6.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c9441d57b0963cf8340268ad62fc83de61f1613034b79c2b1053046af0c5284"}, - {file = "coverage-6.3.3-pp36.pp37.pp38-none-any.whl", hash = "sha256:d522f1dc49127eab0bfbba4e90fa068ecff0899bbf61bf4065c790ddd6c177fe"}, - {file = "coverage-6.3.3.tar.gz", hash = "sha256:2781c43bffbbec2b8867376d4d61916f5e9c4cc168232528562a61d1b4b01879"}, -] -flake8 = [ - {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, - {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, -] -h11 = [ - {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, - {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, -] -httpcore = [ - {file = "httpcore-0.14.7-py3-none-any.whl", hash = "sha256:47d772f754359e56dd9d892d9593b6f9870a37aeb8ba51e9a88b09b3d68cfade"}, - {file = "httpcore-0.14.7.tar.gz", hash = "sha256:7503ec1c0f559066e7e39bc4003fd2ce023d01cf51793e3c173b864eb456ead1"}, -] -httpx = [ - {file = "httpx-0.22.0-py3-none-any.whl", hash = "sha256:e35e83d1d2b9b2a609ef367cc4c1e66fd80b750348b20cc9e19d1952fc2ca3f6"}, - {file = "httpx-0.22.0.tar.gz", hash = "sha256:d8e778f76d9bbd46af49e7f062467e3157a5a3d2ae4876a4bbfd8a51ed9c9cb4"}, -] -idna = [ - {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, - {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, -] -importlib-metadata = [ - {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, - {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, -] -iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, -] -isort = [ - {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, - {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -pathspec = [ - {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, - {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, -] -platformdirs = [ - {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, -] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] -pycodestyle = [ - {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, - {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, -] -pyflakes = [ - {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, - {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, -] -pyparsing = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] -pytest = [ - {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, - {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, -] -pytest-asyncio = [ - {file = "pytest-asyncio-0.18.3.tar.gz", hash = "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91"}, - {file = "pytest_asyncio-0.18.3-1-py3-none-any.whl", hash = "sha256:16cf40bdf2b4fb7fc8e4b82bd05ce3fbcd454cbf7b92afc445fe299dabb88213"}, - {file = "pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84"}, -] -rfc3986 = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, -] -sniffio = [ - {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, - {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, -] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -typed-ast = [ - {file = "typed_ast-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ad3b48cf2b487be140072fb86feff36801487d4abb7382bb1929aaac80638ea"}, - {file = "typed_ast-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:542cd732351ba8235f20faa0fc7398946fe1a57f2cdb289e5497e1e7f48cfedb"}, - {file = "typed_ast-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc2c11ae59003d4a26dda637222d9ae924387f96acae9492df663843aefad55"}, - {file = "typed_ast-1.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd5df1313915dbd70eaaa88c19030b441742e8b05e6103c631c83b75e0435ccc"}, - {file = "typed_ast-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:e34f9b9e61333ecb0f7d79c21c28aa5cd63bec15cb7e1310d7d3da6ce886bc9b"}, - {file = "typed_ast-1.5.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f818c5b81966d4728fec14caa338e30a70dfc3da577984d38f97816c4b3071ec"}, - {file = "typed_ast-1.5.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3042bfc9ca118712c9809201f55355479cfcdc17449f9f8db5e744e9625c6805"}, - {file = "typed_ast-1.5.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4fff9fdcce59dc61ec1b317bdb319f8f4e6b69ebbe61193ae0a60c5f9333dc49"}, - {file = "typed_ast-1.5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8e0b8528838ffd426fea8d18bde4c73bcb4167218998cc8b9ee0a0f2bfe678a6"}, - {file = "typed_ast-1.5.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ef1d96ad05a291f5c36895d86d1375c0ee70595b90f6bb5f5fdbee749b146db"}, - {file = "typed_ast-1.5.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed44e81517364cb5ba367e4f68fca01fba42a7a4690d40c07886586ac267d9b9"}, - {file = "typed_ast-1.5.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f60d9de0d087454c91b3999a296d0c4558c1666771e3460621875021bf899af9"}, - {file = "typed_ast-1.5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9e237e74fd321a55c90eee9bc5d44be976979ad38a29bbd734148295c1ce7617"}, - {file = "typed_ast-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ee852185964744987609b40aee1d2eb81502ae63ee8eef614558f96a56c1902d"}, - {file = "typed_ast-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:27e46cdd01d6c3a0dd8f728b6a938a6751f7bd324817501c15fb056307f918c6"}, - {file = "typed_ast-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d64dabc6336ddc10373922a146fa2256043b3b43e61f28961caec2a5207c56d5"}, - {file = "typed_ast-1.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8cdf91b0c466a6c43f36c1964772918a2c04cfa83df8001ff32a89e357f8eb06"}, - {file = "typed_ast-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:9cc9e1457e1feb06b075c8ef8aeb046a28ec351b1958b42c7c31c989c841403a"}, - {file = "typed_ast-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e20d196815eeffb3d76b75223e8ffed124e65ee62097e4e73afb5fec6b993e7a"}, - {file = "typed_ast-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:37e5349d1d5de2f4763d534ccb26809d1c24b180a477659a12c4bde9dd677d74"}, - {file = "typed_ast-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f1a27592fac87daa4e3f16538713d705599b0a27dfe25518b80b6b017f0a6d"}, - {file = "typed_ast-1.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8831479695eadc8b5ffed06fdfb3e424adc37962a75925668deeb503f446c0a3"}, - {file = "typed_ast-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:20d5118e494478ef2d3a2702d964dae830aedd7b4d3b626d003eea526be18718"}, - {file = "typed_ast-1.5.3.tar.gz", hash = "sha256:27f25232e2dd0edfe1f019d6bfaaf11e86e657d9bdb7b0956db95f560cceb2b3"}, -] -typing-extensions = [ - {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, - {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, -] -zipp = [ - {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, - {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, -] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "8365025d6345ca0940ba747f08fdd81aae8bec8f96ad9c1813133eda673cc388" diff --git a/pyproject.toml b/pyproject.toml index bd22289..a3e7826 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,51 +1,101 @@ [tool.poetry] name = "cent" -version = "4.2.0" +version = "5.0.0b1" description = "Python library to communicate with Centrifugo v3 HTTP API" -authors = ["Alexandr Emelin", "Bogdan Evstratenko"] +authors = ["Alexandr Emelin", "Bogdan Evstratenko", "Katant Savelev"] license = "MIT" readme = 'README.md' classifiers = [ - "Development Status :: 5 - Production/Stable", - "Programming Language :: Python", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Environment :: Console", - "Intended Audience :: Developers", - "Intended Audience :: System Administrators", - "License :: OSI Approved :: Apache Software License", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Software Development", - "Topic :: System :: Networking", - "Topic :: Terminals", - "Topic :: Text Processing", - "Topic :: Utilities", - ] - -[tool.poetry.scripts] -cent = "cent.console:run" + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: Apache Software License", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Software Development", + "Topic :: System :: Networking", + "Topic :: Terminals", + "Topic :: Text Processing", + "Topic :: Utilities", +] [tool.poetry.dependencies] -python = "^3.7" -httpx = "^0.22.0" +python = "^3.9" +aiohttp = "^3.9.3" +pydantic = "^2.6.0" + +[tool.poetry.group.dev.dependencies] +pre-commit = "^3.6.0" +ruff = "^0.1.15" +mypy = "^1.8.0" + +[tool.ruff] +line-length = 88 +select = [ + "PL", # pylint + "F", # pyflakes + "E", # pycodestyle errors + "W", # pycodestyle warnings + "C90", # mccabe + "N", # pep8-naming + "YTT", # flake8-2020 + "S", # flake8-bandit + "B", # flake8-bugbear + "A", # flake8-builtins + "C40", # flake8-comprehensions + "T10", # flake8-debugger + "EXE", # flake8-executable + "ICN", # flake8-import-conventions + "G", # flake8-logging-format + "PIE", # flake8-pie + "T20", # flake8-print + "PT", # flake8-pytest-style + "SIM", # flake8-simplify + "TID", # flake8-tidy-imports + "TCH", # flake8-type-checking + "ARG", # flake8-unused-arguments + "PGH", # pygrep-hooks + "RSE", # flake8-raise + "RUF", # ruff +] +ignore = [ + "PLR0913", # too-many-arguments + "PGH003" # use specific rule code when ignore +] + +[tool.ruff.per-file-ignores] +"cent/types/*" = ["E501"] +"cent/methods/*" = ["E501"] + +[tool.mypy] +strict = true +python_version = "3.9" +show_error_codes = true +show_error_context = true +pretty = true +ignore_missing_imports = false +warn_unused_configs = true +disallow_subclassing_any = true +disallow_any_generics = true +disallow_untyped_calls = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +disallow_untyped_decorators = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_return_any = true +follow_imports_for_stubs = true +namespace_packages = true +show_absolute_path = true -[tool.poetry.dev-dependencies] -black = "^22.3.0" -isort = "^5.10.1" -flake8 = "^4.0.1" -pytest-asyncio = "^0.18.3" -pytest = "^7.1.2" -coverage = "^6.3.3" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" - -[tool.isort] -profile = "black" -multi_line_output = 3 - -[tool.pytest.ini_options] -asyncio_mode = "auto" diff --git a/setup.py b/setup.py deleted file mode 100644 index 03e0ea3..0000000 --- a/setup.py +++ /dev/null @@ -1,56 +0,0 @@ -import os -import sys - -from setuptools import setup - -if sys.argv[-1] == "test": - status = os.system("python tests/tests.py") - sys.exit(1 if status > 127 else status) - - -requirements = ["requests"] - - -def long_description(): - return "Python library to communicate with Centrifugo v3 HTTP API" - - -setup( - name="cent", - version="4.1.0", - description="Python library to communicate with Centrifugo v3 HTTP API", - long_description=long_description(), - url="https://github.com/centrifugal/cent", - download_url="https://github.com/centrifugal/cent", - author="Alexandr Emelin", - author_email="frvzmb@gmail.com", - license="MIT", - packages=["cent"], - entry_points={ - "console_scripts": [ - "cent = cent.console:run", - ], - }, - install_requires=requirements, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Programming Language :: Python", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Environment :: Console", - "Intended Audience :: Developers", - "Intended Audience :: System Administrators", - "License :: OSI Approved :: Apache Software License", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Software Development", - "Topic :: System :: Networking", - "Topic :: Terminals", - "Topic :: Text Processing", - "Topic :: Utilities", - ], -) diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 8524ef4..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,14 +0,0 @@ -import pytest - -from cent.async_core import AsyncClient -from cent.core import Client - - -@pytest.fixture -def sync_client(): - return Client(address="http://localhost:8000/api") - - -@pytest.fixture -def async_client(): - return AsyncClient(address="http://localhost:8000/api") diff --git a/tests/test_async.py b/tests/test_async.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_sync.py b/tests/test_sync.py deleted file mode 100644 index 60aa1cc..0000000 --- a/tests/test_sync.py +++ /dev/null @@ -1,78 +0,0 @@ -import pytest - -from cent.core import Client, ClientNotEmpty - - -def test_add(sync_client: Client): - sync_client.add("info", {}) - assert len(sync_client._messages) == 1 - - -def test_reset(sync_client: Client): - sync_client.add("info", {}) - assert len(sync_client._messages) == 1 - sync_client.reset() - assert len(sync_client._messages) == 0 - - -def test_info(sync_client: Client): - res = sync_client.info() - assert len(res["nodes"]) == 1 - - -def test_check_empty(sync_client: Client): - sync_client.add("info", {}) - - with pytest.raises(ClientNotEmpty): - sync_client.info("info", {}) - - -def test_publish(sync_client: Client): - res = sync_client.publish(channel="public", data={"hello": 1}) - assert res == {} - - -def test_broadcast(sync_client: Client): - res = sync_client.broadcast(channels=["public", "public2"], data={"hello": 1}, skip_history=True) - assert res == {"responses": [{"result": {}}, {"result": {}}]} - - res = sync_client.broadcast(channels=["public", "public2"], data={"hello": 1}, skip_history=False) - assert res == {"responses": [{"result": {}}, {"result": {}}]} - - -def test_subscribe(sync_client: Client): - res = sync_client.subscribe(user="1", channel="public") - assert res is None - - -def test_unsubscribe(sync_client: Client): - res = sync_client.unsubscribe(user="1", channel="public") - assert res is None - - -def test_disconnect(sync_client: Client): - res = sync_client.disconnect(user="1") - assert res is None - - -def test_presence(sync_client: Client): - res = sync_client.presence(channel="public") - assert res is None - - -def test_presence_stats(sync_client: Client): - sync_client.presence_stats(channel="public") - - -def test_history(sync_client: Client): - sync_client.history(channel="public") - sync_client.history(channel="public", reverse=True) - - -def test_history_remove(sync_client: Client): - sync_client.history_remove(channel="public") - - -def test_channels(sync_client: Client): - res = sync_client.channels() - assert res == {} From a62886217c6aafa80f9429d0118cbdba2429a9ff Mon Sep 17 00:00:00 2001 From: KatantDev Date: Tue, 30 Jan 2024 17:57:47 +1000 Subject: [PATCH 04/55] feat: sync client --- cent/__init__.py | 20 ++- cent/client/__init__.py | 21 ++- .../{cent_client.py => async_client.py} | 6 +- cent/client/client.py | 79 +++++++++ cent/client/session/__init__.py | 11 +- cent/client/session/aiohttp.py | 9 +- cent/client/session/base.py | 87 ++++------ cent/client/session/base_async.py | 52 ++++++ cent/client/session/base_sync.py | 46 +++++ cent/client/session/requests.py | 56 ++++++ cent/context_controller.py | 8 +- cent/exceptions.py | 6 + cent/methods/base.py | 14 +- cent/types/publish.py | 2 +- example.py | 26 +++ poetry.lock | 163 +++++++++++++++++- pyproject.toml | 5 +- 17 files changed, 518 insertions(+), 93 deletions(-) rename cent/client/{cent_client.py => async_client.py} (94%) create mode 100644 cent/client/client.py create mode 100644 cent/client/session/base_async.py create mode 100644 cent/client/session/base_sync.py create mode 100644 cent/client/session/requests.py create mode 100644 example.py diff --git a/cent/__init__.py b/cent/__init__.py index 66d5eff..1825439 100644 --- a/cent/__init__.py +++ b/cent/__init__.py @@ -1,11 +1,23 @@ -from .client import CentClient, BaseSession, AiohttpSession +from .client import ( + Client, + AsyncClient, + BaseSession, + BaseAsyncSession, + BaseSyncSession, + RequestsSession, + AiohttpSession, +) from .__meta__ import __version__ __all__ = ( "__version__", - "CentClient", - "BaseSession", - "AiohttpSession", "types", "methods", + "Client", + "AsyncClient", + "BaseSession", + "BaseAsyncSession", + "BaseSyncSession", + "RequestsSession", + "AiohttpSession", ) diff --git a/cent/client/__init__.py b/cent/client/__init__.py index 6a5d086..5784179 100644 --- a/cent/client/__init__.py +++ b/cent/client/__init__.py @@ -1,4 +1,19 @@ -from .cent_client import CentClient -from .session import BaseSession, AiohttpSession +from .session import ( + BaseSession, + BaseAsyncSession, + BaseSyncSession, + AiohttpSession, + RequestsSession, +) +from .client import Client +from .async_client import AsyncClient -__all__ = ("CentClient", "BaseSession", "AiohttpSession") +__all__ = ( + "BaseSession", + "BaseAsyncSession", + "BaseSyncSession", + "AiohttpSession", + "RequestsSession", + "Client", + "AsyncClient", +) diff --git a/cent/client/cent_client.py b/cent/client/async_client.py similarity index 94% rename from cent/client/cent_client.py rename to cent/client/async_client.py index 681ced3..21c1015 100644 --- a/cent/client/cent_client.py +++ b/cent/client/async_client.py @@ -1,6 +1,6 @@ from typing import List, Optional, Any, Dict, TypeVar -from cent.client.session import BaseSession, AiohttpSession +from cent.client.session import BaseAsyncSession, AiohttpSession from cent.methods.base import CentMethod from cent.methods.broadcast import BroadcastMethod from cent.methods.publish import PublishMethod @@ -10,12 +10,12 @@ T = TypeVar("T") -class CentClient: +class AsyncClient: def __init__( self, base_url: str, api_key: str, - session: Optional[BaseSession] = None, + session: Optional[BaseAsyncSession] = None, ) -> None: """ :param base_url: Centrifuge base_url diff --git a/cent/client/client.py b/cent/client/client.py new file mode 100644 index 0000000..305df3a --- /dev/null +++ b/cent/client/client.py @@ -0,0 +1,79 @@ +from typing import List, Optional, Any, Dict, TypeVar + +from cent.client.session import BaseSyncSession, RequestsSession +from cent.methods.base import CentMethod +from cent.methods.broadcast import BroadcastMethod +from cent.methods.publish import PublishMethod +from cent.types.broadcast import BroadcastObject +from cent.types.publish import PublishObject + +T = TypeVar("T") + + +class Client: + def __init__( + self, + base_url: str, + api_key: str, + session: Optional[BaseSyncSession] = None, + ) -> None: + """ + :param base_url: Centrifuge base_url + :param api_key: Centrifuge API key + :param session: Custom Session instance + """ + + self._base_url = base_url + self.api_key = api_key + self.session = session or RequestsSession(base_url=base_url) + + def publish( + self, + channel: str, + data: Dict[str, Any], + skip_history: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + b64data: Optional[str] = None, + idempotency_key: Optional[str] = None, + request_timeout: Optional[int] = None, + ) -> PublishObject: + call = PublishMethod( + channel=channel, + data=data, + skip_history=skip_history, + tags=tags, + b64data=b64data, + idempotency_key=idempotency_key, + ) + return self(call, request_timeout=request_timeout) + + def broadcast( + self, + channels: List[str], + data: Dict[str, Any], + skip_history: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + b64data: Optional[str] = None, + idempotency_key: Optional[str] = None, + request_timeout: Optional[int] = None, + ) -> BroadcastObject: + call = BroadcastMethod( + channels=channels, + data=data, + skip_history=skip_history, + tags=tags, + b64data=b64data, + idempotency_key=idempotency_key, + ) + return self(call, request_timeout=request_timeout) + + def __call__( + self, method: CentMethod[T], request_timeout: Optional[int] = None + ) -> T: + """ + Call API method + + :param method: Centrifugo method + :return: Centrifugo response + """ + return self.session(self, method, timeout=request_timeout) diff --git a/cent/client/session/__init__.py b/cent/client/session/__init__.py index 62703e6..a1a1244 100644 --- a/cent/client/session/__init__.py +++ b/cent/client/session/__init__.py @@ -1,4 +1,13 @@ from .base import BaseSession +from .base_async import BaseAsyncSession +from .base_sync import BaseSyncSession from .aiohttp import AiohttpSession +from .requests import RequestsSession -__all__ = ("BaseSession", "AiohttpSession") +__all__ = ( + "BaseSession", + "BaseAsyncSession", + "BaseSyncSession", + "AiohttpSession", + "RequestsSession", +) diff --git a/cent/client/session/aiohttp.py b/cent/client/session/aiohttp.py index 0106837..c368830 100644 --- a/cent/client/session/aiohttp.py +++ b/cent/client/session/aiohttp.py @@ -6,14 +6,14 @@ from aiohttp.http import SERVER_SOFTWARE from cent.__meta__ import __version__ -from cent.client.session.base import BaseSession +from cent.client.session.base_async import BaseAsyncSession from cent.methods.base import CentMethod, CentType if TYPE_CHECKING: - from cent.client.cent_client import CentClient + from cent.client.async_client import AsyncClient -class AiohttpSession(BaseSession): +class AiohttpSession(BaseAsyncSession): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self._session: Optional[ClientSession] = None @@ -39,7 +39,7 @@ async def close(self) -> None: async def make_request( self, - client: "CentClient", + client: "AsyncClient", method: CentMethod[CentType], timeout: Optional[float] = None, ) -> CentType: @@ -58,6 +58,7 @@ async def make_request( response = self.check_response( client=client, method=method, + status_code=resp.status, content=raw_result, ) return cast(CentType, response.result) diff --git a/cent/client/session/base.py b/cent/client/session/base.py index 0fd8692..a44d728 100644 --- a/cent/client/session/base.py +++ b/cent/client/session/base.py @@ -1,22 +1,22 @@ import json -from abc import ABC, abstractmethod -from types import TracebackType -from typing import Final, TYPE_CHECKING, Callable, Any, Optional, cast, Type +from http import HTTPStatus +from typing import Final, TYPE_CHECKING, Callable, Any, Union -from pydantic import ValidationError +from pydantic import ValidationError, TypeAdapter -from cent.exceptions import ClientDecodeError, DetailedAPIError -from cent.methods.base import CentMethod, CentType, Response +from cent.exceptions import ClientDecodeError, DetailedAPIError, InvalidApiKeyError +from cent.methods.base import CentMethod, CentType, Response, Error if TYPE_CHECKING: - from cent.client.cent_client import CentClient + from cent.client.client import Client + from cent.client.async_client import AsyncClient DEFAULT_TIMEOUT: Final[float] = 60.0 _JsonLoads = Callable[..., Any] _JsonDumps = Callable[..., str] -class BaseSession(ABC): +class BaseSession: """Base class for all sessions.""" def __init__( @@ -38,71 +38,42 @@ def __init__( def check_response( self, - client: "CentClient", + client: Union["Client", "AsyncClient"], method: CentMethod[CentType], + status_code: int, content: str, ) -> Response[CentType]: """Validate response.""" + if status_code == HTTPStatus.FORBIDDEN: + raise InvalidApiKeyError + try: json_data = self.json_loads(content) except Exception as err: raise ClientDecodeError from err + if not (HTTPStatus.OK <= status_code <= HTTPStatus.IM_USED): + error = Error.model_validate(json_data) + raise DetailedAPIError( + method=method, + code=error.code, + message=error.message, + ) + try: response_type = Response[method.__returning__] # type: ignore - response = response_type.model_validate( + response = TypeAdapter(response_type).validate_python( json_data, context={"client": client}, ) except ValidationError as err: raise ClientDecodeError from err - if response.error is None: - return response - - raise DetailedAPIError( - method=method, - code=response.error.code, - message=response.error.message, - ) - - @abstractmethod - async def close(self) -> None: # pragma: no cover - """ - Close client session - """ - - @abstractmethod - async def make_request( - self, - client: "CentClient", - method: CentMethod[CentType], - timeout: Optional[float] = None, - ) -> CentType: # pragma: no cover - """ - Make request to centrifuge API. - - :param client: Centrifuge client. - :param method: Centrifuge method. - :param timeout: Request timeout. - """ - ... - - async def __call__( - self, - client: "CentClient", - method: CentMethod[CentType], - timeout: Optional[float] = None, - ) -> CentType: - return cast(CentType, await self.make_request(client, method, timeout)) - - async def __aenter__(self) -> "BaseSession": - return self + if response.error: + raise DetailedAPIError( + method=method, + code=response.error.code, + message=response.error.message, + ) - async def __aexit__( - self, - exc_type: Optional[Type[BaseException]], - exc_value: Optional[BaseException], - traceback: Optional[TracebackType], - ) -> None: - await self.close() + return response diff --git a/cent/client/session/base_async.py b/cent/client/session/base_async.py new file mode 100644 index 0000000..c6c4efa --- /dev/null +++ b/cent/client/session/base_async.py @@ -0,0 +1,52 @@ +from abc import ABC, abstractmethod +from typing import Final, TYPE_CHECKING, Callable, Any, Optional, cast + +from cent.methods.base import CentMethod, CentType +from cent.client.session.base import BaseSession + +if TYPE_CHECKING: + from cent.client.async_client import AsyncClient + +DEFAULT_TIMEOUT: Final[float] = 60.0 +_JsonLoads = Callable[..., Any] +_JsonDumps = Callable[..., str] + + +class BaseAsyncSession(BaseSession, ABC): + """Base class for all sessions.""" + + @abstractmethod + async def close(self) -> None: + """ + Close client session + """ + + @abstractmethod + async def make_request( + self, + client: "AsyncClient", + method: CentMethod[CentType], + timeout: Optional[float] = None, + ) -> CentType: + """ + Make request to centrifuge API. + + :param client: Centrifuge client. + :param method: Centrifuge method. + :param timeout: Request timeout. + """ + ... + + async def __call__( + self, + client: "AsyncClient", + method: CentMethod[CentType], + timeout: Optional[float] = None, + ) -> CentType: + return cast(CentType, await self.make_request(client, method, timeout)) + + async def __aenter__(self) -> "BaseAsyncSession": + return self + + async def __aexit__(self, *kwargs: Any) -> None: + await self.close() diff --git a/cent/client/session/base_sync.py b/cent/client/session/base_sync.py new file mode 100644 index 0000000..6a4b012 --- /dev/null +++ b/cent/client/session/base_sync.py @@ -0,0 +1,46 @@ +from abc import abstractmethod, ABC +from typing import Final, TYPE_CHECKING, Callable, Any, Optional + +from cent.methods.base import CentMethod, CentType +from cent.client.session.base import BaseSession + +if TYPE_CHECKING: + from cent.client.client import Client + +DEFAULT_TIMEOUT: Final[float] = 60.0 +_JsonLoads = Callable[..., Any] +_JsonDumps = Callable[..., str] + + +class BaseSyncSession(BaseSession, ABC): + """Base class for all sessions.""" + + @abstractmethod + def close(self) -> None: + """ + Close client session + """ + + @abstractmethod + def make_request( + self, + client: "Client", + method: CentMethod[CentType], + timeout: Optional[float] = None, + ) -> CentType: + """ + Make request to centrifuge API. + + :param client: Centrifuge client. + :param method: Centrifuge method. + :param timeout: Request timeout. + """ + ... + + def __call__( + self, + client: "Client", + method: CentMethod[CentType], + timeout: Optional[float] = None, + ) -> CentType: + return self.make_request(client, method, timeout) diff --git a/cent/client/session/requests.py b/cent/client/session/requests.py new file mode 100644 index 0000000..718414d --- /dev/null +++ b/cent/client/session/requests.py @@ -0,0 +1,56 @@ +from typing import Optional, TYPE_CHECKING, cast, Any + +from aiohttp.hdrs import USER_AGENT, CONTENT_TYPE +from aiohttp.http import SERVER_SOFTWARE +from requests import Session + +from cent.__meta__ import __version__ +from cent.methods.base import CentMethod, CentType +from cent.client.session.base_sync import BaseSyncSession + +if TYPE_CHECKING: + from cent.client.client import Client + + +class RequestsSession(BaseSyncSession): + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self._session = Session() + self._session.headers.update( + { + USER_AGENT: f"{SERVER_SOFTWARE} pycent/{__version__}", + CONTENT_TYPE: "application/json", + "X-Centrifugo-Error-Mode": "transport", + } + ) + + def close(self) -> None: + if self._session is not None: + self._session.close() + + def make_request( + self, + client: "Client", + method: CentMethod[CentType], + timeout: Optional[float] = None, + ) -> CentType: + self._session.headers["X-API-Key"] = client.api_key + json_data = method.model_dump(exclude_none=True) + + url = f"{self._base_url}/{method.__api_method__}" + + raw_result = self._session.post( + url=url, + json=json_data, + timeout=timeout or self._timeout, + ) + response = self.check_response( + client=client, + method=method, + status_code=raw_result.status_code, + content=raw_result.text, + ) + return cast(CentType, response.result) + + def __del__(self) -> None: + self.close() diff --git a/cent/context_controller.py b/cent/context_controller.py index ecc6a98..62e8ad5 100644 --- a/cent/context_controller.py +++ b/cent/context_controller.py @@ -3,16 +3,16 @@ from pydantic import BaseModel, PrivateAttr if TYPE_CHECKING: - from cent.client import CentClient + from cent.client import Client class ClientContextController(BaseModel): - _client: Optional["CentClient"] = PrivateAttr() + _client: Optional["Client"] = PrivateAttr() def model_post_init(self, __context: Any) -> None: self._client = __context.get("client") if __context else None - def as_(self, client: Optional["CentClient"]) -> "ClientContextController": + def as_(self, client: Optional["Client"]) -> "ClientContextController": """ Bind an object to a client instance. @@ -23,7 +23,7 @@ def as_(self, client: Optional["CentClient"]) -> "ClientContextController": return self @property - def client(self) -> Optional["CentClient"]: + def client(self) -> Optional["Client"]: """ Get client instance. diff --git a/cent/exceptions.py b/cent/exceptions.py index 972ca56..532e7e7 100644 --- a/cent/exceptions.py +++ b/cent/exceptions.py @@ -14,6 +14,12 @@ class ClientDecodeError(CentError): """ +class InvalidApiKeyError(CentError): + """ + InvalidApiKeyError raised when Centrifugo returns 401 status code. + """ + + class DetailedAPIError(CentError): """ DetailedAPIError raised when response from Centrifugo contains any error diff --git a/cent/methods/base.py b/cent/methods/base.py index c843ea1..0a11d85 100644 --- a/cent/methods/base.py +++ b/cent/methods/base.py @@ -1,13 +1,10 @@ from abc import ABC, abstractmethod -from typing import TypeVar, Any, Generic, TYPE_CHECKING, ClassVar, Generator, Optional +from typing import TypeVar, Any, Generic, TYPE_CHECKING, ClassVar, Optional from pydantic import BaseModel, ConfigDict from cent.context_controller import ClientContextController -if TYPE_CHECKING: - from cent.client.cent_client import CentClient - CentType = TypeVar("CentType", bound=Any) @@ -42,12 +39,3 @@ def __returning__(self) -> type: @abstractmethod def __api_method__(self) -> str: pass - - async def emit(self, client: "CentClient") -> CentType: - return await client(self) - - def __await__(self) -> Generator[Any, None, CentType]: - client = self._client - if not client: - raise RuntimeError("CentMethod is not bound to a client") - return self.emit(client).__await__() diff --git a/cent/types/publish.py b/cent/types/publish.py index e9a0c20..4cb1fb5 100644 --- a/cent/types/publish.py +++ b/cent/types/publish.py @@ -6,7 +6,7 @@ class PublishObject(CentObject): """Publish result.""" - offset: Optional[str] = None + offset: Optional[int] = None """Offset of publication in history stream.""" epoch: Optional[str] = None """Epoch of current stream.""" diff --git a/example.py b/example.py new file mode 100644 index 0000000..111e311 --- /dev/null +++ b/example.py @@ -0,0 +1,26 @@ +import asyncio + +from cent import AsyncClient, Client + +BASE_URL = "http://localhost:8000/api" +API_KEY = "api_key" + +async_client = AsyncClient(BASE_URL, API_KEY) +sync_client = Client(BASE_URL, API_KEY) + + +async def main() -> None: + response = await async_client.publish( + channel="example:123", + data={"message": "Hello world!"}, + ) + print(response) + response = sync_client.publish( + channel="example:123", + data={"message": "Hello world!"}, + ) + print(response) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/poetry.lock b/poetry.lock index ff13369..7a6cff3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -151,6 +151,17 @@ tests = ["attrs[tests-no-zope]", "zope-interface"] tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +[[package]] +name = "certifi" +version = "2023.11.17" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, + {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, +] + [[package]] name = "cfgv" version = "3.4.0" @@ -162,6 +173,105 @@ files = [ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + [[package]] name = "distlib" version = "0.3.8" @@ -657,6 +767,27 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + [[package]] name = "ruff" version = "0.1.15" @@ -710,6 +841,20 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "types-requests" +version = "2.31.0.20240125" +description = "Typing stubs for requests" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-requests-2.31.0.20240125.tar.gz", hash = "sha256:03a28ce1d7cd54199148e043b2079cdded22d6795d19a2c2a6791a4b2b5e2eb5"}, + {file = "types_requests-2.31.0.20240125-py3-none-any.whl", hash = "sha256:9592a9a4cb92d6d75d9b491a41477272b710e021011a2a3061157e2fb1f1a5d1"}, +] + +[package.dependencies] +urllib3 = ">=2" + [[package]] name = "typing-extensions" version = "4.9.0" @@ -721,6 +866,22 @@ files = [ {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] +[[package]] +name = "urllib3" +version = "2.1.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, + {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "virtualenv" version = "20.25.0" @@ -847,4 +1008,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "8365025d6345ca0940ba747f08fdd81aae8bec8f96ad9c1813133eda673cc388" +content-hash = "8eb645dc6c0e421d175dd92128ce0e6305de77cb637f68d30977a9a702279795" diff --git a/pyproject.toml b/pyproject.toml index a3e7826..ef09496 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,8 @@ classifiers = [ python = "^3.9" aiohttp = "^3.9.3" pydantic = "^2.6.0" +requests = "^2.31.0" +types-requests = "^2.31.0.20240125" [tool.poetry.group.dev.dependencies] pre-commit = "^3.6.0" @@ -65,7 +67,8 @@ select = [ ] ignore = [ "PLR0913", # too-many-arguments - "PGH003" # use specific rule code when ignore + "PGH003", # use specific rule code when ignore + "T201" ] [tool.ruff.per-file-ignores] From be5556d39ee60089c2882dfcb47ce5e11fbe051f Mon Sep 17 00:00:00 2001 From: KatantDev Date: Tue, 30 Jan 2024 18:56:06 +1000 Subject: [PATCH 05/55] feat: subscribe method --- cent/client/async_client.py | 46 +++++++++++++++++++++++++++++++---- cent/client/client.py | 46 +++++++++++++++++++++++++++++++---- cent/context_controller.py | 2 +- cent/methods/__init__.py | 4 ++- cent/methods/subscribe.py | 36 +++++++++++++++++++++++++++ cent/types/__init__.py | 11 ++++++++- cent/types/override.py | 18 ++++++++++++++ cent/types/stream_position.py | 10 ++++++++ cent/types/subscribe.py | 5 ++++ 9 files changed, 165 insertions(+), 13 deletions(-) create mode 100644 cent/methods/subscribe.py create mode 100644 cent/types/override.py create mode 100644 cent/types/stream_position.py create mode 100644 cent/types/subscribe.py diff --git a/cent/client/async_client.py b/cent/client/async_client.py index 21c1015..816a375 100644 --- a/cent/client/async_client.py +++ b/cent/client/async_client.py @@ -1,11 +1,19 @@ from typing import List, Optional, Any, Dict, TypeVar from cent.client.session import BaseAsyncSession, AiohttpSession -from cent.methods.base import CentMethod -from cent.methods.broadcast import BroadcastMethod -from cent.methods.publish import PublishMethod -from cent.types.broadcast import BroadcastObject -from cent.types.publish import PublishObject +from cent.methods import ( + CentMethod, + BroadcastMethod, + PublishMethod, + SubscribeMethod, +) +from cent.types import ( + SubscribeObject, + StreamPosition, + PublishObject, + BroadcastObject, + Override, +) T = TypeVar("T") @@ -67,6 +75,34 @@ async def broadcast( ) return await self(call, request_timeout=request_timeout) + async def subscribe( + self, + user: str, + channel: str, + info: Optional[Dict[Any, Any]] = None, + b64info: Optional[str] = None, + client: Optional[str] = None, + session: Optional[str] = None, + data: Optional[Dict[Any, Any]] = None, + b64data: Optional[str] = None, + recover_since: Optional[StreamPosition] = None, + override: Optional[Override] = None, + request_timeout: Optional[int] = None, + ) -> SubscribeObject: + call = SubscribeMethod( + user=user, + channel=channel, + info=info, + b64info=b64info, + client=client, + session=session, + data=data, + b64data=b64data, + recover_since=recover_since, + override=override, + ) + return await self(call, request_timeout=request_timeout) + async def __call__( self, method: CentMethod[T], request_timeout: Optional[int] = None ) -> T: diff --git a/cent/client/client.py b/cent/client/client.py index 305df3a..534f69d 100644 --- a/cent/client/client.py +++ b/cent/client/client.py @@ -1,11 +1,19 @@ from typing import List, Optional, Any, Dict, TypeVar from cent.client.session import BaseSyncSession, RequestsSession -from cent.methods.base import CentMethod -from cent.methods.broadcast import BroadcastMethod -from cent.methods.publish import PublishMethod -from cent.types.broadcast import BroadcastObject -from cent.types.publish import PublishObject +from cent.methods import ( + CentMethod, + BroadcastMethod, + PublishMethod, + SubscribeMethod, +) +from cent.types import ( + StreamPosition, + PublishObject, + BroadcastObject, + Override, + SubscribeObject, +) T = TypeVar("T") @@ -67,6 +75,34 @@ def broadcast( ) return self(call, request_timeout=request_timeout) + def subscribe( + self, + user: str, + channel: str, + info: Optional[Dict[Any, Any]] = None, + b64info: Optional[str] = None, + client: Optional[str] = None, + session: Optional[str] = None, + data: Optional[Dict[Any, Any]] = None, + b64data: Optional[str] = None, + recover_since: Optional[StreamPosition] = None, + override: Optional[Override] = None, + request_timeout: Optional[int] = None, + ) -> SubscribeObject: + call = SubscribeMethod( + user=user, + channel=channel, + info=info, + b64info=b64info, + client=client, + session=session, + data=data, + b64data=b64data, + recover_since=recover_since, + override=override, + ) + return self(call, request_timeout=request_timeout) + def __call__( self, method: CentMethod[T], request_timeout: Optional[int] = None ) -> T: diff --git a/cent/context_controller.py b/cent/context_controller.py index 62e8ad5..a58d22a 100644 --- a/cent/context_controller.py +++ b/cent/context_controller.py @@ -23,7 +23,7 @@ def as_(self, client: Optional["Client"]) -> "ClientContextController": return self @property - def client(self) -> Optional["Client"]: + def client_instance(self) -> Optional["Client"]: """ Get client instance. diff --git a/cent/methods/__init__.py b/cent/methods/__init__.py index ef3c2ff..b8779ea 100644 --- a/cent/methods/__init__.py +++ b/cent/methods/__init__.py @@ -1,4 +1,6 @@ +from .base import CentMethod from .broadcast import BroadcastMethod from .publish import PublishMethod +from .subscribe import SubscribeMethod -__all__ = ("BroadcastMethod", "PublishMethod") +__all__ = ("CentMethod", "BroadcastMethod", "PublishMethod", "SubscribeMethod") diff --git a/cent/methods/subscribe.py b/cent/methods/subscribe.py new file mode 100644 index 0000000..9563436 --- /dev/null +++ b/cent/methods/subscribe.py @@ -0,0 +1,36 @@ +from typing import Optional, Dict, Any + +from cent.methods.base import CentMethod +from cent.types import ( + StreamPosition, + SubscribeObject, + Override, +) + + +class SubscribeMethod(CentMethod[SubscribeObject]): + """Subscribe request.""" + + __returning__ = SubscribeObject + __api_method__ = "subscribe" + + user: str + """User ID to subscribe.""" + channel: str + """Name of channel to subscribe user to.""" + info: Optional[Dict[Any, Any]] = None + """Attach custom data to subscription (will be used in presence and join/leave messages).""" + b64info: Optional[str] = None + """info in base64 for binary mode (will be decoded by Centrifugo).""" + client: Optional[str] = None + """Specific client ID to subscribe (user still required to be set, will ignore other user connections with different client IDs).""" + session: Optional[str] = None + """Specific client session to subscribe (user still required to be set).""" + data: Optional[Dict[Any, Any]] = None + """Custom subscription data (will be sent to client in Subscribe push).""" + b64data: Optional[str] = None + """Same as data but in base64 format (will be decoded by Centrifugo).""" + recover_since: Optional[StreamPosition] = None + """Stream position to recover from.""" + override: Optional[Override] = None + """Allows dynamically override some channel options defined in Centrifugo configuration (see below available fields).""" diff --git a/cent/types/__init__.py b/cent/types/__init__.py index 6ddf73e..8aaf004 100644 --- a/cent/types/__init__.py +++ b/cent/types/__init__.py @@ -1,4 +1,13 @@ from .broadcast import BroadcastObject from .publish import PublishObject +from .override import Override +from .stream_position import StreamPosition +from .subscribe import SubscribeObject -__all__ = ("BroadcastObject", "PublishObject") +__all__ = ( + "SubscribeObject", + "BroadcastObject", + "PublishObject", + "Override", + "StreamPosition", +) diff --git a/cent/types/override.py b/cent/types/override.py new file mode 100644 index 0000000..e636f2c --- /dev/null +++ b/cent/types/override.py @@ -0,0 +1,18 @@ +from typing import Optional + +from cent.types.base import CentObject + + +class Override(CentObject): + """Override object.""" + + presence: Optional[bool] = None + """Override presence.""" + join_leave: Optional[bool] = None + """Override join_leave.""" + force_push_join_leave: Optional[bool] = None + """Override force_push_join_leave.""" + force_positioning: Optional[bool] = None + """Override force_positioning.""" + force_recovery: Optional[bool] = None + """Override force_recovery.""" diff --git a/cent/types/stream_position.py b/cent/types/stream_position.py new file mode 100644 index 0000000..1f8bae0 --- /dev/null +++ b/cent/types/stream_position.py @@ -0,0 +1,10 @@ +from cent.types.base import CentObject + + +class StreamPosition(CentObject): + """Stream position.""" + + offset: int + """Offset of publication in history stream.""" + epoch: str + """Epoch of current stream.""" diff --git a/cent/types/subscribe.py b/cent/types/subscribe.py new file mode 100644 index 0000000..564ee6f --- /dev/null +++ b/cent/types/subscribe.py @@ -0,0 +1,5 @@ +from cent.types.base import CentObject + + +class SubscribeObject(CentObject): + """Subscribe result.""" From fcfebb529784f7f54599d07a541a9db8de9a4a73 Mon Sep 17 00:00:00 2001 From: KatantDev Date: Wed, 31 Jan 2024 06:31:21 +1000 Subject: [PATCH 06/55] feat: resolve review changes --- .editorconfig | 4 +- README.md | 19 ++++----- cent/client/async_client.py | 34 ++++++++--------- cent/client/client.py | 34 ++++++++--------- cent/client/session/aiohttp.py | 20 ++++++---- cent/client/session/requests.py | 17 ++++++--- cent/exceptions.py | 14 +++++++ cent/methods/broadcast.py | 10 ++--- cent/methods/publish.py | 8 ++-- cent/methods/subscribe.py | 16 ++++---- cent/types/__init__.py | 24 ++++++++---- cent/types/base.py | 2 +- cent/types/bool_value.py | 8 ++++ cent/types/broadcast.py | 8 ++-- cent/types/channel_options_override.py | 19 +++++++++ cent/types/override.py | 18 --------- cent/types/publish.py | 4 +- cent/types/stream_position.py | 4 +- cent/types/subscribe.py | 4 +- poetry.lock | 53 ++++++++++++++++++++++++-- pyproject.toml | 3 +- 21 files changed, 205 insertions(+), 118 deletions(-) create mode 100644 cent/types/bool_value.py create mode 100644 cent/types/channel_options_override.py delete mode 100644 cent/types/override.py diff --git a/.editorconfig b/.editorconfig index 376731a..f2e6c52 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,8 +3,8 @@ root = true [*] tab_width = 4 end_of_line = lf -max_line_length = 88 -ij_visual_guides = 88 +max_line_length = 99 +ij_visual_guides = 99 insert_final_newline = true trim_trailing_whitespace = true diff --git a/README.md b/README.md index f9a613c..c7b9a62 100644 --- a/README.md +++ b/README.md @@ -21,25 +21,26 @@ If you need to work with Centrifugo v2 then use Cent v3 First see [available API methods in documentation](https://centrifugal.dev/docs/server/server_api#api-methods). -This library contains `CentClient` class to send messages to Centrifugo from your -python-powered backend: +This library contains `Client` and `AsyncClient` class to send messages to +Centrifugo from your python-powered backend: ```python import asyncio -from cent import CentClient +from cent import AsyncClient, Client url = "http://localhost:8000/api" api_key = "XXX" -# Initialize client -client = CentClient(url, api_key=api_key) +# Initialize a client (you can use sync or async version) +async_client = AsyncClient(url, api_key=api_key) +sync_client = Client(url, api_key=api_key) + +response = sync_client.publish("example:channel", {"input": "Hello world!"}) +print(response) async def main(): - response = await client.publish( - "example:2", - {"input": "Hello world!"}, - ) + response = await async_client.publish("example:channel", {"input": "Hello world!"}) print(response) diff --git a/cent/client/async_client.py b/cent/client/async_client.py index 816a375..03669e1 100644 --- a/cent/client/async_client.py +++ b/cent/client/async_client.py @@ -8,11 +8,11 @@ SubscribeMethod, ) from cent.types import ( - SubscribeObject, + SubscribeResult, StreamPosition, - PublishObject, - BroadcastObject, - Override, + PublishResult, + BroadcastResult, + ChannelOptionsOverride, ) T = TypeVar("T") @@ -38,13 +38,13 @@ def __init__( async def publish( self, channel: str, - data: Dict[str, Any], + data: Any, skip_history: Optional[bool] = None, tags: Optional[Dict[str, str]] = None, b64data: Optional[str] = None, idempotency_key: Optional[str] = None, - request_timeout: Optional[int] = None, - ) -> PublishObject: + request_timeout: Optional[float] = None, + ) -> PublishResult: call = PublishMethod( channel=channel, data=data, @@ -58,13 +58,13 @@ async def publish( async def broadcast( self, channels: List[str], - data: Dict[str, Any], + data: Any, skip_history: Optional[bool] = None, tags: Optional[Dict[str, str]] = None, b64data: Optional[str] = None, idempotency_key: Optional[str] = None, - request_timeout: Optional[int] = None, - ) -> BroadcastObject: + request_timeout: Optional[float] = None, + ) -> BroadcastResult: call = BroadcastMethod( channels=channels, data=data, @@ -79,16 +79,16 @@ async def subscribe( self, user: str, channel: str, - info: Optional[Dict[Any, Any]] = None, + info: Optional[Any] = None, b64info: Optional[str] = None, client: Optional[str] = None, session: Optional[str] = None, - data: Optional[Dict[Any, Any]] = None, + data: Optional[Any] = None, b64data: Optional[str] = None, recover_since: Optional[StreamPosition] = None, - override: Optional[Override] = None, - request_timeout: Optional[int] = None, - ) -> SubscribeObject: + override: Optional[ChannelOptionsOverride] = None, + request_timeout: Optional[float] = None, + ) -> SubscribeResult: call = SubscribeMethod( user=user, channel=channel, @@ -103,9 +103,7 @@ async def subscribe( ) return await self(call, request_timeout=request_timeout) - async def __call__( - self, method: CentMethod[T], request_timeout: Optional[int] = None - ) -> T: + async def __call__(self, method: CentMethod[T], request_timeout: Optional[float] = None) -> T: """ Call API method diff --git a/cent/client/client.py b/cent/client/client.py index 534f69d..ccd9139 100644 --- a/cent/client/client.py +++ b/cent/client/client.py @@ -9,10 +9,10 @@ ) from cent.types import ( StreamPosition, - PublishObject, - BroadcastObject, - Override, - SubscribeObject, + PublishResult, + BroadcastResult, + ChannelOptionsOverride, + SubscribeResult, ) T = TypeVar("T") @@ -38,13 +38,13 @@ def __init__( def publish( self, channel: str, - data: Dict[str, Any], + data: Any, skip_history: Optional[bool] = None, tags: Optional[Dict[str, str]] = None, b64data: Optional[str] = None, idempotency_key: Optional[str] = None, - request_timeout: Optional[int] = None, - ) -> PublishObject: + request_timeout: Optional[float] = None, + ) -> PublishResult: call = PublishMethod( channel=channel, data=data, @@ -58,13 +58,13 @@ def publish( def broadcast( self, channels: List[str], - data: Dict[str, Any], + data: Any, skip_history: Optional[bool] = None, tags: Optional[Dict[str, str]] = None, b64data: Optional[str] = None, idempotency_key: Optional[str] = None, - request_timeout: Optional[int] = None, - ) -> BroadcastObject: + request_timeout: Optional[float] = None, + ) -> BroadcastResult: call = BroadcastMethod( channels=channels, data=data, @@ -79,16 +79,16 @@ def subscribe( self, user: str, channel: str, - info: Optional[Dict[Any, Any]] = None, + info: Optional[Any] = None, b64info: Optional[str] = None, client: Optional[str] = None, session: Optional[str] = None, - data: Optional[Dict[Any, Any]] = None, + data: Optional[Any] = None, b64data: Optional[str] = None, recover_since: Optional[StreamPosition] = None, - override: Optional[Override] = None, - request_timeout: Optional[int] = None, - ) -> SubscribeObject: + override: Optional[ChannelOptionsOverride] = None, + request_timeout: Optional[float] = None, + ) -> SubscribeResult: call = SubscribeMethod( user=user, channel=channel, @@ -103,9 +103,7 @@ def subscribe( ) return self(call, request_timeout=request_timeout) - def __call__( - self, method: CentMethod[T], request_timeout: Optional[int] = None - ) -> T: + def __call__(self, method: CentMethod[T], request_timeout: Optional[float] = None) -> T: """ Call API method diff --git a/cent/client/session/aiohttp.py b/cent/client/session/aiohttp.py index c368830..516f3f8 100644 --- a/cent/client/session/aiohttp.py +++ b/cent/client/session/aiohttp.py @@ -1,13 +1,14 @@ import asyncio from typing import Optional, TYPE_CHECKING, cast, Any -from aiohttp import ClientSession +from aiohttp import ClientSession, ClientError from aiohttp.hdrs import USER_AGENT, CONTENT_TYPE from aiohttp.http import SERVER_SOFTWARE from cent.__meta__ import __version__ from cent.client.session.base_async import BaseAsyncSession from cent.methods.base import CentMethod, CentType +from cent.exceptions import CentNetworkError if TYPE_CHECKING: from cent.client.async_client import AsyncClient @@ -49,12 +50,17 @@ async def make_request( url = f"{self._base_url}/{method.__api_method__}" - async with session.post( - url=url, - json=json_data, - timeout=timeout or self._timeout, - ) as resp: - raw_result = await resp.text() + try: + async with session.post( + url=url, + json=json_data, + timeout=timeout or self._timeout, + ) as resp: + raw_result = await resp.text() + except asyncio.TimeoutError: + raise CentNetworkError(method=method, message="Request timeout") from None + except ClientError as e: + raise CentNetworkError(method=method, message=f"{type(e).__name__}: {e}") from None response = self.check_response( client=client, method=method, diff --git a/cent/client/session/requests.py b/cent/client/session/requests.py index 718414d..6cd3083 100644 --- a/cent/client/session/requests.py +++ b/cent/client/session/requests.py @@ -1,5 +1,6 @@ from typing import Optional, TYPE_CHECKING, cast, Any +import requests from aiohttp.hdrs import USER_AGENT, CONTENT_TYPE from aiohttp.http import SERVER_SOFTWARE from requests import Session @@ -7,6 +8,7 @@ from cent.__meta__ import __version__ from cent.methods.base import CentMethod, CentType from cent.client.session.base_sync import BaseSyncSession +from cent.exceptions import CentNetworkError if TYPE_CHECKING: from cent.client.client import Client @@ -39,11 +41,16 @@ def make_request( url = f"{self._base_url}/{method.__api_method__}" - raw_result = self._session.post( - url=url, - json=json_data, - timeout=timeout or self._timeout, - ) + try: + raw_result = self._session.post( + url=url, + json=json_data, + timeout=timeout or self._timeout, + ) + except requests.exceptions.Timeout: + raise CentNetworkError(method=method, message="Request timeout") from None + except requests.exceptions.ConnectionError as e: + raise CentNetworkError(method=method, message=f"{type(e).__name__}: {e}") from None response = self.check_response( client=client, method=method, diff --git a/cent/exceptions.py b/cent/exceptions.py index 532e7e7..b71c80f 100644 --- a/cent/exceptions.py +++ b/cent/exceptions.py @@ -7,6 +7,20 @@ class CentError(Exception): """ +class CentNetworkError(CentError): + """CentNetworkError raised when Centrifugo is not available.""" + + def __init__(self, method: CentMethod[CentType], message: str) -> None: + self.method = method + self.message = message + + def __str__(self) -> str: + return f"HTTP error - {self.message}" + + def __repr__(self) -> str: + return f"{type(self).__name__}('{self}')" + + class ClientDecodeError(CentError): """ ClientDecodeError raised when response from Centrifugo can't be decoded diff --git a/cent/methods/broadcast.py b/cent/methods/broadcast.py index 1ede13f..21dbcbe 100644 --- a/cent/methods/broadcast.py +++ b/cent/methods/broadcast.py @@ -1,18 +1,18 @@ -from typing import Any, Dict, Optional, List +from typing import Dict, Optional, List, Any from cent.methods.base import CentMethod -from cent.types.broadcast import BroadcastObject +from cent.types.broadcast import BroadcastResult -class BroadcastMethod(CentMethod[BroadcastObject]): +class BroadcastMethod(CentMethod[BroadcastResult]): """Broadcast request.""" - __returning__ = BroadcastObject + __returning__ = BroadcastResult __api_method__ = "broadcast" channels: List[str] """List of channels to publish data to.""" - data: Dict[Any, Any] + data: Any """Custom JSON data to publish into a channel.""" skip_history: Optional[bool] = None """Skip adding publications to channels' history for this request.""" diff --git a/cent/methods/publish.py b/cent/methods/publish.py index 1c2f07b..07b785e 100644 --- a/cent/methods/publish.py +++ b/cent/methods/publish.py @@ -1,18 +1,18 @@ from typing import Any, Dict, Optional from cent.methods.base import CentMethod -from cent.types.publish import PublishObject +from cent.types.publish import PublishResult -class PublishMethod(CentMethod[PublishObject]): +class PublishMethod(CentMethod[PublishResult]): """Publish request.""" - __returning__ = PublishObject + __returning__ = PublishResult __api_method__ = "publish" channel: str """Name of channel to publish.""" - data: Dict[Any, Any] + data: Any """Custom JSON data to publish into a channel.""" skip_history: Optional[bool] = None """Skip adding publication to history for this request.""" diff --git a/cent/methods/subscribe.py b/cent/methods/subscribe.py index 9563436..c71546e 100644 --- a/cent/methods/subscribe.py +++ b/cent/methods/subscribe.py @@ -1,24 +1,24 @@ -from typing import Optional, Dict, Any +from typing import Optional, Any from cent.methods.base import CentMethod from cent.types import ( StreamPosition, - SubscribeObject, - Override, + SubscribeResult, + ChannelOptionsOverride, ) -class SubscribeMethod(CentMethod[SubscribeObject]): +class SubscribeMethod(CentMethod[SubscribeResult]): """Subscribe request.""" - __returning__ = SubscribeObject + __returning__ = SubscribeResult __api_method__ = "subscribe" user: str """User ID to subscribe.""" channel: str """Name of channel to subscribe user to.""" - info: Optional[Dict[Any, Any]] = None + info: Optional[Any] = None """Attach custom data to subscription (will be used in presence and join/leave messages).""" b64info: Optional[str] = None """info in base64 for binary mode (will be decoded by Centrifugo).""" @@ -26,11 +26,11 @@ class SubscribeMethod(CentMethod[SubscribeObject]): """Specific client ID to subscribe (user still required to be set, will ignore other user connections with different client IDs).""" session: Optional[str] = None """Specific client session to subscribe (user still required to be set).""" - data: Optional[Dict[Any, Any]] = None + data: Optional[Any] = None """Custom subscription data (will be sent to client in Subscribe push).""" b64data: Optional[str] = None """Same as data but in base64 format (will be decoded by Centrifugo).""" recover_since: Optional[StreamPosition] = None """Stream position to recover from.""" - override: Optional[Override] = None + override: Optional[ChannelOptionsOverride] = None """Allows dynamically override some channel options defined in Centrifugo configuration (see below available fields).""" diff --git a/cent/types/__init__.py b/cent/types/__init__.py index 8aaf004..603d54b 100644 --- a/cent/types/__init__.py +++ b/cent/types/__init__.py @@ -1,13 +1,21 @@ -from .broadcast import BroadcastObject -from .publish import PublishObject -from .override import Override +import contextlib +import asyncio as _asyncio + +from .broadcast import BroadcastResult +from .publish import PublishResult +from .channel_options_override import ChannelOptionsOverride from .stream_position import StreamPosition -from .subscribe import SubscribeObject +from .subscribe import SubscribeResult + +with contextlib.suppress(ImportError): + import uvloop as _uvloop + + _asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy()) __all__ = ( - "SubscribeObject", - "BroadcastObject", - "PublishObject", - "Override", + "SubscribeResult", + "BroadcastResult", + "PublishResult", + "ChannelOptionsOverride", "StreamPosition", ) diff --git a/cent/types/base.py b/cent/types/base.py index cbfbf46..4caad0d 100644 --- a/cent/types/base.py +++ b/cent/types/base.py @@ -3,7 +3,7 @@ from cent.context_controller import ClientContextController -class CentObject(ClientContextController, BaseModel): +class CentResult(ClientContextController, BaseModel): model_config = ConfigDict( use_enum_values=True, extra="allow", diff --git a/cent/types/bool_value.py b/cent/types/bool_value.py new file mode 100644 index 0000000..b575245 --- /dev/null +++ b/cent/types/bool_value.py @@ -0,0 +1,8 @@ +from cent.types.base import CentResult + + +class BoolValue(CentResult): + """Bool value.""" + + value: bool + """Bool value.""" diff --git a/cent/types/broadcast.py b/cent/types/broadcast.py index 9069685..9801186 100644 --- a/cent/types/broadcast.py +++ b/cent/types/broadcast.py @@ -3,11 +3,11 @@ from pydantic import Field from cent.methods.base import Response -from cent.types.base import CentObject -from cent.types.publish import PublishObject +from cent.types.base import CentResult +from cent.types.publish import PublishResult -class BroadcastObject(CentObject): +class BroadcastResult(CentResult): """Publish result.""" - responses: List[Response[PublishObject]] = Field(default_factory=list) + responses: List[Response[PublishResult]] = Field(default_factory=list) diff --git a/cent/types/channel_options_override.py b/cent/types/channel_options_override.py new file mode 100644 index 0000000..bea8fdc --- /dev/null +++ b/cent/types/channel_options_override.py @@ -0,0 +1,19 @@ +from typing import Optional + +from cent.types.base import CentResult +from cent.types.bool_value import BoolValue + + +class ChannelOptionsOverride(CentResult): + """Override object.""" + + presence: Optional[BoolValue] = None + """Override presence.""" + join_leave: Optional[BoolValue] = None + """Override join_leave.""" + force_push_join_leave: Optional[BoolValue] = None + """Override force_push_join_leave.""" + force_positioning: Optional[BoolValue] = None + """Override force_positioning.""" + force_recovery: Optional[BoolValue] = None + """Override force_recovery.""" diff --git a/cent/types/override.py b/cent/types/override.py deleted file mode 100644 index e636f2c..0000000 --- a/cent/types/override.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Optional - -from cent.types.base import CentObject - - -class Override(CentObject): - """Override object.""" - - presence: Optional[bool] = None - """Override presence.""" - join_leave: Optional[bool] = None - """Override join_leave.""" - force_push_join_leave: Optional[bool] = None - """Override force_push_join_leave.""" - force_positioning: Optional[bool] = None - """Override force_positioning.""" - force_recovery: Optional[bool] = None - """Override force_recovery.""" diff --git a/cent/types/publish.py b/cent/types/publish.py index 4cb1fb5..5baf717 100644 --- a/cent/types/publish.py +++ b/cent/types/publish.py @@ -1,9 +1,9 @@ from typing import Optional -from cent.types.base import CentObject +from cent.types.base import CentResult -class PublishObject(CentObject): +class PublishResult(CentResult): """Publish result.""" offset: Optional[int] = None diff --git a/cent/types/stream_position.py b/cent/types/stream_position.py index 1f8bae0..3aa1921 100644 --- a/cent/types/stream_position.py +++ b/cent/types/stream_position.py @@ -1,7 +1,7 @@ -from cent.types.base import CentObject +from cent.types.base import CentResult -class StreamPosition(CentObject): +class StreamPosition(CentResult): """Stream position.""" offset: int diff --git a/cent/types/subscribe.py b/cent/types/subscribe.py index 564ee6f..02e9eda 100644 --- a/cent/types/subscribe.py +++ b/cent/types/subscribe.py @@ -1,5 +1,5 @@ -from cent.types.base import CentObject +from cent.types.base import CentResult -class SubscribeObject(CentObject): +class SubscribeResult(CentResult): """Subscribe result.""" diff --git a/poetry.lock b/poetry.lock index 7a6cff3..4e3f651 100644 --- a/poetry.lock +++ b/poetry.lock @@ -868,20 +868,65 @@ files = [ [[package]] name = "urllib3" -version = "2.1.0" +version = "2.2.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, - {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, + {file = "urllib3-2.2.0-py3-none-any.whl", hash = "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"}, + {file = "urllib3-2.2.0.tar.gz", hash = "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "uvloop" +version = "0.19.0" +description = "Fast implementation of asyncio event loop on top of libuv" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e"}, + {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428"}, + {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8"}, + {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849"}, + {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957"}, + {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd"}, + {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef"}, + {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2"}, + {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1"}, + {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24"}, + {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533"}, + {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12"}, + {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650"}, + {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec"}, + {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc"}, + {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6"}, + {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593"}, + {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3"}, + {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd"}, + {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd"}, + {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be"}, + {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797"}, + {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d"}, + {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7"}, + {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b"}, + {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67"}, + {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7"}, + {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256"}, + {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17"}, + {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5"}, + {file = "uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd"}, +] + +[package.extras] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] + [[package]] name = "virtualenv" version = "20.25.0" @@ -1008,4 +1053,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "8eb645dc6c0e421d175dd92128ce0e6305de77cb637f68d30977a9a702279795" +content-hash = "54f9ec54e40802da005e6c6ca7507eaafeadcec8c0f6fc1ff0d2fd91da56c074" diff --git a/pyproject.toml b/pyproject.toml index ef09496..2bafae5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,9 +35,10 @@ types-requests = "^2.31.0.20240125" pre-commit = "^3.6.0" ruff = "^0.1.15" mypy = "^1.8.0" +uvloop = "^0.19.0" [tool.ruff] -line-length = 88 +line-length = 99 select = [ "PL", # pylint "F", # pyflakes From 691417a1c0c7e6fae7239d9bcb20b8973eacabf0 Mon Sep 17 00:00:00 2001 From: KatantDev Date: Wed, 31 Jan 2024 06:40:29 +1000 Subject: [PATCH 07/55] fix/chore: change default timeout and unauthorized status --- cent/client/session/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cent/client/session/base.py b/cent/client/session/base.py index a44d728..55ef1d2 100644 --- a/cent/client/session/base.py +++ b/cent/client/session/base.py @@ -11,7 +11,7 @@ from cent.client.client import Client from cent.client.async_client import AsyncClient -DEFAULT_TIMEOUT: Final[float] = 60.0 +DEFAULT_TIMEOUT: Final[float] = 10.0 _JsonLoads = Callable[..., Any] _JsonDumps = Callable[..., str] @@ -44,7 +44,7 @@ def check_response( content: str, ) -> Response[CentType]: """Validate response.""" - if status_code == HTTPStatus.FORBIDDEN: + if status_code == HTTPStatus.UNAUTHORIZED: raise InvalidApiKeyError try: From 36f2bb3b3c2df1ee2ecb1f01f78fccc49c935026 Mon Sep 17 00:00:00 2001 From: KatantDev Date: Wed, 31 Jan 2024 23:03:26 +1000 Subject: [PATCH 08/55] feat: benchmarks --- benchmarks/__init__.py | 0 benchmarks/conftest.py | 90 +++++++++++++++++++++ benchmarks/test_publish.py | 36 +++++++++ example.py | 26 ------ poetry.lock | 160 +++++++++++++++++++++++++++++++++++-- pyproject.toml | 20 +++-- 6 files changed, 295 insertions(+), 37 deletions(-) create mode 100644 benchmarks/__init__.py create mode 100644 benchmarks/conftest.py create mode 100644 benchmarks/test_publish.py delete mode 100644 example.py diff --git a/benchmarks/__init__.py b/benchmarks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/benchmarks/conftest.py b/benchmarks/conftest.py new file mode 100644 index 0000000..d73a540 --- /dev/null +++ b/benchmarks/conftest.py @@ -0,0 +1,90 @@ +import contextlib +from typing import ( + Any, + AsyncGenerator, + Tuple, + Dict, + Callable, + Awaitable, + Optional, + cast, + TYPE_CHECKING, +) + +import pytest +from pytest_benchmark.fixture import BenchmarkFixture + +from cent import Client, AsyncClient + +if TYPE_CHECKING: + from asyncio import AbstractEventLoop + from threading import Thread + +BASE_URL = "http://localhost:8000/api" +API_KEY = "api_key" + +BenchmarkCoroType = Callable[[], Awaitable[None]] +BenchmarkType = Callable[[], Optional[Awaitable[None]]] +BenchmarkDecoratorType = Callable[[BenchmarkType], None] + + +@pytest.fixture(scope="session") +def anyio_backend() -> Tuple[str, Dict[str, bool]]: + return "asyncio", {"use_uvloop": True} + + +@pytest.fixture() +def aio_benchmark(benchmark: BenchmarkFixture) -> BenchmarkDecoratorType: + import asyncio + import threading + + class Sync2Async: + def __init__(self, coro: BenchmarkCoroType) -> None: + self.coro = coro + self.custom_loop: Optional["AbstractEventLoop"] = None + self.thread: Optional["Thread"] = None + + def start_background_loop(self) -> None: + if self.custom_loop: + asyncio.set_event_loop(self.custom_loop) + self.custom_loop.run_forever() + + def __call__(self) -> Any: + awaitable = self.coro() + with contextlib.suppress(RuntimeError): + evloop = asyncio.get_running_loop() + if evloop is None: + return asyncio.run(awaitable) + else: + if not self.custom_loop or not self.thread or not self.thread.is_alive(): + self.custom_loop = asyncio.new_event_loop() + self.thread = threading.Thread( + target=self.start_background_loop, + daemon=True, + ) + self.thread.start() + + return asyncio.run_coroutine_threadsafe(awaitable, self.custom_loop).result() + + def _wrapper(func: BenchmarkType) -> None: + if asyncio.iscoroutinefunction(func): + func = cast(BenchmarkCoroType, func) + benchmark(Sync2Async(func)) + else: + benchmark(func) + + return _wrapper + + +@pytest.fixture() +def sync_client() -> Client: + return Client(BASE_URL, API_KEY) + + +@pytest.fixture() +async def async_client( + anyio_backend: Any, # noqa: ARG001 +) -> AsyncGenerator[AsyncClient, None]: + client = AsyncClient(BASE_URL, API_KEY) + yield client + await client.session.close() diff --git a/benchmarks/test_publish.py b/benchmarks/test_publish.py new file mode 100644 index 0000000..f4d328d --- /dev/null +++ b/benchmarks/test_publish.py @@ -0,0 +1,36 @@ +import pytest + +from benchmarks.conftest import BenchmarkDecoratorType +from cent import AsyncClient, Client + + +def sync_requests(client: Client) -> None: + for j in range(1000): + client.publish( + channel=f"personal:{j}", + data={"message": "Hello world!"}, + ) + + +async def async_requests(client: AsyncClient) -> None: + for j in range(1000): + await client.publish( + channel=f"personal:{j}", + data={"message": "Hello world!"}, + ) + + +def test_sync( + aio_benchmark: BenchmarkDecoratorType, + sync_client: Client, +) -> None: + @aio_benchmark + def _() -> None: + sync_requests(sync_client) + + +@pytest.mark.anyio() +async def test_async(aio_benchmark: BenchmarkDecoratorType, async_client: AsyncClient) -> None: + @aio_benchmark + async def _() -> None: + await async_requests(async_client) diff --git a/example.py b/example.py deleted file mode 100644 index 111e311..0000000 --- a/example.py +++ /dev/null @@ -1,26 +0,0 @@ -import asyncio - -from cent import AsyncClient, Client - -BASE_URL = "http://localhost:8000/api" -API_KEY = "api_key" - -async_client = AsyncClient(BASE_URL, API_KEY) -sync_client = Client(BASE_URL, API_KEY) - - -async def main() -> None: - response = await async_client.publish( - channel="example:123", - data={"message": "Hello world!"}, - ) - print(response) - response = sync_client.publish( - channel="example:123", - data={"message": "Hello world!"}, - ) - print(response) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/poetry.lock b/poetry.lock index 4e3f651..44dfedf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -121,6 +121,28 @@ files = [ {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, ] +[[package]] +name = "anyio" +version = "4.2.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"}, + {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + [[package]] name = "async-timeout" version = "4.0.3" @@ -272,6 +294,17 @@ files = [ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + [[package]] name = "distlib" version = "0.3.8" @@ -283,6 +316,20 @@ files = [ {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "filelock" version = "3.13.1" @@ -410,6 +457,17 @@ files = [ {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "multidict" version = "6.0.4" @@ -565,20 +623,46 @@ files = [ [package.dependencies] setuptools = "*" +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + [[package]] name = "platformdirs" -version = "4.1.0" +version = "4.2.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, - {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, ] [package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] + +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" @@ -598,6 +682,17 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +description = "Get CPU info with pure Python" +optional = false +python-versions = "*" +files = [ + {file = "py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690"}, + {file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"}, +] + [[package]] name = "pydantic" version = "2.6.0" @@ -708,6 +803,48 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pytest" +version = "8.0.0" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.0.0-py3-none-any.whl", hash = "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"}, + {file = "pytest-8.0.0.tar.gz", hash = "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.3.0,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-benchmark" +version = "4.0.0" +description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-benchmark-4.0.0.tar.gz", hash = "sha256:fb0785b83efe599a6a956361c0691ae1dbb5318018561af10f3e915caa0048d1"}, + {file = "pytest_benchmark-4.0.0-py3-none-any.whl", hash = "sha256:fdb7db64e31c8b277dff9850d2a2556d8b60bcb0ea6524e36e28ffd7c87f71d6"}, +] + +[package.dependencies] +py-cpuinfo = "*" +pytest = ">=3.8" + +[package.extras] +aspect = ["aspectlib"] +elasticsearch = ["elasticsearch"] +histogram = ["pygal", "pygaljs"] + [[package]] name = "pyyaml" version = "6.0.1" @@ -830,6 +967,17 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] + [[package]] name = "tomli" version = "2.0.1" @@ -1053,4 +1201,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "54f9ec54e40802da005e6c6ca7507eaafeadcec8c0f6fc1ff0d2fd91da56c074" +content-hash = "0d716cc0baf2ecbf28e96b932074d58b3e5b4726da7709145c4baf5f5b24cbf0" diff --git a/pyproject.toml b/pyproject.toml index 2bafae5..3891f27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = "cent" +name = "new_cent" version = "5.0.0b1" description = "Python library to communicate with Centrifugo v3 HTTP API" authors = ["Alexandr Emelin", "Bogdan Evstratenko", "Katant Savelev"] @@ -26,16 +26,19 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.9" -aiohttp = "^3.9.3" -pydantic = "^2.6.0" -requests = "^2.31.0" -types-requests = "^2.31.0.20240125" +aiohttp = "^3" +pydantic = "^2" +requests = "^2" +types-requests = "^2" [tool.poetry.group.dev.dependencies] pre-commit = "^3.6.0" ruff = "^0.1.15" mypy = "^1.8.0" uvloop = "^0.19.0" +pytest = "^8" +anyio = "^4.2.0" +pytest-benchmark = "^4.0.0" [tool.ruff] line-length = 99 @@ -100,6 +103,13 @@ namespace_packages = true show_absolute_path = true +[[tool.mypy.overrides]] +module = [ + "pytest_benchmark.*" +] +ignore_missing_imports = true + + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" From 71c89eb00d9a15916d05387f392a20e27574fb91 Mon Sep 17 00:00:00 2001 From: KatantDev Date: Thu, 1 Feb 2024 13:38:46 +1000 Subject: [PATCH 09/55] feat: implement all methods from API --- cent/__init__.py | 9 + cent/client/__init__.py | 2 +- cent/client/async_client.py | 138 +++++++++- cent/client/client.py | 113 -------- cent/client/session/base.py | 11 +- cent/client/session/base_sync.py | 2 +- cent/client/session/requests.py | 2 +- cent/client/sync_client.py | 247 ++++++++++++++++++ cent/methods/__init__.py | 25 +- cent/methods/broadcast.py | 2 +- cent/methods/channels.py | 14 + cent/methods/disconnect.py | 23 ++ cent/methods/history.py | 21 ++ cent/methods/history_remove.py | 12 + cent/methods/info.py | 9 + cent/methods/presence.py | 12 + cent/methods/presence_stats.py | 12 + cent/methods/publish.py | 2 +- cent/methods/refresh.py | 22 ++ cent/methods/unsubscribe.py | 20 ++ cent/types/__init__.py | 54 +++- cent/types/bool_value.py | 4 +- .../{broadcast.py => broadcast_result.py} | 3 +- cent/types/channel_info_result.py | 8 + cent/types/channel_options_override.py | 5 +- cent/types/channels_result.py | 11 + cent/types/client_info_result.py | 16 ++ cent/types/disconnect.py | 10 + cent/types/disconnect_result.py | 5 + cent/types/history_remove_result.py | 5 + cent/types/history_result.py | 17 ++ cent/types/info_result.py | 11 + cent/types/metrics_result.py | 12 + cent/types/node_result.py | 30 +++ cent/types/presence_result.py | 11 + cent/types/presence_stats_result.py | 10 + cent/types/process_result.py | 10 + cent/types/publication_result.py | 12 + cent/types/{publish.py => publish_result.py} | 0 cent/types/refresh_result.py | 5 + cent/types/stream_position.py | 4 +- .../{subscribe.py => subscribe_result.py} | 0 cent/types/unsubscribe_result.py | 5 + pyproject.toml | 2 +- tests/__init__.py | 0 tests/conftest.py | 32 +++ tests/test_async_validation.py | 104 ++++++++ tests/test_sync_validation.py | 103 ++++++++ 48 files changed, 1043 insertions(+), 144 deletions(-) delete mode 100644 cent/client/client.py create mode 100644 cent/client/sync_client.py create mode 100644 cent/methods/channels.py create mode 100644 cent/methods/disconnect.py create mode 100644 cent/methods/history.py create mode 100644 cent/methods/history_remove.py create mode 100644 cent/methods/info.py create mode 100644 cent/methods/presence.py create mode 100644 cent/methods/presence_stats.py create mode 100644 cent/methods/refresh.py create mode 100644 cent/methods/unsubscribe.py rename cent/types/{broadcast.py => broadcast_result.py} (65%) create mode 100644 cent/types/channel_info_result.py create mode 100644 cent/types/channels_result.py create mode 100644 cent/types/client_info_result.py create mode 100644 cent/types/disconnect.py create mode 100644 cent/types/disconnect_result.py create mode 100644 cent/types/history_remove_result.py create mode 100644 cent/types/history_result.py create mode 100644 cent/types/info_result.py create mode 100644 cent/types/metrics_result.py create mode 100644 cent/types/node_result.py create mode 100644 cent/types/presence_result.py create mode 100644 cent/types/presence_stats_result.py create mode 100644 cent/types/process_result.py create mode 100644 cent/types/publication_result.py rename cent/types/{publish.py => publish_result.py} (100%) create mode 100644 cent/types/refresh_result.py rename cent/types/{subscribe.py => subscribe_result.py} (100%) create mode 100644 cent/types/unsubscribe_result.py create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_async_validation.py create mode 100644 tests/test_sync_validation.py diff --git a/cent/__init__.py b/cent/__init__.py index 1825439..fa4a41f 100644 --- a/cent/__init__.py +++ b/cent/__init__.py @@ -1,3 +1,6 @@ +import contextlib +import asyncio as _asyncio + from .client import ( Client, AsyncClient, @@ -7,8 +10,14 @@ RequestsSession, AiohttpSession, ) + from .__meta__ import __version__ +with contextlib.suppress(ImportError): + import uvloop as _uvloop + + _asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy()) + __all__ = ( "__version__", "types", diff --git a/cent/client/__init__.py b/cent/client/__init__.py index 5784179..4cb220b 100644 --- a/cent/client/__init__.py +++ b/cent/client/__init__.py @@ -5,7 +5,7 @@ AiohttpSession, RequestsSession, ) -from .client import Client +from .sync_client import Client from .async_client import AsyncClient __all__ = ( diff --git a/cent/client/async_client.py b/cent/client/async_client.py index 03669e1..5595634 100644 --- a/cent/client/async_client.py +++ b/cent/client/async_client.py @@ -6,13 +6,32 @@ BroadcastMethod, PublishMethod, SubscribeMethod, + UnsubscribeMethod, + PresenceMethod, + PresenceStatsMethod, + HistoryMethod, + HistoryRemoveMethod, + RefreshMethod, + ChannelsMethod, + DisconnectMethod, + InfoMethod, ) from cent.types import ( - SubscribeResult, - StreamPosition, PublishResult, BroadcastResult, + SubscribeResult, + UnsubscribeResult, + PresenceResult, + PresenceStatsResult, + HistoryResult, + HistoryRemoveResult, + RefreshResult, + ChannelsResult, + DisconnectResult, + InfoResult, + StreamPosition, ChannelOptionsOverride, + Disconnect, ) T = TypeVar("T") @@ -103,6 +122,121 @@ async def subscribe( ) return await self(call, request_timeout=request_timeout) + async def unsubscribe( + self, + user: str, + channel: str, + client: Optional[str] = None, + session: Optional[str] = None, + request_timeout: Optional[float] = None, + ) -> UnsubscribeResult: + call = UnsubscribeMethod( + user=user, + channel=channel, + client=client, + session=session, + ) + return await self(call, request_timeout=request_timeout) + + async def presence( + self, + channel: str, + request_timeout: Optional[float] = None, + ) -> PresenceResult: + call = PresenceMethod( + channel=channel, + ) + return await self(call, request_timeout=request_timeout) + + async def presence_stats( + self, + channel: str, + request_timeout: Optional[float] = None, + ) -> PresenceStatsResult: + call = PresenceStatsMethod( + channel=channel, + ) + return await self(call, request_timeout=request_timeout) + + async def history( + self, + channel: str, + limit: Optional[int] = None, + since: Optional[StreamPosition] = None, + reverse: Optional[bool] = None, + request_timeout: Optional[float] = None, + ) -> HistoryResult: + call = HistoryMethod( + channel=channel, + limit=limit, + since=since, + reverse=reverse, + ) + return await self(call, request_timeout=request_timeout) + + async def history_remove( + self, + channel: str, + request_timeout: Optional[float] = None, + ) -> HistoryRemoveResult: + call = HistoryRemoveMethod( + channel=channel, + ) + return await self(call, request_timeout=request_timeout) + + async def refresh( + self, + user: str, + client: Optional[str] = None, + session: Optional[str] = None, + expire_at: Optional[int] = None, + expired: Optional[bool] = None, + request_timeout: Optional[float] = None, + ) -> RefreshResult: + call = RefreshMethod( + user=user, + client=client, + session=session, + expire_at=expire_at, + expired=expired, + ) + return await self(call, request_timeout=request_timeout) + + async def channels( + self, + pattern: Optional[str] = None, + request_timeout: Optional[float] = None, + ) -> ChannelsResult: + call = ChannelsMethod( + pattern=pattern, + ) + return await self(call, request_timeout=request_timeout) + + async def disconnect( + self, + user: str, + client: Optional[str] = None, + session: Optional[str] = None, + whitelist: Optional[List[str]] = None, + disconnect: Optional[Disconnect] = None, + request_timeout: Optional[float] = None, + ) -> DisconnectResult: + call = DisconnectMethod( + user=user, + client=client, + session=session, + whitelist=whitelist, + disconnect=disconnect, + ) + return await self(call, request_timeout=request_timeout) + + async def info( + self, + request_timeout: Optional[float] = None, + ) -> InfoResult: + call = InfoMethod() + return await self(call, request_timeout=request_timeout) + async def __call__(self, method: CentMethod[T], request_timeout: Optional[float] = None) -> T: """ Call API method diff --git a/cent/client/client.py b/cent/client/client.py deleted file mode 100644 index ccd9139..0000000 --- a/cent/client/client.py +++ /dev/null @@ -1,113 +0,0 @@ -from typing import List, Optional, Any, Dict, TypeVar - -from cent.client.session import BaseSyncSession, RequestsSession -from cent.methods import ( - CentMethod, - BroadcastMethod, - PublishMethod, - SubscribeMethod, -) -from cent.types import ( - StreamPosition, - PublishResult, - BroadcastResult, - ChannelOptionsOverride, - SubscribeResult, -) - -T = TypeVar("T") - - -class Client: - def __init__( - self, - base_url: str, - api_key: str, - session: Optional[BaseSyncSession] = None, - ) -> None: - """ - :param base_url: Centrifuge base_url - :param api_key: Centrifuge API key - :param session: Custom Session instance - """ - - self._base_url = base_url - self.api_key = api_key - self.session = session or RequestsSession(base_url=base_url) - - def publish( - self, - channel: str, - data: Any, - skip_history: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - b64data: Optional[str] = None, - idempotency_key: Optional[str] = None, - request_timeout: Optional[float] = None, - ) -> PublishResult: - call = PublishMethod( - channel=channel, - data=data, - skip_history=skip_history, - tags=tags, - b64data=b64data, - idempotency_key=idempotency_key, - ) - return self(call, request_timeout=request_timeout) - - def broadcast( - self, - channels: List[str], - data: Any, - skip_history: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - b64data: Optional[str] = None, - idempotency_key: Optional[str] = None, - request_timeout: Optional[float] = None, - ) -> BroadcastResult: - call = BroadcastMethod( - channels=channels, - data=data, - skip_history=skip_history, - tags=tags, - b64data=b64data, - idempotency_key=idempotency_key, - ) - return self(call, request_timeout=request_timeout) - - def subscribe( - self, - user: str, - channel: str, - info: Optional[Any] = None, - b64info: Optional[str] = None, - client: Optional[str] = None, - session: Optional[str] = None, - data: Optional[Any] = None, - b64data: Optional[str] = None, - recover_since: Optional[StreamPosition] = None, - override: Optional[ChannelOptionsOverride] = None, - request_timeout: Optional[float] = None, - ) -> SubscribeResult: - call = SubscribeMethod( - user=user, - channel=channel, - info=info, - b64info=b64info, - client=client, - session=session, - data=data, - b64data=b64data, - recover_since=recover_since, - override=override, - ) - return self(call, request_timeout=request_timeout) - - def __call__(self, method: CentMethod[T], request_timeout: Optional[float] = None) -> T: - """ - Call API method - - :param method: Centrifugo method - :return: Centrifugo response - """ - return self.session(self, method, timeout=request_timeout) diff --git a/cent/client/session/base.py b/cent/client/session/base.py index 55ef1d2..624f53e 100644 --- a/cent/client/session/base.py +++ b/cent/client/session/base.py @@ -7,8 +7,15 @@ from cent.exceptions import ClientDecodeError, DetailedAPIError, InvalidApiKeyError from cent.methods.base import CentMethod, CentType, Response, Error +try: + import orjson + + loads = orjson.loads +except ImportError: + loads = json.loads + if TYPE_CHECKING: - from cent.client.client import Client + from cent.client.sync_client import Client from cent.client.async_client import AsyncClient DEFAULT_TIMEOUT: Final[float] = 10.0 @@ -22,7 +29,7 @@ class BaseSession: def __init__( self, base_url: str, - json_loads: _JsonLoads = json.loads, + json_loads: _JsonLoads = loads, timeout: float = DEFAULT_TIMEOUT, ) -> None: """ diff --git a/cent/client/session/base_sync.py b/cent/client/session/base_sync.py index 6a4b012..3071de9 100644 --- a/cent/client/session/base_sync.py +++ b/cent/client/session/base_sync.py @@ -5,7 +5,7 @@ from cent.client.session.base import BaseSession if TYPE_CHECKING: - from cent.client.client import Client + from cent.client.sync_client import Client DEFAULT_TIMEOUT: Final[float] = 60.0 _JsonLoads = Callable[..., Any] diff --git a/cent/client/session/requests.py b/cent/client/session/requests.py index 6cd3083..29015e6 100644 --- a/cent/client/session/requests.py +++ b/cent/client/session/requests.py @@ -11,7 +11,7 @@ from cent.exceptions import CentNetworkError if TYPE_CHECKING: - from cent.client.client import Client + from cent.client.sync_client import Client class RequestsSession(BaseSyncSession): diff --git a/cent/client/sync_client.py b/cent/client/sync_client.py new file mode 100644 index 0000000..067a208 --- /dev/null +++ b/cent/client/sync_client.py @@ -0,0 +1,247 @@ +from typing import List, Optional, Any, Dict, TypeVar + +from cent.client.session import BaseSyncSession, RequestsSession +from cent.methods import ( + CentMethod, + BroadcastMethod, + PublishMethod, + SubscribeMethod, + UnsubscribeMethod, + PresenceMethod, + PresenceStatsMethod, + HistoryMethod, + HistoryRemoveMethod, + RefreshMethod, + ChannelsMethod, + DisconnectMethod, + InfoMethod, +) +from cent.types import ( + PublishResult, + BroadcastResult, + SubscribeResult, + UnsubscribeResult, + PresenceResult, + PresenceStatsResult, + HistoryResult, + HistoryRemoveResult, + RefreshResult, + ChannelsResult, + DisconnectResult, + InfoResult, + StreamPosition, + ChannelOptionsOverride, + Disconnect, +) + +T = TypeVar("T") + + +class Client: + def __init__( + self, + base_url: str, + api_key: str, + session: Optional[BaseSyncSession] = None, + ) -> None: + """ + :param base_url: Centrifuge base_url + :param api_key: Centrifuge API key + :param session: Custom Session instance + """ + + self._base_url = base_url + self.api_key = api_key + self.session = session or RequestsSession(base_url=base_url) + + def publish( + self, + channel: str, + data: Any, + skip_history: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + b64data: Optional[str] = None, + idempotency_key: Optional[str] = None, + request_timeout: Optional[float] = None, + ) -> PublishResult: + call = PublishMethod( + channel=channel, + data=data, + skip_history=skip_history, + tags=tags, + b64data=b64data, + idempotency_key=idempotency_key, + ) + return self(call, request_timeout=request_timeout) + + def broadcast( + self, + channels: List[str], + data: Any, + skip_history: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + b64data: Optional[str] = None, + idempotency_key: Optional[str] = None, + request_timeout: Optional[float] = None, + ) -> BroadcastResult: + call = BroadcastMethod( + channels=channels, + data=data, + skip_history=skip_history, + tags=tags, + b64data=b64data, + idempotency_key=idempotency_key, + ) + return self(call, request_timeout=request_timeout) + + def subscribe( + self, + user: str, + channel: str, + info: Optional[Any] = None, + b64info: Optional[str] = None, + client: Optional[str] = None, + session: Optional[str] = None, + data: Optional[Any] = None, + b64data: Optional[str] = None, + recover_since: Optional[StreamPosition] = None, + override: Optional[ChannelOptionsOverride] = None, + request_timeout: Optional[float] = None, + ) -> SubscribeResult: + call = SubscribeMethod( + user=user, + channel=channel, + info=info, + b64info=b64info, + client=client, + session=session, + data=data, + b64data=b64data, + recover_since=recover_since, + override=override, + ) + return self(call, request_timeout=request_timeout) + + def unsubscribe( + self, + user: str, + channel: str, + client: Optional[str] = None, + session: Optional[str] = None, + request_timeout: Optional[float] = None, + ) -> UnsubscribeResult: + call = UnsubscribeMethod( + user=user, + channel=channel, + client=client, + session=session, + ) + return self(call, request_timeout=request_timeout) + + def presence( + self, + channel: str, + request_timeout: Optional[float] = None, + ) -> PresenceResult: + call = PresenceMethod( + channel=channel, + ) + return self(call, request_timeout=request_timeout) + + def presence_stats( + self, + channel: str, + request_timeout: Optional[float] = None, + ) -> PresenceStatsResult: + call = PresenceStatsMethod( + channel=channel, + ) + return self(call, request_timeout=request_timeout) + + def history( + self, + channel: str, + limit: Optional[int] = None, + since: Optional[StreamPosition] = None, + reverse: Optional[bool] = None, + request_timeout: Optional[float] = None, + ) -> HistoryResult: + call = HistoryMethod( + channel=channel, + limit=limit, + since=since, + reverse=reverse, + ) + return self(call, request_timeout=request_timeout) + + def history_remove( + self, + channel: str, + request_timeout: Optional[float] = None, + ) -> HistoryRemoveResult: + call = HistoryRemoveMethod( + channel=channel, + ) + return self(call, request_timeout=request_timeout) + + def refresh( + self, + user: str, + client: Optional[str] = None, + session: Optional[str] = None, + expired: Optional[bool] = None, + expire_at: Optional[int] = None, + request_timeout: Optional[float] = None, + ) -> RefreshResult: + call = RefreshMethod( + user=user, + client=client, + session=session, + expired=expired, + expire_at=expire_at, + ) + return self(call, request_timeout=request_timeout) + + def channels( + self, + pattern: Optional[str] = None, + request_timeout: Optional[float] = None, + ) -> ChannelsResult: + call = ChannelsMethod( + pattern=pattern, + ) + return self(call, request_timeout=request_timeout) + + def disconnect( + self, + user: str, + client: Optional[str] = None, + session: Optional[str] = None, + whitelist: Optional[List[str]] = None, + disconnect: Optional[Disconnect] = None, + request_timeout: Optional[float] = None, + ) -> DisconnectResult: + call = DisconnectMethod( + user=user, + client=client, + session=session, + whitelist=whitelist, + disconnect=disconnect, + ) + return self(call, request_timeout=request_timeout) + + def info( + self, + request_timeout: Optional[float] = None, + ) -> InfoResult: + call = InfoMethod() + return self(call, request_timeout=request_timeout) + + def __call__(self, method: CentMethod[T], request_timeout: Optional[float] = None) -> T: + """ + Call API method + + :param method: Centrifugo method + :return: Centrifugo response + """ + return self.session(self, method, timeout=request_timeout) diff --git a/cent/methods/__init__.py b/cent/methods/__init__.py index b8779ea..eed4779 100644 --- a/cent/methods/__init__.py +++ b/cent/methods/__init__.py @@ -2,5 +2,28 @@ from .broadcast import BroadcastMethod from .publish import PublishMethod from .subscribe import SubscribeMethod +from .unsubscribe import UnsubscribeMethod +from .presence import PresenceMethod +from .presence_stats import PresenceStatsMethod +from .history import HistoryMethod +from .history_remove import HistoryRemoveMethod +from .refresh import RefreshMethod +from .channels import ChannelsMethod +from .disconnect import DisconnectMethod +from .info import InfoMethod -__all__ = ("CentMethod", "BroadcastMethod", "PublishMethod", "SubscribeMethod") +__all__ = ( + "CentMethod", + "BroadcastMethod", + "PublishMethod", + "SubscribeMethod", + "UnsubscribeMethod", + "PresenceMethod", + "PresenceStatsMethod", + "HistoryMethod", + "HistoryRemoveMethod", + "RefreshMethod", + "ChannelsMethod", + "DisconnectMethod", + "InfoMethod", +) diff --git a/cent/methods/broadcast.py b/cent/methods/broadcast.py index 21dbcbe..e096ee0 100644 --- a/cent/methods/broadcast.py +++ b/cent/methods/broadcast.py @@ -1,7 +1,7 @@ from typing import Dict, Optional, List, Any from cent.methods.base import CentMethod -from cent.types.broadcast import BroadcastResult +from cent.types.broadcast_result import BroadcastResult class BroadcastMethod(CentMethod[BroadcastResult]): diff --git a/cent/methods/channels.py b/cent/methods/channels.py new file mode 100644 index 0000000..ebc4128 --- /dev/null +++ b/cent/methods/channels.py @@ -0,0 +1,14 @@ +from typing import Optional + +from cent.methods import CentMethod +from cent.types.channels_result import ChannelsResult + + +class ChannelsMethod(CentMethod[ChannelsResult]): + """Channels request.""" + + __returning__ = ChannelsResult + __api_method__ = "channels" + + pattern: Optional[str] = None + """Pattern to filter channels, we are using https://github.com/gobwas/glob library for matching.""" diff --git a/cent/methods/disconnect.py b/cent/methods/disconnect.py new file mode 100644 index 0000000..6d41ca2 --- /dev/null +++ b/cent/methods/disconnect.py @@ -0,0 +1,23 @@ +from typing import Optional, List + +from cent.methods import CentMethod +from cent.types import Disconnect +from cent.types.disconnect_result import DisconnectResult + + +class DisconnectMethod(CentMethod[DisconnectResult]): + """Disconnect request.""" + + __returning__ = DisconnectResult + __api_method__ = "disconnect" + + user: str + """User ID to disconnect.""" + client: Optional[str] = None + """Specific client ID to disconnect (user still required to be set).""" + session: Optional[str] = None + """Specific client session to disconnect (user still required to be set).""" + whitelist: Optional[List[str]] = None + """Array of client IDs to keep.""" + disconnect: Optional[Disconnect] = None + """Provide custom disconnect object, see below.""" diff --git a/cent/methods/history.py b/cent/methods/history.py new file mode 100644 index 0000000..f755cfa --- /dev/null +++ b/cent/methods/history.py @@ -0,0 +1,21 @@ +from typing import Optional + +from cent.methods import CentMethod +from cent.types import StreamPosition +from cent.types.history_result import HistoryResult + + +class HistoryMethod(CentMethod[HistoryResult]): + """History request.""" + + __returning__ = HistoryResult + __api_method__ = "history" + + channel: str + """Name of channel to call history from.""" + limit: Optional[int] = None + """Limit number of returned publications, if not set in request then only current stream position information will present in result (without any publications).""" + since: Optional[StreamPosition] = None + """To return publications after this position.""" + reverse: Optional[bool] = None + """Iterate in reversed order (from latest to earliest).""" diff --git a/cent/methods/history_remove.py b/cent/methods/history_remove.py new file mode 100644 index 0000000..cb895b6 --- /dev/null +++ b/cent/methods/history_remove.py @@ -0,0 +1,12 @@ +from cent.methods import CentMethod +from cent.types.history_remove_result import HistoryRemoveResult + + +class HistoryRemoveMethod(CentMethod[HistoryRemoveResult]): + """History remove request.""" + + __returning__ = HistoryRemoveResult + __api_method__ = "history_remove" + + channel: str + """Name of channel to remove history.""" diff --git a/cent/methods/info.py b/cent/methods/info.py new file mode 100644 index 0000000..13df7a9 --- /dev/null +++ b/cent/methods/info.py @@ -0,0 +1,9 @@ +from cent.methods import CentMethod +from cent.types.info_result import InfoResult + + +class InfoMethod(CentMethod[InfoResult]): + """Info request.""" + + __returning__ = InfoResult + __api_method__ = "info" diff --git a/cent/methods/presence.py b/cent/methods/presence.py new file mode 100644 index 0000000..d68996c --- /dev/null +++ b/cent/methods/presence.py @@ -0,0 +1,12 @@ +from cent.methods import CentMethod +from cent.types.presence_result import PresenceResult + + +class PresenceMethod(CentMethod[PresenceResult]): + """Presence request.""" + + __returning__ = PresenceResult + __api_method__ = "presence" + + channel: str + """Name of channel to call presence from.""" diff --git a/cent/methods/presence_stats.py b/cent/methods/presence_stats.py new file mode 100644 index 0000000..abd048e --- /dev/null +++ b/cent/methods/presence_stats.py @@ -0,0 +1,12 @@ +from cent.methods import CentMethod +from cent.types.presence_stats_result import PresenceStatsResult + + +class PresenceStatsMethod(CentMethod[PresenceStatsResult]): + """Presence request.""" + + __returning__ = PresenceStatsResult + __api_method__ = "presence_stats" + + channel: str + """Name of channel to call presence from.""" diff --git a/cent/methods/publish.py b/cent/methods/publish.py index 07b785e..675196c 100644 --- a/cent/methods/publish.py +++ b/cent/methods/publish.py @@ -1,7 +1,7 @@ from typing import Any, Dict, Optional from cent.methods.base import CentMethod -from cent.types.publish import PublishResult +from cent.types.publish_result import PublishResult class PublishMethod(CentMethod[PublishResult]): diff --git a/cent/methods/refresh.py b/cent/methods/refresh.py new file mode 100644 index 0000000..2e2e3e9 --- /dev/null +++ b/cent/methods/refresh.py @@ -0,0 +1,22 @@ +from typing import Optional + +from cent.methods import CentMethod +from cent.types.refresh_result import RefreshResult + + +class RefreshMethod(CentMethod[RefreshResult]): + """Refresh request.""" + + __returning__ = RefreshResult + __api_method__ = "refresh" + + user: str + """User ID to refresh.""" + client: Optional[str] = None + """Client ID to refresh (user still required to be set).""" + session: Optional[str] = None + """Specific client session to refresh (user still required to be set).""" + expired: Optional[bool] = None + """Mark connection as expired and close with Disconnect Expired reason.""" + expire_at: Optional[int] = None + """Unix time (in seconds) in the future when the connection will expire.""" diff --git a/cent/methods/unsubscribe.py b/cent/methods/unsubscribe.py new file mode 100644 index 0000000..c4ceacc --- /dev/null +++ b/cent/methods/unsubscribe.py @@ -0,0 +1,20 @@ +from typing import Optional + +from cent.methods.base import CentMethod +from cent.types.unsubscribe_result import UnsubscribeResult + + +class UnsubscribeMethod(CentMethod[UnsubscribeResult]): + """Unsubscribe request.""" + + __returning__ = UnsubscribeResult + __api_method__ = "unsubscribe" + + user: str + """User ID to unsubscribe.""" + channel: str + """Name of channel to unsubscribe user to.""" + client: Optional[str] = None + """Specific client ID to unsubscribe (user still required to be set).""" + session: Optional[str] = None + """Specific client session to disconnect (user still required to be set).""" diff --git a/cent/types/__init__.py b/cent/types/__init__.py index 603d54b..10f13ee 100644 --- a/cent/types/__init__.py +++ b/cent/types/__init__.py @@ -1,21 +1,49 @@ -import contextlib -import asyncio as _asyncio - -from .broadcast import BroadcastResult -from .publish import PublishResult +from .base import CentResult +from .bool_value import BoolValue +from .broadcast_result import BroadcastResult +from .channel_info_result import ChannelInfoResult from .channel_options_override import ChannelOptionsOverride +from .channels_result import ChannelsResult +from .client_info_result import ClientInfoResult +from .disconnect import Disconnect +from .disconnect_result import DisconnectResult +from .history_remove_result import HistoryRemoveResult +from .history_result import HistoryResult +from .info_result import InfoResult +from .metrics_result import MetricsResult +from .node_result import NodeResult +from .presence_result import PresenceResult +from .presence_stats_result import PresenceStatsResult +from .process_result import ProcessResult +from .publication_result import PublicationResult +from .publish_result import PublishResult +from .refresh_result import RefreshResult from .stream_position import StreamPosition -from .subscribe import SubscribeResult - -with contextlib.suppress(ImportError): - import uvloop as _uvloop - - _asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy()) +from .subscribe_result import SubscribeResult +from .unsubscribe_result import UnsubscribeResult __all__ = ( - "SubscribeResult", + "CentResult", + "BoolValue", "BroadcastResult", - "PublishResult", + "ChannelInfoResult", "ChannelOptionsOverride", + "ChannelsResult", + "ClientInfoResult", + "Disconnect", + "DisconnectResult", + "HistoryRemoveResult", + "HistoryResult", + "InfoResult", + "MetricsResult", + "NodeResult", + "PresenceResult", + "PresenceStatsResult", + "ProcessResult", + "PublicationResult", + "PublishResult", + "RefreshResult", "StreamPosition", + "SubscribeResult", + "UnsubscribeResult", ) diff --git a/cent/types/bool_value.py b/cent/types/bool_value.py index b575245..b1a73f4 100644 --- a/cent/types/bool_value.py +++ b/cent/types/bool_value.py @@ -1,7 +1,7 @@ -from cent.types.base import CentResult +from pydantic import BaseModel -class BoolValue(CentResult): +class BoolValue(BaseModel): """Bool value.""" value: bool diff --git a/cent/types/broadcast.py b/cent/types/broadcast_result.py similarity index 65% rename from cent/types/broadcast.py rename to cent/types/broadcast_result.py index 9801186..40336da 100644 --- a/cent/types/broadcast.py +++ b/cent/types/broadcast_result.py @@ -4,10 +4,11 @@ from cent.methods.base import Response from cent.types.base import CentResult -from cent.types.publish import PublishResult +from cent.types.publish_result import PublishResult class BroadcastResult(CentResult): """Publish result.""" responses: List[Response[PublishResult]] = Field(default_factory=list) + """Responses for each individual publish (with possible error and publish result).""" diff --git a/cent/types/channel_info_result.py b/cent/types/channel_info_result.py new file mode 100644 index 0000000..ed7994f --- /dev/null +++ b/cent/types/channel_info_result.py @@ -0,0 +1,8 @@ +from cent.types.base import CentResult + + +class ChannelInfoResult(CentResult): + """Channel info result.""" + + num_clients: int + """Total number of connections currently subscribed to a channel.""" diff --git a/cent/types/channel_options_override.py b/cent/types/channel_options_override.py index bea8fdc..4d08508 100644 --- a/cent/types/channel_options_override.py +++ b/cent/types/channel_options_override.py @@ -1,10 +1,11 @@ from typing import Optional -from cent.types.base import CentResult +from pydantic import BaseModel + from cent.types.bool_value import BoolValue -class ChannelOptionsOverride(CentResult): +class ChannelOptionsOverride(BaseModel): """Override object.""" presence: Optional[BoolValue] = None diff --git a/cent/types/channels_result.py b/cent/types/channels_result.py new file mode 100644 index 0000000..e39ed16 --- /dev/null +++ b/cent/types/channels_result.py @@ -0,0 +1,11 @@ +from typing import Dict + +from cent.types.base import CentResult +from cent.types.channel_info_result import ChannelInfoResult + + +class ChannelsResult(CentResult): + """Channels result.""" + + channels: Dict[str, ChannelInfoResult] + """Map where key is channel and value is ChannelInfoResult.""" diff --git a/cent/types/client_info_result.py b/cent/types/client_info_result.py new file mode 100644 index 0000000..7121748 --- /dev/null +++ b/cent/types/client_info_result.py @@ -0,0 +1,16 @@ +from typing import Any, Optional + +from cent.types.base import CentResult + + +class ClientInfoResult(CentResult): + """Client info result.""" + + client: str + """Client ID.""" + user: str + """User ID.""" + conn_info: Optional[Any] = None + """Optional connection info.""" + chan_info: Optional[Any] = None + """Optional channel info.""" diff --git a/cent/types/disconnect.py b/cent/types/disconnect.py new file mode 100644 index 0000000..07b4314 --- /dev/null +++ b/cent/types/disconnect.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel + + +class Disconnect(BaseModel): + """Disconnect result.""" + + code: int + """Disconnect code.""" + reason: str + """Disconnect reason.""" diff --git a/cent/types/disconnect_result.py b/cent/types/disconnect_result.py new file mode 100644 index 0000000..9d85c4c --- /dev/null +++ b/cent/types/disconnect_result.py @@ -0,0 +1,5 @@ +from cent.types.base import CentResult + + +class DisconnectResult(CentResult): + """Disconnect result.""" diff --git a/cent/types/history_remove_result.py b/cent/types/history_remove_result.py new file mode 100644 index 0000000..1ee1816 --- /dev/null +++ b/cent/types/history_remove_result.py @@ -0,0 +1,5 @@ +from cent.types.base import CentResult + + +class HistoryRemoveResult(CentResult): + """History remove result.""" diff --git a/cent/types/history_result.py b/cent/types/history_result.py new file mode 100644 index 0000000..cd4be2c --- /dev/null +++ b/cent/types/history_result.py @@ -0,0 +1,17 @@ +from typing import List, Optional + +from pydantic import Field + +from cent.types.base import CentResult +from cent.types.publication_result import PublicationResult + + +class HistoryResult(CentResult): + """History result.""" + + publications: List[PublicationResult] = Field(default_factory=list) + """List of publications in channel.""" + offset: Optional[int] = None + """Top offset in history stream.""" + epoch: Optional[str] = None + """Epoch of current stream.""" diff --git a/cent/types/info_result.py b/cent/types/info_result.py new file mode 100644 index 0000000..6a2d0db --- /dev/null +++ b/cent/types/info_result.py @@ -0,0 +1,11 @@ +from typing import List + +from cent.types.base import CentResult +from cent.types.node_result import NodeResult + + +class InfoResult(CentResult): + """Info result.""" + + nodes: List[NodeResult] + """Information about all nodes in a cluster.""" diff --git a/cent/types/metrics_result.py b/cent/types/metrics_result.py new file mode 100644 index 0000000..0ab5db9 --- /dev/null +++ b/cent/types/metrics_result.py @@ -0,0 +1,12 @@ +from typing import Dict + +from cent.types.base import CentResult + + +class MetricsResult(CentResult): + """Metrics result.""" + + interval: float + """Interval.""" + items: Dict[str, float] + """Map where key is string and value is float.""" diff --git a/cent/types/node_result.py b/cent/types/node_result.py new file mode 100644 index 0000000..1f2dddb --- /dev/null +++ b/cent/types/node_result.py @@ -0,0 +1,30 @@ +from typing import Optional + +from cent.types.base import CentResult +from cent.types.metrics_result import MetricsResult +from cent.types.process_result import ProcessResult + + +class NodeResult(CentResult): + """Node result.""" + + uid: str + """Node unique identifier.""" + name: str + """Node name.""" + version: str + """Node version.""" + num_clients: int + """Total number of connections.""" + num_users: int + """Total number of users.""" + num_channels: int + """Total number of channels.""" + uptime: int + """Node uptime.""" + metrics: MetricsResult + """Node metrics.""" + process: Optional[ProcessResult] = None + """Node process.""" + num_subs: int + """Total number of subscriptions.""" diff --git a/cent/types/presence_result.py b/cent/types/presence_result.py new file mode 100644 index 0000000..0399b03 --- /dev/null +++ b/cent/types/presence_result.py @@ -0,0 +1,11 @@ +from typing import Dict + +from cent.types.base import CentResult +from cent.types.client_info_result import ClientInfoResult + + +class PresenceResult(CentResult): + """Presence result.""" + + presence: Dict[str, ClientInfoResult] + """Offset of publication in history stream.""" diff --git a/cent/types/presence_stats_result.py b/cent/types/presence_stats_result.py new file mode 100644 index 0000000..6e15c9b --- /dev/null +++ b/cent/types/presence_stats_result.py @@ -0,0 +1,10 @@ +from cent.types.base import CentResult + + +class PresenceStatsResult(CentResult): + """Presence stats result.""" + + num_clients: int + """Total number of clients in channel.""" + num_users: int + """Total number of unique users in channel.""" diff --git a/cent/types/process_result.py b/cent/types/process_result.py new file mode 100644 index 0000000..321e76c --- /dev/null +++ b/cent/types/process_result.py @@ -0,0 +1,10 @@ +from cent.types.base import CentResult + + +class ProcessResult(CentResult): + """Process result.""" + + cpu: float + """Process CPU usage.""" + rss: int + """Process RSS.""" diff --git a/cent/types/publication_result.py b/cent/types/publication_result.py new file mode 100644 index 0000000..46a7ad5 --- /dev/null +++ b/cent/types/publication_result.py @@ -0,0 +1,12 @@ +from typing import Any + +from cent.types.base import CentResult + + +class PublicationResult(CentResult): + """Publication result.""" + + data: Any + """Custom JSON inside publication.""" + offset: int + """Offset of publication in history stream.""" diff --git a/cent/types/publish.py b/cent/types/publish_result.py similarity index 100% rename from cent/types/publish.py rename to cent/types/publish_result.py diff --git a/cent/types/refresh_result.py b/cent/types/refresh_result.py new file mode 100644 index 0000000..6ccfb4d --- /dev/null +++ b/cent/types/refresh_result.py @@ -0,0 +1,5 @@ +from cent.types.base import CentResult + + +class RefreshResult(CentResult): + """Refresh result.""" diff --git a/cent/types/stream_position.py b/cent/types/stream_position.py index 3aa1921..cb948c0 100644 --- a/cent/types/stream_position.py +++ b/cent/types/stream_position.py @@ -1,7 +1,7 @@ -from cent.types.base import CentResult +from pydantic import BaseModel -class StreamPosition(CentResult): +class StreamPosition(BaseModel): """Stream position.""" offset: int diff --git a/cent/types/subscribe.py b/cent/types/subscribe_result.py similarity index 100% rename from cent/types/subscribe.py rename to cent/types/subscribe_result.py diff --git a/cent/types/unsubscribe_result.py b/cent/types/unsubscribe_result.py new file mode 100644 index 0000000..4f72528 --- /dev/null +++ b/cent/types/unsubscribe_result.py @@ -0,0 +1,5 @@ +from cent.types.base import CentResult + + +class UnsubscribeResult(CentResult): + """Unsubscribe result.""" diff --git a/pyproject.toml b/pyproject.toml index 3891f27..d3a1230 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = "new_cent" +name = "cent" version = "5.0.0b1" description = "Python library to communicate with Centrifugo v3 HTTP API" authors = ["Alexandr Emelin", "Bogdan Evstratenko", "Katant Savelev"] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..b293f1c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,32 @@ +from typing import ( + Any, + AsyncGenerator, + Tuple, + Dict, +) + +import pytest + +from cent import Client, AsyncClient + +BASE_URL = "http://localhost:8000/api" +API_KEY = "api_key" + + +@pytest.fixture(scope="session") +def anyio_backend() -> Tuple[str, Dict[str, bool]]: + return "asyncio", {"use_uvloop": True} + + +@pytest.fixture() +def sync_client() -> Client: + return Client(BASE_URL, API_KEY) + + +@pytest.fixture() +async def async_client( + anyio_backend: Any, # noqa: ARG001 +) -> AsyncGenerator[AsyncClient, None]: + client = AsyncClient(BASE_URL, API_KEY) + yield client + await client.session.close() diff --git a/tests/test_async_validation.py b/tests/test_async_validation.py new file mode 100644 index 0000000..c8c1580 --- /dev/null +++ b/tests/test_async_validation.py @@ -0,0 +1,104 @@ +from base64 import b64encode + +from cent import AsyncClient +from cent.types import StreamPosition, Disconnect + + +async def test_publish(async_client: AsyncClient) -> None: + await async_client.publish( + "personal:1", + {"data": "data"}, + skip_history=False, + tags={"tag": "tag"}, + b64data=b64encode(b"data").decode(), + idempotency_key="idempotency_key", + ) + + +async def test_broadcast(async_client: AsyncClient) -> None: + await async_client.broadcast( + ["personal:1", "personal:2"], + {"data": "data"}, + skip_history=False, + tags={"tag": "tag"}, + b64data=b64encode(b"data").decode(), + idempotency_key="idempotency_key", + ) + + +async def test_subscribe(async_client: AsyncClient) -> None: + await async_client.subscribe( + "user", + "personal:1", + info={"info": "info"}, + b64info=b64encode(b"info").decode(), + client="client", + session="session", + data={"data": "data"}, + recover_since=StreamPosition( + offset=1, + epoch="1", + ), + ) + + +async def test_unsubscribe(async_client: AsyncClient) -> None: + await async_client.unsubscribe( + user="user", + channel="personal:1", + session="session", + client="client", + ) + + +async def test_presence(async_client: AsyncClient) -> None: + await async_client.presence("personal:1") + + +async def test_presence_stats(async_client: AsyncClient) -> None: + await async_client.presence_stats("personal:1") + + +async def test_history(async_client: AsyncClient) -> None: + await async_client.history( + channel="personal:1", + limit=1, + reverse=True, + ) + + +async def test_history_remove(async_client: AsyncClient) -> None: + await async_client.history_remove("personal:1") + + +async def test_info(async_client: AsyncClient) -> None: + await async_client.info() + + +async def test_channels(async_client: AsyncClient) -> None: + await async_client.channels( + pattern="*", + ) + + +async def test_disconnect(async_client: AsyncClient) -> None: + await async_client.disconnect( + user="user", + client="client", + session="session", + whitelist=["personal:1"], + disconnect=Disconnect( + code=4000, + reason="reason", + ), + ) + + +async def test_refresh(async_client: AsyncClient) -> None: + await async_client.refresh( + user="user", + client="client", + session="session", + expire_at=1, + expired=True, + ) diff --git a/tests/test_sync_validation.py b/tests/test_sync_validation.py new file mode 100644 index 0000000..1638a8b --- /dev/null +++ b/tests/test_sync_validation.py @@ -0,0 +1,103 @@ +from base64 import b64encode +from cent import Client +from cent.types import StreamPosition, Disconnect + + +def test_publish(sync_client: Client) -> None: + sync_client.publish( + "personal:1", + {"data": "data"}, + skip_history=False, + tags={"tag": "tag"}, + b64data=b64encode(b"data").decode(), + idempotency_key="idempotency_key", + ) + + +def test_broadcast(sync_client: Client) -> None: + sync_client.broadcast( + ["personal:1", "personal:2"], + {"data": "data"}, + skip_history=False, + tags={"tag": "tag"}, + b64data=b64encode(b"data").decode(), + idempotency_key="idempotency_key", + ) + + +def test_subscribe(sync_client: Client) -> None: + sync_client.subscribe( + "user", + "personal:1", + info={"info": "info"}, + b64info=b64encode(b"info").decode(), + client="client", + session="session", + data={"data": "data"}, + recover_since=StreamPosition( + offset=1, + epoch="1", + ), + ) + + +def test_unsubscribe(sync_client: Client) -> None: + sync_client.unsubscribe( + user="user", + channel="personal:1", + session="session", + client="client", + ) + + +def test_presence(sync_client: Client) -> None: + sync_client.presence("personal:1") + + +def test_presence_stats(sync_client: Client) -> None: + sync_client.presence_stats("personal:1") + + +def test_history(sync_client: Client) -> None: + sync_client.history( + channel="personal:1", + limit=1, + reverse=True, + ) + + +def test_history_remove(sync_client: Client) -> None: + sync_client.history_remove("personal:1") + + +def test_info(sync_client: Client) -> None: + sync_client.info() + + +def test_channels(sync_client: Client) -> None: + sync_client.channels( + pattern="*", + ) + + +def test_disconnect(sync_client: Client) -> None: + sync_client.disconnect( + user="user", + client="client", + session="session", + whitelist=["personal:1"], + disconnect=Disconnect( + code=4000, + reason="reason", + ), + ) + + +def test_refresh(sync_client: Client) -> None: + sync_client.refresh( + user="user", + client="client", + session="session", + expire_at=1, + expired=True, + ) From dda90390fc38bee98030b5e7ccf9fdfbccade676 Mon Sep 17 00:00:00 2001 From: KatantDev Date: Fri, 2 Feb 2024 00:08:25 +1000 Subject: [PATCH 10/55] feat: batch method for both clients --- cent/client/async_client.py | 10 ++++++++++ cent/client/session/aiohttp.py | 7 ++++++- cent/client/session/base.py | 32 +++++++++++++++++++++++++++++++- cent/client/session/requests.py | 6 +++++- cent/client/sync_client.py | 10 ++++++++++ cent/methods/batch.py | 14 ++++++++++++++ cent/types/base.py | 4 +++- cent/types/batch_result.py | 10 ++++++++++ cent/types/node_result.py | 2 +- cent/types/presence_result.py | 1 + tests/test_async_validation.py | 23 +++++++++++++++++++++++ tests/test_sync_validation.py | 23 +++++++++++++++++++++++ 12 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 cent/methods/batch.py create mode 100644 cent/types/batch_result.py diff --git a/cent/client/async_client.py b/cent/client/async_client.py index 5595634..cb7bdbb 100644 --- a/cent/client/async_client.py +++ b/cent/client/async_client.py @@ -16,6 +16,7 @@ DisconnectMethod, InfoMethod, ) +from cent.methods.batch import BatchMethod from cent.types import ( PublishResult, BroadcastResult, @@ -33,6 +34,7 @@ ChannelOptionsOverride, Disconnect, ) +from cent.types.batch_result import BatchResult T = TypeVar("T") @@ -237,6 +239,14 @@ async def info( call = InfoMethod() return await self(call, request_timeout=request_timeout) + async def batch( + self, + commands: List[CentMethod[Any]], + request_timeout: Optional[float] = None, + ) -> BatchResult: + call = BatchMethod.model_construct(commands=commands) + return await self(call, request_timeout=request_timeout) + async def __call__(self, method: CentMethod[T], request_timeout: Optional[float] = None) -> T: """ Call API method diff --git a/cent/client/session/aiohttp.py b/cent/client/session/aiohttp.py index 516f3f8..29614f1 100644 --- a/cent/client/session/aiohttp.py +++ b/cent/client/session/aiohttp.py @@ -9,6 +9,7 @@ from cent.client.session.base_async import BaseAsyncSession from cent.methods.base import CentMethod, CentType from cent.exceptions import CentNetworkError +from cent.methods.batch import BatchMethod if TYPE_CHECKING: from cent.client.async_client import AsyncClient @@ -46,7 +47,11 @@ async def make_request( ) -> CentType: session = await self._create_session() session.headers["X-API-Key"] = client.api_key - json_data = method.model_dump(exclude_none=True) + + if isinstance(method, BatchMethod): + json_data = self.get_batch_json_data(method) + else: + json_data = method.model_dump(exclude_none=True) url = f"{self._base_url}/{method.__api_method__}" diff --git a/cent/client/session/base.py b/cent/client/session/base.py index 624f53e..f81b566 100644 --- a/cent/client/session/base.py +++ b/cent/client/session/base.py @@ -1,11 +1,12 @@ import json from http import HTTPStatus -from typing import Final, TYPE_CHECKING, Callable, Any, Union +from typing import Final, TYPE_CHECKING, Callable, Any, Union, Dict, List from pydantic import ValidationError, TypeAdapter from cent.exceptions import ClientDecodeError, DetailedAPIError, InvalidApiKeyError from cent.methods.base import CentMethod, CentType, Response, Error +from cent.methods.batch import BatchMethod try: import orjson @@ -43,6 +44,32 @@ def __init__( self.json_loads = json_loads self._timeout = timeout + @staticmethod + def get_batch_json_data(method: BatchMethod) -> Dict[str, List[Dict[str, Any]]]: + commands = [ + {command.__api_method__: command.model_dump(exclude_none=True)} + for command in method.commands + ] + return {"commands": commands} + + @staticmethod + def validate_batch( + client: Union["Client", "AsyncClient"], + method: BatchMethod, + json_replies: List[Dict[str, Any]], + ) -> Dict[str, Dict[str, List[Any]]]: + """Validate batch method.""" + replies: List[CentMethod[Any]] = [] + for command_method, json_data in zip(method.commands, json_replies): + validated_method: CentMethod[Any] = TypeAdapter( + command_method.__returning__ + ).validate_python( + json_data[command_method.__api_method__], + context={"client": client}, + ) + replies.append(validated_method) + return {"result": {"replies": replies}} + def check_response( self, client: Union["Client", "AsyncClient"], @@ -67,6 +94,9 @@ def check_response( message=error.message, ) + if isinstance(method, BatchMethod): + json_data = self.validate_batch(client, method, json_data["replies"]) + try: response_type = Response[method.__returning__] # type: ignore response = TypeAdapter(response_type).validate_python( diff --git a/cent/client/session/requests.py b/cent/client/session/requests.py index 29015e6..5c15947 100644 --- a/cent/client/session/requests.py +++ b/cent/client/session/requests.py @@ -9,6 +9,7 @@ from cent.methods.base import CentMethod, CentType from cent.client.session.base_sync import BaseSyncSession from cent.exceptions import CentNetworkError +from cent.methods.batch import BatchMethod if TYPE_CHECKING: from cent.client.sync_client import Client @@ -37,7 +38,10 @@ def make_request( timeout: Optional[float] = None, ) -> CentType: self._session.headers["X-API-Key"] = client.api_key - json_data = method.model_dump(exclude_none=True) + if isinstance(method, BatchMethod): + json_data = self.get_batch_json_data(method) + else: + json_data = method.model_dump(exclude_none=True) url = f"{self._base_url}/{method.__api_method__}" diff --git a/cent/client/sync_client.py b/cent/client/sync_client.py index 067a208..4ca1a39 100644 --- a/cent/client/sync_client.py +++ b/cent/client/sync_client.py @@ -16,6 +16,7 @@ DisconnectMethod, InfoMethod, ) +from cent.methods.batch import BatchMethod from cent.types import ( PublishResult, BroadcastResult, @@ -33,6 +34,7 @@ ChannelOptionsOverride, Disconnect, ) +from cent.types.batch_result import BatchResult T = TypeVar("T") @@ -237,6 +239,14 @@ def info( call = InfoMethod() return self(call, request_timeout=request_timeout) + def batch( + self, + commands: List[CentMethod[Any]], + request_timeout: Optional[float] = None, + ) -> BatchResult: + call = BatchMethod.model_construct(commands=commands) + return self(call, request_timeout=request_timeout) + def __call__(self, method: CentMethod[T], request_timeout: Optional[float] = None) -> T: """ Call API method diff --git a/cent/methods/batch.py b/cent/methods/batch.py new file mode 100644 index 0000000..dc69374 --- /dev/null +++ b/cent/methods/batch.py @@ -0,0 +1,14 @@ +from typing import List, Any + +from cent.methods import CentMethod +from cent.types.batch_result import BatchResult + + +class BatchMethod(CentMethod[BatchResult]): + """Batch request.""" + + __returning__ = BatchResult + __api_method__ = "batch" + + commands: List[CentMethod[Any]] + """List of commands to execute in batch.""" diff --git a/cent/types/base.py b/cent/types/base.py index 4caad0d..8d9b149 100644 --- a/cent/types/base.py +++ b/cent/types/base.py @@ -1,9 +1,11 @@ +from abc import ABC + from pydantic import BaseModel, ConfigDict from cent.context_controller import ClientContextController -class CentResult(ClientContextController, BaseModel): +class CentResult(ClientContextController, BaseModel, ABC): model_config = ConfigDict( use_enum_values=True, extra="allow", diff --git a/cent/types/batch_result.py b/cent/types/batch_result.py new file mode 100644 index 0000000..06e1249 --- /dev/null +++ b/cent/types/batch_result.py @@ -0,0 +1,10 @@ +from typing import List, Any + +from cent.types import CentResult + + +class BatchResult(CentResult): + """Batch response.""" + + replies: List[Any] + """List of results from batch request.""" diff --git a/cent/types/node_result.py b/cent/types/node_result.py index 1f2dddb..2fc3751 100644 --- a/cent/types/node_result.py +++ b/cent/types/node_result.py @@ -22,7 +22,7 @@ class NodeResult(CentResult): """Total number of channels.""" uptime: int """Node uptime.""" - metrics: MetricsResult + metrics: Optional[MetricsResult] = None """Node metrics.""" process: Optional[ProcessResult] = None """Node process.""" diff --git a/cent/types/presence_result.py b/cent/types/presence_result.py index 0399b03..fe8df50 100644 --- a/cent/types/presence_result.py +++ b/cent/types/presence_result.py @@ -1,5 +1,6 @@ from typing import Dict + from cent.types.base import CentResult from cent.types.client_info_result import ClientInfoResult diff --git a/tests/test_async_validation.py b/tests/test_async_validation.py index c8c1580..bcf399a 100644 --- a/tests/test_async_validation.py +++ b/tests/test_async_validation.py @@ -1,6 +1,7 @@ from base64 import b64encode from cent import AsyncClient +from cent.methods import PublishMethod, BroadcastMethod, PresenceMethod from cent.types import StreamPosition, Disconnect @@ -102,3 +103,25 @@ async def test_refresh(async_client: AsyncClient) -> None: expire_at=1, expired=True, ) + + +async def test_batch(async_client: AsyncClient) -> None: + await async_client.batch( + commands=[ + PublishMethod( + channel="personal:1", + data={"data": "Second data"}, + ), + PublishMethod( + channel="personal:2", + data={"data": "First data"}, + ), + BroadcastMethod( + channels=["personal:1", "personal:2"], + data={"data": "Third data"}, + ), + PresenceMethod( + channel="personal:1", + ), + ] + ) diff --git a/tests/test_sync_validation.py b/tests/test_sync_validation.py index 1638a8b..e9b3002 100644 --- a/tests/test_sync_validation.py +++ b/tests/test_sync_validation.py @@ -1,5 +1,6 @@ from base64 import b64encode from cent import Client +from cent.methods import PublishMethod, BroadcastMethod, PresenceMethod from cent.types import StreamPosition, Disconnect @@ -101,3 +102,25 @@ def test_refresh(sync_client: Client) -> None: expire_at=1, expired=True, ) + + +def test_batch(sync_client: Client) -> None: + sync_client.batch( + commands=[ + PublishMethod( + channel="personal:1", + data={"data": "Second data"}, + ), + PublishMethod( + channel="personal:2", + data={"data": "First data"}, + ), + BroadcastMethod( + channels=["personal:1", "personal:2"], + data={"data": "Third data"}, + ), + PresenceMethod( + channel="personal:1", + ), + ] + ) From 20838613899aedaf2abb8b639c6a95ec2b05c247 Mon Sep 17 00:00:00 2001 From: KatantDev Date: Fri, 2 Feb 2024 00:52:20 +1000 Subject: [PATCH 11/55] feat: new exception handlers --- cent/client/session/aiohttp.py | 11 +---------- cent/client/session/base.py | 21 ++++++++++++--------- cent/client/session/requests.py | 11 +---------- cent/exceptions.py | 18 ++++++++++++++++-- pyproject.toml | 1 + tests/test_async_validation.py | 13 +++++++++++++ tests/test_sync_validation.py | 13 +++++++++++++ 7 files changed, 57 insertions(+), 31 deletions(-) diff --git a/cent/client/session/aiohttp.py b/cent/client/session/aiohttp.py index 29614f1..10354c5 100644 --- a/cent/client/session/aiohttp.py +++ b/cent/client/session/aiohttp.py @@ -2,10 +2,7 @@ from typing import Optional, TYPE_CHECKING, cast, Any from aiohttp import ClientSession, ClientError -from aiohttp.hdrs import USER_AGENT, CONTENT_TYPE -from aiohttp.http import SERVER_SOFTWARE -from cent.__meta__ import __version__ from cent.client.session.base_async import BaseAsyncSession from cent.methods.base import CentMethod, CentType from cent.exceptions import CentNetworkError @@ -22,13 +19,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: async def _create_session(self) -> ClientSession: if self._session is None or self._session.closed: - self._session = ClientSession( - headers={ - USER_AGENT: f"{SERVER_SOFTWARE} pycent/{__version__}", - CONTENT_TYPE: "application/json", - "X-Centrifugo-Error-Mode": "transport", - }, - ) + self._session = ClientSession(headers=self._headers) return self._session diff --git a/cent/client/session/base.py b/cent/client/session/base.py index f81b566..a5b559d 100644 --- a/cent/client/session/base.py +++ b/cent/client/session/base.py @@ -2,10 +2,11 @@ from http import HTTPStatus from typing import Final, TYPE_CHECKING, Callable, Any, Union, Dict, List -from pydantic import ValidationError, TypeAdapter +from aiohttp.http import SERVER_SOFTWARE +from pydantic import ValidationError, TypeAdapter, __version__ -from cent.exceptions import ClientDecodeError, DetailedAPIError, InvalidApiKeyError -from cent.methods.base import CentMethod, CentType, Response, Error +from cent.exceptions import ClientDecodeError, APIError, InvalidApiKeyError, TransportError +from cent.methods.base import CentMethod, CentType, Response from cent.methods.batch import BatchMethod try: @@ -43,6 +44,10 @@ def __init__( self._base_url = base_url self.json_loads = json_loads self._timeout = timeout + self._headers = { + "User-Agent": f"{SERVER_SOFTWARE} pycent/{__version__}", + "Content-Type": "application/json", + } @staticmethod def get_batch_json_data(method: BatchMethod) -> Dict[str, List[Dict[str, Any]]]: @@ -86,12 +91,10 @@ def check_response( except Exception as err: raise ClientDecodeError from err - if not (HTTPStatus.OK <= status_code <= HTTPStatus.IM_USED): - error = Error.model_validate(json_data) - raise DetailedAPIError( + if status_code != HTTPStatus.OK: + raise TransportError( method=method, - code=error.code, - message=error.message, + status_code=status_code, ) if isinstance(method, BatchMethod): @@ -107,7 +110,7 @@ def check_response( raise ClientDecodeError from err if response.error: - raise DetailedAPIError( + raise APIError( method=method, code=response.error.code, message=response.error.message, diff --git a/cent/client/session/requests.py b/cent/client/session/requests.py index 5c15947..13eb756 100644 --- a/cent/client/session/requests.py +++ b/cent/client/session/requests.py @@ -1,11 +1,8 @@ from typing import Optional, TYPE_CHECKING, cast, Any import requests -from aiohttp.hdrs import USER_AGENT, CONTENT_TYPE -from aiohttp.http import SERVER_SOFTWARE from requests import Session -from cent.__meta__ import __version__ from cent.methods.base import CentMethod, CentType from cent.client.session.base_sync import BaseSyncSession from cent.exceptions import CentNetworkError @@ -19,13 +16,7 @@ class RequestsSession(BaseSyncSession): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self._session = Session() - self._session.headers.update( - { - USER_AGENT: f"{SERVER_SOFTWARE} pycent/{__version__}", - CONTENT_TYPE: "application/json", - "X-Centrifugo-Error-Mode": "transport", - } - ) + self._session.headers.update(self._headers) def close(self) -> None: if self._session is not None: diff --git a/cent/exceptions.py b/cent/exceptions.py index b71c80f..4c1126f 100644 --- a/cent/exceptions.py +++ b/cent/exceptions.py @@ -34,9 +34,9 @@ class InvalidApiKeyError(CentError): """ -class DetailedAPIError(CentError): +class APIError(CentError): """ - DetailedAPIError raised when response from Centrifugo contains any error + APIError raised when response from Centrifugo contains any error as a result of API command execution. """ @@ -50,3 +50,17 @@ def __str__(self) -> str: def __repr__(self) -> str: return f"{type(self).__name__}('{self}')" + + +class TransportError(CentError): + """TransportError raised when returns non-200 status code.""" + + def __init__(self, method: CentMethod[CentType], status_code: int): + self.method = method + self.status_code = status_code + + def __str__(self) -> str: + return f"Transport error - {self.status_code}" + + def __repr__(self) -> str: + return f"{type(self).__name__}('{self}')" diff --git a/pyproject.toml b/pyproject.toml index d3a1230..aaaa5a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,6 +78,7 @@ ignore = [ [tool.ruff.per-file-ignores] "cent/types/*" = ["E501"] "cent/methods/*" = ["E501"] +"tests/*" = ["S101"] [tool.mypy] strict = true diff --git a/tests/test_async_validation.py b/tests/test_async_validation.py index bcf399a..30c65de 100644 --- a/tests/test_async_validation.py +++ b/tests/test_async_validation.py @@ -1,6 +1,7 @@ from base64 import b64encode from cent import AsyncClient +from cent.exceptions import APIError from cent.methods import PublishMethod, BroadcastMethod, PresenceMethod from cent.types import StreamPosition, Disconnect @@ -125,3 +126,15 @@ async def test_batch(async_client: AsyncClient) -> None: ), ] ) + + +async def test_error_publish(async_client: AsyncClient) -> None: + try: + await async_client.publish( + "undefined_channel:123", + {"data": "data"}, + ) + except APIError: + assert True + else: + raise AssertionError diff --git a/tests/test_sync_validation.py b/tests/test_sync_validation.py index e9b3002..683f60c 100644 --- a/tests/test_sync_validation.py +++ b/tests/test_sync_validation.py @@ -1,5 +1,6 @@ from base64 import b64encode from cent import Client +from cent.exceptions import APIError from cent.methods import PublishMethod, BroadcastMethod, PresenceMethod from cent.types import StreamPosition, Disconnect @@ -124,3 +125,15 @@ def test_batch(sync_client: Client) -> None: ), ] ) + + +def test_error_publish(sync_client: Client) -> None: + try: + sync_client.publish( + "undefined_channel:123", + {"data": "data"}, + ) + except APIError: + assert True + else: + raise AssertionError From bc75805dd643fdad13089de5e60de78c71e16752 Mon Sep 17 00:00:00 2001 From: KatantDev Date: Fri, 2 Feb 2024 17:26:23 +1000 Subject: [PATCH 12/55] feat: grpc client --- benchmarks/test_publish.py | 7 +- cent/__init__.py | 3 + cent/centrifugal/__init__.py | 0 cent/centrifugal/centrifugo/__init__.py | 0 cent/centrifugal/centrifugo/api/__init__.py | 2201 +++++++++++++++++ cent/client/__init__.py | 2 + cent/client/grpc_client.py | 222 ++ cent/client/session/base_async.py | 9 +- cent/client/session/base_sync.py | 9 +- cent/client/session/grpc.py | 82 + cent/methods/base.py | 30 +- cent/methods/batch.py | 3 + cent/methods/bool_value.py | 11 + cent/methods/broadcast.py | 15 +- .../channel_options_override.py | 10 +- cent/methods/channels.py | 3 + cent/methods/disconnect.py | 3 + cent/methods/disconnect_data.py | 14 + cent/methods/history.py | 3 + cent/methods/history_remove.py | 3 + cent/methods/info.py | 3 + cent/methods/presence.py | 7 + cent/methods/presence_stats.py | 3 + cent/methods/publish.py | 14 +- cent/methods/refresh.py | 3 + cent/methods/stream_position.py | 13 + cent/methods/subscribe.py | 23 +- cent/methods/unsubscribe.py | 3 + cent/protos/apiproto.proto | 879 +++++++ cent/types/__init__.py | 8 +- cent/types/bool_value.py | 8 - cent/types/channel_info_result.py | 4 +- cent/types/disconnect.py | 10 - cent/types/metrics_result.py | 4 +- cent/types/node_result.py | 14 +- cent/types/presence_stats_result.py | 6 +- cent/types/process_result.py | 4 +- cent/types/publication_result.py | 4 +- cent/types/stream_position.py | 10 - poetry.lock | 427 +++- pyproject.toml | 16 +- tests/conftest.py | 14 +- tests/test_async_validation.py | 10 +- tests/test_grpc_validation.py | 123 + tests/test_sync_validation.py | 11 +- 45 files changed, 4167 insertions(+), 84 deletions(-) create mode 100644 cent/centrifugal/__init__.py create mode 100644 cent/centrifugal/centrifugo/__init__.py create mode 100644 cent/centrifugal/centrifugo/api/__init__.py create mode 100644 cent/client/grpc_client.py create mode 100644 cent/client/session/grpc.py create mode 100644 cent/methods/bool_value.py rename cent/{types => methods}/channel_options_override.py (63%) create mode 100644 cent/methods/disconnect_data.py create mode 100644 cent/methods/stream_position.py create mode 100644 cent/protos/apiproto.proto delete mode 100644 cent/types/bool_value.py delete mode 100644 cent/types/disconnect.py delete mode 100644 cent/types/stream_position.py create mode 100644 tests/test_grpc_validation.py diff --git a/benchmarks/test_publish.py b/benchmarks/test_publish.py index f4d328d..4684216 100644 --- a/benchmarks/test_publish.py +++ b/benchmarks/test_publish.py @@ -1,7 +1,9 @@ +from typing import Union + import pytest from benchmarks.conftest import BenchmarkDecoratorType -from cent import AsyncClient, Client +from cent import AsyncClient, Client, GrpcClient def sync_requests(client: Client) -> None: @@ -12,8 +14,9 @@ def sync_requests(client: Client) -> None: ) -async def async_requests(client: AsyncClient) -> None: +async def async_requests(client: Union[GrpcClient, AsyncClient]) -> None: for j in range(1000): + print(j) await client.publish( channel=f"personal:{j}", data={"message": "Hello world!"}, diff --git a/cent/__init__.py b/cent/__init__.py index fa4a41f..794bffc 100644 --- a/cent/__init__.py +++ b/cent/__init__.py @@ -4,6 +4,7 @@ from .client import ( Client, AsyncClient, + GrpcClient, BaseSession, BaseAsyncSession, BaseSyncSession, @@ -22,6 +23,7 @@ "__version__", "types", "methods", + "exceptions", "Client", "AsyncClient", "BaseSession", @@ -29,4 +31,5 @@ "BaseSyncSession", "RequestsSession", "AiohttpSession", + "GrpcClient", ) diff --git a/cent/centrifugal/__init__.py b/cent/centrifugal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cent/centrifugal/centrifugo/__init__.py b/cent/centrifugal/centrifugo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cent/centrifugal/centrifugo/api/__init__.py b/cent/centrifugal/centrifugo/api/__init__.py new file mode 100644 index 0000000..991413f --- /dev/null +++ b/cent/centrifugal/centrifugo/api/__init__.py @@ -0,0 +1,2201 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# sources: cent/protos/apiproto.proto +# plugin: python-betterproto +# This file has been @generated + +from dataclasses import dataclass +from typing import ( + TYPE_CHECKING, + Dict, + List, + Optional, +) + +import betterproto +import grpclib +from betterproto.grpc.grpclib_server import ServiceBase +import grpclib.server + +if TYPE_CHECKING: + from betterproto.grpc.grpclib_client import MetadataLike + from grpclib.metadata import Deadline + + +class CommandMethodType(betterproto.Enum): + PUBLISH = 0 + BROADCAST = 1 + UNSUBSCRIBE = 2 + DISCONNECT = 3 + PRESENCE = 4 + PRESENCE_STATS = 5 + HISTORY = 6 + HISTORY_REMOVE = 7 + CHANNELS = 8 + INFO = 9 + RPC = 10 + SUBSCRIBE = 11 + REFRESH = 12 + CONNECTIONS = 14 + UPDATE_USER_STATUS = 15 + GET_USER_STATUS = 16 + DELETE_USER_STATUS = 17 + BLOCK_USER = 18 + UNBLOCK_USER = 19 + REVOKE_TOKEN = 20 + INVALIDATE_USER_TOKENS = 21 + DEVICE_REGISTER = 22 + DEVICE_UPDATE = 23 + DEVICE_REMOVE = 24 + DEVICE_LIST = 25 + DEVICE_TOPIC_LIST = 26 + DEVICE_TOPIC_UPDATE = 27 + USER_TOPIC_LIST = 28 + USER_TOPIC_UPDATE = 29 + SEND_PUSH_NOTIFICATION = 30 + UPDATE_PUSH_STATUS = 31 + CANCEL_PUSH = 32 + RATE_LIMIT = 47 + + +@dataclass(eq=False, repr=False) +class Command(betterproto.Message): + id: int = betterproto.uint32_field(1) + method: "CommandMethodType" = betterproto.enum_field(2) + params: bytes = betterproto.bytes_field(3) + publish: "PublishRequest" = betterproto.message_field(4) + broadcast: "BroadcastRequest" = betterproto.message_field(5) + subscribe: "SubscribeRequest" = betterproto.message_field(6) + unsubscribe: "UnsubscribeRequest" = betterproto.message_field(7) + disconnect: "DisconnectRequest" = betterproto.message_field(8) + presence: "PresenceRequest" = betterproto.message_field(9) + presence_stats: "PresenceStatsRequest" = betterproto.message_field(10) + history: "HistoryRequest" = betterproto.message_field(11) + history_remove: "HistoryRemoveRequest" = betterproto.message_field(12) + info: "InfoRequest" = betterproto.message_field(13) + rpc: "RpcRequest" = betterproto.message_field(14) + refresh: "RefreshRequest" = betterproto.message_field(15) + channels: "ChannelsRequest" = betterproto.message_field(16) + connections: "ConnectionsRequest" = betterproto.message_field(17) + update_user_status: "UpdateUserStatusRequest" = betterproto.message_field(18) + get_user_status: "GetUserStatusRequest" = betterproto.message_field(19) + delete_user_status: "DeleteUserStatusRequest" = betterproto.message_field(20) + block_user: "BlockUserRequest" = betterproto.message_field(21) + unblock_user: "UnblockUserRequest" = betterproto.message_field(22) + revoke_token: "RevokeTokenRequest" = betterproto.message_field(23) + invalidate_user_tokens: "InvalidateUserTokensRequest" = betterproto.message_field(24) + device_register: "DeviceRegisterRequest" = betterproto.message_field(25) + device_update: "DeviceUpdateRequest" = betterproto.message_field(26) + device_remove: "DeviceRemoveRequest" = betterproto.message_field(27) + device_list: "DeviceListRequest" = betterproto.message_field(28) + device_topic_list: "DeviceTopicListRequest" = betterproto.message_field(29) + device_topic_update: "DeviceTopicUpdateRequest" = betterproto.message_field(30) + user_topic_list: "UserTopicListRequest" = betterproto.message_field(31) + user_topic_update: "UserTopicUpdateRequest" = betterproto.message_field(32) + send_push_notification: "SendPushNotificationRequest" = betterproto.message_field(33) + update_push_status: "UpdatePushStatusRequest" = betterproto.message_field(34) + cancel_push: "CancelPushRequest" = betterproto.message_field(35) + rate_limit: "RateLimitRequest" = betterproto.message_field(50) + + +@dataclass(eq=False, repr=False) +class Error(betterproto.Message): + code: int = betterproto.uint32_field(1) + message: str = betterproto.string_field(2) + + +@dataclass(eq=False, repr=False) +class Reply(betterproto.Message): + id: int = betterproto.uint32_field(1) + error: "Error" = betterproto.message_field(2) + result: bytes = betterproto.bytes_field(3) + publish: "PublishResult" = betterproto.message_field(4) + broadcast: "BroadcastResult" = betterproto.message_field(5) + subscribe: "SubscribeResult" = betterproto.message_field(6) + unsubscribe: "UnsubscribeResult" = betterproto.message_field(7) + disconnect: "DisconnectResult" = betterproto.message_field(8) + presence: "PresenceResult" = betterproto.message_field(9) + presence_stats: "PresenceStatsResult" = betterproto.message_field(10) + history: "HistoryResult" = betterproto.message_field(11) + history_remove: "HistoryRemoveResult" = betterproto.message_field(12) + info: "InfoResult" = betterproto.message_field(13) + rpc: "RpcResult" = betterproto.message_field(14) + refresh: "RefreshResult" = betterproto.message_field(15) + channels: "ChannelsResult" = betterproto.message_field(16) + connections: "ConnectionsResult" = betterproto.message_field(17) + update_user_status: "UpdateUserStatusResult" = betterproto.message_field(18) + get_user_status: "GetUserStatusResult" = betterproto.message_field(19) + delete_user_status: "DeleteUserStatusResult" = betterproto.message_field(20) + block_user: "BlockUserResult" = betterproto.message_field(21) + unblock_user: "UnblockUserResult" = betterproto.message_field(22) + revoke_token: "RevokeTokenResult" = betterproto.message_field(23) + invalidate_user_tokens: "InvalidateUserTokensResult" = betterproto.message_field(24) + device_register: "DeviceRegisterResult" = betterproto.message_field(25) + device_update: "DeviceUpdateResult" = betterproto.message_field(26) + device_remove: "DeviceRemoveResult" = betterproto.message_field(27) + device_list: "DeviceListResult" = betterproto.message_field(28) + device_topic_list: "DeviceTopicListResult" = betterproto.message_field(29) + device_topic_update: "DeviceTopicUpdateResult" = betterproto.message_field(30) + user_topic_list: "UserTopicListResult" = betterproto.message_field(31) + user_topic_update: "UserTopicUpdateResult" = betterproto.message_field(32) + send_push_notification: "SendPushNotificationResult" = betterproto.message_field(33) + update_push_status: "UpdatePushStatusResult" = betterproto.message_field(34) + cancel_push: "CancelPushResult" = betterproto.message_field(35) + rate_limit: "RateLimitResult" = betterproto.message_field(50) + + +@dataclass(eq=False, repr=False) +class BoolValue(betterproto.Message): + value: bool = betterproto.bool_field(1) + + +@dataclass(eq=False, repr=False) +class Int32Value(betterproto.Message): + value: int = betterproto.int32_field(1) + + +@dataclass(eq=False, repr=False) +class SubscribeOptionOverride(betterproto.Message): + presence: "BoolValue" = betterproto.message_field(1) + join_leave: "BoolValue" = betterproto.message_field(2) + force_recovery: "BoolValue" = betterproto.message_field(3) + force_positioning: "BoolValue" = betterproto.message_field(4) + force_push_join_leave: "BoolValue" = betterproto.message_field(5) + + +@dataclass(eq=False, repr=False) +class BatchRequest(betterproto.Message): + commands: List["Command"] = betterproto.message_field(1) + parallel: bool = betterproto.bool_field(2) + + +@dataclass(eq=False, repr=False) +class BatchResponse(betterproto.Message): + replies: List["Reply"] = betterproto.message_field(1) + + +@dataclass(eq=False, repr=False) +class PublishRequest(betterproto.Message): + channel: str = betterproto.string_field(1) + data: bytes = betterproto.bytes_field(2) + b64_data: str = betterproto.string_field(3) + skip_history: bool = betterproto.bool_field(4) + tags: Dict[str, str] = betterproto.map_field( + 5, betterproto.TYPE_STRING, betterproto.TYPE_STRING + ) + idempotency_key: str = betterproto.string_field(6) + + +@dataclass(eq=False, repr=False) +class PublishResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "PublishResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class PublishResult(betterproto.Message): + offset: int = betterproto.uint64_field(1) + epoch: str = betterproto.string_field(2) + + +@dataclass(eq=False, repr=False) +class BroadcastRequest(betterproto.Message): + channels: List[str] = betterproto.string_field(1) + data: bytes = betterproto.bytes_field(2) + b64_data: str = betterproto.string_field(3) + skip_history: bool = betterproto.bool_field(4) + tags: Dict[str, str] = betterproto.map_field( + 5, betterproto.TYPE_STRING, betterproto.TYPE_STRING + ) + idempotency_key: str = betterproto.string_field(6) + + +@dataclass(eq=False, repr=False) +class BroadcastResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "BroadcastResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class BroadcastResult(betterproto.Message): + responses: List["PublishResponse"] = betterproto.message_field(1) + + +@dataclass(eq=False, repr=False) +class SubscribeRequest(betterproto.Message): + channel: str = betterproto.string_field(1) + user: str = betterproto.string_field(2) + expire_at: int = betterproto.int64_field(3) + info: bytes = betterproto.bytes_field(4) + b64_info: str = betterproto.string_field(5) + client: str = betterproto.string_field(6) + data: bytes = betterproto.bytes_field(7) + b64_data: str = betterproto.string_field(8) + recover_since: "StreamPosition" = betterproto.message_field(9) + override: "SubscribeOptionOverride" = betterproto.message_field(10) + session: str = betterproto.string_field(11) + + +@dataclass(eq=False, repr=False) +class SubscribeResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "SubscribeResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class SubscribeResult(betterproto.Message): + pass + + +@dataclass(eq=False, repr=False) +class UnsubscribeRequest(betterproto.Message): + channel: str = betterproto.string_field(1) + user: str = betterproto.string_field(2) + client: str = betterproto.string_field(3) + session: str = betterproto.string_field(4) + + +@dataclass(eq=False, repr=False) +class UnsubscribeResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "UnsubscribeResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class UnsubscribeResult(betterproto.Message): + pass + + +@dataclass(eq=False, repr=False) +class Disconnect(betterproto.Message): + code: int = betterproto.uint32_field(1) + reason: str = betterproto.string_field(2) + + +@dataclass(eq=False, repr=False) +class DisconnectRequest(betterproto.Message): + user: str = betterproto.string_field(1) + disconnect: "Disconnect" = betterproto.message_field(2) + client: str = betterproto.string_field(3) + whitelist: List[str] = betterproto.string_field(4) + session: str = betterproto.string_field(5) + + +@dataclass(eq=False, repr=False) +class DisconnectResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "DisconnectResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class DisconnectResult(betterproto.Message): + pass + + +@dataclass(eq=False, repr=False) +class PresenceRequest(betterproto.Message): + channel: str = betterproto.string_field(1) + + +@dataclass(eq=False, repr=False) +class PresenceResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "PresenceResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class ClientInfo(betterproto.Message): + user: str = betterproto.string_field(1) + client: str = betterproto.string_field(2) + conn_info: bytes = betterproto.bytes_field(3) + chan_info: bytes = betterproto.bytes_field(4) + + +@dataclass(eq=False, repr=False) +class PresenceResult(betterproto.Message): + presence: Dict[str, "ClientInfo"] = betterproto.map_field( + 1, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE + ) + + +@dataclass(eq=False, repr=False) +class PresenceStatsRequest(betterproto.Message): + channel: str = betterproto.string_field(1) + + +@dataclass(eq=False, repr=False) +class PresenceStatsResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "PresenceStatsResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class PresenceStatsResult(betterproto.Message): + num_clients: int = betterproto.uint32_field(1) + num_users: int = betterproto.uint32_field(2) + + +@dataclass(eq=False, repr=False) +class StreamPosition(betterproto.Message): + offset: int = betterproto.uint64_field(1) + epoch: str = betterproto.string_field(2) + + +@dataclass(eq=False, repr=False) +class HistoryRequest(betterproto.Message): + channel: str = betterproto.string_field(1) + limit: int = betterproto.int32_field(2) + since: "StreamPosition" = betterproto.message_field(3) + reverse: bool = betterproto.bool_field(4) + + +@dataclass(eq=False, repr=False) +class HistoryResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "HistoryResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class Publication(betterproto.Message): + data: bytes = betterproto.bytes_field(2) + """Removed: string uid = 1;""" + + info: "ClientInfo" = betterproto.message_field(3) + offset: int = betterproto.uint64_field(4) + tags: Dict[str, str] = betterproto.map_field( + 5, betterproto.TYPE_STRING, betterproto.TYPE_STRING + ) + + +@dataclass(eq=False, repr=False) +class HistoryResult(betterproto.Message): + publications: List["Publication"] = betterproto.message_field(1) + epoch: str = betterproto.string_field(2) + offset: int = betterproto.uint64_field(3) + + +@dataclass(eq=False, repr=False) +class HistoryRemoveRequest(betterproto.Message): + channel: str = betterproto.string_field(1) + + +@dataclass(eq=False, repr=False) +class HistoryRemoveResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "HistoryRemoveResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class HistoryRemoveResult(betterproto.Message): + pass + + +@dataclass(eq=False, repr=False) +class InfoRequest(betterproto.Message): + pass + + +@dataclass(eq=False, repr=False) +class InfoResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "InfoResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class InfoResult(betterproto.Message): + nodes: List["NodeResult"] = betterproto.message_field(1) + + +@dataclass(eq=False, repr=False) +class RpcRequest(betterproto.Message): + method: str = betterproto.string_field(1) + params: bytes = betterproto.bytes_field(2) + + +@dataclass(eq=False, repr=False) +class RpcResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "RpcResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class RpcResult(betterproto.Message): + data: bytes = betterproto.bytes_field(1) + + +@dataclass(eq=False, repr=False) +class RefreshRequest(betterproto.Message): + user: str = betterproto.string_field(1) + client: str = betterproto.string_field(2) + expired: bool = betterproto.bool_field(3) + expire_at: int = betterproto.int64_field(4) + info: bytes = betterproto.bytes_field(5) + session: str = betterproto.string_field(6) + + +@dataclass(eq=False, repr=False) +class RefreshResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "RefreshResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class RefreshResult(betterproto.Message): + pass + + +@dataclass(eq=False, repr=False) +class NodeResult(betterproto.Message): + uid: str = betterproto.string_field(1) + name: str = betterproto.string_field(2) + version: str = betterproto.string_field(3) + num_clients: int = betterproto.uint32_field(4) + num_users: int = betterproto.uint32_field(5) + num_channels: int = betterproto.uint32_field(6) + uptime: int = betterproto.uint32_field(7) + metrics: "Metrics" = betterproto.message_field(8) + process: "Process" = betterproto.message_field(9) + num_subs: int = betterproto.uint32_field(10) + + +@dataclass(eq=False, repr=False) +class Metrics(betterproto.Message): + interval: float = betterproto.double_field(1) + items: Dict[str, float] = betterproto.map_field( + 2, betterproto.TYPE_STRING, betterproto.TYPE_DOUBLE + ) + + +@dataclass(eq=False, repr=False) +class Process(betterproto.Message): + cpu: float = betterproto.double_field(1) + rss: int = betterproto.int64_field(2) + + +@dataclass(eq=False, repr=False) +class ChannelsRequest(betterproto.Message): + pattern: str = betterproto.string_field(1) + + +@dataclass(eq=False, repr=False) +class ChannelsResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "ChannelsResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class ChannelsResult(betterproto.Message): + channels: Dict[str, "ChannelInfo"] = betterproto.map_field( + 1, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE + ) + + +@dataclass(eq=False, repr=False) +class ChannelInfo(betterproto.Message): + num_clients: int = betterproto.uint32_field(1) + + +@dataclass(eq=False, repr=False) +class ConnectionsRequest(betterproto.Message): + user: str = betterproto.string_field(1) + expression: str = betterproto.string_field(2) + + +@dataclass(eq=False, repr=False) +class ConnectionsResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "ConnectionsResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class ConnectionsResult(betterproto.Message): + connections: Dict[str, "ConnectionInfo"] = betterproto.map_field( + 1, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE + ) + + +@dataclass(eq=False, repr=False) +class ConnectionInfo(betterproto.Message): + app_name: str = betterproto.string_field(1) + app_version: str = betterproto.string_field(2) + transport: str = betterproto.string_field(3) + protocol: str = betterproto.string_field(4) + user: str = betterproto.string_field(8) + """5-7 dropped for backwards compatibility.""" + + state: "ConnectionState" = betterproto.message_field(9) + + +@dataclass(eq=False, repr=False) +class ConnectionState(betterproto.Message): + channels: Dict[str, "ChannelContext"] = betterproto.map_field( + 1, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE + ) + connection_token: "ConnectionTokenInfo" = betterproto.message_field(2) + subscription_tokens: Dict[str, "SubscriptionTokenInfo"] = betterproto.map_field( + 3, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE + ) + meta: bytes = betterproto.bytes_field(4) + + +@dataclass(eq=False, repr=False) +class ChannelContext(betterproto.Message): + source: int = betterproto.uint32_field(1) + + +@dataclass(eq=False, repr=False) +class ConnectionTokenInfo(betterproto.Message): + uid: str = betterproto.string_field(1) + issued_at: int = betterproto.int64_field(2) + + +@dataclass(eq=False, repr=False) +class SubscriptionTokenInfo(betterproto.Message): + uid: str = betterproto.string_field(1) + issued_at: int = betterproto.int64_field(2) + + +@dataclass(eq=False, repr=False) +class UpdateUserStatusRequest(betterproto.Message): + users: List[str] = betterproto.string_field(1) + state: str = betterproto.string_field(2) + + +@dataclass(eq=False, repr=False) +class UpdateUserStatusResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "UpdateUserStatusResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class UpdateUserStatusResult(betterproto.Message): + pass + + +@dataclass(eq=False, repr=False) +class GetUserStatusRequest(betterproto.Message): + users: List[str] = betterproto.string_field(1) + + +@dataclass(eq=False, repr=False) +class GetUserStatusResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "GetUserStatusResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class GetUserStatusResult(betterproto.Message): + statuses: List["UserStatus"] = betterproto.message_field(1) + + +@dataclass(eq=False, repr=False) +class UserStatus(betterproto.Message): + user: str = betterproto.string_field(1) + active: int = betterproto.int64_field(2) + online: int = betterproto.int64_field(3) + state: str = betterproto.string_field(4) + + +@dataclass(eq=False, repr=False) +class DeleteUserStatusRequest(betterproto.Message): + users: List[str] = betterproto.string_field(1) + + +@dataclass(eq=False, repr=False) +class DeleteUserStatusResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "DeleteUserStatusResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class DeleteUserStatusResult(betterproto.Message): + pass + + +@dataclass(eq=False, repr=False) +class BlockUserRequest(betterproto.Message): + expire_at: int = betterproto.int64_field(1) + user: str = betterproto.string_field(2) + + +@dataclass(eq=False, repr=False) +class BlockUserResult(betterproto.Message): + pass + + +@dataclass(eq=False, repr=False) +class BlockUserResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "BlockUserResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class UnblockUserRequest(betterproto.Message): + user: str = betterproto.string_field(1) + + +@dataclass(eq=False, repr=False) +class UnblockUserResult(betterproto.Message): + pass + + +@dataclass(eq=False, repr=False) +class UnblockUserResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "UnblockUserResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class RevokeTokenRequest(betterproto.Message): + expire_at: int = betterproto.int64_field(1) + uid: str = betterproto.string_field(2) + + +@dataclass(eq=False, repr=False) +class RevokeTokenResult(betterproto.Message): + pass + + +@dataclass(eq=False, repr=False) +class RevokeTokenResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "RevokeTokenResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class InvalidateUserTokensRequest(betterproto.Message): + expire_at: int = betterproto.int64_field(1) + user: str = betterproto.string_field(2) + issued_before: int = betterproto.int64_field(3) + channel: str = betterproto.string_field(4) + + +@dataclass(eq=False, repr=False) +class InvalidateUserTokensResult(betterproto.Message): + pass + + +@dataclass(eq=False, repr=False) +class InvalidateUserTokensResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "InvalidateUserTokensResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class DeviceRegisterRequest(betterproto.Message): + id: str = betterproto.string_field(1) + provider: str = betterproto.string_field(2) + token: str = betterproto.string_field(3) + platform: str = betterproto.string_field(4) + user: str = betterproto.string_field(5) + meta: Dict[str, str] = betterproto.map_field( + 6, betterproto.TYPE_STRING, betterproto.TYPE_STRING + ) + topics: List[str] = betterproto.string_field(7) + + +@dataclass(eq=False, repr=False) +class DeviceUpdateRequest(betterproto.Message): + ids: List[str] = betterproto.string_field(1) + users: List[str] = betterproto.string_field(2) + user_update: "DeviceUserUpdate" = betterproto.message_field(4) + meta_update: "DeviceMetaUpdate" = betterproto.message_field(5) + topics_update: "DeviceTopicsUpdate" = betterproto.message_field(6) + + +@dataclass(eq=False, repr=False) +class DeviceRemoveRequest(betterproto.Message): + ids: List[str] = betterproto.string_field(1) + users: List[str] = betterproto.string_field(2) + + +@dataclass(eq=False, repr=False) +class DeviceUserUpdate(betterproto.Message): + user: str = betterproto.string_field(1) + + +@dataclass(eq=False, repr=False) +class DeviceMetaUpdate(betterproto.Message): + meta: Dict[str, str] = betterproto.map_field( + 1, betterproto.TYPE_STRING, betterproto.TYPE_STRING + ) + + +@dataclass(eq=False, repr=False) +class DeviceTopicsUpdate(betterproto.Message): + op: str = betterproto.string_field(1) + topics: List[str] = betterproto.string_field(2) + + +@dataclass(eq=False, repr=False) +class DeviceFilter(betterproto.Message): + ids: List[str] = betterproto.string_field(1) + users: List[str] = betterproto.string_field(2) + topics: List[str] = betterproto.string_field(3) + providers: List[str] = betterproto.string_field(4) + platforms: List[str] = betterproto.string_field(5) + + +@dataclass(eq=False, repr=False) +class DeviceListRequest(betterproto.Message): + filter: "DeviceFilter" = betterproto.message_field(1) + include_total_count: bool = betterproto.bool_field(2) + include_meta: bool = betterproto.bool_field(3) + include_topics: bool = betterproto.bool_field(4) + cursor: str = betterproto.string_field(10) + limit: int = betterproto.int32_field(11) + + +@dataclass(eq=False, repr=False) +class DeviceTopicFilter(betterproto.Message): + device_ids: List[str] = betterproto.string_field(1) + device_providers: List[str] = betterproto.string_field(2) + device_platforms: List[str] = betterproto.string_field(3) + device_users: List[str] = betterproto.string_field(4) + topics: List[str] = betterproto.string_field(5) + topic_prefix: str = betterproto.string_field(6) + + +@dataclass(eq=False, repr=False) +class DeviceTopicListRequest(betterproto.Message): + filter: "DeviceTopicFilter" = betterproto.message_field(1) + include_total_count: bool = betterproto.bool_field(2) + include_device: bool = betterproto.bool_field(3) + cursor: str = betterproto.string_field(10) + limit: int = betterproto.int32_field(11) + + +@dataclass(eq=False, repr=False) +class UserTopicFilter(betterproto.Message): + users: List[str] = betterproto.string_field(1) + topics: List[str] = betterproto.string_field(2) + topic_prefix: str = betterproto.string_field(3) + + +@dataclass(eq=False, repr=False) +class UserTopicListRequest(betterproto.Message): + filter: "UserTopicFilter" = betterproto.message_field(1) + include_total_count: bool = betterproto.bool_field(2) + cursor: str = betterproto.string_field(10) + limit: int = betterproto.int32_field(11) + + +@dataclass(eq=False, repr=False) +class DeviceTopicUpdateRequest(betterproto.Message): + device_id: str = betterproto.string_field(1) + op: str = betterproto.string_field(2) + topics: List[str] = betterproto.string_field(3) + + +@dataclass(eq=False, repr=False) +class UserTopicUpdateRequest(betterproto.Message): + user: str = betterproto.string_field(1) + op: str = betterproto.string_field(2) + topics: List[str] = betterproto.string_field(3) + + +@dataclass(eq=False, repr=False) +class DeviceRegisterResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "DeviceRegisterResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class DeviceUpdateResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "DeviceUpdateResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class DeviceRemoveResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "DeviceRemoveResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class DeviceListResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "DeviceListResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class DeviceTopicListResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "DeviceTopicListResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class UserTopicListResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "UserTopicListResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class DeviceTopicUpdateResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "DeviceTopicUpdateResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class UserTopicUpdateResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "UserTopicUpdateResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class DeviceRegisterResult(betterproto.Message): + id: str = betterproto.string_field(1) + + +@dataclass(eq=False, repr=False) +class DeviceUpdateResult(betterproto.Message): + pass + + +@dataclass(eq=False, repr=False) +class DeviceRemoveResult(betterproto.Message): + pass + + +@dataclass(eq=False, repr=False) +class DeviceListResult(betterproto.Message): + items: List["Device"] = betterproto.message_field(1) + next_cursor: str = betterproto.string_field(2) + total_count: int = betterproto.int64_field(3) + + +@dataclass(eq=False, repr=False) +class Device(betterproto.Message): + id: str = betterproto.string_field(1) + platform: str = betterproto.string_field(2) + provider: str = betterproto.string_field(3) + token: str = betterproto.string_field(4) + user: str = betterproto.string_field(5) + created_at: int = betterproto.int64_field(6) + updated_at: int = betterproto.int64_field(7) + meta: Dict[str, str] = betterproto.map_field( + 10, betterproto.TYPE_STRING, betterproto.TYPE_STRING + ) + topics: List[str] = betterproto.string_field(11) + + +@dataclass(eq=False, repr=False) +class DeviceTopicListResult(betterproto.Message): + items: List["DeviceTopic"] = betterproto.message_field(1) + next_cursor: str = betterproto.string_field(2) + total_count: int = betterproto.int64_field(3) + + +@dataclass(eq=False, repr=False) +class DeviceTopic(betterproto.Message): + id: str = betterproto.string_field(1) + topic: str = betterproto.string_field(2) + device: "Device" = betterproto.message_field(3) + + +@dataclass(eq=False, repr=False) +class UserTopicListResult(betterproto.Message): + items: List["UserTopic"] = betterproto.message_field(1) + next_cursor: str = betterproto.string_field(2) + total_count: int = betterproto.int64_field(3) + + +@dataclass(eq=False, repr=False) +class DeviceTopicUpdateResult(betterproto.Message): + pass + + +@dataclass(eq=False, repr=False) +class UserTopicUpdateResult(betterproto.Message): + pass + + +@dataclass(eq=False, repr=False) +class UserTopic(betterproto.Message): + id: str = betterproto.string_field(1) + user: str = betterproto.string_field(2) + topic: str = betterproto.string_field(3) + + +@dataclass(eq=False, repr=False) +class PushRecipient(betterproto.Message): + filter: "DeviceFilter" = betterproto.message_field(1) + fcm_tokens: List[str] = betterproto.string_field(2) + fcm_topic: str = betterproto.string_field(3) + fcm_condition: str = betterproto.string_field(4) + hms_tokens: List[str] = betterproto.string_field(5) + hms_topic: str = betterproto.string_field(6) + hms_condition: str = betterproto.string_field(7) + apns_tokens: List[str] = betterproto.string_field(8) + + +@dataclass(eq=False, repr=False) +class PushNotification(betterproto.Message): + fcm: "FcmPushNotification" = betterproto.message_field(1) + hms: "HmsPushNotification" = betterproto.message_field(2) + apns: "ApnsPushNotification" = betterproto.message_field(3) + expire_at: int = betterproto.int64_field(5) + + +@dataclass(eq=False, repr=False) +class FcmPushNotification(betterproto.Message): + message: bytes = betterproto.bytes_field(1) + + +@dataclass(eq=False, repr=False) +class HmsPushNotification(betterproto.Message): + message: bytes = betterproto.bytes_field(1) + + +@dataclass(eq=False, repr=False) +class ApnsPushNotification(betterproto.Message): + headers: Dict[str, str] = betterproto.map_field( + 1, betterproto.TYPE_STRING, betterproto.TYPE_STRING + ) + payload: bytes = betterproto.bytes_field(2) + + +@dataclass(eq=False, repr=False) +class SendPushNotificationRequest(betterproto.Message): + recipient: "PushRecipient" = betterproto.message_field(1) + notification: "PushNotification" = betterproto.message_field(2) + uid: str = betterproto.string_field(3) + send_at: int = betterproto.int64_field(4) + + +@dataclass(eq=False, repr=False) +class SendPushNotificationResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "SendPushNotificationResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class SendPushNotificationResult(betterproto.Message): + uid: str = betterproto.string_field(1) + + +@dataclass(eq=False, repr=False) +class UpdatePushStatusRequest(betterproto.Message): + uid: str = betterproto.string_field(1) + status: str = betterproto.string_field(2) + device_id: str = betterproto.string_field(3) + msg_id: str = betterproto.string_field(4) + + +@dataclass(eq=False, repr=False) +class UpdatePushStatusResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "UpdatePushStatusResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class UpdatePushStatusResult(betterproto.Message): + pass + + +@dataclass(eq=False, repr=False) +class CancelPushRequest(betterproto.Message): + uid: str = betterproto.string_field(1) + + +@dataclass(eq=False, repr=False) +class CancelPushResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "CancelPushResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class CancelPushResult(betterproto.Message): + pass + + +@dataclass(eq=False, repr=False) +class RateLimitRequest(betterproto.Message): + pass + + +@dataclass(eq=False, repr=False) +class RateLimitResponse(betterproto.Message): + error: "Error" = betterproto.message_field(1) + result: "RateLimitResult" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class RateLimitResult(betterproto.Message): + pass + + +class CentrifugoApiStub(betterproto.ServiceStub): + async def batch( + self, + batch_request: "BatchRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "BatchResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/Batch", + batch_request, + BatchResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def publish( + self, + publish_request: "PublishRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "PublishResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/Publish", + publish_request, + PublishResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def broadcast( + self, + broadcast_request: "BroadcastRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "BroadcastResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/Broadcast", + broadcast_request, + BroadcastResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def subscribe( + self, + subscribe_request: "SubscribeRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "SubscribeResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/Subscribe", + subscribe_request, + SubscribeResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def unsubscribe( + self, + unsubscribe_request: "UnsubscribeRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "UnsubscribeResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/Unsubscribe", + unsubscribe_request, + UnsubscribeResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def disconnect( + self, + disconnect_request: "DisconnectRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "DisconnectResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/Disconnect", + disconnect_request, + DisconnectResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def presence( + self, + presence_request: "PresenceRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "PresenceResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/Presence", + presence_request, + PresenceResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def presence_stats( + self, + presence_stats_request: "PresenceStatsRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "PresenceStatsResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/PresenceStats", + presence_stats_request, + PresenceStatsResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def history( + self, + history_request: "HistoryRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "HistoryResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/History", + history_request, + HistoryResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def history_remove( + self, + history_remove_request: "HistoryRemoveRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "HistoryRemoveResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/HistoryRemove", + history_remove_request, + HistoryRemoveResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def info( + self, + info_request: "InfoRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "InfoResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/Info", + info_request, + InfoResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def rpc( + self, + rpc_request: "RpcRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "RpcResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/RPC", + rpc_request, + RpcResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def refresh( + self, + refresh_request: "RefreshRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "RefreshResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/Refresh", + refresh_request, + RefreshResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def channels( + self, + channels_request: "ChannelsRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "ChannelsResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/Channels", + channels_request, + ChannelsResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def connections( + self, + connections_request: "ConnectionsRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "ConnectionsResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/Connections", + connections_request, + ConnectionsResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def update_user_status( + self, + update_user_status_request: "UpdateUserStatusRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "UpdateUserStatusResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/UpdateUserStatus", + update_user_status_request, + UpdateUserStatusResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def get_user_status( + self, + get_user_status_request: "GetUserStatusRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "GetUserStatusResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/GetUserStatus", + get_user_status_request, + GetUserStatusResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def delete_user_status( + self, + delete_user_status_request: "DeleteUserStatusRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "DeleteUserStatusResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/DeleteUserStatus", + delete_user_status_request, + DeleteUserStatusResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def block_user( + self, + block_user_request: "BlockUserRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "BlockUserResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/BlockUser", + block_user_request, + BlockUserResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def unblock_user( + self, + unblock_user_request: "UnblockUserRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "UnblockUserResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/UnblockUser", + unblock_user_request, + UnblockUserResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def revoke_token( + self, + revoke_token_request: "RevokeTokenRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "RevokeTokenResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/RevokeToken", + revoke_token_request, + RevokeTokenResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def invalidate_user_tokens( + self, + invalidate_user_tokens_request: "InvalidateUserTokensRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "InvalidateUserTokensResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/InvalidateUserTokens", + invalidate_user_tokens_request, + InvalidateUserTokensResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def device_register( + self, + device_register_request: "DeviceRegisterRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "DeviceRegisterResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/DeviceRegister", + device_register_request, + DeviceRegisterResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def device_update( + self, + device_update_request: "DeviceUpdateRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "DeviceUpdateResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/DeviceUpdate", + device_update_request, + DeviceUpdateResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def device_remove( + self, + device_remove_request: "DeviceRemoveRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "DeviceRemoveResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/DeviceRemove", + device_remove_request, + DeviceRemoveResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def device_list( + self, + device_list_request: "DeviceListRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "DeviceListResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/DeviceList", + device_list_request, + DeviceListResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def device_topic_list( + self, + device_topic_list_request: "DeviceTopicListRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "DeviceTopicListResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/DeviceTopicList", + device_topic_list_request, + DeviceTopicListResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def device_topic_update( + self, + device_topic_update_request: "DeviceTopicUpdateRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "DeviceTopicUpdateResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/DeviceTopicUpdate", + device_topic_update_request, + DeviceTopicUpdateResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def user_topic_list( + self, + user_topic_list_request: "UserTopicListRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "UserTopicListResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/UserTopicList", + user_topic_list_request, + UserTopicListResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def user_topic_update( + self, + user_topic_update_request: "UserTopicUpdateRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "UserTopicUpdateResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/UserTopicUpdate", + user_topic_update_request, + UserTopicUpdateResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def send_push_notification( + self, + send_push_notification_request: "SendPushNotificationRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "SendPushNotificationResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/SendPushNotification", + send_push_notification_request, + SendPushNotificationResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def update_push_status( + self, + update_push_status_request: "UpdatePushStatusRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "UpdatePushStatusResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/UpdatePushStatus", + update_push_status_request, + UpdatePushStatusResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def cancel_push( + self, + cancel_push_request: "CancelPushRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "CancelPushResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/CancelPush", + cancel_push_request, + CancelPushResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + async def rate_limit( + self, + rate_limit_request: "RateLimitRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None, + ) -> "RateLimitResponse": + return await self._unary_unary( + "/centrifugal.centrifugo.api.CentrifugoApi/RateLimit", + rate_limit_request, + RateLimitResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + + +class CentrifugoApiBase(ServiceBase): + async def batch(self, batch_request: "BatchRequest") -> "BatchResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def publish(self, publish_request: "PublishRequest") -> "PublishResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def broadcast(self, broadcast_request: "BroadcastRequest") -> "BroadcastResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def subscribe(self, subscribe_request: "SubscribeRequest") -> "SubscribeResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def unsubscribe( + self, unsubscribe_request: "UnsubscribeRequest" + ) -> "UnsubscribeResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def disconnect(self, disconnect_request: "DisconnectRequest") -> "DisconnectResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def presence(self, presence_request: "PresenceRequest") -> "PresenceResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def presence_stats( + self, presence_stats_request: "PresenceStatsRequest" + ) -> "PresenceStatsResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def history(self, history_request: "HistoryRequest") -> "HistoryResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def history_remove( + self, history_remove_request: "HistoryRemoveRequest" + ) -> "HistoryRemoveResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def info(self, info_request: "InfoRequest") -> "InfoResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def rpc(self, rpc_request: "RpcRequest") -> "RpcResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def refresh(self, refresh_request: "RefreshRequest") -> "RefreshResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def channels(self, channels_request: "ChannelsRequest") -> "ChannelsResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def connections( + self, connections_request: "ConnectionsRequest" + ) -> "ConnectionsResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def update_user_status( + self, update_user_status_request: "UpdateUserStatusRequest" + ) -> "UpdateUserStatusResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def get_user_status( + self, get_user_status_request: "GetUserStatusRequest" + ) -> "GetUserStatusResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def delete_user_status( + self, delete_user_status_request: "DeleteUserStatusRequest" + ) -> "DeleteUserStatusResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def block_user(self, block_user_request: "BlockUserRequest") -> "BlockUserResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def unblock_user( + self, unblock_user_request: "UnblockUserRequest" + ) -> "UnblockUserResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def revoke_token( + self, revoke_token_request: "RevokeTokenRequest" + ) -> "RevokeTokenResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def invalidate_user_tokens( + self, invalidate_user_tokens_request: "InvalidateUserTokensRequest" + ) -> "InvalidateUserTokensResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def device_register( + self, device_register_request: "DeviceRegisterRequest" + ) -> "DeviceRegisterResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def device_update( + self, device_update_request: "DeviceUpdateRequest" + ) -> "DeviceUpdateResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def device_remove( + self, device_remove_request: "DeviceRemoveRequest" + ) -> "DeviceRemoveResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def device_list(self, device_list_request: "DeviceListRequest") -> "DeviceListResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def device_topic_list( + self, device_topic_list_request: "DeviceTopicListRequest" + ) -> "DeviceTopicListResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def device_topic_update( + self, device_topic_update_request: "DeviceTopicUpdateRequest" + ) -> "DeviceTopicUpdateResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def user_topic_list( + self, user_topic_list_request: "UserTopicListRequest" + ) -> "UserTopicListResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def user_topic_update( + self, user_topic_update_request: "UserTopicUpdateRequest" + ) -> "UserTopicUpdateResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def send_push_notification( + self, send_push_notification_request: "SendPushNotificationRequest" + ) -> "SendPushNotificationResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def update_push_status( + self, update_push_status_request: "UpdatePushStatusRequest" + ) -> "UpdatePushStatusResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def cancel_push(self, cancel_push_request: "CancelPushRequest") -> "CancelPushResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def rate_limit(self, rate_limit_request: "RateLimitRequest") -> "RateLimitResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + + async def __rpc_batch( + self, stream: "grpclib.server.Stream[BatchRequest, BatchResponse]" + ) -> None: + request = await stream.recv_message() + response = await self.batch(request) + await stream.send_message(response) + + async def __rpc_publish( + self, stream: "grpclib.server.Stream[PublishRequest, PublishResponse]" + ) -> None: + request = await stream.recv_message() + response = await self.publish(request) + await stream.send_message(response) + + async def __rpc_broadcast( + self, stream: "grpclib.server.Stream[BroadcastRequest, BroadcastResponse]" + ) -> None: + request = await stream.recv_message() + response = await self.broadcast(request) + await stream.send_message(response) + + async def __rpc_subscribe( + self, stream: "grpclib.server.Stream[SubscribeRequest, SubscribeResponse]" + ) -> None: + request = await stream.recv_message() + response = await self.subscribe(request) + await stream.send_message(response) + + async def __rpc_unsubscribe( + self, stream: "grpclib.server.Stream[UnsubscribeRequest, UnsubscribeResponse]" + ) -> None: + request = await stream.recv_message() + response = await self.unsubscribe(request) + await stream.send_message(response) + + async def __rpc_disconnect( + self, stream: "grpclib.server.Stream[DisconnectRequest, DisconnectResponse]" + ) -> None: + request = await stream.recv_message() + response = await self.disconnect(request) + await stream.send_message(response) + + async def __rpc_presence( + self, stream: "grpclib.server.Stream[PresenceRequest, PresenceResponse]" + ) -> None: + request = await stream.recv_message() + response = await self.presence(request) + await stream.send_message(response) + + async def __rpc_presence_stats( + self, + stream: "grpclib.server.Stream[PresenceStatsRequest, PresenceStatsResponse]", + ) -> None: + request = await stream.recv_message() + response = await self.presence_stats(request) + await stream.send_message(response) + + async def __rpc_history( + self, stream: "grpclib.server.Stream[HistoryRequest, HistoryResponse]" + ) -> None: + request = await stream.recv_message() + response = await self.history(request) + await stream.send_message(response) + + async def __rpc_history_remove( + self, + stream: "grpclib.server.Stream[HistoryRemoveRequest, HistoryRemoveResponse]", + ) -> None: + request = await stream.recv_message() + response = await self.history_remove(request) + await stream.send_message(response) + + async def __rpc_info(self, stream: "grpclib.server.Stream[InfoRequest, InfoResponse]") -> None: + request = await stream.recv_message() + response = await self.info(request) + await stream.send_message(response) + + async def __rpc_rpc(self, stream: "grpclib.server.Stream[RpcRequest, RpcResponse]") -> None: + request = await stream.recv_message() + response = await self.rpc(request) + await stream.send_message(response) + + async def __rpc_refresh( + self, stream: "grpclib.server.Stream[RefreshRequest, RefreshResponse]" + ) -> None: + request = await stream.recv_message() + response = await self.refresh(request) + await stream.send_message(response) + + async def __rpc_channels( + self, stream: "grpclib.server.Stream[ChannelsRequest, ChannelsResponse]" + ) -> None: + request = await stream.recv_message() + response = await self.channels(request) + await stream.send_message(response) + + async def __rpc_connections( + self, stream: "grpclib.server.Stream[ConnectionsRequest, ConnectionsResponse]" + ) -> None: + request = await stream.recv_message() + response = await self.connections(request) + await stream.send_message(response) + + async def __rpc_update_user_status( + self, + stream: "grpclib.server.Stream[UpdateUserStatusRequest, UpdateUserStatusResponse]", + ) -> None: + request = await stream.recv_message() + response = await self.update_user_status(request) + await stream.send_message(response) + + async def __rpc_get_user_status( + self, + stream: "grpclib.server.Stream[GetUserStatusRequest, GetUserStatusResponse]", + ) -> None: + request = await stream.recv_message() + response = await self.get_user_status(request) + await stream.send_message(response) + + async def __rpc_delete_user_status( + self, + stream: "grpclib.server.Stream[DeleteUserStatusRequest, DeleteUserStatusResponse]", + ) -> None: + request = await stream.recv_message() + response = await self.delete_user_status(request) + await stream.send_message(response) + + async def __rpc_block_user( + self, stream: "grpclib.server.Stream[BlockUserRequest, BlockUserResponse]" + ) -> None: + request = await stream.recv_message() + response = await self.block_user(request) + await stream.send_message(response) + + async def __rpc_unblock_user( + self, stream: "grpclib.server.Stream[UnblockUserRequest, UnblockUserResponse]" + ) -> None: + request = await stream.recv_message() + response = await self.unblock_user(request) + await stream.send_message(response) + + async def __rpc_revoke_token( + self, stream: "grpclib.server.Stream[RevokeTokenRequest, RevokeTokenResponse]" + ) -> None: + request = await stream.recv_message() + response = await self.revoke_token(request) + await stream.send_message(response) + + async def __rpc_invalidate_user_tokens( + self, + stream: "grpclib.server.Stream[InvalidateUserTokensRequest, InvalidateUserTokensResponse]", + ) -> None: + request = await stream.recv_message() + response = await self.invalidate_user_tokens(request) + await stream.send_message(response) + + async def __rpc_device_register( + self, + stream: "grpclib.server.Stream[DeviceRegisterRequest, DeviceRegisterResponse]", + ) -> None: + request = await stream.recv_message() + response = await self.device_register(request) + await stream.send_message(response) + + async def __rpc_device_update( + self, stream: "grpclib.server.Stream[DeviceUpdateRequest, DeviceUpdateResponse]" + ) -> None: + request = await stream.recv_message() + response = await self.device_update(request) + await stream.send_message(response) + + async def __rpc_device_remove( + self, stream: "grpclib.server.Stream[DeviceRemoveRequest, DeviceRemoveResponse]" + ) -> None: + request = await stream.recv_message() + response = await self.device_remove(request) + await stream.send_message(response) + + async def __rpc_device_list( + self, stream: "grpclib.server.Stream[DeviceListRequest, DeviceListResponse]" + ) -> None: + request = await stream.recv_message() + response = await self.device_list(request) + await stream.send_message(response) + + async def __rpc_device_topic_list( + self, + stream: "grpclib.server.Stream[DeviceTopicListRequest, DeviceTopicListResponse]", + ) -> None: + request = await stream.recv_message() + response = await self.device_topic_list(request) + await stream.send_message(response) + + async def __rpc_device_topic_update( + self, + stream: "grpclib.server.Stream[DeviceTopicUpdateRequest, DeviceTopicUpdateResponse]", + ) -> None: + request = await stream.recv_message() + response = await self.device_topic_update(request) + await stream.send_message(response) + + async def __rpc_user_topic_list( + self, + stream: "grpclib.server.Stream[UserTopicListRequest, UserTopicListResponse]", + ) -> None: + request = await stream.recv_message() + response = await self.user_topic_list(request) + await stream.send_message(response) + + async def __rpc_user_topic_update( + self, + stream: "grpclib.server.Stream[UserTopicUpdateRequest, UserTopicUpdateResponse]", + ) -> None: + request = await stream.recv_message() + response = await self.user_topic_update(request) + await stream.send_message(response) + + async def __rpc_send_push_notification( + self, + stream: "grpclib.server.Stream[SendPushNotificationRequest, SendPushNotificationResponse]", + ) -> None: + request = await stream.recv_message() + response = await self.send_push_notification(request) + await stream.send_message(response) + + async def __rpc_update_push_status( + self, + stream: "grpclib.server.Stream[UpdatePushStatusRequest, UpdatePushStatusResponse]", + ) -> None: + request = await stream.recv_message() + response = await self.update_push_status(request) + await stream.send_message(response) + + async def __rpc_cancel_push( + self, stream: "grpclib.server.Stream[CancelPushRequest, CancelPushResponse]" + ) -> None: + request = await stream.recv_message() + response = await self.cancel_push(request) + await stream.send_message(response) + + async def __rpc_rate_limit( + self, stream: "grpclib.server.Stream[RateLimitRequest, RateLimitResponse]" + ) -> None: + request = await stream.recv_message() + response = await self.rate_limit(request) + await stream.send_message(response) + + def __mapping__(self) -> Dict[str, grpclib.const.Handler]: + return { + "/centrifugal.centrifugo.api.CentrifugoApi/Batch": grpclib.const.Handler( + self.__rpc_batch, + grpclib.const.Cardinality.UNARY_UNARY, + BatchRequest, + BatchResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/Publish": grpclib.const.Handler( + self.__rpc_publish, + grpclib.const.Cardinality.UNARY_UNARY, + PublishRequest, + PublishResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/Broadcast": grpclib.const.Handler( + self.__rpc_broadcast, + grpclib.const.Cardinality.UNARY_UNARY, + BroadcastRequest, + BroadcastResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/Subscribe": grpclib.const.Handler( + self.__rpc_subscribe, + grpclib.const.Cardinality.UNARY_UNARY, + SubscribeRequest, + SubscribeResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/Unsubscribe": grpclib.const.Handler( + self.__rpc_unsubscribe, + grpclib.const.Cardinality.UNARY_UNARY, + UnsubscribeRequest, + UnsubscribeResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/Disconnect": grpclib.const.Handler( + self.__rpc_disconnect, + grpclib.const.Cardinality.UNARY_UNARY, + DisconnectRequest, + DisconnectResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/Presence": grpclib.const.Handler( + self.__rpc_presence, + grpclib.const.Cardinality.UNARY_UNARY, + PresenceRequest, + PresenceResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/PresenceStats": grpclib.const.Handler( + self.__rpc_presence_stats, + grpclib.const.Cardinality.UNARY_UNARY, + PresenceStatsRequest, + PresenceStatsResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/History": grpclib.const.Handler( + self.__rpc_history, + grpclib.const.Cardinality.UNARY_UNARY, + HistoryRequest, + HistoryResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/HistoryRemove": grpclib.const.Handler( + self.__rpc_history_remove, + grpclib.const.Cardinality.UNARY_UNARY, + HistoryRemoveRequest, + HistoryRemoveResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/Info": grpclib.const.Handler( + self.__rpc_info, + grpclib.const.Cardinality.UNARY_UNARY, + InfoRequest, + InfoResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/RPC": grpclib.const.Handler( + self.__rpc_rpc, + grpclib.const.Cardinality.UNARY_UNARY, + RpcRequest, + RpcResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/Refresh": grpclib.const.Handler( + self.__rpc_refresh, + grpclib.const.Cardinality.UNARY_UNARY, + RefreshRequest, + RefreshResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/Channels": grpclib.const.Handler( + self.__rpc_channels, + grpclib.const.Cardinality.UNARY_UNARY, + ChannelsRequest, + ChannelsResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/Connections": grpclib.const.Handler( + self.__rpc_connections, + grpclib.const.Cardinality.UNARY_UNARY, + ConnectionsRequest, + ConnectionsResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/UpdateUserStatus": grpclib.const.Handler( + self.__rpc_update_user_status, + grpclib.const.Cardinality.UNARY_UNARY, + UpdateUserStatusRequest, + UpdateUserStatusResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/GetUserStatus": grpclib.const.Handler( + self.__rpc_get_user_status, + grpclib.const.Cardinality.UNARY_UNARY, + GetUserStatusRequest, + GetUserStatusResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/DeleteUserStatus": grpclib.const.Handler( + self.__rpc_delete_user_status, + grpclib.const.Cardinality.UNARY_UNARY, + DeleteUserStatusRequest, + DeleteUserStatusResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/BlockUser": grpclib.const.Handler( + self.__rpc_block_user, + grpclib.const.Cardinality.UNARY_UNARY, + BlockUserRequest, + BlockUserResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/UnblockUser": grpclib.const.Handler( + self.__rpc_unblock_user, + grpclib.const.Cardinality.UNARY_UNARY, + UnblockUserRequest, + UnblockUserResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/RevokeToken": grpclib.const.Handler( + self.__rpc_revoke_token, + grpclib.const.Cardinality.UNARY_UNARY, + RevokeTokenRequest, + RevokeTokenResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/InvalidateUserTokens": grpclib.const.Handler( + self.__rpc_invalidate_user_tokens, + grpclib.const.Cardinality.UNARY_UNARY, + InvalidateUserTokensRequest, + InvalidateUserTokensResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/DeviceRegister": grpclib.const.Handler( + self.__rpc_device_register, + grpclib.const.Cardinality.UNARY_UNARY, + DeviceRegisterRequest, + DeviceRegisterResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/DeviceUpdate": grpclib.const.Handler( + self.__rpc_device_update, + grpclib.const.Cardinality.UNARY_UNARY, + DeviceUpdateRequest, + DeviceUpdateResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/DeviceRemove": grpclib.const.Handler( + self.__rpc_device_remove, + grpclib.const.Cardinality.UNARY_UNARY, + DeviceRemoveRequest, + DeviceRemoveResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/DeviceList": grpclib.const.Handler( + self.__rpc_device_list, + grpclib.const.Cardinality.UNARY_UNARY, + DeviceListRequest, + DeviceListResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/DeviceTopicList": grpclib.const.Handler( + self.__rpc_device_topic_list, + grpclib.const.Cardinality.UNARY_UNARY, + DeviceTopicListRequest, + DeviceTopicListResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/DeviceTopicUpdate": grpclib.const.Handler( + self.__rpc_device_topic_update, + grpclib.const.Cardinality.UNARY_UNARY, + DeviceTopicUpdateRequest, + DeviceTopicUpdateResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/UserTopicList": grpclib.const.Handler( + self.__rpc_user_topic_list, + grpclib.const.Cardinality.UNARY_UNARY, + UserTopicListRequest, + UserTopicListResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/UserTopicUpdate": grpclib.const.Handler( + self.__rpc_user_topic_update, + grpclib.const.Cardinality.UNARY_UNARY, + UserTopicUpdateRequest, + UserTopicUpdateResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/SendPushNotification": grpclib.const.Handler( + self.__rpc_send_push_notification, + grpclib.const.Cardinality.UNARY_UNARY, + SendPushNotificationRequest, + SendPushNotificationResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/UpdatePushStatus": grpclib.const.Handler( + self.__rpc_update_push_status, + grpclib.const.Cardinality.UNARY_UNARY, + UpdatePushStatusRequest, + UpdatePushStatusResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/CancelPush": grpclib.const.Handler( + self.__rpc_cancel_push, + grpclib.const.Cardinality.UNARY_UNARY, + CancelPushRequest, + CancelPushResponse, + ), + "/centrifugal.centrifugo.api.CentrifugoApi/RateLimit": grpclib.const.Handler( + self.__rpc_rate_limit, + grpclib.const.Cardinality.UNARY_UNARY, + RateLimitRequest, + RateLimitResponse, + ), + } diff --git a/cent/client/__init__.py b/cent/client/__init__.py index 4cb220b..67a0967 100644 --- a/cent/client/__init__.py +++ b/cent/client/__init__.py @@ -7,6 +7,7 @@ ) from .sync_client import Client from .async_client import AsyncClient +from .grpc_client import GrpcClient __all__ = ( "BaseSession", @@ -16,4 +17,5 @@ "RequestsSession", "Client", "AsyncClient", + "GrpcClient", ) diff --git a/cent/client/grpc_client.py b/cent/client/grpc_client.py new file mode 100644 index 0000000..41d5202 --- /dev/null +++ b/cent/client/grpc_client.py @@ -0,0 +1,222 @@ +from typing import Any, Optional, Dict, TypeVar, List + +from cent.client.session.grpc import GrpcSession +from cent.methods import ( + CentMethod, + BroadcastMethod, + PublishMethod, + SubscribeMethod, + UnsubscribeMethod, + PresenceMethod, + PresenceStatsMethod, + HistoryMethod, + HistoryRemoveMethod, + RefreshMethod, + ChannelsMethod, + DisconnectMethod, + InfoMethod, +) +from cent.types import ( + PublishResult, + BroadcastResult, + SubscribeResult, + UnsubscribeResult, + PresenceResult, + PresenceStatsResult, + HistoryResult, + HistoryRemoveResult, + RefreshResult, + ChannelsResult, + DisconnectResult, + InfoResult, + StreamPosition, + ChannelOptionsOverride, + Disconnect, +) + +T = TypeVar("T") + + +class GrpcClient: + def __init__(self, host: str, port: int) -> None: + self.session = GrpcSession(host=host, port=port) + + async def publish( + self, + channel: str, + data: Any, + skip_history: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + b64data: Optional[str] = None, + idempotency_key: Optional[str] = None, + ) -> PublishResult: + call = PublishMethod( + channel=channel, + data=data, + skip_history=skip_history, + tags=tags, + b64data=b64data, + idempotency_key=idempotency_key, + ) + return await self(call) + + async def broadcast( + self, + channels: List[str], + data: Any, + skip_history: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + b64data: Optional[str] = None, + idempotency_key: Optional[str] = None, + ) -> BroadcastResult: + call = BroadcastMethod( + channels=channels, + data=data, + skip_history=skip_history, + tags=tags, + b64data=b64data, + idempotency_key=idempotency_key, + ) + return await self(call) + + async def subscribe( + self, + user: str, + channel: str, + info: Optional[Any] = None, + b64info: Optional[str] = None, + client: Optional[str] = None, + session: Optional[str] = None, + data: Optional[Any] = None, + b64data: Optional[str] = None, + recover_since: Optional[StreamPosition] = None, + override: Optional[ChannelOptionsOverride] = None, + ) -> SubscribeResult: + call = SubscribeMethod( + user=user, + channel=channel, + info=info, + b64info=b64info, + client=client, + session=session, + data=data, + b64data=b64data, + recover_since=recover_since, + override=override, + ) + return await self(call) + + async def unsubscribe( + self, + user: str, + channel: str, + client: Optional[str] = None, + session: Optional[str] = None, + ) -> UnsubscribeResult: + call = UnsubscribeMethod( + user=user, + channel=channel, + client=client, + session=session, + ) + return await self(call) + + async def presence( + self, + channel: str, + ) -> PresenceResult: + call = PresenceMethod( + channel=channel, + ) + return await self(call) + + async def presence_stats( + self, + channel: str, + ) -> PresenceStatsResult: + call = PresenceStatsMethod( + channel=channel, + ) + return await self(call) + + async def history( + self, + channel: str, + limit: Optional[int] = None, + since: Optional[StreamPosition] = None, + reverse: Optional[bool] = None, + ) -> HistoryResult: + call = HistoryMethod( + channel=channel, + limit=limit, + since=since, + reverse=reverse, + ) + return await self(call) + + async def history_remove( + self, + channel: str, + ) -> HistoryRemoveResult: + call = HistoryRemoveMethod( + channel=channel, + ) + return await self(call) + + async def refresh( + self, + user: str, + client: Optional[str] = None, + session: Optional[str] = None, + expire_at: Optional[int] = None, + expired: Optional[bool] = None, + ) -> RefreshResult: + call = RefreshMethod( + user=user, + client=client, + session=session, + expire_at=expire_at, + expired=expired, + ) + return await self(call) + + async def channels( + self, + pattern: Optional[str] = None, + ) -> ChannelsResult: + call = ChannelsMethod( + pattern=pattern, + ) + return await self(call) + + async def disconnect( + self, + user: str, + client: Optional[str] = None, + session: Optional[str] = None, + whitelist: Optional[List[str]] = None, + disconnect: Optional[Disconnect] = None, + ) -> DisconnectResult: + call = DisconnectMethod( + user=user, + client=client, + session=session, + whitelist=whitelist, + disconnect=disconnect, + ) + return await self(call) + + async def info( + self, + ) -> InfoResult: + call = InfoMethod() + return await self(call) + + async def __call__(self, method: CentMethod[T]) -> T: + """ + Call API method + + :param method: Centrifugo method + :return: Centrifugo response + """ + return await self.session(self, method) diff --git a/cent/client/session/base_async.py b/cent/client/session/base_async.py index c6c4efa..0c5cdf3 100644 --- a/cent/client/session/base_async.py +++ b/cent/client/session/base_async.py @@ -1,16 +1,12 @@ from abc import ABC, abstractmethod -from typing import Final, TYPE_CHECKING, Callable, Any, Optional, cast +from typing import TYPE_CHECKING, Any, Optional, cast -from cent.methods.base import CentMethod, CentType from cent.client.session.base import BaseSession +from cent.methods.base import CentMethod, CentType if TYPE_CHECKING: from cent.client.async_client import AsyncClient -DEFAULT_TIMEOUT: Final[float] = 60.0 -_JsonLoads = Callable[..., Any] -_JsonDumps = Callable[..., str] - class BaseAsyncSession(BaseSession, ABC): """Base class for all sessions.""" @@ -35,7 +31,6 @@ async def make_request( :param method: Centrifuge method. :param timeout: Request timeout. """ - ... async def __call__( self, diff --git a/cent/client/session/base_sync.py b/cent/client/session/base_sync.py index 3071de9..1d92d88 100644 --- a/cent/client/session/base_sync.py +++ b/cent/client/session/base_sync.py @@ -1,16 +1,12 @@ from abc import abstractmethod, ABC -from typing import Final, TYPE_CHECKING, Callable, Any, Optional +from typing import TYPE_CHECKING, Optional -from cent.methods.base import CentMethod, CentType from cent.client.session.base import BaseSession +from cent.methods.base import CentMethod, CentType if TYPE_CHECKING: from cent.client.sync_client import Client -DEFAULT_TIMEOUT: Final[float] = 60.0 -_JsonLoads = Callable[..., Any] -_JsonDumps = Callable[..., str] - class BaseSyncSession(BaseSession, ABC): """Base class for all sessions.""" @@ -35,7 +31,6 @@ def make_request( :param method: Centrifuge method. :param timeout: Request timeout. """ - ... def __call__( self, diff --git a/cent/client/session/grpc.py b/cent/client/session/grpc.py new file mode 100644 index 0000000..e7eed8b --- /dev/null +++ b/cent/client/session/grpc.py @@ -0,0 +1,82 @@ +from dataclasses import dataclass, asdict +from typing import TYPE_CHECKING, cast, Type, Dict, Any, List, Tuple + +import betterproto +from grpclib.client import Channel +from pydantic import TypeAdapter, BaseModel + +from cent.centrifugal.centrifugo.api import CentrifugoApiStub +from cent.exceptions import APIError +from cent.methods.base import CentMethod, CentType, Response, Error + +if TYPE_CHECKING: + from cent.client.grpc_client import GrpcClient + + +@dataclass +class BaseResponse(betterproto.Message): + error: Error + result: Type[betterproto.Message] + + +def dict_factory(x: List[Tuple[str, Any]]) -> Dict[str, Any]: + response = {} + for k, v in x: + if v: + response[k] = v + return response + + +class GrpcSession: + def __init__(self, host: str, port: int) -> None: + self._channel = Channel(host=host, port=port) + self._stub = CentrifugoApiStub(channel=self._channel) + + def close(self) -> None: + self._channel.close() + + @staticmethod + def check_response( + client: "GrpcClient", + method: CentMethod[CentType], + content: BaseResponse, + ) -> None: + """Validate response.""" + response_type = Response[method.__returning__] # type: ignore + response = TypeAdapter(response_type).validate_python( + asdict(content, dict_factory=dict_factory), context={"client": client} + ) + if response.error: + raise APIError( + method=method, + code=response.error.code, + message=response.error.message, + ) + + def convert_to_grpc(self, method: CentMethod[CentType]) -> Any: + request = method.model_dump(by_alias=True, exclude_none=True, mode="grpc") + for key, value in method.model_fields.items(): + attr = getattr(method, key) + if issubclass(attr.__class__, BaseModel): + request[value.alias or key] = self.convert_to_grpc(attr) + return method.__grpc_method__(**request) + + async def make_request( + self, + client: "GrpcClient", + method: CentMethod[CentType], + ) -> None: + api_method = getattr(self._stub, method.__api_method__) + response = await api_method(self.convert_to_grpc(method)) + + self.check_response(client, method, response) + + async def __call__( + self, + client: "GrpcClient", + method: CentMethod[CentType], + ) -> CentType: + return cast(CentType, await self.make_request(client, method)) + + def __del__(self) -> None: + self.close() diff --git a/cent/methods/base.py b/cent/methods/base.py index 0a11d85..26e36eb 100644 --- a/cent/methods/base.py +++ b/cent/methods/base.py @@ -18,7 +18,12 @@ class Response(BaseModel, Generic[CentType]): result: Optional[CentType] = None -class CentMethod(ClientContextController, BaseModel, Generic[CentType], ABC): +class CentMethod( + ClientContextController, + BaseModel, + Generic[CentType], + ABC, +): model_config = ConfigDict( extra="allow", populate_by_name=True, @@ -28,6 +33,8 @@ class CentMethod(ClientContextController, BaseModel, Generic[CentType], ABC): if TYPE_CHECKING: __returning__: ClassVar[type] __api_method__: ClassVar[str] + + __grpc_method__: ClassVar[type] else: @property @@ -39,3 +46,24 @@ def __returning__(self) -> type: @abstractmethod def __api_method__(self) -> str: pass + + @property + @abstractmethod + def __grpc_method__(self) -> type: + pass + + +class NestedModel(ClientContextController, BaseModel, ABC): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + arbitrary_types_allowed=True, + ) + if TYPE_CHECKING: + __grpc_method__: ClassVar[type] + else: + + @property + @abstractmethod + def __grpc_method__(self) -> type: + pass diff --git a/cent/methods/batch.py b/cent/methods/batch.py index dc69374..c59cc62 100644 --- a/cent/methods/batch.py +++ b/cent/methods/batch.py @@ -1,5 +1,6 @@ from typing import List, Any +from cent.centrifugal.centrifugo.api import BatchRequest as GrpcBatchRequest from cent.methods import CentMethod from cent.types.batch_result import BatchResult @@ -10,5 +11,7 @@ class BatchMethod(CentMethod[BatchResult]): __returning__ = BatchResult __api_method__ = "batch" + __grpc_method__ = GrpcBatchRequest + commands: List[CentMethod[Any]] """List of commands to execute in batch.""" diff --git a/cent/methods/bool_value.py b/cent/methods/bool_value.py new file mode 100644 index 0000000..6fefdf4 --- /dev/null +++ b/cent/methods/bool_value.py @@ -0,0 +1,11 @@ +from cent.centrifugal.centrifugo.api import BoolValue as GrpcBoolValue +from cent.methods.base import NestedModel + + +class BoolValue(NestedModel): + """Bool value.""" + + __grpc_method__ = GrpcBoolValue + + value: bool + """Bool value.""" diff --git a/cent/methods/broadcast.py b/cent/methods/broadcast.py index e096ee0..252e4c6 100644 --- a/cent/methods/broadcast.py +++ b/cent/methods/broadcast.py @@ -1,5 +1,10 @@ +import json from typing import Dict, Optional, List, Any +from pydantic import field_serializer, Field +from pydantic_core.core_schema import SerializationInfo + +from cent.centrifugal.centrifugo.api import BroadcastRequest as GrpcBroadcastRequest from cent.methods.base import CentMethod from cent.types.broadcast_result import BroadcastResult @@ -10,6 +15,8 @@ class BroadcastMethod(CentMethod[BroadcastResult]): __returning__ = BroadcastResult __api_method__ = "broadcast" + __grpc_method__ = GrpcBroadcastRequest + channels: List[str] """List of channels to publish data to.""" data: Any @@ -18,7 +25,13 @@ class BroadcastMethod(CentMethod[BroadcastResult]): """Skip adding publications to channels' history for this request.""" tags: Optional[Dict[str, str]] = None """Publication tags - map with arbitrary string keys and values which is attached to publication and will be delivered to clients.""" - b64data: Optional[str] = None + b64data: Optional[str] = Field(None, alias="b64_data") """Custom binary data to publish into a channel encoded to base64 so it's possible to use HTTP API to send binary to clients. Centrifugo will decode it from base64 before publishing. In case of GRPC you can publish binary using data field.""" idempotency_key: Optional[str] = None """Optional idempotency key to drop duplicate publications upon retries. It acts per channel. Centrifugo currently keeps the cache of idempotent publish results during 5 minutes window. Available since Centrifugo v5.2.0""" + + @field_serializer("data") + def grpc_serialize_data(self, data: Any, _info: SerializationInfo) -> Any: + if _info.mode == "grpc": + return json.dumps(data).encode() + return data diff --git a/cent/types/channel_options_override.py b/cent/methods/channel_options_override.py similarity index 63% rename from cent/types/channel_options_override.py rename to cent/methods/channel_options_override.py index 4d08508..4855d82 100644 --- a/cent/types/channel_options_override.py +++ b/cent/methods/channel_options_override.py @@ -1,13 +1,15 @@ from typing import Optional -from pydantic import BaseModel +from cent.centrifugal.centrifugo.api import SubscribeOptionOverride as GrpcChannelOptionOverride +from cent.methods.base import NestedModel +from cent.methods.bool_value import BoolValue -from cent.types.bool_value import BoolValue - -class ChannelOptionsOverride(BaseModel): +class ChannelOptionsOverride(NestedModel): """Override object.""" + __grpc_method__ = GrpcChannelOptionOverride + presence: Optional[BoolValue] = None """Override presence.""" join_leave: Optional[BoolValue] = None diff --git a/cent/methods/channels.py b/cent/methods/channels.py index ebc4128..658c92e 100644 --- a/cent/methods/channels.py +++ b/cent/methods/channels.py @@ -1,5 +1,6 @@ from typing import Optional +from cent.centrifugal.centrifugo.api import ChannelsRequest as GrpcChannelsRequest from cent.methods import CentMethod from cent.types.channels_result import ChannelsResult @@ -10,5 +11,7 @@ class ChannelsMethod(CentMethod[ChannelsResult]): __returning__ = ChannelsResult __api_method__ = "channels" + __grpc_method__ = GrpcChannelsRequest + pattern: Optional[str] = None """Pattern to filter channels, we are using https://github.com/gobwas/glob library for matching.""" diff --git a/cent/methods/disconnect.py b/cent/methods/disconnect.py index 6d41ca2..0111f00 100644 --- a/cent/methods/disconnect.py +++ b/cent/methods/disconnect.py @@ -1,5 +1,6 @@ from typing import Optional, List +from cent.centrifugal.centrifugo.api import DisconnectRequest as GrpcDisconnectRequest from cent.methods import CentMethod from cent.types import Disconnect from cent.types.disconnect_result import DisconnectResult @@ -11,6 +12,8 @@ class DisconnectMethod(CentMethod[DisconnectResult]): __returning__ = DisconnectResult __api_method__ = "disconnect" + __grpc_method__ = GrpcDisconnectRequest + user: str """User ID to disconnect.""" client: Optional[str] = None diff --git a/cent/methods/disconnect_data.py b/cent/methods/disconnect_data.py new file mode 100644 index 0000000..112bc4b --- /dev/null +++ b/cent/methods/disconnect_data.py @@ -0,0 +1,14 @@ +from cent.centrifugal.centrifugo.api import Disconnect as GrpcDisconnect + +from cent.methods.base import NestedModel + + +class Disconnect(NestedModel): + """Disconnect data.""" + + __grpc_method__ = GrpcDisconnect + + code: int + """Disconnect code.""" + reason: str + """Disconnect reason.""" diff --git a/cent/methods/history.py b/cent/methods/history.py index f755cfa..6818389 100644 --- a/cent/methods/history.py +++ b/cent/methods/history.py @@ -1,5 +1,6 @@ from typing import Optional +from cent.centrifugal.centrifugo.api import HistoryRequest as GrpcHistoryRequest from cent.methods import CentMethod from cent.types import StreamPosition from cent.types.history_result import HistoryResult @@ -11,6 +12,8 @@ class HistoryMethod(CentMethod[HistoryResult]): __returning__ = HistoryResult __api_method__ = "history" + __grpc_method__ = GrpcHistoryRequest + channel: str """Name of channel to call history from.""" limit: Optional[int] = None diff --git a/cent/methods/history_remove.py b/cent/methods/history_remove.py index cb895b6..0cc2e57 100644 --- a/cent/methods/history_remove.py +++ b/cent/methods/history_remove.py @@ -1,3 +1,4 @@ +from cent.centrifugal.centrifugo.api import HistoryRemoveRequest as GrpcHistoryRemoveRequest from cent.methods import CentMethod from cent.types.history_remove_result import HistoryRemoveResult @@ -8,5 +9,7 @@ class HistoryRemoveMethod(CentMethod[HistoryRemoveResult]): __returning__ = HistoryRemoveResult __api_method__ = "history_remove" + __grpc_method__ = GrpcHistoryRemoveRequest + channel: str """Name of channel to remove history.""" diff --git a/cent/methods/info.py b/cent/methods/info.py index 13df7a9..6ba26dd 100644 --- a/cent/methods/info.py +++ b/cent/methods/info.py @@ -1,3 +1,4 @@ +from cent.centrifugal.centrifugo.api import InfoRequest as GrpcInfoRequest from cent.methods import CentMethod from cent.types.info_result import InfoResult @@ -7,3 +8,5 @@ class InfoMethod(CentMethod[InfoResult]): __returning__ = InfoResult __api_method__ = "info" + + __grpc_method__ = GrpcInfoRequest diff --git a/cent/methods/presence.py b/cent/methods/presence.py index d68996c..f26b9f4 100644 --- a/cent/methods/presence.py +++ b/cent/methods/presence.py @@ -1,3 +1,7 @@ +from cent.centrifugal.centrifugo.api import ( + PresenceRequest as GrpcPresenceRequest, + PresenceResult as GrpcPresenceResult, +) from cent.methods import CentMethod from cent.types.presence_result import PresenceResult @@ -8,5 +12,8 @@ class PresenceMethod(CentMethod[PresenceResult]): __returning__ = PresenceResult __api_method__ = "presence" + __grpc_returning__ = GrpcPresenceResult + __grpc_method__ = GrpcPresenceRequest + channel: str """Name of channel to call presence from.""" diff --git a/cent/methods/presence_stats.py b/cent/methods/presence_stats.py index abd048e..ae168ca 100644 --- a/cent/methods/presence_stats.py +++ b/cent/methods/presence_stats.py @@ -1,3 +1,4 @@ +from cent.centrifugal.centrifugo.api import PresenceStatsRequest as GrpcPresenceStatsRequest from cent.methods import CentMethod from cent.types.presence_stats_result import PresenceStatsResult @@ -8,5 +9,7 @@ class PresenceStatsMethod(CentMethod[PresenceStatsResult]): __returning__ = PresenceStatsResult __api_method__ = "presence_stats" + __grpc_method__ = GrpcPresenceStatsRequest + channel: str """Name of channel to call presence from.""" diff --git a/cent/methods/publish.py b/cent/methods/publish.py index 675196c..6f6361c 100644 --- a/cent/methods/publish.py +++ b/cent/methods/publish.py @@ -1,5 +1,9 @@ +import json from typing import Any, Dict, Optional +from cent.centrifugal.centrifugo.api import PublishRequest as GrpcPublishRequest +from pydantic import Field, field_serializer, SerializationInfo + from cent.methods.base import CentMethod from cent.types.publish_result import PublishResult @@ -10,6 +14,8 @@ class PublishMethod(CentMethod[PublishResult]): __returning__ = PublishResult __api_method__ = "publish" + __grpc_method__ = GrpcPublishRequest + channel: str """Name of channel to publish.""" data: Any @@ -18,7 +24,13 @@ class PublishMethod(CentMethod[PublishResult]): """Skip adding publication to history for this request.""" tags: Optional[Dict[str, str]] = None """Publication tags - map with arbitrary string keys and values which is attached to publication and will be delivered to clients.""" - b64data: Optional[str] = None + b64data: Optional[str] = Field(None, alias="b64_data") """Custom binary data to publish into a channel encoded to base64 so it's possible to use HTTP API to send binary to clients. Centrifugo will decode it from base64 before publishing. In case of GRPC you can publish binary using data field.""" idempotency_key: Optional[str] = None """Optional idempotency key to drop duplicate publications upon retries. It acts per channel. Centrifugo currently keeps the cache of idempotent publish results during 5 minutes window. Available since Centrifugo v5.2.0""" + + @field_serializer("data") + def grpc_serialize_data(self, data: Any, _info: SerializationInfo) -> Any: + if _info.mode == "grpc": + return json.dumps(data).encode() + return data diff --git a/cent/methods/refresh.py b/cent/methods/refresh.py index 2e2e3e9..18f80b6 100644 --- a/cent/methods/refresh.py +++ b/cent/methods/refresh.py @@ -1,5 +1,6 @@ from typing import Optional +from cent.centrifugal.centrifugo.api import RefreshRequest as GrpcRefreshRequest from cent.methods import CentMethod from cent.types.refresh_result import RefreshResult @@ -10,6 +11,8 @@ class RefreshMethod(CentMethod[RefreshResult]): __returning__ = RefreshResult __api_method__ = "refresh" + __grpc_method__ = GrpcRefreshRequest + user: str """User ID to refresh.""" client: Optional[str] = None diff --git a/cent/methods/stream_position.py b/cent/methods/stream_position.py new file mode 100644 index 0000000..78b133e --- /dev/null +++ b/cent/methods/stream_position.py @@ -0,0 +1,13 @@ +from cent.centrifugal.centrifugo.api import StreamPosition as GrpcStreamPosition +from cent.methods.base import NestedModel + + +class StreamPosition(NestedModel): + """Stream position.""" + + __grpc_method__ = GrpcStreamPosition + + offset: int + """Offset of publication in history stream.""" + epoch: str + """Epoch of current stream.""" diff --git a/cent/methods/subscribe.py b/cent/methods/subscribe.py index c71546e..8863b2c 100644 --- a/cent/methods/subscribe.py +++ b/cent/methods/subscribe.py @@ -1,5 +1,10 @@ +import json from typing import Optional, Any +from pydantic import Field, field_serializer +from pydantic_core.core_schema import SerializationInfo + +from cent.centrifugal.centrifugo.api import SubscribeRequest as GrpcSubscribeRequest from cent.methods.base import CentMethod from cent.types import ( StreamPosition, @@ -14,13 +19,15 @@ class SubscribeMethod(CentMethod[SubscribeResult]): __returning__ = SubscribeResult __api_method__ = "subscribe" + __grpc_method__ = GrpcSubscribeRequest + user: str """User ID to subscribe.""" channel: str """Name of channel to subscribe user to.""" info: Optional[Any] = None """Attach custom data to subscription (will be used in presence and join/leave messages).""" - b64info: Optional[str] = None + b64info: Optional[str] = Field(None, alias="b64_info") """info in base64 for binary mode (will be decoded by Centrifugo).""" client: Optional[str] = None """Specific client ID to subscribe (user still required to be set, will ignore other user connections with different client IDs).""" @@ -28,9 +35,21 @@ class SubscribeMethod(CentMethod[SubscribeResult]): """Specific client session to subscribe (user still required to be set).""" data: Optional[Any] = None """Custom subscription data (will be sent to client in Subscribe push).""" - b64data: Optional[str] = None + b64data: Optional[str] = Field(None, alias="b64_data") """Same as data but in base64 format (will be decoded by Centrifugo).""" recover_since: Optional[StreamPosition] = None """Stream position to recover from.""" override: Optional[ChannelOptionsOverride] = None """Allows dynamically override some channel options defined in Centrifugo configuration (see below available fields).""" + + @field_serializer("data", when_used="unless-none") + def grpc_serialize_data(self, data: Any, _info: SerializationInfo) -> Any: + if _info.mode == "grpc": + return json.dumps(data).encode() + return data + + @field_serializer("info", when_used="unless-none") + def grpc_serialize_info(self, info: Any, _info: SerializationInfo) -> Any: + if _info.mode == "grpc": + return json.dumps(info).encode() + return info diff --git a/cent/methods/unsubscribe.py b/cent/methods/unsubscribe.py index c4ceacc..b10ce96 100644 --- a/cent/methods/unsubscribe.py +++ b/cent/methods/unsubscribe.py @@ -1,5 +1,6 @@ from typing import Optional +from cent.centrifugal.centrifugo.api import UnsubscribeRequest as GrpcUnsubscribeRequest from cent.methods.base import CentMethod from cent.types.unsubscribe_result import UnsubscribeResult @@ -10,6 +11,8 @@ class UnsubscribeMethod(CentMethod[UnsubscribeResult]): __returning__ = UnsubscribeResult __api_method__ = "unsubscribe" + __grpc_method__ = GrpcUnsubscribeRequest + user: str """User ID to unsubscribe.""" channel: str diff --git a/cent/protos/apiproto.proto b/cent/protos/apiproto.proto new file mode 100644 index 0000000..4fd60ca --- /dev/null +++ b/cent/protos/apiproto.proto @@ -0,0 +1,879 @@ +syntax = "proto3"; + +package centrifugal.centrifugo.api; + +option go_package = "./;apiproto"; + +service CentrifugoApi { + rpc Batch (BatchRequest) returns (BatchResponse) {} + rpc Publish (PublishRequest) returns (PublishResponse) {} + rpc Broadcast (BroadcastRequest) returns (BroadcastResponse) {} + rpc Subscribe (SubscribeRequest) returns (SubscribeResponse) {} + rpc Unsubscribe (UnsubscribeRequest) returns (UnsubscribeResponse) {} + rpc Disconnect (DisconnectRequest) returns (DisconnectResponse) {} + rpc Presence (PresenceRequest) returns (PresenceResponse) {} + rpc PresenceStats (PresenceStatsRequest) returns (PresenceStatsResponse) {} + rpc History (HistoryRequest) returns (HistoryResponse) {} + rpc HistoryRemove (HistoryRemoveRequest) returns (HistoryRemoveResponse) {} + rpc Info (InfoRequest) returns (InfoResponse) {} + rpc RPC (RPCRequest) returns (RPCResponse) {} + rpc Refresh (RefreshRequest) returns (RefreshResponse) {} + rpc Channels (ChannelsRequest) returns (ChannelsResponse) {} + rpc Connections (ConnectionsRequest) returns (ConnectionsResponse) {} + rpc UpdateUserStatus (UpdateUserStatusRequest) returns (UpdateUserStatusResponse) {} + rpc GetUserStatus (GetUserStatusRequest) returns (GetUserStatusResponse) {} + rpc DeleteUserStatus (DeleteUserStatusRequest) returns (DeleteUserStatusResponse) {} + rpc BlockUser (BlockUserRequest) returns (BlockUserResponse) {} + rpc UnblockUser (UnblockUserRequest) returns (UnblockUserResponse) {} + rpc RevokeToken (RevokeTokenRequest) returns (RevokeTokenResponse) {} + rpc InvalidateUserTokens (InvalidateUserTokensRequest) returns (InvalidateUserTokensResponse) {} + rpc DeviceRegister (DeviceRegisterRequest) returns (DeviceRegisterResponse) {} + rpc DeviceUpdate (DeviceUpdateRequest) returns (DeviceUpdateResponse) {} + rpc DeviceRemove (DeviceRemoveRequest) returns (DeviceRemoveResponse) {} + rpc DeviceList (DeviceListRequest) returns (DeviceListResponse) {} + rpc DeviceTopicList (DeviceTopicListRequest) returns (DeviceTopicListResponse) {} + rpc DeviceTopicUpdate (DeviceTopicUpdateRequest) returns (DeviceTopicUpdateResponse) {} + rpc UserTopicList (UserTopicListRequest) returns (UserTopicListResponse) {} + rpc UserTopicUpdate (UserTopicUpdateRequest) returns (UserTopicUpdateResponse) {} + rpc SendPushNotification (SendPushNotificationRequest) returns (SendPushNotificationResponse) {} + rpc UpdatePushStatus (UpdatePushStatusRequest) returns (UpdatePushStatusResponse) {} + rpc CancelPush (CancelPushRequest) returns (CancelPushResponse) {} + rpc RateLimit (RateLimitRequest) returns (RateLimitResponse) {} +} + +message Command { + enum MethodType { + PUBLISH = 0; + BROADCAST = 1; + UNSUBSCRIBE = 2; + DISCONNECT = 3; + PRESENCE = 4; + PRESENCE_STATS = 5; + HISTORY = 6; + HISTORY_REMOVE = 7; + CHANNELS = 8; + INFO = 9; + RPC = 10; + SUBSCRIBE = 11; + REFRESH = 12; + CONNECTIONS = 14; + UPDATE_USER_STATUS = 15; + GET_USER_STATUS = 16; + DELETE_USER_STATUS = 17; + BLOCK_USER = 18; + UNBLOCK_USER = 19; + REVOKE_TOKEN = 20; + INVALIDATE_USER_TOKENS = 21; + DEVICE_REGISTER = 22; + DEVICE_UPDATE = 23; + DEVICE_REMOVE = 24; + DEVICE_LIST = 25; + DEVICE_TOPIC_LIST = 26; + DEVICE_TOPIC_UPDATE = 27; + USER_TOPIC_LIST = 28; + USER_TOPIC_UPDATE = 29; + SEND_PUSH_NOTIFICATION = 30; + UPDATE_PUSH_STATUS = 31; + CANCEL_PUSH = 32; + RATE_LIMIT = 47; + } + uint32 id = 1; + MethodType method = 2; + bytes params = 3; + + PublishRequest publish = 4; + BroadcastRequest broadcast = 5; + SubscribeRequest subscribe = 6; + UnsubscribeRequest unsubscribe = 7; + DisconnectRequest disconnect = 8; + PresenceRequest presence = 9; + PresenceStatsRequest presence_stats = 10; + HistoryRequest history = 11; + HistoryRemoveRequest history_remove = 12; + InfoRequest info = 13; + RPCRequest rpc = 14; + RefreshRequest refresh = 15; + ChannelsRequest channels = 16; + ConnectionsRequest connections = 17; + UpdateUserStatusRequest update_user_status = 18; + GetUserStatusRequest get_user_status = 19; + DeleteUserStatusRequest delete_user_status = 20; + BlockUserRequest block_user = 21; + UnblockUserRequest unblock_user = 22; + RevokeTokenRequest revoke_token = 23; + InvalidateUserTokensRequest invalidate_user_tokens = 24; + DeviceRegisterRequest device_register = 25; + DeviceUpdateRequest device_update = 26; + DeviceRemoveRequest device_remove = 27; + DeviceListRequest device_list = 28; + DeviceTopicListRequest device_topic_list = 29; + DeviceTopicUpdateRequest device_topic_update = 30; + UserTopicListRequest user_topic_list = 31; + UserTopicUpdateRequest user_topic_update = 32; + SendPushNotificationRequest send_push_notification = 33; + UpdatePushStatusRequest update_push_status = 34; + CancelPushRequest cancel_push = 35; + RateLimitRequest rate_limit = 50; +} + +message Error { + uint32 code = 1; + string message = 2; +} + +message Reply { + uint32 id = 1; + Error error = 2; + bytes result = 3; + + PublishResult publish = 4; + BroadcastResult broadcast = 5; + SubscribeResult subscribe = 6; + UnsubscribeResult unsubscribe = 7; + DisconnectResult disconnect = 8; + PresenceResult presence = 9; + PresenceStatsResult presence_stats = 10; + HistoryResult history = 11; + HistoryRemoveResult history_remove = 12; + InfoResult info = 13; + RPCResult rpc = 14; + RefreshResult refresh = 15; + ChannelsResult channels = 16; + ConnectionsResult connections = 17; + UpdateUserStatusResult update_user_status = 18; + GetUserStatusResult get_user_status = 19; + DeleteUserStatusResult delete_user_status = 20; + BlockUserResult block_user = 21; + UnblockUserResult unblock_user = 22; + RevokeTokenResult revoke_token = 23; + InvalidateUserTokensResult invalidate_user_tokens = 24; + DeviceRegisterResult device_register = 25; + DeviceUpdateResult device_update = 26; + DeviceRemoveResult device_remove = 27; + DeviceListResult device_list = 28; + DeviceTopicListResult device_topic_list = 29; + DeviceTopicUpdateResult device_topic_update = 30; + UserTopicListResult user_topic_list = 31; + UserTopicUpdateResult user_topic_update = 32; + SendPushNotificationResult send_push_notification = 33; + UpdatePushStatusResult update_push_status = 34; + CancelPushResult cancel_push = 35; + RateLimitResult rate_limit = 50; +} + +message BoolValue { + bool value = 1; +} + +message Int32Value { + int32 value = 1; +} + +message SubscribeOptionOverride { + BoolValue presence = 1; + BoolValue join_leave = 2; + BoolValue force_recovery = 3; + BoolValue force_positioning = 4; + BoolValue force_push_join_leave = 5; +} + +message BatchRequest { + repeated Command commands = 1; + bool parallel = 2; +} + +message BatchResponse { + repeated Reply replies = 1; +} + +message PublishRequest { + string channel = 1; + bytes data = 2; + string b64data = 3; + bool skip_history = 4; + map tags = 5; + string idempotency_key = 6; +} + +message PublishResponse { + Error error = 1; + PublishResult result = 2; +} + +message PublishResult { + uint64 offset = 1; + string epoch = 2; +} + +message BroadcastRequest { + repeated string channels = 1; + bytes data = 2; + string b64data = 3; + bool skip_history = 4; + map tags = 5; + string idempotency_key = 6; +} + +message BroadcastResponse { + Error error = 1; + BroadcastResult result = 2; +} + +message BroadcastResult { + repeated PublishResponse responses = 1; +} + +message SubscribeRequest { + string channel = 1; + string user = 2; + int64 expire_at = 3; + bytes info = 4; + string b64info = 5; + string client = 6; + bytes data = 7; + string b64data = 8; + StreamPosition recover_since = 9; + SubscribeOptionOverride override = 10; + string session = 11; +} + +message SubscribeResponse { + Error error = 1; + SubscribeResult result = 2; +} + +message SubscribeResult {} + +message UnsubscribeRequest { + string channel = 1; + string user = 2; + string client = 3; + string session = 4; +} + +message UnsubscribeResponse { + Error error = 1; + UnsubscribeResult result = 2; +} + +message UnsubscribeResult {} + +message Disconnect { + reserved 3; + uint32 code = 1; + string reason = 2; +} + +message DisconnectRequest { + string user = 1; + Disconnect disconnect = 2; + string client = 3; + repeated string whitelist = 4; + string session = 5; +} + +message DisconnectResponse { + Error error = 1; + DisconnectResult result = 2; +} + +message DisconnectResult {} + +message PresenceRequest { + string channel = 1; +} + +message PresenceResponse { + Error error = 1; + PresenceResult result = 2; +} + +message ClientInfo { + string user = 1; + string client = 2; + bytes conn_info = 3; + bytes chan_info = 4; +} + +message PresenceResult { + map presence = 1; +} + +message PresenceStatsRequest { + string channel = 1; +} + +message PresenceStatsResponse { + Error error = 1; + PresenceStatsResult result = 2; +} + +message PresenceStatsResult { + uint32 num_clients = 1; + uint32 num_users = 2; +} + +message StreamPosition { + uint64 offset = 1; + string epoch = 2; +} + +message HistoryRequest { + string channel = 1; + int32 limit = 2; + StreamPosition since = 3; + bool reverse = 4; +} + +message HistoryResponse { + Error error = 1; + HistoryResult result = 2; +} + +message Publication { + // Removed: string uid = 1; + bytes data = 2; + ClientInfo info = 3; + uint64 offset = 4; + map tags = 5; +} + +message HistoryResult { + repeated Publication publications = 1; + string epoch = 2; + uint64 offset = 3; +} + +message HistoryRemoveRequest { + string channel = 1; +} + +message HistoryRemoveResponse { + Error error = 1; + HistoryRemoveResult result = 2; +} + +message HistoryRemoveResult {} + +message InfoRequest {} + +message InfoResponse { + Error error = 1; + InfoResult result = 2; +} + +message InfoResult { + repeated NodeResult nodes = 1; +} + +message RPCRequest { + string method = 1; + bytes params = 2; +} + +message RPCResponse { + Error error = 1; + RPCResult result = 2; +} + +message RPCResult { + bytes data = 1; +} + +message RefreshRequest { + string user = 1; + string client = 2; + bool expired = 3; + int64 expire_at = 4; + bytes info = 5; + string session = 6; +} + +message RefreshResponse { + Error error = 1; + RefreshResult result = 2; +} + +message RefreshResult {} + +message NodeResult { + string uid = 1; + string name = 2; + string version = 3; + uint32 num_clients = 4; + uint32 num_users = 5; + uint32 num_channels = 6; + uint32 uptime = 7; + Metrics metrics = 8; + Process process = 9; + uint32 num_subs = 10; +} + +message Metrics { + double interval = 1; + map items = 2; +} + +message Process { + double cpu = 1; + int64 rss = 2; +} + +message ChannelsRequest { + string pattern = 1; +} + +message ChannelsResponse { + Error error = 1; + ChannelsResult result = 2; +} + +message ChannelsResult { + map channels = 1; +} + +message ChannelInfo { + uint32 num_clients = 1; +} + +message ConnectionsRequest { + string user = 1; + string expression = 2; +} + +message ConnectionsResponse { + Error error = 1; + ConnectionsResult result = 2; +} + +message ConnectionsResult { + map connections = 1; +} + +message ConnectionInfo { + string app_name = 1; + string app_version = 2; + string transport = 3; + string protocol = 4; + // 5-7 dropped for backwards compatibility. + string user = 8; + ConnectionState state = 9; +} + +message ConnectionState { + map channels = 1; + ConnectionTokenInfo connection_token = 2; + map subscription_tokens = 3; + bytes meta = 4; +} + +message ChannelContext { + uint32 source = 1; +} + +message ConnectionTokenInfo { + string uid = 1; + int64 issued_at = 2; +} + +message SubscriptionTokenInfo { + string uid = 1; + int64 issued_at = 2; +} + +message UpdateUserStatusRequest { + repeated string users = 1; + string state = 2; +} + +message UpdateUserStatusResponse { + Error error = 1; + UpdateUserStatusResult result = 2; +} + +message UpdateUserStatusResult {} + +message GetUserStatusRequest { + repeated string users = 1; +} + +message GetUserStatusResponse { + Error error = 1; + GetUserStatusResult result = 2; +} + +message GetUserStatusResult { + repeated UserStatus statuses = 1; +} + +message UserStatus { + string user = 1; + int64 active = 2; + int64 online = 3; + string state = 4; +} + +message DeleteUserStatusRequest { + repeated string users = 1; +} + +message DeleteUserStatusResponse { + Error error = 1; + DeleteUserStatusResult result = 2; +} + +message DeleteUserStatusResult { +} + +message BlockUserRequest { + int64 expire_at = 1; + string user = 2; +} + +message BlockUserResult {} + +message BlockUserResponse { + Error error = 1; + BlockUserResult result = 2; +} + +message UnblockUserRequest { + string user = 1; +} + +message UnblockUserResult {} + +message UnblockUserResponse { + Error error = 1; + UnblockUserResult result = 2; +} + +message RevokeTokenRequest { + int64 expire_at = 1; + string uid = 2; +} + +message RevokeTokenResult {} + +message RevokeTokenResponse { + Error error = 1; + RevokeTokenResult result = 2; +} + +message InvalidateUserTokensRequest { + int64 expire_at = 1; + string user = 2; + int64 issued_before = 3; + string channel = 4; +} + +message InvalidateUserTokensResult {} + +message InvalidateUserTokensResponse { + Error error = 1; + InvalidateUserTokensResult result = 2; +} + +message DeviceRegisterRequest { + string id = 1; + string provider = 2; + string token = 3; + string platform = 4; + string user = 5; + map meta = 6; + repeated string topics = 7; + //map labels = 8; + //map scores = 9; +} + +message DeviceUpdateRequest { + repeated string ids = 1; + repeated string users = 2; + + DeviceUserUpdate user_update = 4; + DeviceMetaUpdate meta_update = 5; + DeviceTopicsUpdate topics_update = 6; + //DeviceLabelsUpdate labels_update = 7; + //DeviceScoresUpdate scores_update = 8; +} + +message DeviceRemoveRequest { + repeated string ids = 1; + repeated string users = 2; +} + +message DeviceUserUpdate { + string user = 1; +} + +message DeviceMetaUpdate { + map meta = 1; +} + +message DeviceTopicsUpdate { + string op = 1; // add | remove | set + repeated string topics = 2; +} + +message DeviceFilter { + repeated string ids = 1; + repeated string users = 2; + repeated string topics = 3; + repeated string providers = 4; + repeated string platforms = 5; +} + +message DeviceListRequest { + DeviceFilter filter = 1; + + bool include_total_count = 2; + bool include_meta = 3; + bool include_topics = 4; + //bool include_labels = 5; + //bool include_scores = 6; + + string cursor = 10; + int32 limit = 11; +} + +message DeviceTopicFilter { + repeated string device_ids = 1; + repeated string device_providers = 2; + repeated string device_platforms = 3; + repeated string device_users = 4; + repeated string topics = 5; + string topic_prefix = 6; +} + +message DeviceTopicListRequest { + DeviceTopicFilter filter = 1; + + bool include_total_count = 2; + bool include_device = 3; + + string cursor = 10; + int32 limit = 11; +} + +message UserTopicFilter { + repeated string users = 1; + repeated string topics = 2; + string topic_prefix = 3; +} + +message UserTopicListRequest { + UserTopicFilter filter = 1; + + bool include_total_count = 2; + + string cursor = 10; + int32 limit = 11; +} + +message DeviceTopicUpdateRequest { + string device_id = 1; + string op = 2; // add | remove | set + repeated string topics = 3; +} + +message UserTopicUpdateRequest { + string user = 1; + string op = 2; // add | remove | set + repeated string topics = 3; +} + +message DeviceRegisterResponse { + Error error = 1; + DeviceRegisterResult result = 2; +} + +message DeviceUpdateResponse { + Error error = 1; + DeviceUpdateResult result = 2; +} + +message DeviceRemoveResponse { + Error error = 1; + DeviceRemoveResult result = 2; +} + +message DeviceListResponse { + Error error = 1; + DeviceListResult result = 2; +} + +message DeviceTopicListResponse { + Error error = 1; + DeviceTopicListResult result = 2; +} + +message UserTopicListResponse { + Error error = 1; + UserTopicListResult result = 2; +} + +message DeviceTopicUpdateResponse { + Error error = 1; + DeviceTopicUpdateResult result = 2; +} + +message UserTopicUpdateResponse { + Error error = 1; + UserTopicUpdateResult result = 2; +} + +message DeviceRegisterResult { + string id = 1; +} + +message DeviceUpdateResult { +} + +message DeviceRemoveResult { +} + +message DeviceListResult { + repeated Device items = 1; + string next_cursor = 2; + int64 total_count = 3; +} + +message Device { + string id = 1; + string platform = 2; + string provider = 3; + string token = 4; + string user = 5; + int64 created_at = 6; + int64 updated_at = 7; + + map meta = 10; + repeated string topics = 11; + //map labels = 12; + //map scores = 13; +} + +message DeviceTopicListResult { + repeated DeviceTopic items = 1; + string next_cursor = 2; + int64 total_count = 3; +} + +message DeviceTopic { + string id = 1; + string topic = 2; + Device device = 3; +} + +message UserTopicListResult { + repeated UserTopic items = 1; + string next_cursor = 2; + int64 total_count = 3; +} + +message DeviceTopicUpdateResult { +} + +message UserTopicUpdateResult { +} + +message UserTopic { + string id = 1; + string user = 2; + string topic = 3; +} + +message PushRecipient { + DeviceFilter filter = 1; + + repeated string fcm_tokens = 2; + string fcm_topic = 3; + string fcm_condition = 4; + + repeated string hms_tokens = 5; + string hms_topic = 6; + string hms_condition = 7; + + repeated string apns_tokens = 8; +} + +message PushNotification { + FcmPushNotification fcm = 1; + HmsPushNotification hms = 2; + ApnsPushNotification apns = 3; + + int64 expire_at = 5; // timestamp in the future when Centrifugo should stop trying to send push notification. +} + +message FcmPushNotification { + bytes message = 1; +} + +message HmsPushNotification { + bytes message = 1; +} + +message ApnsPushNotification { + map headers = 1; + bytes payload = 2; +} + +message SendPushNotificationRequest { + PushRecipient recipient = 1; + PushNotification notification = 2; + string uid = 3; // unique identifier for push notification, used for matching in Centrifugo analytics. + int64 send_at = 4; +} + +message SendPushNotificationResponse { + Error error = 1; + SendPushNotificationResult result = 2; +} + +message SendPushNotificationResult { + string uid = 1; // Unique identifier of notification send request (it's not a FCM message id). +} + +message UpdatePushStatusRequest { + string uid = 1; // uid of push notification (matches SendPushNotificationResult.uid) + string status = 2; // failed | sent | delivered | interacted + string device_id = 3; // Centrifugo device id. + string msg_id = 4; // Provider issued message id. +} + +message UpdatePushStatusResponse { + Error error = 1; + UpdatePushStatusResult result = 2; +} + +message UpdatePushStatusResult {} + +message CancelPushRequest { + string uid = 1; +} + +message CancelPushResponse { + Error error = 1; + CancelPushResult result = 2; +} + +message CancelPushResult {} + +message RateLimitRequest { + // string key = 1; + // int64 rate = 2; + // int64 interval = 3; + // int64 score = 4; +} + +message RateLimitResponse { + Error error = 1; + RateLimitResult result = 2; +} + +message RateLimitResult { + // bool allowed = 1; + // int64 tokens_left = 2; + // int64 allowed_in = 3; + // int64 server_time = 4; +} diff --git a/cent/types/__init__.py b/cent/types/__init__.py index 10f13ee..2a56ab0 100644 --- a/cent/types/__init__.py +++ b/cent/types/__init__.py @@ -1,11 +1,11 @@ from .base import CentResult -from .bool_value import BoolValue +from cent.methods.bool_value import BoolValue from .broadcast_result import BroadcastResult from .channel_info_result import ChannelInfoResult -from .channel_options_override import ChannelOptionsOverride +from cent.methods.channel_options_override import ChannelOptionsOverride from .channels_result import ChannelsResult from .client_info_result import ClientInfoResult -from .disconnect import Disconnect +from cent.methods.disconnect_data import Disconnect from .disconnect_result import DisconnectResult from .history_remove_result import HistoryRemoveResult from .history_result import HistoryResult @@ -18,7 +18,7 @@ from .publication_result import PublicationResult from .publish_result import PublishResult from .refresh_result import RefreshResult -from .stream_position import StreamPosition +from cent.methods.stream_position import StreamPosition from .subscribe_result import SubscribeResult from .unsubscribe_result import UnsubscribeResult diff --git a/cent/types/bool_value.py b/cent/types/bool_value.py deleted file mode 100644 index b1a73f4..0000000 --- a/cent/types/bool_value.py +++ /dev/null @@ -1,8 +0,0 @@ -from pydantic import BaseModel - - -class BoolValue(BaseModel): - """Bool value.""" - - value: bool - """Bool value.""" diff --git a/cent/types/channel_info_result.py b/cent/types/channel_info_result.py index ed7994f..dbcfd9e 100644 --- a/cent/types/channel_info_result.py +++ b/cent/types/channel_info_result.py @@ -1,8 +1,10 @@ +from pydantic import Field + from cent.types.base import CentResult class ChannelInfoResult(CentResult): """Channel info result.""" - num_clients: int + num_clients: int = Field(default=0) """Total number of connections currently subscribed to a channel.""" diff --git a/cent/types/disconnect.py b/cent/types/disconnect.py deleted file mode 100644 index 07b4314..0000000 --- a/cent/types/disconnect.py +++ /dev/null @@ -1,10 +0,0 @@ -from pydantic import BaseModel - - -class Disconnect(BaseModel): - """Disconnect result.""" - - code: int - """Disconnect code.""" - reason: str - """Disconnect reason.""" diff --git a/cent/types/metrics_result.py b/cent/types/metrics_result.py index 0ab5db9..ce0ca80 100644 --- a/cent/types/metrics_result.py +++ b/cent/types/metrics_result.py @@ -1,12 +1,14 @@ from typing import Dict +from pydantic import Field + from cent.types.base import CentResult class MetricsResult(CentResult): """Metrics result.""" - interval: float + interval: float = Field(default=0.0) """Interval.""" items: Dict[str, float] """Map where key is string and value is float.""" diff --git a/cent/types/node_result.py b/cent/types/node_result.py index 2fc3751..6550411 100644 --- a/cent/types/node_result.py +++ b/cent/types/node_result.py @@ -1,5 +1,7 @@ from typing import Optional +from pydantic import Field + from cent.types.base import CentResult from cent.types.metrics_result import MetricsResult from cent.types.process_result import ProcessResult @@ -14,17 +16,17 @@ class NodeResult(CentResult): """Node name.""" version: str """Node version.""" - num_clients: int + num_clients: int = Field(default=0) """Total number of connections.""" - num_users: int + num_subs: int = Field(default=0) + """Total number of subscriptions.""" + num_users: int = Field(default=0) """Total number of users.""" - num_channels: int + num_channels: int = Field(default=0) """Total number of channels.""" - uptime: int + uptime: int = Field(default=0) """Node uptime.""" metrics: Optional[MetricsResult] = None """Node metrics.""" process: Optional[ProcessResult] = None """Node process.""" - num_subs: int - """Total number of subscriptions.""" diff --git a/cent/types/presence_stats_result.py b/cent/types/presence_stats_result.py index 6e15c9b..2170ec1 100644 --- a/cent/types/presence_stats_result.py +++ b/cent/types/presence_stats_result.py @@ -1,10 +1,12 @@ +from pydantic import Field + from cent.types.base import CentResult class PresenceStatsResult(CentResult): """Presence stats result.""" - num_clients: int + num_clients: int = Field(default=0) """Total number of clients in channel.""" - num_users: int + num_users: int = Field(default=0) """Total number of unique users in channel.""" diff --git a/cent/types/process_result.py b/cent/types/process_result.py index 321e76c..9ea58e6 100644 --- a/cent/types/process_result.py +++ b/cent/types/process_result.py @@ -1,10 +1,12 @@ +from pydantic import Field + from cent.types.base import CentResult class ProcessResult(CentResult): """Process result.""" - cpu: float + cpu: float = Field(default=0.0) """Process CPU usage.""" rss: int """Process RSS.""" diff --git a/cent/types/publication_result.py b/cent/types/publication_result.py index 46a7ad5..18cdc3d 100644 --- a/cent/types/publication_result.py +++ b/cent/types/publication_result.py @@ -1,5 +1,7 @@ from typing import Any +from pydantic import Field + from cent.types.base import CentResult @@ -8,5 +10,5 @@ class PublicationResult(CentResult): data: Any """Custom JSON inside publication.""" - offset: int + offset: int = Field(default=0) """Offset of publication in history stream.""" diff --git a/cent/types/stream_position.py b/cent/types/stream_position.py deleted file mode 100644 index cb948c0..0000000 --- a/cent/types/stream_position.py +++ /dev/null @@ -1,10 +0,0 @@ -from pydantic import BaseModel - - -class StreamPosition(BaseModel): - """Stream position.""" - - offset: int - """Offset of publication in history stream.""" - epoch: str - """Epoch of current stream.""" diff --git a/poetry.lock b/poetry.lock index 44dfedf..ade7396 100644 --- a/poetry.lock +++ b/poetry.lock @@ -173,6 +173,73 @@ tests = ["attrs[tests-no-zope]", "zope-interface"] tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +[[package]] +name = "betterproto" +version = "2.0.0b6" +description = "A better Protobuf / gRPC generator & library" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "betterproto-2.0.0b6-py3-none-any.whl", hash = "sha256:a0839ec165d110a69d0d116f4d0e2bec8d186af4db826257931f0831dab73fcf"}, + {file = "betterproto-2.0.0b6.tar.gz", hash = "sha256:720ae92697000f6fcf049c69267d957f0871654c8b0d7458906607685daee784"}, +] + +[package.dependencies] +black = {version = ">=19.3b0", optional = true, markers = "extra == \"compiler\""} +grpclib = ">=0.4.1,<0.5.0" +isort = {version = ">=5.11.5,<6.0.0", optional = true, markers = "extra == \"compiler\""} +jinja2 = {version = ">=3.0.3", optional = true, markers = "extra == \"compiler\""} +python-dateutil = ">=2.8,<3.0" + +[package.extras] +compiler = ["black (>=19.3b0)", "isort (>=5.11.5,<6.0.0)", "jinja2 (>=3.0.3)"] + +[[package]] +name = "black" +version = "24.1.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-24.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c"}, + {file = "black-24.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445"}, + {file = "black-24.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a"}, + {file = "black-24.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4"}, + {file = "black-24.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7"}, + {file = "black-24.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8"}, + {file = "black-24.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161"}, + {file = "black-24.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d"}, + {file = "black-24.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8"}, + {file = "black-24.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e"}, + {file = "black-24.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6"}, + {file = "black-24.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b"}, + {file = "black-24.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62"}, + {file = "black-24.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5"}, + {file = "black-24.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6"}, + {file = "black-24.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717"}, + {file = "black-24.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9"}, + {file = "black-24.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024"}, + {file = "black-24.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2"}, + {file = "black-24.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac"}, + {file = "black-24.1.1-py3-none-any.whl", hash = "sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168"}, + {file = "black-24.1.1.tar.gz", hash = "sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "certifi" version = "2023.11.17" @@ -294,6 +361,20 @@ files = [ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -432,6 +513,194 @@ files = [ {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, ] +[[package]] +name = "grpcio" +version = "1.60.1" +description = "HTTP/2-based RPC framework" +optional = false +python-versions = ">=3.7" +files = [ + {file = "grpcio-1.60.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:14e8f2c84c0832773fb3958240c69def72357bc11392571f87b2d7b91e0bb092"}, + {file = "grpcio-1.60.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:33aed0a431f5befeffd9d346b0fa44b2c01aa4aeae5ea5b2c03d3e25e0071216"}, + {file = "grpcio-1.60.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:fead980fbc68512dfd4e0c7b1f5754c2a8e5015a04dea454b9cada54a8423525"}, + {file = "grpcio-1.60.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:082081e6a36b6eb5cf0fd9a897fe777dbb3802176ffd08e3ec6567edd85bc104"}, + {file = "grpcio-1.60.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55ccb7db5a665079d68b5c7c86359ebd5ebf31a19bc1a91c982fd622f1e31ff2"}, + {file = "grpcio-1.60.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9b54577032d4f235452f77a83169b6527bf4b77d73aeada97d45b2aaf1bf5ce0"}, + {file = "grpcio-1.60.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7d142bcd604166417929b071cd396aa13c565749a4c840d6c702727a59d835eb"}, + {file = "grpcio-1.60.1-cp310-cp310-win32.whl", hash = "sha256:2a6087f234cb570008a6041c8ffd1b7d657b397fdd6d26e83d72283dae3527b1"}, + {file = "grpcio-1.60.1-cp310-cp310-win_amd64.whl", hash = "sha256:f2212796593ad1d0235068c79836861f2201fc7137a99aa2fea7beeb3b101177"}, + {file = "grpcio-1.60.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:79ae0dc785504cb1e1788758c588c711f4e4a0195d70dff53db203c95a0bd303"}, + {file = "grpcio-1.60.1-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:4eec8b8c1c2c9b7125508ff7c89d5701bf933c99d3910e446ed531cd16ad5d87"}, + {file = "grpcio-1.60.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:8c9554ca8e26241dabe7951aa1fa03a1ba0856688ecd7e7bdbdd286ebc272e4c"}, + {file = "grpcio-1.60.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91422ba785a8e7a18725b1dc40fbd88f08a5bb4c7f1b3e8739cab24b04fa8a03"}, + {file = "grpcio-1.60.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cba6209c96828711cb7c8fcb45ecef8c8859238baf15119daa1bef0f6c84bfe7"}, + {file = "grpcio-1.60.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c71be3f86d67d8d1311c6076a4ba3b75ba5703c0b856b4e691c9097f9b1e8bd2"}, + {file = "grpcio-1.60.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:af5ef6cfaf0d023c00002ba25d0751e5995fa0e4c9eec6cd263c30352662cbce"}, + {file = "grpcio-1.60.1-cp311-cp311-win32.whl", hash = "sha256:a09506eb48fa5493c58f946c46754ef22f3ec0df64f2b5149373ff31fb67f3dd"}, + {file = "grpcio-1.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:49c9b6a510e3ed8df5f6f4f3c34d7fbf2d2cae048ee90a45cd7415abab72912c"}, + {file = "grpcio-1.60.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:b58b855d0071575ea9c7bc0d84a06d2edfbfccec52e9657864386381a7ce1ae9"}, + {file = "grpcio-1.60.1-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:a731ac5cffc34dac62053e0da90f0c0b8560396a19f69d9703e88240c8f05858"}, + {file = "grpcio-1.60.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:cf77f8cf2a651fbd869fbdcb4a1931464189cd210abc4cfad357f1cacc8642a6"}, + {file = "grpcio-1.60.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c557e94e91a983e5b1e9c60076a8fd79fea1e7e06848eb2e48d0ccfb30f6e073"}, + {file = "grpcio-1.60.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:069fe2aeee02dfd2135d562d0663fe70fbb69d5eed6eb3389042a7e963b54de8"}, + {file = "grpcio-1.60.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb0af13433dbbd1c806e671d81ec75bd324af6ef75171fd7815ca3074fe32bfe"}, + {file = "grpcio-1.60.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2f44c32aef186bbba254129cea1df08a20be414144ac3bdf0e84b24e3f3b2e05"}, + {file = "grpcio-1.60.1-cp312-cp312-win32.whl", hash = "sha256:a212e5dea1a4182e40cd3e4067ee46be9d10418092ce3627475e995cca95de21"}, + {file = "grpcio-1.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:6e490fa5f7f5326222cb9f0b78f207a2b218a14edf39602e083d5f617354306f"}, + {file = "grpcio-1.60.1-cp37-cp37m-linux_armv7l.whl", hash = "sha256:4216e67ad9a4769117433814956031cb300f85edc855252a645a9a724b3b6594"}, + {file = "grpcio-1.60.1-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:73e14acd3d4247169955fae8fb103a2b900cfad21d0c35f0dcd0fdd54cd60367"}, + {file = "grpcio-1.60.1-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:6ecf21d20d02d1733e9c820fb5c114c749d888704a7ec824b545c12e78734d1c"}, + {file = "grpcio-1.60.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33bdea30dcfd4f87b045d404388469eb48a48c33a6195a043d116ed1b9a0196c"}, + {file = "grpcio-1.60.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53b69e79d00f78c81eecfb38f4516080dc7f36a198b6b37b928f1c13b3c063e9"}, + {file = "grpcio-1.60.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:39aa848794b887120b1d35b1b994e445cc028ff602ef267f87c38122c1add50d"}, + {file = "grpcio-1.60.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:72153a0d2e425f45b884540a61c6639436ddafa1829a42056aa5764b84108b8e"}, + {file = "grpcio-1.60.1-cp37-cp37m-win_amd64.whl", hash = "sha256:50d56280b482875d1f9128ce596e59031a226a8b84bec88cb2bf76c289f5d0de"}, + {file = "grpcio-1.60.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:6d140bdeb26cad8b93c1455fa00573c05592793c32053d6e0016ce05ba267549"}, + {file = "grpcio-1.60.1-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:bc808924470643b82b14fe121923c30ec211d8c693e747eba8a7414bc4351a23"}, + {file = "grpcio-1.60.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:70c83bb530572917be20c21f3b6be92cd86b9aecb44b0c18b1d3b2cc3ae47df0"}, + {file = "grpcio-1.60.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b106bc52e7f28170e624ba61cc7dc6829566e535a6ec68528f8e1afbed1c41f"}, + {file = "grpcio-1.60.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e980cd6db1088c144b92fe376747328d5554bc7960ce583ec7b7d81cd47287"}, + {file = "grpcio-1.60.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0c5807e9152eff15f1d48f6b9ad3749196f79a4a050469d99eecb679be592acc"}, + {file = "grpcio-1.60.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f1c3dc536b3ee124e8b24feb7533e5c70b9f2ef833e3b2e5513b2897fd46763a"}, + {file = "grpcio-1.60.1-cp38-cp38-win32.whl", hash = "sha256:d7404cebcdb11bb5bd40bf94131faf7e9a7c10a6c60358580fe83913f360f929"}, + {file = "grpcio-1.60.1-cp38-cp38-win_amd64.whl", hash = "sha256:c8754c75f55781515a3005063d9a05878b2cfb3cb7e41d5401ad0cf19de14872"}, + {file = "grpcio-1.60.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:0250a7a70b14000fa311de04b169cc7480be6c1a769b190769d347939d3232a8"}, + {file = "grpcio-1.60.1-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:660fc6b9c2a9ea3bb2a7e64ba878c98339abaf1811edca904ac85e9e662f1d73"}, + {file = "grpcio-1.60.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:76eaaba891083fcbe167aa0f03363311a9f12da975b025d30e94b93ac7a765fc"}, + {file = "grpcio-1.60.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d97c65ea7e097056f3d1ead77040ebc236feaf7f71489383d20f3b4c28412a"}, + {file = "grpcio-1.60.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb2a2911b028f01c8c64d126f6b632fcd8a9ac975aa1b3855766c94e4107180"}, + {file = "grpcio-1.60.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5a1ebbae7e2214f51b1f23b57bf98eeed2cf1ba84e4d523c48c36d5b2f8829ff"}, + {file = "grpcio-1.60.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a66f4d2a005bc78e61d805ed95dedfcb35efa84b7bba0403c6d60d13a3de2d6"}, + {file = "grpcio-1.60.1-cp39-cp39-win32.whl", hash = "sha256:8d488fbdbf04283f0d20742b64968d44825617aa6717b07c006168ed16488804"}, + {file = "grpcio-1.60.1-cp39-cp39-win_amd64.whl", hash = "sha256:61b7199cd2a55e62e45bfb629a35b71fc2c0cb88f686a047f25b1112d3810904"}, + {file = "grpcio-1.60.1.tar.gz", hash = "sha256:dd1d3a8d1d2e50ad9b59e10aa7f07c7d1be2b367f3f2d33c5fade96ed5460962"}, +] + +[package.extras] +protobuf = ["grpcio-tools (>=1.60.1)"] + +[[package]] +name = "grpcio-tools" +version = "1.60.1" +description = "Protobuf code generator for gRPC" +optional = false +python-versions = ">=3.7" +files = [ + {file = "grpcio-tools-1.60.1.tar.gz", hash = "sha256:da08224ab8675c6d464b988bd8ca02cccd2bf0275bceefe8f6219bfd4a4f5e85"}, + {file = "grpcio_tools-1.60.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:184b27333b627a7cc0972fb70d21a8bb7c02ac4a6febc16768d78ea8ff883ddd"}, + {file = "grpcio_tools-1.60.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:18d7737f29ef5bbe3352547d0eccd080807834f00df223867dfc860bf81e9180"}, + {file = "grpcio_tools-1.60.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:cc8ba358d2c658c6ecbc58e779bf0fc5a673fecac015a70db27fc5b4d37b76b6"}, + {file = "grpcio_tools-1.60.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2973f75e8ba5c551033a1d59cc97654f6f386deaf2559082011d245d7ed87bba"}, + {file = "grpcio_tools-1.60.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28ae665113affebdd109247386786e5ab4dccfcfad1b5f68e9cce2e326b57ee6"}, + {file = "grpcio_tools-1.60.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5c7ed086fef5ff59f46d53a052b1934b73e0f7d12365d656d6af3a88057d5a3e"}, + {file = "grpcio_tools-1.60.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8540f6480428a52614db71dd6394f52cbc0d2565b5ea1136a982f26390a42c7a"}, + {file = "grpcio_tools-1.60.1-cp310-cp310-win32.whl", hash = "sha256:5b4a939097005531edec331f22d0b82bff26e71ede009354d2f375b5d41e74f0"}, + {file = "grpcio_tools-1.60.1-cp310-cp310-win_amd64.whl", hash = "sha256:075bb67895970f96aabc1761ca674bf4db193f8fcad387f08e50402023b5f953"}, + {file = "grpcio_tools-1.60.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:284749d20fb22418f17d3d351b9eb838caf4a0393a9cb02c36e5c32fa4bbe9db"}, + {file = "grpcio_tools-1.60.1-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:b1041377cf32ee2338284ee26e6b9c10f9ea7728092376b19803dcb9b91d510d"}, + {file = "grpcio_tools-1.60.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:e529cd3d4109a6f4a3f7bdaca68946eb33734e2d7ffe861785a0586abe99ee67"}, + {file = "grpcio_tools-1.60.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31294b534f25f02ead204e58dcbe0e5437a95a1a6f276bb9378905595b02ff6d"}, + {file = "grpcio_tools-1.60.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fb6f4d2df0388c35c2804ba170f511238a681b679ead013bfe5e39d0ea9cf48"}, + {file = "grpcio_tools-1.60.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:40cd8268a675269ce59c4fa50877597ec638bb1099c52237bb726c8ac9791868"}, + {file = "grpcio_tools-1.60.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:985ac476da365267a2367ab20060f9096fbfc2e190fb02dd394f9ec05edf03ca"}, + {file = "grpcio_tools-1.60.1-cp311-cp311-win32.whl", hash = "sha256:bd85f6c368b93ae45edf8568473053cb1cc075ef3489efb18f9832d4ecce062f"}, + {file = "grpcio_tools-1.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:c20e752ff5057758845f4e5c7a298739bfba291f373ed18ea9c7c7acbe69e8ab"}, + {file = "grpcio_tools-1.60.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:aafc94616c5f89c891d859057b194a153c451f9921053454e9d7d4cbf79047eb"}, + {file = "grpcio_tools-1.60.1-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:9bba347000f57dae8aea79c0d76ef7d72895597524d30d0170c7d1974a3a03f3"}, + {file = "grpcio_tools-1.60.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:1e96a532d38411f0543fe1903ff522f7142a9901afb0ed94de58d79caf1905be"}, + {file = "grpcio_tools-1.60.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ea6e397d87f458bb2c387a4a6e1b65df74ce5b5194a1f16850c38309012e981"}, + {file = "grpcio_tools-1.60.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aeecd5b8faa2aab67e6c8b8a57e888c00ce70d39f331ede0a21312e92def1a6"}, + {file = "grpcio_tools-1.60.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:d2c26ce5f774c98bd2d3d8d1703048394018b55d297ebdb41ed2ba35b9a34f68"}, + {file = "grpcio_tools-1.60.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:214281cdafb7acfdcde848eca2de7c888a6e2b5cd25ab579712b965ea09a9cd4"}, + {file = "grpcio_tools-1.60.1-cp312-cp312-win32.whl", hash = "sha256:8c4b917aa4fcdc77990773063f0f14540aab8d4a8bf6c862b964a45d891a31d2"}, + {file = "grpcio_tools-1.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:0aa34c7c21cff2177a4096b2b0d51dfbc9f8a41f929847a434e89b352c5a215d"}, + {file = "grpcio_tools-1.60.1-cp37-cp37m-linux_armv7l.whl", hash = "sha256:acdba77584981fe799104aa545d9d97910bcf88c69b668b768c1f3e7d7e5afac"}, + {file = "grpcio_tools-1.60.1-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:2a7fa55bc62d4b8ebe6fb26f8cf89df3cf3b504eb6c5f3a2f0174689d35fddb0"}, + {file = "grpcio_tools-1.60.1-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:dffa326cf901fe08a0e218d9fdf593f12276088a8caa07fcbec7d051149cf9ef"}, + {file = "grpcio_tools-1.60.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf945bd22f396c0d0c691e0990db2bfc4e77816b1edc2aea8a69c35ae721aac9"}, + {file = "grpcio_tools-1.60.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6801cfc5a85f0fb6fd12cade45942aaa1c814422328d594d12d364815fe34123"}, + {file = "grpcio_tools-1.60.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f95bdc6c7c50b7fc442e53537bc5b4eb8cab2a671c1da80d40b5a4ab1fd5d416"}, + {file = "grpcio_tools-1.60.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:402efeec36d8b12b792bae8a900085416fc2f57a34b599445ace2e847b6b0d75"}, + {file = "grpcio_tools-1.60.1-cp37-cp37m-win_amd64.whl", hash = "sha256:af88a2062b9c35034a80b25f289034b9c3c00c42bb88efaa465503a06fbd6a87"}, + {file = "grpcio_tools-1.60.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:46b495bae31c5d3f6ac0240eb848f0642b5410f80dff2aacdea20cdea3938c1d"}, + {file = "grpcio_tools-1.60.1-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:b5ae375207af9aa82f516dcd513d2e0c83690b7788d45844daad846ed87550f8"}, + {file = "grpcio_tools-1.60.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:15f13e8f3d77b96adcb1e3615acec5b100bd836c6010c58a51465bcb9c06d128"}, + {file = "grpcio_tools-1.60.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c354505e6a3d170da374f20404ea6a78135502df4f5534e5c532bdf24c4cc2a5"}, + {file = "grpcio_tools-1.60.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8cfab27ba2bd36a3e3b522aed686133531e8b919703d0247a0885dae8815317"}, + {file = "grpcio_tools-1.60.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b6ef213cb0aecb2832ee82a2eac32f29f31f50b17ce020604d82205096a6bd0c"}, + {file = "grpcio_tools-1.60.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0b62cb2d43a7f0eacc6a6962dfff7c2564874012e1a72ae4167e762f449e2912"}, + {file = "grpcio_tools-1.60.1-cp38-cp38-win32.whl", hash = "sha256:3fcabf484720a9fa1690e2825fc940027a05a0c79a1075a730008ef634bd8ad2"}, + {file = "grpcio_tools-1.60.1-cp38-cp38-win_amd64.whl", hash = "sha256:22ce3e3d861321d208d8bfd6161ab976623520b179712c90b2c175151463a6b1"}, + {file = "grpcio_tools-1.60.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:4e66fe204da15e08e599adb3060109a42927c0868fe8933e2d341ea649eceb03"}, + {file = "grpcio_tools-1.60.1-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:c1047bd831de5d9da761e9dc246988d5f07d722186938dfd5f34807398101010"}, + {file = "grpcio_tools-1.60.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:eba5fafd70585fbd4cb6ae45e3c5e11d8598e2426c9f289b78f682c0606e81cb"}, + {file = "grpcio_tools-1.60.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bba7230c60238c7a4ffa29f1aff6d78edb41f2c79cbe4443406472b1c80ccb5d"}, + {file = "grpcio_tools-1.60.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2bb8efc2cd64bd8f2779b426dd7e94e60924078ba5150cbbb60a846e62d1ed2"}, + {file = "grpcio_tools-1.60.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:26f91161a91f1601777751230eaaafdf416fed08a15c3ba2ae391088e4a906c6"}, + {file = "grpcio_tools-1.60.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2c19be2bba5583e30f88bb5d71b430176c396f0d6d0db3785e5845bfa3d28cd2"}, + {file = "grpcio_tools-1.60.1-cp39-cp39-win32.whl", hash = "sha256:9aadc9c00baa2064baa4414cff7c269455449f14805a355226674d89c507342c"}, + {file = "grpcio_tools-1.60.1-cp39-cp39-win_amd64.whl", hash = "sha256:652b08c9fef39186ce4f97f05f5440c0ed41f117db0f7d6cb0e0d75dbc6afd3f"}, +] + +[package.dependencies] +grpcio = ">=1.60.1" +protobuf = ">=4.21.6,<5.0dev" +setuptools = "*" + +[[package]] +name = "grpclib" +version = "0.4.7" +description = "Pure-Python gRPC implementation for asyncio" +optional = false +python-versions = ">=3.7" +files = [ + {file = "grpclib-0.4.7.tar.gz", hash = "sha256:2988ef57c02b22b7a2e8e961792c41ccf97efc2ace91ae7a5b0de03c363823c3"}, +] + +[package.dependencies] +h2 = ">=3.1.0,<5" +multidict = "*" + +[package.extras] +protobuf = ["protobuf (>=3.20.0)"] + +[[package]] +name = "h2" +version = "4.1.0" +description = "HTTP/2 State-Machine based protocol implementation" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, + {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, +] + +[package.dependencies] +hpack = ">=4.0,<5" +hyperframe = ">=6.0,<7" + +[[package]] +name = "hpack" +version = "4.0.0" +description = "Pure-Python HPACK header compression" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, + {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, +] + +[[package]] +name = "hyperframe" +version = "6.0.1" +description = "HTTP/2 framing layer for Python" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, + {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, +] + [[package]] name = "identify" version = "2.5.33" @@ -468,6 +737,106 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "isort" +version = "5.13.2" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[package.extras] +colors = ["colorama (>=0.4.6)"] + +[[package]] +name = "jinja2" +version = "3.1.3" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "2.1.4" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-win32.whl", hash = "sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-win_amd64.whl", hash = "sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-win32.whl", hash = "sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-win32.whl", hash = "sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a07f40ef8f0fbc5ef1000d0c78771f4d5ca03b4953fc162749772916b298fc4"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d18b66fe626ac412d96c2ab536306c736c66cf2a31c243a45025156cc190dc8a"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:698e84142f3f884114ea8cf83e7a67ca8f4ace8454e78fe960646c6c91c63bfa"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a3b78a5af63ec10d8604180380c13dcd870aba7928c1fe04e881d5c792dc4e"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:15866d7f2dc60cfdde12ebb4e75e41be862348b4728300c36cdf405e258415ec"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6aa5e2e7fc9bc042ae82d8b79d795b9a62bd8f15ba1e7594e3db243f158b5565"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:54635102ba3cf5da26eb6f96c4b8c53af8a9c0d97b64bdcb592596a6255d8518"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-win32.whl", hash = "sha256:3583a3a3ab7958e354dc1d25be74aee6228938312ee875a22330c4dc2e41beb0"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-win_amd64.whl", hash = "sha256:d6e427c7378c7f1b2bef6a344c925b8b63623d3321c09a237b7cc0e77dd98ceb"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bf1196dcc239e608605b716e7b166eb5faf4bc192f8a44b81e85251e62584bd2"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4df98d4a9cd6a88d6a585852f56f2155c9cdb6aec78361a19f938810aa020954"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b835aba863195269ea358cecc21b400276747cc977492319fd7682b8cd2c253d"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23984d1bdae01bee794267424af55eef4dfc038dc5d1272860669b2aa025c9e3"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c98c33ffe20e9a489145d97070a435ea0679fddaabcafe19982fe9c971987d5"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9896fca4a8eb246defc8b2a7ac77ef7553b638e04fbf170bff78a40fa8a91474"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b0fe73bac2fed83839dbdbe6da84ae2a31c11cfc1c777a40dbd8ac8a6ed1560f"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c7556bafeaa0a50e2fe7dc86e0382dea349ebcad8f010d5a7dc6ba568eaaa789"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-win32.whl", hash = "sha256:fc1a75aa8f11b87910ffd98de62b29d6520b6d6e8a3de69a70ca34dea85d2a8a"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-win_amd64.whl", hash = "sha256:3a66c36a3864df95e4f62f9167c734b3b1192cb0851b43d7cc08040c074c6279"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:765f036a3d00395a326df2835d8f86b637dbaf9832f90f5d196c3b8a7a5080cb"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21e7af8091007bf4bebf4521184f4880a6acab8df0df52ef9e513d8e5db23411"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5c31fe855c77cad679b302aabc42d724ed87c043b1432d457f4976add1c2c3e"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7653fa39578957bc42e5ebc15cf4361d9e0ee4b702d7d5ec96cdac860953c5b4"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47bb5f0142b8b64ed1399b6b60f700a580335c8e1c57f2f15587bd072012decc"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fe8512ed897d5daf089e5bd010c3dc03bb1bdae00b35588c49b98268d4a01e00"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:36d7626a8cca4d34216875aee5a1d3d654bb3dac201c1c003d182283e3205949"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b6f14a9cd50c3cb100eb94b3273131c80d102e19bb20253ac7bd7336118a673a"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-win32.whl", hash = "sha256:c8f253a84dbd2c63c19590fa86a032ef3d8cc18923b8049d91bcdeeb2581fbf6"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-win_amd64.whl", hash = "sha256:8b570a1537367b52396e53325769608f2a687ec9a4363647af1cded8928af959"}, + {file = "MarkupSafe-2.1.4.tar.gz", hash = "sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f"}, +] + [[package]] name = "multidict" version = "6.0.4" @@ -634,6 +1003,17 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + [[package]] name = "platformdirs" version = "4.2.0" @@ -682,6 +1062,26 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "protobuf" +version = "4.25.2" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "protobuf-4.25.2-cp310-abi3-win32.whl", hash = "sha256:b50c949608682b12efb0b2717f53256f03636af5f60ac0c1d900df6213910fd6"}, + {file = "protobuf-4.25.2-cp310-abi3-win_amd64.whl", hash = "sha256:8f62574857ee1de9f770baf04dde4165e30b15ad97ba03ceac65f760ff018ac9"}, + {file = "protobuf-4.25.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:2db9f8fa64fbdcdc93767d3cf81e0f2aef176284071507e3ede160811502fd3d"}, + {file = "protobuf-4.25.2-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:10894a2885b7175d3984f2be8d9850712c57d5e7587a2410720af8be56cdaf62"}, + {file = "protobuf-4.25.2-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:fc381d1dd0516343f1440019cedf08a7405f791cd49eef4ae1ea06520bc1c020"}, + {file = "protobuf-4.25.2-cp38-cp38-win32.whl", hash = "sha256:33a1aeef4b1927431d1be780e87b641e322b88d654203a9e9d93f218ee359e61"}, + {file = "protobuf-4.25.2-cp38-cp38-win_amd64.whl", hash = "sha256:47f3de503fe7c1245f6f03bea7e8d3ec11c6c4a2ea9ef910e3221c8a15516d62"}, + {file = "protobuf-4.25.2-cp39-cp39-win32.whl", hash = "sha256:5e5c933b4c30a988b52e0b7c02641760a5ba046edc5e43d3b94a74c9fc57c1b3"}, + {file = "protobuf-4.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:d66a769b8d687df9024f2985d5137a337f957a0916cf5464d1513eee96a63ff0"}, + {file = "protobuf-4.25.2-py3-none-any.whl", hash = "sha256:a8b7a98d4ce823303145bf3c1a8bdb0f2f4642a414b196f04ad9853ed0c8f830"}, + {file = "protobuf-4.25.2.tar.gz", hash = "sha256:fe599e175cb347efc8ee524bcd4b902d11f7262c0e569ececcb89995c15f0a5e"}, +] + [[package]] name = "py-cpuinfo" version = "9.0.0" @@ -845,6 +1245,20 @@ aspect = ["aspectlib"] elasticsearch = ["elasticsearch"] histogram = ["pygal", "pygaljs"] +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "pyyaml" version = "6.0.1" @@ -967,6 +1381,17 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + [[package]] name = "sniffio" version = "1.3.0" @@ -1201,4 +1626,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "0d716cc0baf2ecbf28e96b932074d58b3e5b4726da7709145c4baf5f5b24cbf0" +content-hash = "78605a1c6468aadd1ebf56fe9c38d1150f25d4b8b68993f014f5c7bbf585ae0a" diff --git a/pyproject.toml b/pyproject.toml index aaaa5a1..bb22865 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "cent" version = "5.0.0b1" description = "Python library to communicate with Centrifugo v3 HTTP API" -authors = ["Alexandr Emelin", "Bogdan Evstratenko", "Katant Savelev"] +authors = ["Alexandr Emelin", "Katant Savelev"] license = "MIT" readme = 'README.md' classifiers = [ @@ -30,6 +30,7 @@ aiohttp = "^3" pydantic = "^2" requests = "^2" types-requests = "^2" +betterproto = { version = "^2.0.0b6", allow-prereleases = true } [tool.poetry.group.dev.dependencies] pre-commit = "^3.6.0" @@ -39,6 +40,8 @@ uvloop = "^0.19.0" pytest = "^8" anyio = "^4.2.0" pytest-benchmark = "^4.0.0" +betterproto = { extras = ["compiler"], version = "^2.0.0b6", allow-prereleases = true } +grpcio-tools = "^1.60.1" [tool.ruff] line-length = 99 @@ -78,7 +81,8 @@ ignore = [ [tool.ruff.per-file-ignores] "cent/types/*" = ["E501"] "cent/methods/*" = ["E501"] -"tests/*" = ["S101"] +"tests/*" = ["S101", "PT012"] +"cent/centrifugal/*" = ["RUF009", "ARG002", "E501"] [tool.mypy] strict = true @@ -102,6 +106,10 @@ warn_return_any = true follow_imports_for_stubs = true namespace_packages = true show_absolute_path = true +plugins = ["pydantic.mypy"] + +[tool.pydantic-mypy] +warn_required_dynamic_aliases = true [[tool.mypy.overrides]] @@ -110,6 +118,10 @@ module = [ ] ignore_missing_imports = true +[[tool.mypy.overrides]] +module = ["cent.centrifugal.*"] +ignore_errors = true + [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/conftest.py b/tests/conftest.py index b293f1c..c7b014e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,15 +7,16 @@ import pytest -from cent import Client, AsyncClient +from cent import Client, AsyncClient, GrpcClient BASE_URL = "http://localhost:8000/api" API_KEY = "api_key" +UNKNOWN_CHANNEL_ERROR_CODE = 102 @pytest.fixture(scope="session") def anyio_backend() -> Tuple[str, Dict[str, bool]]: - return "asyncio", {"use_uvloop": True} + return "asyncio", {"use_uvloop": False} @pytest.fixture() @@ -23,6 +24,15 @@ def sync_client() -> Client: return Client(BASE_URL, API_KEY) +@pytest.fixture() +async def grpc_client( + anyio_backend: Any, # noqa: ARG001 +) -> AsyncGenerator[GrpcClient, None]: + client = GrpcClient("localhost", 10000) + yield client + client.session.close() + + @pytest.fixture() async def async_client( anyio_backend: Any, # noqa: ARG001 diff --git a/tests/test_async_validation.py b/tests/test_async_validation.py index 30c65de..b6c6eef 100644 --- a/tests/test_async_validation.py +++ b/tests/test_async_validation.py @@ -1,9 +1,12 @@ from base64 import b64encode +import pytest + from cent import AsyncClient from cent.exceptions import APIError from cent.methods import PublishMethod, BroadcastMethod, PresenceMethod from cent.types import StreamPosition, Disconnect +from tests.conftest import UNKNOWN_CHANNEL_ERROR_CODE async def test_publish(async_client: AsyncClient) -> None: @@ -129,12 +132,9 @@ async def test_batch(async_client: AsyncClient) -> None: async def test_error_publish(async_client: AsyncClient) -> None: - try: + with pytest.raises(APIError, match="unknown channel") as exc_info: await async_client.publish( "undefined_channel:123", {"data": "data"}, ) - except APIError: - assert True - else: - raise AssertionError + assert exc_info.value.code == UNKNOWN_CHANNEL_ERROR_CODE diff --git a/tests/test_grpc_validation.py b/tests/test_grpc_validation.py new file mode 100644 index 0000000..bc50b5f --- /dev/null +++ b/tests/test_grpc_validation.py @@ -0,0 +1,123 @@ +from base64 import b64encode + +import pytest + +from cent.client.grpc_client import GrpcClient +from cent.exceptions import APIError +from cent.methods.disconnect_data import Disconnect +from cent.types import StreamPosition, ChannelOptionsOverride, BoolValue +from tests.conftest import UNKNOWN_CHANNEL_ERROR_CODE + + +async def test_publish(grpc_client: GrpcClient) -> None: + await grpc_client.publish( + "personal:1", + {"data": "data"}, + skip_history=False, + tags={"tag": "tag"}, + b64data=b64encode(b"data").decode(), + idempotency_key="idempotency_key", + ) + + +async def test_broadcast(grpc_client: GrpcClient) -> None: + await grpc_client.broadcast( + ["personal:1", "personal:2"], + {"data": "data"}, + skip_history=False, + tags={"tag": "tag"}, + b64data=b64encode(b"data").decode(), + idempotency_key="idempotency_key", + ) + + +async def test_subscribe(grpc_client: GrpcClient) -> None: + await grpc_client.subscribe( + "user", + "personal:1", + info={"info": "info"}, + b64info=b64encode(b"info").decode(), + client="client", + session="session", + data={"data": "data"}, + recover_since=StreamPosition( + offset=1, + epoch="1", + ), + override=ChannelOptionsOverride( + presence=BoolValue(value=True), + join_leave=BoolValue(value=True), + force_recovery=BoolValue(value=True), + ), + ) + + +async def test_unsubscribe(grpc_client: GrpcClient) -> None: + await grpc_client.unsubscribe( + user="user", + channel="personal:1", + session="session", + client="client", + ) + + +async def test_presence(grpc_client: GrpcClient) -> None: + await grpc_client.presence("personal:1") + + +async def test_presence_stats(grpc_client: GrpcClient) -> None: + await grpc_client.presence_stats("personal:1") + + +async def test_history(grpc_client: GrpcClient) -> None: + await grpc_client.history( + channel="personal:1", + limit=1, + reverse=True, + ) + + +async def test_history_remove(grpc_client: GrpcClient) -> None: + await grpc_client.history_remove(channel="personal:1") + + +async def test_info(grpc_client: GrpcClient) -> None: + await grpc_client.info() + + +async def test_channels(grpc_client: GrpcClient) -> None: + await grpc_client.channels( + pattern="*", + ) + + +async def test_disconnect(grpc_client: GrpcClient) -> None: + await grpc_client.disconnect( + user="user", + client="client", + session="session", + whitelist=["personal:1"], + disconnect=Disconnect( + code=4000, + reason="reason", + ), + ) + + +async def test_refresh(grpc_client: GrpcClient) -> None: + await grpc_client.refresh( + user="user", + client="client", + session="session", + expire_at=1, + expired=True, + ) + + +async def test_error_publish(grpc_client: GrpcClient) -> None: + with pytest.raises(APIError, match="unknown channel") as exc_info: + await grpc_client.publish( + "undefined_channel:123", + {"data": "data"}, + ) + assert exc_info.value.code == UNKNOWN_CHANNEL_ERROR_CODE diff --git a/tests/test_sync_validation.py b/tests/test_sync_validation.py index 683f60c..54604a8 100644 --- a/tests/test_sync_validation.py +++ b/tests/test_sync_validation.py @@ -1,8 +1,12 @@ from base64 import b64encode + +import pytest + from cent import Client from cent.exceptions import APIError from cent.methods import PublishMethod, BroadcastMethod, PresenceMethod from cent.types import StreamPosition, Disconnect +from tests.conftest import UNKNOWN_CHANNEL_ERROR_CODE def test_publish(sync_client: Client) -> None: @@ -128,12 +132,9 @@ def test_batch(sync_client: Client) -> None: def test_error_publish(sync_client: Client) -> None: - try: + with pytest.raises(APIError, match="unknown channel") as exc_info: sync_client.publish( "undefined_channel:123", {"data": "data"}, ) - except APIError: - assert True - else: - raise AssertionError + assert exc_info.value.code == UNKNOWN_CHANNEL_ERROR_CODE From 173991bd8de82681afffb3b163a9d8e7adf50fe7 Mon Sep 17 00:00:00 2001 From: KatantDev Date: Fri, 2 Feb 2024 17:50:04 +1000 Subject: [PATCH 13/55] feat: grpc exception handler --- cent/client/session/grpc.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cent/client/session/grpc.py b/cent/client/session/grpc.py index e7eed8b..7fddad8 100644 --- a/cent/client/session/grpc.py +++ b/cent/client/session/grpc.py @@ -2,11 +2,12 @@ from typing import TYPE_CHECKING, cast, Type, Dict, Any, List, Tuple import betterproto +from grpclib import GRPCError from grpclib.client import Channel from pydantic import TypeAdapter, BaseModel from cent.centrifugal.centrifugo.api import CentrifugoApiStub -from cent.exceptions import APIError +from cent.exceptions import APIError, TransportError from cent.methods.base import CentMethod, CentType, Response, Error if TYPE_CHECKING: @@ -67,7 +68,10 @@ async def make_request( method: CentMethod[CentType], ) -> None: api_method = getattr(self._stub, method.__api_method__) - response = await api_method(self.convert_to_grpc(method)) + try: + response = await api_method(self.convert_to_grpc(method)) + except GRPCError as error: + raise TransportError(method=method, status_code=error.status.value) from None self.check_response(client, method, response) From 4ad813540e45db373c8b0786cad66080aeebcdd4 Mon Sep 17 00:00:00 2001 From: KatantDev Date: Fri, 2 Feb 2024 22:50:46 +1000 Subject: [PATCH 14/55] fix: put transport errors handler before json loads --- cent/client/session/base.py | 13 +++++++------ cent/client/session/grpc.py | 11 +++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/cent/client/session/base.py b/cent/client/session/base.py index a5b559d..6baf9c9 100644 --- a/cent/client/session/base.py +++ b/cent/client/session/base.py @@ -1,4 +1,3 @@ -import json from http import HTTPStatus from typing import Final, TYPE_CHECKING, Callable, Any, Union, Dict, List @@ -14,6 +13,8 @@ loads = orjson.loads except ImportError: + import json + loads = json.loads if TYPE_CHECKING: @@ -86,17 +87,17 @@ def check_response( if status_code == HTTPStatus.UNAUTHORIZED: raise InvalidApiKeyError - try: - json_data = self.json_loads(content) - except Exception as err: - raise ClientDecodeError from err - if status_code != HTTPStatus.OK: raise TransportError( method=method, status_code=status_code, ) + try: + json_data = self.json_loads(content) + except Exception as err: + raise ClientDecodeError from err + if isinstance(method, BatchMethod): json_data = self.validate_batch(client, method, json_data["replies"]) diff --git a/cent/client/session/grpc.py b/cent/client/session/grpc.py index 7fddad8..fa2fa71 100644 --- a/cent/client/session/grpc.py +++ b/cent/client/session/grpc.py @@ -10,6 +10,17 @@ from cent.exceptions import APIError, TransportError from cent.methods.base import CentMethod, CentType, Response, Error +try: + import orjson + + dumps = orjson.dumps +except ImportError: + import json + + def dumps(x: Any) -> bytes: + return json.dumps(x).encode() + + if TYPE_CHECKING: from cent.client.grpc_client import GrpcClient From 0a703283b12ecd024cd613085cd9635cb6dfab3b Mon Sep 17 00:00:00 2001 From: KatantDev Date: Sat, 3 Feb 2024 20:12:36 +1000 Subject: [PATCH 15/55] chore/docs: rename method names, add docs for contributors in README.md, rename errors --- README.md | 34 +- cent/__meta__.py | 2 +- cent/client/async_client.py | 58 ++-- cent/client/grpc_client.py | 52 +-- cent/client/session/aiohttp.py | 22 +- cent/client/session/base.py | 49 +-- cent/client/session/base_async.py | 6 +- cent/client/session/base_sync.py | 6 +- cent/client/session/grpc.py | 29 +- cent/client/session/requests.py | 22 +- cent/client/sync_client.py | 58 ++-- cent/exceptions.py | 24 +- cent/methods/__init__.py | 52 +-- cent/methods/base.py | 15 +- cent/methods/batch.py | 8 +- cent/methods/bool_value.py | 2 +- cent/methods/broadcast.py | 9 +- cent/methods/channel_options_override.py | 4 +- cent/methods/channels.py | 6 +- cent/methods/disconnect.py | 6 +- cent/methods/disconnect_data.py | 2 +- cent/methods/history.py | 6 +- cent/methods/history_remove.py | 6 +- cent/methods/info.py | 6 +- cent/methods/presence.py | 6 +- cent/methods/presence_stats.py | 6 +- cent/methods/publish.py | 9 +- cent/methods/refresh.py | 6 +- cent/methods/stream_position.py | 2 +- cent/methods/subscribe.py | 11 +- cent/methods/unsubscribe.py | 6 +- cent/{centrifugal => protos}/__init__.py | 0 .../centrifugal}/__init__.py | 0 .../protos/centrifugal/centrifugo/__init__.py | 0 .../centrifugal/centrifugo/api/__init__.py | 1 + poetry.lock | 296 +++++++++--------- pyproject.toml | 8 +- tests/test_async_validation.py | 14 +- tests/test_grpc_validation.py | 4 +- tests/test_sync_validation.py | 14 +- 40 files changed, 463 insertions(+), 404 deletions(-) rename cent/{centrifugal => protos}/__init__.py (100%) rename cent/{centrifugal/centrifugo => protos/centrifugal}/__init__.py (100%) create mode 100644 cent/protos/centrifugal/centrifugo/__init__.py rename cent/{ => protos}/centrifugal/centrifugo/api/__init__.py (99%) diff --git a/README.md b/README.md index c7b9a62..1970ee5 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,9 @@ To install run: ```bash pip install cent ``` + --- + ### Centrifugo compatibility **Cent v5 and higher works only with Centrifugo v5**. @@ -16,6 +18,7 @@ pip install cent If you need to work with Centrifugo v3 then use Cent v4 If you need to work with Centrifugo v2 then use Cent v3 --- + ### High-level library API First @@ -47,8 +50,10 @@ async def main(): if __name__ == "__main__": asyncio.run(main()) ``` + --- -### CentClient init arguments + +### Client init arguments Required: @@ -69,5 +74,28 @@ Required: Optional: -* json_loads — function to load JSON from response body -* timeout - timeout for requests +* json_loads — function to load JSON from response body (default is json, but you can use + orjson, ujson etc.) +* timeout - timeout for requests (default is 10.0) + +## For contributors + +### Tests and benchmarks + +To start tests, you can use pytest with any additional options, for example: + +```bash +pytest -vv tests +``` + +To start benchmarks, you can use pytest too, for example: + +```bash +pytest benchmarks --benchmark-verbose +``` + +### Generate code from proto file, if needed + +```bash +poetry run python -m grpc_tools.protoc -I . --python_betterproto_out=./cent/protos cent/protos/apiproto.proto +``` diff --git a/cent/__meta__.py b/cent/__meta__.py index 9a5effc..ba7be38 100644 --- a/cent/__meta__.py +++ b/cent/__meta__.py @@ -1 +1 @@ -__version__ = "5.0.0b1" +__version__ = "5.0.0" diff --git a/cent/client/async_client.py b/cent/client/async_client.py index cb7bdbb..0bf438d 100644 --- a/cent/client/async_client.py +++ b/cent/client/async_client.py @@ -2,21 +2,21 @@ from cent.client.session import BaseAsyncSession, AiohttpSession from cent.methods import ( - CentMethod, - BroadcastMethod, - PublishMethod, - SubscribeMethod, - UnsubscribeMethod, - PresenceMethod, - PresenceStatsMethod, - HistoryMethod, - HistoryRemoveMethod, - RefreshMethod, - ChannelsMethod, - DisconnectMethod, - InfoMethod, + CentRequest, + BroadcastRequest, + PublishRequest, + SubscribeRequest, + UnsubscribeRequest, + PresenceRequest, + PresenceStatsRequest, + HistoryRequest, + HistoryRemoveRequest, + RefreshRequest, + ChannelsRequest, + DisconnectRequest, + InfoRequest, ) -from cent.methods.batch import BatchMethod +from cent.methods.batch import BatchRequest from cent.types import ( PublishResult, BroadcastResult, @@ -66,7 +66,7 @@ async def publish( idempotency_key: Optional[str] = None, request_timeout: Optional[float] = None, ) -> PublishResult: - call = PublishMethod( + call = PublishRequest( channel=channel, data=data, skip_history=skip_history, @@ -86,7 +86,7 @@ async def broadcast( idempotency_key: Optional[str] = None, request_timeout: Optional[float] = None, ) -> BroadcastResult: - call = BroadcastMethod( + call = BroadcastRequest( channels=channels, data=data, skip_history=skip_history, @@ -110,7 +110,7 @@ async def subscribe( override: Optional[ChannelOptionsOverride] = None, request_timeout: Optional[float] = None, ) -> SubscribeResult: - call = SubscribeMethod( + call = SubscribeRequest( user=user, channel=channel, info=info, @@ -132,7 +132,7 @@ async def unsubscribe( session: Optional[str] = None, request_timeout: Optional[float] = None, ) -> UnsubscribeResult: - call = UnsubscribeMethod( + call = UnsubscribeRequest( user=user, channel=channel, client=client, @@ -145,7 +145,7 @@ async def presence( channel: str, request_timeout: Optional[float] = None, ) -> PresenceResult: - call = PresenceMethod( + call = PresenceRequest( channel=channel, ) return await self(call, request_timeout=request_timeout) @@ -155,7 +155,7 @@ async def presence_stats( channel: str, request_timeout: Optional[float] = None, ) -> PresenceStatsResult: - call = PresenceStatsMethod( + call = PresenceStatsRequest( channel=channel, ) return await self(call, request_timeout=request_timeout) @@ -168,7 +168,7 @@ async def history( reverse: Optional[bool] = None, request_timeout: Optional[float] = None, ) -> HistoryResult: - call = HistoryMethod( + call = HistoryRequest( channel=channel, limit=limit, since=since, @@ -181,7 +181,7 @@ async def history_remove( channel: str, request_timeout: Optional[float] = None, ) -> HistoryRemoveResult: - call = HistoryRemoveMethod( + call = HistoryRemoveRequest( channel=channel, ) return await self(call, request_timeout=request_timeout) @@ -195,7 +195,7 @@ async def refresh( expired: Optional[bool] = None, request_timeout: Optional[float] = None, ) -> RefreshResult: - call = RefreshMethod( + call = RefreshRequest( user=user, client=client, session=session, @@ -209,7 +209,7 @@ async def channels( pattern: Optional[str] = None, request_timeout: Optional[float] = None, ) -> ChannelsResult: - call = ChannelsMethod( + call = ChannelsRequest( pattern=pattern, ) return await self(call, request_timeout=request_timeout) @@ -223,7 +223,7 @@ async def disconnect( disconnect: Optional[Disconnect] = None, request_timeout: Optional[float] = None, ) -> DisconnectResult: - call = DisconnectMethod( + call = DisconnectRequest( user=user, client=client, session=session, @@ -236,18 +236,18 @@ async def info( self, request_timeout: Optional[float] = None, ) -> InfoResult: - call = InfoMethod() + call = InfoRequest() return await self(call, request_timeout=request_timeout) async def batch( self, - commands: List[CentMethod[Any]], + commands: List[CentRequest[Any]], request_timeout: Optional[float] = None, ) -> BatchResult: - call = BatchMethod.model_construct(commands=commands) + call = BatchRequest.model_construct(commands=commands) return await self(call, request_timeout=request_timeout) - async def __call__(self, method: CentMethod[T], request_timeout: Optional[float] = None) -> T: + async def __call__(self, method: CentRequest[T], request_timeout: Optional[float] = None) -> T: """ Call API method diff --git a/cent/client/grpc_client.py b/cent/client/grpc_client.py index 41d5202..54c5dcd 100644 --- a/cent/client/grpc_client.py +++ b/cent/client/grpc_client.py @@ -2,19 +2,19 @@ from cent.client.session.grpc import GrpcSession from cent.methods import ( - CentMethod, - BroadcastMethod, - PublishMethod, - SubscribeMethod, - UnsubscribeMethod, - PresenceMethod, - PresenceStatsMethod, - HistoryMethod, - HistoryRemoveMethod, - RefreshMethod, - ChannelsMethod, - DisconnectMethod, - InfoMethod, + CentRequest, + BroadcastRequest, + PublishRequest, + SubscribeRequest, + UnsubscribeRequest, + PresenceRequest, + PresenceStatsRequest, + HistoryRequest, + HistoryRemoveRequest, + RefreshRequest, + ChannelsRequest, + DisconnectRequest, + InfoRequest, ) from cent.types import ( PublishResult, @@ -50,7 +50,7 @@ async def publish( b64data: Optional[str] = None, idempotency_key: Optional[str] = None, ) -> PublishResult: - call = PublishMethod( + call = PublishRequest( channel=channel, data=data, skip_history=skip_history, @@ -69,7 +69,7 @@ async def broadcast( b64data: Optional[str] = None, idempotency_key: Optional[str] = None, ) -> BroadcastResult: - call = BroadcastMethod( + call = BroadcastRequest( channels=channels, data=data, skip_history=skip_history, @@ -92,7 +92,7 @@ async def subscribe( recover_since: Optional[StreamPosition] = None, override: Optional[ChannelOptionsOverride] = None, ) -> SubscribeResult: - call = SubscribeMethod( + call = SubscribeRequest( user=user, channel=channel, info=info, @@ -113,7 +113,7 @@ async def unsubscribe( client: Optional[str] = None, session: Optional[str] = None, ) -> UnsubscribeResult: - call = UnsubscribeMethod( + call = UnsubscribeRequest( user=user, channel=channel, client=client, @@ -125,7 +125,7 @@ async def presence( self, channel: str, ) -> PresenceResult: - call = PresenceMethod( + call = PresenceRequest( channel=channel, ) return await self(call) @@ -134,7 +134,7 @@ async def presence_stats( self, channel: str, ) -> PresenceStatsResult: - call = PresenceStatsMethod( + call = PresenceStatsRequest( channel=channel, ) return await self(call) @@ -146,7 +146,7 @@ async def history( since: Optional[StreamPosition] = None, reverse: Optional[bool] = None, ) -> HistoryResult: - call = HistoryMethod( + call = HistoryRequest( channel=channel, limit=limit, since=since, @@ -158,7 +158,7 @@ async def history_remove( self, channel: str, ) -> HistoryRemoveResult: - call = HistoryRemoveMethod( + call = HistoryRemoveRequest( channel=channel, ) return await self(call) @@ -171,7 +171,7 @@ async def refresh( expire_at: Optional[int] = None, expired: Optional[bool] = None, ) -> RefreshResult: - call = RefreshMethod( + call = RefreshRequest( user=user, client=client, session=session, @@ -184,7 +184,7 @@ async def channels( self, pattern: Optional[str] = None, ) -> ChannelsResult: - call = ChannelsMethod( + call = ChannelsRequest( pattern=pattern, ) return await self(call) @@ -197,7 +197,7 @@ async def disconnect( whitelist: Optional[List[str]] = None, disconnect: Optional[Disconnect] = None, ) -> DisconnectResult: - call = DisconnectMethod( + call = DisconnectRequest( user=user, client=client, session=session, @@ -209,10 +209,10 @@ async def disconnect( async def info( self, ) -> InfoResult: - call = InfoMethod() + call = InfoRequest() return await self(call) - async def __call__(self, method: CentMethod[T]) -> T: + async def __call__(self, method: CentRequest[T]) -> T: """ Call API method diff --git a/cent/client/session/aiohttp.py b/cent/client/session/aiohttp.py index 10354c5..78ce378 100644 --- a/cent/client/session/aiohttp.py +++ b/cent/client/session/aiohttp.py @@ -4,9 +4,9 @@ from aiohttp import ClientSession, ClientError from cent.client.session.base_async import BaseAsyncSession -from cent.methods.base import CentMethod, CentType +from cent.methods.base import CentRequest, CentType from cent.exceptions import CentNetworkError -from cent.methods.batch import BatchMethod +from cent.methods.batch import BatchRequest if TYPE_CHECKING: from cent.client.async_client import AsyncClient @@ -33,13 +33,13 @@ async def close(self) -> None: async def make_request( self, client: "AsyncClient", - method: CentMethod[CentType], + method: CentRequest[CentType], timeout: Optional[float] = None, ) -> CentType: session = await self._create_session() session.headers["X-API-Key"] = client.api_key - if isinstance(method, BatchMethod): + if isinstance(method, BatchRequest): json_data = self.get_batch_json_data(method) else: json_data = method.model_dump(exclude_none=True) @@ -53,10 +53,16 @@ async def make_request( timeout=timeout or self._timeout, ) as resp: raw_result = await resp.text() - except asyncio.TimeoutError: - raise CentNetworkError(method=method, message="Request timeout") from None - except ClientError as e: - raise CentNetworkError(method=method, message=f"{type(e).__name__}: {e}") from None + except asyncio.TimeoutError as error: + raise CentNetworkError( + method=method, + message="Request timeout", + ) from error + except ClientError as error: + raise CentNetworkError( + method=method, + message=f"{type(error).__name__}: {error}", + ) from error response = self.check_response( client=client, method=method, diff --git a/cent/client/session/base.py b/cent/client/session/base.py index 6baf9c9..8cdcb2e 100644 --- a/cent/client/session/base.py +++ b/cent/client/session/base.py @@ -4,18 +4,19 @@ from aiohttp.http import SERVER_SOFTWARE from pydantic import ValidationError, TypeAdapter, __version__ -from cent.exceptions import ClientDecodeError, APIError, InvalidApiKeyError, TransportError -from cent.methods.base import CentMethod, CentType, Response -from cent.methods.batch import BatchMethod - -try: - import orjson - - loads = orjson.loads -except ImportError: - import json - - loads = json.loads +from cent.exceptions import ( + CentClientDecodeError, + CentAPIError, + CentUnauthorizedError, + CentTransportError, +) +from cent.methods.base import ( + json_loads as _json_loads, + CentRequest, + CentType, + Response, +) +from cent.methods.batch import BatchRequest if TYPE_CHECKING: from cent.client.sync_client import Client @@ -32,7 +33,7 @@ class BaseSession: def __init__( self, base_url: str, - json_loads: _JsonLoads = loads, + json_loads: _JsonLoads = _json_loads, timeout: float = DEFAULT_TIMEOUT, ) -> None: """ @@ -51,7 +52,7 @@ def __init__( } @staticmethod - def get_batch_json_data(method: BatchMethod) -> Dict[str, List[Dict[str, Any]]]: + def get_batch_json_data(method: BatchRequest) -> Dict[str, List[Dict[str, Any]]]: commands = [ {command.__api_method__: command.model_dump(exclude_none=True)} for command in method.commands @@ -61,13 +62,13 @@ def get_batch_json_data(method: BatchMethod) -> Dict[str, List[Dict[str, Any]]]: @staticmethod def validate_batch( client: Union["Client", "AsyncClient"], - method: BatchMethod, + method: BatchRequest, json_replies: List[Dict[str, Any]], ) -> Dict[str, Dict[str, List[Any]]]: """Validate batch method.""" - replies: List[CentMethod[Any]] = [] + replies: List[CentRequest[Any]] = [] for command_method, json_data in zip(method.commands, json_replies): - validated_method: CentMethod[Any] = TypeAdapter( + validated_method: CentRequest[Any] = TypeAdapter( command_method.__returning__ ).validate_python( json_data[command_method.__api_method__], @@ -79,16 +80,16 @@ def validate_batch( def check_response( self, client: Union["Client", "AsyncClient"], - method: CentMethod[CentType], + method: CentRequest[CentType], status_code: int, content: str, ) -> Response[CentType]: """Validate response.""" if status_code == HTTPStatus.UNAUTHORIZED: - raise InvalidApiKeyError + raise CentUnauthorizedError if status_code != HTTPStatus.OK: - raise TransportError( + raise CentTransportError( method=method, status_code=status_code, ) @@ -96,9 +97,9 @@ def check_response( try: json_data = self.json_loads(content) except Exception as err: - raise ClientDecodeError from err + raise CentClientDecodeError from err - if isinstance(method, BatchMethod): + if isinstance(method, BatchRequest): json_data = self.validate_batch(client, method, json_data["replies"]) try: @@ -108,10 +109,10 @@ def check_response( context={"client": client}, ) except ValidationError as err: - raise ClientDecodeError from err + raise CentClientDecodeError from err if response.error: - raise APIError( + raise CentAPIError( method=method, code=response.error.code, message=response.error.message, diff --git a/cent/client/session/base_async.py b/cent/client/session/base_async.py index 0c5cdf3..a488558 100644 --- a/cent/client/session/base_async.py +++ b/cent/client/session/base_async.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, Optional, cast from cent.client.session.base import BaseSession -from cent.methods.base import CentMethod, CentType +from cent.methods.base import CentRequest, CentType if TYPE_CHECKING: from cent.client.async_client import AsyncClient @@ -21,7 +21,7 @@ async def close(self) -> None: async def make_request( self, client: "AsyncClient", - method: CentMethod[CentType], + method: CentRequest[CentType], timeout: Optional[float] = None, ) -> CentType: """ @@ -35,7 +35,7 @@ async def make_request( async def __call__( self, client: "AsyncClient", - method: CentMethod[CentType], + method: CentRequest[CentType], timeout: Optional[float] = None, ) -> CentType: return cast(CentType, await self.make_request(client, method, timeout)) diff --git a/cent/client/session/base_sync.py b/cent/client/session/base_sync.py index 1d92d88..6c553ae 100644 --- a/cent/client/session/base_sync.py +++ b/cent/client/session/base_sync.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Optional from cent.client.session.base import BaseSession -from cent.methods.base import CentMethod, CentType +from cent.methods.base import CentRequest, CentType if TYPE_CHECKING: from cent.client.sync_client import Client @@ -21,7 +21,7 @@ def close(self) -> None: def make_request( self, client: "Client", - method: CentMethod[CentType], + method: CentRequest[CentType], timeout: Optional[float] = None, ) -> CentType: """ @@ -35,7 +35,7 @@ def make_request( def __call__( self, client: "Client", - method: CentMethod[CentType], + method: CentRequest[CentType], timeout: Optional[float] = None, ) -> CentType: return self.make_request(client, method, timeout) diff --git a/cent/client/session/grpc.py b/cent/client/session/grpc.py index fa2fa71..b6b08d5 100644 --- a/cent/client/session/grpc.py +++ b/cent/client/session/grpc.py @@ -6,20 +6,9 @@ from grpclib.client import Channel from pydantic import TypeAdapter, BaseModel -from cent.centrifugal.centrifugo.api import CentrifugoApiStub -from cent.exceptions import APIError, TransportError -from cent.methods.base import CentMethod, CentType, Response, Error - -try: - import orjson - - dumps = orjson.dumps -except ImportError: - import json - - def dumps(x: Any) -> bytes: - return json.dumps(x).encode() - +from cent.protos.centrifugal.centrifugo.api import CentrifugoApiStub +from cent.exceptions import CentAPIError, CentTransportError +from cent.methods.base import CentRequest, CentType, Response, Error if TYPE_CHECKING: from cent.client.grpc_client import GrpcClient @@ -50,7 +39,7 @@ def close(self) -> None: @staticmethod def check_response( client: "GrpcClient", - method: CentMethod[CentType], + method: CentRequest[CentType], content: BaseResponse, ) -> None: """Validate response.""" @@ -59,13 +48,13 @@ def check_response( asdict(content, dict_factory=dict_factory), context={"client": client} ) if response.error: - raise APIError( + raise CentAPIError( method=method, code=response.error.code, message=response.error.message, ) - def convert_to_grpc(self, method: CentMethod[CentType]) -> Any: + def convert_to_grpc(self, method: CentRequest[CentType]) -> Any: request = method.model_dump(by_alias=True, exclude_none=True, mode="grpc") for key, value in method.model_fields.items(): attr = getattr(method, key) @@ -76,20 +65,20 @@ def convert_to_grpc(self, method: CentMethod[CentType]) -> Any: async def make_request( self, client: "GrpcClient", - method: CentMethod[CentType], + method: CentRequest[CentType], ) -> None: api_method = getattr(self._stub, method.__api_method__) try: response = await api_method(self.convert_to_grpc(method)) except GRPCError as error: - raise TransportError(method=method, status_code=error.status.value) from None + raise CentTransportError(method=method, status_code=error.status.value) from error self.check_response(client, method, response) async def __call__( self, client: "GrpcClient", - method: CentMethod[CentType], + method: CentRequest[CentType], ) -> CentType: return cast(CentType, await self.make_request(client, method)) diff --git a/cent/client/session/requests.py b/cent/client/session/requests.py index 13eb756..72d7f41 100644 --- a/cent/client/session/requests.py +++ b/cent/client/session/requests.py @@ -3,10 +3,10 @@ import requests from requests import Session -from cent.methods.base import CentMethod, CentType +from cent.methods.base import CentRequest, CentType from cent.client.session.base_sync import BaseSyncSession from cent.exceptions import CentNetworkError -from cent.methods.batch import BatchMethod +from cent.methods.batch import BatchRequest if TYPE_CHECKING: from cent.client.sync_client import Client @@ -25,11 +25,11 @@ def close(self) -> None: def make_request( self, client: "Client", - method: CentMethod[CentType], + method: CentRequest[CentType], timeout: Optional[float] = None, ) -> CentType: self._session.headers["X-API-Key"] = client.api_key - if isinstance(method, BatchMethod): + if isinstance(method, BatchRequest): json_data = self.get_batch_json_data(method) else: json_data = method.model_dump(exclude_none=True) @@ -42,10 +42,16 @@ def make_request( json=json_data, timeout=timeout or self._timeout, ) - except requests.exceptions.Timeout: - raise CentNetworkError(method=method, message="Request timeout") from None - except requests.exceptions.ConnectionError as e: - raise CentNetworkError(method=method, message=f"{type(e).__name__}: {e}") from None + except requests.exceptions.Timeout as error: + raise CentNetworkError( + method=method, + message="Request timeout", + ) from error + except requests.exceptions.ConnectionError as error: + raise CentNetworkError( + method=method, + message=f"{type(error).__name__}: {error}", + ) from error response = self.check_response( client=client, method=method, diff --git a/cent/client/sync_client.py b/cent/client/sync_client.py index 4ca1a39..639a194 100644 --- a/cent/client/sync_client.py +++ b/cent/client/sync_client.py @@ -2,21 +2,21 @@ from cent.client.session import BaseSyncSession, RequestsSession from cent.methods import ( - CentMethod, - BroadcastMethod, - PublishMethod, - SubscribeMethod, - UnsubscribeMethod, - PresenceMethod, - PresenceStatsMethod, - HistoryMethod, - HistoryRemoveMethod, - RefreshMethod, - ChannelsMethod, - DisconnectMethod, - InfoMethod, + CentRequest, + BroadcastRequest, + PublishRequest, + SubscribeRequest, + UnsubscribeRequest, + PresenceRequest, + PresenceStatsRequest, + HistoryRequest, + HistoryRemoveRequest, + RefreshRequest, + ChannelsRequest, + DisconnectRequest, + InfoRequest, ) -from cent.methods.batch import BatchMethod +from cent.methods.batch import BatchRequest from cent.types import ( PublishResult, BroadcastResult, @@ -66,7 +66,7 @@ def publish( idempotency_key: Optional[str] = None, request_timeout: Optional[float] = None, ) -> PublishResult: - call = PublishMethod( + call = PublishRequest( channel=channel, data=data, skip_history=skip_history, @@ -86,7 +86,7 @@ def broadcast( idempotency_key: Optional[str] = None, request_timeout: Optional[float] = None, ) -> BroadcastResult: - call = BroadcastMethod( + call = BroadcastRequest( channels=channels, data=data, skip_history=skip_history, @@ -110,7 +110,7 @@ def subscribe( override: Optional[ChannelOptionsOverride] = None, request_timeout: Optional[float] = None, ) -> SubscribeResult: - call = SubscribeMethod( + call = SubscribeRequest( user=user, channel=channel, info=info, @@ -132,7 +132,7 @@ def unsubscribe( session: Optional[str] = None, request_timeout: Optional[float] = None, ) -> UnsubscribeResult: - call = UnsubscribeMethod( + call = UnsubscribeRequest( user=user, channel=channel, client=client, @@ -145,7 +145,7 @@ def presence( channel: str, request_timeout: Optional[float] = None, ) -> PresenceResult: - call = PresenceMethod( + call = PresenceRequest( channel=channel, ) return self(call, request_timeout=request_timeout) @@ -155,7 +155,7 @@ def presence_stats( channel: str, request_timeout: Optional[float] = None, ) -> PresenceStatsResult: - call = PresenceStatsMethod( + call = PresenceStatsRequest( channel=channel, ) return self(call, request_timeout=request_timeout) @@ -168,7 +168,7 @@ def history( reverse: Optional[bool] = None, request_timeout: Optional[float] = None, ) -> HistoryResult: - call = HistoryMethod( + call = HistoryRequest( channel=channel, limit=limit, since=since, @@ -181,7 +181,7 @@ def history_remove( channel: str, request_timeout: Optional[float] = None, ) -> HistoryRemoveResult: - call = HistoryRemoveMethod( + call = HistoryRemoveRequest( channel=channel, ) return self(call, request_timeout=request_timeout) @@ -195,7 +195,7 @@ def refresh( expire_at: Optional[int] = None, request_timeout: Optional[float] = None, ) -> RefreshResult: - call = RefreshMethod( + call = RefreshRequest( user=user, client=client, session=session, @@ -209,7 +209,7 @@ def channels( pattern: Optional[str] = None, request_timeout: Optional[float] = None, ) -> ChannelsResult: - call = ChannelsMethod( + call = ChannelsRequest( pattern=pattern, ) return self(call, request_timeout=request_timeout) @@ -223,7 +223,7 @@ def disconnect( disconnect: Optional[Disconnect] = None, request_timeout: Optional[float] = None, ) -> DisconnectResult: - call = DisconnectMethod( + call = DisconnectRequest( user=user, client=client, session=session, @@ -236,18 +236,18 @@ def info( self, request_timeout: Optional[float] = None, ) -> InfoResult: - call = InfoMethod() + call = InfoRequest() return self(call, request_timeout=request_timeout) def batch( self, - commands: List[CentMethod[Any]], + commands: List[CentRequest[Any]], request_timeout: Optional[float] = None, ) -> BatchResult: - call = BatchMethod.model_construct(commands=commands) + call = BatchRequest.model_construct(commands=commands) return self(call, request_timeout=request_timeout) - def __call__(self, method: CentMethod[T], request_timeout: Optional[float] = None) -> T: + def __call__(self, method: CentRequest[T], request_timeout: Optional[float] = None) -> T: """ Call API method diff --git a/cent/exceptions.py b/cent/exceptions.py index 4c1126f..dba1c5e 100644 --- a/cent/exceptions.py +++ b/cent/exceptions.py @@ -1,4 +1,4 @@ -from cent.methods.base import CentMethod, CentType +from cent.methods.base import CentRequest, CentType class CentError(Exception): @@ -10,7 +10,7 @@ class CentError(Exception): class CentNetworkError(CentError): """CentNetworkError raised when Centrifugo is not available.""" - def __init__(self, method: CentMethod[CentType], message: str) -> None: + def __init__(self, method: CentRequest[CentType], message: str) -> None: self.method = method self.message = message @@ -21,26 +21,26 @@ def __repr__(self) -> str: return f"{type(self).__name__}('{self}')" -class ClientDecodeError(CentError): +class CentClientDecodeError(CentError): """ - ClientDecodeError raised when response from Centrifugo can't be decoded + CentClientDecodeError raised when response from Centrifugo can't be decoded from JSON. """ -class InvalidApiKeyError(CentError): +class CentUnauthorizedError(CentError): """ - InvalidApiKeyError raised when Centrifugo returns 401 status code. + CentUnauthorizedError raised when Centrifugo returns 401 status code. """ -class APIError(CentError): +class CentAPIError(CentError): """ - APIError raised when response from Centrifugo contains any error + CentAPIError raised when response from Centrifugo contains any error as a result of API command execution. """ - def __init__(self, method: CentMethod[CentType], code: int, message: str) -> None: + def __init__(self, method: CentRequest[CentType], code: int, message: str) -> None: self.method = method self.code = code self.message = message @@ -52,10 +52,10 @@ def __repr__(self) -> str: return f"{type(self).__name__}('{self}')" -class TransportError(CentError): - """TransportError raised when returns non-200 status code.""" +class CentTransportError(CentError): + """CentTransportError raised when returns non-200 status code.""" - def __init__(self, method: CentMethod[CentType], status_code: int): + def __init__(self, method: CentRequest[CentType], status_code: int): self.method = method self.status_code = status_code diff --git a/cent/methods/__init__.py b/cent/methods/__init__.py index eed4779..8723334 100644 --- a/cent/methods/__init__.py +++ b/cent/methods/__init__.py @@ -1,29 +1,29 @@ -from .base import CentMethod -from .broadcast import BroadcastMethod -from .publish import PublishMethod -from .subscribe import SubscribeMethod -from .unsubscribe import UnsubscribeMethod -from .presence import PresenceMethod -from .presence_stats import PresenceStatsMethod -from .history import HistoryMethod -from .history_remove import HistoryRemoveMethod -from .refresh import RefreshMethod -from .channels import ChannelsMethod -from .disconnect import DisconnectMethod -from .info import InfoMethod +from .base import CentRequest +from .broadcast import BroadcastRequest +from .publish import PublishRequest +from .subscribe import SubscribeRequest +from .unsubscribe import UnsubscribeRequest +from .presence import PresenceRequest +from .presence_stats import PresenceStatsRequest +from .history import HistoryRequest +from .history_remove import HistoryRemoveRequest +from .refresh import RefreshRequest +from .channels import ChannelsRequest +from .disconnect import DisconnectRequest +from .info import InfoRequest __all__ = ( - "CentMethod", - "BroadcastMethod", - "PublishMethod", - "SubscribeMethod", - "UnsubscribeMethod", - "PresenceMethod", - "PresenceStatsMethod", - "HistoryMethod", - "HistoryRemoveMethod", - "RefreshMethod", - "ChannelsMethod", - "DisconnectMethod", - "InfoMethod", + "CentRequest", + "BroadcastRequest", + "PublishRequest", + "SubscribeRequest", + "UnsubscribeRequest", + "PresenceRequest", + "PresenceStatsRequest", + "HistoryRequest", + "HistoryRemoveRequest", + "RefreshRequest", + "ChannelsRequest", + "DisconnectRequest", + "InfoRequest", ) diff --git a/cent/methods/base.py b/cent/methods/base.py index 26e36eb..6c37953 100644 --- a/cent/methods/base.py +++ b/cent/methods/base.py @@ -5,6 +5,19 @@ from cent.context_controller import ClientContextController +try: + import orjson as _orjson # type: ignore[import-not-found] + + json_dumps = _orjson.dumps + json_loads = _orjson.loads +except ImportError: + import json + + def json_dumps(x: Any) -> bytes: + return json.dumps(x).encode() + + json_loads = json.loads + CentType = TypeVar("CentType", bound=Any) @@ -18,7 +31,7 @@ class Response(BaseModel, Generic[CentType]): result: Optional[CentType] = None -class CentMethod( +class CentRequest( ClientContextController, BaseModel, Generic[CentType], diff --git a/cent/methods/batch.py b/cent/methods/batch.py index c59cc62..bd8114e 100644 --- a/cent/methods/batch.py +++ b/cent/methods/batch.py @@ -1,11 +1,11 @@ from typing import List, Any -from cent.centrifugal.centrifugo.api import BatchRequest as GrpcBatchRequest -from cent.methods import CentMethod +from cent.protos.centrifugal.centrifugo.api import BatchRequest as GrpcBatchRequest +from cent.methods import CentRequest from cent.types.batch_result import BatchResult -class BatchMethod(CentMethod[BatchResult]): +class BatchRequest(CentRequest[BatchResult]): """Batch request.""" __returning__ = BatchResult @@ -13,5 +13,5 @@ class BatchMethod(CentMethod[BatchResult]): __grpc_method__ = GrpcBatchRequest - commands: List[CentMethod[Any]] + commands: List[CentRequest[Any]] """List of commands to execute in batch.""" diff --git a/cent/methods/bool_value.py b/cent/methods/bool_value.py index 6fefdf4..d981acd 100644 --- a/cent/methods/bool_value.py +++ b/cent/methods/bool_value.py @@ -1,4 +1,4 @@ -from cent.centrifugal.centrifugo.api import BoolValue as GrpcBoolValue +from cent.protos.centrifugal.centrifugo.api import BoolValue as GrpcBoolValue from cent.methods.base import NestedModel diff --git a/cent/methods/broadcast.py b/cent/methods/broadcast.py index 252e4c6..ae5a220 100644 --- a/cent/methods/broadcast.py +++ b/cent/methods/broadcast.py @@ -1,15 +1,14 @@ -import json from typing import Dict, Optional, List, Any from pydantic import field_serializer, Field from pydantic_core.core_schema import SerializationInfo -from cent.centrifugal.centrifugo.api import BroadcastRequest as GrpcBroadcastRequest -from cent.methods.base import CentMethod +from cent.protos.centrifugal.centrifugo.api import BroadcastRequest as GrpcBroadcastRequest +from cent.methods.base import CentRequest, json_dumps from cent.types.broadcast_result import BroadcastResult -class BroadcastMethod(CentMethod[BroadcastResult]): +class BroadcastRequest(CentRequest[BroadcastResult]): """Broadcast request.""" __returning__ = BroadcastResult @@ -33,5 +32,5 @@ class BroadcastMethod(CentMethod[BroadcastResult]): @field_serializer("data") def grpc_serialize_data(self, data: Any, _info: SerializationInfo) -> Any: if _info.mode == "grpc": - return json.dumps(data).encode() + return json_dumps(data) return data diff --git a/cent/methods/channel_options_override.py b/cent/methods/channel_options_override.py index 4855d82..5d0c12f 100644 --- a/cent/methods/channel_options_override.py +++ b/cent/methods/channel_options_override.py @@ -1,6 +1,8 @@ from typing import Optional -from cent.centrifugal.centrifugo.api import SubscribeOptionOverride as GrpcChannelOptionOverride +from cent.protos.centrifugal.centrifugo.api import ( + SubscribeOptionOverride as GrpcChannelOptionOverride, +) from cent.methods.base import NestedModel from cent.methods.bool_value import BoolValue diff --git a/cent/methods/channels.py b/cent/methods/channels.py index 658c92e..da487e7 100644 --- a/cent/methods/channels.py +++ b/cent/methods/channels.py @@ -1,11 +1,11 @@ from typing import Optional -from cent.centrifugal.centrifugo.api import ChannelsRequest as GrpcChannelsRequest -from cent.methods import CentMethod +from cent.protos.centrifugal.centrifugo.api import ChannelsRequest as GrpcChannelsRequest +from cent.methods import CentRequest from cent.types.channels_result import ChannelsResult -class ChannelsMethod(CentMethod[ChannelsResult]): +class ChannelsRequest(CentRequest[ChannelsResult]): """Channels request.""" __returning__ = ChannelsResult diff --git a/cent/methods/disconnect.py b/cent/methods/disconnect.py index 0111f00..a341b73 100644 --- a/cent/methods/disconnect.py +++ b/cent/methods/disconnect.py @@ -1,12 +1,12 @@ from typing import Optional, List -from cent.centrifugal.centrifugo.api import DisconnectRequest as GrpcDisconnectRequest -from cent.methods import CentMethod +from cent.protos.centrifugal.centrifugo.api import DisconnectRequest as GrpcDisconnectRequest +from cent.methods import CentRequest from cent.types import Disconnect from cent.types.disconnect_result import DisconnectResult -class DisconnectMethod(CentMethod[DisconnectResult]): +class DisconnectRequest(CentRequest[DisconnectResult]): """Disconnect request.""" __returning__ = DisconnectResult diff --git a/cent/methods/disconnect_data.py b/cent/methods/disconnect_data.py index 112bc4b..28a7474 100644 --- a/cent/methods/disconnect_data.py +++ b/cent/methods/disconnect_data.py @@ -1,4 +1,4 @@ -from cent.centrifugal.centrifugo.api import Disconnect as GrpcDisconnect +from cent.protos.centrifugal.centrifugo.api import Disconnect as GrpcDisconnect from cent.methods.base import NestedModel diff --git a/cent/methods/history.py b/cent/methods/history.py index 6818389..7f55d95 100644 --- a/cent/methods/history.py +++ b/cent/methods/history.py @@ -1,12 +1,12 @@ from typing import Optional -from cent.centrifugal.centrifugo.api import HistoryRequest as GrpcHistoryRequest -from cent.methods import CentMethod +from cent.protos.centrifugal.centrifugo.api import HistoryRequest as GrpcHistoryRequest +from cent.methods import CentRequest from cent.types import StreamPosition from cent.types.history_result import HistoryResult -class HistoryMethod(CentMethod[HistoryResult]): +class HistoryRequest(CentRequest[HistoryResult]): """History request.""" __returning__ = HistoryResult diff --git a/cent/methods/history_remove.py b/cent/methods/history_remove.py index 0cc2e57..3e9362c 100644 --- a/cent/methods/history_remove.py +++ b/cent/methods/history_remove.py @@ -1,9 +1,9 @@ -from cent.centrifugal.centrifugo.api import HistoryRemoveRequest as GrpcHistoryRemoveRequest -from cent.methods import CentMethod +from cent.protos.centrifugal.centrifugo.api import HistoryRemoveRequest as GrpcHistoryRemoveRequest +from cent.methods import CentRequest from cent.types.history_remove_result import HistoryRemoveResult -class HistoryRemoveMethod(CentMethod[HistoryRemoveResult]): +class HistoryRemoveRequest(CentRequest[HistoryRemoveResult]): """History remove request.""" __returning__ = HistoryRemoveResult diff --git a/cent/methods/info.py b/cent/methods/info.py index 6ba26dd..cfb6ceb 100644 --- a/cent/methods/info.py +++ b/cent/methods/info.py @@ -1,9 +1,9 @@ -from cent.centrifugal.centrifugo.api import InfoRequest as GrpcInfoRequest -from cent.methods import CentMethod +from cent.protos.centrifugal.centrifugo.api import InfoRequest as GrpcInfoRequest +from cent.methods import CentRequest from cent.types.info_result import InfoResult -class InfoMethod(CentMethod[InfoResult]): +class InfoRequest(CentRequest[InfoResult]): """Info request.""" __returning__ = InfoResult diff --git a/cent/methods/presence.py b/cent/methods/presence.py index f26b9f4..b4b2d56 100644 --- a/cent/methods/presence.py +++ b/cent/methods/presence.py @@ -1,12 +1,12 @@ -from cent.centrifugal.centrifugo.api import ( +from cent.protos.centrifugal.centrifugo.api import ( PresenceRequest as GrpcPresenceRequest, PresenceResult as GrpcPresenceResult, ) -from cent.methods import CentMethod +from cent.methods import CentRequest from cent.types.presence_result import PresenceResult -class PresenceMethod(CentMethod[PresenceResult]): +class PresenceRequest(CentRequest[PresenceResult]): """Presence request.""" __returning__ = PresenceResult diff --git a/cent/methods/presence_stats.py b/cent/methods/presence_stats.py index ae168ca..031eb9a 100644 --- a/cent/methods/presence_stats.py +++ b/cent/methods/presence_stats.py @@ -1,9 +1,9 @@ -from cent.centrifugal.centrifugo.api import PresenceStatsRequest as GrpcPresenceStatsRequest -from cent.methods import CentMethod +from cent.protos.centrifugal.centrifugo.api import PresenceStatsRequest as GrpcPresenceStatsRequest +from cent.methods import CentRequest from cent.types.presence_stats_result import PresenceStatsResult -class PresenceStatsMethod(CentMethod[PresenceStatsResult]): +class PresenceStatsRequest(CentRequest[PresenceStatsResult]): """Presence request.""" __returning__ = PresenceStatsResult diff --git a/cent/methods/publish.py b/cent/methods/publish.py index 6f6361c..a40f7b7 100644 --- a/cent/methods/publish.py +++ b/cent/methods/publish.py @@ -1,14 +1,13 @@ -import json from typing import Any, Dict, Optional -from cent.centrifugal.centrifugo.api import PublishRequest as GrpcPublishRequest +from cent.protos.centrifugal.centrifugo.api import PublishRequest as GrpcPublishRequest from pydantic import Field, field_serializer, SerializationInfo -from cent.methods.base import CentMethod +from cent.methods.base import CentRequest, json_dumps from cent.types.publish_result import PublishResult -class PublishMethod(CentMethod[PublishResult]): +class PublishRequest(CentRequest[PublishResult]): """Publish request.""" __returning__ = PublishResult @@ -32,5 +31,5 @@ class PublishMethod(CentMethod[PublishResult]): @field_serializer("data") def grpc_serialize_data(self, data: Any, _info: SerializationInfo) -> Any: if _info.mode == "grpc": - return json.dumps(data).encode() + return json_dumps(data) return data diff --git a/cent/methods/refresh.py b/cent/methods/refresh.py index 18f80b6..90d7d54 100644 --- a/cent/methods/refresh.py +++ b/cent/methods/refresh.py @@ -1,11 +1,11 @@ from typing import Optional -from cent.centrifugal.centrifugo.api import RefreshRequest as GrpcRefreshRequest -from cent.methods import CentMethod +from cent.protos.centrifugal.centrifugo.api import RefreshRequest as GrpcRefreshRequest +from cent.methods import CentRequest from cent.types.refresh_result import RefreshResult -class RefreshMethod(CentMethod[RefreshResult]): +class RefreshRequest(CentRequest[RefreshResult]): """Refresh request.""" __returning__ = RefreshResult diff --git a/cent/methods/stream_position.py b/cent/methods/stream_position.py index 78b133e..832419d 100644 --- a/cent/methods/stream_position.py +++ b/cent/methods/stream_position.py @@ -1,4 +1,4 @@ -from cent.centrifugal.centrifugo.api import StreamPosition as GrpcStreamPosition +from cent.protos.centrifugal.centrifugo.api import StreamPosition as GrpcStreamPosition from cent.methods.base import NestedModel diff --git a/cent/methods/subscribe.py b/cent/methods/subscribe.py index 8863b2c..2105ff2 100644 --- a/cent/methods/subscribe.py +++ b/cent/methods/subscribe.py @@ -1,11 +1,10 @@ -import json from typing import Optional, Any from pydantic import Field, field_serializer from pydantic_core.core_schema import SerializationInfo -from cent.centrifugal.centrifugo.api import SubscribeRequest as GrpcSubscribeRequest -from cent.methods.base import CentMethod +from cent.protos.centrifugal.centrifugo.api import SubscribeRequest as GrpcSubscribeRequest +from cent.methods.base import CentRequest, json_dumps from cent.types import ( StreamPosition, SubscribeResult, @@ -13,7 +12,7 @@ ) -class SubscribeMethod(CentMethod[SubscribeResult]): +class SubscribeRequest(CentRequest[SubscribeResult]): """Subscribe request.""" __returning__ = SubscribeResult @@ -45,11 +44,11 @@ class SubscribeMethod(CentMethod[SubscribeResult]): @field_serializer("data", when_used="unless-none") def grpc_serialize_data(self, data: Any, _info: SerializationInfo) -> Any: if _info.mode == "grpc": - return json.dumps(data).encode() + return json_dumps(data) return data @field_serializer("info", when_used="unless-none") def grpc_serialize_info(self, info: Any, _info: SerializationInfo) -> Any: if _info.mode == "grpc": - return json.dumps(info).encode() + return json_dumps(info) return info diff --git a/cent/methods/unsubscribe.py b/cent/methods/unsubscribe.py index b10ce96..fafa0d5 100644 --- a/cent/methods/unsubscribe.py +++ b/cent/methods/unsubscribe.py @@ -1,11 +1,11 @@ from typing import Optional -from cent.centrifugal.centrifugo.api import UnsubscribeRequest as GrpcUnsubscribeRequest -from cent.methods.base import CentMethod +from cent.protos.centrifugal.centrifugo.api import UnsubscribeRequest as GrpcUnsubscribeRequest +from cent.methods.base import CentRequest from cent.types.unsubscribe_result import UnsubscribeResult -class UnsubscribeMethod(CentMethod[UnsubscribeResult]): +class UnsubscribeRequest(CentRequest[UnsubscribeResult]): """Unsubscribe request.""" __returning__ = UnsubscribeResult diff --git a/cent/centrifugal/__init__.py b/cent/protos/__init__.py similarity index 100% rename from cent/centrifugal/__init__.py rename to cent/protos/__init__.py diff --git a/cent/centrifugal/centrifugo/__init__.py b/cent/protos/centrifugal/__init__.py similarity index 100% rename from cent/centrifugal/centrifugo/__init__.py rename to cent/protos/centrifugal/__init__.py diff --git a/cent/protos/centrifugal/centrifugo/__init__.py b/cent/protos/centrifugal/centrifugo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cent/centrifugal/centrifugo/api/__init__.py b/cent/protos/centrifugal/centrifugo/api/__init__.py similarity index 99% rename from cent/centrifugal/centrifugo/api/__init__.py rename to cent/protos/centrifugal/centrifugo/api/__init__.py index 991413f..8563ec3 100644 --- a/cent/centrifugal/centrifugo/api/__init__.py +++ b/cent/protos/centrifugal/centrifugo/api/__init__.py @@ -16,6 +16,7 @@ from betterproto.grpc.grpclib_server import ServiceBase import grpclib.server + if TYPE_CHECKING: from betterproto.grpc.grpclib_client import MetadataLike from grpclib.metadata import Deadline diff --git a/poetry.lock b/poetry.lock index ade7396..780a956 100644 --- a/poetry.lock +++ b/poetry.lock @@ -242,13 +242,13 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2023.11.17" +version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, - {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] @@ -770,154 +770,170 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "markupsafe" -version = "2.1.4" +version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-win32.whl", hash = "sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-win_amd64.whl", hash = "sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-win32.whl", hash = "sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-win32.whl", hash = "sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a07f40ef8f0fbc5ef1000d0c78771f4d5ca03b4953fc162749772916b298fc4"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d18b66fe626ac412d96c2ab536306c736c66cf2a31c243a45025156cc190dc8a"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:698e84142f3f884114ea8cf83e7a67ca8f4ace8454e78fe960646c6c91c63bfa"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a3b78a5af63ec10d8604180380c13dcd870aba7928c1fe04e881d5c792dc4e"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:15866d7f2dc60cfdde12ebb4e75e41be862348b4728300c36cdf405e258415ec"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6aa5e2e7fc9bc042ae82d8b79d795b9a62bd8f15ba1e7594e3db243f158b5565"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:54635102ba3cf5da26eb6f96c4b8c53af8a9c0d97b64bdcb592596a6255d8518"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-win32.whl", hash = "sha256:3583a3a3ab7958e354dc1d25be74aee6228938312ee875a22330c4dc2e41beb0"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-win_amd64.whl", hash = "sha256:d6e427c7378c7f1b2bef6a344c925b8b63623d3321c09a237b7cc0e77dd98ceb"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bf1196dcc239e608605b716e7b166eb5faf4bc192f8a44b81e85251e62584bd2"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4df98d4a9cd6a88d6a585852f56f2155c9cdb6aec78361a19f938810aa020954"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b835aba863195269ea358cecc21b400276747cc977492319fd7682b8cd2c253d"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23984d1bdae01bee794267424af55eef4dfc038dc5d1272860669b2aa025c9e3"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c98c33ffe20e9a489145d97070a435ea0679fddaabcafe19982fe9c971987d5"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9896fca4a8eb246defc8b2a7ac77ef7553b638e04fbf170bff78a40fa8a91474"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b0fe73bac2fed83839dbdbe6da84ae2a31c11cfc1c777a40dbd8ac8a6ed1560f"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c7556bafeaa0a50e2fe7dc86e0382dea349ebcad8f010d5a7dc6ba568eaaa789"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-win32.whl", hash = "sha256:fc1a75aa8f11b87910ffd98de62b29d6520b6d6e8a3de69a70ca34dea85d2a8a"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-win_amd64.whl", hash = "sha256:3a66c36a3864df95e4f62f9167c734b3b1192cb0851b43d7cc08040c074c6279"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:765f036a3d00395a326df2835d8f86b637dbaf9832f90f5d196c3b8a7a5080cb"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21e7af8091007bf4bebf4521184f4880a6acab8df0df52ef9e513d8e5db23411"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5c31fe855c77cad679b302aabc42d724ed87c043b1432d457f4976add1c2c3e"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7653fa39578957bc42e5ebc15cf4361d9e0ee4b702d7d5ec96cdac860953c5b4"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47bb5f0142b8b64ed1399b6b60f700a580335c8e1c57f2f15587bd072012decc"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fe8512ed897d5daf089e5bd010c3dc03bb1bdae00b35588c49b98268d4a01e00"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:36d7626a8cca4d34216875aee5a1d3d654bb3dac201c1c003d182283e3205949"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b6f14a9cd50c3cb100eb94b3273131c80d102e19bb20253ac7bd7336118a673a"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-win32.whl", hash = "sha256:c8f253a84dbd2c63c19590fa86a032ef3d8cc18923b8049d91bcdeeb2581fbf6"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-win_amd64.whl", hash = "sha256:8b570a1537367b52396e53325769608f2a687ec9a4363647af1cded8928af959"}, - {file = "MarkupSafe-2.1.4.tar.gz", hash = "sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] [[package]] name = "multidict" -version = "6.0.4" +version = "6.0.5" description = "multidict implementation" optional = false python-versions = ">=3.7" files = [ - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, - {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, - {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, - {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, - {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, - {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, - {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, - {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, - {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, - {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, - {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, - {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, - {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, + {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, + {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, + {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, + {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, + {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, + {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, + {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, + {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, + {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, + {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, + {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, + {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, + {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, + {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, + {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, ] [[package]] @@ -1626,4 +1642,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "78605a1c6468aadd1ebf56fe9c38d1150f25d4b8b68993f014f5c7bbf585ae0a" +content-hash = "58b1474e3ef2acd49e78bb4414b99d92e35e2a88b1ee42f491e1c66ffbba7e7b" diff --git a/pyproject.toml b/pyproject.toml index bb22865..24351f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cent" -version = "5.0.0b1" +version = "5.0.0" description = "Python library to communicate with Centrifugo v3 HTTP API" authors = ["Alexandr Emelin", "Katant Savelev"] license = "MIT" @@ -41,7 +41,7 @@ pytest = "^8" anyio = "^4.2.0" pytest-benchmark = "^4.0.0" betterproto = { extras = ["compiler"], version = "^2.0.0b6", allow-prereleases = true } -grpcio-tools = "^1.60.1" +grpcio-tools = "^1.60.0" [tool.ruff] line-length = 99 @@ -82,7 +82,7 @@ ignore = [ "cent/types/*" = ["E501"] "cent/methods/*" = ["E501"] "tests/*" = ["S101", "PT012"] -"cent/centrifugal/*" = ["RUF009", "ARG002", "E501"] +"cent/protos/centrifugal/*" = ["RUF009", "ARG002", "E501"] [tool.mypy] strict = true @@ -119,7 +119,7 @@ module = [ ignore_missing_imports = true [[tool.mypy.overrides]] -module = ["cent.centrifugal.*"] +module = ["cent.protos.centrifugal.*"] ignore_errors = true diff --git a/tests/test_async_validation.py b/tests/test_async_validation.py index b6c6eef..24fd8f8 100644 --- a/tests/test_async_validation.py +++ b/tests/test_async_validation.py @@ -3,8 +3,8 @@ import pytest from cent import AsyncClient -from cent.exceptions import APIError -from cent.methods import PublishMethod, BroadcastMethod, PresenceMethod +from cent.exceptions import CentAPIError +from cent.methods import PublishRequest, BroadcastRequest, PresenceRequest from cent.types import StreamPosition, Disconnect from tests.conftest import UNKNOWN_CHANNEL_ERROR_CODE @@ -112,19 +112,19 @@ async def test_refresh(async_client: AsyncClient) -> None: async def test_batch(async_client: AsyncClient) -> None: await async_client.batch( commands=[ - PublishMethod( + PublishRequest( channel="personal:1", data={"data": "Second data"}, ), - PublishMethod( + PublishRequest( channel="personal:2", data={"data": "First data"}, ), - BroadcastMethod( + BroadcastRequest( channels=["personal:1", "personal:2"], data={"data": "Third data"}, ), - PresenceMethod( + PresenceRequest( channel="personal:1", ), ] @@ -132,7 +132,7 @@ async def test_batch(async_client: AsyncClient) -> None: async def test_error_publish(async_client: AsyncClient) -> None: - with pytest.raises(APIError, match="unknown channel") as exc_info: + with pytest.raises(CentAPIError, match="unknown channel") as exc_info: await async_client.publish( "undefined_channel:123", {"data": "data"}, diff --git a/tests/test_grpc_validation.py b/tests/test_grpc_validation.py index bc50b5f..51b6633 100644 --- a/tests/test_grpc_validation.py +++ b/tests/test_grpc_validation.py @@ -3,7 +3,7 @@ import pytest from cent.client.grpc_client import GrpcClient -from cent.exceptions import APIError +from cent.exceptions import CentAPIError from cent.methods.disconnect_data import Disconnect from cent.types import StreamPosition, ChannelOptionsOverride, BoolValue from tests.conftest import UNKNOWN_CHANNEL_ERROR_CODE @@ -115,7 +115,7 @@ async def test_refresh(grpc_client: GrpcClient) -> None: async def test_error_publish(grpc_client: GrpcClient) -> None: - with pytest.raises(APIError, match="unknown channel") as exc_info: + with pytest.raises(CentAPIError, match="unknown channel") as exc_info: await grpc_client.publish( "undefined_channel:123", {"data": "data"}, diff --git a/tests/test_sync_validation.py b/tests/test_sync_validation.py index 54604a8..f18621b 100644 --- a/tests/test_sync_validation.py +++ b/tests/test_sync_validation.py @@ -3,8 +3,8 @@ import pytest from cent import Client -from cent.exceptions import APIError -from cent.methods import PublishMethod, BroadcastMethod, PresenceMethod +from cent.exceptions import CentAPIError +from cent.methods import PublishRequest, BroadcastRequest, PresenceRequest from cent.types import StreamPosition, Disconnect from tests.conftest import UNKNOWN_CHANNEL_ERROR_CODE @@ -112,19 +112,19 @@ def test_refresh(sync_client: Client) -> None: def test_batch(sync_client: Client) -> None: sync_client.batch( commands=[ - PublishMethod( + PublishRequest( channel="personal:1", data={"data": "Second data"}, ), - PublishMethod( + PublishRequest( channel="personal:2", data={"data": "First data"}, ), - BroadcastMethod( + BroadcastRequest( channels=["personal:1", "personal:2"], data={"data": "Third data"}, ), - PresenceMethod( + PresenceRequest( channel="personal:1", ), ] @@ -132,7 +132,7 @@ def test_batch(sync_client: Client) -> None: def test_error_publish(sync_client: Client) -> None: - with pytest.raises(APIError, match="unknown channel") as exc_info: + with pytest.raises(CentAPIError, match="unknown channel") as exc_info: sync_client.publish( "undefined_channel:123", {"data": "data"}, From 5b22a2ae241f5e78d0c66acb4577c4ec93e6bbed Mon Sep 17 00:00:00 2001 From: KatantDev Date: Sat, 3 Feb 2024 20:42:50 +1000 Subject: [PATCH 16/55] chore: remove context controller --- cent/context_controller.py | 32 -------------------------------- cent/methods/base.py | 11 ++--------- cent/types/base.py | 4 +--- 3 files changed, 3 insertions(+), 44 deletions(-) delete mode 100644 cent/context_controller.py diff --git a/cent/context_controller.py b/cent/context_controller.py deleted file mode 100644 index a58d22a..0000000 --- a/cent/context_controller.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import Optional, TYPE_CHECKING, Any - -from pydantic import BaseModel, PrivateAttr - -if TYPE_CHECKING: - from cent.client import Client - - -class ClientContextController(BaseModel): - _client: Optional["Client"] = PrivateAttr() - - def model_post_init(self, __context: Any) -> None: - self._client = __context.get("client") if __context else None - - def as_(self, client: Optional["Client"]) -> "ClientContextController": - """ - Bind an object to a client instance. - - :param client: Client instance - :return: self - """ - self._client = client - return self - - @property - def client_instance(self) -> Optional["Client"]: - """ - Get client instance. - - :return: Client instance - """ - return self._client diff --git a/cent/methods/base.py b/cent/methods/base.py index 6c37953..8a05cd9 100644 --- a/cent/methods/base.py +++ b/cent/methods/base.py @@ -3,8 +3,6 @@ from pydantic import BaseModel, ConfigDict -from cent.context_controller import ClientContextController - try: import orjson as _orjson # type: ignore[import-not-found] @@ -31,12 +29,7 @@ class Response(BaseModel, Generic[CentType]): result: Optional[CentType] = None -class CentRequest( - ClientContextController, - BaseModel, - Generic[CentType], - ABC, -): +class CentRequest(BaseModel, Generic[CentType], ABC): model_config = ConfigDict( extra="allow", populate_by_name=True, @@ -66,7 +59,7 @@ def __grpc_method__(self) -> type: pass -class NestedModel(ClientContextController, BaseModel, ABC): +class NestedModel(BaseModel, ABC): model_config = ConfigDict( extra="allow", populate_by_name=True, diff --git a/cent/types/base.py b/cent/types/base.py index 8d9b149..1a55b32 100644 --- a/cent/types/base.py +++ b/cent/types/base.py @@ -2,10 +2,8 @@ from pydantic import BaseModel, ConfigDict -from cent.context_controller import ClientContextController - -class CentResult(ClientContextController, BaseModel, ABC): +class CentResult(BaseModel, ABC): model_config = ConfigDict( use_enum_values=True, extra="allow", From 4ef6dbf3ec1f9bf65d9d2e6bb8075eb136a44655 Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Sat, 10 Feb 2024 19:19:30 +0200 Subject: [PATCH 17/55] simplify layout, make GRPC work with bytes --- Makefile | 19 ++ README.md | 21 +- cent/__init__.py | 86 +++++- cent/{methods => }/base.py | 17 +- cent/client/async_client.py | 11 +- cent/client/grpc_client.py | 6 +- cent/client/session/aiohttp.py | 6 +- cent/client/session/base.py | 4 +- cent/client/session/base_async.py | 3 +- cent/client/session/base_sync.py | 3 +- cent/client/session/grpc.py | 5 +- cent/client/session/requests.py | 5 +- cent/client/sync_client.py | 11 +- cent/exceptions.py | 2 +- cent/methods/__init__.py | 29 -- cent/methods/batch.py | 17 -- cent/methods/bool_value.py | 11 - cent/methods/broadcast.py | 36 --- cent/methods/channel_options_override.py | 24 -- cent/methods/channels.py | 17 -- cent/methods/disconnect.py | 26 -- cent/methods/disconnect_data.py | 14 - cent/methods/history.py | 24 -- cent/methods/history_remove.py | 15 -- cent/methods/info.py | 12 - cent/methods/presence.py | 19 -- cent/methods/presence_stats.py | 15 -- cent/methods/publish.py | 35 --- cent/methods/refresh.py | 25 -- cent/methods/stream_position.py | 13 - cent/methods/subscribe.py | 54 ---- cent/methods/unsubscribe.py | 23 -- cent/{protos => proto}/__init__.py | 0 cent/{protos => proto}/apiproto.proto | 0 .../{protos => proto}/centrifugal/__init__.py | 0 .../centrifugal/centrifugo/__init__.py | 0 .../centrifugal/centrifugo/api/__init__.py | 0 cent/requests.py | 250 ++++++++++++++++++ cent/results.py | 98 +++++++ cent/types.py | 141 ++++++++++ cent/types/__init__.py | 49 ---- cent/types/base.py | 15 -- cent/types/batch_result.py | 10 - cent/types/broadcast_result.py | 14 - cent/types/channel_info_result.py | 10 - cent/types/channels_result.py | 11 - cent/types/client_info_result.py | 16 -- cent/types/disconnect_result.py | 5 - cent/types/history_remove_result.py | 5 - cent/types/history_result.py | 17 -- cent/types/info_result.py | 11 - cent/types/metrics_result.py | 14 - cent/types/node_result.py | 32 --- cent/types/presence_result.py | 12 - cent/types/presence_stats_result.py | 12 - cent/types/process_result.py | 12 - cent/types/publication_result.py | 14 - cent/types/publish_result.py | 12 - cent/types/refresh_result.py | 5 - cent/types/subscribe_result.py | 5 - cent/types/unsubscribe_result.py | 5 - pyproject.toml | 6 +- tests/test_async_validation.py | 42 ++- tests/test_grpc_validation.py | 42 ++- tests/test_sync_validation.py | 40 ++- 65 files changed, 709 insertions(+), 804 deletions(-) create mode 100644 Makefile rename cent/{methods => }/base.py (85%) delete mode 100644 cent/methods/__init__.py delete mode 100644 cent/methods/batch.py delete mode 100644 cent/methods/bool_value.py delete mode 100644 cent/methods/broadcast.py delete mode 100644 cent/methods/channel_options_override.py delete mode 100644 cent/methods/channels.py delete mode 100644 cent/methods/disconnect.py delete mode 100644 cent/methods/disconnect_data.py delete mode 100644 cent/methods/history.py delete mode 100644 cent/methods/history_remove.py delete mode 100644 cent/methods/info.py delete mode 100644 cent/methods/presence.py delete mode 100644 cent/methods/presence_stats.py delete mode 100644 cent/methods/publish.py delete mode 100644 cent/methods/refresh.py delete mode 100644 cent/methods/stream_position.py delete mode 100644 cent/methods/subscribe.py delete mode 100644 cent/methods/unsubscribe.py rename cent/{protos => proto}/__init__.py (100%) rename cent/{protos => proto}/apiproto.proto (100%) rename cent/{protos => proto}/centrifugal/__init__.py (100%) rename cent/{protos => proto}/centrifugal/centrifugo/__init__.py (100%) rename cent/{protos => proto}/centrifugal/centrifugo/api/__init__.py (100%) create mode 100644 cent/requests.py create mode 100644 cent/results.py create mode 100644 cent/types.py delete mode 100644 cent/types/__init__.py delete mode 100644 cent/types/base.py delete mode 100644 cent/types/batch_result.py delete mode 100644 cent/types/broadcast_result.py delete mode 100644 cent/types/channel_info_result.py delete mode 100644 cent/types/channels_result.py delete mode 100644 cent/types/client_info_result.py delete mode 100644 cent/types/disconnect_result.py delete mode 100644 cent/types/history_remove_result.py delete mode 100644 cent/types/history_result.py delete mode 100644 cent/types/info_result.py delete mode 100644 cent/types/metrics_result.py delete mode 100644 cent/types/node_result.py delete mode 100644 cent/types/presence_result.py delete mode 100644 cent/types/presence_stats_result.py delete mode 100644 cent/types/process_result.py delete mode 100644 cent/types/publication_result.py delete mode 100644 cent/types/publish_result.py delete mode 100644 cent/types/refresh_result.py delete mode 100644 cent/types/subscribe_result.py delete mode 100644 cent/types/unsubscribe_result.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6f0d3fb --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +.PHONY: proto test lint lint-fix lint-ci + +dev: + pip install -e ".[dev]" + +proto: + poetry run python -m grpc_tools.protoc -I . --python_betterproto_out=./cent/proto cent/proto/apiproto.proto + +test: + pytest -vv tests + +lint: + ruff . + +lint-fix: + ruff . --fix + +lint-ci: + ruff . --output-format=github diff --git a/README.md b/README.md index 1970ee5..259686c 100644 --- a/README.md +++ b/README.md @@ -9,23 +9,18 @@ To install run: pip install cent ``` ---- - -### Centrifugo compatibility +## Centrifugo compatibility **Cent v5 and higher works only with Centrifugo v5**. -If you need to work with Centrifugo v3 then use Cent v4 -If you need to work with Centrifugo v2 then use Cent v3 ---- +* If you need to work with Centrifugo v3 then use Cent v4 +* If you need to work with Centrifugo v2 then use Cent v3 -### High-level library API +## High-level library API -First -see [available API methods in documentation](https://centrifugal.dev/docs/server/server_api#api-methods). +First see [available API methods in documentation](https://centrifugal.dev/docs/server/server_api#api-methods). -This library contains `Client` and `AsyncClient` class to send messages to -Centrifugo from your python-powered backend: +This library contains `Client` and `AsyncClient` class to send messages to Centrifugo from your python-powered backend: ```python import asyncio @@ -51,8 +46,6 @@ if __name__ == "__main__": asyncio.run(main()) ``` ---- - ### Client init arguments Required: @@ -97,5 +90,5 @@ pytest benchmarks --benchmark-verbose ### Generate code from proto file, if needed ```bash -poetry run python -m grpc_tools.protoc -I . --python_betterproto_out=./cent/protos cent/protos/apiproto.proto +poetry run python -m grpc_tools.protoc -I . --python_betterproto_out=./cent/proto cent/proto/apiproto.proto ``` diff --git a/cent/__init__.py b/cent/__init__.py index 794bffc..b84dd6c 100644 --- a/cent/__init__.py +++ b/cent/__init__.py @@ -11,6 +11,55 @@ RequestsSession, AiohttpSession, ) +from cent.requests import ( + CentRequest, + BroadcastRequest, + PublishRequest, + SubscribeRequest, + UnsubscribeRequest, + PresenceRequest, + PresenceStatsRequest, + HistoryRequest, + HistoryRemoveRequest, + RefreshRequest, + ChannelsRequest, + DisconnectRequest, + InfoRequest, + BatchRequest, +) +from cent.results import ( + PublishResult, + BroadcastResult, + SubscribeResult, + UnsubscribeResult, + PresenceResult, + PresenceStatsResult, + HistoryResult, + HistoryRemoveResult, + RefreshResult, + ChannelsResult, + DisconnectResult, + InfoResult, + BatchResult, +) +from cent.types import ( + StreamPosition, + ChannelOptionsOverride, + Disconnect, + BoolValue, + ProcessStats, + Node, + Publication, + ClientInfo, +) +from cent.exceptions import ( + CentError, + CentNetworkError, + CentClientDecodeError, + CentUnauthorizedError, + CentAPIError, + CentTransportError, +) from .__meta__ import __version__ @@ -22,7 +71,7 @@ __all__ = ( "__version__", "types", - "methods", + "requests", "exceptions", "Client", "AsyncClient", @@ -32,4 +81,39 @@ "RequestsSession", "AiohttpSession", "GrpcClient", + "CentRequest", + "BroadcastRequest", + "PublishRequest", + "SubscribeRequest", + "UnsubscribeRequest", + "PresenceRequest", + "PresenceStatsRequest", + "HistoryRequest", + "HistoryRemoveRequest", + "RefreshRequest", + "ChannelsRequest", + "DisconnectRequest", + "InfoRequest", + "BatchRequest", + "PublishResult", + "BroadcastResult", + "SubscribeResult", + "UnsubscribeResult", + "PresenceResult", + "PresenceStatsResult", + "HistoryResult", + "HistoryRemoveResult", + "RefreshResult", + "ChannelsResult", + "DisconnectResult", + "InfoResult", + "BatchResult", + "StreamPosition", + "ChannelOptionsOverride", + "Disconnect", + "BoolValue", + "ProcessStats", + "Node", + "Publication", + "ClientInfo", ) diff --git a/cent/methods/base.py b/cent/base.py similarity index 85% rename from cent/methods/base.py rename to cent/base.py index 8a05cd9..a6a7667 100644 --- a/cent/methods/base.py +++ b/cent/base.py @@ -3,6 +3,19 @@ from pydantic import BaseModel, ConfigDict + +class BaseResult(BaseModel, ABC): + model_config = ConfigDict( + use_enum_values=True, + extra="allow", + validate_assignment=True, + frozen=True, + populate_by_name=True, + arbitrary_types_allowed=True, + defer_build=True, + ) + + try: import orjson as _orjson # type: ignore[import-not-found] @@ -16,6 +29,7 @@ def json_dumps(x: Any) -> bytes: json_loads = json.loads + CentType = TypeVar("CentType", bound=Any) @@ -39,10 +53,8 @@ class CentRequest(BaseModel, Generic[CentType], ABC): if TYPE_CHECKING: __returning__: ClassVar[type] __api_method__: ClassVar[str] - __grpc_method__: ClassVar[type] else: - @property @abstractmethod def __returning__(self) -> type: @@ -68,7 +80,6 @@ class NestedModel(BaseModel, ABC): if TYPE_CHECKING: __grpc_method__: ClassVar[type] else: - @property @abstractmethod def __grpc_method__(self) -> type: diff --git a/cent/client/async_client.py b/cent/client/async_client.py index 0bf438d..0726cd8 100644 --- a/cent/client/async_client.py +++ b/cent/client/async_client.py @@ -1,7 +1,7 @@ from typing import List, Optional, Any, Dict, TypeVar from cent.client.session import BaseAsyncSession, AiohttpSession -from cent.methods import ( +from cent.requests import ( CentRequest, BroadcastRequest, PublishRequest, @@ -15,9 +15,9 @@ ChannelsRequest, DisconnectRequest, InfoRequest, + BatchRequest, ) -from cent.methods.batch import BatchRequest -from cent.types import ( +from cent.results import ( PublishResult, BroadcastResult, SubscribeResult, @@ -30,11 +30,14 @@ ChannelsResult, DisconnectResult, InfoResult, +) +from cent.types import ( StreamPosition, ChannelOptionsOverride, Disconnect, ) -from cent.types.batch_result import BatchResult +from cent.results import BatchResult + T = TypeVar("T") diff --git a/cent/client/grpc_client.py b/cent/client/grpc_client.py index 54c5dcd..6eb3af8 100644 --- a/cent/client/grpc_client.py +++ b/cent/client/grpc_client.py @@ -1,7 +1,7 @@ from typing import Any, Optional, Dict, TypeVar, List from cent.client.session.grpc import GrpcSession -from cent.methods import ( +from cent.requests import ( CentRequest, BroadcastRequest, PublishRequest, @@ -16,7 +16,7 @@ DisconnectRequest, InfoRequest, ) -from cent.types import ( +from cent.results import ( PublishResult, BroadcastResult, SubscribeResult, @@ -29,6 +29,8 @@ ChannelsResult, DisconnectResult, InfoResult, +) +from cent.types import ( StreamPosition, ChannelOptionsOverride, Disconnect, diff --git a/cent/client/session/aiohttp.py b/cent/client/session/aiohttp.py index 78ce378..3d0baf5 100644 --- a/cent/client/session/aiohttp.py +++ b/cent/client/session/aiohttp.py @@ -4,12 +4,12 @@ from aiohttp import ClientSession, ClientError from cent.client.session.base_async import BaseAsyncSession -from cent.methods.base import CentRequest, CentType +from cent.base import CentType +from cent.requests import CentRequest, BatchRequest from cent.exceptions import CentNetworkError -from cent.methods.batch import BatchRequest if TYPE_CHECKING: - from cent.client.async_client import AsyncClient + from cent.client import AsyncClient class AiohttpSession(BaseAsyncSession): diff --git a/cent/client/session/base.py b/cent/client/session/base.py index 8cdcb2e..c9c56cb 100644 --- a/cent/client/session/base.py +++ b/cent/client/session/base.py @@ -10,13 +10,13 @@ CentUnauthorizedError, CentTransportError, ) -from cent.methods.base import ( +from cent.base import ( json_loads as _json_loads, CentRequest, CentType, Response, ) -from cent.methods.batch import BatchRequest +from cent.requests import BatchRequest if TYPE_CHECKING: from cent.client.sync_client import Client diff --git a/cent/client/session/base_async.py b/cent/client/session/base_async.py index a488558..1916f93 100644 --- a/cent/client/session/base_async.py +++ b/cent/client/session/base_async.py @@ -2,7 +2,8 @@ from typing import TYPE_CHECKING, Any, Optional, cast from cent.client.session.base import BaseSession -from cent.methods.base import CentRequest, CentType +from cent.requests import CentRequest +from cent.base import CentType if TYPE_CHECKING: from cent.client.async_client import AsyncClient diff --git a/cent/client/session/base_sync.py b/cent/client/session/base_sync.py index 6c553ae..5b739c2 100644 --- a/cent/client/session/base_sync.py +++ b/cent/client/session/base_sync.py @@ -2,7 +2,8 @@ from typing import TYPE_CHECKING, Optional from cent.client.session.base import BaseSession -from cent.methods.base import CentRequest, CentType +from cent.requests import CentRequest +from cent.base import CentType if TYPE_CHECKING: from cent.client.sync_client import Client diff --git a/cent/client/session/grpc.py b/cent/client/session/grpc.py index b6b08d5..bd86fd2 100644 --- a/cent/client/session/grpc.py +++ b/cent/client/session/grpc.py @@ -6,9 +6,10 @@ from grpclib.client import Channel from pydantic import TypeAdapter, BaseModel -from cent.protos.centrifugal.centrifugo.api import CentrifugoApiStub +from cent.proto.centrifugal.centrifugo.api import CentrifugoApiStub from cent.exceptions import CentAPIError, CentTransportError -from cent.methods.base import CentRequest, CentType, Response, Error +from cent.requests import CentRequest +from cent.base import CentType, Response, Error if TYPE_CHECKING: from cent.client.grpc_client import GrpcClient diff --git a/cent/client/session/requests.py b/cent/client/session/requests.py index 72d7f41..832b7b0 100644 --- a/cent/client/session/requests.py +++ b/cent/client/session/requests.py @@ -3,10 +3,11 @@ import requests from requests import Session -from cent.methods.base import CentRequest, CentType +from cent.requests import CentRequest +from cent.base import CentType from cent.client.session.base_sync import BaseSyncSession from cent.exceptions import CentNetworkError -from cent.methods.batch import BatchRequest +from cent.requests import BatchRequest if TYPE_CHECKING: from cent.client.sync_client import Client diff --git a/cent/client/sync_client.py b/cent/client/sync_client.py index 639a194..b4837a0 100644 --- a/cent/client/sync_client.py +++ b/cent/client/sync_client.py @@ -1,7 +1,7 @@ from typing import List, Optional, Any, Dict, TypeVar from cent.client.session import BaseSyncSession, RequestsSession -from cent.methods import ( +from cent.requests import ( CentRequest, BroadcastRequest, PublishRequest, @@ -15,9 +15,9 @@ ChannelsRequest, DisconnectRequest, InfoRequest, + BatchRequest ) -from cent.methods.batch import BatchRequest -from cent.types import ( +from cent.results import ( PublishResult, BroadcastResult, SubscribeResult, @@ -30,11 +30,14 @@ ChannelsResult, DisconnectResult, InfoResult, +) +from cent.types import ( StreamPosition, ChannelOptionsOverride, Disconnect, ) -from cent.types.batch_result import BatchResult +from cent.results import BatchResult + T = TypeVar("T") diff --git a/cent/exceptions.py b/cent/exceptions.py index dba1c5e..3e080ec 100644 --- a/cent/exceptions.py +++ b/cent/exceptions.py @@ -1,4 +1,4 @@ -from cent.methods.base import CentRequest, CentType +from cent.base import CentType, CentRequest class CentError(Exception): diff --git a/cent/methods/__init__.py b/cent/methods/__init__.py deleted file mode 100644 index 8723334..0000000 --- a/cent/methods/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -from .base import CentRequest -from .broadcast import BroadcastRequest -from .publish import PublishRequest -from .subscribe import SubscribeRequest -from .unsubscribe import UnsubscribeRequest -from .presence import PresenceRequest -from .presence_stats import PresenceStatsRequest -from .history import HistoryRequest -from .history_remove import HistoryRemoveRequest -from .refresh import RefreshRequest -from .channels import ChannelsRequest -from .disconnect import DisconnectRequest -from .info import InfoRequest - -__all__ = ( - "CentRequest", - "BroadcastRequest", - "PublishRequest", - "SubscribeRequest", - "UnsubscribeRequest", - "PresenceRequest", - "PresenceStatsRequest", - "HistoryRequest", - "HistoryRemoveRequest", - "RefreshRequest", - "ChannelsRequest", - "DisconnectRequest", - "InfoRequest", -) diff --git a/cent/methods/batch.py b/cent/methods/batch.py deleted file mode 100644 index bd8114e..0000000 --- a/cent/methods/batch.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import List, Any - -from cent.protos.centrifugal.centrifugo.api import BatchRequest as GrpcBatchRequest -from cent.methods import CentRequest -from cent.types.batch_result import BatchResult - - -class BatchRequest(CentRequest[BatchResult]): - """Batch request.""" - - __returning__ = BatchResult - __api_method__ = "batch" - - __grpc_method__ = GrpcBatchRequest - - commands: List[CentRequest[Any]] - """List of commands to execute in batch.""" diff --git a/cent/methods/bool_value.py b/cent/methods/bool_value.py deleted file mode 100644 index d981acd..0000000 --- a/cent/methods/bool_value.py +++ /dev/null @@ -1,11 +0,0 @@ -from cent.protos.centrifugal.centrifugo.api import BoolValue as GrpcBoolValue -from cent.methods.base import NestedModel - - -class BoolValue(NestedModel): - """Bool value.""" - - __grpc_method__ = GrpcBoolValue - - value: bool - """Bool value.""" diff --git a/cent/methods/broadcast.py b/cent/methods/broadcast.py deleted file mode 100644 index ae5a220..0000000 --- a/cent/methods/broadcast.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import Dict, Optional, List, Any - -from pydantic import field_serializer, Field -from pydantic_core.core_schema import SerializationInfo - -from cent.protos.centrifugal.centrifugo.api import BroadcastRequest as GrpcBroadcastRequest -from cent.methods.base import CentRequest, json_dumps -from cent.types.broadcast_result import BroadcastResult - - -class BroadcastRequest(CentRequest[BroadcastResult]): - """Broadcast request.""" - - __returning__ = BroadcastResult - __api_method__ = "broadcast" - - __grpc_method__ = GrpcBroadcastRequest - - channels: List[str] - """List of channels to publish data to.""" - data: Any - """Custom JSON data to publish into a channel.""" - skip_history: Optional[bool] = None - """Skip adding publications to channels' history for this request.""" - tags: Optional[Dict[str, str]] = None - """Publication tags - map with arbitrary string keys and values which is attached to publication and will be delivered to clients.""" - b64data: Optional[str] = Field(None, alias="b64_data") - """Custom binary data to publish into a channel encoded to base64 so it's possible to use HTTP API to send binary to clients. Centrifugo will decode it from base64 before publishing. In case of GRPC you can publish binary using data field.""" - idempotency_key: Optional[str] = None - """Optional idempotency key to drop duplicate publications upon retries. It acts per channel. Centrifugo currently keeps the cache of idempotent publish results during 5 minutes window. Available since Centrifugo v5.2.0""" - - @field_serializer("data") - def grpc_serialize_data(self, data: Any, _info: SerializationInfo) -> Any: - if _info.mode == "grpc": - return json_dumps(data) - return data diff --git a/cent/methods/channel_options_override.py b/cent/methods/channel_options_override.py deleted file mode 100644 index 5d0c12f..0000000 --- a/cent/methods/channel_options_override.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import Optional - -from cent.protos.centrifugal.centrifugo.api import ( - SubscribeOptionOverride as GrpcChannelOptionOverride, -) -from cent.methods.base import NestedModel -from cent.methods.bool_value import BoolValue - - -class ChannelOptionsOverride(NestedModel): - """Override object.""" - - __grpc_method__ = GrpcChannelOptionOverride - - presence: Optional[BoolValue] = None - """Override presence.""" - join_leave: Optional[BoolValue] = None - """Override join_leave.""" - force_push_join_leave: Optional[BoolValue] = None - """Override force_push_join_leave.""" - force_positioning: Optional[BoolValue] = None - """Override force_positioning.""" - force_recovery: Optional[BoolValue] = None - """Override force_recovery.""" diff --git a/cent/methods/channels.py b/cent/methods/channels.py deleted file mode 100644 index da487e7..0000000 --- a/cent/methods/channels.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Optional - -from cent.protos.centrifugal.centrifugo.api import ChannelsRequest as GrpcChannelsRequest -from cent.methods import CentRequest -from cent.types.channels_result import ChannelsResult - - -class ChannelsRequest(CentRequest[ChannelsResult]): - """Channels request.""" - - __returning__ = ChannelsResult - __api_method__ = "channels" - - __grpc_method__ = GrpcChannelsRequest - - pattern: Optional[str] = None - """Pattern to filter channels, we are using https://github.com/gobwas/glob library for matching.""" diff --git a/cent/methods/disconnect.py b/cent/methods/disconnect.py deleted file mode 100644 index a341b73..0000000 --- a/cent/methods/disconnect.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Optional, List - -from cent.protos.centrifugal.centrifugo.api import DisconnectRequest as GrpcDisconnectRequest -from cent.methods import CentRequest -from cent.types import Disconnect -from cent.types.disconnect_result import DisconnectResult - - -class DisconnectRequest(CentRequest[DisconnectResult]): - """Disconnect request.""" - - __returning__ = DisconnectResult - __api_method__ = "disconnect" - - __grpc_method__ = GrpcDisconnectRequest - - user: str - """User ID to disconnect.""" - client: Optional[str] = None - """Specific client ID to disconnect (user still required to be set).""" - session: Optional[str] = None - """Specific client session to disconnect (user still required to be set).""" - whitelist: Optional[List[str]] = None - """Array of client IDs to keep.""" - disconnect: Optional[Disconnect] = None - """Provide custom disconnect object, see below.""" diff --git a/cent/methods/disconnect_data.py b/cent/methods/disconnect_data.py deleted file mode 100644 index 28a7474..0000000 --- a/cent/methods/disconnect_data.py +++ /dev/null @@ -1,14 +0,0 @@ -from cent.protos.centrifugal.centrifugo.api import Disconnect as GrpcDisconnect - -from cent.methods.base import NestedModel - - -class Disconnect(NestedModel): - """Disconnect data.""" - - __grpc_method__ = GrpcDisconnect - - code: int - """Disconnect code.""" - reason: str - """Disconnect reason.""" diff --git a/cent/methods/history.py b/cent/methods/history.py deleted file mode 100644 index 7f55d95..0000000 --- a/cent/methods/history.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import Optional - -from cent.protos.centrifugal.centrifugo.api import HistoryRequest as GrpcHistoryRequest -from cent.methods import CentRequest -from cent.types import StreamPosition -from cent.types.history_result import HistoryResult - - -class HistoryRequest(CentRequest[HistoryResult]): - """History request.""" - - __returning__ = HistoryResult - __api_method__ = "history" - - __grpc_method__ = GrpcHistoryRequest - - channel: str - """Name of channel to call history from.""" - limit: Optional[int] = None - """Limit number of returned publications, if not set in request then only current stream position information will present in result (without any publications).""" - since: Optional[StreamPosition] = None - """To return publications after this position.""" - reverse: Optional[bool] = None - """Iterate in reversed order (from latest to earliest).""" diff --git a/cent/methods/history_remove.py b/cent/methods/history_remove.py deleted file mode 100644 index 3e9362c..0000000 --- a/cent/methods/history_remove.py +++ /dev/null @@ -1,15 +0,0 @@ -from cent.protos.centrifugal.centrifugo.api import HistoryRemoveRequest as GrpcHistoryRemoveRequest -from cent.methods import CentRequest -from cent.types.history_remove_result import HistoryRemoveResult - - -class HistoryRemoveRequest(CentRequest[HistoryRemoveResult]): - """History remove request.""" - - __returning__ = HistoryRemoveResult - __api_method__ = "history_remove" - - __grpc_method__ = GrpcHistoryRemoveRequest - - channel: str - """Name of channel to remove history.""" diff --git a/cent/methods/info.py b/cent/methods/info.py deleted file mode 100644 index cfb6ceb..0000000 --- a/cent/methods/info.py +++ /dev/null @@ -1,12 +0,0 @@ -from cent.protos.centrifugal.centrifugo.api import InfoRequest as GrpcInfoRequest -from cent.methods import CentRequest -from cent.types.info_result import InfoResult - - -class InfoRequest(CentRequest[InfoResult]): - """Info request.""" - - __returning__ = InfoResult - __api_method__ = "info" - - __grpc_method__ = GrpcInfoRequest diff --git a/cent/methods/presence.py b/cent/methods/presence.py deleted file mode 100644 index b4b2d56..0000000 --- a/cent/methods/presence.py +++ /dev/null @@ -1,19 +0,0 @@ -from cent.protos.centrifugal.centrifugo.api import ( - PresenceRequest as GrpcPresenceRequest, - PresenceResult as GrpcPresenceResult, -) -from cent.methods import CentRequest -from cent.types.presence_result import PresenceResult - - -class PresenceRequest(CentRequest[PresenceResult]): - """Presence request.""" - - __returning__ = PresenceResult - __api_method__ = "presence" - - __grpc_returning__ = GrpcPresenceResult - __grpc_method__ = GrpcPresenceRequest - - channel: str - """Name of channel to call presence from.""" diff --git a/cent/methods/presence_stats.py b/cent/methods/presence_stats.py deleted file mode 100644 index 031eb9a..0000000 --- a/cent/methods/presence_stats.py +++ /dev/null @@ -1,15 +0,0 @@ -from cent.protos.centrifugal.centrifugo.api import PresenceStatsRequest as GrpcPresenceStatsRequest -from cent.methods import CentRequest -from cent.types.presence_stats_result import PresenceStatsResult - - -class PresenceStatsRequest(CentRequest[PresenceStatsResult]): - """Presence request.""" - - __returning__ = PresenceStatsResult - __api_method__ = "presence_stats" - - __grpc_method__ = GrpcPresenceStatsRequest - - channel: str - """Name of channel to call presence from.""" diff --git a/cent/methods/publish.py b/cent/methods/publish.py deleted file mode 100644 index a40f7b7..0000000 --- a/cent/methods/publish.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import Any, Dict, Optional - -from cent.protos.centrifugal.centrifugo.api import PublishRequest as GrpcPublishRequest -from pydantic import Field, field_serializer, SerializationInfo - -from cent.methods.base import CentRequest, json_dumps -from cent.types.publish_result import PublishResult - - -class PublishRequest(CentRequest[PublishResult]): - """Publish request.""" - - __returning__ = PublishResult - __api_method__ = "publish" - - __grpc_method__ = GrpcPublishRequest - - channel: str - """Name of channel to publish.""" - data: Any - """Custom JSON data to publish into a channel.""" - skip_history: Optional[bool] = None - """Skip adding publication to history for this request.""" - tags: Optional[Dict[str, str]] = None - """Publication tags - map with arbitrary string keys and values which is attached to publication and will be delivered to clients.""" - b64data: Optional[str] = Field(None, alias="b64_data") - """Custom binary data to publish into a channel encoded to base64 so it's possible to use HTTP API to send binary to clients. Centrifugo will decode it from base64 before publishing. In case of GRPC you can publish binary using data field.""" - idempotency_key: Optional[str] = None - """Optional idempotency key to drop duplicate publications upon retries. It acts per channel. Centrifugo currently keeps the cache of idempotent publish results during 5 minutes window. Available since Centrifugo v5.2.0""" - - @field_serializer("data") - def grpc_serialize_data(self, data: Any, _info: SerializationInfo) -> Any: - if _info.mode == "grpc": - return json_dumps(data) - return data diff --git a/cent/methods/refresh.py b/cent/methods/refresh.py deleted file mode 100644 index 90d7d54..0000000 --- a/cent/methods/refresh.py +++ /dev/null @@ -1,25 +0,0 @@ -from typing import Optional - -from cent.protos.centrifugal.centrifugo.api import RefreshRequest as GrpcRefreshRequest -from cent.methods import CentRequest -from cent.types.refresh_result import RefreshResult - - -class RefreshRequest(CentRequest[RefreshResult]): - """Refresh request.""" - - __returning__ = RefreshResult - __api_method__ = "refresh" - - __grpc_method__ = GrpcRefreshRequest - - user: str - """User ID to refresh.""" - client: Optional[str] = None - """Client ID to refresh (user still required to be set).""" - session: Optional[str] = None - """Specific client session to refresh (user still required to be set).""" - expired: Optional[bool] = None - """Mark connection as expired and close with Disconnect Expired reason.""" - expire_at: Optional[int] = None - """Unix time (in seconds) in the future when the connection will expire.""" diff --git a/cent/methods/stream_position.py b/cent/methods/stream_position.py deleted file mode 100644 index 832419d..0000000 --- a/cent/methods/stream_position.py +++ /dev/null @@ -1,13 +0,0 @@ -from cent.protos.centrifugal.centrifugo.api import StreamPosition as GrpcStreamPosition -from cent.methods.base import NestedModel - - -class StreamPosition(NestedModel): - """Stream position.""" - - __grpc_method__ = GrpcStreamPosition - - offset: int - """Offset of publication in history stream.""" - epoch: str - """Epoch of current stream.""" diff --git a/cent/methods/subscribe.py b/cent/methods/subscribe.py deleted file mode 100644 index 2105ff2..0000000 --- a/cent/methods/subscribe.py +++ /dev/null @@ -1,54 +0,0 @@ -from typing import Optional, Any - -from pydantic import Field, field_serializer -from pydantic_core.core_schema import SerializationInfo - -from cent.protos.centrifugal.centrifugo.api import SubscribeRequest as GrpcSubscribeRequest -from cent.methods.base import CentRequest, json_dumps -from cent.types import ( - StreamPosition, - SubscribeResult, - ChannelOptionsOverride, -) - - -class SubscribeRequest(CentRequest[SubscribeResult]): - """Subscribe request.""" - - __returning__ = SubscribeResult - __api_method__ = "subscribe" - - __grpc_method__ = GrpcSubscribeRequest - - user: str - """User ID to subscribe.""" - channel: str - """Name of channel to subscribe user to.""" - info: Optional[Any] = None - """Attach custom data to subscription (will be used in presence and join/leave messages).""" - b64info: Optional[str] = Field(None, alias="b64_info") - """info in base64 for binary mode (will be decoded by Centrifugo).""" - client: Optional[str] = None - """Specific client ID to subscribe (user still required to be set, will ignore other user connections with different client IDs).""" - session: Optional[str] = None - """Specific client session to subscribe (user still required to be set).""" - data: Optional[Any] = None - """Custom subscription data (will be sent to client in Subscribe push).""" - b64data: Optional[str] = Field(None, alias="b64_data") - """Same as data but in base64 format (will be decoded by Centrifugo).""" - recover_since: Optional[StreamPosition] = None - """Stream position to recover from.""" - override: Optional[ChannelOptionsOverride] = None - """Allows dynamically override some channel options defined in Centrifugo configuration (see below available fields).""" - - @field_serializer("data", when_used="unless-none") - def grpc_serialize_data(self, data: Any, _info: SerializationInfo) -> Any: - if _info.mode == "grpc": - return json_dumps(data) - return data - - @field_serializer("info", when_used="unless-none") - def grpc_serialize_info(self, info: Any, _info: SerializationInfo) -> Any: - if _info.mode == "grpc": - return json_dumps(info) - return info diff --git a/cent/methods/unsubscribe.py b/cent/methods/unsubscribe.py deleted file mode 100644 index fafa0d5..0000000 --- a/cent/methods/unsubscribe.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import Optional - -from cent.protos.centrifugal.centrifugo.api import UnsubscribeRequest as GrpcUnsubscribeRequest -from cent.methods.base import CentRequest -from cent.types.unsubscribe_result import UnsubscribeResult - - -class UnsubscribeRequest(CentRequest[UnsubscribeResult]): - """Unsubscribe request.""" - - __returning__ = UnsubscribeResult - __api_method__ = "unsubscribe" - - __grpc_method__ = GrpcUnsubscribeRequest - - user: str - """User ID to unsubscribe.""" - channel: str - """Name of channel to unsubscribe user to.""" - client: Optional[str] = None - """Specific client ID to unsubscribe (user still required to be set).""" - session: Optional[str] = None - """Specific client session to disconnect (user still required to be set).""" diff --git a/cent/protos/__init__.py b/cent/proto/__init__.py similarity index 100% rename from cent/protos/__init__.py rename to cent/proto/__init__.py diff --git a/cent/protos/apiproto.proto b/cent/proto/apiproto.proto similarity index 100% rename from cent/protos/apiproto.proto rename to cent/proto/apiproto.proto diff --git a/cent/protos/centrifugal/__init__.py b/cent/proto/centrifugal/__init__.py similarity index 100% rename from cent/protos/centrifugal/__init__.py rename to cent/proto/centrifugal/__init__.py diff --git a/cent/protos/centrifugal/centrifugo/__init__.py b/cent/proto/centrifugal/centrifugo/__init__.py similarity index 100% rename from cent/protos/centrifugal/centrifugo/__init__.py rename to cent/proto/centrifugal/centrifugo/__init__.py diff --git a/cent/protos/centrifugal/centrifugo/api/__init__.py b/cent/proto/centrifugal/centrifugo/api/__init__.py similarity index 100% rename from cent/protos/centrifugal/centrifugo/api/__init__.py rename to cent/proto/centrifugal/centrifugo/api/__init__.py diff --git a/cent/requests.py b/cent/requests.py new file mode 100644 index 0000000..3d8f496 --- /dev/null +++ b/cent/requests.py @@ -0,0 +1,250 @@ +from typing import Any, Optional, Dict, List + +from pydantic import Field + +from cent.proto.centrifugal.centrifugo.api import ( + ChannelsRequest as GrpcChannelsRequest, + PublishRequest as GrpcPublishRequest, + PresenceStatsRequest as GrpcPresenceStatsRequest, + InfoRequest as GrpcInfoRequest, + BroadcastRequest as GrpcBroadcastRequest, + BatchRequest as GrpcBatchRequest, + RefreshRequest as GrpcRefreshRequest, + UnsubscribeRequest as GrpcUnsubscribeRequest, + SubscribeRequest as GrpcSubscribeRequest, + HistoryRequest as GrpcHistoryRequest, + HistoryRemoveRequest as GrpcHistoryRemoveRequest, + PresenceRequest as GrpcPresenceRequest, + DisconnectRequest as GrpcDisconnectRequest, +) + +from cent.base import CentRequest +from cent.types import StreamPosition, ChannelOptionsOverride, Disconnect + +from cent.results import ( + BatchResult, BroadcastResult, ChannelsResult, + DisconnectResult, HistoryResult, HistoryRemoveResult, + InfoResult, PresenceResult, PresenceStatsResult, + PublishResult, RefreshResult, SubscribeResult, + UnsubscribeResult, +) + + +class BatchRequest(CentRequest[BatchResult]): + """Batch request.""" + + __returning__ = BatchResult + __api_method__ = "batch" + __grpc_method__ = GrpcBatchRequest + + commands: List[CentRequest[Any]] + """List of commands to execute in batch.""" + + +class BroadcastRequest(CentRequest[BroadcastResult]): + """Broadcast request.""" + + __returning__ = BroadcastResult + __api_method__ = "broadcast" + __grpc_method__ = GrpcBroadcastRequest + + channels: List[str] + """List of channels to publish data to.""" + data: Any + """Custom JSON data to publish into a channel.""" + skip_history: Optional[bool] = None + """Skip adding publications to channels' history for this request.""" + tags: Optional[Dict[str, str]] = None + """Publication tags - map with arbitrary string keys and values which is attached to + publication and will be delivered to clients.""" + b64data: Optional[str] = Field(None, alias="b64_data") + """Custom binary data to publish into a channel encoded to base64 so it's possible + to use HTTP API to send binary to clients. Centrifugo will decode it from base64 before + publishing. In case of GRPC you can publish binary using data field.""" + idempotency_key: Optional[str] = None + """Optional idempotency key to drop duplicate publications upon retries. It acts per channel. + Centrifugo currently keeps the cache of idempotent publish results during 5 minutes window. + Available since Centrifugo v5.2.0""" + + +class ChannelsRequest(CentRequest[ChannelsResult]): + """Channels request.""" + + __returning__ = ChannelsResult + __api_method__ = "channels" + __grpc_method__ = GrpcChannelsRequest + + pattern: Optional[str] = None + """Pattern to filter channels, we are using https://github.com/gobwas/glob + library for matching.""" + + +class DisconnectRequest(CentRequest[DisconnectResult]): + """Disconnect request.""" + + __returning__ = DisconnectResult + __api_method__ = "disconnect" + __grpc_method__ = GrpcDisconnectRequest + + user: str + """User ID to disconnect.""" + client: Optional[str] = None + """Specific client ID to disconnect (user still required to be set).""" + session: Optional[str] = None + """Specific client session to disconnect (user still required to be set).""" + whitelist: Optional[List[str]] = None + """Array of client IDs to keep.""" + disconnect: Optional[Disconnect] = None + """Provide custom disconnect object, see below.""" + + +class HistoryRequest(CentRequest[HistoryResult]): + """History request.""" + + __returning__ = HistoryResult + __api_method__ = "history" + __grpc_method__ = GrpcHistoryRequest + + channel: str + """Name of channel to call history from.""" + limit: Optional[int] = None + """Limit number of returned publications, if not set in request then only current stream + position information will present in result (without any publications).""" + since: Optional[StreamPosition] = None + """To return publications after this position.""" + reverse: Optional[bool] = None + """Iterate in reversed order (from latest to earliest).""" + + +class HistoryRemoveRequest(CentRequest[HistoryRemoveResult]): + """History remove request.""" + + __returning__ = HistoryRemoveResult + __api_method__ = "history_remove" + __grpc_method__ = GrpcHistoryRemoveRequest + + channel: str + """Name of channel to remove history.""" + + +class InfoRequest(CentRequest[InfoResult]): + """Info request.""" + + __returning__ = InfoResult + __api_method__ = "info" + __grpc_method__ = GrpcInfoRequest + + +class PresenceRequest(CentRequest[PresenceResult]): + """Presence request.""" + + __returning__ = PresenceResult + __api_method__ = "presence" + __grpc_method__ = GrpcPresenceRequest + + channel: str + """Name of channel to call presence from.""" + + +class PresenceStatsRequest(CentRequest[PresenceStatsResult]): + """Presence request.""" + + __returning__ = PresenceStatsResult + __api_method__ = "presence_stats" + __grpc_method__ = GrpcPresenceStatsRequest + + channel: str + """Name of channel to call presence from.""" + + +class PublishRequest(CentRequest[PublishResult]): + """Publish request.""" + + __returning__ = PublishResult + __api_method__ = "publish" + __grpc_method__ = GrpcPublishRequest + + channel: str + """Name of channel to publish.""" + data: Any + """Custom JSON data to publish into a channel.""" + skip_history: Optional[bool] = None + """Skip adding publication to history for this request.""" + tags: Optional[Dict[str, str]] = None + """Publication tags - map with arbitrary string keys and values which is attached to + publication and will be delivered to clients.""" + b64data: Optional[str] = Field(None, alias="b64_data") + """Custom binary data to publish into a channel encoded to base64 so it's possible to use + HTTP API to send binary to clients. Centrifugo will decode it from base64 before publishing. + In case of GRPC you can publish binary using data field.""" + idempotency_key: Optional[str] = None + """Optional idempotency key to drop duplicate publications upon retries. It acts per channel. + Centrifugo currently keeps the cache of idempotent publish results during 5 minutes window. + Available since Centrifugo v5.2.0""" + + +class RefreshRequest(CentRequest[RefreshResult]): + """Refresh request.""" + + __returning__ = RefreshResult + __api_method__ = "refresh" + __grpc_method__ = GrpcRefreshRequest + + user: str + """User ID to refresh.""" + client: Optional[str] = None + """Client ID to refresh (user still required to be set).""" + session: Optional[str] = None + """Specific client session to refresh (user still required to be set).""" + expired: Optional[bool] = None + """Mark connection as expired and close with Disconnect Expired reason.""" + expire_at: Optional[int] = None + """Unix time (in seconds) in the future when the connection will expire.""" + + +class SubscribeRequest(CentRequest[SubscribeResult]): + """Subscribe request.""" + + __returning__ = SubscribeResult + __api_method__ = "subscribe" + __grpc_method__ = GrpcSubscribeRequest + + user: str + """User ID to subscribe.""" + channel: str + """Name of channel to subscribe user to.""" + info: Optional[Any] = None + """Attach custom data to subscription (will be used in presence and join/leave messages).""" + b64info: Optional[str] = Field(None, alias="b64_info") + """info in base64 for binary mode (will be decoded by Centrifugo).""" + client: Optional[str] = None + """Specific client ID to subscribe (user still required to be set, will ignore other user + connections with different client IDs).""" + session: Optional[str] = None + """Specific client session to subscribe (user still required to be set).""" + data: Optional[Any] = None + """Custom subscription data (will be sent to client in Subscribe push).""" + b64data: Optional[str] = Field(None, alias="b64_data") + """Same as data but in base64 format (will be decoded by Centrifugo).""" + recover_since: Optional[StreamPosition] = None + """Stream position to recover from.""" + override: Optional[ChannelOptionsOverride] = None + """Allows dynamically override some channel options defined in Centrifugo + configuration (see below available fields).""" + + +class UnsubscribeRequest(CentRequest[UnsubscribeResult]): + """Unsubscribe request.""" + + __returning__ = UnsubscribeResult + __api_method__ = "unsubscribe" + __grpc_method__ = GrpcUnsubscribeRequest + + user: str + """User ID to unsubscribe.""" + channel: str + """Name of channel to unsubscribe user to.""" + client: Optional[str] = None + """Specific client ID to unsubscribe (user still required to be set).""" + session: Optional[str] = None + """Specific client session to disconnect (user still required to be set).""" diff --git a/cent/results.py b/cent/results.py new file mode 100644 index 0000000..dc6d6d8 --- /dev/null +++ b/cent/results.py @@ -0,0 +1,98 @@ +from typing import List, Any, Optional, Dict + +from pydantic import Field + +from cent.base import BaseResult +from cent.base import Response +from cent.types import Publication, Node, ClientInfo + + +class BatchResult(BaseResult): + """Batch response.""" + + replies: List[Any] + """List of results from batch request.""" + + +class PublishResult(BaseResult): + """Publish result.""" + + offset: Optional[int] = None + """Offset of publication in history stream.""" + epoch: Optional[str] = None + """Epoch of current stream.""" + + +class BroadcastResult(BaseResult): + """Publish result.""" + + responses: List[Response[PublishResult]] = Field(default_factory=list) + """Responses for each individual publish (with possible error and publish result).""" + + +class ChannelInfoResult(BaseResult): + """Channel info result.""" + + num_clients: int = Field(default=0) + """Total number of connections currently subscribed to a channel.""" + + +class ChannelsResult(BaseResult): + """Channels result.""" + + channels: Dict[str, ChannelInfoResult] + """Map where key is channel and value is ChannelInfoResult.""" + + +class DisconnectResult(BaseResult): + """Disconnect result.""" + + +class HistoryRemoveResult(BaseResult): + """History remove result.""" + + +class HistoryResult(BaseResult): + """History result.""" + + publications: List[Publication] = Field(default_factory=list) + """List of publications in channel.""" + offset: Optional[int] = None + """Top offset in history stream.""" + epoch: Optional[str] = None + """Epoch of current stream.""" + + +class InfoResult(BaseResult): + """Info result.""" + + nodes: List[Node] + """Information about all nodes in a cluster.""" + + +class PresenceResult(BaseResult): + """Presence result.""" + + presence: Dict[str, ClientInfo] + """Offset of publication in history stream.""" + + +class PresenceStatsResult(BaseResult): + """Presence stats result.""" + + num_clients: int = Field(default=0) + """Total number of clients in channel.""" + num_users: int = Field(default=0) + """Total number of unique users in channel.""" + + +class RefreshResult(BaseResult): + """Refresh result.""" + + +class SubscribeResult(BaseResult): + """Subscribe result.""" + + +class UnsubscribeResult(BaseResult): + """Unsubscribe result.""" diff --git a/cent/types.py b/cent/types.py new file mode 100644 index 0000000..9c95e2d --- /dev/null +++ b/cent/types.py @@ -0,0 +1,141 @@ +from typing import Optional, Any, Dict + +from pydantic import Field + +from cent.base import BaseResult, NestedModel +from cent.proto.centrifugal.centrifugo.api import ( + SubscribeOptionOverride as GrpcChannelOptionOverride, + BoolValue as GrpcBoolValue, + StreamPosition as GrpcStreamPosition, + Disconnect as GrpcDisconnect, +) + + +class BoolValue(NestedModel): + """Bool value.""" + + __grpc_method__ = GrpcBoolValue + + value: bool + + +class StreamPosition(NestedModel): + """ + Stream position representation. + + Attributes: + offset (int): Offset of publication in history stream. + epoch (str): Epoch of current stream. + """ + + __grpc_method__ = GrpcStreamPosition + + offset: int + epoch: str + + +class ChannelOptionsOverride(NestedModel): + """ + Override object for channel options. + + Attributes: + presence (Optional[BoolValue]): Override for presence. + join_leave (Optional[BoolValue]): Override for join_leave behavior. + force_push_join_leave (Optional[BoolValue]): Force push for join_leave events. + force_positioning (Optional[BoolValue]): Override for force positioning. + force_recovery (Optional[BoolValue]): Override for force recovery. + """ + + __grpc_method__ = GrpcChannelOptionOverride + + presence: Optional[BoolValue] = None + join_leave: Optional[BoolValue] = None + force_push_join_leave: Optional[BoolValue] = None + force_positioning: Optional[BoolValue] = None + force_recovery: Optional[BoolValue] = None + + +class ProcessStats(BaseResult): + """ + Represents statistics of a process. + + Attributes: + cpu (float): Process CPU usage as a percentage. Defaults to 0.0. + rss (int): Process Resident Set Size (RSS) in bytes. + """ + + cpu: float = Field(default=0.0) + rss: int + + +class ClientInfo(BaseResult): + """ + Represents the result containing client information. + + Attributes: + client (str): Client ID. + user (str): User ID. + conn_info (Optional[Any]): Optional connection info. This can include details + such as IP address, location, etc. + chan_info (Optional[Any]): Optional channel info. This might include specific + settings or preferences related to the channel. + """ + + client: str + user: str + conn_info: Optional[Any] = None + chan_info: Optional[Any] = None + + +class Publication(BaseResult): + """Publication result.""" + + data: Any + """Custom JSON inside publication.""" + offset: int = Field(default=0) + """Offset of publication in history stream.""" + + +class Metrics(BaseResult): + """Metrics result.""" + + interval: float = Field(default=0.0) + """Interval.""" + items: Dict[str, float] + """Map where key is string and value is float.""" + + +class Node(BaseResult): + """Node result.""" + + uid: str + """Node unique identifier.""" + name: str + """Node name.""" + version: str + """Node version.""" + num_clients: int = Field(default=0) + """Total number of connections.""" + num_subs: int = Field(default=0) + """Total number of subscriptions.""" + num_users: int = Field(default=0) + """Total number of users.""" + num_channels: int = Field(default=0) + """Total number of channels.""" + uptime: int = Field(default=0) + """Node uptime.""" + metrics: Optional[Metrics] = None + """Node metrics.""" + process: Optional[ProcessStats] = None + """Node process.""" + + +class Disconnect(NestedModel): + """Disconnect data.""" + + __grpc_method__ = GrpcDisconnect + + code: int + """Disconnect code.""" + reason: str + """Disconnect reason.""" diff --git a/cent/types/__init__.py b/cent/types/__init__.py deleted file mode 100644 index 2a56ab0..0000000 --- a/cent/types/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -from .base import CentResult -from cent.methods.bool_value import BoolValue -from .broadcast_result import BroadcastResult -from .channel_info_result import ChannelInfoResult -from cent.methods.channel_options_override import ChannelOptionsOverride -from .channels_result import ChannelsResult -from .client_info_result import ClientInfoResult -from cent.methods.disconnect_data import Disconnect -from .disconnect_result import DisconnectResult -from .history_remove_result import HistoryRemoveResult -from .history_result import HistoryResult -from .info_result import InfoResult -from .metrics_result import MetricsResult -from .node_result import NodeResult -from .presence_result import PresenceResult -from .presence_stats_result import PresenceStatsResult -from .process_result import ProcessResult -from .publication_result import PublicationResult -from .publish_result import PublishResult -from .refresh_result import RefreshResult -from cent.methods.stream_position import StreamPosition -from .subscribe_result import SubscribeResult -from .unsubscribe_result import UnsubscribeResult - -__all__ = ( - "CentResult", - "BoolValue", - "BroadcastResult", - "ChannelInfoResult", - "ChannelOptionsOverride", - "ChannelsResult", - "ClientInfoResult", - "Disconnect", - "DisconnectResult", - "HistoryRemoveResult", - "HistoryResult", - "InfoResult", - "MetricsResult", - "NodeResult", - "PresenceResult", - "PresenceStatsResult", - "ProcessResult", - "PublicationResult", - "PublishResult", - "RefreshResult", - "StreamPosition", - "SubscribeResult", - "UnsubscribeResult", -) diff --git a/cent/types/base.py b/cent/types/base.py deleted file mode 100644 index 1a55b32..0000000 --- a/cent/types/base.py +++ /dev/null @@ -1,15 +0,0 @@ -from abc import ABC - -from pydantic import BaseModel, ConfigDict - - -class CentResult(BaseModel, ABC): - model_config = ConfigDict( - use_enum_values=True, - extra="allow", - validate_assignment=True, - frozen=True, - populate_by_name=True, - arbitrary_types_allowed=True, - defer_build=True, - ) diff --git a/cent/types/batch_result.py b/cent/types/batch_result.py deleted file mode 100644 index 06e1249..0000000 --- a/cent/types/batch_result.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import List, Any - -from cent.types import CentResult - - -class BatchResult(CentResult): - """Batch response.""" - - replies: List[Any] - """List of results from batch request.""" diff --git a/cent/types/broadcast_result.py b/cent/types/broadcast_result.py deleted file mode 100644 index 40336da..0000000 --- a/cent/types/broadcast_result.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import List - -from pydantic import Field - -from cent.methods.base import Response -from cent.types.base import CentResult -from cent.types.publish_result import PublishResult - - -class BroadcastResult(CentResult): - """Publish result.""" - - responses: List[Response[PublishResult]] = Field(default_factory=list) - """Responses for each individual publish (with possible error and publish result).""" diff --git a/cent/types/channel_info_result.py b/cent/types/channel_info_result.py deleted file mode 100644 index dbcfd9e..0000000 --- a/cent/types/channel_info_result.py +++ /dev/null @@ -1,10 +0,0 @@ -from pydantic import Field - -from cent.types.base import CentResult - - -class ChannelInfoResult(CentResult): - """Channel info result.""" - - num_clients: int = Field(default=0) - """Total number of connections currently subscribed to a channel.""" diff --git a/cent/types/channels_result.py b/cent/types/channels_result.py deleted file mode 100644 index e39ed16..0000000 --- a/cent/types/channels_result.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import Dict - -from cent.types.base import CentResult -from cent.types.channel_info_result import ChannelInfoResult - - -class ChannelsResult(CentResult): - """Channels result.""" - - channels: Dict[str, ChannelInfoResult] - """Map where key is channel and value is ChannelInfoResult.""" diff --git a/cent/types/client_info_result.py b/cent/types/client_info_result.py deleted file mode 100644 index 7121748..0000000 --- a/cent/types/client_info_result.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import Any, Optional - -from cent.types.base import CentResult - - -class ClientInfoResult(CentResult): - """Client info result.""" - - client: str - """Client ID.""" - user: str - """User ID.""" - conn_info: Optional[Any] = None - """Optional connection info.""" - chan_info: Optional[Any] = None - """Optional channel info.""" diff --git a/cent/types/disconnect_result.py b/cent/types/disconnect_result.py deleted file mode 100644 index 9d85c4c..0000000 --- a/cent/types/disconnect_result.py +++ /dev/null @@ -1,5 +0,0 @@ -from cent.types.base import CentResult - - -class DisconnectResult(CentResult): - """Disconnect result.""" diff --git a/cent/types/history_remove_result.py b/cent/types/history_remove_result.py deleted file mode 100644 index 1ee1816..0000000 --- a/cent/types/history_remove_result.py +++ /dev/null @@ -1,5 +0,0 @@ -from cent.types.base import CentResult - - -class HistoryRemoveResult(CentResult): - """History remove result.""" diff --git a/cent/types/history_result.py b/cent/types/history_result.py deleted file mode 100644 index cd4be2c..0000000 --- a/cent/types/history_result.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import List, Optional - -from pydantic import Field - -from cent.types.base import CentResult -from cent.types.publication_result import PublicationResult - - -class HistoryResult(CentResult): - """History result.""" - - publications: List[PublicationResult] = Field(default_factory=list) - """List of publications in channel.""" - offset: Optional[int] = None - """Top offset in history stream.""" - epoch: Optional[str] = None - """Epoch of current stream.""" diff --git a/cent/types/info_result.py b/cent/types/info_result.py deleted file mode 100644 index 6a2d0db..0000000 --- a/cent/types/info_result.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import List - -from cent.types.base import CentResult -from cent.types.node_result import NodeResult - - -class InfoResult(CentResult): - """Info result.""" - - nodes: List[NodeResult] - """Information about all nodes in a cluster.""" diff --git a/cent/types/metrics_result.py b/cent/types/metrics_result.py deleted file mode 100644 index ce0ca80..0000000 --- a/cent/types/metrics_result.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import Dict - -from pydantic import Field - -from cent.types.base import CentResult - - -class MetricsResult(CentResult): - """Metrics result.""" - - interval: float = Field(default=0.0) - """Interval.""" - items: Dict[str, float] - """Map where key is string and value is float.""" diff --git a/cent/types/node_result.py b/cent/types/node_result.py deleted file mode 100644 index 6550411..0000000 --- a/cent/types/node_result.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import Optional - -from pydantic import Field - -from cent.types.base import CentResult -from cent.types.metrics_result import MetricsResult -from cent.types.process_result import ProcessResult - - -class NodeResult(CentResult): - """Node result.""" - - uid: str - """Node unique identifier.""" - name: str - """Node name.""" - version: str - """Node version.""" - num_clients: int = Field(default=0) - """Total number of connections.""" - num_subs: int = Field(default=0) - """Total number of subscriptions.""" - num_users: int = Field(default=0) - """Total number of users.""" - num_channels: int = Field(default=0) - """Total number of channels.""" - uptime: int = Field(default=0) - """Node uptime.""" - metrics: Optional[MetricsResult] = None - """Node metrics.""" - process: Optional[ProcessResult] = None - """Node process.""" diff --git a/cent/types/presence_result.py b/cent/types/presence_result.py deleted file mode 100644 index fe8df50..0000000 --- a/cent/types/presence_result.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Dict - - -from cent.types.base import CentResult -from cent.types.client_info_result import ClientInfoResult - - -class PresenceResult(CentResult): - """Presence result.""" - - presence: Dict[str, ClientInfoResult] - """Offset of publication in history stream.""" diff --git a/cent/types/presence_stats_result.py b/cent/types/presence_stats_result.py deleted file mode 100644 index 2170ec1..0000000 --- a/cent/types/presence_stats_result.py +++ /dev/null @@ -1,12 +0,0 @@ -from pydantic import Field - -from cent.types.base import CentResult - - -class PresenceStatsResult(CentResult): - """Presence stats result.""" - - num_clients: int = Field(default=0) - """Total number of clients in channel.""" - num_users: int = Field(default=0) - """Total number of unique users in channel.""" diff --git a/cent/types/process_result.py b/cent/types/process_result.py deleted file mode 100644 index 9ea58e6..0000000 --- a/cent/types/process_result.py +++ /dev/null @@ -1,12 +0,0 @@ -from pydantic import Field - -from cent.types.base import CentResult - - -class ProcessResult(CentResult): - """Process result.""" - - cpu: float = Field(default=0.0) - """Process CPU usage.""" - rss: int - """Process RSS.""" diff --git a/cent/types/publication_result.py b/cent/types/publication_result.py deleted file mode 100644 index 18cdc3d..0000000 --- a/cent/types/publication_result.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import Any - -from pydantic import Field - -from cent.types.base import CentResult - - -class PublicationResult(CentResult): - """Publication result.""" - - data: Any - """Custom JSON inside publication.""" - offset: int = Field(default=0) - """Offset of publication in history stream.""" diff --git a/cent/types/publish_result.py b/cent/types/publish_result.py deleted file mode 100644 index 5baf717..0000000 --- a/cent/types/publish_result.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Optional - -from cent.types.base import CentResult - - -class PublishResult(CentResult): - """Publish result.""" - - offset: Optional[int] = None - """Offset of publication in history stream.""" - epoch: Optional[str] = None - """Epoch of current stream.""" diff --git a/cent/types/refresh_result.py b/cent/types/refresh_result.py deleted file mode 100644 index 6ccfb4d..0000000 --- a/cent/types/refresh_result.py +++ /dev/null @@ -1,5 +0,0 @@ -from cent.types.base import CentResult - - -class RefreshResult(CentResult): - """Refresh result.""" diff --git a/cent/types/subscribe_result.py b/cent/types/subscribe_result.py deleted file mode 100644 index 02e9eda..0000000 --- a/cent/types/subscribe_result.py +++ /dev/null @@ -1,5 +0,0 @@ -from cent.types.base import CentResult - - -class SubscribeResult(CentResult): - """Subscribe result.""" diff --git a/cent/types/unsubscribe_result.py b/cent/types/unsubscribe_result.py deleted file mode 100644 index 4f72528..0000000 --- a/cent/types/unsubscribe_result.py +++ /dev/null @@ -1,5 +0,0 @@ -from cent.types.base import CentResult - - -class UnsubscribeResult(CentResult): - """Unsubscribe result.""" diff --git a/pyproject.toml b/pyproject.toml index 24351f2..24719da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,14 +75,12 @@ select = [ ignore = [ "PLR0913", # too-many-arguments "PGH003", # use specific rule code when ignore - "T201" + "T201", ] [tool.ruff.per-file-ignores] -"cent/types/*" = ["E501"] -"cent/methods/*" = ["E501"] "tests/*" = ["S101", "PT012"] -"cent/protos/centrifugal/*" = ["RUF009", "ARG002", "E501"] +"cent/proto/centrifugal/*" = ["RUF009", "ARG002", "E501"] [tool.mypy] strict = true diff --git a/tests/test_async_validation.py b/tests/test_async_validation.py index 24fd8f8..93f5dd2 100644 --- a/tests/test_async_validation.py +++ b/tests/test_async_validation.py @@ -1,32 +1,30 @@ -from base64 import b64encode - import pytest -from cent import AsyncClient -from cent.exceptions import CentAPIError -from cent.methods import PublishRequest, BroadcastRequest, PresenceRequest -from cent.types import StreamPosition, Disconnect +from cent import ( + AsyncClient, CentAPIError, PublishRequest, StreamPosition, Disconnect, + BroadcastRequest, PresenceRequest) + from tests.conftest import UNKNOWN_CHANNEL_ERROR_CODE async def test_publish(async_client: AsyncClient) -> None: await async_client.publish( - "personal:1", + "personal_1", {"data": "data"}, skip_history=False, tags={"tag": "tag"}, - b64data=b64encode(b"data").decode(), + # b64data=b64encode(b"data").decode(), idempotency_key="idempotency_key", ) async def test_broadcast(async_client: AsyncClient) -> None: await async_client.broadcast( - ["personal:1", "personal:2"], + ["personal_1", "personal_2"], {"data": "data"}, skip_history=False, tags={"tag": "tag"}, - b64data=b64encode(b"data").decode(), + # b64data=b64encode(b"data").decode(), idempotency_key="idempotency_key", ) @@ -34,9 +32,9 @@ async def test_broadcast(async_client: AsyncClient) -> None: async def test_subscribe(async_client: AsyncClient) -> None: await async_client.subscribe( "user", - "personal:1", + "personal_1", info={"info": "info"}, - b64info=b64encode(b"info").decode(), + # b64info=b64encode(b"info").decode(), client="client", session="session", data={"data": "data"}, @@ -50,30 +48,30 @@ async def test_subscribe(async_client: AsyncClient) -> None: async def test_unsubscribe(async_client: AsyncClient) -> None: await async_client.unsubscribe( user="user", - channel="personal:1", + channel="personal_1", session="session", client="client", ) async def test_presence(async_client: AsyncClient) -> None: - await async_client.presence("personal:1") + await async_client.presence("personal_1") async def test_presence_stats(async_client: AsyncClient) -> None: - await async_client.presence_stats("personal:1") + await async_client.presence_stats("personal_1") async def test_history(async_client: AsyncClient) -> None: await async_client.history( - channel="personal:1", + channel="personal_1", limit=1, reverse=True, ) async def test_history_remove(async_client: AsyncClient) -> None: - await async_client.history_remove("personal:1") + await async_client.history_remove("personal_1") async def test_info(async_client: AsyncClient) -> None: @@ -91,7 +89,7 @@ async def test_disconnect(async_client: AsyncClient) -> None: user="user", client="client", session="session", - whitelist=["personal:1"], + whitelist=["personal_1"], disconnect=Disconnect( code=4000, reason="reason", @@ -113,19 +111,19 @@ async def test_batch(async_client: AsyncClient) -> None: await async_client.batch( commands=[ PublishRequest( - channel="personal:1", + channel="personal_1", data={"data": "Second data"}, ), PublishRequest( - channel="personal:2", + channel="personal_2", data={"data": "First data"}, ), BroadcastRequest( - channels=["personal:1", "personal:2"], + channels=["personal_1", "personal_2"], data={"data": "Third data"}, ), PresenceRequest( - channel="personal:1", + channel="personal_1", ), ] ) diff --git a/tests/test_grpc_validation.py b/tests/test_grpc_validation.py index 51b6633..1b9af67 100644 --- a/tests/test_grpc_validation.py +++ b/tests/test_grpc_validation.py @@ -1,32 +1,30 @@ -from base64 import b64encode +import json import pytest -from cent.client.grpc_client import GrpcClient -from cent.exceptions import CentAPIError -from cent.methods.disconnect_data import Disconnect -from cent.types import StreamPosition, ChannelOptionsOverride, BoolValue +from cent import (GrpcClient, CentAPIError, StreamPosition, + ChannelOptionsOverride, BoolValue, Disconnect) from tests.conftest import UNKNOWN_CHANNEL_ERROR_CODE async def test_publish(grpc_client: GrpcClient) -> None: await grpc_client.publish( - "personal:1", - {"data": "data"}, + "personal_1", + json.dumps({"data": "data"}).encode(), skip_history=False, tags={"tag": "tag"}, - b64data=b64encode(b"data").decode(), + # b64data=b64encode(b"data").decode(), idempotency_key="idempotency_key", ) async def test_broadcast(grpc_client: GrpcClient) -> None: await grpc_client.broadcast( - ["personal:1", "personal:2"], - {"data": "data"}, + ["personal_1", "personal_2"], + json.dumps({"data": "data"}).encode(), skip_history=False, tags={"tag": "tag"}, - b64data=b64encode(b"data").decode(), + # b64data=b64encode(b"data").decode(), idempotency_key="idempotency_key", ) @@ -34,12 +32,12 @@ async def test_broadcast(grpc_client: GrpcClient) -> None: async def test_subscribe(grpc_client: GrpcClient) -> None: await grpc_client.subscribe( "user", - "personal:1", - info={"info": "info"}, - b64info=b64encode(b"info").decode(), + "personal_1", + info=json.dumps({"info": "info"}).encode(), + # b64info=b64encode(b"info").decode(), client="client", session="session", - data={"data": "data"}, + data=json.dumps({"data": "data"}).encode(), recover_since=StreamPosition( offset=1, epoch="1", @@ -55,30 +53,30 @@ async def test_subscribe(grpc_client: GrpcClient) -> None: async def test_unsubscribe(grpc_client: GrpcClient) -> None: await grpc_client.unsubscribe( user="user", - channel="personal:1", + channel="personal_1", session="session", client="client", ) async def test_presence(grpc_client: GrpcClient) -> None: - await grpc_client.presence("personal:1") + await grpc_client.presence("personal_1") async def test_presence_stats(grpc_client: GrpcClient) -> None: - await grpc_client.presence_stats("personal:1") + await grpc_client.presence_stats("personal_1") async def test_history(grpc_client: GrpcClient) -> None: await grpc_client.history( - channel="personal:1", + channel="personal_1", limit=1, reverse=True, ) async def test_history_remove(grpc_client: GrpcClient) -> None: - await grpc_client.history_remove(channel="personal:1") + await grpc_client.history_remove(channel="personal_1") async def test_info(grpc_client: GrpcClient) -> None: @@ -96,7 +94,7 @@ async def test_disconnect(grpc_client: GrpcClient) -> None: user="user", client="client", session="session", - whitelist=["personal:1"], + whitelist=["personal_1"], disconnect=Disconnect( code=4000, reason="reason", @@ -118,6 +116,6 @@ async def test_error_publish(grpc_client: GrpcClient) -> None: with pytest.raises(CentAPIError, match="unknown channel") as exc_info: await grpc_client.publish( "undefined_channel:123", - {"data": "data"}, + json.dumps({"data": "data"}).encode(), ) assert exc_info.value.code == UNKNOWN_CHANNEL_ERROR_CODE diff --git a/tests/test_sync_validation.py b/tests/test_sync_validation.py index f18621b..5d734e4 100644 --- a/tests/test_sync_validation.py +++ b/tests/test_sync_validation.py @@ -1,32 +1,30 @@ -from base64 import b64encode import pytest -from cent import Client -from cent.exceptions import CentAPIError -from cent.methods import PublishRequest, BroadcastRequest, PresenceRequest -from cent.types import StreamPosition, Disconnect +from cent import (Client, CentAPIError, PublishRequest, BroadcastRequest, PresenceRequest, + StreamPosition, Disconnect) + from tests.conftest import UNKNOWN_CHANNEL_ERROR_CODE def test_publish(sync_client: Client) -> None: sync_client.publish( - "personal:1", + "personal_1", {"data": "data"}, skip_history=False, tags={"tag": "tag"}, - b64data=b64encode(b"data").decode(), + # b64data=b64encode(b"data").decode(), idempotency_key="idempotency_key", ) def test_broadcast(sync_client: Client) -> None: sync_client.broadcast( - ["personal:1", "personal:2"], + ["personal_1", "personal_2"], {"data": "data"}, skip_history=False, tags={"tag": "tag"}, - b64data=b64encode(b"data").decode(), + # b64data=b64encode(b"data").decode(), idempotency_key="idempotency_key", ) @@ -34,9 +32,9 @@ def test_broadcast(sync_client: Client) -> None: def test_subscribe(sync_client: Client) -> None: sync_client.subscribe( "user", - "personal:1", + "personal_1", info={"info": "info"}, - b64info=b64encode(b"info").decode(), + # b64info=b64encode(b"info").decode(), client="client", session="session", data={"data": "data"}, @@ -50,30 +48,30 @@ def test_subscribe(sync_client: Client) -> None: def test_unsubscribe(sync_client: Client) -> None: sync_client.unsubscribe( user="user", - channel="personal:1", + channel="personal_1", session="session", client="client", ) def test_presence(sync_client: Client) -> None: - sync_client.presence("personal:1") + sync_client.presence("personal_1") def test_presence_stats(sync_client: Client) -> None: - sync_client.presence_stats("personal:1") + sync_client.presence_stats("personal_1") def test_history(sync_client: Client) -> None: sync_client.history( - channel="personal:1", + channel="personal_1", limit=1, reverse=True, ) def test_history_remove(sync_client: Client) -> None: - sync_client.history_remove("personal:1") + sync_client.history_remove("personal_1") def test_info(sync_client: Client) -> None: @@ -91,7 +89,7 @@ def test_disconnect(sync_client: Client) -> None: user="user", client="client", session="session", - whitelist=["personal:1"], + whitelist=["personal_1"], disconnect=Disconnect( code=4000, reason="reason", @@ -113,19 +111,19 @@ def test_batch(sync_client: Client) -> None: sync_client.batch( commands=[ PublishRequest( - channel="personal:1", + channel="personal_1", data={"data": "Second data"}, ), PublishRequest( - channel="personal:2", + channel="personal_2", data={"data": "First data"}, ), BroadcastRequest( - channels=["personal:1", "personal:2"], + channels=["personal_1", "personal_2"], data={"data": "Third data"}, ), PresenceRequest( - channel="personal:1", + channel="personal_1", ), ] ) From 81e30d707f6653f78d1ee3b5b167e4d0d33eaf09 Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Sat, 10 Feb 2024 19:55:16 +0200 Subject: [PATCH 18/55] regenerate proto, add workflows --- .github/workflows/release.yml | 33 +++++ .github/workflows/test.yml | 47 +++++++ .../centrifugal/centrifugo/api/__init__.py | 117 +++++++++++------- 3 files changed, 150 insertions(+), 47 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e1bbc4d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,33 @@ +name: Release to PyPI + +on: + push: + tags: + - '*' + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: check GITHUB_REF matches package version + uses: samuelcolvin/check-python-version@v4.1 + with: + version_file_path: centrifuge/__meta__.py + + - name: Install build dependencies + run: pip install build + + - name: Build distribution + run: python -m build + + - name: Publish package to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_PASSWORD }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..d9ee148 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,47 @@ +name: Test + +on: [ push, pull_request ] + +jobs: + build: + # Prevent duplicate builds on internal PRs. + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository + + strategy: + matrix: + os: + - ubuntu-latest + python-version: + - "3.9" + - "3.10" + - "3.11" + - "3.12" + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Setup docker + if: matrix.os == 'macos-latest' + uses: crazy-max/ghaction-setup-docker@v3 + + - name: Start Centrifugo + run: docker run -d -p 8000:8000 -e CENTRIFUGO_API_KEY=api_key -e CENTRIFUGO_HISTORY_TTL=300s -e CENTRIFUGO_HISTORY_SIZE=100 -e CENTRIFUGO_PRESENCE=true -e CENTRIFUGO_GRPC_API=true centrifugo/centrifugo:v5 centrifugo + + - name: Install dependencies + run: | + make dev + + - name: Run tests + run: | + make test + + - name: Run linter + run: | + make lint-ci diff --git a/cent/proto/centrifugal/centrifugo/api/__init__.py b/cent/proto/centrifugal/centrifugo/api/__init__.py index 8563ec3..ad4e3ee 100644 --- a/cent/proto/centrifugal/centrifugo/api/__init__.py +++ b/cent/proto/centrifugal/centrifugo/api/__init__.py @@ -1,5 +1,5 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! -# sources: cent/protos/apiproto.proto +# sources: cent/proto/apiproto.proto # plugin: python-betterproto # This file has been @generated @@ -14,10 +14,10 @@ import betterproto import grpclib from betterproto.grpc.grpclib_server import ServiceBase -import grpclib.server if TYPE_CHECKING: + import grpclib.server from betterproto.grpc.grpclib_client import MetadataLike from grpclib.metadata import Deadline @@ -83,7 +83,9 @@ class Command(betterproto.Message): block_user: "BlockUserRequest" = betterproto.message_field(21) unblock_user: "UnblockUserRequest" = betterproto.message_field(22) revoke_token: "RevokeTokenRequest" = betterproto.message_field(23) - invalidate_user_tokens: "InvalidateUserTokensRequest" = betterproto.message_field(24) + invalidate_user_tokens: "InvalidateUserTokensRequest" = betterproto.message_field( + 24 + ) device_register: "DeviceRegisterRequest" = betterproto.message_field(25) device_update: "DeviceUpdateRequest" = betterproto.message_field(26) device_remove: "DeviceRemoveRequest" = betterproto.message_field(27) @@ -92,7 +94,9 @@ class Command(betterproto.Message): device_topic_update: "DeviceTopicUpdateRequest" = betterproto.message_field(30) user_topic_list: "UserTopicListRequest" = betterproto.message_field(31) user_topic_update: "UserTopicUpdateRequest" = betterproto.message_field(32) - send_push_notification: "SendPushNotificationRequest" = betterproto.message_field(33) + send_push_notification: "SendPushNotificationRequest" = betterproto.message_field( + 33 + ) update_push_status: "UpdatePushStatusRequest" = betterproto.message_field(34) cancel_push: "CancelPushRequest" = betterproto.message_field(35) rate_limit: "RateLimitRequest" = betterproto.message_field(50) @@ -1032,7 +1036,7 @@ async def batch( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "BatchResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/Batch", @@ -1049,7 +1053,7 @@ async def publish( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "PublishResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/Publish", @@ -1066,7 +1070,7 @@ async def broadcast( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "BroadcastResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/Broadcast", @@ -1083,7 +1087,7 @@ async def subscribe( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "SubscribeResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/Subscribe", @@ -1100,7 +1104,7 @@ async def unsubscribe( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "UnsubscribeResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/Unsubscribe", @@ -1117,7 +1121,7 @@ async def disconnect( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "DisconnectResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/Disconnect", @@ -1134,7 +1138,7 @@ async def presence( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "PresenceResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/Presence", @@ -1151,7 +1155,7 @@ async def presence_stats( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "PresenceStatsResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/PresenceStats", @@ -1168,7 +1172,7 @@ async def history( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "HistoryResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/History", @@ -1185,7 +1189,7 @@ async def history_remove( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "HistoryRemoveResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/HistoryRemove", @@ -1202,7 +1206,7 @@ async def info( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "InfoResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/Info", @@ -1219,7 +1223,7 @@ async def rpc( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "RpcResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/RPC", @@ -1236,7 +1240,7 @@ async def refresh( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "RefreshResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/Refresh", @@ -1253,7 +1257,7 @@ async def channels( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "ChannelsResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/Channels", @@ -1270,7 +1274,7 @@ async def connections( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "ConnectionsResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/Connections", @@ -1287,7 +1291,7 @@ async def update_user_status( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "UpdateUserStatusResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/UpdateUserStatus", @@ -1304,7 +1308,7 @@ async def get_user_status( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "GetUserStatusResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/GetUserStatus", @@ -1321,7 +1325,7 @@ async def delete_user_status( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "DeleteUserStatusResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/DeleteUserStatus", @@ -1338,7 +1342,7 @@ async def block_user( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "BlockUserResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/BlockUser", @@ -1355,7 +1359,7 @@ async def unblock_user( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "UnblockUserResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/UnblockUser", @@ -1372,7 +1376,7 @@ async def revoke_token( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "RevokeTokenResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/RevokeToken", @@ -1389,7 +1393,7 @@ async def invalidate_user_tokens( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "InvalidateUserTokensResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/InvalidateUserTokens", @@ -1406,7 +1410,7 @@ async def device_register( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "DeviceRegisterResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/DeviceRegister", @@ -1423,7 +1427,7 @@ async def device_update( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "DeviceUpdateResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/DeviceUpdate", @@ -1440,7 +1444,7 @@ async def device_remove( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "DeviceRemoveResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/DeviceRemove", @@ -1457,7 +1461,7 @@ async def device_list( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "DeviceListResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/DeviceList", @@ -1474,7 +1478,7 @@ async def device_topic_list( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "DeviceTopicListResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/DeviceTopicList", @@ -1491,7 +1495,7 @@ async def device_topic_update( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "DeviceTopicUpdateResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/DeviceTopicUpdate", @@ -1508,7 +1512,7 @@ async def user_topic_list( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "UserTopicListResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/UserTopicList", @@ -1525,7 +1529,7 @@ async def user_topic_update( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "UserTopicUpdateResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/UserTopicUpdate", @@ -1542,7 +1546,7 @@ async def send_push_notification( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "SendPushNotificationResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/SendPushNotification", @@ -1559,7 +1563,7 @@ async def update_push_status( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "UpdatePushStatusResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/UpdatePushStatus", @@ -1576,7 +1580,7 @@ async def cancel_push( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "CancelPushResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/CancelPush", @@ -1593,7 +1597,7 @@ async def rate_limit( *, timeout: Optional[float] = None, deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None, + metadata: Optional["MetadataLike"] = None ) -> "RateLimitResponse": return await self._unary_unary( "/centrifugal.centrifugo.api.CentrifugoApi/RateLimit", @@ -1606,16 +1610,21 @@ async def rate_limit( class CentrifugoApiBase(ServiceBase): + async def batch(self, batch_request: "BatchRequest") -> "BatchResponse": raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) async def publish(self, publish_request: "PublishRequest") -> "PublishResponse": raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - async def broadcast(self, broadcast_request: "BroadcastRequest") -> "BroadcastResponse": + async def broadcast( + self, broadcast_request: "BroadcastRequest" + ) -> "BroadcastResponse": raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - async def subscribe(self, subscribe_request: "SubscribeRequest") -> "SubscribeResponse": + async def subscribe( + self, subscribe_request: "SubscribeRequest" + ) -> "SubscribeResponse": raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) async def unsubscribe( @@ -1623,7 +1632,9 @@ async def unsubscribe( ) -> "UnsubscribeResponse": raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - async def disconnect(self, disconnect_request: "DisconnectRequest") -> "DisconnectResponse": + async def disconnect( + self, disconnect_request: "DisconnectRequest" + ) -> "DisconnectResponse": raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) async def presence(self, presence_request: "PresenceRequest") -> "PresenceResponse": @@ -1674,7 +1685,9 @@ async def delete_user_status( ) -> "DeleteUserStatusResponse": raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - async def block_user(self, block_user_request: "BlockUserRequest") -> "BlockUserResponse": + async def block_user( + self, block_user_request: "BlockUserRequest" + ) -> "BlockUserResponse": raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) async def unblock_user( @@ -1707,7 +1720,9 @@ async def device_remove( ) -> "DeviceRemoveResponse": raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - async def device_list(self, device_list_request: "DeviceListRequest") -> "DeviceListResponse": + async def device_list( + self, device_list_request: "DeviceListRequest" + ) -> "DeviceListResponse": raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) async def device_topic_list( @@ -1740,10 +1755,14 @@ async def update_push_status( ) -> "UpdatePushStatusResponse": raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - async def cancel_push(self, cancel_push_request: "CancelPushRequest") -> "CancelPushResponse": + async def cancel_push( + self, cancel_push_request: "CancelPushRequest" + ) -> "CancelPushResponse": raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - async def rate_limit(self, rate_limit_request: "RateLimitRequest") -> "RateLimitResponse": + async def rate_limit( + self, rate_limit_request: "RateLimitRequest" + ) -> "RateLimitResponse": raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) async def __rpc_batch( @@ -1818,12 +1837,16 @@ async def __rpc_history_remove( response = await self.history_remove(request) await stream.send_message(response) - async def __rpc_info(self, stream: "grpclib.server.Stream[InfoRequest, InfoResponse]") -> None: + async def __rpc_info( + self, stream: "grpclib.server.Stream[InfoRequest, InfoResponse]" + ) -> None: request = await stream.recv_message() response = await self.info(request) await stream.send_message(response) - async def __rpc_rpc(self, stream: "grpclib.server.Stream[RpcRequest, RpcResponse]") -> None: + async def __rpc_rpc( + self, stream: "grpclib.server.Stream[RpcRequest, RpcResponse]" + ) -> None: request = await stream.recv_message() response = await self.rpc(request) await stream.send_message(response) From c8a1c5e3ae2b343abb920f19664de12f2df4d1b9 Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Sat, 10 Feb 2024 20:18:46 +0200 Subject: [PATCH 19/55] use poetry install --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6f0d3fb..5de8695 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,8 @@ .PHONY: proto test lint lint-fix lint-ci dev: - pip install -e ".[dev]" + pip install poetry + poetry install proto: poetry run python -m grpc_tools.protoc -I . --python_betterproto_out=./cent/proto cent/proto/apiproto.proto From 649ebbe98f68d3f052fb72da16bb21857ceaeb35 Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Sat, 10 Feb 2024 20:22:24 +0200 Subject: [PATCH 20/55] use poetry to run pytest --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5de8695..1921274 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ proto: poetry run python -m grpc_tools.protoc -I . --python_betterproto_out=./cent/proto cent/proto/apiproto.proto test: - pytest -vv tests + poetry run pytest -vv tests lint: ruff . From 17ae06001afb9e6cd45341ea802be79653fa4f23 Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Sat, 10 Feb 2024 20:24:10 +0200 Subject: [PATCH 21/55] export GRPC port --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d9ee148..6d2cecf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,7 +32,7 @@ jobs: uses: crazy-max/ghaction-setup-docker@v3 - name: Start Centrifugo - run: docker run -d -p 8000:8000 -e CENTRIFUGO_API_KEY=api_key -e CENTRIFUGO_HISTORY_TTL=300s -e CENTRIFUGO_HISTORY_SIZE=100 -e CENTRIFUGO_PRESENCE=true -e CENTRIFUGO_GRPC_API=true centrifugo/centrifugo:v5 centrifugo + run: docker run -d -p 8000:8000 -p 10000:10000 -e CENTRIFUGO_API_KEY=api_key -e CENTRIFUGO_HISTORY_TTL=300s -e CENTRIFUGO_HISTORY_SIZE=100 -e CENTRIFUGO_PRESENCE=true -e CENTRIFUGO_GRPC_API=true centrifugo/centrifugo:v5 centrifugo - name: Install dependencies run: | From c87ae6618ba24639076cdf14507539566c865521 Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Sat, 10 Feb 2024 20:38:16 +0200 Subject: [PATCH 22/55] fix other make commands --- Makefile | 6 +++--- cent/__init__.py | 6 ++++++ pyproject.toml | 6 +++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 1921274..972243a 100644 --- a/Makefile +++ b/Makefile @@ -11,10 +11,10 @@ test: poetry run pytest -vv tests lint: - ruff . + poetry run ruff . lint-fix: - ruff . --fix + poetry run ruff . --fix lint-ci: - ruff . --output-format=github + poetry run ruff . --output-format=github diff --git a/cent/__init__.py b/cent/__init__.py index b84dd6c..9efcd3d 100644 --- a/cent/__init__.py +++ b/cent/__init__.py @@ -116,4 +116,10 @@ "Node", "Publication", "ClientInfo", + "CentError", + "CentNetworkError", + "CentClientDecodeError", + "CentUnauthorizedError", + "CentAPIError", + "CentTransportError", ) diff --git a/pyproject.toml b/pyproject.toml index 24719da..3a55fc4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "cent" version = "5.0.0" -description = "Python library to communicate with Centrifugo v3 HTTP API" +description = "Python library to communicate with Centrifugo v5 HTTP API" authors = ["Alexandr Emelin", "Katant Savelev"] license = "MIT" readme = 'README.md' @@ -80,7 +80,7 @@ ignore = [ [tool.ruff.per-file-ignores] "tests/*" = ["S101", "PT012"] -"cent/proto/centrifugal/*" = ["RUF009", "ARG002", "E501"] +"cent/proto/centrifugal/*" = ["RUF009", "ARG002", "E501", "TCH004"] [tool.mypy] strict = true @@ -117,7 +117,7 @@ module = [ ignore_missing_imports = true [[tool.mypy.overrides]] -module = ["cent.protos.centrifugal.*"] +module = ["cent.proto.centrifugal.*"] ignore_errors = true From 62444c0215978b9f98bfa6a0ee4ea6a68f6b568a Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Sat, 10 Feb 2024 23:21:01 +0200 Subject: [PATCH 23/55] various tweaks --- Makefile | 5 +- README.md | 4 +- benchmarks/conftest.py | 5 +- benchmarks/test_publish.py | 32 ++++++------- cent/__init__.py | 85 ++++++++++++++++----------------- cent/__meta__.py | 1 - cent/client/__init__.py | 8 ++-- cent/client/session/__init__.py | 4 +- pyproject.toml | 6 +-- 9 files changed, 73 insertions(+), 77 deletions(-) delete mode 100644 cent/__meta__.py diff --git a/Makefile b/Makefile index 972243a..e83c3a9 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: proto test lint lint-fix lint-ci +.PHONY: proto test lint lint-fix lint-ci bench dev: pip install poetry @@ -18,3 +18,6 @@ lint-fix: lint-ci: poetry run ruff . --output-format=github + +bench: + poetry run pytest benchmarks --benchmark-verbose diff --git a/README.md b/README.md index 259686c..a36f0dd 100644 --- a/README.md +++ b/README.md @@ -78,13 +78,13 @@ Optional: To start tests, you can use pytest with any additional options, for example: ```bash -pytest -vv tests +make test ``` To start benchmarks, you can use pytest too, for example: ```bash -pytest benchmarks --benchmark-verbose +make bench ``` ### Generate code from proto file, if needed diff --git a/benchmarks/conftest.py b/benchmarks/conftest.py index d73a540..4daaf12 100644 --- a/benchmarks/conftest.py +++ b/benchmarks/conftest.py @@ -1,3 +1,5 @@ +import asyncio +import threading import contextlib from typing import ( Any, @@ -35,9 +37,6 @@ def anyio_backend() -> Tuple[str, Dict[str, bool]]: @pytest.fixture() def aio_benchmark(benchmark: BenchmarkFixture) -> BenchmarkDecoratorType: - import asyncio - import threading - class Sync2Async: def __init__(self, coro: BenchmarkCoroType) -> None: self.coro = coro diff --git a/benchmarks/test_publish.py b/benchmarks/test_publish.py index 4684216..c26f792 100644 --- a/benchmarks/test_publish.py +++ b/benchmarks/test_publish.py @@ -1,26 +1,24 @@ -from typing import Union - +import random import pytest from benchmarks.conftest import BenchmarkDecoratorType -from cent import AsyncClient, Client, GrpcClient +from cent import AsyncClient, Client def sync_requests(client: Client) -> None: - for j in range(1000): - client.publish( - channel=f"personal:{j}", - data={"message": "Hello world!"}, - ) - - -async def async_requests(client: Union[GrpcClient, AsyncClient]) -> None: - for j in range(1000): - print(j) - await client.publish( - channel=f"personal:{j}", - data={"message": "Hello world!"}, - ) + channel_number = random.randint(0, 1000) # noqa: S311 + client.publish( + channel=f"personal_{channel_number}", + data={"message": "Hello world!"}, + ) + + +async def async_requests(client: AsyncClient) -> None: + channel_number = random.randint(0, 1000) # noqa: S311 + await client.publish( + channel=f"personal_{channel_number}", + data={"message": "Hello world!"}, + ) def test_sync( diff --git a/cent/__init__.py b/cent/__init__.py index 9efcd3d..be2f1f9 100644 --- a/cent/__init__.py +++ b/cent/__init__.py @@ -61,65 +61,62 @@ CentTransportError, ) -from .__meta__ import __version__ - with contextlib.suppress(ImportError): import uvloop as _uvloop _asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy()) __all__ = ( - "__version__", - "types", - "requests", - "exceptions", - "Client", + "AiohttpSession", "AsyncClient", - "BaseSession", "BaseAsyncSession", + "BaseSession", "BaseSyncSession", - "RequestsSession", - "AiohttpSession", - "GrpcClient", - "CentRequest", + "BatchRequest", + "BatchResult", + "BoolValue", "BroadcastRequest", - "PublishRequest", - "SubscribeRequest", - "UnsubscribeRequest", - "PresenceRequest", - "PresenceStatsRequest", - "HistoryRequest", - "HistoryRemoveRequest", - "RefreshRequest", + "BroadcastResult", + "CentAPIError", + "CentClientDecodeError", + "CentError", + "CentNetworkError", + "CentRequest", + "CentTransportError", + "CentUnauthorizedError", + "ChannelOptionsOverride", "ChannelsRequest", + "ChannelsResult", + "Client", + "ClientInfo", + "Disconnect", "DisconnectRequest", + "DisconnectResult", + "GrpcClient", + "HistoryRemoveRequest", + "HistoryRemoveResult", + "HistoryRequest", + "HistoryResult", "InfoRequest", - "BatchRequest", - "PublishResult", - "BroadcastResult", - "SubscribeResult", - "UnsubscribeResult", + "InfoResult", + "Node", + "PresenceRequest", "PresenceResult", + "PresenceStatsRequest", "PresenceStatsResult", - "HistoryResult", - "HistoryRemoveResult", - "RefreshResult", - "ChannelsResult", - "DisconnectResult", - "InfoResult", - "BatchResult", - "StreamPosition", - "ChannelOptionsOverride", - "Disconnect", - "BoolValue", "ProcessStats", - "Node", "Publication", - "ClientInfo", - "CentError", - "CentNetworkError", - "CentClientDecodeError", - "CentUnauthorizedError", - "CentAPIError", - "CentTransportError", + "PublishRequest", + "PublishResult", + "RefreshRequest", + "RefreshResult", + "RequestsSession", + "StreamPosition", + "SubscribeRequest", + "SubscribeResult", + "UnsubscribeRequest", + "UnsubscribeResult", + "exceptions", + "requests", + "types", ) diff --git a/cent/__meta__.py b/cent/__meta__.py deleted file mode 100644 index ba7be38..0000000 --- a/cent/__meta__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "5.0.0" diff --git a/cent/client/__init__.py b/cent/client/__init__.py index 67a0967..04835bb 100644 --- a/cent/client/__init__.py +++ b/cent/client/__init__.py @@ -10,12 +10,12 @@ from .grpc_client import GrpcClient __all__ = ( - "BaseSession", + "AiohttpSession", + "AsyncClient", "BaseAsyncSession", + "BaseSession", "BaseSyncSession", - "AiohttpSession", - "RequestsSession", "Client", - "AsyncClient", "GrpcClient", + "RequestsSession", ) diff --git a/cent/client/session/__init__.py b/cent/client/session/__init__.py index a1a1244..ca7f2ab 100644 --- a/cent/client/session/__init__.py +++ b/cent/client/session/__init__.py @@ -5,9 +5,9 @@ from .requests import RequestsSession __all__ = ( - "BaseSession", + "AiohttpSession", "BaseAsyncSession", + "BaseSession", "BaseSyncSession", - "AiohttpSession", "RequestsSession", ) diff --git a/pyproject.toml b/pyproject.toml index 3a55fc4..9d2bee5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ betterproto = { extras = ["compiler"], version = "^2.0.0b6", allow-prereleases = grpcio-tools = "^1.60.0" [tool.ruff] +preview = true line-length = 99 select = [ "PL", # pylint @@ -76,11 +77,12 @@ ignore = [ "PLR0913", # too-many-arguments "PGH003", # use specific rule code when ignore "T201", + "PLR0917", ] [tool.ruff.per-file-ignores] "tests/*" = ["S101", "PT012"] -"cent/proto/centrifugal/*" = ["RUF009", "ARG002", "E501", "TCH004"] +"cent/proto/centrifugal/*" = ["RUF009", "ARG002", "E501", "TCH004", "PLR6301", "PLR0904", "PLW3201"] [tool.mypy] strict = true @@ -109,7 +111,6 @@ plugins = ["pydantic.mypy"] [tool.pydantic-mypy] warn_required_dynamic_aliases = true - [[tool.mypy.overrides]] module = [ "pytest_benchmark.*" @@ -120,7 +121,6 @@ ignore_missing_imports = true module = ["cent.proto.centrifugal.*"] ignore_errors = true - [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" From a0f5c16536dc84f994228c75c6cfc5995cd09afa Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Mon, 12 Feb 2024 20:36:36 +0200 Subject: [PATCH 24/55] fix grpc api, reduce API scope --- .github/workflows/release.yml | 18 +--- .pre-commit-config.yaml | 2 - README.md | 63 +++++++++---- benchmarks/conftest.py | 2 +- cent/__init__.py | 21 +---- cent/base.py | 16 +--- cent/client/__init__.py | 12 --- cent/client/async_client.py | 37 +++++--- cent/client/grpc_client.py | 79 +++++++++------- cent/client/session/__init__.py | 8 +- cent/client/session/aiohttp.py | 44 +++++---- cent/client/session/base.py | 121 ------------------------- cent/client/session/base_async.py | 48 ---------- cent/client/session/base_http.py | 97 ++++++++++++++++++++ cent/client/session/base_http_async.py | 44 +++++++++ cent/client/session/base_http_sync.py | 38 ++++++++ cent/client/session/base_sync.py | 42 --------- cent/client/session/grpc.py | 81 ++++++++++------- cent/client/session/requests.py | 37 ++++---- cent/client/sync_client.py | 36 +++++--- cent/exceptions.py | 63 ++++++++----- cent/requests.py | 26 ++++-- cent/types.py | 76 ++++++++++------ pyproject.toml | 4 +- tests/conftest.py | 4 +- tests/test_async_validation.py | 34 +++++-- tests/test_grpc_validation.py | 32 +++++-- tests/test_sync_validation.py | 39 +++++--- 28 files changed, 591 insertions(+), 533 deletions(-) delete mode 100644 cent/client/session/base.py delete mode 100644 cent/client/session/base_async.py create mode 100644 cent/client/session/base_http.py create mode 100644 cent/client/session/base_http_async.py create mode 100644 cent/client/session/base_http_sync.py delete mode 100644 cent/client/session/base_sync.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e1bbc4d..363d435 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,18 +16,10 @@ jobs: with: python-version: '3.12' - - name: check GITHUB_REF matches package version - uses: samuelcolvin/check-python-version@v4.1 - with: - version_file_path: centrifuge/__meta__.py - - - name: Install build dependencies - run: pip install build + - name: Install poetry + run: pip install poetry - - name: Build distribution - run: python -m build + - run: poetry config pypi-token.pypi "${{ secrets.PYPI_PASSWORD }}" - - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_PASSWORD }} + - name: Publish package + run: poetry publish --build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e7ded36..d62f21b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,5 +26,3 @@ repos: name: Validate types with MyPy language: system types: [ python ] - args: - - "cent" diff --git a/README.md b/README.md index a36f0dd..7f8c970 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,14 @@ pip install cent **Cent v5 and higher works only with Centrifugo v5**. -* If you need to work with Centrifugo v3 then use Cent v4 +* If you need to work with Centrifugo v3, v4 then use Cent v4 * If you need to work with Centrifugo v2 then use Cent v3 -## High-level library API +## Usage First see [available API methods in documentation](https://centrifugal.dev/docs/server/server_api#api-methods). -This library contains `Client` and `AsyncClient` class to send messages to Centrifugo from your python-powered backend: +This library contains `Client`, `AsyncClient` and `GrpcClient` classes to work with Centrifugo HTTP API. ```python import asyncio @@ -30,46 +30,69 @@ url = "http://localhost:8000/api" api_key = "XXX" # Initialize a client (you can use sync or async version) -async_client = AsyncClient(url, api_key=api_key) sync_client = Client(url, api_key=api_key) +async_client = AsyncClient(url, api_key=api_key) -response = sync_client.publish("example:channel", {"input": "Hello world!"}) -print(response) +# Now you can use sync client to call API methods. +result = sync_client.publish("example:channel", {"input": "Hello world!"}) +print(result) async def main(): - response = await async_client.publish("example:channel", {"input": "Hello world!"}) - print(response) + # And async client to call API methods too. + result = await async_client.publish("example:channel", {"input": "Hello world!"}) + print(result) if __name__ == "__main__": asyncio.run(main()) ``` -### Client init arguments +### Handling errors + +This library may raise exceptions if sth goes wrong. All exceptions are subclasses of `cent.CentError`. + +* CentError - base class for all exceptions +* CentNetworkError - raised in case of network related errors (connection refused) +* CentTransportError - raised in case of transport related errors (HTTP status code is not 2xx) +* CentTimeoutError - raised in case of timeout +* CentUnauthorizedError - raised in case of unauthorized access +* CentDecodeError - raised in case of server response decoding error +* CentAPIError - raised in case of API error (error returned by Centrifugo itself) + +### HTTP client init arguments Required: -* base_url - Centrifugo HTTP API endpoint address -* api_key - Centrifugo HTTP API key +* `api_url` (str) - Centrifugo HTTP API URL address +* `api_key` (str) - Centrifugo HTTP API key Optional: -* session (`BaseSession`) - session to use - -You can use `AiohttpSession` or create custom from `BaseSession` class. +* `request_timeout` (float) - base timeout for all requests in seconds, default is 10 seconds. -Arguments for default session: +### GRPC client init arguments Required: -* base_url - Centrifugo HTTP API endpoint address +* `host` (str) - Centrifugo GRPC API host +* `port` (int) - Centrifugo GRPC API port Optional: -* json_loads — function to load JSON from response body (default is json, but you can use - orjson, ujson etc.) -* timeout - timeout for requests (default is 10.0) +* `request_timeout` (float) - base timeout for all requests in seconds, default is 10 seconds. + +## HTTP vs GRPC for payloads + +When using HTTP-based clients (`Client` and `AsyncClient`): + +* you should pass payload as a Python objects which can be serialized to JSON +* in results, you will receive Python objects already deserialized from JSON. + +When using GRPC-based client (`GrpcClient`): + +* you must pass payloads as `bytes` +* in results, you will receive `bytes` for payloads ## For contributors @@ -90,5 +113,5 @@ make bench ### Generate code from proto file, if needed ```bash -poetry run python -m grpc_tools.protoc -I . --python_betterproto_out=./cent/proto cent/proto/apiproto.proto +make proto ``` diff --git a/benchmarks/conftest.py b/benchmarks/conftest.py index 4daaf12..d2db6c9 100644 --- a/benchmarks/conftest.py +++ b/benchmarks/conftest.py @@ -86,4 +86,4 @@ async def async_client( ) -> AsyncGenerator[AsyncClient, None]: client = AsyncClient(BASE_URL, API_KEY) yield client - await client.session.close() + await client.close() diff --git a/cent/__init__.py b/cent/__init__.py index be2f1f9..b908632 100644 --- a/cent/__init__.py +++ b/cent/__init__.py @@ -5,14 +5,9 @@ Client, AsyncClient, GrpcClient, - BaseSession, - BaseAsyncSession, - BaseSyncSession, - RequestsSession, - AiohttpSession, ) +from cent.base import CentRequest from cent.requests import ( - CentRequest, BroadcastRequest, PublishRequest, SubscribeRequest, @@ -55,10 +50,10 @@ from cent.exceptions import ( CentError, CentNetworkError, - CentClientDecodeError, + CentTransportError, CentUnauthorizedError, + CentDecodeError, CentAPIError, - CentTransportError, ) with contextlib.suppress(ImportError): @@ -67,18 +62,14 @@ _asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy()) __all__ = ( - "AiohttpSession", "AsyncClient", - "BaseAsyncSession", - "BaseSession", - "BaseSyncSession", "BatchRequest", "BatchResult", "BoolValue", "BroadcastRequest", "BroadcastResult", "CentAPIError", - "CentClientDecodeError", + "CentDecodeError", "CentError", "CentNetworkError", "CentRequest", @@ -110,13 +101,9 @@ "PublishResult", "RefreshRequest", "RefreshResult", - "RequestsSession", "StreamPosition", "SubscribeRequest", "SubscribeResult", "UnsubscribeRequest", "UnsubscribeResult", - "exceptions", - "requests", - "types", ) diff --git a/cent/base.py b/cent/base.py index a6a7667..e690ee4 100644 --- a/cent/base.py +++ b/cent/base.py @@ -16,20 +16,6 @@ class BaseResult(BaseModel, ABC): ) -try: - import orjson as _orjson # type: ignore[import-not-found] - - json_dumps = _orjson.dumps - json_loads = _orjson.loads -except ImportError: - import json - - def json_dumps(x: Any) -> bytes: - return json.dumps(x).encode() - - json_loads = json.loads - - CentType = TypeVar("CentType", bound=Any) @@ -55,6 +41,7 @@ class CentRequest(BaseModel, Generic[CentType], ABC): __api_method__: ClassVar[str] __grpc_method__: ClassVar[type] else: + @property @abstractmethod def __returning__(self) -> type: @@ -80,6 +67,7 @@ class NestedModel(BaseModel, ABC): if TYPE_CHECKING: __grpc_method__: ClassVar[type] else: + @property @abstractmethod def __grpc_method__(self) -> type: diff --git a/cent/client/__init__.py b/cent/client/__init__.py index 04835bb..961ceb4 100644 --- a/cent/client/__init__.py +++ b/cent/client/__init__.py @@ -1,21 +1,9 @@ -from .session import ( - BaseSession, - BaseAsyncSession, - BaseSyncSession, - AiohttpSession, - RequestsSession, -) from .sync_client import Client from .async_client import AsyncClient from .grpc_client import GrpcClient __all__ = ( - "AiohttpSession", "AsyncClient", - "BaseAsyncSession", - "BaseSession", - "BaseSyncSession", "Client", "GrpcClient", - "RequestsSession", ) diff --git a/cent/client/async_client.py b/cent/client/async_client.py index 0726cd8..8f4e0d4 100644 --- a/cent/client/async_client.py +++ b/cent/client/async_client.py @@ -1,8 +1,8 @@ from typing import List, Optional, Any, Dict, TypeVar -from cent.client.session import BaseAsyncSession, AiohttpSession +from cent.client.session import AiohttpSession +from cent.base import CentRequest from cent.requests import ( - CentRequest, BroadcastRequest, PublishRequest, SubscribeRequest, @@ -18,6 +18,7 @@ BatchRequest, ) from cent.results import ( + BatchResult, PublishResult, BroadcastResult, SubscribeResult, @@ -36,7 +37,6 @@ ChannelOptionsOverride, Disconnect, ) -from cent.results import BatchResult T = TypeVar("T") @@ -45,19 +45,21 @@ class AsyncClient: def __init__( self, - base_url: str, + api_url: str, api_key: str, - session: Optional[BaseAsyncSession] = None, + request_timeout: Optional[float] = 10.0, ) -> None: """ - :param base_url: Centrifuge base_url - :param api_key: Centrifuge API key - :param session: Custom Session instance + :param api_url: Centrifugo API URL + :param api_key: Centrifugo API key + :param request_timeout: base timeout for all requests. """ - - self._base_url = base_url - self.api_key = api_key - self.session = session or AiohttpSession(base_url=base_url) + self._base_url = api_url + self._api_key = api_key + self._session = AiohttpSession( + base_url=api_url, + timeout=request_timeout, + ) async def publish( self, @@ -250,11 +252,16 @@ async def batch( call = BatchRequest.model_construct(commands=commands) return await self(call, request_timeout=request_timeout) - async def __call__(self, method: CentRequest[T], request_timeout: Optional[float] = None) -> T: + async def close(self) -> None: + await self._session.close() + + async def __call__( + self, request: CentRequest[T], request_timeout: Optional[float] = None + ) -> T: """ Call API method - :param method: Centrifugo method + :param request: Centrifugo request :return: Centrifugo response """ - return await self.session(self, method, timeout=request_timeout) + return await self._session(self._api_key, request, timeout=request_timeout) diff --git a/cent/client/grpc_client.py b/cent/client/grpc_client.py index 6eb3af8..a9ce271 100644 --- a/cent/client/grpc_client.py +++ b/cent/client/grpc_client.py @@ -1,8 +1,8 @@ -from typing import Any, Optional, Dict, TypeVar, List +from typing import Optional, Dict, TypeVar, List -from cent.client.session.grpc import GrpcSession +from cent.client.session import GrpcSession +from cent.base import CentRequest from cent.requests import ( - CentRequest, BroadcastRequest, PublishRequest, SubscribeRequest, @@ -36,77 +36,78 @@ Disconnect, ) + T = TypeVar("T") class GrpcClient: - def __init__(self, host: str, port: int) -> None: - self.session = GrpcSession(host=host, port=port) + def __init__( + self, + host: str, + port: int, + request_timeout: Optional[float] = 10.0, + ) -> None: + self._session = GrpcSession(host=host, port=port, timeout=request_timeout) async def publish( self, channel: str, - data: Any, + data: bytes, skip_history: Optional[bool] = None, tags: Optional[Dict[str, str]] = None, - b64data: Optional[str] = None, idempotency_key: Optional[str] = None, + request_timeout: Optional[float] = None, ) -> PublishResult: call = PublishRequest( channel=channel, data=data, skip_history=skip_history, tags=tags, - b64data=b64data, idempotency_key=idempotency_key, ) - return await self(call) + return await self(call, request_timeout=request_timeout) async def broadcast( self, channels: List[str], - data: Any, + data: bytes, skip_history: Optional[bool] = None, tags: Optional[Dict[str, str]] = None, - b64data: Optional[str] = None, idempotency_key: Optional[str] = None, + request_timeout: Optional[float] = None, ) -> BroadcastResult: call = BroadcastRequest( channels=channels, data=data, skip_history=skip_history, tags=tags, - b64data=b64data, idempotency_key=idempotency_key, ) - return await self(call) + return await self(call, request_timeout=request_timeout) async def subscribe( self, user: str, channel: str, - info: Optional[Any] = None, - b64info: Optional[str] = None, + info: Optional[bytes] = None, client: Optional[str] = None, session: Optional[str] = None, - data: Optional[Any] = None, - b64data: Optional[str] = None, + data: Optional[bytes] = None, recover_since: Optional[StreamPosition] = None, override: Optional[ChannelOptionsOverride] = None, + request_timeout: Optional[float] = None, ) -> SubscribeResult: call = SubscribeRequest( user=user, channel=channel, info=info, - b64info=b64info, client=client, session=session, data=data, - b64data=b64data, recover_since=recover_since, override=override, ) - return await self(call) + return await self(call, request_timeout=request_timeout) async def unsubscribe( self, @@ -114,6 +115,7 @@ async def unsubscribe( channel: str, client: Optional[str] = None, session: Optional[str] = None, + request_timeout: Optional[float] = None, ) -> UnsubscribeResult: call = UnsubscribeRequest( user=user, @@ -121,25 +123,27 @@ async def unsubscribe( client=client, session=session, ) - return await self(call) + return await self(call, request_timeout=request_timeout) async def presence( self, channel: str, + request_timeout: Optional[float] = None, ) -> PresenceResult: call = PresenceRequest( channel=channel, ) - return await self(call) + return await self(call, request_timeout=request_timeout) async def presence_stats( self, channel: str, + request_timeout: Optional[float] = None, ) -> PresenceStatsResult: call = PresenceStatsRequest( channel=channel, ) - return await self(call) + return await self(call, request_timeout=request_timeout) async def history( self, @@ -147,6 +151,7 @@ async def history( limit: Optional[int] = None, since: Optional[StreamPosition] = None, reverse: Optional[bool] = None, + request_timeout: Optional[float] = None, ) -> HistoryResult: call = HistoryRequest( channel=channel, @@ -154,16 +159,17 @@ async def history( since=since, reverse=reverse, ) - return await self(call) + return await self(call, request_timeout=request_timeout) async def history_remove( self, channel: str, + request_timeout: Optional[float] = None, ) -> HistoryRemoveResult: call = HistoryRemoveRequest( channel=channel, ) - return await self(call) + return await self(call, request_timeout=request_timeout) async def refresh( self, @@ -172,6 +178,7 @@ async def refresh( session: Optional[str] = None, expire_at: Optional[int] = None, expired: Optional[bool] = None, + request_timeout: Optional[float] = None, ) -> RefreshResult: call = RefreshRequest( user=user, @@ -180,16 +187,17 @@ async def refresh( expire_at=expire_at, expired=expired, ) - return await self(call) + return await self(call, request_timeout=request_timeout) async def channels( self, pattern: Optional[str] = None, + request_timeout: Optional[float] = None, ) -> ChannelsResult: call = ChannelsRequest( pattern=pattern, ) - return await self(call) + return await self(call, request_timeout=request_timeout) async def disconnect( self, @@ -198,6 +206,7 @@ async def disconnect( session: Optional[str] = None, whitelist: Optional[List[str]] = None, disconnect: Optional[Disconnect] = None, + request_timeout: Optional[float] = None, ) -> DisconnectResult: call = DisconnectRequest( user=user, @@ -206,19 +215,25 @@ async def disconnect( whitelist=whitelist, disconnect=disconnect, ) - return await self(call) + return await self(call, request_timeout=request_timeout) async def info( self, + request_timeout: Optional[float] = None, ) -> InfoResult: call = InfoRequest() - return await self(call) + return await self(call, request_timeout=request_timeout) + + async def close(self) -> None: + self._session.close() - async def __call__(self, method: CentRequest[T]) -> T: + async def __call__( + self, request: CentRequest[T], request_timeout: Optional[float] = None + ) -> T: """ Call API method - :param method: Centrifugo method + :param request: Centrifugo request :return: Centrifugo response """ - return await self.session(self, method) + return await self._session(request, request_timeout) diff --git a/cent/client/session/__init__.py b/cent/client/session/__init__.py index ca7f2ab..08f2771 100644 --- a/cent/client/session/__init__.py +++ b/cent/client/session/__init__.py @@ -1,13 +1,9 @@ -from .base import BaseSession -from .base_async import BaseAsyncSession -from .base_sync import BaseSyncSession from .aiohttp import AiohttpSession from .requests import RequestsSession +from .grpc import GrpcSession __all__ = ( "AiohttpSession", - "BaseAsyncSession", - "BaseSession", - "BaseSyncSession", + "GrpcSession", "RequestsSession", ) diff --git a/cent/client/session/aiohttp.py b/cent/client/session/aiohttp.py index 3d0baf5..ce1d019 100644 --- a/cent/client/session/aiohttp.py +++ b/cent/client/session/aiohttp.py @@ -1,25 +1,24 @@ import asyncio -from typing import Optional, TYPE_CHECKING, cast, Any +from typing import Optional, cast, Any -from aiohttp import ClientSession, ClientError +from aiohttp import ClientSession, ClientError, ClientTimeout -from cent.client.session.base_async import BaseAsyncSession -from cent.base import CentType -from cent.requests import CentRequest, BatchRequest -from cent.exceptions import CentNetworkError +from cent.client.session.base_http_async import BaseHttpAsyncSession +from cent.base import CentType, CentRequest +from cent.requests import BatchRequest +from cent.exceptions import CentNetworkError, CentTimeoutError -if TYPE_CHECKING: - from cent.client import AsyncClient - -class AiohttpSession(BaseAsyncSession): +class AiohttpSession(BaseHttpAsyncSession): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self._session: Optional[ClientSession] = None async def _create_session(self) -> ClientSession: if self._session is None or self._session.closed: - self._session = ClientSession(headers=self._headers) + self._session = ClientSession( + headers=self._headers, timeout=ClientTimeout(total=self._timeout) + ) return self._session @@ -32,19 +31,19 @@ async def close(self) -> None: async def make_request( self, - client: "AsyncClient", - method: CentRequest[CentType], + api_key: str, + request: CentRequest[CentType], timeout: Optional[float] = None, ) -> CentType: session = await self._create_session() - session.headers["X-API-Key"] = client.api_key + session.headers["X-API-Key"] = api_key - if isinstance(method, BatchRequest): - json_data = self.get_batch_json_data(method) + if isinstance(request, BatchRequest): + json_data = self.get_batch_json_data(request) else: - json_data = method.model_dump(exclude_none=True) + json_data = request.model_dump(exclude_none=True) - url = f"{self._base_url}/{method.__api_method__}" + url = f"{self._base_url}/{request.__api_method__}" try: async with session.post( @@ -54,18 +53,17 @@ async def make_request( ) as resp: raw_result = await resp.text() except asyncio.TimeoutError as error: - raise CentNetworkError( - method=method, + raise CentTimeoutError( + request=request, message="Request timeout", ) from error except ClientError as error: raise CentNetworkError( - method=method, + request=request, message=f"{type(error).__name__}: {error}", ) from error response = self.check_response( - client=client, - method=method, + request=request, status_code=resp.status, content=raw_result, ) diff --git a/cent/client/session/base.py b/cent/client/session/base.py deleted file mode 100644 index c9c56cb..0000000 --- a/cent/client/session/base.py +++ /dev/null @@ -1,121 +0,0 @@ -from http import HTTPStatus -from typing import Final, TYPE_CHECKING, Callable, Any, Union, Dict, List - -from aiohttp.http import SERVER_SOFTWARE -from pydantic import ValidationError, TypeAdapter, __version__ - -from cent.exceptions import ( - CentClientDecodeError, - CentAPIError, - CentUnauthorizedError, - CentTransportError, -) -from cent.base import ( - json_loads as _json_loads, - CentRequest, - CentType, - Response, -) -from cent.requests import BatchRequest - -if TYPE_CHECKING: - from cent.client.sync_client import Client - from cent.client.async_client import AsyncClient - -DEFAULT_TIMEOUT: Final[float] = 10.0 -_JsonLoads = Callable[..., Any] -_JsonDumps = Callable[..., str] - - -class BaseSession: - """Base class for all sessions.""" - - def __init__( - self, - base_url: str, - json_loads: _JsonLoads = _json_loads, - timeout: float = DEFAULT_TIMEOUT, - ) -> None: - """ - Initialize session. - - :param base_url: Centrifuge base url. - :param json_loads: JSON loader. - :param timeout: Default request timeout. - """ - self._base_url = base_url - self.json_loads = json_loads - self._timeout = timeout - self._headers = { - "User-Agent": f"{SERVER_SOFTWARE} pycent/{__version__}", - "Content-Type": "application/json", - } - - @staticmethod - def get_batch_json_data(method: BatchRequest) -> Dict[str, List[Dict[str, Any]]]: - commands = [ - {command.__api_method__: command.model_dump(exclude_none=True)} - for command in method.commands - ] - return {"commands": commands} - - @staticmethod - def validate_batch( - client: Union["Client", "AsyncClient"], - method: BatchRequest, - json_replies: List[Dict[str, Any]], - ) -> Dict[str, Dict[str, List[Any]]]: - """Validate batch method.""" - replies: List[CentRequest[Any]] = [] - for command_method, json_data in zip(method.commands, json_replies): - validated_method: CentRequest[Any] = TypeAdapter( - command_method.__returning__ - ).validate_python( - json_data[command_method.__api_method__], - context={"client": client}, - ) - replies.append(validated_method) - return {"result": {"replies": replies}} - - def check_response( - self, - client: Union["Client", "AsyncClient"], - method: CentRequest[CentType], - status_code: int, - content: str, - ) -> Response[CentType]: - """Validate response.""" - if status_code == HTTPStatus.UNAUTHORIZED: - raise CentUnauthorizedError - - if status_code != HTTPStatus.OK: - raise CentTransportError( - method=method, - status_code=status_code, - ) - - try: - json_data = self.json_loads(content) - except Exception as err: - raise CentClientDecodeError from err - - if isinstance(method, BatchRequest): - json_data = self.validate_batch(client, method, json_data["replies"]) - - try: - response_type = Response[method.__returning__] # type: ignore - response = TypeAdapter(response_type).validate_python( - json_data, - context={"client": client}, - ) - except ValidationError as err: - raise CentClientDecodeError from err - - if response.error: - raise CentAPIError( - method=method, - code=response.error.code, - message=response.error.message, - ) - - return response diff --git a/cent/client/session/base_async.py b/cent/client/session/base_async.py deleted file mode 100644 index 1916f93..0000000 --- a/cent/client/session/base_async.py +++ /dev/null @@ -1,48 +0,0 @@ -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Optional, cast - -from cent.client.session.base import BaseSession -from cent.requests import CentRequest -from cent.base import CentType - -if TYPE_CHECKING: - from cent.client.async_client import AsyncClient - - -class BaseAsyncSession(BaseSession, ABC): - """Base class for all sessions.""" - - @abstractmethod - async def close(self) -> None: - """ - Close client session - """ - - @abstractmethod - async def make_request( - self, - client: "AsyncClient", - method: CentRequest[CentType], - timeout: Optional[float] = None, - ) -> CentType: - """ - Make request to centrifuge API. - - :param client: Centrifuge client. - :param method: Centrifuge method. - :param timeout: Request timeout. - """ - - async def __call__( - self, - client: "AsyncClient", - method: CentRequest[CentType], - timeout: Optional[float] = None, - ) -> CentType: - return cast(CentType, await self.make_request(client, method, timeout)) - - async def __aenter__(self) -> "BaseAsyncSession": - return self - - async def __aexit__(self, *kwargs: Any) -> None: - await self.close() diff --git a/cent/client/session/base_http.py b/cent/client/session/base_http.py new file mode 100644 index 0000000..737a971 --- /dev/null +++ b/cent/client/session/base_http.py @@ -0,0 +1,97 @@ +import json +from http import HTTPStatus +from typing import Any, Dict, List + +from pydantic import ValidationError, TypeAdapter + +from cent.exceptions import ( + CentDecodeError, + CentAPIError, + CentUnauthorizedError, + CentTransportError, +) +from cent.base import ( + CentRequest, + CentType, + Response, +) +from cent.requests import BatchRequest + + +class BaseHttpSession: + """Base class for all sessions.""" + + def __init__( + self, + base_url: str, + timeout: float = 10.0, + ) -> None: + self._base_url = base_url + self._timeout = timeout + self._headers = { + "User-Agent": "centrifugal/pycent", + "Content-Type": "application/json", + } + + @staticmethod + def get_batch_json_data(request: BatchRequest) -> Dict[str, List[Dict[str, Any]]]: + commands = [ + {command.__api_method__: command.model_dump(exclude_none=True)} + for command in request.commands + ] + return {"commands": commands} + + @staticmethod + def validate_batch( + request: BatchRequest, + json_replies: List[Dict[str, Any]], + ) -> Dict[str, Dict[str, List[Any]]]: + replies: List[CentRequest[Any]] = [] + for command_method, json_data in zip(request.commands, json_replies): + validated_request: CentRequest[Any] = TypeAdapter( + command_method.__returning__ + ).validate_python( + json_data[command_method.__api_method__], + ) + replies.append(validated_request) + return {"result": {"replies": replies}} + + def check_response( + self, + request: CentRequest[CentType], + status_code: int, + content: str, + ) -> Response[CentType]: + if status_code == HTTPStatus.UNAUTHORIZED: + raise CentUnauthorizedError + + if status_code != HTTPStatus.OK: + raise CentTransportError( + request=request, + status_code=status_code, + ) + + try: + json_data = json.loads(content) + except Exception as err: + raise CentDecodeError from err + + if isinstance(request, BatchRequest): + json_data = self.validate_batch(request, json_data["replies"]) + + try: + response_type = Response[request.__returning__] # type: ignore + response = TypeAdapter(response_type).validate_python( + json_data, + ) + except ValidationError as err: + raise CentDecodeError from err + + if response.error: + raise CentAPIError( + request=request, + code=response.error.code, + message=response.error.message, + ) + + return response diff --git a/cent/client/session/base_http_async.py b/cent/client/session/base_http_async.py new file mode 100644 index 0000000..4afcdfd --- /dev/null +++ b/cent/client/session/base_http_async.py @@ -0,0 +1,44 @@ +from abc import ABC, abstractmethod +from typing import Any, Optional, cast + +from cent.client.session.base_http import BaseHttpSession +from cent.base import CentType, CentRequest + + +class BaseHttpAsyncSession(BaseHttpSession, ABC): + """Base class for all sessions.""" + + @abstractmethod + async def close(self) -> None: + """ + Close client session + """ + + @abstractmethod + async def make_request( + self, + api_key: str, + request: CentRequest[CentType], + timeout: Optional[float] = None, + ) -> CentType: + """ + Make request to centrifuge API. + + :param api_key: Centrifugo API key. + :param request: Centrifugo API request. + :param timeout: Request timeout. + """ + + async def __call__( + self, + api_key: str, + request: CentRequest[CentType], + timeout: Optional[float] = None, + ) -> CentType: + return cast(CentType, await self.make_request(api_key, request, timeout)) + + async def __aenter__(self) -> "BaseHttpAsyncSession": + return self + + async def __aexit__(self, *kwargs: Any) -> None: + await self.close() diff --git a/cent/client/session/base_http_sync.py b/cent/client/session/base_http_sync.py new file mode 100644 index 0000000..54542ab --- /dev/null +++ b/cent/client/session/base_http_sync.py @@ -0,0 +1,38 @@ +from abc import abstractmethod, ABC +from typing import Optional + +from cent.client.session.base_http import BaseHttpSession +from cent.base import CentType, CentRequest + + +class BaseHttpSyncSession(BaseHttpSession, ABC): + """Base class for all sessions.""" + + @abstractmethod + def close(self) -> None: + """ + Close client session + """ + + @abstractmethod + def make_request( + self, + api_key: str, + request: CentRequest[CentType], + timeout: Optional[float] = None, + ) -> CentType: + """ + Make request to Centrifugo API. + + :param api_key: Centrifugo API key. + :param request: Centrifugo API request. + :param timeout: Request timeout. + """ + + def __call__( + self, + api_key: str, + request: CentRequest[CentType], + timeout: Optional[float] = None, + ) -> CentType: + return self.make_request(api_key, request, timeout) diff --git a/cent/client/session/base_sync.py b/cent/client/session/base_sync.py deleted file mode 100644 index 5b739c2..0000000 --- a/cent/client/session/base_sync.py +++ /dev/null @@ -1,42 +0,0 @@ -from abc import abstractmethod, ABC -from typing import TYPE_CHECKING, Optional - -from cent.client.session.base import BaseSession -from cent.requests import CentRequest -from cent.base import CentType - -if TYPE_CHECKING: - from cent.client.sync_client import Client - - -class BaseSyncSession(BaseSession, ABC): - """Base class for all sessions.""" - - @abstractmethod - def close(self) -> None: - """ - Close client session - """ - - @abstractmethod - def make_request( - self, - client: "Client", - method: CentRequest[CentType], - timeout: Optional[float] = None, - ) -> CentType: - """ - Make request to centrifuge API. - - :param client: Centrifuge client. - :param method: Centrifuge method. - :param timeout: Request timeout. - """ - - def __call__( - self, - client: "Client", - method: CentRequest[CentType], - timeout: Optional[float] = None, - ) -> CentType: - return self.make_request(client, method, timeout) diff --git a/cent/client/session/grpc.py b/cent/client/session/grpc.py index bd86fd2..915c143 100644 --- a/cent/client/session/grpc.py +++ b/cent/client/session/grpc.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, asdict -from typing import TYPE_CHECKING, cast, Type, Dict, Any, List, Tuple +from typing import cast, Type, Dict, Any, List, Tuple, Optional import betterproto from grpclib import GRPCError @@ -7,21 +7,17 @@ from pydantic import TypeAdapter, BaseModel from cent.proto.centrifugal.centrifugo.api import CentrifugoApiStub -from cent.exceptions import CentAPIError, CentTransportError -from cent.requests import CentRequest -from cent.base import CentType, Response, Error - -if TYPE_CHECKING: - from cent.client.grpc_client import GrpcClient +from cent.exceptions import CentAPIError, CentTransportError, CentTimeoutError, CentNetworkError +from cent.base import CentType, Response, Error, CentRequest @dataclass -class BaseResponse(betterproto.Message): +class _BaseResponse(betterproto.Message): error: Error result: Type[betterproto.Message] -def dict_factory(x: List[Tuple[str, Any]]) -> Dict[str, Any]: +def _dict_factory(x: List[Tuple[str, Any]]) -> Dict[str, Any]: response = {} for k, v in x: if v: @@ -30,58 +26,75 @@ def dict_factory(x: List[Tuple[str, Any]]) -> Dict[str, Any]: class GrpcSession: - def __init__(self, host: str, port: int) -> None: + def __init__(self, host: str, port: int, timeout: Optional[float] = 10.0) -> None: self._channel = Channel(host=host, port=port) self._stub = CentrifugoApiStub(channel=self._channel) + self._timeout = timeout def close(self) -> None: self._channel.close() @staticmethod def check_response( - client: "GrpcClient", - method: CentRequest[CentType], - content: BaseResponse, - ) -> None: + request: CentRequest[CentType], + content: _BaseResponse, + ) -> Response[CentType]: """Validate response.""" - response_type = Response[method.__returning__] # type: ignore + response_type = Response[request.__returning__] # type: ignore response = TypeAdapter(response_type).validate_python( - asdict(content, dict_factory=dict_factory), context={"client": client} + asdict(content, dict_factory=_dict_factory) ) if response.error: raise CentAPIError( - method=method, + request=request, code=response.error.code, message=response.error.message, ) + return response - def convert_to_grpc(self, method: CentRequest[CentType]) -> Any: - request = method.model_dump(by_alias=True, exclude_none=True, mode="grpc") - for key, value in method.model_fields.items(): - attr = getattr(method, key) + def convert_to_grpc(self, request: CentRequest[CentType]) -> Any: + request_dump = request.model_dump(by_alias=True, exclude_none=True, mode="grpc") + for key, value in request.model_fields.items(): + attr = getattr(request, key) if issubclass(attr.__class__, BaseModel): - request[value.alias or key] = self.convert_to_grpc(attr) - return method.__grpc_method__(**request) + request_dump[value.alias or key] = self.convert_to_grpc(attr) + return request.__grpc_method__(**request_dump) async def make_request( self, - client: "GrpcClient", - method: CentRequest[CentType], - ) -> None: - api_method = getattr(self._stub, method.__api_method__) + request: CentRequest[CentType], + timeout: Optional[float] = None, + ) -> CentType: + api_method = getattr(self._stub, request.__api_method__) try: - response = await api_method(self.convert_to_grpc(method)) + response = await api_method( + self.convert_to_grpc(request), timeout=timeout or self._timeout + ) + except TimeoutError as error: + raise CentTimeoutError( + request=request, + message="Request timeout", + ) from error except GRPCError as error: - raise CentTransportError(method=method, status_code=error.status.value) from error - - self.check_response(client, method, response) + raise CentTransportError( + request=request, + status_code=error.status.value, + ) from error + except Exception as error: + raise CentNetworkError( + request=request, + message=f"{type(error).__name__}: {error}", + ) from error + + resp = self.check_response(request, response) + return cast(CentType, resp.result) async def __call__( self, - client: "GrpcClient", - method: CentRequest[CentType], + request: CentRequest[CentType], + timeout: Optional[float] = None, ) -> CentType: - return cast(CentType, await self.make_request(client, method)) + return cast(CentType, await self.make_request(request, timeout)) def __del__(self) -> None: self.close() diff --git a/cent/client/session/requests.py b/cent/client/session/requests.py index 832b7b0..5f4ee8f 100644 --- a/cent/client/session/requests.py +++ b/cent/client/session/requests.py @@ -1,19 +1,15 @@ -from typing import Optional, TYPE_CHECKING, cast, Any +from typing import Optional, cast, Any import requests from requests import Session -from cent.requests import CentRequest -from cent.base import CentType -from cent.client.session.base_sync import BaseSyncSession -from cent.exceptions import CentNetworkError +from cent.base import CentType, CentRequest +from cent.client.session.base_http_sync import BaseHttpSyncSession +from cent.exceptions import CentNetworkError, CentTimeoutError from cent.requests import BatchRequest -if TYPE_CHECKING: - from cent.client.sync_client import Client - -class RequestsSession(BaseSyncSession): +class RequestsSession(BaseHttpSyncSession): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self._session = Session() @@ -25,17 +21,17 @@ def close(self) -> None: def make_request( self, - client: "Client", - method: CentRequest[CentType], + api_key: str, + request: CentRequest[CentType], timeout: Optional[float] = None, ) -> CentType: - self._session.headers["X-API-Key"] = client.api_key - if isinstance(method, BatchRequest): - json_data = self.get_batch_json_data(method) + self._session.headers["X-API-Key"] = api_key + if isinstance(request, BatchRequest): + json_data = self.get_batch_json_data(request) else: - json_data = method.model_dump(exclude_none=True) + json_data = request.model_dump(exclude_none=True) - url = f"{self._base_url}/{method.__api_method__}" + url = f"{self._base_url}/{request.__api_method__}" try: raw_result = self._session.post( @@ -44,18 +40,17 @@ def make_request( timeout=timeout or self._timeout, ) except requests.exceptions.Timeout as error: - raise CentNetworkError( - method=method, + raise CentTimeoutError( + request=request, message="Request timeout", ) from error except requests.exceptions.ConnectionError as error: raise CentNetworkError( - method=method, + request=request, message=f"{type(error).__name__}: {error}", ) from error response = self.check_response( - client=client, - method=method, + request=request, status_code=raw_result.status_code, content=raw_result.text, ) diff --git a/cent/client/sync_client.py b/cent/client/sync_client.py index b4837a0..474ff1c 100644 --- a/cent/client/sync_client.py +++ b/cent/client/sync_client.py @@ -1,8 +1,8 @@ from typing import List, Optional, Any, Dict, TypeVar -from cent.client.session import BaseSyncSession, RequestsSession +from cent.client.session import RequestsSession +from cent.base import CentRequest from cent.requests import ( - CentRequest, BroadcastRequest, PublishRequest, SubscribeRequest, @@ -15,9 +15,10 @@ ChannelsRequest, DisconnectRequest, InfoRequest, - BatchRequest + BatchRequest, ) from cent.results import ( + BatchResult, PublishResult, BroadcastResult, SubscribeResult, @@ -36,7 +37,6 @@ ChannelOptionsOverride, Disconnect, ) -from cent.results import BatchResult T = TypeVar("T") @@ -45,19 +45,22 @@ class Client: def __init__( self, - base_url: str, + api_url: str, api_key: str, - session: Optional[BaseSyncSession] = None, + request_timeout: Optional[float] = 10.0, ) -> None: """ - :param base_url: Centrifuge base_url - :param api_key: Centrifuge API key - :param session: Custom Session instance + :param api_url: Centrifugo API URL + :param api_key: Centrifugo API key + :param request_timeout: Base timeout for all requests. """ - self._base_url = base_url - self.api_key = api_key - self.session = session or RequestsSession(base_url=base_url) + self._api_url = api_url + self._api_key = api_key + self._session = RequestsSession( + base_url=api_url, + timeout=request_timeout, + ) def publish( self, @@ -250,11 +253,14 @@ def batch( call = BatchRequest.model_construct(commands=commands) return self(call, request_timeout=request_timeout) - def __call__(self, method: CentRequest[T], request_timeout: Optional[float] = None) -> T: + def close(self) -> None: + self._session.close() + + def __call__(self, request: CentRequest[T], request_timeout: Optional[float] = None) -> T: """ Call API method - :param method: Centrifugo method + :param request: Centrifugo request :return: Centrifugo response """ - return self.session(self, method, timeout=request_timeout) + return self._session(self._api_key, request, timeout=request_timeout) diff --git a/cent/exceptions.py b/cent/exceptions.py index 3e080ec..bc0449d 100644 --- a/cent/exceptions.py +++ b/cent/exceptions.py @@ -10,22 +10,43 @@ class CentError(Exception): class CentNetworkError(CentError): """CentNetworkError raised when Centrifugo is not available.""" - def __init__(self, method: CentRequest[CentType], message: str) -> None: - self.method = method + def __init__(self, request: CentRequest[CentType], message: str) -> None: + self.request = request self.message = message def __str__(self) -> str: - return f"HTTP error - {self.message}" + return f"Network error - {self.message}" def __repr__(self) -> str: return f"{type(self).__name__}('{self}')" -class CentClientDecodeError(CentError): - """ - CentClientDecodeError raised when response from Centrifugo can't be decoded - from JSON. - """ +class CentTransportError(CentError): + """CentTransportError raised when returns non-200 status code.""" + + def __init__(self, request: CentRequest[CentType], status_code: int): + self.request = request + self.status_code = status_code + + def __str__(self) -> str: + return f"Transport error - {self.status_code}" + + def __repr__(self) -> str: + return f"{type(self).__name__}('{self}')" + + +class CentTimeoutError(CentError): + """CentTimeoutError raised when request is timed out""" + + def __init__(self, request: CentRequest[CentType], message: str) -> None: + self.request = request + self.message = message + + def __str__(self) -> str: + return f"Timeout error - {self.message}" + + def __repr__(self) -> str: + return f"{type(self).__name__}('{self}')" class CentUnauthorizedError(CentError): @@ -34,33 +55,25 @@ class CentUnauthorizedError(CentError): """ +class CentDecodeError(CentError): + """ + CentDecodeError raised when response from Centrifugo can't be decoded. + """ + + class CentAPIError(CentError): """ CentAPIError raised when response from Centrifugo contains any error as a result of API command execution. """ - def __init__(self, method: CentRequest[CentType], code: int, message: str) -> None: - self.method = method + def __init__(self, request: CentRequest[CentType], code: int, message: str) -> None: + self.request = request self.code = code self.message = message def __str__(self) -> str: - return f"Centrifuge error #{self.code}: {self.message}" - - def __repr__(self) -> str: - return f"{type(self).__name__}('{self}')" - - -class CentTransportError(CentError): - """CentTransportError raised when returns non-200 status code.""" - - def __init__(self, method: CentRequest[CentType], status_code: int): - self.method = method - self.status_code = status_code - - def __str__(self) -> str: - return f"Transport error - {self.status_code}" + return f"API error #{self.code}: {self.message}" def __repr__(self) -> str: return f"{type(self).__name__}('{self}')" diff --git a/cent/requests.py b/cent/requests.py index 3d8f496..f548126 100644 --- a/cent/requests.py +++ b/cent/requests.py @@ -1,7 +1,5 @@ from typing import Any, Optional, Dict, List -from pydantic import Field - from cent.proto.centrifugal.centrifugo.api import ( ChannelsRequest as GrpcChannelsRequest, PublishRequest as GrpcPublishRequest, @@ -22,10 +20,18 @@ from cent.types import StreamPosition, ChannelOptionsOverride, Disconnect from cent.results import ( - BatchResult, BroadcastResult, ChannelsResult, - DisconnectResult, HistoryResult, HistoryRemoveResult, - InfoResult, PresenceResult, PresenceStatsResult, - PublishResult, RefreshResult, SubscribeResult, + BatchResult, + BroadcastResult, + ChannelsResult, + DisconnectResult, + HistoryResult, + HistoryRemoveResult, + InfoResult, + PresenceResult, + PresenceStatsResult, + PublishResult, + RefreshResult, + SubscribeResult, UnsubscribeResult, ) @@ -57,7 +63,7 @@ class BroadcastRequest(CentRequest[BroadcastResult]): tags: Optional[Dict[str, str]] = None """Publication tags - map with arbitrary string keys and values which is attached to publication and will be delivered to clients.""" - b64data: Optional[str] = Field(None, alias="b64_data") + b64data: Optional[str] = None """Custom binary data to publish into a channel encoded to base64 so it's possible to use HTTP API to send binary to clients. Centrifugo will decode it from base64 before publishing. In case of GRPC you can publish binary using data field.""" @@ -173,7 +179,7 @@ class PublishRequest(CentRequest[PublishResult]): tags: Optional[Dict[str, str]] = None """Publication tags - map with arbitrary string keys and values which is attached to publication and will be delivered to clients.""" - b64data: Optional[str] = Field(None, alias="b64_data") + b64data: Optional[str] = None """Custom binary data to publish into a channel encoded to base64 so it's possible to use HTTP API to send binary to clients. Centrifugo will decode it from base64 before publishing. In case of GRPC you can publish binary using data field.""" @@ -215,7 +221,7 @@ class SubscribeRequest(CentRequest[SubscribeResult]): """Name of channel to subscribe user to.""" info: Optional[Any] = None """Attach custom data to subscription (will be used in presence and join/leave messages).""" - b64info: Optional[str] = Field(None, alias="b64_info") + b64info: Optional[str] = None """info in base64 for binary mode (will be decoded by Centrifugo).""" client: Optional[str] = None """Specific client ID to subscribe (user still required to be set, will ignore other user @@ -224,7 +230,7 @@ class SubscribeRequest(CentRequest[SubscribeResult]): """Specific client session to subscribe (user still required to be set).""" data: Optional[Any] = None """Custom subscription data (will be sent to client in Subscribe push).""" - b64data: Optional[str] = Field(None, alias="b64_data") + b64data: Optional[str] = None """Same as data but in base64 format (will be decoded by Centrifugo).""" recover_since: Optional[StreamPosition] = None """Stream position to recover from.""" diff --git a/cent/types.py b/cent/types.py index 9c95e2d..dff49a8 100644 --- a/cent/types.py +++ b/cent/types.py @@ -11,8 +11,26 @@ ) +class Disconnect(NestedModel): + """Disconnect data. + + Attributes: + code (int): Disconnect code. + reason (str): Disconnect reason. + """ + + __grpc_method__ = GrpcDisconnect + + code: int + reason: str + + class BoolValue(NestedModel): - """Bool value.""" + """Bool value. + + Attributes: + value (bool): Value. + """ __grpc_method__ = GrpcBoolValue @@ -88,54 +106,54 @@ class ClientInfo(BaseResult): class Publication(BaseResult): - """Publication result.""" + """Publication result. + + Attributes: + offset (int): Offset of publication in history stream. + data (Any): Custom JSON inside publication. + tags (Optional[Dict[str, str]]): Tags are optional. + """ data: Any - """Custom JSON inside publication.""" offset: int = Field(default=0) - """Offset of publication in history stream.""" + tags: Optional[Dict[str, str]] = None class Metrics(BaseResult): - """Metrics result.""" + """Metrics result. + + Attributes: + interval (float): Metrics aggregation interval. + items (Dict[str, float]): metric values. + """ interval: float = Field(default=0.0) - """Interval.""" items: Dict[str, float] - """Map where key is string and value is float.""" class Node(BaseResult): - """Node result.""" + """Node result. + + Attributes: + uid (str): Node unique identifier. + name (str): Node name. + version (str): Node version. + num_clients (int): Total number of connections. + num_subs (int): Total number of subscriptions. + num_users (int): Total number of users. + num_channels (int): Total number of channels. + uptime (int): Node uptime. + metrics (Optional[Metrics]): Node metrics. + process (Optional[ProcessStats]): Node process stats. + """ uid: str - """Node unique identifier.""" name: str - """Node name.""" version: str - """Node version.""" num_clients: int = Field(default=0) - """Total number of connections.""" num_subs: int = Field(default=0) - """Total number of subscriptions.""" num_users: int = Field(default=0) - """Total number of users.""" num_channels: int = Field(default=0) - """Total number of channels.""" uptime: int = Field(default=0) - """Node uptime.""" metrics: Optional[Metrics] = None - """Node metrics.""" process: Optional[ProcessStats] = None - """Node process.""" - - -class Disconnect(NestedModel): - """Disconnect data.""" - - __grpc_method__ = GrpcDisconnect - - code: int - """Disconnect code.""" - reason: str - """Disconnect reason.""" diff --git a/pyproject.toml b/pyproject.toml index 9d2bee5..491c4cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "cent" -version = "5.0.0" -description = "Python library to communicate with Centrifugo v5 HTTP API" +version = "5.0.0b1" +description = "Python library to communicate with Centrifugo v5 server API" authors = ["Alexandr Emelin", "Katant Savelev"] license = "MIT" readme = 'README.md' diff --git a/tests/conftest.py b/tests/conftest.py index c7b014e..4028149 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -30,7 +30,7 @@ async def grpc_client( ) -> AsyncGenerator[GrpcClient, None]: client = GrpcClient("localhost", 10000) yield client - client.session.close() + client._session.close() @pytest.fixture() @@ -39,4 +39,4 @@ async def async_client( ) -> AsyncGenerator[AsyncClient, None]: client = AsyncClient(BASE_URL, API_KEY) yield client - await client.session.close() + await client._session.close() diff --git a/tests/test_async_validation.py b/tests/test_async_validation.py index 93f5dd2..e4e56dd 100644 --- a/tests/test_async_validation.py +++ b/tests/test_async_validation.py @@ -1,21 +1,28 @@ +import uuid import pytest from cent import ( - AsyncClient, CentAPIError, PublishRequest, StreamPosition, Disconnect, - BroadcastRequest, PresenceRequest) + AsyncClient, + CentAPIError, + PublishRequest, + StreamPosition, + Disconnect, + BroadcastRequest, + PresenceRequest, +) from tests.conftest import UNKNOWN_CHANNEL_ERROR_CODE async def test_publish(async_client: AsyncClient) -> None: - await async_client.publish( + result = await async_client.publish( "personal_1", {"data": "data"}, skip_history=False, tags={"tag": "tag"}, - # b64data=b64encode(b"data").decode(), idempotency_key="idempotency_key", ) + assert result.offset async def test_broadcast(async_client: AsyncClient) -> None: @@ -24,7 +31,6 @@ async def test_broadcast(async_client: AsyncClient) -> None: {"data": "data"}, skip_history=False, tags={"tag": "tag"}, - # b64data=b64encode(b"data").decode(), idempotency_key="idempotency_key", ) @@ -34,7 +40,6 @@ async def test_subscribe(async_client: AsyncClient) -> None: "user", "personal_1", info={"info": "info"}, - # b64info=b64encode(b"info").decode(), client="client", session="session", data={"data": "data"}, @@ -63,11 +68,22 @@ async def test_presence_stats(async_client: AsyncClient) -> None: async def test_history(async_client: AsyncClient) -> None: - await async_client.history( - channel="personal_1", - limit=1, + num_pubs = 10 + channel = "personal_" + uuid.uuid4().hex + for i in range(num_pubs): + await async_client.publish( + channel, + {"data": f"data {i}"}, + ) + result = await async_client.history( + channel=channel, + limit=num_pubs, reverse=True, ) + assert isinstance(result.offset, int) + assert result.offset > 0 + assert len(result.publications) == num_pubs + assert result.publications[0].data == {"data": "data 9"} async def test_history_remove(async_client: AsyncClient) -> None: diff --git a/tests/test_grpc_validation.py b/tests/test_grpc_validation.py index 1b9af67..616ca56 100644 --- a/tests/test_grpc_validation.py +++ b/tests/test_grpc_validation.py @@ -1,21 +1,27 @@ +import uuid import json - import pytest -from cent import (GrpcClient, CentAPIError, StreamPosition, - ChannelOptionsOverride, BoolValue, Disconnect) +from cent import ( + GrpcClient, + CentAPIError, + StreamPosition, + ChannelOptionsOverride, + BoolValue, + Disconnect, +) from tests.conftest import UNKNOWN_CHANNEL_ERROR_CODE async def test_publish(grpc_client: GrpcClient) -> None: - await grpc_client.publish( + result = await grpc_client.publish( "personal_1", json.dumps({"data": "data"}).encode(), skip_history=False, tags={"tag": "tag"}, - # b64data=b64encode(b"data").decode(), idempotency_key="idempotency_key", ) + assert result.offset async def test_broadcast(grpc_client: GrpcClient) -> None: @@ -24,7 +30,6 @@ async def test_broadcast(grpc_client: GrpcClient) -> None: json.dumps({"data": "data"}).encode(), skip_history=False, tags={"tag": "tag"}, - # b64data=b64encode(b"data").decode(), idempotency_key="idempotency_key", ) @@ -34,7 +39,6 @@ async def test_subscribe(grpc_client: GrpcClient) -> None: "user", "personal_1", info=json.dumps({"info": "info"}).encode(), - # b64info=b64encode(b"info").decode(), client="client", session="session", data=json.dumps({"data": "data"}).encode(), @@ -68,11 +72,21 @@ async def test_presence_stats(grpc_client: GrpcClient) -> None: async def test_history(grpc_client: GrpcClient) -> None: - await grpc_client.history( - channel="personal_1", + channel = "personal_" + uuid.uuid4().hex + for i in range(10): + await grpc_client.publish( + channel, + json.dumps({"data": f"data {i}"}).encode(), + ) + result = await grpc_client.history( + channel=channel, limit=1, reverse=True, ) + assert isinstance(result.offset, int) + assert result.offset > 0 + assert len(result.publications) == 1 + assert result.publications[0].data == b'{"data": "data 9"}' async def test_history_remove(grpc_client: GrpcClient) -> None: diff --git a/tests/test_sync_validation.py b/tests/test_sync_validation.py index 5d734e4..8f0f695 100644 --- a/tests/test_sync_validation.py +++ b/tests/test_sync_validation.py @@ -1,21 +1,28 @@ - +import uuid import pytest -from cent import (Client, CentAPIError, PublishRequest, BroadcastRequest, PresenceRequest, - StreamPosition, Disconnect) +from cent import ( + Client, + CentAPIError, + PublishRequest, + BroadcastRequest, + PresenceRequest, + StreamPosition, + Disconnect, +) from tests.conftest import UNKNOWN_CHANNEL_ERROR_CODE def test_publish(sync_client: Client) -> None: - sync_client.publish( + result = sync_client.publish( "personal_1", {"data": "data"}, skip_history=False, tags={"tag": "tag"}, - # b64data=b64encode(b"data").decode(), idempotency_key="idempotency_key", ) + assert result.offset def test_broadcast(sync_client: Client) -> None: @@ -24,7 +31,6 @@ def test_broadcast(sync_client: Client) -> None: {"data": "data"}, skip_history=False, tags={"tag": "tag"}, - # b64data=b64encode(b"data").decode(), idempotency_key="idempotency_key", ) @@ -34,7 +40,6 @@ def test_subscribe(sync_client: Client) -> None: "user", "personal_1", info={"info": "info"}, - # b64info=b64encode(b"info").decode(), client="client", session="session", data={"data": "data"}, @@ -63,11 +68,23 @@ def test_presence_stats(sync_client: Client) -> None: def test_history(sync_client: Client) -> None: - sync_client.history( - channel="personal_1", - limit=1, - reverse=True, + num_pubs = 10 + channel = "personal_" + uuid.uuid4().hex + for i in range(num_pubs): + sync_client.publish( + channel, + {"data": f"data {i}"}, + ) + + result = sync_client.history( + channel=channel, + limit=num_pubs, + reverse=False, ) + assert isinstance(result.offset, int) + assert result.offset > 0 + assert len(result.publications) == num_pubs + assert result.publications[0].data == {"data": "data 0"} def test_history_remove(sync_client: Client) -> None: From 0178983a60115b9f0644ba5da2925f7f032ae75d Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Mon, 12 Feb 2024 22:35:28 +0200 Subject: [PATCH 25/55] possibility to provide custom sessions --- README.md | 113 +++++++++++++++----- cent/client/async_client.py | 10 +- cent/client/grpc_client.py | 9 +- cent/client/session/aiohttp.py | 31 ++++-- cent/client/session/base_http.py | 14 +-- cent/client/session/requests.py | 25 ++++- cent/client/sync_client.py | 7 +- cent/requests.py | 171 +++++++++++++++++++------------ 8 files changed, 251 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index 7f8c970..f76b7a7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,4 @@ -CENT -==== - -Python tools to communicate with Centrifugo v5 HTTP API. Python >= 3.9 supported. +Python SDK to communicate with Centrifugo v5 HTTP API. Python >= 3.9 supported. To install run: @@ -18,29 +15,29 @@ pip install cent ## Usage -First see [available API methods in documentation](https://centrifugal.dev/docs/server/server_api#api-methods). +See the description of Centrifugo [server API](https://centrifugal.dev/docs/server/server_api) in documentation. -This library contains `Client`, `AsyncClient` and `GrpcClient` classes to work with Centrifugo HTTP API. +This library contains `Client`, `AsyncClient` and `GrpcClient` classes to work with Centrifugo HTTP and GRPC server API. ```python import asyncio -from cent import AsyncClient, Client +from cent import Client, AsyncClient -url = "http://localhost:8000/api" -api_key = "XXX" +api_url = "http://localhost:8000/api" +api_key = "" # Initialize a client (you can use sync or async version) -sync_client = Client(url, api_key=api_key) -async_client = AsyncClient(url, api_key=api_key) +sync_client = Client(api_url, api_key) +async_client = AsyncClient(api_url, api_key) # Now you can use sync client to call API methods. -result = sync_client.publish("example:channel", {"input": "Hello world!"}) +result = sync_client.publish("channel", {"input": "Hello world!"}) print(result) async def main(): # And async client to call API methods too. - result = await async_client.publish("example:channel", {"input": "Hello world!"}) + result = await async_client.publish("channel", {"input": "Hello world!"}) print(result) @@ -48,19 +45,35 @@ if __name__ == "__main__": asyncio.run(main()) ``` -### Handling errors +For GRPC the usage is slightly different: + +```python +import asyncio +import json +from cent import GrpcClient + +host = "localhost" +port = 10000 + +grpc_client = GrpcClient(host, port) + +async def main(): + result = await grpc_client.publish( + "example:channel", json.dumps({"input": "Hello world!"}).encode()) + print(result) + + +if __name__ == "__main__": + asyncio.run(main()) +``` -This library may raise exceptions if sth goes wrong. All exceptions are subclasses of `cent.CentError`. +Note that in GRPC case you must pass payload as `bytes`, see below more details about payloads in HTTP vs GRPC cases. -* CentError - base class for all exceptions -* CentNetworkError - raised in case of network related errors (connection refused) -* CentTransportError - raised in case of transport related errors (HTTP status code is not 2xx) -* CentTimeoutError - raised in case of timeout -* CentUnauthorizedError - raised in case of unauthorized access -* CentDecodeError - raised in case of server response decoding error -* CentAPIError - raised in case of API error (error returned by Centrifugo itself) +## Sync HTTP client init arguments -### HTTP client init arguments +```python +from cent import Client +``` Required: @@ -70,8 +83,29 @@ Required: Optional: * `request_timeout` (float) - base timeout for all requests in seconds, default is 10 seconds. +* `session` (requests.Session) - custom `requests` session to use. -### GRPC client init arguments +## Async HTTP client init arguments + +```python +from cent import AsyncClient +``` + +Required: + +* `api_url` (str) - Centrifugo HTTP API URL address +* `api_key` (str) - Centrifugo HTTP API key + +Optional: + +* `request_timeout` (float) - base timeout for all requests in seconds, default is 10 seconds. +* `session` (aiohttp.ClientSession) - custom `aiohttp` session to use. + +## GRPC client init arguments + +```python +from cent import GrpcClient +``` Required: @@ -82,7 +116,7 @@ Optional: * `request_timeout` (float) - base timeout for all requests in seconds, default is 10 seconds. -## HTTP vs GRPC for payloads +## Payloads in HTTP vs GRPC cases When using HTTP-based clients (`Client` and `AsyncClient`): @@ -94,17 +128,42 @@ When using GRPC-based client (`GrpcClient`): * you must pass payloads as `bytes` * in results, you will receive `bytes` for payloads +## Handling errors + +This library raises exceptions if sth goes wrong. All exceptions are subclasses of `cent.CentError`. + +* `CentError` - base class for all exceptions +* `CentNetworkError` - raised in case of network related errors (connection refused) +* `CentTransportError` - raised in case of transport related errors (HTTP status code is not 2xx) +* `CentTimeoutError` - raised in case of timeout +* `CentUnauthorizedError` - raised in case of unauthorized access +* `CentDecodeError` - raised in case of server response decoding error +* `CentAPIError` - raised in case of API error (error returned by Centrifugo itself) + ## For contributors ### Tests and benchmarks -To start tests, you can use pytest with any additional options, for example: +Prerequisites – start Centrifugo server locally: + +```bash +CENTRIFUGO_API_KEY=api_key CENTRIFUGO_HISTORY_TTL=300s CENTRIFUGO_HISTORY_SIZE=100 \ +CENTRIFUGO_PRESENCE=true CENTRIFUGO_GRPC_API=true ./centrifugo +``` + +And install dependencies: + +```bash +make dev +``` + +To start tests, run: ```bash make test ``` -To start benchmarks, you can use pytest too, for example: +To start benchmarks, run: ```bash make bench diff --git a/cent/client/async_client.py b/cent/client/async_client.py index 8f4e0d4..b97fcb6 100644 --- a/cent/client/async_client.py +++ b/cent/client/async_client.py @@ -1,5 +1,7 @@ from typing import List, Optional, Any, Dict, TypeVar +from aiohttp import ClientSession + from cent.client.session import AiohttpSession from cent.base import CentRequest from cent.requests import ( @@ -48,17 +50,19 @@ def __init__( api_url: str, api_key: str, request_timeout: Optional[float] = 10.0, + session: Optional[ClientSession] = None, ) -> None: """ :param api_url: Centrifugo API URL :param api_key: Centrifugo API key - :param request_timeout: base timeout for all requests. + :param request_timeout: Base timeout for all requests + :param session: Custom `aiohttp` session """ - self._base_url = api_url self._api_key = api_key self._session = AiohttpSession( - base_url=api_url, + api_url, timeout=request_timeout, + session=session, ) async def publish( diff --git a/cent/client/grpc_client.py b/cent/client/grpc_client.py index a9ce271..5c00a87 100644 --- a/cent/client/grpc_client.py +++ b/cent/client/grpc_client.py @@ -47,7 +47,14 @@ def __init__( port: int, request_timeout: Optional[float] = 10.0, ) -> None: - self._session = GrpcSession(host=host, port=port, timeout=request_timeout) + """ + Initialize GRPC client. + + :param host: Centrifugo host. + :param port: Centrifugo port. + :param request_timeout: Request timeout. + """ + self._session = GrpcSession(host, port, timeout=request_timeout) async def publish( self, diff --git a/cent/client/session/aiohttp.py b/cent/client/session/aiohttp.py index ce1d019..90c6f80 100644 --- a/cent/client/session/aiohttp.py +++ b/cent/client/session/aiohttp.py @@ -1,5 +1,5 @@ import asyncio -from typing import Optional, cast, Any +from typing import Optional, cast from aiohttp import ClientSession, ClientError, ClientTimeout @@ -10,18 +10,27 @@ class AiohttpSession(BaseHttpAsyncSession): - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self._session: Optional[ClientSession] = None - - async def _create_session(self) -> ClientSession: - if self._session is None or self._session.closed: + def __init__( + self, + base_url: str, + timeout: Optional[float] = 10.0, + session: Optional[ClientSession] = None, + ) -> None: + super().__init__() + self._base_url = base_url + self._timeout = timeout + self._session: ClientSession + if session: + self._session = session + else: self._session = ClientSession( - headers=self._headers, timeout=ClientTimeout(total=self._timeout) + headers={ + "User-Agent": "centrifugal/pycent", + "Content-Type": "application/json", + }, + timeout=ClientTimeout(total=self._timeout), ) - return self._session - async def close(self) -> None: if self._session is not None and not self._session.closed: await self._session.close() @@ -35,7 +44,7 @@ async def make_request( request: CentRequest[CentType], timeout: Optional[float] = None, ) -> CentType: - session = await self._create_session() + session = self._session session.headers["X-API-Key"] = api_key if isinstance(request, BatchRequest): diff --git a/cent/client/session/base_http.py b/cent/client/session/base_http.py index 737a971..79450cd 100644 --- a/cent/client/session/base_http.py +++ b/cent/client/session/base_http.py @@ -19,19 +19,7 @@ class BaseHttpSession: - """Base class for all sessions.""" - - def __init__( - self, - base_url: str, - timeout: float = 10.0, - ) -> None: - self._base_url = base_url - self._timeout = timeout - self._headers = { - "User-Agent": "centrifugal/pycent", - "Content-Type": "application/json", - } + """Base class for all HTTP sessions.""" @staticmethod def get_batch_json_data(request: BatchRequest) -> Dict[str, List[Dict[str, Any]]]: diff --git a/cent/client/session/requests.py b/cent/client/session/requests.py index 5f4ee8f..44975fc 100644 --- a/cent/client/session/requests.py +++ b/cent/client/session/requests.py @@ -1,4 +1,4 @@ -from typing import Optional, cast, Any +from typing import Optional, cast import requests from requests import Session @@ -10,10 +10,25 @@ class RequestsSession(BaseHttpSyncSession): - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self._session = Session() - self._session.headers.update(self._headers) + def __init__( + self, + base_url: str, + timeout: Optional[float] = 10.0, + session: Optional[Session] = None, + ) -> None: + super().__init__() + self._base_url = base_url + self._timeout = timeout + self._headers = { + "User-Agent": "centrifugal/pycent", + "Content-Type": "application/json", + } + self._session: Session + if session: + self._session = session + else: + self._session = Session() + self._session.headers.update(self._headers) def close(self) -> None: if self._session is not None: diff --git a/cent/client/sync_client.py b/cent/client/sync_client.py index 474ff1c..fbfc9e6 100644 --- a/cent/client/sync_client.py +++ b/cent/client/sync_client.py @@ -1,5 +1,7 @@ from typing import List, Optional, Any, Dict, TypeVar +from requests import Session + from cent.client.session import RequestsSession from cent.base import CentRequest from cent.requests import ( @@ -48,18 +50,21 @@ def __init__( api_url: str, api_key: str, request_timeout: Optional[float] = 10.0, + session: Optional[Session] = None, ) -> None: """ :param api_url: Centrifugo API URL :param api_key: Centrifugo API key :param request_timeout: Base timeout for all requests. + :param session: Custom `requests` session. """ self._api_url = api_url self._api_key = api_key self._session = RequestsSession( - base_url=api_url, + api_url, timeout=request_timeout, + session=session, ) def publish( diff --git a/cent/requests.py b/cent/requests.py index f548126..d98c05d 100644 --- a/cent/requests.py +++ b/cent/requests.py @@ -37,7 +37,11 @@ class BatchRequest(CentRequest[BatchResult]): - """Batch request.""" + """Batch request. + + Attributes: + commands: List of commands to execute in batch. + """ __returning__ = BatchResult __api_method__ = "batch" @@ -48,89 +52,104 @@ class BatchRequest(CentRequest[BatchResult]): class BroadcastRequest(CentRequest[BroadcastResult]): - """Broadcast request.""" + """Broadcast request. + + Attributes: + channels: List of channels to publish data to. + data: Custom data to publish into a channel. + skip_history: Skip adding publications to channels' history for this request. + tags: Publication tags - map with arbitrary string keys and values which is attached to + publication and will be delivered to clients. + b64data: Custom binary data to publish into a channel encoded to base64, so it's possible + to use HTTP API to send binary to clients. Centrifugo will decode it from base64 before + publishing. In case of GRPC you can publish binary using data field. + idempotency_key: Optional idempotency key to drop duplicate publications upon retries. It + acts per channel. Centrifugo currently keeps the cache of idempotent publish results + during 5 minutes window. Available since Centrifugo v5.2.0 + """ __returning__ = BroadcastResult __api_method__ = "broadcast" __grpc_method__ = GrpcBroadcastRequest channels: List[str] - """List of channels to publish data to.""" data: Any - """Custom JSON data to publish into a channel.""" skip_history: Optional[bool] = None - """Skip adding publications to channels' history for this request.""" tags: Optional[Dict[str, str]] = None - """Publication tags - map with arbitrary string keys and values which is attached to - publication and will be delivered to clients.""" b64data: Optional[str] = None - """Custom binary data to publish into a channel encoded to base64 so it's possible - to use HTTP API to send binary to clients. Centrifugo will decode it from base64 before - publishing. In case of GRPC you can publish binary using data field.""" idempotency_key: Optional[str] = None - """Optional idempotency key to drop duplicate publications upon retries. It acts per channel. - Centrifugo currently keeps the cache of idempotent publish results during 5 minutes window. - Available since Centrifugo v5.2.0""" class ChannelsRequest(CentRequest[ChannelsResult]): - """Channels request.""" + """Channels request. + + Attributes: + pattern: Pattern to filter channels, we are using https://github.com/gobwas/glob + library for matching. + """ __returning__ = ChannelsResult __api_method__ = "channels" __grpc_method__ = GrpcChannelsRequest pattern: Optional[str] = None - """Pattern to filter channels, we are using https://github.com/gobwas/glob - library for matching.""" class DisconnectRequest(CentRequest[DisconnectResult]): - """Disconnect request.""" + """Disconnect request. + + Attributes: + user: User ID to disconnect. + client: Specific client ID to disconnect (user still required to be set). + session: Specific client session to disconnect (user still required to be set). + whitelist: Array of client IDs to keep. + disconnect: Provide custom disconnect object. + """ __returning__ = DisconnectResult __api_method__ = "disconnect" __grpc_method__ = GrpcDisconnectRequest user: str - """User ID to disconnect.""" client: Optional[str] = None - """Specific client ID to disconnect (user still required to be set).""" session: Optional[str] = None - """Specific client session to disconnect (user still required to be set).""" whitelist: Optional[List[str]] = None - """Array of client IDs to keep.""" disconnect: Optional[Disconnect] = None - """Provide custom disconnect object, see below.""" class HistoryRequest(CentRequest[HistoryResult]): - """History request.""" + """History request. + + Attributes: + channel: Name of channel to call history from. + limit: Limit number of returned publications, if not set in request then only + current stream position information will present in result (without any publications). + since: Return publications after this position. + reverse: Iterate in reversed order (from latest to earliest). + """ __returning__ = HistoryResult __api_method__ = "history" __grpc_method__ = GrpcHistoryRequest channel: str - """Name of channel to call history from.""" limit: Optional[int] = None - """Limit number of returned publications, if not set in request then only current stream - position information will present in result (without any publications).""" since: Optional[StreamPosition] = None - """To return publications after this position.""" reverse: Optional[bool] = None - """Iterate in reversed order (from latest to earliest).""" class HistoryRemoveRequest(CentRequest[HistoryRemoveResult]): - """History remove request.""" + """History remove request. + + Attributes: + channel: Name of channel to remove history. + """ __returning__ = HistoryRemoveResult __api_method__ = "history_remove" __grpc_method__ = GrpcHistoryRemoveRequest channel: str - """Name of channel to remove history.""" class InfoRequest(CentRequest[InfoResult]): @@ -142,14 +161,17 @@ class InfoRequest(CentRequest[InfoResult]): class PresenceRequest(CentRequest[PresenceResult]): - """Presence request.""" + """Presence request. + + Attributes: + channel: Name of channel to call presence from. + """ __returning__ = PresenceResult __api_method__ = "presence" __grpc_method__ = GrpcPresenceRequest channel: str - """Name of channel to call presence from.""" class PresenceStatsRequest(CentRequest[PresenceStatsResult]): @@ -164,93 +186,106 @@ class PresenceStatsRequest(CentRequest[PresenceStatsResult]): class PublishRequest(CentRequest[PublishResult]): - """Publish request.""" + """Publish request. + + Attributes: + channel: Name of channel to publish. + data: Custom data to publish into a channel. + skip_history: Skip adding publication to history for this request. + tags: Publication tags - map with arbitrary string keys and values which is attached to + publication and will be delivered to clients. + b64data: Custom binary data to publish into a channel encoded to base64, so it's possible + to use HTTP API to send binary to clients. Centrifugo will decode it from base64 + before publishing. In case of GRPC you can publish binary using data field. + idempotency_key: Optional idempotency key to drop duplicate publications upon retries. + It acts per channel. Centrifugo currently keeps the cache of idempotent publish + results during 5 minutes window. Available since Centrifugo v5.2.0 + """ __returning__ = PublishResult __api_method__ = "publish" __grpc_method__ = GrpcPublishRequest channel: str - """Name of channel to publish.""" data: Any - """Custom JSON data to publish into a channel.""" skip_history: Optional[bool] = None - """Skip adding publication to history for this request.""" tags: Optional[Dict[str, str]] = None - """Publication tags - map with arbitrary string keys and values which is attached to - publication and will be delivered to clients.""" b64data: Optional[str] = None - """Custom binary data to publish into a channel encoded to base64 so it's possible to use - HTTP API to send binary to clients. Centrifugo will decode it from base64 before publishing. - In case of GRPC you can publish binary using data field.""" idempotency_key: Optional[str] = None - """Optional idempotency key to drop duplicate publications upon retries. It acts per channel. - Centrifugo currently keeps the cache of idempotent publish results during 5 minutes window. - Available since Centrifugo v5.2.0""" class RefreshRequest(CentRequest[RefreshResult]): - """Refresh request.""" + """Refresh request. + + Attributes: + user: User ID to refresh. + client: Client ID to refresh (user still required to be set). + session: Specific client session to refresh (user still required to be set). + expired: Mark connection as expired and close with Disconnect Expired reason. + expire_at: Unix time (in seconds) in the future when the connection will expire. + """ __returning__ = RefreshResult __api_method__ = "refresh" __grpc_method__ = GrpcRefreshRequest user: str - """User ID to refresh.""" client: Optional[str] = None - """Client ID to refresh (user still required to be set).""" session: Optional[str] = None - """Specific client session to refresh (user still required to be set).""" expired: Optional[bool] = None - """Mark connection as expired and close with Disconnect Expired reason.""" expire_at: Optional[int] = None - """Unix time (in seconds) in the future when the connection will expire.""" class SubscribeRequest(CentRequest[SubscribeResult]): - """Subscribe request.""" + """Subscribe request. + + Attributes: + user: User ID to subscribe. + channel: Name of channel to subscribe user to. + info: Attach custom data to subscription (will be used in presence and join/leave + messages). + b64info: info in base64 for binary mode (will be decoded by Centrifugo). + client: Specific client ID to subscribe (user still required to be set, will ignore other + user connections with different client IDs). + session: Specific client session to subscribe (user still required to be set). + data: Custom subscription data (will be sent to client in Subscribe push). + b64data: Same as data but in base64 format (will be decoded by Centrifugo). + recover_since: Stream position to recover from. + override: Allows dynamically override some channel options defined in Centrifugo + configuration (see below available fields). + """ __returning__ = SubscribeResult __api_method__ = "subscribe" __grpc_method__ = GrpcSubscribeRequest user: str - """User ID to subscribe.""" channel: str - """Name of channel to subscribe user to.""" info: Optional[Any] = None - """Attach custom data to subscription (will be used in presence and join/leave messages).""" b64info: Optional[str] = None - """info in base64 for binary mode (will be decoded by Centrifugo).""" client: Optional[str] = None - """Specific client ID to subscribe (user still required to be set, will ignore other user - connections with different client IDs).""" session: Optional[str] = None - """Specific client session to subscribe (user still required to be set).""" data: Optional[Any] = None - """Custom subscription data (will be sent to client in Subscribe push).""" b64data: Optional[str] = None - """Same as data but in base64 format (will be decoded by Centrifugo).""" recover_since: Optional[StreamPosition] = None - """Stream position to recover from.""" override: Optional[ChannelOptionsOverride] = None - """Allows dynamically override some channel options defined in Centrifugo - configuration (see below available fields).""" class UnsubscribeRequest(CentRequest[UnsubscribeResult]): - """Unsubscribe request.""" + """Unsubscribe request. + + Attributes: + user: User ID to unsubscribe. + channel: Name of channel to unsubscribe user to. + client: Specific client ID to unsubscribe (user still required to be set). + session: Specific client session to disconnect (user still required to be set). + """ __returning__ = UnsubscribeResult __api_method__ = "unsubscribe" __grpc_method__ = GrpcUnsubscribeRequest user: str - """User ID to unsubscribe.""" channel: str - """Name of channel to unsubscribe user to.""" client: Optional[str] = None - """Specific client ID to unsubscribe (user still required to be set).""" session: Optional[str] = None - """Specific client session to disconnect (user still required to be set).""" From 64e39eec987a706a0fde678d09087bdcecf160e0 Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Mon, 12 Feb 2024 22:49:39 +0200 Subject: [PATCH 26/55] use as context managers --- cent/client/async_client.py | 6 ++++++ cent/client/grpc_client.py | 8 +++++++- cent/client/session/aiohttp.py | 3 ++- cent/client/session/base_http_async.py | 8 +------- cent/client/session/requests.py | 3 ++- cent/client/sync_client.py | 6 ++++++ 6 files changed, 24 insertions(+), 10 deletions(-) diff --git a/cent/client/async_client.py b/cent/client/async_client.py index b97fcb6..a3c072b 100644 --- a/cent/client/async_client.py +++ b/cent/client/async_client.py @@ -269,3 +269,9 @@ async def __call__( :return: Centrifugo response """ return await self._session(self._api_key, request, timeout=request_timeout) + + async def __aenter__(self) -> "AsyncClient": + return self + + async def __aexit__(self, *kwargs: Any) -> None: + await self.close() diff --git a/cent/client/grpc_client.py b/cent/client/grpc_client.py index 5c00a87..b63c041 100644 --- a/cent/client/grpc_client.py +++ b/cent/client/grpc_client.py @@ -1,4 +1,4 @@ -from typing import Optional, Dict, TypeVar, List +from typing import Optional, Dict, TypeVar, List, Any from cent.client.session import GrpcSession from cent.base import CentRequest @@ -244,3 +244,9 @@ async def __call__( :return: Centrifugo response """ return await self._session(request, request_timeout) + + async def __aenter__(self) -> "GrpcClient": + return self + + async def __aexit__(self, *kwargs: Any) -> None: + await self.close() diff --git a/cent/client/session/aiohttp.py b/cent/client/session/aiohttp.py index 90c6f80..6d96976 100644 --- a/cent/client/session/aiohttp.py +++ b/cent/client/session/aiohttp.py @@ -45,7 +45,8 @@ async def make_request( timeout: Optional[float] = None, ) -> CentType: session = self._session - session.headers["X-API-Key"] = api_key + if api_key: + session.headers["X-API-Key"] = api_key if isinstance(request, BatchRequest): json_data = self.get_batch_json_data(request) diff --git a/cent/client/session/base_http_async.py b/cent/client/session/base_http_async.py index 4afcdfd..4f93185 100644 --- a/cent/client/session/base_http_async.py +++ b/cent/client/session/base_http_async.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Any, Optional, cast +from typing import Optional, cast from cent.client.session.base_http import BaseHttpSession from cent.base import CentType, CentRequest @@ -36,9 +36,3 @@ async def __call__( timeout: Optional[float] = None, ) -> CentType: return cast(CentType, await self.make_request(api_key, request, timeout)) - - async def __aenter__(self) -> "BaseHttpAsyncSession": - return self - - async def __aexit__(self, *kwargs: Any) -> None: - await self.close() diff --git a/cent/client/session/requests.py b/cent/client/session/requests.py index 44975fc..f4fd790 100644 --- a/cent/client/session/requests.py +++ b/cent/client/session/requests.py @@ -40,7 +40,8 @@ def make_request( request: CentRequest[CentType], timeout: Optional[float] = None, ) -> CentType: - self._session.headers["X-API-Key"] = api_key + if api_key: + self._session.headers["X-API-Key"] = api_key if isinstance(request, BatchRequest): json_data = self.get_batch_json_data(request) else: diff --git a/cent/client/sync_client.py b/cent/client/sync_client.py index fbfc9e6..96cf1fb 100644 --- a/cent/client/sync_client.py +++ b/cent/client/sync_client.py @@ -269,3 +269,9 @@ def __call__(self, request: CentRequest[T], request_timeout: Optional[float] = N :return: Centrifugo response """ return self._session(self._api_key, request, timeout=request_timeout) + + def __enter__(self) -> "Client": + return self + + def __exit__(self, *kwargs: Any) -> None: + self.close() From 53e2670b09ccc111355c83d160ddaed9450e2e08 Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Tue, 13 Feb 2024 21:55:49 +0200 Subject: [PATCH 27/55] drop grpc support --- README.md | 127 +- benchmarks/conftest.py | 82 +- benchmarks/test_publish.py | 4 +- cent/__init__.py | 12 - cent/base.py | 38 +- cent/client/__init__.py | 2 - cent/client/async_client.py | 3 +- cent/client/grpc_client.py | 252 -- cent/client/session/__init__.py | 2 - cent/client/session/aiohttp.py | 5 +- cent/client/session/base_http.py | 8 +- cent/client/session/base_http_async.py | 6 +- cent/client/session/base_http_sync.py | 4 +- cent/client/session/grpc.py | 100 - cent/client/session/requests.py | 18 +- cent/client/sync_client.py | 3 +- cent/proto/__init__.py | 0 cent/proto/apiproto.proto | 879 ------- cent/proto/centrifugal/__init__.py | 0 cent/proto/centrifugal/centrifugo/__init__.py | 0 .../centrifugal/centrifugo/api/__init__.py | 2225 ----------------- cent/requests.py | 38 +- cent/results.py | 102 +- cent/types.py | 26 +- poetry.lock | 727 +----- pyproject.toml | 14 +- tests/conftest.py | 11 +- tests/test_async_validation.py | 10 +- tests/test_grpc_validation.py | 135 - tests/test_sync_validation.py | 10 +- 30 files changed, 306 insertions(+), 4537 deletions(-) delete mode 100644 cent/client/grpc_client.py delete mode 100644 cent/client/session/grpc.py delete mode 100644 cent/proto/__init__.py delete mode 100644 cent/proto/apiproto.proto delete mode 100644 cent/proto/centrifugal/__init__.py delete mode 100644 cent/proto/centrifugal/centrifugo/__init__.py delete mode 100644 cent/proto/centrifugal/centrifugo/api/__init__.py delete mode 100644 tests/test_grpc_validation.py diff --git a/README.md b/README.md index f76b7a7..0edc063 100644 --- a/README.md +++ b/README.md @@ -10,123 +10,78 @@ pip install cent **Cent v5 and higher works only with Centrifugo v5**. -* If you need to work with Centrifugo v3, v4 then use Cent v4 -* If you need to work with Centrifugo v2 then use Cent v3 +* If you need to work with Centrifugo v3, v4 => use Cent v4 +* If you need to work with Centrifugo v2 => use Cent v3 ## Usage -See the description of Centrifugo [server API](https://centrifugal.dev/docs/server/server_api) in documentation. +See the description of Centrifugo [server API](https://centrifugal.dev/docs/server/server_api) in the documentation. -This library contains `Client`, `AsyncClient` and `GrpcClient` classes to work with Centrifugo HTTP and GRPC server API. +This library contains `Client` and `AsyncClient` to work with Centrifugo HTTP server API. Both clients have the same methods to work with Centrifugo API and raise the same top-level exceptions. -```python -import asyncio -from cent import Client, AsyncClient - -api_url = "http://localhost:8000/api" -api_key = "" - -# Initialize a client (you can use sync or async version) -sync_client = Client(api_url, api_key) -async_client = AsyncClient(api_url, api_key) - -# Now you can use sync client to call API methods. -result = sync_client.publish("channel", {"input": "Hello world!"}) -print(result) - - -async def main(): - # And async client to call API methods too. - result = await async_client.publish("channel", {"input": "Hello world!"}) - print(result) - - -if __name__ == "__main__": - asyncio.run(main()) -``` - -For GRPC the usage is slightly different: - -```python -import asyncio -import json -from cent import GrpcClient - -host = "localhost" -port = 10000 - -grpc_client = GrpcClient(host, port) - -async def main(): - result = await grpc_client.publish( - "example:channel", json.dumps({"input": "Hello world!"}).encode()) - print(result) - - -if __name__ == "__main__": - asyncio.run(main()) -``` - -Note that in GRPC case you must pass payload as `bytes`, see below more details about payloads in HTTP vs GRPC cases. - -## Sync HTTP client init arguments +## Sync HTTP client ```python from cent import Client ``` -Required: +Required init arguments: * `api_url` (str) - Centrifugo HTTP API URL address * `api_key` (str) - Centrifugo HTTP API key -Optional: +Optional arguments: * `request_timeout` (float) - base timeout for all requests in seconds, default is 10 seconds. * `session` (requests.Session) - custom `requests` session to use. -## Async HTTP client init arguments +Example: + +```python +from cent import Client + +api_url = "http://localhost:8000/api" +api_key = "" + +client = Client(api_url, api_key) +result = client.publish("channel", {"input": "Hello world!"}) +print(result) +``` + +## Async HTTP client ```python from cent import AsyncClient ``` -Required: +Required init arguments: * `api_url` (str) - Centrifugo HTTP API URL address * `api_key` (str) - Centrifugo HTTP API key -Optional: +Optional arguments: * `request_timeout` (float) - base timeout for all requests in seconds, default is 10 seconds. * `session` (aiohttp.ClientSession) - custom `aiohttp` session to use. -## GRPC client init arguments +Example: ```python -from cent import GrpcClient -``` - -Required: - -* `host` (str) - Centrifugo GRPC API host -* `port` (int) - Centrifugo GRPC API port - -Optional: - -* `request_timeout` (float) - base timeout for all requests in seconds, default is 10 seconds. - -## Payloads in HTTP vs GRPC cases +import asyncio +from cent import AsyncClient -When using HTTP-based clients (`Client` and `AsyncClient`): +api_url = "http://localhost:8000/api" +api_key = "" -* you should pass payload as a Python objects which can be serialized to JSON -* in results, you will receive Python objects already deserialized from JSON. +client = AsyncClient(api_url, api_key) -When using GRPC-based client (`GrpcClient`): +async def main(): + result = await client.publish("channel", {"input": "Hello world!"}) + print(result) -* you must pass payloads as `bytes` -* in results, you will receive `bytes` for payloads +if __name__ == "__main__": + asyncio.run(main()) +``` ## Handling errors @@ -138,7 +93,7 @@ This library raises exceptions if sth goes wrong. All exceptions are subclasses * `CentTimeoutError` - raised in case of timeout * `CentUnauthorizedError` - raised in case of unauthorized access * `CentDecodeError` - raised in case of server response decoding error -* `CentAPIError` - raised in case of API error (error returned by Centrifugo itself) +* `CentAPIError` - raised in case of API error (error returned by Centrifugo itself, you can inspect code and message in this case) ## For contributors @@ -157,20 +112,14 @@ And install dependencies: make dev ``` -To start tests, run: +Then to run tests, run: ```bash make test ``` -To start benchmarks, run: +To run benchmarks, run: ```bash make bench ``` - -### Generate code from proto file, if needed - -```bash -make proto -``` diff --git a/benchmarks/conftest.py b/benchmarks/conftest.py index d2db6c9..6a149f1 100644 --- a/benchmarks/conftest.py +++ b/benchmarks/conftest.py @@ -1,28 +1,17 @@ import asyncio -import threading -import contextlib +import pytest_asyncio from typing import ( - Any, AsyncGenerator, - Tuple, - Dict, Callable, Awaitable, Optional, - cast, - TYPE_CHECKING, ) import pytest -from pytest_benchmark.fixture import BenchmarkFixture from cent import Client, AsyncClient -if TYPE_CHECKING: - from asyncio import AbstractEventLoop - from threading import Thread - -BASE_URL = "http://localhost:8000/api" +API_URL = "http://localhost:8000/api" API_KEY = "api_key" BenchmarkCoroType = Callable[[], Awaitable[None]] @@ -30,60 +19,31 @@ BenchmarkDecoratorType = Callable[[BenchmarkType], None] -@pytest.fixture(scope="session") -def anyio_backend() -> Tuple[str, Dict[str, bool]]: - return "asyncio", {"use_uvloop": True} - - @pytest.fixture() -def aio_benchmark(benchmark: BenchmarkFixture) -> BenchmarkDecoratorType: - class Sync2Async: - def __init__(self, coro: BenchmarkCoroType) -> None: - self.coro = coro - self.custom_loop: Optional["AbstractEventLoop"] = None - self.thread: Optional["Thread"] = None +def sync_client() -> Client: + return Client(API_URL, API_KEY) - def start_background_loop(self) -> None: - if self.custom_loop: - asyncio.set_event_loop(self.custom_loop) - self.custom_loop.run_forever() - def __call__(self) -> Any: - awaitable = self.coro() - with contextlib.suppress(RuntimeError): - evloop = asyncio.get_running_loop() - if evloop is None: - return asyncio.run(awaitable) - else: - if not self.custom_loop or not self.thread or not self.thread.is_alive(): - self.custom_loop = asyncio.new_event_loop() - self.thread = threading.Thread( - target=self.start_background_loop, - daemon=True, - ) - self.thread.start() +@pytest_asyncio.fixture() +async def async_client( + # anyio_backend: Any, +) -> AsyncGenerator[AsyncClient, None]: + client = AsyncClient(API_URL, API_KEY) + yield client + await client.close() - return asyncio.run_coroutine_threadsafe(awaitable, self.custom_loop).result() - def _wrapper(func: BenchmarkType) -> None: +# async support for pytest-benchmark +# https://github.com/ionelmc/pytest-benchmark/issues/66 +@pytest_asyncio.fixture +def aio_benchmark(benchmark, event_loop): # type: ignore + def _wrapper(func, *args, **kwargs): # type: ignore if asyncio.iscoroutinefunction(func): - func = cast(BenchmarkCoroType, func) - benchmark(Sync2Async(func)) + + @benchmark + def _(): # type: ignore + return event_loop.run_until_complete(func(*args, **kwargs)) else: - benchmark(func) + benchmark(func, *args, **kwargs) return _wrapper - - -@pytest.fixture() -def sync_client() -> Client: - return Client(BASE_URL, API_KEY) - - -@pytest.fixture() -async def async_client( - anyio_backend: Any, # noqa: ARG001 -) -> AsyncGenerator[AsyncClient, None]: - client = AsyncClient(BASE_URL, API_KEY) - yield client - await client.close() diff --git a/benchmarks/test_publish.py b/benchmarks/test_publish.py index c26f792..06ef867 100644 --- a/benchmarks/test_publish.py +++ b/benchmarks/test_publish.py @@ -1,5 +1,4 @@ import random -import pytest from benchmarks.conftest import BenchmarkDecoratorType from cent import AsyncClient, Client @@ -30,8 +29,7 @@ def _() -> None: sync_requests(sync_client) -@pytest.mark.anyio() -async def test_async(aio_benchmark: BenchmarkDecoratorType, async_client: AsyncClient) -> None: +def test_async(aio_benchmark: BenchmarkDecoratorType, async_client: AsyncClient) -> None: @aio_benchmark async def _() -> None: await async_requests(async_client) diff --git a/cent/__init__.py b/cent/__init__.py index b908632..8828aec 100644 --- a/cent/__init__.py +++ b/cent/__init__.py @@ -1,12 +1,7 @@ -import contextlib -import asyncio as _asyncio - from .client import ( Client, AsyncClient, - GrpcClient, ) -from cent.base import CentRequest from cent.requests import ( BroadcastRequest, PublishRequest, @@ -56,11 +51,6 @@ CentAPIError, ) -with contextlib.suppress(ImportError): - import uvloop as _uvloop - - _asyncio.set_event_loop_policy(_uvloop.EventLoopPolicy()) - __all__ = ( "AsyncClient", "BatchRequest", @@ -72,7 +62,6 @@ "CentDecodeError", "CentError", "CentNetworkError", - "CentRequest", "CentTransportError", "CentUnauthorizedError", "ChannelOptionsOverride", @@ -83,7 +72,6 @@ "Disconnect", "DisconnectRequest", "DisconnectResult", - "GrpcClient", "HistoryRemoveRequest", "HistoryRemoveResult", "HistoryRequest", diff --git a/cent/base.py b/cent/base.py index e690ee4..75d488d 100644 --- a/cent/base.py +++ b/cent/base.py @@ -4,18 +4,6 @@ from pydantic import BaseModel, ConfigDict -class BaseResult(BaseModel, ABC): - model_config = ConfigDict( - use_enum_values=True, - extra="allow", - validate_assignment=True, - frozen=True, - populate_by_name=True, - arbitrary_types_allowed=True, - defer_build=True, - ) - - CentType = TypeVar("CentType", bound=Any) @@ -29,6 +17,18 @@ class Response(BaseModel, Generic[CentType]): result: Optional[CentType] = None +class CentResult(BaseModel, ABC): + model_config = ConfigDict( + use_enum_values=True, + extra="allow", + validate_assignment=True, + frozen=True, + populate_by_name=True, + arbitrary_types_allowed=True, + defer_build=True, + ) + + class CentRequest(BaseModel, Generic[CentType], ABC): model_config = ConfigDict( extra="allow", @@ -39,7 +39,6 @@ class CentRequest(BaseModel, Generic[CentType], ABC): if TYPE_CHECKING: __returning__: ClassVar[type] __api_method__: ClassVar[str] - __grpc_method__: ClassVar[type] else: @property @@ -52,11 +51,6 @@ def __returning__(self) -> type: def __api_method__(self) -> str: pass - @property - @abstractmethod - def __grpc_method__(self) -> type: - pass - class NestedModel(BaseModel, ABC): model_config = ConfigDict( @@ -64,11 +58,3 @@ class NestedModel(BaseModel, ABC): populate_by_name=True, arbitrary_types_allowed=True, ) - if TYPE_CHECKING: - __grpc_method__: ClassVar[type] - else: - - @property - @abstractmethod - def __grpc_method__(self) -> type: - pass diff --git a/cent/client/__init__.py b/cent/client/__init__.py index 961ceb4..dee881a 100644 --- a/cent/client/__init__.py +++ b/cent/client/__init__.py @@ -1,9 +1,7 @@ from .sync_client import Client from .async_client import AsyncClient -from .grpc_client import GrpcClient __all__ = ( "AsyncClient", "Client", - "GrpcClient", ) diff --git a/cent/client/async_client.py b/cent/client/async_client.py index a3c072b..c0c1718 100644 --- a/cent/client/async_client.py +++ b/cent/client/async_client.py @@ -251,9 +251,10 @@ async def info( async def batch( self, commands: List[CentRequest[Any]], + parallel: Optional[bool] = None, request_timeout: Optional[float] = None, ) -> BatchResult: - call = BatchRequest.model_construct(commands=commands) + call = BatchRequest.model_construct(commands=commands, parallel=parallel) return await self(call, request_timeout=request_timeout) async def close(self) -> None: diff --git a/cent/client/grpc_client.py b/cent/client/grpc_client.py deleted file mode 100644 index b63c041..0000000 --- a/cent/client/grpc_client.py +++ /dev/null @@ -1,252 +0,0 @@ -from typing import Optional, Dict, TypeVar, List, Any - -from cent.client.session import GrpcSession -from cent.base import CentRequest -from cent.requests import ( - BroadcastRequest, - PublishRequest, - SubscribeRequest, - UnsubscribeRequest, - PresenceRequest, - PresenceStatsRequest, - HistoryRequest, - HistoryRemoveRequest, - RefreshRequest, - ChannelsRequest, - DisconnectRequest, - InfoRequest, -) -from cent.results import ( - PublishResult, - BroadcastResult, - SubscribeResult, - UnsubscribeResult, - PresenceResult, - PresenceStatsResult, - HistoryResult, - HistoryRemoveResult, - RefreshResult, - ChannelsResult, - DisconnectResult, - InfoResult, -) -from cent.types import ( - StreamPosition, - ChannelOptionsOverride, - Disconnect, -) - - -T = TypeVar("T") - - -class GrpcClient: - def __init__( - self, - host: str, - port: int, - request_timeout: Optional[float] = 10.0, - ) -> None: - """ - Initialize GRPC client. - - :param host: Centrifugo host. - :param port: Centrifugo port. - :param request_timeout: Request timeout. - """ - self._session = GrpcSession(host, port, timeout=request_timeout) - - async def publish( - self, - channel: str, - data: bytes, - skip_history: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - idempotency_key: Optional[str] = None, - request_timeout: Optional[float] = None, - ) -> PublishResult: - call = PublishRequest( - channel=channel, - data=data, - skip_history=skip_history, - tags=tags, - idempotency_key=idempotency_key, - ) - return await self(call, request_timeout=request_timeout) - - async def broadcast( - self, - channels: List[str], - data: bytes, - skip_history: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - idempotency_key: Optional[str] = None, - request_timeout: Optional[float] = None, - ) -> BroadcastResult: - call = BroadcastRequest( - channels=channels, - data=data, - skip_history=skip_history, - tags=tags, - idempotency_key=idempotency_key, - ) - return await self(call, request_timeout=request_timeout) - - async def subscribe( - self, - user: str, - channel: str, - info: Optional[bytes] = None, - client: Optional[str] = None, - session: Optional[str] = None, - data: Optional[bytes] = None, - recover_since: Optional[StreamPosition] = None, - override: Optional[ChannelOptionsOverride] = None, - request_timeout: Optional[float] = None, - ) -> SubscribeResult: - call = SubscribeRequest( - user=user, - channel=channel, - info=info, - client=client, - session=session, - data=data, - recover_since=recover_since, - override=override, - ) - return await self(call, request_timeout=request_timeout) - - async def unsubscribe( - self, - user: str, - channel: str, - client: Optional[str] = None, - session: Optional[str] = None, - request_timeout: Optional[float] = None, - ) -> UnsubscribeResult: - call = UnsubscribeRequest( - user=user, - channel=channel, - client=client, - session=session, - ) - return await self(call, request_timeout=request_timeout) - - async def presence( - self, - channel: str, - request_timeout: Optional[float] = None, - ) -> PresenceResult: - call = PresenceRequest( - channel=channel, - ) - return await self(call, request_timeout=request_timeout) - - async def presence_stats( - self, - channel: str, - request_timeout: Optional[float] = None, - ) -> PresenceStatsResult: - call = PresenceStatsRequest( - channel=channel, - ) - return await self(call, request_timeout=request_timeout) - - async def history( - self, - channel: str, - limit: Optional[int] = None, - since: Optional[StreamPosition] = None, - reverse: Optional[bool] = None, - request_timeout: Optional[float] = None, - ) -> HistoryResult: - call = HistoryRequest( - channel=channel, - limit=limit, - since=since, - reverse=reverse, - ) - return await self(call, request_timeout=request_timeout) - - async def history_remove( - self, - channel: str, - request_timeout: Optional[float] = None, - ) -> HistoryRemoveResult: - call = HistoryRemoveRequest( - channel=channel, - ) - return await self(call, request_timeout=request_timeout) - - async def refresh( - self, - user: str, - client: Optional[str] = None, - session: Optional[str] = None, - expire_at: Optional[int] = None, - expired: Optional[bool] = None, - request_timeout: Optional[float] = None, - ) -> RefreshResult: - call = RefreshRequest( - user=user, - client=client, - session=session, - expire_at=expire_at, - expired=expired, - ) - return await self(call, request_timeout=request_timeout) - - async def channels( - self, - pattern: Optional[str] = None, - request_timeout: Optional[float] = None, - ) -> ChannelsResult: - call = ChannelsRequest( - pattern=pattern, - ) - return await self(call, request_timeout=request_timeout) - - async def disconnect( - self, - user: str, - client: Optional[str] = None, - session: Optional[str] = None, - whitelist: Optional[List[str]] = None, - disconnect: Optional[Disconnect] = None, - request_timeout: Optional[float] = None, - ) -> DisconnectResult: - call = DisconnectRequest( - user=user, - client=client, - session=session, - whitelist=whitelist, - disconnect=disconnect, - ) - return await self(call, request_timeout=request_timeout) - - async def info( - self, - request_timeout: Optional[float] = None, - ) -> InfoResult: - call = InfoRequest() - return await self(call, request_timeout=request_timeout) - - async def close(self) -> None: - self._session.close() - - async def __call__( - self, request: CentRequest[T], request_timeout: Optional[float] = None - ) -> T: - """ - Call API method - - :param request: Centrifugo request - :return: Centrifugo response - """ - return await self._session(request, request_timeout) - - async def __aenter__(self) -> "GrpcClient": - return self - - async def __aexit__(self, *kwargs: Any) -> None: - await self.close() diff --git a/cent/client/session/__init__.py b/cent/client/session/__init__.py index 08f2771..b050430 100644 --- a/cent/client/session/__init__.py +++ b/cent/client/session/__init__.py @@ -1,9 +1,7 @@ from .aiohttp import AiohttpSession from .requests import RequestsSession -from .grpc import GrpcSession __all__ = ( "AiohttpSession", - "GrpcSession", "RequestsSession", ) diff --git a/cent/client/session/aiohttp.py b/cent/client/session/aiohttp.py index 6d96976..792acf4 100644 --- a/cent/client/session/aiohttp.py +++ b/cent/client/session/aiohttp.py @@ -1,5 +1,5 @@ import asyncio -from typing import Optional, cast +from typing import Optional from aiohttp import ClientSession, ClientError, ClientTimeout @@ -72,12 +72,11 @@ async def make_request( request=request, message=f"{type(error).__name__}: {error}", ) from error - response = self.check_response( + return self.check_response( request=request, status_code=resp.status, content=raw_result, ) - return cast(CentType, response.result) def __del__(self) -> None: if self._session and not self._session.closed: diff --git a/cent/client/session/base_http.py b/cent/client/session/base_http.py index 79450cd..f3e58e1 100644 --- a/cent/client/session/base_http.py +++ b/cent/client/session/base_http.py @@ -1,6 +1,6 @@ import json from http import HTTPStatus -from typing import Any, Dict, List +from typing import Any, Dict, List, cast from pydantic import ValidationError, TypeAdapter @@ -19,7 +19,7 @@ class BaseHttpSession: - """Base class for all HTTP sessions.""" + """Base class for HTTP sessions.""" @staticmethod def get_batch_json_data(request: BatchRequest) -> Dict[str, List[Dict[str, Any]]]: @@ -49,7 +49,7 @@ def check_response( request: CentRequest[CentType], status_code: int, content: str, - ) -> Response[CentType]: + ) -> CentType: if status_code == HTTPStatus.UNAUTHORIZED: raise CentUnauthorizedError @@ -82,4 +82,4 @@ def check_response( message=response.error.message, ) - return response + return cast(CentType, response.result) diff --git a/cent/client/session/base_http_async.py b/cent/client/session/base_http_async.py index 4f93185..56d0ce8 100644 --- a/cent/client/session/base_http_async.py +++ b/cent/client/session/base_http_async.py @@ -1,13 +1,11 @@ from abc import ABC, abstractmethod -from typing import Optional, cast +from typing import Optional from cent.client.session.base_http import BaseHttpSession from cent.base import CentType, CentRequest class BaseHttpAsyncSession(BaseHttpSession, ABC): - """Base class for all sessions.""" - @abstractmethod async def close(self) -> None: """ @@ -35,4 +33,4 @@ async def __call__( request: CentRequest[CentType], timeout: Optional[float] = None, ) -> CentType: - return cast(CentType, await self.make_request(api_key, request, timeout)) + return await self.make_request(api_key, request, timeout) diff --git a/cent/client/session/base_http_sync.py b/cent/client/session/base_http_sync.py index 54542ab..1d535af 100644 --- a/cent/client/session/base_http_sync.py +++ b/cent/client/session/base_http_sync.py @@ -1,4 +1,4 @@ -from abc import abstractmethod, ABC +from abc import ABC, abstractmethod from typing import Optional from cent.client.session.base_http import BaseHttpSession @@ -6,8 +6,6 @@ class BaseHttpSyncSession(BaseHttpSession, ABC): - """Base class for all sessions.""" - @abstractmethod def close(self) -> None: """ diff --git a/cent/client/session/grpc.py b/cent/client/session/grpc.py deleted file mode 100644 index 915c143..0000000 --- a/cent/client/session/grpc.py +++ /dev/null @@ -1,100 +0,0 @@ -from dataclasses import dataclass, asdict -from typing import cast, Type, Dict, Any, List, Tuple, Optional - -import betterproto -from grpclib import GRPCError -from grpclib.client import Channel -from pydantic import TypeAdapter, BaseModel - -from cent.proto.centrifugal.centrifugo.api import CentrifugoApiStub -from cent.exceptions import CentAPIError, CentTransportError, CentTimeoutError, CentNetworkError -from cent.base import CentType, Response, Error, CentRequest - - -@dataclass -class _BaseResponse(betterproto.Message): - error: Error - result: Type[betterproto.Message] - - -def _dict_factory(x: List[Tuple[str, Any]]) -> Dict[str, Any]: - response = {} - for k, v in x: - if v: - response[k] = v - return response - - -class GrpcSession: - def __init__(self, host: str, port: int, timeout: Optional[float] = 10.0) -> None: - self._channel = Channel(host=host, port=port) - self._stub = CentrifugoApiStub(channel=self._channel) - self._timeout = timeout - - def close(self) -> None: - self._channel.close() - - @staticmethod - def check_response( - request: CentRequest[CentType], - content: _BaseResponse, - ) -> Response[CentType]: - """Validate response.""" - response_type = Response[request.__returning__] # type: ignore - response = TypeAdapter(response_type).validate_python( - asdict(content, dict_factory=_dict_factory) - ) - if response.error: - raise CentAPIError( - request=request, - code=response.error.code, - message=response.error.message, - ) - return response - - def convert_to_grpc(self, request: CentRequest[CentType]) -> Any: - request_dump = request.model_dump(by_alias=True, exclude_none=True, mode="grpc") - for key, value in request.model_fields.items(): - attr = getattr(request, key) - if issubclass(attr.__class__, BaseModel): - request_dump[value.alias or key] = self.convert_to_grpc(attr) - return request.__grpc_method__(**request_dump) - - async def make_request( - self, - request: CentRequest[CentType], - timeout: Optional[float] = None, - ) -> CentType: - api_method = getattr(self._stub, request.__api_method__) - try: - response = await api_method( - self.convert_to_grpc(request), timeout=timeout or self._timeout - ) - except TimeoutError as error: - raise CentTimeoutError( - request=request, - message="Request timeout", - ) from error - except GRPCError as error: - raise CentTransportError( - request=request, - status_code=error.status.value, - ) from error - except Exception as error: - raise CentNetworkError( - request=request, - message=f"{type(error).__name__}: {error}", - ) from error - - resp = self.check_response(request, response) - return cast(CentType, resp.result) - - async def __call__( - self, - request: CentRequest[CentType], - timeout: Optional[float] = None, - ) -> CentType: - return cast(CentType, await self.make_request(request, timeout)) - - def __del__(self) -> None: - self.close() diff --git a/cent/client/session/requests.py b/cent/client/session/requests.py index f4fd790..d5cc03e 100644 --- a/cent/client/session/requests.py +++ b/cent/client/session/requests.py @@ -1,12 +1,12 @@ -from typing import Optional, cast +from typing import Optional import requests from requests import Session -from cent.base import CentType, CentRequest from cent.client.session.base_http_sync import BaseHttpSyncSession -from cent.exceptions import CentNetworkError, CentTimeoutError +from cent.base import CentType, CentRequest from cent.requests import BatchRequest +from cent.exceptions import CentNetworkError, CentTimeoutError class RequestsSession(BaseHttpSyncSession): @@ -19,16 +19,15 @@ def __init__( super().__init__() self._base_url = base_url self._timeout = timeout - self._headers = { - "User-Agent": "centrifugal/pycent", - "Content-Type": "application/json", - } self._session: Session if session: self._session = session else: self._session = Session() - self._session.headers.update(self._headers) + self._session.headers.update({ + "User-Agent": "centrifugal/pycent", + "Content-Type": "application/json", + }) def close(self) -> None: if self._session is not None: @@ -65,12 +64,11 @@ def make_request( request=request, message=f"{type(error).__name__}: {error}", ) from error - response = self.check_response( + return self.check_response( request=request, status_code=raw_result.status_code, content=raw_result.text, ) - return cast(CentType, response.result) def __del__(self) -> None: self.close() diff --git a/cent/client/sync_client.py b/cent/client/sync_client.py index 96cf1fb..50c1ac4 100644 --- a/cent/client/sync_client.py +++ b/cent/client/sync_client.py @@ -253,9 +253,10 @@ def info( def batch( self, commands: List[CentRequest[Any]], + parallel: Optional[bool] = False, request_timeout: Optional[float] = None, ) -> BatchResult: - call = BatchRequest.model_construct(commands=commands) + call = BatchRequest.model_construct(commands=commands, parallel=parallel) return self(call, request_timeout=request_timeout) def close(self) -> None: diff --git a/cent/proto/__init__.py b/cent/proto/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/cent/proto/apiproto.proto b/cent/proto/apiproto.proto deleted file mode 100644 index 4fd60ca..0000000 --- a/cent/proto/apiproto.proto +++ /dev/null @@ -1,879 +0,0 @@ -syntax = "proto3"; - -package centrifugal.centrifugo.api; - -option go_package = "./;apiproto"; - -service CentrifugoApi { - rpc Batch (BatchRequest) returns (BatchResponse) {} - rpc Publish (PublishRequest) returns (PublishResponse) {} - rpc Broadcast (BroadcastRequest) returns (BroadcastResponse) {} - rpc Subscribe (SubscribeRequest) returns (SubscribeResponse) {} - rpc Unsubscribe (UnsubscribeRequest) returns (UnsubscribeResponse) {} - rpc Disconnect (DisconnectRequest) returns (DisconnectResponse) {} - rpc Presence (PresenceRequest) returns (PresenceResponse) {} - rpc PresenceStats (PresenceStatsRequest) returns (PresenceStatsResponse) {} - rpc History (HistoryRequest) returns (HistoryResponse) {} - rpc HistoryRemove (HistoryRemoveRequest) returns (HistoryRemoveResponse) {} - rpc Info (InfoRequest) returns (InfoResponse) {} - rpc RPC (RPCRequest) returns (RPCResponse) {} - rpc Refresh (RefreshRequest) returns (RefreshResponse) {} - rpc Channels (ChannelsRequest) returns (ChannelsResponse) {} - rpc Connections (ConnectionsRequest) returns (ConnectionsResponse) {} - rpc UpdateUserStatus (UpdateUserStatusRequest) returns (UpdateUserStatusResponse) {} - rpc GetUserStatus (GetUserStatusRequest) returns (GetUserStatusResponse) {} - rpc DeleteUserStatus (DeleteUserStatusRequest) returns (DeleteUserStatusResponse) {} - rpc BlockUser (BlockUserRequest) returns (BlockUserResponse) {} - rpc UnblockUser (UnblockUserRequest) returns (UnblockUserResponse) {} - rpc RevokeToken (RevokeTokenRequest) returns (RevokeTokenResponse) {} - rpc InvalidateUserTokens (InvalidateUserTokensRequest) returns (InvalidateUserTokensResponse) {} - rpc DeviceRegister (DeviceRegisterRequest) returns (DeviceRegisterResponse) {} - rpc DeviceUpdate (DeviceUpdateRequest) returns (DeviceUpdateResponse) {} - rpc DeviceRemove (DeviceRemoveRequest) returns (DeviceRemoveResponse) {} - rpc DeviceList (DeviceListRequest) returns (DeviceListResponse) {} - rpc DeviceTopicList (DeviceTopicListRequest) returns (DeviceTopicListResponse) {} - rpc DeviceTopicUpdate (DeviceTopicUpdateRequest) returns (DeviceTopicUpdateResponse) {} - rpc UserTopicList (UserTopicListRequest) returns (UserTopicListResponse) {} - rpc UserTopicUpdate (UserTopicUpdateRequest) returns (UserTopicUpdateResponse) {} - rpc SendPushNotification (SendPushNotificationRequest) returns (SendPushNotificationResponse) {} - rpc UpdatePushStatus (UpdatePushStatusRequest) returns (UpdatePushStatusResponse) {} - rpc CancelPush (CancelPushRequest) returns (CancelPushResponse) {} - rpc RateLimit (RateLimitRequest) returns (RateLimitResponse) {} -} - -message Command { - enum MethodType { - PUBLISH = 0; - BROADCAST = 1; - UNSUBSCRIBE = 2; - DISCONNECT = 3; - PRESENCE = 4; - PRESENCE_STATS = 5; - HISTORY = 6; - HISTORY_REMOVE = 7; - CHANNELS = 8; - INFO = 9; - RPC = 10; - SUBSCRIBE = 11; - REFRESH = 12; - CONNECTIONS = 14; - UPDATE_USER_STATUS = 15; - GET_USER_STATUS = 16; - DELETE_USER_STATUS = 17; - BLOCK_USER = 18; - UNBLOCK_USER = 19; - REVOKE_TOKEN = 20; - INVALIDATE_USER_TOKENS = 21; - DEVICE_REGISTER = 22; - DEVICE_UPDATE = 23; - DEVICE_REMOVE = 24; - DEVICE_LIST = 25; - DEVICE_TOPIC_LIST = 26; - DEVICE_TOPIC_UPDATE = 27; - USER_TOPIC_LIST = 28; - USER_TOPIC_UPDATE = 29; - SEND_PUSH_NOTIFICATION = 30; - UPDATE_PUSH_STATUS = 31; - CANCEL_PUSH = 32; - RATE_LIMIT = 47; - } - uint32 id = 1; - MethodType method = 2; - bytes params = 3; - - PublishRequest publish = 4; - BroadcastRequest broadcast = 5; - SubscribeRequest subscribe = 6; - UnsubscribeRequest unsubscribe = 7; - DisconnectRequest disconnect = 8; - PresenceRequest presence = 9; - PresenceStatsRequest presence_stats = 10; - HistoryRequest history = 11; - HistoryRemoveRequest history_remove = 12; - InfoRequest info = 13; - RPCRequest rpc = 14; - RefreshRequest refresh = 15; - ChannelsRequest channels = 16; - ConnectionsRequest connections = 17; - UpdateUserStatusRequest update_user_status = 18; - GetUserStatusRequest get_user_status = 19; - DeleteUserStatusRequest delete_user_status = 20; - BlockUserRequest block_user = 21; - UnblockUserRequest unblock_user = 22; - RevokeTokenRequest revoke_token = 23; - InvalidateUserTokensRequest invalidate_user_tokens = 24; - DeviceRegisterRequest device_register = 25; - DeviceUpdateRequest device_update = 26; - DeviceRemoveRequest device_remove = 27; - DeviceListRequest device_list = 28; - DeviceTopicListRequest device_topic_list = 29; - DeviceTopicUpdateRequest device_topic_update = 30; - UserTopicListRequest user_topic_list = 31; - UserTopicUpdateRequest user_topic_update = 32; - SendPushNotificationRequest send_push_notification = 33; - UpdatePushStatusRequest update_push_status = 34; - CancelPushRequest cancel_push = 35; - RateLimitRequest rate_limit = 50; -} - -message Error { - uint32 code = 1; - string message = 2; -} - -message Reply { - uint32 id = 1; - Error error = 2; - bytes result = 3; - - PublishResult publish = 4; - BroadcastResult broadcast = 5; - SubscribeResult subscribe = 6; - UnsubscribeResult unsubscribe = 7; - DisconnectResult disconnect = 8; - PresenceResult presence = 9; - PresenceStatsResult presence_stats = 10; - HistoryResult history = 11; - HistoryRemoveResult history_remove = 12; - InfoResult info = 13; - RPCResult rpc = 14; - RefreshResult refresh = 15; - ChannelsResult channels = 16; - ConnectionsResult connections = 17; - UpdateUserStatusResult update_user_status = 18; - GetUserStatusResult get_user_status = 19; - DeleteUserStatusResult delete_user_status = 20; - BlockUserResult block_user = 21; - UnblockUserResult unblock_user = 22; - RevokeTokenResult revoke_token = 23; - InvalidateUserTokensResult invalidate_user_tokens = 24; - DeviceRegisterResult device_register = 25; - DeviceUpdateResult device_update = 26; - DeviceRemoveResult device_remove = 27; - DeviceListResult device_list = 28; - DeviceTopicListResult device_topic_list = 29; - DeviceTopicUpdateResult device_topic_update = 30; - UserTopicListResult user_topic_list = 31; - UserTopicUpdateResult user_topic_update = 32; - SendPushNotificationResult send_push_notification = 33; - UpdatePushStatusResult update_push_status = 34; - CancelPushResult cancel_push = 35; - RateLimitResult rate_limit = 50; -} - -message BoolValue { - bool value = 1; -} - -message Int32Value { - int32 value = 1; -} - -message SubscribeOptionOverride { - BoolValue presence = 1; - BoolValue join_leave = 2; - BoolValue force_recovery = 3; - BoolValue force_positioning = 4; - BoolValue force_push_join_leave = 5; -} - -message BatchRequest { - repeated Command commands = 1; - bool parallel = 2; -} - -message BatchResponse { - repeated Reply replies = 1; -} - -message PublishRequest { - string channel = 1; - bytes data = 2; - string b64data = 3; - bool skip_history = 4; - map tags = 5; - string idempotency_key = 6; -} - -message PublishResponse { - Error error = 1; - PublishResult result = 2; -} - -message PublishResult { - uint64 offset = 1; - string epoch = 2; -} - -message BroadcastRequest { - repeated string channels = 1; - bytes data = 2; - string b64data = 3; - bool skip_history = 4; - map tags = 5; - string idempotency_key = 6; -} - -message BroadcastResponse { - Error error = 1; - BroadcastResult result = 2; -} - -message BroadcastResult { - repeated PublishResponse responses = 1; -} - -message SubscribeRequest { - string channel = 1; - string user = 2; - int64 expire_at = 3; - bytes info = 4; - string b64info = 5; - string client = 6; - bytes data = 7; - string b64data = 8; - StreamPosition recover_since = 9; - SubscribeOptionOverride override = 10; - string session = 11; -} - -message SubscribeResponse { - Error error = 1; - SubscribeResult result = 2; -} - -message SubscribeResult {} - -message UnsubscribeRequest { - string channel = 1; - string user = 2; - string client = 3; - string session = 4; -} - -message UnsubscribeResponse { - Error error = 1; - UnsubscribeResult result = 2; -} - -message UnsubscribeResult {} - -message Disconnect { - reserved 3; - uint32 code = 1; - string reason = 2; -} - -message DisconnectRequest { - string user = 1; - Disconnect disconnect = 2; - string client = 3; - repeated string whitelist = 4; - string session = 5; -} - -message DisconnectResponse { - Error error = 1; - DisconnectResult result = 2; -} - -message DisconnectResult {} - -message PresenceRequest { - string channel = 1; -} - -message PresenceResponse { - Error error = 1; - PresenceResult result = 2; -} - -message ClientInfo { - string user = 1; - string client = 2; - bytes conn_info = 3; - bytes chan_info = 4; -} - -message PresenceResult { - map presence = 1; -} - -message PresenceStatsRequest { - string channel = 1; -} - -message PresenceStatsResponse { - Error error = 1; - PresenceStatsResult result = 2; -} - -message PresenceStatsResult { - uint32 num_clients = 1; - uint32 num_users = 2; -} - -message StreamPosition { - uint64 offset = 1; - string epoch = 2; -} - -message HistoryRequest { - string channel = 1; - int32 limit = 2; - StreamPosition since = 3; - bool reverse = 4; -} - -message HistoryResponse { - Error error = 1; - HistoryResult result = 2; -} - -message Publication { - // Removed: string uid = 1; - bytes data = 2; - ClientInfo info = 3; - uint64 offset = 4; - map tags = 5; -} - -message HistoryResult { - repeated Publication publications = 1; - string epoch = 2; - uint64 offset = 3; -} - -message HistoryRemoveRequest { - string channel = 1; -} - -message HistoryRemoveResponse { - Error error = 1; - HistoryRemoveResult result = 2; -} - -message HistoryRemoveResult {} - -message InfoRequest {} - -message InfoResponse { - Error error = 1; - InfoResult result = 2; -} - -message InfoResult { - repeated NodeResult nodes = 1; -} - -message RPCRequest { - string method = 1; - bytes params = 2; -} - -message RPCResponse { - Error error = 1; - RPCResult result = 2; -} - -message RPCResult { - bytes data = 1; -} - -message RefreshRequest { - string user = 1; - string client = 2; - bool expired = 3; - int64 expire_at = 4; - bytes info = 5; - string session = 6; -} - -message RefreshResponse { - Error error = 1; - RefreshResult result = 2; -} - -message RefreshResult {} - -message NodeResult { - string uid = 1; - string name = 2; - string version = 3; - uint32 num_clients = 4; - uint32 num_users = 5; - uint32 num_channels = 6; - uint32 uptime = 7; - Metrics metrics = 8; - Process process = 9; - uint32 num_subs = 10; -} - -message Metrics { - double interval = 1; - map items = 2; -} - -message Process { - double cpu = 1; - int64 rss = 2; -} - -message ChannelsRequest { - string pattern = 1; -} - -message ChannelsResponse { - Error error = 1; - ChannelsResult result = 2; -} - -message ChannelsResult { - map channels = 1; -} - -message ChannelInfo { - uint32 num_clients = 1; -} - -message ConnectionsRequest { - string user = 1; - string expression = 2; -} - -message ConnectionsResponse { - Error error = 1; - ConnectionsResult result = 2; -} - -message ConnectionsResult { - map connections = 1; -} - -message ConnectionInfo { - string app_name = 1; - string app_version = 2; - string transport = 3; - string protocol = 4; - // 5-7 dropped for backwards compatibility. - string user = 8; - ConnectionState state = 9; -} - -message ConnectionState { - map channels = 1; - ConnectionTokenInfo connection_token = 2; - map subscription_tokens = 3; - bytes meta = 4; -} - -message ChannelContext { - uint32 source = 1; -} - -message ConnectionTokenInfo { - string uid = 1; - int64 issued_at = 2; -} - -message SubscriptionTokenInfo { - string uid = 1; - int64 issued_at = 2; -} - -message UpdateUserStatusRequest { - repeated string users = 1; - string state = 2; -} - -message UpdateUserStatusResponse { - Error error = 1; - UpdateUserStatusResult result = 2; -} - -message UpdateUserStatusResult {} - -message GetUserStatusRequest { - repeated string users = 1; -} - -message GetUserStatusResponse { - Error error = 1; - GetUserStatusResult result = 2; -} - -message GetUserStatusResult { - repeated UserStatus statuses = 1; -} - -message UserStatus { - string user = 1; - int64 active = 2; - int64 online = 3; - string state = 4; -} - -message DeleteUserStatusRequest { - repeated string users = 1; -} - -message DeleteUserStatusResponse { - Error error = 1; - DeleteUserStatusResult result = 2; -} - -message DeleteUserStatusResult { -} - -message BlockUserRequest { - int64 expire_at = 1; - string user = 2; -} - -message BlockUserResult {} - -message BlockUserResponse { - Error error = 1; - BlockUserResult result = 2; -} - -message UnblockUserRequest { - string user = 1; -} - -message UnblockUserResult {} - -message UnblockUserResponse { - Error error = 1; - UnblockUserResult result = 2; -} - -message RevokeTokenRequest { - int64 expire_at = 1; - string uid = 2; -} - -message RevokeTokenResult {} - -message RevokeTokenResponse { - Error error = 1; - RevokeTokenResult result = 2; -} - -message InvalidateUserTokensRequest { - int64 expire_at = 1; - string user = 2; - int64 issued_before = 3; - string channel = 4; -} - -message InvalidateUserTokensResult {} - -message InvalidateUserTokensResponse { - Error error = 1; - InvalidateUserTokensResult result = 2; -} - -message DeviceRegisterRequest { - string id = 1; - string provider = 2; - string token = 3; - string platform = 4; - string user = 5; - map meta = 6; - repeated string topics = 7; - //map labels = 8; - //map scores = 9; -} - -message DeviceUpdateRequest { - repeated string ids = 1; - repeated string users = 2; - - DeviceUserUpdate user_update = 4; - DeviceMetaUpdate meta_update = 5; - DeviceTopicsUpdate topics_update = 6; - //DeviceLabelsUpdate labels_update = 7; - //DeviceScoresUpdate scores_update = 8; -} - -message DeviceRemoveRequest { - repeated string ids = 1; - repeated string users = 2; -} - -message DeviceUserUpdate { - string user = 1; -} - -message DeviceMetaUpdate { - map meta = 1; -} - -message DeviceTopicsUpdate { - string op = 1; // add | remove | set - repeated string topics = 2; -} - -message DeviceFilter { - repeated string ids = 1; - repeated string users = 2; - repeated string topics = 3; - repeated string providers = 4; - repeated string platforms = 5; -} - -message DeviceListRequest { - DeviceFilter filter = 1; - - bool include_total_count = 2; - bool include_meta = 3; - bool include_topics = 4; - //bool include_labels = 5; - //bool include_scores = 6; - - string cursor = 10; - int32 limit = 11; -} - -message DeviceTopicFilter { - repeated string device_ids = 1; - repeated string device_providers = 2; - repeated string device_platforms = 3; - repeated string device_users = 4; - repeated string topics = 5; - string topic_prefix = 6; -} - -message DeviceTopicListRequest { - DeviceTopicFilter filter = 1; - - bool include_total_count = 2; - bool include_device = 3; - - string cursor = 10; - int32 limit = 11; -} - -message UserTopicFilter { - repeated string users = 1; - repeated string topics = 2; - string topic_prefix = 3; -} - -message UserTopicListRequest { - UserTopicFilter filter = 1; - - bool include_total_count = 2; - - string cursor = 10; - int32 limit = 11; -} - -message DeviceTopicUpdateRequest { - string device_id = 1; - string op = 2; // add | remove | set - repeated string topics = 3; -} - -message UserTopicUpdateRequest { - string user = 1; - string op = 2; // add | remove | set - repeated string topics = 3; -} - -message DeviceRegisterResponse { - Error error = 1; - DeviceRegisterResult result = 2; -} - -message DeviceUpdateResponse { - Error error = 1; - DeviceUpdateResult result = 2; -} - -message DeviceRemoveResponse { - Error error = 1; - DeviceRemoveResult result = 2; -} - -message DeviceListResponse { - Error error = 1; - DeviceListResult result = 2; -} - -message DeviceTopicListResponse { - Error error = 1; - DeviceTopicListResult result = 2; -} - -message UserTopicListResponse { - Error error = 1; - UserTopicListResult result = 2; -} - -message DeviceTopicUpdateResponse { - Error error = 1; - DeviceTopicUpdateResult result = 2; -} - -message UserTopicUpdateResponse { - Error error = 1; - UserTopicUpdateResult result = 2; -} - -message DeviceRegisterResult { - string id = 1; -} - -message DeviceUpdateResult { -} - -message DeviceRemoveResult { -} - -message DeviceListResult { - repeated Device items = 1; - string next_cursor = 2; - int64 total_count = 3; -} - -message Device { - string id = 1; - string platform = 2; - string provider = 3; - string token = 4; - string user = 5; - int64 created_at = 6; - int64 updated_at = 7; - - map meta = 10; - repeated string topics = 11; - //map labels = 12; - //map scores = 13; -} - -message DeviceTopicListResult { - repeated DeviceTopic items = 1; - string next_cursor = 2; - int64 total_count = 3; -} - -message DeviceTopic { - string id = 1; - string topic = 2; - Device device = 3; -} - -message UserTopicListResult { - repeated UserTopic items = 1; - string next_cursor = 2; - int64 total_count = 3; -} - -message DeviceTopicUpdateResult { -} - -message UserTopicUpdateResult { -} - -message UserTopic { - string id = 1; - string user = 2; - string topic = 3; -} - -message PushRecipient { - DeviceFilter filter = 1; - - repeated string fcm_tokens = 2; - string fcm_topic = 3; - string fcm_condition = 4; - - repeated string hms_tokens = 5; - string hms_topic = 6; - string hms_condition = 7; - - repeated string apns_tokens = 8; -} - -message PushNotification { - FcmPushNotification fcm = 1; - HmsPushNotification hms = 2; - ApnsPushNotification apns = 3; - - int64 expire_at = 5; // timestamp in the future when Centrifugo should stop trying to send push notification. -} - -message FcmPushNotification { - bytes message = 1; -} - -message HmsPushNotification { - bytes message = 1; -} - -message ApnsPushNotification { - map headers = 1; - bytes payload = 2; -} - -message SendPushNotificationRequest { - PushRecipient recipient = 1; - PushNotification notification = 2; - string uid = 3; // unique identifier for push notification, used for matching in Centrifugo analytics. - int64 send_at = 4; -} - -message SendPushNotificationResponse { - Error error = 1; - SendPushNotificationResult result = 2; -} - -message SendPushNotificationResult { - string uid = 1; // Unique identifier of notification send request (it's not a FCM message id). -} - -message UpdatePushStatusRequest { - string uid = 1; // uid of push notification (matches SendPushNotificationResult.uid) - string status = 2; // failed | sent | delivered | interacted - string device_id = 3; // Centrifugo device id. - string msg_id = 4; // Provider issued message id. -} - -message UpdatePushStatusResponse { - Error error = 1; - UpdatePushStatusResult result = 2; -} - -message UpdatePushStatusResult {} - -message CancelPushRequest { - string uid = 1; -} - -message CancelPushResponse { - Error error = 1; - CancelPushResult result = 2; -} - -message CancelPushResult {} - -message RateLimitRequest { - // string key = 1; - // int64 rate = 2; - // int64 interval = 3; - // int64 score = 4; -} - -message RateLimitResponse { - Error error = 1; - RateLimitResult result = 2; -} - -message RateLimitResult { - // bool allowed = 1; - // int64 tokens_left = 2; - // int64 allowed_in = 3; - // int64 server_time = 4; -} diff --git a/cent/proto/centrifugal/__init__.py b/cent/proto/centrifugal/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/cent/proto/centrifugal/centrifugo/__init__.py b/cent/proto/centrifugal/centrifugo/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/cent/proto/centrifugal/centrifugo/api/__init__.py b/cent/proto/centrifugal/centrifugo/api/__init__.py deleted file mode 100644 index ad4e3ee..0000000 --- a/cent/proto/centrifugal/centrifugo/api/__init__.py +++ /dev/null @@ -1,2225 +0,0 @@ -# Generated by the protocol buffer compiler. DO NOT EDIT! -# sources: cent/proto/apiproto.proto -# plugin: python-betterproto -# This file has been @generated - -from dataclasses import dataclass -from typing import ( - TYPE_CHECKING, - Dict, - List, - Optional, -) - -import betterproto -import grpclib -from betterproto.grpc.grpclib_server import ServiceBase - - -if TYPE_CHECKING: - import grpclib.server - from betterproto.grpc.grpclib_client import MetadataLike - from grpclib.metadata import Deadline - - -class CommandMethodType(betterproto.Enum): - PUBLISH = 0 - BROADCAST = 1 - UNSUBSCRIBE = 2 - DISCONNECT = 3 - PRESENCE = 4 - PRESENCE_STATS = 5 - HISTORY = 6 - HISTORY_REMOVE = 7 - CHANNELS = 8 - INFO = 9 - RPC = 10 - SUBSCRIBE = 11 - REFRESH = 12 - CONNECTIONS = 14 - UPDATE_USER_STATUS = 15 - GET_USER_STATUS = 16 - DELETE_USER_STATUS = 17 - BLOCK_USER = 18 - UNBLOCK_USER = 19 - REVOKE_TOKEN = 20 - INVALIDATE_USER_TOKENS = 21 - DEVICE_REGISTER = 22 - DEVICE_UPDATE = 23 - DEVICE_REMOVE = 24 - DEVICE_LIST = 25 - DEVICE_TOPIC_LIST = 26 - DEVICE_TOPIC_UPDATE = 27 - USER_TOPIC_LIST = 28 - USER_TOPIC_UPDATE = 29 - SEND_PUSH_NOTIFICATION = 30 - UPDATE_PUSH_STATUS = 31 - CANCEL_PUSH = 32 - RATE_LIMIT = 47 - - -@dataclass(eq=False, repr=False) -class Command(betterproto.Message): - id: int = betterproto.uint32_field(1) - method: "CommandMethodType" = betterproto.enum_field(2) - params: bytes = betterproto.bytes_field(3) - publish: "PublishRequest" = betterproto.message_field(4) - broadcast: "BroadcastRequest" = betterproto.message_field(5) - subscribe: "SubscribeRequest" = betterproto.message_field(6) - unsubscribe: "UnsubscribeRequest" = betterproto.message_field(7) - disconnect: "DisconnectRequest" = betterproto.message_field(8) - presence: "PresenceRequest" = betterproto.message_field(9) - presence_stats: "PresenceStatsRequest" = betterproto.message_field(10) - history: "HistoryRequest" = betterproto.message_field(11) - history_remove: "HistoryRemoveRequest" = betterproto.message_field(12) - info: "InfoRequest" = betterproto.message_field(13) - rpc: "RpcRequest" = betterproto.message_field(14) - refresh: "RefreshRequest" = betterproto.message_field(15) - channels: "ChannelsRequest" = betterproto.message_field(16) - connections: "ConnectionsRequest" = betterproto.message_field(17) - update_user_status: "UpdateUserStatusRequest" = betterproto.message_field(18) - get_user_status: "GetUserStatusRequest" = betterproto.message_field(19) - delete_user_status: "DeleteUserStatusRequest" = betterproto.message_field(20) - block_user: "BlockUserRequest" = betterproto.message_field(21) - unblock_user: "UnblockUserRequest" = betterproto.message_field(22) - revoke_token: "RevokeTokenRequest" = betterproto.message_field(23) - invalidate_user_tokens: "InvalidateUserTokensRequest" = betterproto.message_field( - 24 - ) - device_register: "DeviceRegisterRequest" = betterproto.message_field(25) - device_update: "DeviceUpdateRequest" = betterproto.message_field(26) - device_remove: "DeviceRemoveRequest" = betterproto.message_field(27) - device_list: "DeviceListRequest" = betterproto.message_field(28) - device_topic_list: "DeviceTopicListRequest" = betterproto.message_field(29) - device_topic_update: "DeviceTopicUpdateRequest" = betterproto.message_field(30) - user_topic_list: "UserTopicListRequest" = betterproto.message_field(31) - user_topic_update: "UserTopicUpdateRequest" = betterproto.message_field(32) - send_push_notification: "SendPushNotificationRequest" = betterproto.message_field( - 33 - ) - update_push_status: "UpdatePushStatusRequest" = betterproto.message_field(34) - cancel_push: "CancelPushRequest" = betterproto.message_field(35) - rate_limit: "RateLimitRequest" = betterproto.message_field(50) - - -@dataclass(eq=False, repr=False) -class Error(betterproto.Message): - code: int = betterproto.uint32_field(1) - message: str = betterproto.string_field(2) - - -@dataclass(eq=False, repr=False) -class Reply(betterproto.Message): - id: int = betterproto.uint32_field(1) - error: "Error" = betterproto.message_field(2) - result: bytes = betterproto.bytes_field(3) - publish: "PublishResult" = betterproto.message_field(4) - broadcast: "BroadcastResult" = betterproto.message_field(5) - subscribe: "SubscribeResult" = betterproto.message_field(6) - unsubscribe: "UnsubscribeResult" = betterproto.message_field(7) - disconnect: "DisconnectResult" = betterproto.message_field(8) - presence: "PresenceResult" = betterproto.message_field(9) - presence_stats: "PresenceStatsResult" = betterproto.message_field(10) - history: "HistoryResult" = betterproto.message_field(11) - history_remove: "HistoryRemoveResult" = betterproto.message_field(12) - info: "InfoResult" = betterproto.message_field(13) - rpc: "RpcResult" = betterproto.message_field(14) - refresh: "RefreshResult" = betterproto.message_field(15) - channels: "ChannelsResult" = betterproto.message_field(16) - connections: "ConnectionsResult" = betterproto.message_field(17) - update_user_status: "UpdateUserStatusResult" = betterproto.message_field(18) - get_user_status: "GetUserStatusResult" = betterproto.message_field(19) - delete_user_status: "DeleteUserStatusResult" = betterproto.message_field(20) - block_user: "BlockUserResult" = betterproto.message_field(21) - unblock_user: "UnblockUserResult" = betterproto.message_field(22) - revoke_token: "RevokeTokenResult" = betterproto.message_field(23) - invalidate_user_tokens: "InvalidateUserTokensResult" = betterproto.message_field(24) - device_register: "DeviceRegisterResult" = betterproto.message_field(25) - device_update: "DeviceUpdateResult" = betterproto.message_field(26) - device_remove: "DeviceRemoveResult" = betterproto.message_field(27) - device_list: "DeviceListResult" = betterproto.message_field(28) - device_topic_list: "DeviceTopicListResult" = betterproto.message_field(29) - device_topic_update: "DeviceTopicUpdateResult" = betterproto.message_field(30) - user_topic_list: "UserTopicListResult" = betterproto.message_field(31) - user_topic_update: "UserTopicUpdateResult" = betterproto.message_field(32) - send_push_notification: "SendPushNotificationResult" = betterproto.message_field(33) - update_push_status: "UpdatePushStatusResult" = betterproto.message_field(34) - cancel_push: "CancelPushResult" = betterproto.message_field(35) - rate_limit: "RateLimitResult" = betterproto.message_field(50) - - -@dataclass(eq=False, repr=False) -class BoolValue(betterproto.Message): - value: bool = betterproto.bool_field(1) - - -@dataclass(eq=False, repr=False) -class Int32Value(betterproto.Message): - value: int = betterproto.int32_field(1) - - -@dataclass(eq=False, repr=False) -class SubscribeOptionOverride(betterproto.Message): - presence: "BoolValue" = betterproto.message_field(1) - join_leave: "BoolValue" = betterproto.message_field(2) - force_recovery: "BoolValue" = betterproto.message_field(3) - force_positioning: "BoolValue" = betterproto.message_field(4) - force_push_join_leave: "BoolValue" = betterproto.message_field(5) - - -@dataclass(eq=False, repr=False) -class BatchRequest(betterproto.Message): - commands: List["Command"] = betterproto.message_field(1) - parallel: bool = betterproto.bool_field(2) - - -@dataclass(eq=False, repr=False) -class BatchResponse(betterproto.Message): - replies: List["Reply"] = betterproto.message_field(1) - - -@dataclass(eq=False, repr=False) -class PublishRequest(betterproto.Message): - channel: str = betterproto.string_field(1) - data: bytes = betterproto.bytes_field(2) - b64_data: str = betterproto.string_field(3) - skip_history: bool = betterproto.bool_field(4) - tags: Dict[str, str] = betterproto.map_field( - 5, betterproto.TYPE_STRING, betterproto.TYPE_STRING - ) - idempotency_key: str = betterproto.string_field(6) - - -@dataclass(eq=False, repr=False) -class PublishResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "PublishResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class PublishResult(betterproto.Message): - offset: int = betterproto.uint64_field(1) - epoch: str = betterproto.string_field(2) - - -@dataclass(eq=False, repr=False) -class BroadcastRequest(betterproto.Message): - channels: List[str] = betterproto.string_field(1) - data: bytes = betterproto.bytes_field(2) - b64_data: str = betterproto.string_field(3) - skip_history: bool = betterproto.bool_field(4) - tags: Dict[str, str] = betterproto.map_field( - 5, betterproto.TYPE_STRING, betterproto.TYPE_STRING - ) - idempotency_key: str = betterproto.string_field(6) - - -@dataclass(eq=False, repr=False) -class BroadcastResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "BroadcastResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class BroadcastResult(betterproto.Message): - responses: List["PublishResponse"] = betterproto.message_field(1) - - -@dataclass(eq=False, repr=False) -class SubscribeRequest(betterproto.Message): - channel: str = betterproto.string_field(1) - user: str = betterproto.string_field(2) - expire_at: int = betterproto.int64_field(3) - info: bytes = betterproto.bytes_field(4) - b64_info: str = betterproto.string_field(5) - client: str = betterproto.string_field(6) - data: bytes = betterproto.bytes_field(7) - b64_data: str = betterproto.string_field(8) - recover_since: "StreamPosition" = betterproto.message_field(9) - override: "SubscribeOptionOverride" = betterproto.message_field(10) - session: str = betterproto.string_field(11) - - -@dataclass(eq=False, repr=False) -class SubscribeResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "SubscribeResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class SubscribeResult(betterproto.Message): - pass - - -@dataclass(eq=False, repr=False) -class UnsubscribeRequest(betterproto.Message): - channel: str = betterproto.string_field(1) - user: str = betterproto.string_field(2) - client: str = betterproto.string_field(3) - session: str = betterproto.string_field(4) - - -@dataclass(eq=False, repr=False) -class UnsubscribeResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "UnsubscribeResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class UnsubscribeResult(betterproto.Message): - pass - - -@dataclass(eq=False, repr=False) -class Disconnect(betterproto.Message): - code: int = betterproto.uint32_field(1) - reason: str = betterproto.string_field(2) - - -@dataclass(eq=False, repr=False) -class DisconnectRequest(betterproto.Message): - user: str = betterproto.string_field(1) - disconnect: "Disconnect" = betterproto.message_field(2) - client: str = betterproto.string_field(3) - whitelist: List[str] = betterproto.string_field(4) - session: str = betterproto.string_field(5) - - -@dataclass(eq=False, repr=False) -class DisconnectResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "DisconnectResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class DisconnectResult(betterproto.Message): - pass - - -@dataclass(eq=False, repr=False) -class PresenceRequest(betterproto.Message): - channel: str = betterproto.string_field(1) - - -@dataclass(eq=False, repr=False) -class PresenceResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "PresenceResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class ClientInfo(betterproto.Message): - user: str = betterproto.string_field(1) - client: str = betterproto.string_field(2) - conn_info: bytes = betterproto.bytes_field(3) - chan_info: bytes = betterproto.bytes_field(4) - - -@dataclass(eq=False, repr=False) -class PresenceResult(betterproto.Message): - presence: Dict[str, "ClientInfo"] = betterproto.map_field( - 1, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE - ) - - -@dataclass(eq=False, repr=False) -class PresenceStatsRequest(betterproto.Message): - channel: str = betterproto.string_field(1) - - -@dataclass(eq=False, repr=False) -class PresenceStatsResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "PresenceStatsResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class PresenceStatsResult(betterproto.Message): - num_clients: int = betterproto.uint32_field(1) - num_users: int = betterproto.uint32_field(2) - - -@dataclass(eq=False, repr=False) -class StreamPosition(betterproto.Message): - offset: int = betterproto.uint64_field(1) - epoch: str = betterproto.string_field(2) - - -@dataclass(eq=False, repr=False) -class HistoryRequest(betterproto.Message): - channel: str = betterproto.string_field(1) - limit: int = betterproto.int32_field(2) - since: "StreamPosition" = betterproto.message_field(3) - reverse: bool = betterproto.bool_field(4) - - -@dataclass(eq=False, repr=False) -class HistoryResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "HistoryResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class Publication(betterproto.Message): - data: bytes = betterproto.bytes_field(2) - """Removed: string uid = 1;""" - - info: "ClientInfo" = betterproto.message_field(3) - offset: int = betterproto.uint64_field(4) - tags: Dict[str, str] = betterproto.map_field( - 5, betterproto.TYPE_STRING, betterproto.TYPE_STRING - ) - - -@dataclass(eq=False, repr=False) -class HistoryResult(betterproto.Message): - publications: List["Publication"] = betterproto.message_field(1) - epoch: str = betterproto.string_field(2) - offset: int = betterproto.uint64_field(3) - - -@dataclass(eq=False, repr=False) -class HistoryRemoveRequest(betterproto.Message): - channel: str = betterproto.string_field(1) - - -@dataclass(eq=False, repr=False) -class HistoryRemoveResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "HistoryRemoveResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class HistoryRemoveResult(betterproto.Message): - pass - - -@dataclass(eq=False, repr=False) -class InfoRequest(betterproto.Message): - pass - - -@dataclass(eq=False, repr=False) -class InfoResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "InfoResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class InfoResult(betterproto.Message): - nodes: List["NodeResult"] = betterproto.message_field(1) - - -@dataclass(eq=False, repr=False) -class RpcRequest(betterproto.Message): - method: str = betterproto.string_field(1) - params: bytes = betterproto.bytes_field(2) - - -@dataclass(eq=False, repr=False) -class RpcResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "RpcResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class RpcResult(betterproto.Message): - data: bytes = betterproto.bytes_field(1) - - -@dataclass(eq=False, repr=False) -class RefreshRequest(betterproto.Message): - user: str = betterproto.string_field(1) - client: str = betterproto.string_field(2) - expired: bool = betterproto.bool_field(3) - expire_at: int = betterproto.int64_field(4) - info: bytes = betterproto.bytes_field(5) - session: str = betterproto.string_field(6) - - -@dataclass(eq=False, repr=False) -class RefreshResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "RefreshResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class RefreshResult(betterproto.Message): - pass - - -@dataclass(eq=False, repr=False) -class NodeResult(betterproto.Message): - uid: str = betterproto.string_field(1) - name: str = betterproto.string_field(2) - version: str = betterproto.string_field(3) - num_clients: int = betterproto.uint32_field(4) - num_users: int = betterproto.uint32_field(5) - num_channels: int = betterproto.uint32_field(6) - uptime: int = betterproto.uint32_field(7) - metrics: "Metrics" = betterproto.message_field(8) - process: "Process" = betterproto.message_field(9) - num_subs: int = betterproto.uint32_field(10) - - -@dataclass(eq=False, repr=False) -class Metrics(betterproto.Message): - interval: float = betterproto.double_field(1) - items: Dict[str, float] = betterproto.map_field( - 2, betterproto.TYPE_STRING, betterproto.TYPE_DOUBLE - ) - - -@dataclass(eq=False, repr=False) -class Process(betterproto.Message): - cpu: float = betterproto.double_field(1) - rss: int = betterproto.int64_field(2) - - -@dataclass(eq=False, repr=False) -class ChannelsRequest(betterproto.Message): - pattern: str = betterproto.string_field(1) - - -@dataclass(eq=False, repr=False) -class ChannelsResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "ChannelsResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class ChannelsResult(betterproto.Message): - channels: Dict[str, "ChannelInfo"] = betterproto.map_field( - 1, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE - ) - - -@dataclass(eq=False, repr=False) -class ChannelInfo(betterproto.Message): - num_clients: int = betterproto.uint32_field(1) - - -@dataclass(eq=False, repr=False) -class ConnectionsRequest(betterproto.Message): - user: str = betterproto.string_field(1) - expression: str = betterproto.string_field(2) - - -@dataclass(eq=False, repr=False) -class ConnectionsResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "ConnectionsResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class ConnectionsResult(betterproto.Message): - connections: Dict[str, "ConnectionInfo"] = betterproto.map_field( - 1, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE - ) - - -@dataclass(eq=False, repr=False) -class ConnectionInfo(betterproto.Message): - app_name: str = betterproto.string_field(1) - app_version: str = betterproto.string_field(2) - transport: str = betterproto.string_field(3) - protocol: str = betterproto.string_field(4) - user: str = betterproto.string_field(8) - """5-7 dropped for backwards compatibility.""" - - state: "ConnectionState" = betterproto.message_field(9) - - -@dataclass(eq=False, repr=False) -class ConnectionState(betterproto.Message): - channels: Dict[str, "ChannelContext"] = betterproto.map_field( - 1, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE - ) - connection_token: "ConnectionTokenInfo" = betterproto.message_field(2) - subscription_tokens: Dict[str, "SubscriptionTokenInfo"] = betterproto.map_field( - 3, betterproto.TYPE_STRING, betterproto.TYPE_MESSAGE - ) - meta: bytes = betterproto.bytes_field(4) - - -@dataclass(eq=False, repr=False) -class ChannelContext(betterproto.Message): - source: int = betterproto.uint32_field(1) - - -@dataclass(eq=False, repr=False) -class ConnectionTokenInfo(betterproto.Message): - uid: str = betterproto.string_field(1) - issued_at: int = betterproto.int64_field(2) - - -@dataclass(eq=False, repr=False) -class SubscriptionTokenInfo(betterproto.Message): - uid: str = betterproto.string_field(1) - issued_at: int = betterproto.int64_field(2) - - -@dataclass(eq=False, repr=False) -class UpdateUserStatusRequest(betterproto.Message): - users: List[str] = betterproto.string_field(1) - state: str = betterproto.string_field(2) - - -@dataclass(eq=False, repr=False) -class UpdateUserStatusResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "UpdateUserStatusResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class UpdateUserStatusResult(betterproto.Message): - pass - - -@dataclass(eq=False, repr=False) -class GetUserStatusRequest(betterproto.Message): - users: List[str] = betterproto.string_field(1) - - -@dataclass(eq=False, repr=False) -class GetUserStatusResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "GetUserStatusResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class GetUserStatusResult(betterproto.Message): - statuses: List["UserStatus"] = betterproto.message_field(1) - - -@dataclass(eq=False, repr=False) -class UserStatus(betterproto.Message): - user: str = betterproto.string_field(1) - active: int = betterproto.int64_field(2) - online: int = betterproto.int64_field(3) - state: str = betterproto.string_field(4) - - -@dataclass(eq=False, repr=False) -class DeleteUserStatusRequest(betterproto.Message): - users: List[str] = betterproto.string_field(1) - - -@dataclass(eq=False, repr=False) -class DeleteUserStatusResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "DeleteUserStatusResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class DeleteUserStatusResult(betterproto.Message): - pass - - -@dataclass(eq=False, repr=False) -class BlockUserRequest(betterproto.Message): - expire_at: int = betterproto.int64_field(1) - user: str = betterproto.string_field(2) - - -@dataclass(eq=False, repr=False) -class BlockUserResult(betterproto.Message): - pass - - -@dataclass(eq=False, repr=False) -class BlockUserResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "BlockUserResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class UnblockUserRequest(betterproto.Message): - user: str = betterproto.string_field(1) - - -@dataclass(eq=False, repr=False) -class UnblockUserResult(betterproto.Message): - pass - - -@dataclass(eq=False, repr=False) -class UnblockUserResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "UnblockUserResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class RevokeTokenRequest(betterproto.Message): - expire_at: int = betterproto.int64_field(1) - uid: str = betterproto.string_field(2) - - -@dataclass(eq=False, repr=False) -class RevokeTokenResult(betterproto.Message): - pass - - -@dataclass(eq=False, repr=False) -class RevokeTokenResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "RevokeTokenResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class InvalidateUserTokensRequest(betterproto.Message): - expire_at: int = betterproto.int64_field(1) - user: str = betterproto.string_field(2) - issued_before: int = betterproto.int64_field(3) - channel: str = betterproto.string_field(4) - - -@dataclass(eq=False, repr=False) -class InvalidateUserTokensResult(betterproto.Message): - pass - - -@dataclass(eq=False, repr=False) -class InvalidateUserTokensResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "InvalidateUserTokensResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class DeviceRegisterRequest(betterproto.Message): - id: str = betterproto.string_field(1) - provider: str = betterproto.string_field(2) - token: str = betterproto.string_field(3) - platform: str = betterproto.string_field(4) - user: str = betterproto.string_field(5) - meta: Dict[str, str] = betterproto.map_field( - 6, betterproto.TYPE_STRING, betterproto.TYPE_STRING - ) - topics: List[str] = betterproto.string_field(7) - - -@dataclass(eq=False, repr=False) -class DeviceUpdateRequest(betterproto.Message): - ids: List[str] = betterproto.string_field(1) - users: List[str] = betterproto.string_field(2) - user_update: "DeviceUserUpdate" = betterproto.message_field(4) - meta_update: "DeviceMetaUpdate" = betterproto.message_field(5) - topics_update: "DeviceTopicsUpdate" = betterproto.message_field(6) - - -@dataclass(eq=False, repr=False) -class DeviceRemoveRequest(betterproto.Message): - ids: List[str] = betterproto.string_field(1) - users: List[str] = betterproto.string_field(2) - - -@dataclass(eq=False, repr=False) -class DeviceUserUpdate(betterproto.Message): - user: str = betterproto.string_field(1) - - -@dataclass(eq=False, repr=False) -class DeviceMetaUpdate(betterproto.Message): - meta: Dict[str, str] = betterproto.map_field( - 1, betterproto.TYPE_STRING, betterproto.TYPE_STRING - ) - - -@dataclass(eq=False, repr=False) -class DeviceTopicsUpdate(betterproto.Message): - op: str = betterproto.string_field(1) - topics: List[str] = betterproto.string_field(2) - - -@dataclass(eq=False, repr=False) -class DeviceFilter(betterproto.Message): - ids: List[str] = betterproto.string_field(1) - users: List[str] = betterproto.string_field(2) - topics: List[str] = betterproto.string_field(3) - providers: List[str] = betterproto.string_field(4) - platforms: List[str] = betterproto.string_field(5) - - -@dataclass(eq=False, repr=False) -class DeviceListRequest(betterproto.Message): - filter: "DeviceFilter" = betterproto.message_field(1) - include_total_count: bool = betterproto.bool_field(2) - include_meta: bool = betterproto.bool_field(3) - include_topics: bool = betterproto.bool_field(4) - cursor: str = betterproto.string_field(10) - limit: int = betterproto.int32_field(11) - - -@dataclass(eq=False, repr=False) -class DeviceTopicFilter(betterproto.Message): - device_ids: List[str] = betterproto.string_field(1) - device_providers: List[str] = betterproto.string_field(2) - device_platforms: List[str] = betterproto.string_field(3) - device_users: List[str] = betterproto.string_field(4) - topics: List[str] = betterproto.string_field(5) - topic_prefix: str = betterproto.string_field(6) - - -@dataclass(eq=False, repr=False) -class DeviceTopicListRequest(betterproto.Message): - filter: "DeviceTopicFilter" = betterproto.message_field(1) - include_total_count: bool = betterproto.bool_field(2) - include_device: bool = betterproto.bool_field(3) - cursor: str = betterproto.string_field(10) - limit: int = betterproto.int32_field(11) - - -@dataclass(eq=False, repr=False) -class UserTopicFilter(betterproto.Message): - users: List[str] = betterproto.string_field(1) - topics: List[str] = betterproto.string_field(2) - topic_prefix: str = betterproto.string_field(3) - - -@dataclass(eq=False, repr=False) -class UserTopicListRequest(betterproto.Message): - filter: "UserTopicFilter" = betterproto.message_field(1) - include_total_count: bool = betterproto.bool_field(2) - cursor: str = betterproto.string_field(10) - limit: int = betterproto.int32_field(11) - - -@dataclass(eq=False, repr=False) -class DeviceTopicUpdateRequest(betterproto.Message): - device_id: str = betterproto.string_field(1) - op: str = betterproto.string_field(2) - topics: List[str] = betterproto.string_field(3) - - -@dataclass(eq=False, repr=False) -class UserTopicUpdateRequest(betterproto.Message): - user: str = betterproto.string_field(1) - op: str = betterproto.string_field(2) - topics: List[str] = betterproto.string_field(3) - - -@dataclass(eq=False, repr=False) -class DeviceRegisterResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "DeviceRegisterResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class DeviceUpdateResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "DeviceUpdateResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class DeviceRemoveResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "DeviceRemoveResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class DeviceListResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "DeviceListResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class DeviceTopicListResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "DeviceTopicListResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class UserTopicListResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "UserTopicListResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class DeviceTopicUpdateResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "DeviceTopicUpdateResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class UserTopicUpdateResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "UserTopicUpdateResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class DeviceRegisterResult(betterproto.Message): - id: str = betterproto.string_field(1) - - -@dataclass(eq=False, repr=False) -class DeviceUpdateResult(betterproto.Message): - pass - - -@dataclass(eq=False, repr=False) -class DeviceRemoveResult(betterproto.Message): - pass - - -@dataclass(eq=False, repr=False) -class DeviceListResult(betterproto.Message): - items: List["Device"] = betterproto.message_field(1) - next_cursor: str = betterproto.string_field(2) - total_count: int = betterproto.int64_field(3) - - -@dataclass(eq=False, repr=False) -class Device(betterproto.Message): - id: str = betterproto.string_field(1) - platform: str = betterproto.string_field(2) - provider: str = betterproto.string_field(3) - token: str = betterproto.string_field(4) - user: str = betterproto.string_field(5) - created_at: int = betterproto.int64_field(6) - updated_at: int = betterproto.int64_field(7) - meta: Dict[str, str] = betterproto.map_field( - 10, betterproto.TYPE_STRING, betterproto.TYPE_STRING - ) - topics: List[str] = betterproto.string_field(11) - - -@dataclass(eq=False, repr=False) -class DeviceTopicListResult(betterproto.Message): - items: List["DeviceTopic"] = betterproto.message_field(1) - next_cursor: str = betterproto.string_field(2) - total_count: int = betterproto.int64_field(3) - - -@dataclass(eq=False, repr=False) -class DeviceTopic(betterproto.Message): - id: str = betterproto.string_field(1) - topic: str = betterproto.string_field(2) - device: "Device" = betterproto.message_field(3) - - -@dataclass(eq=False, repr=False) -class UserTopicListResult(betterproto.Message): - items: List["UserTopic"] = betterproto.message_field(1) - next_cursor: str = betterproto.string_field(2) - total_count: int = betterproto.int64_field(3) - - -@dataclass(eq=False, repr=False) -class DeviceTopicUpdateResult(betterproto.Message): - pass - - -@dataclass(eq=False, repr=False) -class UserTopicUpdateResult(betterproto.Message): - pass - - -@dataclass(eq=False, repr=False) -class UserTopic(betterproto.Message): - id: str = betterproto.string_field(1) - user: str = betterproto.string_field(2) - topic: str = betterproto.string_field(3) - - -@dataclass(eq=False, repr=False) -class PushRecipient(betterproto.Message): - filter: "DeviceFilter" = betterproto.message_field(1) - fcm_tokens: List[str] = betterproto.string_field(2) - fcm_topic: str = betterproto.string_field(3) - fcm_condition: str = betterproto.string_field(4) - hms_tokens: List[str] = betterproto.string_field(5) - hms_topic: str = betterproto.string_field(6) - hms_condition: str = betterproto.string_field(7) - apns_tokens: List[str] = betterproto.string_field(8) - - -@dataclass(eq=False, repr=False) -class PushNotification(betterproto.Message): - fcm: "FcmPushNotification" = betterproto.message_field(1) - hms: "HmsPushNotification" = betterproto.message_field(2) - apns: "ApnsPushNotification" = betterproto.message_field(3) - expire_at: int = betterproto.int64_field(5) - - -@dataclass(eq=False, repr=False) -class FcmPushNotification(betterproto.Message): - message: bytes = betterproto.bytes_field(1) - - -@dataclass(eq=False, repr=False) -class HmsPushNotification(betterproto.Message): - message: bytes = betterproto.bytes_field(1) - - -@dataclass(eq=False, repr=False) -class ApnsPushNotification(betterproto.Message): - headers: Dict[str, str] = betterproto.map_field( - 1, betterproto.TYPE_STRING, betterproto.TYPE_STRING - ) - payload: bytes = betterproto.bytes_field(2) - - -@dataclass(eq=False, repr=False) -class SendPushNotificationRequest(betterproto.Message): - recipient: "PushRecipient" = betterproto.message_field(1) - notification: "PushNotification" = betterproto.message_field(2) - uid: str = betterproto.string_field(3) - send_at: int = betterproto.int64_field(4) - - -@dataclass(eq=False, repr=False) -class SendPushNotificationResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "SendPushNotificationResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class SendPushNotificationResult(betterproto.Message): - uid: str = betterproto.string_field(1) - - -@dataclass(eq=False, repr=False) -class UpdatePushStatusRequest(betterproto.Message): - uid: str = betterproto.string_field(1) - status: str = betterproto.string_field(2) - device_id: str = betterproto.string_field(3) - msg_id: str = betterproto.string_field(4) - - -@dataclass(eq=False, repr=False) -class UpdatePushStatusResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "UpdatePushStatusResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class UpdatePushStatusResult(betterproto.Message): - pass - - -@dataclass(eq=False, repr=False) -class CancelPushRequest(betterproto.Message): - uid: str = betterproto.string_field(1) - - -@dataclass(eq=False, repr=False) -class CancelPushResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "CancelPushResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class CancelPushResult(betterproto.Message): - pass - - -@dataclass(eq=False, repr=False) -class RateLimitRequest(betterproto.Message): - pass - - -@dataclass(eq=False, repr=False) -class RateLimitResponse(betterproto.Message): - error: "Error" = betterproto.message_field(1) - result: "RateLimitResult" = betterproto.message_field(2) - - -@dataclass(eq=False, repr=False) -class RateLimitResult(betterproto.Message): - pass - - -class CentrifugoApiStub(betterproto.ServiceStub): - async def batch( - self, - batch_request: "BatchRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "BatchResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/Batch", - batch_request, - BatchResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def publish( - self, - publish_request: "PublishRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "PublishResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/Publish", - publish_request, - PublishResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def broadcast( - self, - broadcast_request: "BroadcastRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "BroadcastResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/Broadcast", - broadcast_request, - BroadcastResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def subscribe( - self, - subscribe_request: "SubscribeRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "SubscribeResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/Subscribe", - subscribe_request, - SubscribeResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def unsubscribe( - self, - unsubscribe_request: "UnsubscribeRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "UnsubscribeResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/Unsubscribe", - unsubscribe_request, - UnsubscribeResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def disconnect( - self, - disconnect_request: "DisconnectRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "DisconnectResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/Disconnect", - disconnect_request, - DisconnectResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def presence( - self, - presence_request: "PresenceRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "PresenceResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/Presence", - presence_request, - PresenceResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def presence_stats( - self, - presence_stats_request: "PresenceStatsRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "PresenceStatsResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/PresenceStats", - presence_stats_request, - PresenceStatsResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def history( - self, - history_request: "HistoryRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "HistoryResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/History", - history_request, - HistoryResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def history_remove( - self, - history_remove_request: "HistoryRemoveRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "HistoryRemoveResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/HistoryRemove", - history_remove_request, - HistoryRemoveResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def info( - self, - info_request: "InfoRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "InfoResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/Info", - info_request, - InfoResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def rpc( - self, - rpc_request: "RpcRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "RpcResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/RPC", - rpc_request, - RpcResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def refresh( - self, - refresh_request: "RefreshRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "RefreshResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/Refresh", - refresh_request, - RefreshResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def channels( - self, - channels_request: "ChannelsRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "ChannelsResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/Channels", - channels_request, - ChannelsResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def connections( - self, - connections_request: "ConnectionsRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "ConnectionsResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/Connections", - connections_request, - ConnectionsResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def update_user_status( - self, - update_user_status_request: "UpdateUserStatusRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "UpdateUserStatusResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/UpdateUserStatus", - update_user_status_request, - UpdateUserStatusResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def get_user_status( - self, - get_user_status_request: "GetUserStatusRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "GetUserStatusResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/GetUserStatus", - get_user_status_request, - GetUserStatusResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def delete_user_status( - self, - delete_user_status_request: "DeleteUserStatusRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "DeleteUserStatusResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/DeleteUserStatus", - delete_user_status_request, - DeleteUserStatusResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def block_user( - self, - block_user_request: "BlockUserRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "BlockUserResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/BlockUser", - block_user_request, - BlockUserResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def unblock_user( - self, - unblock_user_request: "UnblockUserRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "UnblockUserResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/UnblockUser", - unblock_user_request, - UnblockUserResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def revoke_token( - self, - revoke_token_request: "RevokeTokenRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "RevokeTokenResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/RevokeToken", - revoke_token_request, - RevokeTokenResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def invalidate_user_tokens( - self, - invalidate_user_tokens_request: "InvalidateUserTokensRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "InvalidateUserTokensResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/InvalidateUserTokens", - invalidate_user_tokens_request, - InvalidateUserTokensResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def device_register( - self, - device_register_request: "DeviceRegisterRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "DeviceRegisterResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/DeviceRegister", - device_register_request, - DeviceRegisterResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def device_update( - self, - device_update_request: "DeviceUpdateRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "DeviceUpdateResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/DeviceUpdate", - device_update_request, - DeviceUpdateResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def device_remove( - self, - device_remove_request: "DeviceRemoveRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "DeviceRemoveResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/DeviceRemove", - device_remove_request, - DeviceRemoveResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def device_list( - self, - device_list_request: "DeviceListRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "DeviceListResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/DeviceList", - device_list_request, - DeviceListResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def device_topic_list( - self, - device_topic_list_request: "DeviceTopicListRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "DeviceTopicListResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/DeviceTopicList", - device_topic_list_request, - DeviceTopicListResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def device_topic_update( - self, - device_topic_update_request: "DeviceTopicUpdateRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "DeviceTopicUpdateResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/DeviceTopicUpdate", - device_topic_update_request, - DeviceTopicUpdateResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def user_topic_list( - self, - user_topic_list_request: "UserTopicListRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "UserTopicListResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/UserTopicList", - user_topic_list_request, - UserTopicListResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def user_topic_update( - self, - user_topic_update_request: "UserTopicUpdateRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "UserTopicUpdateResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/UserTopicUpdate", - user_topic_update_request, - UserTopicUpdateResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def send_push_notification( - self, - send_push_notification_request: "SendPushNotificationRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "SendPushNotificationResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/SendPushNotification", - send_push_notification_request, - SendPushNotificationResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def update_push_status( - self, - update_push_status_request: "UpdatePushStatusRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "UpdatePushStatusResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/UpdatePushStatus", - update_push_status_request, - UpdatePushStatusResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def cancel_push( - self, - cancel_push_request: "CancelPushRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "CancelPushResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/CancelPush", - cancel_push_request, - CancelPushResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - async def rate_limit( - self, - rate_limit_request: "RateLimitRequest", - *, - timeout: Optional[float] = None, - deadline: Optional["Deadline"] = None, - metadata: Optional["MetadataLike"] = None - ) -> "RateLimitResponse": - return await self._unary_unary( - "/centrifugal.centrifugo.api.CentrifugoApi/RateLimit", - rate_limit_request, - RateLimitResponse, - timeout=timeout, - deadline=deadline, - metadata=metadata, - ) - - -class CentrifugoApiBase(ServiceBase): - - async def batch(self, batch_request: "BatchRequest") -> "BatchResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def publish(self, publish_request: "PublishRequest") -> "PublishResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def broadcast( - self, broadcast_request: "BroadcastRequest" - ) -> "BroadcastResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def subscribe( - self, subscribe_request: "SubscribeRequest" - ) -> "SubscribeResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def unsubscribe( - self, unsubscribe_request: "UnsubscribeRequest" - ) -> "UnsubscribeResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def disconnect( - self, disconnect_request: "DisconnectRequest" - ) -> "DisconnectResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def presence(self, presence_request: "PresenceRequest") -> "PresenceResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def presence_stats( - self, presence_stats_request: "PresenceStatsRequest" - ) -> "PresenceStatsResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def history(self, history_request: "HistoryRequest") -> "HistoryResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def history_remove( - self, history_remove_request: "HistoryRemoveRequest" - ) -> "HistoryRemoveResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def info(self, info_request: "InfoRequest") -> "InfoResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def rpc(self, rpc_request: "RpcRequest") -> "RpcResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def refresh(self, refresh_request: "RefreshRequest") -> "RefreshResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def channels(self, channels_request: "ChannelsRequest") -> "ChannelsResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def connections( - self, connections_request: "ConnectionsRequest" - ) -> "ConnectionsResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def update_user_status( - self, update_user_status_request: "UpdateUserStatusRequest" - ) -> "UpdateUserStatusResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def get_user_status( - self, get_user_status_request: "GetUserStatusRequest" - ) -> "GetUserStatusResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def delete_user_status( - self, delete_user_status_request: "DeleteUserStatusRequest" - ) -> "DeleteUserStatusResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def block_user( - self, block_user_request: "BlockUserRequest" - ) -> "BlockUserResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def unblock_user( - self, unblock_user_request: "UnblockUserRequest" - ) -> "UnblockUserResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def revoke_token( - self, revoke_token_request: "RevokeTokenRequest" - ) -> "RevokeTokenResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def invalidate_user_tokens( - self, invalidate_user_tokens_request: "InvalidateUserTokensRequest" - ) -> "InvalidateUserTokensResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def device_register( - self, device_register_request: "DeviceRegisterRequest" - ) -> "DeviceRegisterResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def device_update( - self, device_update_request: "DeviceUpdateRequest" - ) -> "DeviceUpdateResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def device_remove( - self, device_remove_request: "DeviceRemoveRequest" - ) -> "DeviceRemoveResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def device_list( - self, device_list_request: "DeviceListRequest" - ) -> "DeviceListResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def device_topic_list( - self, device_topic_list_request: "DeviceTopicListRequest" - ) -> "DeviceTopicListResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def device_topic_update( - self, device_topic_update_request: "DeviceTopicUpdateRequest" - ) -> "DeviceTopicUpdateResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def user_topic_list( - self, user_topic_list_request: "UserTopicListRequest" - ) -> "UserTopicListResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def user_topic_update( - self, user_topic_update_request: "UserTopicUpdateRequest" - ) -> "UserTopicUpdateResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def send_push_notification( - self, send_push_notification_request: "SendPushNotificationRequest" - ) -> "SendPushNotificationResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def update_push_status( - self, update_push_status_request: "UpdatePushStatusRequest" - ) -> "UpdatePushStatusResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def cancel_push( - self, cancel_push_request: "CancelPushRequest" - ) -> "CancelPushResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def rate_limit( - self, rate_limit_request: "RateLimitRequest" - ) -> "RateLimitResponse": - raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) - - async def __rpc_batch( - self, stream: "grpclib.server.Stream[BatchRequest, BatchResponse]" - ) -> None: - request = await stream.recv_message() - response = await self.batch(request) - await stream.send_message(response) - - async def __rpc_publish( - self, stream: "grpclib.server.Stream[PublishRequest, PublishResponse]" - ) -> None: - request = await stream.recv_message() - response = await self.publish(request) - await stream.send_message(response) - - async def __rpc_broadcast( - self, stream: "grpclib.server.Stream[BroadcastRequest, BroadcastResponse]" - ) -> None: - request = await stream.recv_message() - response = await self.broadcast(request) - await stream.send_message(response) - - async def __rpc_subscribe( - self, stream: "grpclib.server.Stream[SubscribeRequest, SubscribeResponse]" - ) -> None: - request = await stream.recv_message() - response = await self.subscribe(request) - await stream.send_message(response) - - async def __rpc_unsubscribe( - self, stream: "grpclib.server.Stream[UnsubscribeRequest, UnsubscribeResponse]" - ) -> None: - request = await stream.recv_message() - response = await self.unsubscribe(request) - await stream.send_message(response) - - async def __rpc_disconnect( - self, stream: "grpclib.server.Stream[DisconnectRequest, DisconnectResponse]" - ) -> None: - request = await stream.recv_message() - response = await self.disconnect(request) - await stream.send_message(response) - - async def __rpc_presence( - self, stream: "grpclib.server.Stream[PresenceRequest, PresenceResponse]" - ) -> None: - request = await stream.recv_message() - response = await self.presence(request) - await stream.send_message(response) - - async def __rpc_presence_stats( - self, - stream: "grpclib.server.Stream[PresenceStatsRequest, PresenceStatsResponse]", - ) -> None: - request = await stream.recv_message() - response = await self.presence_stats(request) - await stream.send_message(response) - - async def __rpc_history( - self, stream: "grpclib.server.Stream[HistoryRequest, HistoryResponse]" - ) -> None: - request = await stream.recv_message() - response = await self.history(request) - await stream.send_message(response) - - async def __rpc_history_remove( - self, - stream: "grpclib.server.Stream[HistoryRemoveRequest, HistoryRemoveResponse]", - ) -> None: - request = await stream.recv_message() - response = await self.history_remove(request) - await stream.send_message(response) - - async def __rpc_info( - self, stream: "grpclib.server.Stream[InfoRequest, InfoResponse]" - ) -> None: - request = await stream.recv_message() - response = await self.info(request) - await stream.send_message(response) - - async def __rpc_rpc( - self, stream: "grpclib.server.Stream[RpcRequest, RpcResponse]" - ) -> None: - request = await stream.recv_message() - response = await self.rpc(request) - await stream.send_message(response) - - async def __rpc_refresh( - self, stream: "grpclib.server.Stream[RefreshRequest, RefreshResponse]" - ) -> None: - request = await stream.recv_message() - response = await self.refresh(request) - await stream.send_message(response) - - async def __rpc_channels( - self, stream: "grpclib.server.Stream[ChannelsRequest, ChannelsResponse]" - ) -> None: - request = await stream.recv_message() - response = await self.channels(request) - await stream.send_message(response) - - async def __rpc_connections( - self, stream: "grpclib.server.Stream[ConnectionsRequest, ConnectionsResponse]" - ) -> None: - request = await stream.recv_message() - response = await self.connections(request) - await stream.send_message(response) - - async def __rpc_update_user_status( - self, - stream: "grpclib.server.Stream[UpdateUserStatusRequest, UpdateUserStatusResponse]", - ) -> None: - request = await stream.recv_message() - response = await self.update_user_status(request) - await stream.send_message(response) - - async def __rpc_get_user_status( - self, - stream: "grpclib.server.Stream[GetUserStatusRequest, GetUserStatusResponse]", - ) -> None: - request = await stream.recv_message() - response = await self.get_user_status(request) - await stream.send_message(response) - - async def __rpc_delete_user_status( - self, - stream: "grpclib.server.Stream[DeleteUserStatusRequest, DeleteUserStatusResponse]", - ) -> None: - request = await stream.recv_message() - response = await self.delete_user_status(request) - await stream.send_message(response) - - async def __rpc_block_user( - self, stream: "grpclib.server.Stream[BlockUserRequest, BlockUserResponse]" - ) -> None: - request = await stream.recv_message() - response = await self.block_user(request) - await stream.send_message(response) - - async def __rpc_unblock_user( - self, stream: "grpclib.server.Stream[UnblockUserRequest, UnblockUserResponse]" - ) -> None: - request = await stream.recv_message() - response = await self.unblock_user(request) - await stream.send_message(response) - - async def __rpc_revoke_token( - self, stream: "grpclib.server.Stream[RevokeTokenRequest, RevokeTokenResponse]" - ) -> None: - request = await stream.recv_message() - response = await self.revoke_token(request) - await stream.send_message(response) - - async def __rpc_invalidate_user_tokens( - self, - stream: "grpclib.server.Stream[InvalidateUserTokensRequest, InvalidateUserTokensResponse]", - ) -> None: - request = await stream.recv_message() - response = await self.invalidate_user_tokens(request) - await stream.send_message(response) - - async def __rpc_device_register( - self, - stream: "grpclib.server.Stream[DeviceRegisterRequest, DeviceRegisterResponse]", - ) -> None: - request = await stream.recv_message() - response = await self.device_register(request) - await stream.send_message(response) - - async def __rpc_device_update( - self, stream: "grpclib.server.Stream[DeviceUpdateRequest, DeviceUpdateResponse]" - ) -> None: - request = await stream.recv_message() - response = await self.device_update(request) - await stream.send_message(response) - - async def __rpc_device_remove( - self, stream: "grpclib.server.Stream[DeviceRemoveRequest, DeviceRemoveResponse]" - ) -> None: - request = await stream.recv_message() - response = await self.device_remove(request) - await stream.send_message(response) - - async def __rpc_device_list( - self, stream: "grpclib.server.Stream[DeviceListRequest, DeviceListResponse]" - ) -> None: - request = await stream.recv_message() - response = await self.device_list(request) - await stream.send_message(response) - - async def __rpc_device_topic_list( - self, - stream: "grpclib.server.Stream[DeviceTopicListRequest, DeviceTopicListResponse]", - ) -> None: - request = await stream.recv_message() - response = await self.device_topic_list(request) - await stream.send_message(response) - - async def __rpc_device_topic_update( - self, - stream: "grpclib.server.Stream[DeviceTopicUpdateRequest, DeviceTopicUpdateResponse]", - ) -> None: - request = await stream.recv_message() - response = await self.device_topic_update(request) - await stream.send_message(response) - - async def __rpc_user_topic_list( - self, - stream: "grpclib.server.Stream[UserTopicListRequest, UserTopicListResponse]", - ) -> None: - request = await stream.recv_message() - response = await self.user_topic_list(request) - await stream.send_message(response) - - async def __rpc_user_topic_update( - self, - stream: "grpclib.server.Stream[UserTopicUpdateRequest, UserTopicUpdateResponse]", - ) -> None: - request = await stream.recv_message() - response = await self.user_topic_update(request) - await stream.send_message(response) - - async def __rpc_send_push_notification( - self, - stream: "grpclib.server.Stream[SendPushNotificationRequest, SendPushNotificationResponse]", - ) -> None: - request = await stream.recv_message() - response = await self.send_push_notification(request) - await stream.send_message(response) - - async def __rpc_update_push_status( - self, - stream: "grpclib.server.Stream[UpdatePushStatusRequest, UpdatePushStatusResponse]", - ) -> None: - request = await stream.recv_message() - response = await self.update_push_status(request) - await stream.send_message(response) - - async def __rpc_cancel_push( - self, stream: "grpclib.server.Stream[CancelPushRequest, CancelPushResponse]" - ) -> None: - request = await stream.recv_message() - response = await self.cancel_push(request) - await stream.send_message(response) - - async def __rpc_rate_limit( - self, stream: "grpclib.server.Stream[RateLimitRequest, RateLimitResponse]" - ) -> None: - request = await stream.recv_message() - response = await self.rate_limit(request) - await stream.send_message(response) - - def __mapping__(self) -> Dict[str, grpclib.const.Handler]: - return { - "/centrifugal.centrifugo.api.CentrifugoApi/Batch": grpclib.const.Handler( - self.__rpc_batch, - grpclib.const.Cardinality.UNARY_UNARY, - BatchRequest, - BatchResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/Publish": grpclib.const.Handler( - self.__rpc_publish, - grpclib.const.Cardinality.UNARY_UNARY, - PublishRequest, - PublishResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/Broadcast": grpclib.const.Handler( - self.__rpc_broadcast, - grpclib.const.Cardinality.UNARY_UNARY, - BroadcastRequest, - BroadcastResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/Subscribe": grpclib.const.Handler( - self.__rpc_subscribe, - grpclib.const.Cardinality.UNARY_UNARY, - SubscribeRequest, - SubscribeResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/Unsubscribe": grpclib.const.Handler( - self.__rpc_unsubscribe, - grpclib.const.Cardinality.UNARY_UNARY, - UnsubscribeRequest, - UnsubscribeResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/Disconnect": grpclib.const.Handler( - self.__rpc_disconnect, - grpclib.const.Cardinality.UNARY_UNARY, - DisconnectRequest, - DisconnectResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/Presence": grpclib.const.Handler( - self.__rpc_presence, - grpclib.const.Cardinality.UNARY_UNARY, - PresenceRequest, - PresenceResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/PresenceStats": grpclib.const.Handler( - self.__rpc_presence_stats, - grpclib.const.Cardinality.UNARY_UNARY, - PresenceStatsRequest, - PresenceStatsResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/History": grpclib.const.Handler( - self.__rpc_history, - grpclib.const.Cardinality.UNARY_UNARY, - HistoryRequest, - HistoryResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/HistoryRemove": grpclib.const.Handler( - self.__rpc_history_remove, - grpclib.const.Cardinality.UNARY_UNARY, - HistoryRemoveRequest, - HistoryRemoveResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/Info": grpclib.const.Handler( - self.__rpc_info, - grpclib.const.Cardinality.UNARY_UNARY, - InfoRequest, - InfoResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/RPC": grpclib.const.Handler( - self.__rpc_rpc, - grpclib.const.Cardinality.UNARY_UNARY, - RpcRequest, - RpcResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/Refresh": grpclib.const.Handler( - self.__rpc_refresh, - grpclib.const.Cardinality.UNARY_UNARY, - RefreshRequest, - RefreshResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/Channels": grpclib.const.Handler( - self.__rpc_channels, - grpclib.const.Cardinality.UNARY_UNARY, - ChannelsRequest, - ChannelsResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/Connections": grpclib.const.Handler( - self.__rpc_connections, - grpclib.const.Cardinality.UNARY_UNARY, - ConnectionsRequest, - ConnectionsResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/UpdateUserStatus": grpclib.const.Handler( - self.__rpc_update_user_status, - grpclib.const.Cardinality.UNARY_UNARY, - UpdateUserStatusRequest, - UpdateUserStatusResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/GetUserStatus": grpclib.const.Handler( - self.__rpc_get_user_status, - grpclib.const.Cardinality.UNARY_UNARY, - GetUserStatusRequest, - GetUserStatusResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/DeleteUserStatus": grpclib.const.Handler( - self.__rpc_delete_user_status, - grpclib.const.Cardinality.UNARY_UNARY, - DeleteUserStatusRequest, - DeleteUserStatusResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/BlockUser": grpclib.const.Handler( - self.__rpc_block_user, - grpclib.const.Cardinality.UNARY_UNARY, - BlockUserRequest, - BlockUserResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/UnblockUser": grpclib.const.Handler( - self.__rpc_unblock_user, - grpclib.const.Cardinality.UNARY_UNARY, - UnblockUserRequest, - UnblockUserResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/RevokeToken": grpclib.const.Handler( - self.__rpc_revoke_token, - grpclib.const.Cardinality.UNARY_UNARY, - RevokeTokenRequest, - RevokeTokenResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/InvalidateUserTokens": grpclib.const.Handler( - self.__rpc_invalidate_user_tokens, - grpclib.const.Cardinality.UNARY_UNARY, - InvalidateUserTokensRequest, - InvalidateUserTokensResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/DeviceRegister": grpclib.const.Handler( - self.__rpc_device_register, - grpclib.const.Cardinality.UNARY_UNARY, - DeviceRegisterRequest, - DeviceRegisterResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/DeviceUpdate": grpclib.const.Handler( - self.__rpc_device_update, - grpclib.const.Cardinality.UNARY_UNARY, - DeviceUpdateRequest, - DeviceUpdateResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/DeviceRemove": grpclib.const.Handler( - self.__rpc_device_remove, - grpclib.const.Cardinality.UNARY_UNARY, - DeviceRemoveRequest, - DeviceRemoveResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/DeviceList": grpclib.const.Handler( - self.__rpc_device_list, - grpclib.const.Cardinality.UNARY_UNARY, - DeviceListRequest, - DeviceListResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/DeviceTopicList": grpclib.const.Handler( - self.__rpc_device_topic_list, - grpclib.const.Cardinality.UNARY_UNARY, - DeviceTopicListRequest, - DeviceTopicListResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/DeviceTopicUpdate": grpclib.const.Handler( - self.__rpc_device_topic_update, - grpclib.const.Cardinality.UNARY_UNARY, - DeviceTopicUpdateRequest, - DeviceTopicUpdateResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/UserTopicList": grpclib.const.Handler( - self.__rpc_user_topic_list, - grpclib.const.Cardinality.UNARY_UNARY, - UserTopicListRequest, - UserTopicListResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/UserTopicUpdate": grpclib.const.Handler( - self.__rpc_user_topic_update, - grpclib.const.Cardinality.UNARY_UNARY, - UserTopicUpdateRequest, - UserTopicUpdateResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/SendPushNotification": grpclib.const.Handler( - self.__rpc_send_push_notification, - grpclib.const.Cardinality.UNARY_UNARY, - SendPushNotificationRequest, - SendPushNotificationResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/UpdatePushStatus": grpclib.const.Handler( - self.__rpc_update_push_status, - grpclib.const.Cardinality.UNARY_UNARY, - UpdatePushStatusRequest, - UpdatePushStatusResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/CancelPush": grpclib.const.Handler( - self.__rpc_cancel_push, - grpclib.const.Cardinality.UNARY_UNARY, - CancelPushRequest, - CancelPushResponse, - ), - "/centrifugal.centrifugo.api.CentrifugoApi/RateLimit": grpclib.const.Handler( - self.__rpc_rate_limit, - grpclib.const.Cardinality.UNARY_UNARY, - RateLimitRequest, - RateLimitResponse, - ), - } diff --git a/cent/requests.py b/cent/requests.py index d98c05d..665448d 100644 --- a/cent/requests.py +++ b/cent/requests.py @@ -1,21 +1,5 @@ from typing import Any, Optional, Dict, List -from cent.proto.centrifugal.centrifugo.api import ( - ChannelsRequest as GrpcChannelsRequest, - PublishRequest as GrpcPublishRequest, - PresenceStatsRequest as GrpcPresenceStatsRequest, - InfoRequest as GrpcInfoRequest, - BroadcastRequest as GrpcBroadcastRequest, - BatchRequest as GrpcBatchRequest, - RefreshRequest as GrpcRefreshRequest, - UnsubscribeRequest as GrpcUnsubscribeRequest, - SubscribeRequest as GrpcSubscribeRequest, - HistoryRequest as GrpcHistoryRequest, - HistoryRemoveRequest as GrpcHistoryRemoveRequest, - PresenceRequest as GrpcPresenceRequest, - DisconnectRequest as GrpcDisconnectRequest, -) - from cent.base import CentRequest from cent.types import StreamPosition, ChannelOptionsOverride, Disconnect @@ -45,10 +29,9 @@ class BatchRequest(CentRequest[BatchResult]): __returning__ = BatchResult __api_method__ = "batch" - __grpc_method__ = GrpcBatchRequest commands: List[CentRequest[Any]] - """List of commands to execute in batch.""" + parallel: Optional[bool] = None class BroadcastRequest(CentRequest[BroadcastResult]): @@ -70,7 +53,6 @@ class BroadcastRequest(CentRequest[BroadcastResult]): __returning__ = BroadcastResult __api_method__ = "broadcast" - __grpc_method__ = GrpcBroadcastRequest channels: List[str] data: Any @@ -90,7 +72,6 @@ class ChannelsRequest(CentRequest[ChannelsResult]): __returning__ = ChannelsResult __api_method__ = "channels" - __grpc_method__ = GrpcChannelsRequest pattern: Optional[str] = None @@ -108,7 +89,6 @@ class DisconnectRequest(CentRequest[DisconnectResult]): __returning__ = DisconnectResult __api_method__ = "disconnect" - __grpc_method__ = GrpcDisconnectRequest user: str client: Optional[str] = None @@ -130,7 +110,6 @@ class HistoryRequest(CentRequest[HistoryResult]): __returning__ = HistoryResult __api_method__ = "history" - __grpc_method__ = GrpcHistoryRequest channel: str limit: Optional[int] = None @@ -147,7 +126,6 @@ class HistoryRemoveRequest(CentRequest[HistoryRemoveResult]): __returning__ = HistoryRemoveResult __api_method__ = "history_remove" - __grpc_method__ = GrpcHistoryRemoveRequest channel: str @@ -157,7 +135,6 @@ class InfoRequest(CentRequest[InfoResult]): __returning__ = InfoResult __api_method__ = "info" - __grpc_method__ = GrpcInfoRequest class PresenceRequest(CentRequest[PresenceResult]): @@ -169,20 +146,21 @@ class PresenceRequest(CentRequest[PresenceResult]): __returning__ = PresenceResult __api_method__ = "presence" - __grpc_method__ = GrpcPresenceRequest channel: str class PresenceStatsRequest(CentRequest[PresenceStatsResult]): - """Presence request.""" + """Presence request. + + Attributes: + channel: Name of channel to call presence from. + """ __returning__ = PresenceStatsResult __api_method__ = "presence_stats" - __grpc_method__ = GrpcPresenceStatsRequest channel: str - """Name of channel to call presence from.""" class PublishRequest(CentRequest[PublishResult]): @@ -204,7 +182,6 @@ class PublishRequest(CentRequest[PublishResult]): __returning__ = PublishResult __api_method__ = "publish" - __grpc_method__ = GrpcPublishRequest channel: str data: Any @@ -227,7 +204,6 @@ class RefreshRequest(CentRequest[RefreshResult]): __returning__ = RefreshResult __api_method__ = "refresh" - __grpc_method__ = GrpcRefreshRequest user: str client: Optional[str] = None @@ -257,7 +233,6 @@ class SubscribeRequest(CentRequest[SubscribeResult]): __returning__ = SubscribeResult __api_method__ = "subscribe" - __grpc_method__ = GrpcSubscribeRequest user: str channel: str @@ -283,7 +258,6 @@ class UnsubscribeRequest(CentRequest[UnsubscribeResult]): __returning__ = UnsubscribeResult __api_method__ = "unsubscribe" - __grpc_method__ = GrpcUnsubscribeRequest user: str channel: str diff --git a/cent/results.py b/cent/results.py index dc6d6d8..4599ef2 100644 --- a/cent/results.py +++ b/cent/results.py @@ -2,97 +2,125 @@ from pydantic import Field -from cent.base import BaseResult +from cent.base import CentResult from cent.base import Response from cent.types import Publication, Node, ClientInfo -class BatchResult(BaseResult): - """Batch response.""" +class BatchResult(CentResult): + """Batch response. + + Attributes: + replies: List of results from batch request. + """ replies: List[Any] - """List of results from batch request.""" -class PublishResult(BaseResult): - """Publish result.""" +class PublishResult(CentResult): + """Publish result. + + Attributes: + offset: Offset of publication in history stream. + epoch: Epoch of current stream. + """ offset: Optional[int] = None - """Offset of publication in history stream.""" epoch: Optional[str] = None - """Epoch of current stream.""" -class BroadcastResult(BaseResult): - """Publish result.""" +class BroadcastResult(CentResult): + """Broadcast result. + + Attributes: + responses: List of responses for each individual publish + (with possible error and publish result) + """ responses: List[Response[PublishResult]] = Field(default_factory=list) - """Responses for each individual publish (with possible error and publish result).""" -class ChannelInfoResult(BaseResult): - """Channel info result.""" +class ChannelInfoResult(CentResult): + """Channel info result. + + Attributes: + num_clients: Total number of connections currently subscribed to a channel. + """ num_clients: int = Field(default=0) - """Total number of connections currently subscribed to a channel.""" -class ChannelsResult(BaseResult): - """Channels result.""" +class ChannelsResult(CentResult): + """Channels result. + + Attributes: + channels: Map where key is channel and value is ChannelInfoResult. + """ channels: Dict[str, ChannelInfoResult] - """Map where key is channel and value is ChannelInfoResult.""" -class DisconnectResult(BaseResult): +class DisconnectResult(CentResult): """Disconnect result.""" -class HistoryRemoveResult(BaseResult): +class HistoryRemoveResult(CentResult): """History remove result.""" -class HistoryResult(BaseResult): - """History result.""" +class HistoryResult(CentResult): + """History result. + + Attributes: + publications: List of publications in channel. + offset: Top offset in history stream. + epoch: Epoch of current stream. + """ publications: List[Publication] = Field(default_factory=list) - """List of publications in channel.""" offset: Optional[int] = None - """Top offset in history stream.""" epoch: Optional[str] = None - """Epoch of current stream.""" -class InfoResult(BaseResult): - """Info result.""" +class InfoResult(CentResult): + """Info result. + + Attributes: + nodes: Information about all nodes in a cluster. + """ nodes: List[Node] - """Information about all nodes in a cluster.""" -class PresenceResult(BaseResult): - """Presence result.""" +class PresenceResult(CentResult): + """Presence result. + + Attributes: + presence: Map where key is client ID and value is ClientInfo. + """ presence: Dict[str, ClientInfo] - """Offset of publication in history stream.""" -class PresenceStatsResult(BaseResult): - """Presence stats result.""" +class PresenceStatsResult(CentResult): + """Presence stats result. + + Attributes: + num_clients: Total number of clients in channel. + num_users: Total number of unique users in channel. + """ num_clients: int = Field(default=0) - """Total number of clients in channel.""" num_users: int = Field(default=0) - """Total number of unique users in channel.""" -class RefreshResult(BaseResult): +class RefreshResult(CentResult): """Refresh result.""" -class SubscribeResult(BaseResult): +class SubscribeResult(CentResult): """Subscribe result.""" -class UnsubscribeResult(BaseResult): +class UnsubscribeResult(CentResult): """Unsubscribe result.""" diff --git a/cent/types.py b/cent/types.py index dff49a8..3f3480f 100644 --- a/cent/types.py +++ b/cent/types.py @@ -2,13 +2,7 @@ from pydantic import Field -from cent.base import BaseResult, NestedModel -from cent.proto.centrifugal.centrifugo.api import ( - SubscribeOptionOverride as GrpcChannelOptionOverride, - BoolValue as GrpcBoolValue, - StreamPosition as GrpcStreamPosition, - Disconnect as GrpcDisconnect, -) +from cent.base import CentResult, NestedModel class Disconnect(NestedModel): @@ -19,8 +13,6 @@ class Disconnect(NestedModel): reason (str): Disconnect reason. """ - __grpc_method__ = GrpcDisconnect - code: int reason: str @@ -32,8 +24,6 @@ class BoolValue(NestedModel): value (bool): Value. """ - __grpc_method__ = GrpcBoolValue - value: bool @@ -46,8 +36,6 @@ class StreamPosition(NestedModel): epoch (str): Epoch of current stream. """ - __grpc_method__ = GrpcStreamPosition - offset: int epoch: str @@ -64,8 +52,6 @@ class ChannelOptionsOverride(NestedModel): force_recovery (Optional[BoolValue]): Override for force recovery. """ - __grpc_method__ = GrpcChannelOptionOverride - presence: Optional[BoolValue] = None join_leave: Optional[BoolValue] = None force_push_join_leave: Optional[BoolValue] = None @@ -73,7 +59,7 @@ class ChannelOptionsOverride(NestedModel): force_recovery: Optional[BoolValue] = None -class ProcessStats(BaseResult): +class ProcessStats(CentResult): """ Represents statistics of a process. @@ -86,7 +72,7 @@ class ProcessStats(BaseResult): rss: int -class ClientInfo(BaseResult): +class ClientInfo(CentResult): """ Represents the result containing client information. @@ -105,7 +91,7 @@ class ClientInfo(BaseResult): chan_info: Optional[Any] = None -class Publication(BaseResult): +class Publication(CentResult): """Publication result. Attributes: @@ -119,7 +105,7 @@ class Publication(BaseResult): tags: Optional[Dict[str, str]] = None -class Metrics(BaseResult): +class Metrics(CentResult): """Metrics result. Attributes: @@ -131,7 +117,7 @@ class Metrics(BaseResult): items: Dict[str, float] -class Node(BaseResult): +class Node(CentResult): """Node result. Attributes: diff --git a/poetry.lock b/poetry.lock index 780a956..b454a49 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "aiohttp" @@ -121,28 +121,6 @@ files = [ {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, ] -[[package]] -name = "anyio" -version = "4.2.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = false -python-versions = ">=3.8" -files = [ - {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"}, - {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} - -[package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.23)"] - [[package]] name = "async-timeout" version = "4.0.3" @@ -173,73 +151,6 @@ tests = ["attrs[tests-no-zope]", "zope-interface"] tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] -[[package]] -name = "betterproto" -version = "2.0.0b6" -description = "A better Protobuf / gRPC generator & library" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "betterproto-2.0.0b6-py3-none-any.whl", hash = "sha256:a0839ec165d110a69d0d116f4d0e2bec8d186af4db826257931f0831dab73fcf"}, - {file = "betterproto-2.0.0b6.tar.gz", hash = "sha256:720ae92697000f6fcf049c69267d957f0871654c8b0d7458906607685daee784"}, -] - -[package.dependencies] -black = {version = ">=19.3b0", optional = true, markers = "extra == \"compiler\""} -grpclib = ">=0.4.1,<0.5.0" -isort = {version = ">=5.11.5,<6.0.0", optional = true, markers = "extra == \"compiler\""} -jinja2 = {version = ">=3.0.3", optional = true, markers = "extra == \"compiler\""} -python-dateutil = ">=2.8,<3.0" - -[package.extras] -compiler = ["black (>=19.3b0)", "isort (>=5.11.5,<6.0.0)", "jinja2 (>=3.0.3)"] - -[[package]] -name = "black" -version = "24.1.1" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.8" -files = [ - {file = "black-24.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c"}, - {file = "black-24.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445"}, - {file = "black-24.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a"}, - {file = "black-24.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4"}, - {file = "black-24.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7"}, - {file = "black-24.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8"}, - {file = "black-24.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161"}, - {file = "black-24.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d"}, - {file = "black-24.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8"}, - {file = "black-24.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e"}, - {file = "black-24.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6"}, - {file = "black-24.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b"}, - {file = "black-24.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62"}, - {file = "black-24.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5"}, - {file = "black-24.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6"}, - {file = "black-24.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717"}, - {file = "black-24.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9"}, - {file = "black-24.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024"}, - {file = "black-24.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2"}, - {file = "black-24.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac"}, - {file = "black-24.1.1-py3-none-any.whl", hash = "sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168"}, - {file = "black-24.1.1.tar.gz", hash = "sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "certifi" version = "2024.2.2" @@ -361,20 +272,6 @@ files = [ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - [[package]] name = "colorama" version = "0.4.6" @@ -513,203 +410,15 @@ files = [ {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, ] -[[package]] -name = "grpcio" -version = "1.60.1" -description = "HTTP/2-based RPC framework" -optional = false -python-versions = ">=3.7" -files = [ - {file = "grpcio-1.60.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:14e8f2c84c0832773fb3958240c69def72357bc11392571f87b2d7b91e0bb092"}, - {file = "grpcio-1.60.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:33aed0a431f5befeffd9d346b0fa44b2c01aa4aeae5ea5b2c03d3e25e0071216"}, - {file = "grpcio-1.60.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:fead980fbc68512dfd4e0c7b1f5754c2a8e5015a04dea454b9cada54a8423525"}, - {file = "grpcio-1.60.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:082081e6a36b6eb5cf0fd9a897fe777dbb3802176ffd08e3ec6567edd85bc104"}, - {file = "grpcio-1.60.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55ccb7db5a665079d68b5c7c86359ebd5ebf31a19bc1a91c982fd622f1e31ff2"}, - {file = "grpcio-1.60.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9b54577032d4f235452f77a83169b6527bf4b77d73aeada97d45b2aaf1bf5ce0"}, - {file = "grpcio-1.60.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7d142bcd604166417929b071cd396aa13c565749a4c840d6c702727a59d835eb"}, - {file = "grpcio-1.60.1-cp310-cp310-win32.whl", hash = "sha256:2a6087f234cb570008a6041c8ffd1b7d657b397fdd6d26e83d72283dae3527b1"}, - {file = "grpcio-1.60.1-cp310-cp310-win_amd64.whl", hash = "sha256:f2212796593ad1d0235068c79836861f2201fc7137a99aa2fea7beeb3b101177"}, - {file = "grpcio-1.60.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:79ae0dc785504cb1e1788758c588c711f4e4a0195d70dff53db203c95a0bd303"}, - {file = "grpcio-1.60.1-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:4eec8b8c1c2c9b7125508ff7c89d5701bf933c99d3910e446ed531cd16ad5d87"}, - {file = "grpcio-1.60.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:8c9554ca8e26241dabe7951aa1fa03a1ba0856688ecd7e7bdbdd286ebc272e4c"}, - {file = "grpcio-1.60.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91422ba785a8e7a18725b1dc40fbd88f08a5bb4c7f1b3e8739cab24b04fa8a03"}, - {file = "grpcio-1.60.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cba6209c96828711cb7c8fcb45ecef8c8859238baf15119daa1bef0f6c84bfe7"}, - {file = "grpcio-1.60.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c71be3f86d67d8d1311c6076a4ba3b75ba5703c0b856b4e691c9097f9b1e8bd2"}, - {file = "grpcio-1.60.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:af5ef6cfaf0d023c00002ba25d0751e5995fa0e4c9eec6cd263c30352662cbce"}, - {file = "grpcio-1.60.1-cp311-cp311-win32.whl", hash = "sha256:a09506eb48fa5493c58f946c46754ef22f3ec0df64f2b5149373ff31fb67f3dd"}, - {file = "grpcio-1.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:49c9b6a510e3ed8df5f6f4f3c34d7fbf2d2cae048ee90a45cd7415abab72912c"}, - {file = "grpcio-1.60.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:b58b855d0071575ea9c7bc0d84a06d2edfbfccec52e9657864386381a7ce1ae9"}, - {file = "grpcio-1.60.1-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:a731ac5cffc34dac62053e0da90f0c0b8560396a19f69d9703e88240c8f05858"}, - {file = "grpcio-1.60.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:cf77f8cf2a651fbd869fbdcb4a1931464189cd210abc4cfad357f1cacc8642a6"}, - {file = "grpcio-1.60.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c557e94e91a983e5b1e9c60076a8fd79fea1e7e06848eb2e48d0ccfb30f6e073"}, - {file = "grpcio-1.60.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:069fe2aeee02dfd2135d562d0663fe70fbb69d5eed6eb3389042a7e963b54de8"}, - {file = "grpcio-1.60.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb0af13433dbbd1c806e671d81ec75bd324af6ef75171fd7815ca3074fe32bfe"}, - {file = "grpcio-1.60.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2f44c32aef186bbba254129cea1df08a20be414144ac3bdf0e84b24e3f3b2e05"}, - {file = "grpcio-1.60.1-cp312-cp312-win32.whl", hash = "sha256:a212e5dea1a4182e40cd3e4067ee46be9d10418092ce3627475e995cca95de21"}, - {file = "grpcio-1.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:6e490fa5f7f5326222cb9f0b78f207a2b218a14edf39602e083d5f617354306f"}, - {file = "grpcio-1.60.1-cp37-cp37m-linux_armv7l.whl", hash = "sha256:4216e67ad9a4769117433814956031cb300f85edc855252a645a9a724b3b6594"}, - {file = "grpcio-1.60.1-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:73e14acd3d4247169955fae8fb103a2b900cfad21d0c35f0dcd0fdd54cd60367"}, - {file = "grpcio-1.60.1-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:6ecf21d20d02d1733e9c820fb5c114c749d888704a7ec824b545c12e78734d1c"}, - {file = "grpcio-1.60.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33bdea30dcfd4f87b045d404388469eb48a48c33a6195a043d116ed1b9a0196c"}, - {file = "grpcio-1.60.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53b69e79d00f78c81eecfb38f4516080dc7f36a198b6b37b928f1c13b3c063e9"}, - {file = "grpcio-1.60.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:39aa848794b887120b1d35b1b994e445cc028ff602ef267f87c38122c1add50d"}, - {file = "grpcio-1.60.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:72153a0d2e425f45b884540a61c6639436ddafa1829a42056aa5764b84108b8e"}, - {file = "grpcio-1.60.1-cp37-cp37m-win_amd64.whl", hash = "sha256:50d56280b482875d1f9128ce596e59031a226a8b84bec88cb2bf76c289f5d0de"}, - {file = "grpcio-1.60.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:6d140bdeb26cad8b93c1455fa00573c05592793c32053d6e0016ce05ba267549"}, - {file = "grpcio-1.60.1-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:bc808924470643b82b14fe121923c30ec211d8c693e747eba8a7414bc4351a23"}, - {file = "grpcio-1.60.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:70c83bb530572917be20c21f3b6be92cd86b9aecb44b0c18b1d3b2cc3ae47df0"}, - {file = "grpcio-1.60.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b106bc52e7f28170e624ba61cc7dc6829566e535a6ec68528f8e1afbed1c41f"}, - {file = "grpcio-1.60.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e980cd6db1088c144b92fe376747328d5554bc7960ce583ec7b7d81cd47287"}, - {file = "grpcio-1.60.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0c5807e9152eff15f1d48f6b9ad3749196f79a4a050469d99eecb679be592acc"}, - {file = "grpcio-1.60.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f1c3dc536b3ee124e8b24feb7533e5c70b9f2ef833e3b2e5513b2897fd46763a"}, - {file = "grpcio-1.60.1-cp38-cp38-win32.whl", hash = "sha256:d7404cebcdb11bb5bd40bf94131faf7e9a7c10a6c60358580fe83913f360f929"}, - {file = "grpcio-1.60.1-cp38-cp38-win_amd64.whl", hash = "sha256:c8754c75f55781515a3005063d9a05878b2cfb3cb7e41d5401ad0cf19de14872"}, - {file = "grpcio-1.60.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:0250a7a70b14000fa311de04b169cc7480be6c1a769b190769d347939d3232a8"}, - {file = "grpcio-1.60.1-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:660fc6b9c2a9ea3bb2a7e64ba878c98339abaf1811edca904ac85e9e662f1d73"}, - {file = "grpcio-1.60.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:76eaaba891083fcbe167aa0f03363311a9f12da975b025d30e94b93ac7a765fc"}, - {file = "grpcio-1.60.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d97c65ea7e097056f3d1ead77040ebc236feaf7f71489383d20f3b4c28412a"}, - {file = "grpcio-1.60.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb2a2911b028f01c8c64d126f6b632fcd8a9ac975aa1b3855766c94e4107180"}, - {file = "grpcio-1.60.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5a1ebbae7e2214f51b1f23b57bf98eeed2cf1ba84e4d523c48c36d5b2f8829ff"}, - {file = "grpcio-1.60.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a66f4d2a005bc78e61d805ed95dedfcb35efa84b7bba0403c6d60d13a3de2d6"}, - {file = "grpcio-1.60.1-cp39-cp39-win32.whl", hash = "sha256:8d488fbdbf04283f0d20742b64968d44825617aa6717b07c006168ed16488804"}, - {file = "grpcio-1.60.1-cp39-cp39-win_amd64.whl", hash = "sha256:61b7199cd2a55e62e45bfb629a35b71fc2c0cb88f686a047f25b1112d3810904"}, - {file = "grpcio-1.60.1.tar.gz", hash = "sha256:dd1d3a8d1d2e50ad9b59e10aa7f07c7d1be2b367f3f2d33c5fade96ed5460962"}, -] - -[package.extras] -protobuf = ["grpcio-tools (>=1.60.1)"] - -[[package]] -name = "grpcio-tools" -version = "1.60.1" -description = "Protobuf code generator for gRPC" -optional = false -python-versions = ">=3.7" -files = [ - {file = "grpcio-tools-1.60.1.tar.gz", hash = "sha256:da08224ab8675c6d464b988bd8ca02cccd2bf0275bceefe8f6219bfd4a4f5e85"}, - {file = "grpcio_tools-1.60.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:184b27333b627a7cc0972fb70d21a8bb7c02ac4a6febc16768d78ea8ff883ddd"}, - {file = "grpcio_tools-1.60.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:18d7737f29ef5bbe3352547d0eccd080807834f00df223867dfc860bf81e9180"}, - {file = "grpcio_tools-1.60.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:cc8ba358d2c658c6ecbc58e779bf0fc5a673fecac015a70db27fc5b4d37b76b6"}, - {file = "grpcio_tools-1.60.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2973f75e8ba5c551033a1d59cc97654f6f386deaf2559082011d245d7ed87bba"}, - {file = "grpcio_tools-1.60.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28ae665113affebdd109247386786e5ab4dccfcfad1b5f68e9cce2e326b57ee6"}, - {file = "grpcio_tools-1.60.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5c7ed086fef5ff59f46d53a052b1934b73e0f7d12365d656d6af3a88057d5a3e"}, - {file = "grpcio_tools-1.60.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8540f6480428a52614db71dd6394f52cbc0d2565b5ea1136a982f26390a42c7a"}, - {file = "grpcio_tools-1.60.1-cp310-cp310-win32.whl", hash = "sha256:5b4a939097005531edec331f22d0b82bff26e71ede009354d2f375b5d41e74f0"}, - {file = "grpcio_tools-1.60.1-cp310-cp310-win_amd64.whl", hash = "sha256:075bb67895970f96aabc1761ca674bf4db193f8fcad387f08e50402023b5f953"}, - {file = "grpcio_tools-1.60.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:284749d20fb22418f17d3d351b9eb838caf4a0393a9cb02c36e5c32fa4bbe9db"}, - {file = "grpcio_tools-1.60.1-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:b1041377cf32ee2338284ee26e6b9c10f9ea7728092376b19803dcb9b91d510d"}, - {file = "grpcio_tools-1.60.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:e529cd3d4109a6f4a3f7bdaca68946eb33734e2d7ffe861785a0586abe99ee67"}, - {file = "grpcio_tools-1.60.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31294b534f25f02ead204e58dcbe0e5437a95a1a6f276bb9378905595b02ff6d"}, - {file = "grpcio_tools-1.60.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fb6f4d2df0388c35c2804ba170f511238a681b679ead013bfe5e39d0ea9cf48"}, - {file = "grpcio_tools-1.60.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:40cd8268a675269ce59c4fa50877597ec638bb1099c52237bb726c8ac9791868"}, - {file = "grpcio_tools-1.60.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:985ac476da365267a2367ab20060f9096fbfc2e190fb02dd394f9ec05edf03ca"}, - {file = "grpcio_tools-1.60.1-cp311-cp311-win32.whl", hash = "sha256:bd85f6c368b93ae45edf8568473053cb1cc075ef3489efb18f9832d4ecce062f"}, - {file = "grpcio_tools-1.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:c20e752ff5057758845f4e5c7a298739bfba291f373ed18ea9c7c7acbe69e8ab"}, - {file = "grpcio_tools-1.60.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:aafc94616c5f89c891d859057b194a153c451f9921053454e9d7d4cbf79047eb"}, - {file = "grpcio_tools-1.60.1-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:9bba347000f57dae8aea79c0d76ef7d72895597524d30d0170c7d1974a3a03f3"}, - {file = "grpcio_tools-1.60.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:1e96a532d38411f0543fe1903ff522f7142a9901afb0ed94de58d79caf1905be"}, - {file = "grpcio_tools-1.60.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ea6e397d87f458bb2c387a4a6e1b65df74ce5b5194a1f16850c38309012e981"}, - {file = "grpcio_tools-1.60.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aeecd5b8faa2aab67e6c8b8a57e888c00ce70d39f331ede0a21312e92def1a6"}, - {file = "grpcio_tools-1.60.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:d2c26ce5f774c98bd2d3d8d1703048394018b55d297ebdb41ed2ba35b9a34f68"}, - {file = "grpcio_tools-1.60.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:214281cdafb7acfdcde848eca2de7c888a6e2b5cd25ab579712b965ea09a9cd4"}, - {file = "grpcio_tools-1.60.1-cp312-cp312-win32.whl", hash = "sha256:8c4b917aa4fcdc77990773063f0f14540aab8d4a8bf6c862b964a45d891a31d2"}, - {file = "grpcio_tools-1.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:0aa34c7c21cff2177a4096b2b0d51dfbc9f8a41f929847a434e89b352c5a215d"}, - {file = "grpcio_tools-1.60.1-cp37-cp37m-linux_armv7l.whl", hash = "sha256:acdba77584981fe799104aa545d9d97910bcf88c69b668b768c1f3e7d7e5afac"}, - {file = "grpcio_tools-1.60.1-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:2a7fa55bc62d4b8ebe6fb26f8cf89df3cf3b504eb6c5f3a2f0174689d35fddb0"}, - {file = "grpcio_tools-1.60.1-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:dffa326cf901fe08a0e218d9fdf593f12276088a8caa07fcbec7d051149cf9ef"}, - {file = "grpcio_tools-1.60.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf945bd22f396c0d0c691e0990db2bfc4e77816b1edc2aea8a69c35ae721aac9"}, - {file = "grpcio_tools-1.60.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6801cfc5a85f0fb6fd12cade45942aaa1c814422328d594d12d364815fe34123"}, - {file = "grpcio_tools-1.60.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f95bdc6c7c50b7fc442e53537bc5b4eb8cab2a671c1da80d40b5a4ab1fd5d416"}, - {file = "grpcio_tools-1.60.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:402efeec36d8b12b792bae8a900085416fc2f57a34b599445ace2e847b6b0d75"}, - {file = "grpcio_tools-1.60.1-cp37-cp37m-win_amd64.whl", hash = "sha256:af88a2062b9c35034a80b25f289034b9c3c00c42bb88efaa465503a06fbd6a87"}, - {file = "grpcio_tools-1.60.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:46b495bae31c5d3f6ac0240eb848f0642b5410f80dff2aacdea20cdea3938c1d"}, - {file = "grpcio_tools-1.60.1-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:b5ae375207af9aa82f516dcd513d2e0c83690b7788d45844daad846ed87550f8"}, - {file = "grpcio_tools-1.60.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:15f13e8f3d77b96adcb1e3615acec5b100bd836c6010c58a51465bcb9c06d128"}, - {file = "grpcio_tools-1.60.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c354505e6a3d170da374f20404ea6a78135502df4f5534e5c532bdf24c4cc2a5"}, - {file = "grpcio_tools-1.60.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8cfab27ba2bd36a3e3b522aed686133531e8b919703d0247a0885dae8815317"}, - {file = "grpcio_tools-1.60.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b6ef213cb0aecb2832ee82a2eac32f29f31f50b17ce020604d82205096a6bd0c"}, - {file = "grpcio_tools-1.60.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0b62cb2d43a7f0eacc6a6962dfff7c2564874012e1a72ae4167e762f449e2912"}, - {file = "grpcio_tools-1.60.1-cp38-cp38-win32.whl", hash = "sha256:3fcabf484720a9fa1690e2825fc940027a05a0c79a1075a730008ef634bd8ad2"}, - {file = "grpcio_tools-1.60.1-cp38-cp38-win_amd64.whl", hash = "sha256:22ce3e3d861321d208d8bfd6161ab976623520b179712c90b2c175151463a6b1"}, - {file = "grpcio_tools-1.60.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:4e66fe204da15e08e599adb3060109a42927c0868fe8933e2d341ea649eceb03"}, - {file = "grpcio_tools-1.60.1-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:c1047bd831de5d9da761e9dc246988d5f07d722186938dfd5f34807398101010"}, - {file = "grpcio_tools-1.60.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:eba5fafd70585fbd4cb6ae45e3c5e11d8598e2426c9f289b78f682c0606e81cb"}, - {file = "grpcio_tools-1.60.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bba7230c60238c7a4ffa29f1aff6d78edb41f2c79cbe4443406472b1c80ccb5d"}, - {file = "grpcio_tools-1.60.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2bb8efc2cd64bd8f2779b426dd7e94e60924078ba5150cbbb60a846e62d1ed2"}, - {file = "grpcio_tools-1.60.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:26f91161a91f1601777751230eaaafdf416fed08a15c3ba2ae391088e4a906c6"}, - {file = "grpcio_tools-1.60.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2c19be2bba5583e30f88bb5d71b430176c396f0d6d0db3785e5845bfa3d28cd2"}, - {file = "grpcio_tools-1.60.1-cp39-cp39-win32.whl", hash = "sha256:9aadc9c00baa2064baa4414cff7c269455449f14805a355226674d89c507342c"}, - {file = "grpcio_tools-1.60.1-cp39-cp39-win_amd64.whl", hash = "sha256:652b08c9fef39186ce4f97f05f5440c0ed41f117db0f7d6cb0e0d75dbc6afd3f"}, -] - -[package.dependencies] -grpcio = ">=1.60.1" -protobuf = ">=4.21.6,<5.0dev" -setuptools = "*" - -[[package]] -name = "grpclib" -version = "0.4.7" -description = "Pure-Python gRPC implementation for asyncio" -optional = false -python-versions = ">=3.7" -files = [ - {file = "grpclib-0.4.7.tar.gz", hash = "sha256:2988ef57c02b22b7a2e8e961792c41ccf97efc2ace91ae7a5b0de03c363823c3"}, -] - -[package.dependencies] -h2 = ">=3.1.0,<5" -multidict = "*" - -[package.extras] -protobuf = ["protobuf (>=3.20.0)"] - -[[package]] -name = "h2" -version = "4.1.0" -description = "HTTP/2 State-Machine based protocol implementation" -optional = false -python-versions = ">=3.6.1" -files = [ - {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, - {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, -] - -[package.dependencies] -hpack = ">=4.0,<5" -hyperframe = ">=6.0,<7" - -[[package]] -name = "hpack" -version = "4.0.0" -description = "Pure-Python HPACK header compression" -optional = false -python-versions = ">=3.6.1" -files = [ - {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, - {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, -] - -[[package]] -name = "hyperframe" -version = "6.0.1" -description = "HTTP/2 framing layer for Python" -optional = false -python-versions = ">=3.6.1" -files = [ - {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, - {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, -] - [[package]] name = "identify" -version = "2.5.33" +version = "2.5.34" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, - {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, + {file = "identify-2.5.34-py2.py3-none-any.whl", hash = "sha256:a4316013779e433d08b96e5eabb7f641e6c7942e4ab5d4c509ebd2e7a8994aed"}, + {file = "identify-2.5.34.tar.gz", hash = "sha256:ee17bc9d499899bc9eaec1ac7bf2dc9eedd480db9d88b96d123d3b64a9d34f5d"}, ] [package.extras] @@ -737,106 +446,6 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] -[[package]] -name = "isort" -version = "5.13.2" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, - {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, -] - -[package.extras] -colors = ["colorama (>=0.4.6)"] - -[[package]] -name = "jinja2" -version = "3.1.3" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -files = [ - {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, - {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "markupsafe" -version = "2.1.5" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, -] - [[package]] name = "multidict" version = "6.0.5" @@ -1019,17 +628,6 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - [[package]] name = "platformdirs" version = "4.2.0" @@ -1062,13 +660,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "3.6.0" +version = "3.6.1" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ - {file = "pre_commit-3.6.0-py2.py3-none-any.whl", hash = "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376"}, - {file = "pre_commit-3.6.0.tar.gz", hash = "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"}, + {file = "pre_commit-3.6.1-py2.py3-none-any.whl", hash = "sha256:9fe989afcf095d2c4796ce7c553cf28d4d4a9b9346de3cda079bcf40748454a4"}, + {file = "pre_commit-3.6.1.tar.gz", hash = "sha256:c90961d8aa706f75d60935aba09469a6b0bcb8345f127c3fbee4bdc5f114cf4b"}, ] [package.dependencies] @@ -1078,26 +676,6 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" -[[package]] -name = "protobuf" -version = "4.25.2" -description = "" -optional = false -python-versions = ">=3.8" -files = [ - {file = "protobuf-4.25.2-cp310-abi3-win32.whl", hash = "sha256:b50c949608682b12efb0b2717f53256f03636af5f60ac0c1d900df6213910fd6"}, - {file = "protobuf-4.25.2-cp310-abi3-win_amd64.whl", hash = "sha256:8f62574857ee1de9f770baf04dde4165e30b15ad97ba03ceac65f760ff018ac9"}, - {file = "protobuf-4.25.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:2db9f8fa64fbdcdc93767d3cf81e0f2aef176284071507e3ede160811502fd3d"}, - {file = "protobuf-4.25.2-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:10894a2885b7175d3984f2be8d9850712c57d5e7587a2410720af8be56cdaf62"}, - {file = "protobuf-4.25.2-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:fc381d1dd0516343f1440019cedf08a7405f791cd49eef4ae1ea06520bc1c020"}, - {file = "protobuf-4.25.2-cp38-cp38-win32.whl", hash = "sha256:33a1aeef4b1927431d1be780e87b641e322b88d654203a9e9d93f218ee359e61"}, - {file = "protobuf-4.25.2-cp38-cp38-win_amd64.whl", hash = "sha256:47f3de503fe7c1245f6f03bea7e8d3ec11c6c4a2ea9ef910e3221c8a15516d62"}, - {file = "protobuf-4.25.2-cp39-cp39-win32.whl", hash = "sha256:5e5c933b4c30a988b52e0b7c02641760a5ba046edc5e43d3b94a74c9fc57c1b3"}, - {file = "protobuf-4.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:d66a769b8d687df9024f2985d5137a337f957a0916cf5464d1513eee96a63ff0"}, - {file = "protobuf-4.25.2-py3-none-any.whl", hash = "sha256:a8b7a98d4ce823303145bf3c1a8bdb0f2f4642a414b196f04ad9853ed0c8f830"}, - {file = "protobuf-4.25.2.tar.gz", hash = "sha256:fe599e175cb347efc8ee524bcd4b902d11f7262c0e569ececcb89995c15f0a5e"}, -] - [[package]] name = "py-cpuinfo" version = "9.0.0" @@ -1111,18 +689,18 @@ files = [ [[package]] name = "pydantic" -version = "2.6.0" +version = "2.6.1" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.6.0-py3-none-any.whl", hash = "sha256:1440966574e1b5b99cf75a13bec7b20e3512e8a61b894ae252f56275e2c465ae"}, - {file = "pydantic-2.6.0.tar.gz", hash = "sha256:ae887bd94eb404b09d86e4d12f93893bdca79d766e738528c6fa1c849f3c6bcf"}, + {file = "pydantic-2.6.1-py3-none-any.whl", hash = "sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f"}, + {file = "pydantic-2.6.1.tar.gz", hash = "sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9"}, ] [package.dependencies] annotated-types = ">=0.4.0" -pydantic-core = "2.16.1" +pydantic-core = "2.16.2" typing-extensions = ">=4.6.1" [package.extras] @@ -1130,90 +708,90 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.16.1" +version = "2.16.2" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.16.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:300616102fb71241ff477a2cbbc847321dbec49428434a2f17f37528721c4948"}, - {file = "pydantic_core-2.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5511f962dd1b9b553e9534c3b9c6a4b0c9ded3d8c2be96e61d56f933feef9e1f"}, - {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98f0edee7ee9cc7f9221af2e1b95bd02810e1c7a6d115cfd82698803d385b28f"}, - {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9795f56aa6b2296f05ac79d8a424e94056730c0b860a62b0fdcfe6340b658cc8"}, - {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c45f62e4107ebd05166717ac58f6feb44471ed450d07fecd90e5f69d9bf03c48"}, - {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:462d599299c5971f03c676e2b63aa80fec5ebc572d89ce766cd11ca8bcb56f3f"}, - {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ebaa4bf6386a3b22eec518da7d679c8363fb7fb70cf6972161e5542f470798"}, - {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:99f9a50b56713a598d33bc23a9912224fc5d7f9f292444e6664236ae471ddf17"}, - {file = "pydantic_core-2.16.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8ec364e280db4235389b5e1e6ee924723c693cbc98e9d28dc1767041ff9bc388"}, - {file = "pydantic_core-2.16.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:653a5dfd00f601a0ed6654a8b877b18d65ac32c9d9997456e0ab240807be6cf7"}, - {file = "pydantic_core-2.16.1-cp310-none-win32.whl", hash = "sha256:1661c668c1bb67b7cec96914329d9ab66755911d093bb9063c4c8914188af6d4"}, - {file = "pydantic_core-2.16.1-cp310-none-win_amd64.whl", hash = "sha256:561be4e3e952c2f9056fba5267b99be4ec2afadc27261505d4992c50b33c513c"}, - {file = "pydantic_core-2.16.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:102569d371fadc40d8f8598a59379c37ec60164315884467052830b28cc4e9da"}, - {file = "pydantic_core-2.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:735dceec50fa907a3c314b84ed609dec54b76a814aa14eb90da31d1d36873a5e"}, - {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e83ebbf020be727d6e0991c1b192a5c2e7113eb66e3def0cd0c62f9f266247e4"}, - {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:30a8259569fbeec49cfac7fda3ec8123486ef1b729225222f0d41d5f840b476f"}, - {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:920c4897e55e2881db6a6da151198e5001552c3777cd42b8a4c2f72eedc2ee91"}, - {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5247a3d74355f8b1d780d0f3b32a23dd9f6d3ff43ef2037c6dcd249f35ecf4c"}, - {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5bea8012df5bb6dda1e67d0563ac50b7f64a5d5858348b5c8cb5043811c19d"}, - {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ed3025a8a7e5a59817b7494686d449ebfbe301f3e757b852c8d0d1961d6be864"}, - {file = "pydantic_core-2.16.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06f0d5a1d9e1b7932477c172cc720b3b23c18762ed7a8efa8398298a59d177c7"}, - {file = "pydantic_core-2.16.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:150ba5c86f502c040b822777e2e519b5625b47813bd05f9273a8ed169c97d9ae"}, - {file = "pydantic_core-2.16.1-cp311-none-win32.whl", hash = "sha256:d6cbdf12ef967a6aa401cf5cdf47850559e59eedad10e781471c960583f25aa1"}, - {file = "pydantic_core-2.16.1-cp311-none-win_amd64.whl", hash = "sha256:afa01d25769af33a8dac0d905d5c7bb2d73c7c3d5161b2dd6f8b5b5eea6a3c4c"}, - {file = "pydantic_core-2.16.1-cp311-none-win_arm64.whl", hash = "sha256:1a2fe7b00a49b51047334d84aafd7e39f80b7675cad0083678c58983662da89b"}, - {file = "pydantic_core-2.16.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f478ec204772a5c8218e30eb813ca43e34005dff2eafa03931b3d8caef87d51"}, - {file = "pydantic_core-2.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1936ef138bed2165dd8573aa65e3095ef7c2b6247faccd0e15186aabdda7f66"}, - {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99d3a433ef5dc3021c9534a58a3686c88363c591974c16c54a01af7efd741f13"}, - {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd88f40f2294440d3f3c6308e50d96a0d3d0973d6f1a5732875d10f569acef49"}, - {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fac641bbfa43d5a1bed99d28aa1fded1984d31c670a95aac1bf1d36ac6ce137"}, - {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72bf9308a82b75039b8c8edd2be2924c352eda5da14a920551a8b65d5ee89253"}, - {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb4363e6c9fc87365c2bc777a1f585a22f2f56642501885ffc7942138499bf54"}, - {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:20f724a023042588d0f4396bbbcf4cffd0ddd0ad3ed4f0d8e6d4ac4264bae81e"}, - {file = "pydantic_core-2.16.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fb4370b15111905bf8b5ba2129b926af9470f014cb0493a67d23e9d7a48348e8"}, - {file = "pydantic_core-2.16.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23632132f1fd608034f1a56cc3e484be00854db845b3a4a508834be5a6435a6f"}, - {file = "pydantic_core-2.16.1-cp312-none-win32.whl", hash = "sha256:b9f3e0bffad6e238f7acc20c393c1ed8fab4371e3b3bc311020dfa6020d99212"}, - {file = "pydantic_core-2.16.1-cp312-none-win_amd64.whl", hash = "sha256:a0b4cfe408cd84c53bab7d83e4209458de676a6ec5e9c623ae914ce1cb79b96f"}, - {file = "pydantic_core-2.16.1-cp312-none-win_arm64.whl", hash = "sha256:d195add190abccefc70ad0f9a0141ad7da53e16183048380e688b466702195dd"}, - {file = "pydantic_core-2.16.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:502c062a18d84452858f8aea1e520e12a4d5228fc3621ea5061409d666ea1706"}, - {file = "pydantic_core-2.16.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8c032ccee90b37b44e05948b449a2d6baed7e614df3d3f47fe432c952c21b60"}, - {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:920f4633bee43d7a2818e1a1a788906df5a17b7ab6fe411220ed92b42940f818"}, - {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f5d37ff01edcbace53a402e80793640c25798fb7208f105d87a25e6fcc9ea06"}, - {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:399166f24c33a0c5759ecc4801f040dbc87d412c1a6d6292b2349b4c505effc9"}, - {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac89ccc39cd1d556cc72d6752f252dc869dde41c7c936e86beac5eb555041b66"}, - {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73802194f10c394c2bedce7a135ba1d8ba6cff23adf4217612bfc5cf060de34c"}, - {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8fa00fa24ffd8c31fac081bf7be7eb495be6d248db127f8776575a746fa55c95"}, - {file = "pydantic_core-2.16.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:601d3e42452cd4f2891c13fa8c70366d71851c1593ed42f57bf37f40f7dca3c8"}, - {file = "pydantic_core-2.16.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07982b82d121ed3fc1c51faf6e8f57ff09b1325d2efccaa257dd8c0dd937acca"}, - {file = "pydantic_core-2.16.1-cp38-none-win32.whl", hash = "sha256:d0bf6f93a55d3fa7a079d811b29100b019784e2ee6bc06b0bb839538272a5610"}, - {file = "pydantic_core-2.16.1-cp38-none-win_amd64.whl", hash = "sha256:fbec2af0ebafa57eb82c18c304b37c86a8abddf7022955d1742b3d5471a6339e"}, - {file = "pydantic_core-2.16.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a497be217818c318d93f07e14502ef93d44e6a20c72b04c530611e45e54c2196"}, - {file = "pydantic_core-2.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:694a5e9f1f2c124a17ff2d0be613fd53ba0c26de588eb4bdab8bca855e550d95"}, - {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d4dfc66abea3ec6d9f83e837a8f8a7d9d3a76d25c9911735c76d6745950e62c"}, - {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8655f55fe68c4685673265a650ef71beb2d31871c049c8b80262026f23605ee3"}, - {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21e3298486c4ea4e4d5cc6fb69e06fb02a4e22089304308817035ac006a7f506"}, - {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71b4a48a7427f14679f0015b13c712863d28bb1ab700bd11776a5368135c7d60"}, - {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dca874e35bb60ce4f9f6665bfbfad050dd7573596608aeb9e098621ac331dc"}, - {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa496cd45cda0165d597e9d6f01e36c33c9508f75cf03c0a650018c5048f578e"}, - {file = "pydantic_core-2.16.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5317c04349472e683803da262c781c42c5628a9be73f4750ac7d13040efb5d2d"}, - {file = "pydantic_core-2.16.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:42c29d54ed4501a30cd71015bf982fa95e4a60117b44e1a200290ce687d3e640"}, - {file = "pydantic_core-2.16.1-cp39-none-win32.whl", hash = "sha256:ba07646f35e4e49376c9831130039d1b478fbfa1215ae62ad62d2ee63cf9c18f"}, - {file = "pydantic_core-2.16.1-cp39-none-win_amd64.whl", hash = "sha256:2133b0e412a47868a358713287ff9f9a328879da547dc88be67481cdac529118"}, - {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d25ef0c33f22649b7a088035fd65ac1ce6464fa2876578df1adad9472f918a76"}, - {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:99c095457eea8550c9fa9a7a992e842aeae1429dab6b6b378710f62bfb70b394"}, - {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b49c604ace7a7aa8af31196abbf8f2193be605db6739ed905ecaf62af31ccae0"}, - {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c56da23034fe66221f2208c813d8aa509eea34d97328ce2add56e219c3a9f41c"}, - {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cebf8d56fee3b08ad40d332a807ecccd4153d3f1ba8231e111d9759f02edfd05"}, - {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1ae8048cba95f382dba56766525abca438328455e35c283bb202964f41a780b0"}, - {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:780daad9e35b18d10d7219d24bfb30148ca2afc309928e1d4d53de86822593dc"}, - {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c94b5537bf6ce66e4d7830c6993152940a188600f6ae044435287753044a8fe2"}, - {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:adf28099d061a25fbcc6531febb7a091e027605385de9fe14dd6a97319d614cf"}, - {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:644904600c15816a1f9a1bafa6aab0d21db2788abcdf4e2a77951280473f33e1"}, - {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87bce04f09f0552b66fca0c4e10da78d17cb0e71c205864bab4e9595122cb9d9"}, - {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:877045a7969ace04d59516d5d6a7dee13106822f99a5d8df5e6822941f7bedc8"}, - {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9c46e556ee266ed3fb7b7a882b53df3c76b45e872fdab8d9cf49ae5e91147fd7"}, - {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4eebbd049008eb800f519578e944b8dc8e0f7d59a5abb5924cc2d4ed3a1834ff"}, - {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c0be58529d43d38ae849a91932391eb93275a06b93b79a8ab828b012e916a206"}, - {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b1fc07896fc1851558f532dffc8987e526b682ec73140886c831d773cef44b76"}, - {file = "pydantic_core-2.16.1.tar.gz", hash = "sha256:daff04257b49ab7f4b3f73f98283d3dbb1a65bf3500d55c7beac3c66c310fe34"}, + {file = "pydantic_core-2.16.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3fab4e75b8c525a4776e7630b9ee48aea50107fea6ca9f593c98da3f4d11bf7c"}, + {file = "pydantic_core-2.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8bde5b48c65b8e807409e6f20baee5d2cd880e0fad00b1a811ebc43e39a00ab2"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2924b89b16420712e9bb8192396026a8fbd6d8726224f918353ac19c4c043d2a"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16aa02e7a0f539098e215fc193c8926c897175d64c7926d00a36188917717a05"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:936a787f83db1f2115ee829dd615c4f684ee48ac4de5779ab4300994d8af325b"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:459d6be6134ce3b38e0ef76f8a672924460c455d45f1ad8fdade36796df1ddc8"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9ee4febb249c591d07b2d4dd36ebcad0ccd128962aaa1801508320896575ef"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40a0bd0bed96dae5712dab2aba7d334a6c67cbcac2ddfca7dbcc4a8176445990"}, + {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:870dbfa94de9b8866b37b867a2cb37a60c401d9deb4a9ea392abf11a1f98037b"}, + {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:308974fdf98046db28440eb3377abba274808bf66262e042c412eb2adf852731"}, + {file = "pydantic_core-2.16.2-cp310-none-win32.whl", hash = "sha256:a477932664d9611d7a0816cc3c0eb1f8856f8a42435488280dfbf4395e141485"}, + {file = "pydantic_core-2.16.2-cp310-none-win_amd64.whl", hash = "sha256:8f9142a6ed83d90c94a3efd7af8873bf7cefed2d3d44387bf848888482e2d25f"}, + {file = "pydantic_core-2.16.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:406fac1d09edc613020ce9cf3f2ccf1a1b2f57ab00552b4c18e3d5276c67eb11"}, + {file = "pydantic_core-2.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce232a6170dd6532096cadbf6185271e4e8c70fc9217ebe105923ac105da9978"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a90fec23b4b05a09ad988e7a4f4e081711a90eb2a55b9c984d8b74597599180f"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8aafeedb6597a163a9c9727d8a8bd363a93277701b7bfd2749fbefee2396469e"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9957433c3a1b67bdd4c63717eaf174ebb749510d5ea612cd4e83f2d9142f3fc8"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0d7a9165167269758145756db43a133608a531b1e5bb6a626b9ee24bc38a8f7"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dffaf740fe2e147fedcb6b561353a16243e654f7fe8e701b1b9db148242e1272"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ed79883b4328b7f0bd142733d99c8e6b22703e908ec63d930b06be3a0e7113"}, + {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cf903310a34e14651c9de056fcc12ce090560864d5a2bb0174b971685684e1d8"}, + {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46b0d5520dbcafea9a8645a8164658777686c5c524d381d983317d29687cce97"}, + {file = "pydantic_core-2.16.2-cp311-none-win32.whl", hash = "sha256:70651ff6e663428cea902dac297066d5c6e5423fda345a4ca62430575364d62b"}, + {file = "pydantic_core-2.16.2-cp311-none-win_amd64.whl", hash = "sha256:98dc6f4f2095fc7ad277782a7c2c88296badcad92316b5a6e530930b1d475ebc"}, + {file = "pydantic_core-2.16.2-cp311-none-win_arm64.whl", hash = "sha256:ef6113cd31411eaf9b39fc5a8848e71c72656fd418882488598758b2c8c6dfa0"}, + {file = "pydantic_core-2.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:88646cae28eb1dd5cd1e09605680c2b043b64d7481cdad7f5003ebef401a3039"}, + {file = "pydantic_core-2.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b883af50eaa6bb3299780651e5be921e88050ccf00e3e583b1e92020333304b"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bf26c2e2ea59d32807081ad51968133af3025c4ba5753e6a794683d2c91bf6e"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99af961d72ac731aae2a1b55ccbdae0733d816f8bfb97b41909e143de735f522"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02906e7306cb8c5901a1feb61f9ab5e5c690dbbeaa04d84c1b9ae2a01ebe9379"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5362d099c244a2d2f9659fb3c9db7c735f0004765bbe06b99be69fbd87c3f15"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ac426704840877a285d03a445e162eb258924f014e2f074e209d9b4ff7bf380"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b94cbda27267423411c928208e89adddf2ea5dd5f74b9528513f0358bba019cb"}, + {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6db58c22ac6c81aeac33912fb1af0e930bc9774166cdd56eade913d5f2fff35e"}, + {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396fdf88b1b503c9c59c84a08b6833ec0c3b5ad1a83230252a9e17b7dfb4cffc"}, + {file = "pydantic_core-2.16.2-cp312-none-win32.whl", hash = "sha256:7c31669e0c8cc68400ef0c730c3a1e11317ba76b892deeefaf52dcb41d56ed5d"}, + {file = "pydantic_core-2.16.2-cp312-none-win_amd64.whl", hash = "sha256:a3b7352b48fbc8b446b75f3069124e87f599d25afb8baa96a550256c031bb890"}, + {file = "pydantic_core-2.16.2-cp312-none-win_arm64.whl", hash = "sha256:a9e523474998fb33f7c1a4d55f5504c908d57add624599e095c20fa575b8d943"}, + {file = "pydantic_core-2.16.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ae34418b6b389d601b31153b84dce480351a352e0bb763684a1b993d6be30f17"}, + {file = "pydantic_core-2.16.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:732bd062c9e5d9582a30e8751461c1917dd1ccbdd6cafb032f02c86b20d2e7ec"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b52776a2e3230f4854907a1e0946eec04d41b1fc64069ee774876bbe0eab55"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef551c053692b1e39e3f7950ce2296536728871110e7d75c4e7753fb30ca87f4"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ebb892ed8599b23fa8f1799e13a12c87a97a6c9d0f497525ce9858564c4575a4"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa6c8c582036275997a733427b88031a32ffa5dfc3124dc25a730658c47a572f"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ba0884a91f1aecce75202473ab138724aa4fb26d7707f2e1fa6c3e68c84fbf"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7924e54f7ce5d253d6160090ddc6df25ed2feea25bfb3339b424a9dd591688bc"}, + {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69a7b96b59322a81c2203be537957313b07dd333105b73db0b69212c7d867b4b"}, + {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7e6231aa5bdacda78e96ad7b07d0c312f34ba35d717115f4b4bff6cb87224f0f"}, + {file = "pydantic_core-2.16.2-cp38-none-win32.whl", hash = "sha256:41dac3b9fce187a25c6253ec79a3f9e2a7e761eb08690e90415069ea4a68ff7a"}, + {file = "pydantic_core-2.16.2-cp38-none-win_amd64.whl", hash = "sha256:f685dbc1fdadb1dcd5b5e51e0a378d4685a891b2ddaf8e2bba89bd3a7144e44a"}, + {file = "pydantic_core-2.16.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:55749f745ebf154c0d63d46c8c58594d8894b161928aa41adbb0709c1fe78b77"}, + {file = "pydantic_core-2.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b30b0dd58a4509c3bd7eefddf6338565c4905406aee0c6e4a5293841411a1286"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18de31781cdc7e7b28678df7c2d7882f9692ad060bc6ee3c94eb15a5d733f8f7"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5864b0242f74b9dd0b78fd39db1768bc3f00d1ffc14e596fd3e3f2ce43436a33"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8f9186ca45aee030dc8234118b9c0784ad91a0bb27fc4e7d9d6608a5e3d386c"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc6f6c9be0ab6da37bc77c2dda5f14b1d532d5dbef00311ee6e13357a418e646"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa057095f621dad24a1e906747179a69780ef45cc8f69e97463692adbcdae878"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ad84731a26bcfb299f9eab56c7932d46f9cad51c52768cace09e92a19e4cf55"}, + {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3b052c753c4babf2d1edc034c97851f867c87d6f3ea63a12e2700f159f5c41c3"}, + {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e0f686549e32ccdb02ae6f25eee40cc33900910085de6aa3790effd391ae10c2"}, + {file = "pydantic_core-2.16.2-cp39-none-win32.whl", hash = "sha256:7afb844041e707ac9ad9acad2188a90bffce2c770e6dc2318be0c9916aef1469"}, + {file = "pydantic_core-2.16.2-cp39-none-win_amd64.whl", hash = "sha256:9da90d393a8227d717c19f5397688a38635afec89f2e2d7af0df037f3249c39a"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f60f920691a620b03082692c378661947d09415743e437a7478c309eb0e4f82"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:47924039e785a04d4a4fa49455e51b4eb3422d6eaacfde9fc9abf8fdef164e8a"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6294e76b0380bb7a61eb8a39273c40b20beb35e8c87ee101062834ced19c545"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe56851c3f1d6f5384b3051c536cc81b3a93a73faf931f404fef95217cf1e10d"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9d776d30cde7e541b8180103c3f294ef7c1862fd45d81738d156d00551005784"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:72f7919af5de5ecfaf1eba47bf9a5d8aa089a3340277276e5636d16ee97614d7"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:4bfcbde6e06c56b30668a0c872d75a7ef3025dc3c1823a13cf29a0e9b33f67e8"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ff7c97eb7a29aba230389a2661edf2e9e06ce616c7e35aa764879b6894a44b25"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9b5f13857da99325dcabe1cc4e9e6a3d7b2e2c726248ba5dd4be3e8e4a0b6d0e"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a7e41e3ada4cca5f22b478c08e973c930e5e6c7ba3588fb8e35f2398cdcc1545"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60eb8ceaa40a41540b9acae6ae7c1f0a67d233c40dc4359c256ad2ad85bdf5e5"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7beec26729d496a12fd23cf8da9944ee338c8b8a17035a560b585c36fe81af20"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22c5f022799f3cd6741e24f0443ead92ef42be93ffda0d29b2597208c94c3753"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:eca58e319f4fd6df004762419612122b2c7e7d95ffafc37e890252f869f3fb2a"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed957db4c33bc99895f3a1672eca7e80e8cda8bd1e29a80536b4ec2153fa9804"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:459c0d338cc55d099798618f714b21b7ece17eb1a87879f2da20a3ff4c7628e2"}, + {file = "pydantic_core-2.16.2.tar.gz", hash = "sha256:0ba503850d8b8dcc18391f10de896ae51d37fe5fe43dbfb6a35c5c5cad271a06"}, ] [package.dependencies] @@ -1241,6 +819,24 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-asyncio" +version = "0.23.5" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-asyncio-0.23.5.tar.gz", hash = "sha256:3a048872a9c4ba14c3e90cc1aa20cbc2def7d01c7c8db3777ec281ba9c057675"}, + {file = "pytest_asyncio-0.23.5-py3-none-any.whl", hash = "sha256:4e7093259ba018d58ede7d5315131d21923a60f8a6e9ee266ce1589685c89eac"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + [[package]] name = "pytest-benchmark" version = "4.0.0" @@ -1261,20 +857,6 @@ aspect = ["aspectlib"] elasticsearch = ["elasticsearch"] histogram = ["pygal", "pygaljs"] -[[package]] -name = "python-dateutil" -version = "2.8.2" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, -] - -[package.dependencies] -six = ">=1.5" - [[package]] name = "pyyaml" version = "6.0.1" @@ -1300,6 +882,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -1383,42 +966,20 @@ files = [ [[package]] name = "setuptools" -version = "69.0.3" +version = "69.1.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, - {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, + {file = "setuptools-69.1.0-py3-none-any.whl", hash = "sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6"}, + {file = "setuptools-69.1.0.tar.gz", hash = "sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "sniffio" -version = "1.3.0" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, -] - [[package]] name = "tomli" version = "2.0.1" @@ -1430,20 +991,6 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -[[package]] -name = "types-requests" -version = "2.31.0.20240125" -description = "Typing stubs for requests" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-requests-2.31.0.20240125.tar.gz", hash = "sha256:03a28ce1d7cd54199148e043b2079cdded22d6795d19a2c2a6791a4b2b5e2eb5"}, - {file = "types_requests-2.31.0.20240125-py3-none-any.whl", hash = "sha256:9592a9a4cb92d6d75d9b491a41477272b710e021011a2a3061157e2fb1f1a5d1"}, -] - -[package.dependencies] -urllib3 = ">=2" - [[package]] name = "typing-extensions" version = "4.9.0" @@ -1472,50 +1019,6 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] -[[package]] -name = "uvloop" -version = "0.19.0" -description = "Fast implementation of asyncio event loop on top of libuv" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e"}, - {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428"}, - {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8"}, - {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849"}, - {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957"}, - {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd"}, - {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef"}, - {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2"}, - {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1"}, - {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24"}, - {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533"}, - {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12"}, - {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650"}, - {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec"}, - {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc"}, - {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6"}, - {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593"}, - {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3"}, - {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd"}, - {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd"}, - {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be"}, - {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797"}, - {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d"}, - {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7"}, - {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b"}, - {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67"}, - {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7"}, - {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256"}, - {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17"}, - {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5"}, - {file = "uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd"}, -] - -[package.extras] -docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] -test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] - [[package]] name = "virtualenv" version = "20.25.0" @@ -1642,4 +1145,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "58b1474e3ef2acd49e78bb4414b99d92e35e2a88b1ee42f491e1c66ffbba7e7b" +content-hash = "d77bf417174a1e7c0d3d8fddd47d1b5bf7c2f604315b48412f0fdb687593aa8c" diff --git a/pyproject.toml b/pyproject.toml index 491c4cb..6f18ead 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "cent" version = "5.0.0b1" -description = "Python library to communicate with Centrifugo v5 server API" +description = "Python library to communicate with Centrifugo v5 server HTTP API" authors = ["Alexandr Emelin", "Katant Savelev"] license = "MIT" readme = 'README.md' @@ -29,19 +29,14 @@ python = "^3.9" aiohttp = "^3" pydantic = "^2" requests = "^2" -types-requests = "^2" -betterproto = { version = "^2.0.0b6", allow-prereleases = true } [tool.poetry.group.dev.dependencies] pre-commit = "^3.6.0" ruff = "^0.1.15" mypy = "^1.8.0" -uvloop = "^0.19.0" pytest = "^8" -anyio = "^4.2.0" pytest-benchmark = "^4.0.0" -betterproto = { extras = ["compiler"], version = "^2.0.0b6", allow-prereleases = true } -grpcio-tools = "^1.60.0" +pytest-asyncio = "^0.23.5" [tool.ruff] preview = true @@ -82,7 +77,6 @@ ignore = [ [tool.ruff.per-file-ignores] "tests/*" = ["S101", "PT012"] -"cent/proto/centrifugal/*" = ["RUF009", "ARG002", "E501", "TCH004", "PLR6301", "PLR0904", "PLW3201"] [tool.mypy] strict = true @@ -117,10 +111,6 @@ module = [ ] ignore_missing_imports = true -[[tool.mypy.overrides]] -module = ["cent.proto.centrifugal.*"] -ignore_errors = true - [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/tests/conftest.py b/tests/conftest.py index 4028149..1a3301a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,7 @@ import pytest -from cent import Client, AsyncClient, GrpcClient +from cent import Client, AsyncClient BASE_URL = "http://localhost:8000/api" API_KEY = "api_key" @@ -24,15 +24,6 @@ def sync_client() -> Client: return Client(BASE_URL, API_KEY) -@pytest.fixture() -async def grpc_client( - anyio_backend: Any, # noqa: ARG001 -) -> AsyncGenerator[GrpcClient, None]: - client = GrpcClient("localhost", 10000) - yield client - client._session.close() - - @pytest.fixture() async def async_client( anyio_backend: Any, # noqa: ARG001 diff --git a/tests/test_async_validation.py b/tests/test_async_validation.py index e4e56dd..b2b70fb 100644 --- a/tests/test_async_validation.py +++ b/tests/test_async_validation.py @@ -124,7 +124,7 @@ async def test_refresh(async_client: AsyncClient) -> None: async def test_batch(async_client: AsyncClient) -> None: - await async_client.batch( + result = await async_client.batch( commands=[ PublishRequest( channel="personal_1", @@ -144,6 +144,14 @@ async def test_batch(async_client: AsyncClient) -> None: ] ) + num_expected_replies = 4 + assert len(result.replies) == num_expected_replies + assert result.replies[0].offset + assert result.replies[1].offset + assert result.replies[2].responses[0].result.offset + assert result.replies[2].responses[1].result.offset + assert result.replies[3].presence == {} + async def test_error_publish(async_client: AsyncClient) -> None: with pytest.raises(CentAPIError, match="unknown channel") as exc_info: diff --git a/tests/test_grpc_validation.py b/tests/test_grpc_validation.py deleted file mode 100644 index 616ca56..0000000 --- a/tests/test_grpc_validation.py +++ /dev/null @@ -1,135 +0,0 @@ -import uuid -import json -import pytest - -from cent import ( - GrpcClient, - CentAPIError, - StreamPosition, - ChannelOptionsOverride, - BoolValue, - Disconnect, -) -from tests.conftest import UNKNOWN_CHANNEL_ERROR_CODE - - -async def test_publish(grpc_client: GrpcClient) -> None: - result = await grpc_client.publish( - "personal_1", - json.dumps({"data": "data"}).encode(), - skip_history=False, - tags={"tag": "tag"}, - idempotency_key="idempotency_key", - ) - assert result.offset - - -async def test_broadcast(grpc_client: GrpcClient) -> None: - await grpc_client.broadcast( - ["personal_1", "personal_2"], - json.dumps({"data": "data"}).encode(), - skip_history=False, - tags={"tag": "tag"}, - idempotency_key="idempotency_key", - ) - - -async def test_subscribe(grpc_client: GrpcClient) -> None: - await grpc_client.subscribe( - "user", - "personal_1", - info=json.dumps({"info": "info"}).encode(), - client="client", - session="session", - data=json.dumps({"data": "data"}).encode(), - recover_since=StreamPosition( - offset=1, - epoch="1", - ), - override=ChannelOptionsOverride( - presence=BoolValue(value=True), - join_leave=BoolValue(value=True), - force_recovery=BoolValue(value=True), - ), - ) - - -async def test_unsubscribe(grpc_client: GrpcClient) -> None: - await grpc_client.unsubscribe( - user="user", - channel="personal_1", - session="session", - client="client", - ) - - -async def test_presence(grpc_client: GrpcClient) -> None: - await grpc_client.presence("personal_1") - - -async def test_presence_stats(grpc_client: GrpcClient) -> None: - await grpc_client.presence_stats("personal_1") - - -async def test_history(grpc_client: GrpcClient) -> None: - channel = "personal_" + uuid.uuid4().hex - for i in range(10): - await grpc_client.publish( - channel, - json.dumps({"data": f"data {i}"}).encode(), - ) - result = await grpc_client.history( - channel=channel, - limit=1, - reverse=True, - ) - assert isinstance(result.offset, int) - assert result.offset > 0 - assert len(result.publications) == 1 - assert result.publications[0].data == b'{"data": "data 9"}' - - -async def test_history_remove(grpc_client: GrpcClient) -> None: - await grpc_client.history_remove(channel="personal_1") - - -async def test_info(grpc_client: GrpcClient) -> None: - await grpc_client.info() - - -async def test_channels(grpc_client: GrpcClient) -> None: - await grpc_client.channels( - pattern="*", - ) - - -async def test_disconnect(grpc_client: GrpcClient) -> None: - await grpc_client.disconnect( - user="user", - client="client", - session="session", - whitelist=["personal_1"], - disconnect=Disconnect( - code=4000, - reason="reason", - ), - ) - - -async def test_refresh(grpc_client: GrpcClient) -> None: - await grpc_client.refresh( - user="user", - client="client", - session="session", - expire_at=1, - expired=True, - ) - - -async def test_error_publish(grpc_client: GrpcClient) -> None: - with pytest.raises(CentAPIError, match="unknown channel") as exc_info: - await grpc_client.publish( - "undefined_channel:123", - json.dumps({"data": "data"}).encode(), - ) - assert exc_info.value.code == UNKNOWN_CHANNEL_ERROR_CODE diff --git a/tests/test_sync_validation.py b/tests/test_sync_validation.py index 8f0f695..07a3aec 100644 --- a/tests/test_sync_validation.py +++ b/tests/test_sync_validation.py @@ -125,7 +125,7 @@ def test_refresh(sync_client: Client) -> None: def test_batch(sync_client: Client) -> None: - sync_client.batch( + result = sync_client.batch( commands=[ PublishRequest( channel="personal_1", @@ -145,6 +145,14 @@ def test_batch(sync_client: Client) -> None: ] ) + num_expected_replies = 4 + assert len(result.replies) == num_expected_replies + assert result.replies[0].offset + assert result.replies[1].offset + assert result.replies[2].responses[0].result.offset + assert result.replies[2].responses[1].result.offset + assert result.replies[3].presence == {} + def test_error_publish(sync_client: Client) -> None: with pytest.raises(CentAPIError, match="unknown channel") as exc_info: From 0f417c29485c99029bc5b4cba53c78d0d43ffd6a Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Tue, 13 Feb 2024 23:01:25 +0200 Subject: [PATCH 28/55] simplify layout --- README.md | 28 +- cent/__init__.py | 10 +- cent/base.py | 60 ----- cent/client/async_client.py | 72 +++--- cent/client/session/aiohttp.py | 13 +- cent/client/session/base_http.py | 8 +- cent/client/session/base_http_async.py | 2 +- cent/client/session/base_http_sync.py | 2 +- cent/client/session/requests.py | 7 +- cent/client/sync_client.py | 70 +++-- cent/{requests.py => dto.py} | 338 +++++++++++++++++++++++-- cent/exceptions.py | 10 +- cent/results.py | 126 --------- cent/types.py | 145 ----------- tests/test_async_validation.py | 4 +- tests/test_sync_validation.py | 4 +- 16 files changed, 424 insertions(+), 475 deletions(-) delete mode 100644 cent/base.py rename cent/{requests.py => dto.py} (51%) delete mode 100644 cent/results.py delete mode 100644 cent/types.py diff --git a/README.md b/README.md index 0edc063..dcaa1ce 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,13 @@ from cent import Client Required init arguments: -* `api_url` (str) - Centrifugo HTTP API URL address -* `api_key` (str) - Centrifugo HTTP API key +* `api_url` (`str`) - Centrifugo HTTP API URL address, for example, `http://localhost:8000/api` +* `api_key` (`str`) - Centrifugo HTTP API key Optional arguments: -* `request_timeout` (float) - base timeout for all requests in seconds, default is 10 seconds. -* `session` (requests.Session) - custom `requests` session to use. +* `timeout` (`float`) - base timeout for all requests in seconds, default is 10 seconds. +* `session` (`requests.Session`) - custom `requests` session to use. Example: @@ -56,13 +56,13 @@ from cent import AsyncClient Required init arguments: -* `api_url` (str) - Centrifugo HTTP API URL address -* `api_key` (str) - Centrifugo HTTP API key +* `api_url` (`str`) - Centrifugo HTTP API URL address, for example, `http://localhost:8000 +* `api_key` (`str`) - Centrifugo HTTP API key Optional arguments: -* `request_timeout` (float) - base timeout for all requests in seconds, default is 10 seconds. -* `session` (aiohttp.ClientSession) - custom `aiohttp` session to use. +* `timeout` (`float`) - base timeout for all requests in seconds, default is 10 seconds. +* `session` (`aiohttp.ClientSession`) - custom `aiohttp` session to use. Example: @@ -91,9 +91,9 @@ This library raises exceptions if sth goes wrong. All exceptions are subclasses * `CentNetworkError` - raised in case of network related errors (connection refused) * `CentTransportError` - raised in case of transport related errors (HTTP status code is not 2xx) * `CentTimeoutError` - raised in case of timeout -* `CentUnauthorizedError` - raised in case of unauthorized access +* `CentUnauthorizedError` - raised in case of unauthorized access (signal of invalid API key) * `CentDecodeError` - raised in case of server response decoding error -* `CentAPIError` - raised in case of API error (error returned by Centrifugo itself, you can inspect code and message in this case) +* `CentResponseError` - raised in case of API response error (i.e. error returned by Centrifugo itself, you can inspect code and message returned by Centrifugo in this case) ## For contributors @@ -123,3 +123,11 @@ To run benchmarks, run: ```bash make bench ``` + +## Migrate to Cent v5 + +Cent v5 contains the following notable changes compared to Cent v4: + +* Constructor slightly changed, refer to the examples above. +* Base exception class is now `CentError` instead of `CentException`, exceptions SDK raises were refactored. +* To send multiple commands in one HTTP request SDK provides `batch` method. diff --git a/cent/__init__.py b/cent/__init__.py index 8828aec..7659bdc 100644 --- a/cent/__init__.py +++ b/cent/__init__.py @@ -2,7 +2,7 @@ Client, AsyncClient, ) -from cent.requests import ( +from cent.dto import ( BroadcastRequest, PublishRequest, SubscribeRequest, @@ -16,8 +16,6 @@ DisconnectRequest, InfoRequest, BatchRequest, -) -from cent.results import ( PublishResult, BroadcastResult, SubscribeResult, @@ -31,8 +29,6 @@ DisconnectResult, InfoResult, BatchResult, -) -from cent.types import ( StreamPosition, ChannelOptionsOverride, Disconnect, @@ -48,7 +44,7 @@ CentTransportError, CentUnauthorizedError, CentDecodeError, - CentAPIError, + CentResponseError, ) __all__ = ( @@ -58,10 +54,10 @@ "BoolValue", "BroadcastRequest", "BroadcastResult", - "CentAPIError", "CentDecodeError", "CentError", "CentNetworkError", + "CentResponseError", "CentTransportError", "CentUnauthorizedError", "ChannelOptionsOverride", diff --git a/cent/base.py b/cent/base.py deleted file mode 100644 index 75d488d..0000000 --- a/cent/base.py +++ /dev/null @@ -1,60 +0,0 @@ -from abc import ABC, abstractmethod -from typing import TypeVar, Any, Generic, TYPE_CHECKING, ClassVar, Optional - -from pydantic import BaseModel, ConfigDict - - -CentType = TypeVar("CentType", bound=Any) - - -class Error(BaseModel): - code: int - message: str - - -class Response(BaseModel, Generic[CentType]): - error: Optional[Error] = None - result: Optional[CentType] = None - - -class CentResult(BaseModel, ABC): - model_config = ConfigDict( - use_enum_values=True, - extra="allow", - validate_assignment=True, - frozen=True, - populate_by_name=True, - arbitrary_types_allowed=True, - defer_build=True, - ) - - -class CentRequest(BaseModel, Generic[CentType], ABC): - model_config = ConfigDict( - extra="allow", - populate_by_name=True, - arbitrary_types_allowed=True, - ) - - if TYPE_CHECKING: - __returning__: ClassVar[type] - __api_method__: ClassVar[str] - else: - - @property - @abstractmethod - def __returning__(self) -> type: - pass - - @property - @abstractmethod - def __api_method__(self) -> str: - pass - - -class NestedModel(BaseModel, ABC): - model_config = ConfigDict( - extra="allow", - populate_by_name=True, - arbitrary_types_allowed=True, - ) diff --git a/cent/client/async_client.py b/cent/client/async_client.py index c0c1718..791fd54 100644 --- a/cent/client/async_client.py +++ b/cent/client/async_client.py @@ -3,8 +3,8 @@ from aiohttp import ClientSession from cent.client.session import AiohttpSession -from cent.base import CentRequest -from cent.requests import ( +from cent.dto import ( + CentRequest, BroadcastRequest, PublishRequest, SubscribeRequest, @@ -18,8 +18,6 @@ DisconnectRequest, InfoRequest, BatchRequest, -) -from cent.results import ( BatchResult, PublishResult, BroadcastResult, @@ -33,8 +31,6 @@ ChannelsResult, DisconnectResult, InfoResult, -) -from cent.types import ( StreamPosition, ChannelOptionsOverride, Disconnect, @@ -49,19 +45,19 @@ def __init__( self, api_url: str, api_key: str, - request_timeout: Optional[float] = 10.0, + timeout: Optional[float] = 10.0, session: Optional[ClientSession] = None, ) -> None: """ :param api_url: Centrifugo API URL :param api_key: Centrifugo API key - :param request_timeout: Base timeout for all requests + :param timeout: Base timeout for all requests :param session: Custom `aiohttp` session """ self._api_key = api_key self._session = AiohttpSession( api_url, - timeout=request_timeout, + timeout=timeout, session=session, ) @@ -73,7 +69,7 @@ async def publish( tags: Optional[Dict[str, str]] = None, b64data: Optional[str] = None, idempotency_key: Optional[str] = None, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> PublishResult: call = PublishRequest( channel=channel, @@ -83,7 +79,7 @@ async def publish( b64data=b64data, idempotency_key=idempotency_key, ) - return await self(call, request_timeout=request_timeout) + return await self(call, timeout=timeout) async def broadcast( self, @@ -93,7 +89,7 @@ async def broadcast( tags: Optional[Dict[str, str]] = None, b64data: Optional[str] = None, idempotency_key: Optional[str] = None, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> BroadcastResult: call = BroadcastRequest( channels=channels, @@ -103,7 +99,7 @@ async def broadcast( b64data=b64data, idempotency_key=idempotency_key, ) - return await self(call, request_timeout=request_timeout) + return await self(call, timeout=timeout) async def subscribe( self, @@ -117,7 +113,7 @@ async def subscribe( b64data: Optional[str] = None, recover_since: Optional[StreamPosition] = None, override: Optional[ChannelOptionsOverride] = None, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> SubscribeResult: call = SubscribeRequest( user=user, @@ -131,7 +127,7 @@ async def subscribe( recover_since=recover_since, override=override, ) - return await self(call, request_timeout=request_timeout) + return await self(call, timeout=timeout) async def unsubscribe( self, @@ -139,7 +135,7 @@ async def unsubscribe( channel: str, client: Optional[str] = None, session: Optional[str] = None, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> UnsubscribeResult: call = UnsubscribeRequest( user=user, @@ -147,27 +143,27 @@ async def unsubscribe( client=client, session=session, ) - return await self(call, request_timeout=request_timeout) + return await self(call, timeout=timeout) async def presence( self, channel: str, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> PresenceResult: call = PresenceRequest( channel=channel, ) - return await self(call, request_timeout=request_timeout) + return await self(call, timeout=timeout) async def presence_stats( self, channel: str, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> PresenceStatsResult: call = PresenceStatsRequest( channel=channel, ) - return await self(call, request_timeout=request_timeout) + return await self(call, timeout=timeout) async def history( self, @@ -175,7 +171,7 @@ async def history( limit: Optional[int] = None, since: Optional[StreamPosition] = None, reverse: Optional[bool] = None, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> HistoryResult: call = HistoryRequest( channel=channel, @@ -183,17 +179,17 @@ async def history( since=since, reverse=reverse, ) - return await self(call, request_timeout=request_timeout) + return await self(call, timeout=timeout) async def history_remove( self, channel: str, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> HistoryRemoveResult: call = HistoryRemoveRequest( channel=channel, ) - return await self(call, request_timeout=request_timeout) + return await self(call, timeout=timeout) async def refresh( self, @@ -202,7 +198,7 @@ async def refresh( session: Optional[str] = None, expire_at: Optional[int] = None, expired: Optional[bool] = None, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> RefreshResult: call = RefreshRequest( user=user, @@ -211,17 +207,17 @@ async def refresh( expire_at=expire_at, expired=expired, ) - return await self(call, request_timeout=request_timeout) + return await self(call, timeout=timeout) async def channels( self, pattern: Optional[str] = None, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> ChannelsResult: call = ChannelsRequest( pattern=pattern, ) - return await self(call, request_timeout=request_timeout) + return await self(call, timeout=timeout) async def disconnect( self, @@ -230,7 +226,7 @@ async def disconnect( session: Optional[str] = None, whitelist: Optional[List[str]] = None, disconnect: Optional[Disconnect] = None, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> DisconnectResult: call = DisconnectRequest( user=user, @@ -239,37 +235,35 @@ async def disconnect( whitelist=whitelist, disconnect=disconnect, ) - return await self(call, request_timeout=request_timeout) + return await self(call, timeout=timeout) async def info( self, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> InfoResult: call = InfoRequest() - return await self(call, request_timeout=request_timeout) + return await self(call, timeout=timeout) async def batch( self, commands: List[CentRequest[Any]], parallel: Optional[bool] = None, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> BatchResult: call = BatchRequest.model_construct(commands=commands, parallel=parallel) - return await self(call, request_timeout=request_timeout) + return await self(call, timeout=timeout) async def close(self) -> None: await self._session.close() - async def __call__( - self, request: CentRequest[T], request_timeout: Optional[float] = None - ) -> T: + async def __call__(self, request: CentRequest[T], timeout: Optional[float] = None) -> T: """ Call API method :param request: Centrifugo request :return: Centrifugo response """ - return await self._session(self._api_key, request, timeout=request_timeout) + return await self._session(self._api_key, request, timeout=timeout) async def __aenter__(self) -> "AsyncClient": return self diff --git a/cent/client/session/aiohttp.py b/cent/client/session/aiohttp.py index 792acf4..1cbab00 100644 --- a/cent/client/session/aiohttp.py +++ b/cent/client/session/aiohttp.py @@ -1,11 +1,10 @@ import asyncio from typing import Optional -from aiohttp import ClientSession, ClientError, ClientTimeout +from aiohttp import ClientSession, ClientError from cent.client.session.base_http_async import BaseHttpAsyncSession -from cent.base import CentType, CentRequest -from cent.requests import BatchRequest +from cent.dto import CentType, CentRequest, BatchRequest from cent.exceptions import CentNetworkError, CentTimeoutError @@ -23,13 +22,7 @@ def __init__( if session: self._session = session else: - self._session = ClientSession( - headers={ - "User-Agent": "centrifugal/pycent", - "Content-Type": "application/json", - }, - timeout=ClientTimeout(total=self._timeout), - ) + self._session = ClientSession() async def close(self) -> None: if self._session is not None and not self._session.closed: diff --git a/cent/client/session/base_http.py b/cent/client/session/base_http.py index f3e58e1..d81c5ad 100644 --- a/cent/client/session/base_http.py +++ b/cent/client/session/base_http.py @@ -6,16 +6,16 @@ from cent.exceptions import ( CentDecodeError, - CentAPIError, + CentResponseError, CentUnauthorizedError, CentTransportError, ) -from cent.base import ( +from cent.dto import ( CentRequest, CentType, Response, + BatchRequest, ) -from cent.requests import BatchRequest class BaseHttpSession: @@ -76,7 +76,7 @@ def check_response( raise CentDecodeError from err if response.error: - raise CentAPIError( + raise CentResponseError( request=request, code=response.error.code, message=response.error.message, diff --git a/cent/client/session/base_http_async.py b/cent/client/session/base_http_async.py index 56d0ce8..b0fb057 100644 --- a/cent/client/session/base_http_async.py +++ b/cent/client/session/base_http_async.py @@ -2,7 +2,7 @@ from typing import Optional from cent.client.session.base_http import BaseHttpSession -from cent.base import CentType, CentRequest +from cent.dto import CentType, CentRequest class BaseHttpAsyncSession(BaseHttpSession, ABC): diff --git a/cent/client/session/base_http_sync.py b/cent/client/session/base_http_sync.py index 1d535af..5770817 100644 --- a/cent/client/session/base_http_sync.py +++ b/cent/client/session/base_http_sync.py @@ -2,7 +2,7 @@ from typing import Optional from cent.client.session.base_http import BaseHttpSession -from cent.base import CentType, CentRequest +from cent.dto import CentType, CentRequest class BaseHttpSyncSession(BaseHttpSession, ABC): diff --git a/cent/client/session/requests.py b/cent/client/session/requests.py index d5cc03e..570095a 100644 --- a/cent/client/session/requests.py +++ b/cent/client/session/requests.py @@ -4,8 +4,7 @@ from requests import Session from cent.client.session.base_http_sync import BaseHttpSyncSession -from cent.base import CentType, CentRequest -from cent.requests import BatchRequest +from cent.dto import CentType, CentRequest, BatchRequest from cent.exceptions import CentNetworkError, CentTimeoutError @@ -24,10 +23,6 @@ def __init__( self._session = session else: self._session = Session() - self._session.headers.update({ - "User-Agent": "centrifugal/pycent", - "Content-Type": "application/json", - }) def close(self) -> None: if self._session is not None: diff --git a/cent/client/sync_client.py b/cent/client/sync_client.py index 50c1ac4..b146cfa 100644 --- a/cent/client/sync_client.py +++ b/cent/client/sync_client.py @@ -3,8 +3,8 @@ from requests import Session from cent.client.session import RequestsSession -from cent.base import CentRequest -from cent.requests import ( +from cent.dto import ( + CentRequest, BroadcastRequest, PublishRequest, SubscribeRequest, @@ -18,8 +18,6 @@ DisconnectRequest, InfoRequest, BatchRequest, -) -from cent.results import ( BatchResult, PublishResult, BroadcastResult, @@ -33,8 +31,6 @@ ChannelsResult, DisconnectResult, InfoResult, -) -from cent.types import ( StreamPosition, ChannelOptionsOverride, Disconnect, @@ -49,13 +45,13 @@ def __init__( self, api_url: str, api_key: str, - request_timeout: Optional[float] = 10.0, + timeout: Optional[float] = 10.0, session: Optional[Session] = None, ) -> None: """ :param api_url: Centrifugo API URL :param api_key: Centrifugo API key - :param request_timeout: Base timeout for all requests. + :param timeout: Base timeout for all requests. :param session: Custom `requests` session. """ @@ -63,7 +59,7 @@ def __init__( self._api_key = api_key self._session = RequestsSession( api_url, - timeout=request_timeout, + timeout=timeout, session=session, ) @@ -75,7 +71,7 @@ def publish( tags: Optional[Dict[str, str]] = None, b64data: Optional[str] = None, idempotency_key: Optional[str] = None, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> PublishResult: call = PublishRequest( channel=channel, @@ -85,7 +81,7 @@ def publish( b64data=b64data, idempotency_key=idempotency_key, ) - return self(call, request_timeout=request_timeout) + return self(call, timeout=timeout) def broadcast( self, @@ -95,7 +91,7 @@ def broadcast( tags: Optional[Dict[str, str]] = None, b64data: Optional[str] = None, idempotency_key: Optional[str] = None, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> BroadcastResult: call = BroadcastRequest( channels=channels, @@ -105,7 +101,7 @@ def broadcast( b64data=b64data, idempotency_key=idempotency_key, ) - return self(call, request_timeout=request_timeout) + return self(call, timeout=timeout) def subscribe( self, @@ -119,7 +115,7 @@ def subscribe( b64data: Optional[str] = None, recover_since: Optional[StreamPosition] = None, override: Optional[ChannelOptionsOverride] = None, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> SubscribeResult: call = SubscribeRequest( user=user, @@ -133,7 +129,7 @@ def subscribe( recover_since=recover_since, override=override, ) - return self(call, request_timeout=request_timeout) + return self(call, timeout=timeout) def unsubscribe( self, @@ -141,7 +137,7 @@ def unsubscribe( channel: str, client: Optional[str] = None, session: Optional[str] = None, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> UnsubscribeResult: call = UnsubscribeRequest( user=user, @@ -149,27 +145,27 @@ def unsubscribe( client=client, session=session, ) - return self(call, request_timeout=request_timeout) + return self(call, timeout=timeout) def presence( self, channel: str, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> PresenceResult: call = PresenceRequest( channel=channel, ) - return self(call, request_timeout=request_timeout) + return self(call, timeout=timeout) def presence_stats( self, channel: str, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> PresenceStatsResult: call = PresenceStatsRequest( channel=channel, ) - return self(call, request_timeout=request_timeout) + return self(call, timeout=timeout) def history( self, @@ -177,7 +173,7 @@ def history( limit: Optional[int] = None, since: Optional[StreamPosition] = None, reverse: Optional[bool] = None, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> HistoryResult: call = HistoryRequest( channel=channel, @@ -185,17 +181,17 @@ def history( since=since, reverse=reverse, ) - return self(call, request_timeout=request_timeout) + return self(call, timeout=timeout) def history_remove( self, channel: str, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> HistoryRemoveResult: call = HistoryRemoveRequest( channel=channel, ) - return self(call, request_timeout=request_timeout) + return self(call, timeout=timeout) def refresh( self, @@ -204,7 +200,7 @@ def refresh( session: Optional[str] = None, expired: Optional[bool] = None, expire_at: Optional[int] = None, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> RefreshResult: call = RefreshRequest( user=user, @@ -213,17 +209,17 @@ def refresh( expired=expired, expire_at=expire_at, ) - return self(call, request_timeout=request_timeout) + return self(call, timeout=timeout) def channels( self, pattern: Optional[str] = None, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> ChannelsResult: call = ChannelsRequest( pattern=pattern, ) - return self(call, request_timeout=request_timeout) + return self(call, timeout=timeout) def disconnect( self, @@ -232,7 +228,7 @@ def disconnect( session: Optional[str] = None, whitelist: Optional[List[str]] = None, disconnect: Optional[Disconnect] = None, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> DisconnectResult: call = DisconnectRequest( user=user, @@ -241,35 +237,35 @@ def disconnect( whitelist=whitelist, disconnect=disconnect, ) - return self(call, request_timeout=request_timeout) + return self(call, timeout=timeout) def info( self, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> InfoResult: call = InfoRequest() - return self(call, request_timeout=request_timeout) + return self(call, timeout=timeout) def batch( self, commands: List[CentRequest[Any]], parallel: Optional[bool] = False, - request_timeout: Optional[float] = None, + timeout: Optional[float] = None, ) -> BatchResult: call = BatchRequest.model_construct(commands=commands, parallel=parallel) - return self(call, request_timeout=request_timeout) + return self(call, timeout=timeout) def close(self) -> None: self._session.close() - def __call__(self, request: CentRequest[T], request_timeout: Optional[float] = None) -> T: + def __call__(self, request: CentRequest[T], timeout: Optional[float] = None) -> T: """ Call API method :param request: Centrifugo request :return: Centrifugo response """ - return self._session(self._api_key, request, timeout=request_timeout) + return self._session(self._api_key, request, timeout=timeout) def __enter__(self) -> "Client": return self diff --git a/cent/requests.py b/cent/dto.py similarity index 51% rename from cent/requests.py rename to cent/dto.py index 665448d..5bc43c2 100644 --- a/cent/requests.py +++ b/cent/dto.py @@ -1,23 +1,321 @@ -from typing import Any, Optional, Dict, List - -from cent.base import CentRequest -from cent.types import StreamPosition, ChannelOptionsOverride, Disconnect - -from cent.results import ( - BatchResult, - BroadcastResult, - ChannelsResult, - DisconnectResult, - HistoryResult, - HistoryRemoveResult, - InfoResult, - PresenceResult, - PresenceStatsResult, - PublishResult, - RefreshResult, - SubscribeResult, - UnsubscribeResult, -) +from abc import ABC, abstractmethod +from typing import TypeVar, Any, Generic, TYPE_CHECKING, ClassVar, Optional, List, Dict +from pydantic import BaseModel, ConfigDict, Field + + +class Error(BaseModel): + code: int + message: str + + +CentType = TypeVar("CentType", bound=Any) + + +class Response(BaseModel, Generic[CentType]): + error: Optional[Error] = None + result: Optional[CentType] = None + + +class CentRequest(BaseModel, Generic[CentType], ABC): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + arbitrary_types_allowed=True, + ) + + if TYPE_CHECKING: + __returning__: ClassVar[type] + __api_method__: ClassVar[str] + else: + + @property + @abstractmethod + def __returning__(self) -> type: + pass + + @property + @abstractmethod + def __api_method__(self) -> str: + pass + + +class CentResult(BaseModel, ABC): + model_config = ConfigDict( + use_enum_values=True, + extra="allow", + validate_assignment=True, + frozen=True, + populate_by_name=True, + arbitrary_types_allowed=True, + defer_build=True, + ) + + +class NestedModel(BaseModel, ABC): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + arbitrary_types_allowed=True, + ) + + +class Disconnect(NestedModel): + """Disconnect data. + + Attributes: + code (int): Disconnect code. + reason (str): Disconnect reason. + """ + + code: int + reason: str + + +class BoolValue(NestedModel): + """Bool value. + + Attributes: + value (bool): Value. + """ + + value: bool + + +class StreamPosition(NestedModel): + """ + Stream position representation. + + Attributes: + offset (int): Offset of publication in history stream. + epoch (str): Epoch of current stream. + """ + + offset: int + epoch: str + + +class ChannelOptionsOverride(NestedModel): + """ + Override object for channel options. + + Attributes: + presence (Optional[BoolValue]): Override for presence. + join_leave (Optional[BoolValue]): Override for join_leave behavior. + force_push_join_leave (Optional[BoolValue]): Force push for join_leave events. + force_positioning (Optional[BoolValue]): Override for force positioning. + force_recovery (Optional[BoolValue]): Override for force recovery. + """ + + presence: Optional[BoolValue] = None + join_leave: Optional[BoolValue] = None + force_push_join_leave: Optional[BoolValue] = None + force_positioning: Optional[BoolValue] = None + force_recovery: Optional[BoolValue] = None + + +class ProcessStats(CentResult): + """ + Represents statistics of a process. + + Attributes: + cpu (float): Process CPU usage as a percentage. Defaults to 0.0. + rss (int): Process Resident Set Size (RSS) in bytes. + """ + + cpu: float = Field(default=0.0) + rss: int + + +class ClientInfo(CentResult): + """ + Represents the result containing client information. + + Attributes: + client (str): Client ID. + user (str): User ID. + conn_info (Optional[Any]): Optional connection info. This can include details + such as IP address, location, etc. + chan_info (Optional[Any]): Optional channel info. This might include specific + settings or preferences related to the channel. + """ + + client: str + user: str + conn_info: Optional[Any] = None + chan_info: Optional[Any] = None + + +class Publication(CentResult): + """Publication result. + + Attributes: + offset (int): Offset of publication in history stream. + data (Any): Custom JSON inside publication. + tags (Optional[Dict[str, str]]): Tags are optional. + """ + + data: Any + offset: int = Field(default=0) + tags: Optional[Dict[str, str]] = None + + +class Metrics(CentResult): + """Metrics result. + + Attributes: + interval (float): Metrics aggregation interval. + items (Dict[str, float]): metric values. + """ + + interval: float = Field(default=0.0) + items: Dict[str, float] + + +class Node(CentResult): + """Node result. + + Attributes: + uid (str): Node unique identifier. + name (str): Node name. + version (str): Node version. + num_clients (int): Total number of connections. + num_subs (int): Total number of subscriptions. + num_users (int): Total number of users. + num_channels (int): Total number of channels. + uptime (int): Node uptime. + metrics (Optional[Metrics]): Node metrics. + process (Optional[ProcessStats]): Node process stats. + """ + + uid: str + name: str + version: str + num_clients: int = Field(default=0) + num_subs: int = Field(default=0) + num_users: int = Field(default=0) + num_channels: int = Field(default=0) + uptime: int = Field(default=0) + metrics: Optional[Metrics] = None + process: Optional[ProcessStats] = None + + +class BatchResult(CentResult): + """Batch response. + + Attributes: + replies: List of results from batch request. + """ + + replies: List[Any] + + +class PublishResult(CentResult): + """Publish result. + + Attributes: + offset: Offset of publication in history stream. + epoch: Epoch of current stream. + """ + + offset: Optional[int] = None + epoch: Optional[str] = None + + +class BroadcastResult(CentResult): + """Broadcast result. + + Attributes: + responses: List of responses for each individual publish + (with possible error and publish result) + """ + + responses: List[Response[PublishResult]] = Field(default_factory=list) + + +class ChannelInfoResult(CentResult): + """Channel info result. + + Attributes: + num_clients: Total number of connections currently subscribed to a channel. + """ + + num_clients: int = Field(default=0) + + +class ChannelsResult(CentResult): + """Channels result. + + Attributes: + channels: Map where key is channel and value is ChannelInfoResult. + """ + + channels: Dict[str, ChannelInfoResult] + + +class DisconnectResult(CentResult): + """Disconnect result.""" + + +class HistoryRemoveResult(CentResult): + """History remove result.""" + + +class HistoryResult(CentResult): + """History result. + + Attributes: + publications: List of publications in channel. + offset: Top offset in history stream. + epoch: Epoch of current stream. + """ + + publications: List[Publication] = Field(default_factory=list) + offset: Optional[int] = None + epoch: Optional[str] = None + + +class InfoResult(CentResult): + """Info result. + + Attributes: + nodes: Information about all nodes in a cluster. + """ + + nodes: List[Node] + + +class PresenceResult(CentResult): + """Presence result. + + Attributes: + presence: Map where key is client ID and value is ClientInfo. + """ + + presence: Dict[str, ClientInfo] + + +class PresenceStatsResult(CentResult): + """Presence stats result. + + Attributes: + num_clients: Total number of clients in channel. + num_users: Total number of unique users in channel. + """ + + num_clients: int = Field(default=0) + num_users: int = Field(default=0) + + +class RefreshResult(CentResult): + """Refresh result.""" + + +class SubscribeResult(CentResult): + """Subscribe result.""" + + +class UnsubscribeResult(CentResult): + """Unsubscribe result.""" class BatchRequest(CentRequest[BatchResult]): diff --git a/cent/exceptions.py b/cent/exceptions.py index bc0449d..de1e01b 100644 --- a/cent/exceptions.py +++ b/cent/exceptions.py @@ -1,4 +1,4 @@ -from cent.base import CentType, CentRequest +from cent.dto import CentType, CentRequest class CentError(Exception): @@ -8,7 +8,7 @@ class CentError(Exception): class CentNetworkError(CentError): - """CentNetworkError raised when Centrifugo is not available.""" + """CentNetworkError raised when Centrifugo is unreachable or not available.""" def __init__(self, request: CentRequest[CentType], message: str) -> None: self.request = request @@ -22,7 +22,7 @@ def __repr__(self) -> str: class CentTransportError(CentError): - """CentTransportError raised when returns non-200 status code.""" + """CentTransportError raised when HTTP request results into non-200 status code.""" def __init__(self, request: CentRequest[CentType], status_code: int): self.request = request @@ -61,7 +61,7 @@ class CentDecodeError(CentError): """ -class CentAPIError(CentError): +class CentResponseError(CentError): """ CentAPIError raised when response from Centrifugo contains any error as a result of API command execution. @@ -73,7 +73,7 @@ def __init__(self, request: CentRequest[CentType], code: int, message: str) -> N self.message = message def __str__(self) -> str: - return f"API error #{self.code}: {self.message}" + return f"Server API response error #{self.code}: {self.message}" def __repr__(self) -> str: return f"{type(self).__name__}('{self}')" diff --git a/cent/results.py b/cent/results.py deleted file mode 100644 index 4599ef2..0000000 --- a/cent/results.py +++ /dev/null @@ -1,126 +0,0 @@ -from typing import List, Any, Optional, Dict - -from pydantic import Field - -from cent.base import CentResult -from cent.base import Response -from cent.types import Publication, Node, ClientInfo - - -class BatchResult(CentResult): - """Batch response. - - Attributes: - replies: List of results from batch request. - """ - - replies: List[Any] - - -class PublishResult(CentResult): - """Publish result. - - Attributes: - offset: Offset of publication in history stream. - epoch: Epoch of current stream. - """ - - offset: Optional[int] = None - epoch: Optional[str] = None - - -class BroadcastResult(CentResult): - """Broadcast result. - - Attributes: - responses: List of responses for each individual publish - (with possible error and publish result) - """ - - responses: List[Response[PublishResult]] = Field(default_factory=list) - - -class ChannelInfoResult(CentResult): - """Channel info result. - - Attributes: - num_clients: Total number of connections currently subscribed to a channel. - """ - - num_clients: int = Field(default=0) - - -class ChannelsResult(CentResult): - """Channels result. - - Attributes: - channels: Map where key is channel and value is ChannelInfoResult. - """ - - channels: Dict[str, ChannelInfoResult] - - -class DisconnectResult(CentResult): - """Disconnect result.""" - - -class HistoryRemoveResult(CentResult): - """History remove result.""" - - -class HistoryResult(CentResult): - """History result. - - Attributes: - publications: List of publications in channel. - offset: Top offset in history stream. - epoch: Epoch of current stream. - """ - - publications: List[Publication] = Field(default_factory=list) - offset: Optional[int] = None - epoch: Optional[str] = None - - -class InfoResult(CentResult): - """Info result. - - Attributes: - nodes: Information about all nodes in a cluster. - """ - - nodes: List[Node] - - -class PresenceResult(CentResult): - """Presence result. - - Attributes: - presence: Map where key is client ID and value is ClientInfo. - """ - - presence: Dict[str, ClientInfo] - - -class PresenceStatsResult(CentResult): - """Presence stats result. - - Attributes: - num_clients: Total number of clients in channel. - num_users: Total number of unique users in channel. - """ - - num_clients: int = Field(default=0) - num_users: int = Field(default=0) - - -class RefreshResult(CentResult): - """Refresh result.""" - - -class SubscribeResult(CentResult): - """Subscribe result.""" - - -class UnsubscribeResult(CentResult): - """Unsubscribe result.""" diff --git a/cent/types.py b/cent/types.py deleted file mode 100644 index 3f3480f..0000000 --- a/cent/types.py +++ /dev/null @@ -1,145 +0,0 @@ -from typing import Optional, Any, Dict - -from pydantic import Field - -from cent.base import CentResult, NestedModel - - -class Disconnect(NestedModel): - """Disconnect data. - - Attributes: - code (int): Disconnect code. - reason (str): Disconnect reason. - """ - - code: int - reason: str - - -class BoolValue(NestedModel): - """Bool value. - - Attributes: - value (bool): Value. - """ - - value: bool - - -class StreamPosition(NestedModel): - """ - Stream position representation. - - Attributes: - offset (int): Offset of publication in history stream. - epoch (str): Epoch of current stream. - """ - - offset: int - epoch: str - - -class ChannelOptionsOverride(NestedModel): - """ - Override object for channel options. - - Attributes: - presence (Optional[BoolValue]): Override for presence. - join_leave (Optional[BoolValue]): Override for join_leave behavior. - force_push_join_leave (Optional[BoolValue]): Force push for join_leave events. - force_positioning (Optional[BoolValue]): Override for force positioning. - force_recovery (Optional[BoolValue]): Override for force recovery. - """ - - presence: Optional[BoolValue] = None - join_leave: Optional[BoolValue] = None - force_push_join_leave: Optional[BoolValue] = None - force_positioning: Optional[BoolValue] = None - force_recovery: Optional[BoolValue] = None - - -class ProcessStats(CentResult): - """ - Represents statistics of a process. - - Attributes: - cpu (float): Process CPU usage as a percentage. Defaults to 0.0. - rss (int): Process Resident Set Size (RSS) in bytes. - """ - - cpu: float = Field(default=0.0) - rss: int - - -class ClientInfo(CentResult): - """ - Represents the result containing client information. - - Attributes: - client (str): Client ID. - user (str): User ID. - conn_info (Optional[Any]): Optional connection info. This can include details - such as IP address, location, etc. - chan_info (Optional[Any]): Optional channel info. This might include specific - settings or preferences related to the channel. - """ - - client: str - user: str - conn_info: Optional[Any] = None - chan_info: Optional[Any] = None - - -class Publication(CentResult): - """Publication result. - - Attributes: - offset (int): Offset of publication in history stream. - data (Any): Custom JSON inside publication. - tags (Optional[Dict[str, str]]): Tags are optional. - """ - - data: Any - offset: int = Field(default=0) - tags: Optional[Dict[str, str]] = None - - -class Metrics(CentResult): - """Metrics result. - - Attributes: - interval (float): Metrics aggregation interval. - items (Dict[str, float]): metric values. - """ - - interval: float = Field(default=0.0) - items: Dict[str, float] - - -class Node(CentResult): - """Node result. - - Attributes: - uid (str): Node unique identifier. - name (str): Node name. - version (str): Node version. - num_clients (int): Total number of connections. - num_subs (int): Total number of subscriptions. - num_users (int): Total number of users. - num_channels (int): Total number of channels. - uptime (int): Node uptime. - metrics (Optional[Metrics]): Node metrics. - process (Optional[ProcessStats]): Node process stats. - """ - - uid: str - name: str - version: str - num_clients: int = Field(default=0) - num_subs: int = Field(default=0) - num_users: int = Field(default=0) - num_channels: int = Field(default=0) - uptime: int = Field(default=0) - metrics: Optional[Metrics] = None - process: Optional[ProcessStats] = None diff --git a/tests/test_async_validation.py b/tests/test_async_validation.py index b2b70fb..c0bbe5e 100644 --- a/tests/test_async_validation.py +++ b/tests/test_async_validation.py @@ -3,7 +3,7 @@ from cent import ( AsyncClient, - CentAPIError, + CentResponseError, PublishRequest, StreamPosition, Disconnect, @@ -154,7 +154,7 @@ async def test_batch(async_client: AsyncClient) -> None: async def test_error_publish(async_client: AsyncClient) -> None: - with pytest.raises(CentAPIError, match="unknown channel") as exc_info: + with pytest.raises(CentResponseError, match="unknown channel") as exc_info: await async_client.publish( "undefined_channel:123", {"data": "data"}, diff --git a/tests/test_sync_validation.py b/tests/test_sync_validation.py index 07a3aec..edc187e 100644 --- a/tests/test_sync_validation.py +++ b/tests/test_sync_validation.py @@ -3,7 +3,7 @@ from cent import ( Client, - CentAPIError, + CentResponseError, PublishRequest, BroadcastRequest, PresenceRequest, @@ -155,7 +155,7 @@ def test_batch(sync_client: Client) -> None: def test_error_publish(sync_client: Client) -> None: - with pytest.raises(CentAPIError, match="unknown channel") as exc_info: + with pytest.raises(CentResponseError, match="unknown channel") as exc_info: sync_client.publish( "undefined_channel:123", {"data": "data"}, From 085e736cb7de282022ebb99626fea012abfa30e8 Mon Sep 17 00:00:00 2001 From: FZambia Date: Sat, 17 Feb 2024 10:08:43 +0200 Subject: [PATCH 29/55] minor readme fixes --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index dcaa1ce..6436f56 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ pip install cent ## Usage -See the description of Centrifugo [server API](https://centrifugal.dev/docs/server/server_api) in the documentation. +First of all, see the description of Centrifugo [server API](https://centrifugal.dev/docs/server/server_api) in the documentation. This library contains `Client` and `AsyncClient` to work with Centrifugo HTTP server API. Both clients have the same methods to work with Centrifugo API and raise the same top-level exceptions. @@ -28,7 +28,7 @@ from cent import Client Required init arguments: * `api_url` (`str`) - Centrifugo HTTP API URL address, for example, `http://localhost:8000/api` -* `api_key` (`str`) - Centrifugo HTTP API key +* `api_key` (`str`) - Centrifugo HTTP API key for auth Optional arguments: @@ -56,8 +56,8 @@ from cent import AsyncClient Required init arguments: -* `api_url` (`str`) - Centrifugo HTTP API URL address, for example, `http://localhost:8000 -* `api_key` (`str`) - Centrifugo HTTP API key +* `api_url` (`str`) - Centrifugo HTTP API URL address, for example, `http://localhost:8000` +* `api_key` (`str`) - Centrifugo HTTP API key for auth Optional arguments: From 790bd30211f79c3a906ab6504ec0c68a92bb64ba Mon Sep 17 00:00:00 2001 From: FZambia Date: Sat, 17 Feb 2024 10:14:27 +0200 Subject: [PATCH 30/55] use CentApiResponseError name --- cent/__init__.py | 4 ++-- cent/client/session/base_http.py | 4 ++-- cent/exceptions.py | 6 +++--- tests/test_async_validation.py | 4 ++-- tests/test_sync_validation.py | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cent/__init__.py b/cent/__init__.py index 7659bdc..1b9241c 100644 --- a/cent/__init__.py +++ b/cent/__init__.py @@ -44,7 +44,7 @@ CentTransportError, CentUnauthorizedError, CentDecodeError, - CentResponseError, + CentApiResponseError, ) __all__ = ( @@ -54,10 +54,10 @@ "BoolValue", "BroadcastRequest", "BroadcastResult", + "CentApiResponseError", "CentDecodeError", "CentError", "CentNetworkError", - "CentResponseError", "CentTransportError", "CentUnauthorizedError", "ChannelOptionsOverride", diff --git a/cent/client/session/base_http.py b/cent/client/session/base_http.py index d81c5ad..21c5bcf 100644 --- a/cent/client/session/base_http.py +++ b/cent/client/session/base_http.py @@ -6,7 +6,7 @@ from cent.exceptions import ( CentDecodeError, - CentResponseError, + CentApiResponseError, CentUnauthorizedError, CentTransportError, ) @@ -76,7 +76,7 @@ def check_response( raise CentDecodeError from err if response.error: - raise CentResponseError( + raise CentApiResponseError( request=request, code=response.error.code, message=response.error.message, diff --git a/cent/exceptions.py b/cent/exceptions.py index de1e01b..436d3ad 100644 --- a/cent/exceptions.py +++ b/cent/exceptions.py @@ -61,10 +61,10 @@ class CentDecodeError(CentError): """ -class CentResponseError(CentError): +class CentApiResponseError(CentError): """ - CentAPIError raised when response from Centrifugo contains any error - as a result of API command execution. + CentApiResponseError raised when the response from Centrifugo server API contains + any error as a result of API command execution. """ def __init__(self, request: CentRequest[CentType], code: int, message: str) -> None: diff --git a/tests/test_async_validation.py b/tests/test_async_validation.py index c0bbe5e..1fb88eb 100644 --- a/tests/test_async_validation.py +++ b/tests/test_async_validation.py @@ -3,7 +3,7 @@ from cent import ( AsyncClient, - CentResponseError, + CentApiResponseError, PublishRequest, StreamPosition, Disconnect, @@ -154,7 +154,7 @@ async def test_batch(async_client: AsyncClient) -> None: async def test_error_publish(async_client: AsyncClient) -> None: - with pytest.raises(CentResponseError, match="unknown channel") as exc_info: + with pytest.raises(CentApiResponseError, match="unknown channel") as exc_info: await async_client.publish( "undefined_channel:123", {"data": "data"}, diff --git a/tests/test_sync_validation.py b/tests/test_sync_validation.py index edc187e..f3512b5 100644 --- a/tests/test_sync_validation.py +++ b/tests/test_sync_validation.py @@ -3,7 +3,7 @@ from cent import ( Client, - CentResponseError, + CentApiResponseError, PublishRequest, BroadcastRequest, PresenceRequest, @@ -155,7 +155,7 @@ def test_batch(sync_client: Client) -> None: def test_error_publish(sync_client: Client) -> None: - with pytest.raises(CentResponseError, match="unknown channel") as exc_info: + with pytest.raises(CentApiResponseError, match="unknown channel") as exc_info: sync_client.publish( "undefined_channel:123", {"data": "data"}, From 1377ea2ad202a7354c608702616a1996c9218312 Mon Sep 17 00:00:00 2001 From: FZambia Date: Sat, 17 Feb 2024 10:15:19 +0200 Subject: [PATCH 31/55] fix readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6436f56..07479c0 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ This library raises exceptions if sth goes wrong. All exceptions are subclasses * `CentTimeoutError` - raised in case of timeout * `CentUnauthorizedError` - raised in case of unauthorized access (signal of invalid API key) * `CentDecodeError` - raised in case of server response decoding error -* `CentResponseError` - raised in case of API response error (i.e. error returned by Centrifugo itself, you can inspect code and message returned by Centrifugo in this case) +* `CentApiResponseError` - raised in case of API response error (i.e. error returned by Centrifugo itself, you can inspect code and message returned by Centrifugo in this case) ## For contributors From 49f04b2423621e8e88d6dace9cd3d8ed02ea31d1 Mon Sep 17 00:00:00 2001 From: FZambia Date: Sat, 17 Feb 2024 13:41:27 +0200 Subject: [PATCH 32/55] refactor to send any request --- cent/client/async_client.py | 237 +------------- cent/client/sync_client.py | 237 +------------- cent/dto.py | 573 +++++++++++++++++++++++++++++++++ tests/conftest.py | 11 + tests/test_async_validation.py | 162 ---------- tests/test_sync_validation.py | 163 ---------- tests/test_validation.py | 233 ++++++++++++++ 7 files changed, 825 insertions(+), 791 deletions(-) delete mode 100644 tests/test_async_validation.py delete mode 100644 tests/test_sync_validation.py create mode 100644 tests/test_validation.py diff --git a/cent/client/async_client.py b/cent/client/async_client.py index 791fd54..8d78fdf 100644 --- a/cent/client/async_client.py +++ b/cent/client/async_client.py @@ -1,40 +1,9 @@ -from typing import List, Optional, Any, Dict, TypeVar +from typing import Optional, Any, TypeVar from aiohttp import ClientSession from cent.client.session import AiohttpSession -from cent.dto import ( - CentRequest, - BroadcastRequest, - PublishRequest, - SubscribeRequest, - UnsubscribeRequest, - PresenceRequest, - PresenceStatsRequest, - HistoryRequest, - HistoryRemoveRequest, - RefreshRequest, - ChannelsRequest, - DisconnectRequest, - InfoRequest, - BatchRequest, - BatchResult, - PublishResult, - BroadcastResult, - SubscribeResult, - UnsubscribeResult, - PresenceResult, - PresenceStatsResult, - HistoryResult, - HistoryRemoveResult, - RefreshResult, - ChannelsResult, - DisconnectResult, - InfoResult, - StreamPosition, - ChannelOptionsOverride, - Disconnect, -) +from cent.dto import CentRequest T = TypeVar("T") @@ -61,210 +30,12 @@ def __init__( session=session, ) - async def publish( - self, - channel: str, - data: Any, - skip_history: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - b64data: Optional[str] = None, - idempotency_key: Optional[str] = None, - timeout: Optional[float] = None, - ) -> PublishResult: - call = PublishRequest( - channel=channel, - data=data, - skip_history=skip_history, - tags=tags, - b64data=b64data, - idempotency_key=idempotency_key, - ) - return await self(call, timeout=timeout) - - async def broadcast( - self, - channels: List[str], - data: Any, - skip_history: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - b64data: Optional[str] = None, - idempotency_key: Optional[str] = None, - timeout: Optional[float] = None, - ) -> BroadcastResult: - call = BroadcastRequest( - channels=channels, - data=data, - skip_history=skip_history, - tags=tags, - b64data=b64data, - idempotency_key=idempotency_key, - ) - return await self(call, timeout=timeout) - - async def subscribe( - self, - user: str, - channel: str, - info: Optional[Any] = None, - b64info: Optional[str] = None, - client: Optional[str] = None, - session: Optional[str] = None, - data: Optional[Any] = None, - b64data: Optional[str] = None, - recover_since: Optional[StreamPosition] = None, - override: Optional[ChannelOptionsOverride] = None, - timeout: Optional[float] = None, - ) -> SubscribeResult: - call = SubscribeRequest( - user=user, - channel=channel, - info=info, - b64info=b64info, - client=client, - session=session, - data=data, - b64data=b64data, - recover_since=recover_since, - override=override, - ) - return await self(call, timeout=timeout) - - async def unsubscribe( - self, - user: str, - channel: str, - client: Optional[str] = None, - session: Optional[str] = None, - timeout: Optional[float] = None, - ) -> UnsubscribeResult: - call = UnsubscribeRequest( - user=user, - channel=channel, - client=client, - session=session, - ) - return await self(call, timeout=timeout) - - async def presence( - self, - channel: str, - timeout: Optional[float] = None, - ) -> PresenceResult: - call = PresenceRequest( - channel=channel, - ) - return await self(call, timeout=timeout) - - async def presence_stats( - self, - channel: str, - timeout: Optional[float] = None, - ) -> PresenceStatsResult: - call = PresenceStatsRequest( - channel=channel, - ) - return await self(call, timeout=timeout) - - async def history( - self, - channel: str, - limit: Optional[int] = None, - since: Optional[StreamPosition] = None, - reverse: Optional[bool] = None, - timeout: Optional[float] = None, - ) -> HistoryResult: - call = HistoryRequest( - channel=channel, - limit=limit, - since=since, - reverse=reverse, - ) - return await self(call, timeout=timeout) - - async def history_remove( - self, - channel: str, - timeout: Optional[float] = None, - ) -> HistoryRemoveResult: - call = HistoryRemoveRequest( - channel=channel, - ) - return await self(call, timeout=timeout) - - async def refresh( - self, - user: str, - client: Optional[str] = None, - session: Optional[str] = None, - expire_at: Optional[int] = None, - expired: Optional[bool] = None, - timeout: Optional[float] = None, - ) -> RefreshResult: - call = RefreshRequest( - user=user, - client=client, - session=session, - expire_at=expire_at, - expired=expired, - ) - return await self(call, timeout=timeout) - - async def channels( - self, - pattern: Optional[str] = None, - timeout: Optional[float] = None, - ) -> ChannelsResult: - call = ChannelsRequest( - pattern=pattern, - ) - return await self(call, timeout=timeout) - - async def disconnect( - self, - user: str, - client: Optional[str] = None, - session: Optional[str] = None, - whitelist: Optional[List[str]] = None, - disconnect: Optional[Disconnect] = None, - timeout: Optional[float] = None, - ) -> DisconnectResult: - call = DisconnectRequest( - user=user, - client=client, - session=session, - whitelist=whitelist, - disconnect=disconnect, - ) - return await self(call, timeout=timeout) - - async def info( - self, - timeout: Optional[float] = None, - ) -> InfoResult: - call = InfoRequest() - return await self(call, timeout=timeout) - - async def batch( - self, - commands: List[CentRequest[Any]], - parallel: Optional[bool] = None, - timeout: Optional[float] = None, - ) -> BatchResult: - call = BatchRequest.model_construct(commands=commands, parallel=parallel) - return await self(call, timeout=timeout) + async def send(self, request: CentRequest[T], timeout: Optional[float] = None) -> T: + return await self._session(self._api_key, request, timeout=timeout) async def close(self) -> None: await self._session.close() - async def __call__(self, request: CentRequest[T], timeout: Optional[float] = None) -> T: - """ - Call API method - - :param request: Centrifugo request - :return: Centrifugo response - """ - return await self._session(self._api_key, request, timeout=timeout) - async def __aenter__(self) -> "AsyncClient": return self diff --git a/cent/client/sync_client.py b/cent/client/sync_client.py index b146cfa..8de366a 100644 --- a/cent/client/sync_client.py +++ b/cent/client/sync_client.py @@ -1,40 +1,9 @@ -from typing import List, Optional, Any, Dict, TypeVar +from typing import Optional, Any, TypeVar from requests import Session from cent.client.session import RequestsSession -from cent.dto import ( - CentRequest, - BroadcastRequest, - PublishRequest, - SubscribeRequest, - UnsubscribeRequest, - PresenceRequest, - PresenceStatsRequest, - HistoryRequest, - HistoryRemoveRequest, - RefreshRequest, - ChannelsRequest, - DisconnectRequest, - InfoRequest, - BatchRequest, - BatchResult, - PublishResult, - BroadcastResult, - SubscribeResult, - UnsubscribeResult, - PresenceResult, - PresenceStatsResult, - HistoryResult, - HistoryRemoveResult, - RefreshResult, - ChannelsResult, - DisconnectResult, - InfoResult, - StreamPosition, - ChannelOptionsOverride, - Disconnect, -) +from cent.dto import CentRequest T = TypeVar("T") @@ -63,210 +32,12 @@ def __init__( session=session, ) - def publish( - self, - channel: str, - data: Any, - skip_history: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - b64data: Optional[str] = None, - idempotency_key: Optional[str] = None, - timeout: Optional[float] = None, - ) -> PublishResult: - call = PublishRequest( - channel=channel, - data=data, - skip_history=skip_history, - tags=tags, - b64data=b64data, - idempotency_key=idempotency_key, - ) - return self(call, timeout=timeout) - - def broadcast( - self, - channels: List[str], - data: Any, - skip_history: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - b64data: Optional[str] = None, - idempotency_key: Optional[str] = None, - timeout: Optional[float] = None, - ) -> BroadcastResult: - call = BroadcastRequest( - channels=channels, - data=data, - skip_history=skip_history, - tags=tags, - b64data=b64data, - idempotency_key=idempotency_key, - ) - return self(call, timeout=timeout) - - def subscribe( - self, - user: str, - channel: str, - info: Optional[Any] = None, - b64info: Optional[str] = None, - client: Optional[str] = None, - session: Optional[str] = None, - data: Optional[Any] = None, - b64data: Optional[str] = None, - recover_since: Optional[StreamPosition] = None, - override: Optional[ChannelOptionsOverride] = None, - timeout: Optional[float] = None, - ) -> SubscribeResult: - call = SubscribeRequest( - user=user, - channel=channel, - info=info, - b64info=b64info, - client=client, - session=session, - data=data, - b64data=b64data, - recover_since=recover_since, - override=override, - ) - return self(call, timeout=timeout) - - def unsubscribe( - self, - user: str, - channel: str, - client: Optional[str] = None, - session: Optional[str] = None, - timeout: Optional[float] = None, - ) -> UnsubscribeResult: - call = UnsubscribeRequest( - user=user, - channel=channel, - client=client, - session=session, - ) - return self(call, timeout=timeout) - - def presence( - self, - channel: str, - timeout: Optional[float] = None, - ) -> PresenceResult: - call = PresenceRequest( - channel=channel, - ) - return self(call, timeout=timeout) - - def presence_stats( - self, - channel: str, - timeout: Optional[float] = None, - ) -> PresenceStatsResult: - call = PresenceStatsRequest( - channel=channel, - ) - return self(call, timeout=timeout) - - def history( - self, - channel: str, - limit: Optional[int] = None, - since: Optional[StreamPosition] = None, - reverse: Optional[bool] = None, - timeout: Optional[float] = None, - ) -> HistoryResult: - call = HistoryRequest( - channel=channel, - limit=limit, - since=since, - reverse=reverse, - ) - return self(call, timeout=timeout) - - def history_remove( - self, - channel: str, - timeout: Optional[float] = None, - ) -> HistoryRemoveResult: - call = HistoryRemoveRequest( - channel=channel, - ) - return self(call, timeout=timeout) - - def refresh( - self, - user: str, - client: Optional[str] = None, - session: Optional[str] = None, - expired: Optional[bool] = None, - expire_at: Optional[int] = None, - timeout: Optional[float] = None, - ) -> RefreshResult: - call = RefreshRequest( - user=user, - client=client, - session=session, - expired=expired, - expire_at=expire_at, - ) - return self(call, timeout=timeout) - - def channels( - self, - pattern: Optional[str] = None, - timeout: Optional[float] = None, - ) -> ChannelsResult: - call = ChannelsRequest( - pattern=pattern, - ) - return self(call, timeout=timeout) - - def disconnect( - self, - user: str, - client: Optional[str] = None, - session: Optional[str] = None, - whitelist: Optional[List[str]] = None, - disconnect: Optional[Disconnect] = None, - timeout: Optional[float] = None, - ) -> DisconnectResult: - call = DisconnectRequest( - user=user, - client=client, - session=session, - whitelist=whitelist, - disconnect=disconnect, - ) - return self(call, timeout=timeout) - - def info( - self, - timeout: Optional[float] = None, - ) -> InfoResult: - call = InfoRequest() - return self(call, timeout=timeout) - - def batch( - self, - commands: List[CentRequest[Any]], - parallel: Optional[bool] = False, - timeout: Optional[float] = None, - ) -> BatchResult: - call = BatchRequest.model_construct(commands=commands, parallel=parallel) - return self(call, timeout=timeout) + def send(self, request: CentRequest[T], timeout: Optional[float] = None) -> T: + return self._session(self._api_key, request, timeout=timeout) def close(self) -> None: self._session.close() - def __call__(self, request: CentRequest[T], timeout: Optional[float] = None) -> T: - """ - Call API method - - :param request: Centrifugo request - :return: Centrifugo response - """ - return self._session(self._api_key, request, timeout=timeout) - def __enter__(self) -> "Client": return self diff --git a/cent/dto.py b/cent/dto.py index 5bc43c2..817d393 100644 --- a/cent/dto.py +++ b/cent/dto.py @@ -561,3 +561,576 @@ class UnsubscribeRequest(CentRequest[UnsubscribeResult]): channel: str client: Optional[str] = None session: Optional[str] = None + + +class ConnectionTokenInfo(NestedModel): + """Connection token info.""" + + uid: str + issued_at: int + + +class SubscriptionTokenInfo(NestedModel): + """Subscription token info.""" + + uid: str + issued_at: int + + +class ChannelContext(NestedModel): + """Channel context.""" + + source: int + + +class ConnectionState(NestedModel): + """ + Connection state. + """ + + channels: Dict[str, ChannelContext] + connection_token: ConnectionTokenInfo + subscription_tokens: Dict[str, SubscriptionTokenInfo] + meta: Any + + +class ConnectionInfo(NestedModel): + """Connection info.""" + + app_name: Optional[str] = "" + app_version: Optional[str] = "" + transport: str + protocol: str + user: str + state: ConnectionState + + +class ConnectionsResult(CentResult): + connections: Dict[str, ConnectionInfo] + + +class ConnectionsRequest(CentRequest[ConnectionsResult]): + """Connections request.""" + + __api_method__ = "connections" + __returning__ = ConnectionsResult + + user: str + expression: str + + +class UpdateUserStatusResult(CentResult): + """ + Update user status result. + """ + + +class UpdateUserStatusRequest(CentRequest[UpdateUserStatusResult]): + """Update user status request.""" + + __api_method__ = "update_user_status" + __returning__ = UpdateUserStatusResult + + users: List[str] + state: str + + +class UserStatus(NestedModel): + """ + User status. + """ + + user: str + active: int + online: int + state: str + + +class GetUserStatusResult(CentResult): + """ + Get user status result. + """ + + statuses: List[UserStatus] + + +class GetUserStatusRequest(CentRequest[GetUserStatusResult]): + """ + Get user status request. + """ + + __api_method__ = "get_user_status" + __returning__ = GetUserStatusResult + + users: List[str] + + +class DeleteUserStatusResult(CentResult): + """ + Delete user status result. + """ + + +class DeleteUserStatusRequest(CentRequest[DeleteUserStatusResult]): + """ + Delete user status request. + """ + + __api_method__ = "delete_user_status" + __returning__ = DeleteUserStatusResult + + users: List[str] + + +class BlockUserResult(CentResult): + """ + Block user result. + """ + + +class BlockUserRequest(CentRequest[BlockUserResult]): + """ + Block user request. + """ + + __api_method__ = "block_user" + __returning__ = BlockUserResult + + expire_at: int + user: str + + +class UnblockUserResult(CentResult): + """ + Unblock user result. + """ + + +class UnblockUserRequest(CentRequest[UnblockUserResult]): + """ + Unblock user request. + """ + + __api_method__ = "unblock_user" + __returning__ = UnblockUserResult + + user: str + + +class RevokeTokenResult(CentResult): + """ + Revoke token result. + """ + + +class RevokeTokenRequest(CentRequest[RevokeTokenResult]): + """ + Revoke token request. + """ + + __api_method__ = "revoke_token" + __returning__ = RevokeTokenResult + + expire_at: int + uid: str + + +class InvalidateUserTokensResult(CentResult): + """ + Invalidate user tokens result. + """ + + +class InvalidateUserTokensRequest(CentRequest[InvalidateUserTokensResult]): + """ + Invalidate user tokens request. + """ + + __api_method__ = "invalidate_user_tokens" + __returning__ = InvalidateUserTokensResult + + expire_at: int + user: str + issued_before: int + channel: Optional[str] = None + + +class DeviceRegisterResult(CentResult): + """ + Device register result. + """ + + +class DeviceRegisterRequest(CentRequest[DeviceRegisterResult]): + """ + Device register request. + """ + + __api_method__ = "device_register" + __returning__ = DeviceRegisterResult + + id: str + provider: str + token: str + platform: str + user: str + meta: Dict[str, str] + topics: List[str] + + +class DeviceUserUpdate(NestedModel): + """ + Device user update. + """ + + user: str + + +class DeviceMetaUpdate(NestedModel): + """ + Device meta update. + """ + + meta: Dict[str, str] + + +class DeviceTopicsUpdate(NestedModel): + """ + Device topics update. + """ + + op: str + topics: List[str] + + +class DeviceUpdateResult(CentResult): + """ + Device update result. + """ + + +class DeviceUpdateRequest(CentRequest[DeviceUpdateResult]): + """ + Device update request. + """ + + __api_method__ = "device_update" + __returning__ = DeviceUpdateResult + + ids: Optional[List[str]] = None + users: Optional[List[str]] = None + user_update: Optional[DeviceUserUpdate] = None + meta_update: Optional[DeviceMetaUpdate] = None + topics_update: Optional[DeviceTopicsUpdate] = None + + +class DeviceRemoveResult(CentResult): + """ + Device remove result. + """ + + +class DeviceRemoveRequest(CentRequest[DeviceRemoveResult]): + """ + Device remove request. + """ + + __api_method__ = "device_remove" + __returning__ = DeviceRemoveResult + + ids: List[str] + users: List[str] + + +class DeviceFilter(NestedModel): + """ + Device filter. + """ + + ids: List[str] + users: List[str] + topics: List[str] + providers: List[str] + platforms: List[str] + + +class Device(NestedModel): + """ + Device. + """ + + id: str + platform: str + provider: str + token: str + user: str + created_at: int + updated_at: int + meta: Dict[str, str] + topics: List[str] + + +class DeviceListResult(CentResult): + """ + Device list result. + """ + + items: List[Device] + next_cursor: str + total_count: int + + +class DeviceListRequest(CentRequest[DeviceListResult]): + """ + Device list request. + """ + + __api_method__ = "device_list" + __returning__ = DeviceListResult + + filter: DeviceFilter + include_total_count: bool + include_meta: bool + include_topics: bool + cursor: str + limit: int + + +class DeviceTopicFilter(NestedModel): + """ + Device topic filter. + """ + + device_ids: List[str] + device_providers: List[str] + device_platforms: List[str] + device_users: List[str] + topics: List[str] + topic_prefix: str + + +class DeviceTopic(NestedModel): + """ + Device topic. + """ + + id: str + topic: str + device: Device + + +class DeviceTopicListResult(CentResult): + """ + Device topic list result. + """ + + items: List[DeviceTopic] + next_cursor: str + total_count: int + + +class DeviceTopicListRequest(CentRequest[DeviceTopicListResult]): + """ + Device topic list request. + """ + + __api_method__ = "device_topic_list" + __returning__ = DeviceTopicListResult + + filter: DeviceTopicFilter + include_total_count: bool + include_device: bool + cursor: str + limit: int + + +class UserTopicFilter(NestedModel): + """ + User topic filter. + """ + + users: List[str] + topics: List[str] + topic_prefix: str + + +class UserTopic(NestedModel): + """ + User topic. + """ + + id: str + user: str + topic: str + + +class UserTopicListResult(CentResult): + """ + User topic list result. + """ + + items: List[UserTopic] + next_cursor: str + total_count: int + + +class UserTopicListRequest(CentRequest[UserTopicListResult]): + """ + User topic list request. + """ + + __api_method__ = "user_topic_list" + __returning__ = UserTopicListResult + + filter: UserTopicFilter + include_total_count: bool + cursor: str + limit: int + + +class DeviceTopicUpdateResult(CentResult): + """ + Device topic update result. + """ + + +class DeviceTopicUpdateRequest(CentRequest[DeviceTopicUpdateResult]): + """ + Device topic update request. + """ + + __api_method__ = "device_topic_update" + __returning__ = DeviceTopicUpdateResult + + device_id: str + op: str + topics: List[str] + + +class UserTopicUpdateResult(CentResult): + """ + User topic update result. + """ + + +class UserTopicUpdateRequest(CentRequest[UserTopicUpdateResult]): + """ + User topic update request. + """ + + __api_method__ = "user_topic_update" + __returning__ = UserTopicUpdateResult + + user: str + op: str + topics: List[str] + + +class PushRecipient(NestedModel): + """ + Push recipient. + """ + + filter: DeviceFilter + fcm_tokens: List[str] + fcm_topic: str + fcm_condition: str + hms_tokens: List[str] + hms_topic: str + hms_condition: str + apns_tokens: List[str] + + +class FcmPushNotification(NestedModel): + """ + FCM push notification. + """ + + message: Any + + +class HmsPushNotification(NestedModel): + """ + HMS push notification. + """ + + message: Any + + +class ApnsPushNotification(NestedModel): + """ + APNS push notification. + """ + + headers: Dict[str, str] + payload: Any + + +class PushNotification(NestedModel): + """ + Push notification. + """ + + fcm: FcmPushNotification + hms: HmsPushNotification + apns: ApnsPushNotification + expire_at: int + + +class SendPushNotificationResult(CentResult): + """ + Send push notification result. + """ + + uid: str + + +class SendPushNotificationRequest(CentRequest[SendPushNotificationResult]): + """ + Send push notification request. + """ + + __api_method__ = "send_push_notification" + __returning__ = SendPushNotificationResult + + recipient: PushRecipient + notification: PushNotification + uid: str + send_at: int + + +class UpdatePushStatusResult(CentResult): + """ + Update push status result. + """ + + +class UpdatePushStatusRequest(CentRequest[UpdatePushStatusResult]): + """ + Update push status request. + """ + + __api_method__ = "update_push_status" + __returning__ = UpdatePushStatusResult + + uid: str + status: str + device_id: str + msg_id: str + + +class CancelPushResult(CentResult): + """ + Cancel push result. + """ + + +class CancelPushRequest(CentRequest[CancelPushResult]): + """ + Cancel push request. + """ + + __returning__ = CancelPushResult + __api_method__ = "cancel_push" + + uid: str diff --git a/tests/conftest.py b/tests/conftest.py index 1a3301a..eb57d35 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -31,3 +31,14 @@ async def async_client( client = AsyncClient(BASE_URL, API_KEY) yield client await client._session.close() + + +@pytest.fixture() +async def clients( + anyio_backend: Any, # noqa: ARG001 +) -> AsyncGenerator[Any, None]: + sync_client = Client(BASE_URL, API_KEY) + async_client = AsyncClient(BASE_URL, API_KEY) + yield sync_client, async_client + await async_client.close() + sync_client.close() diff --git a/tests/test_async_validation.py b/tests/test_async_validation.py deleted file mode 100644 index 1fb88eb..0000000 --- a/tests/test_async_validation.py +++ /dev/null @@ -1,162 +0,0 @@ -import uuid -import pytest - -from cent import ( - AsyncClient, - CentApiResponseError, - PublishRequest, - StreamPosition, - Disconnect, - BroadcastRequest, - PresenceRequest, -) - -from tests.conftest import UNKNOWN_CHANNEL_ERROR_CODE - - -async def test_publish(async_client: AsyncClient) -> None: - result = await async_client.publish( - "personal_1", - {"data": "data"}, - skip_history=False, - tags={"tag": "tag"}, - idempotency_key="idempotency_key", - ) - assert result.offset - - -async def test_broadcast(async_client: AsyncClient) -> None: - await async_client.broadcast( - ["personal_1", "personal_2"], - {"data": "data"}, - skip_history=False, - tags={"tag": "tag"}, - idempotency_key="idempotency_key", - ) - - -async def test_subscribe(async_client: AsyncClient) -> None: - await async_client.subscribe( - "user", - "personal_1", - info={"info": "info"}, - client="client", - session="session", - data={"data": "data"}, - recover_since=StreamPosition( - offset=1, - epoch="1", - ), - ) - - -async def test_unsubscribe(async_client: AsyncClient) -> None: - await async_client.unsubscribe( - user="user", - channel="personal_1", - session="session", - client="client", - ) - - -async def test_presence(async_client: AsyncClient) -> None: - await async_client.presence("personal_1") - - -async def test_presence_stats(async_client: AsyncClient) -> None: - await async_client.presence_stats("personal_1") - - -async def test_history(async_client: AsyncClient) -> None: - num_pubs = 10 - channel = "personal_" + uuid.uuid4().hex - for i in range(num_pubs): - await async_client.publish( - channel, - {"data": f"data {i}"}, - ) - result = await async_client.history( - channel=channel, - limit=num_pubs, - reverse=True, - ) - assert isinstance(result.offset, int) - assert result.offset > 0 - assert len(result.publications) == num_pubs - assert result.publications[0].data == {"data": "data 9"} - - -async def test_history_remove(async_client: AsyncClient) -> None: - await async_client.history_remove("personal_1") - - -async def test_info(async_client: AsyncClient) -> None: - await async_client.info() - - -async def test_channels(async_client: AsyncClient) -> None: - await async_client.channels( - pattern="*", - ) - - -async def test_disconnect(async_client: AsyncClient) -> None: - await async_client.disconnect( - user="user", - client="client", - session="session", - whitelist=["personal_1"], - disconnect=Disconnect( - code=4000, - reason="reason", - ), - ) - - -async def test_refresh(async_client: AsyncClient) -> None: - await async_client.refresh( - user="user", - client="client", - session="session", - expire_at=1, - expired=True, - ) - - -async def test_batch(async_client: AsyncClient) -> None: - result = await async_client.batch( - commands=[ - PublishRequest( - channel="personal_1", - data={"data": "Second data"}, - ), - PublishRequest( - channel="personal_2", - data={"data": "First data"}, - ), - BroadcastRequest( - channels=["personal_1", "personal_2"], - data={"data": "Third data"}, - ), - PresenceRequest( - channel="personal_1", - ), - ] - ) - - num_expected_replies = 4 - assert len(result.replies) == num_expected_replies - assert result.replies[0].offset - assert result.replies[1].offset - assert result.replies[2].responses[0].result.offset - assert result.replies[2].responses[1].result.offset - assert result.replies[3].presence == {} - - -async def test_error_publish(async_client: AsyncClient) -> None: - with pytest.raises(CentApiResponseError, match="unknown channel") as exc_info: - await async_client.publish( - "undefined_channel:123", - {"data": "data"}, - ) - assert exc_info.value.code == UNKNOWN_CHANNEL_ERROR_CODE diff --git a/tests/test_sync_validation.py b/tests/test_sync_validation.py deleted file mode 100644 index f3512b5..0000000 --- a/tests/test_sync_validation.py +++ /dev/null @@ -1,163 +0,0 @@ -import uuid -import pytest - -from cent import ( - Client, - CentApiResponseError, - PublishRequest, - BroadcastRequest, - PresenceRequest, - StreamPosition, - Disconnect, -) - -from tests.conftest import UNKNOWN_CHANNEL_ERROR_CODE - - -def test_publish(sync_client: Client) -> None: - result = sync_client.publish( - "personal_1", - {"data": "data"}, - skip_history=False, - tags={"tag": "tag"}, - idempotency_key="idempotency_key", - ) - assert result.offset - - -def test_broadcast(sync_client: Client) -> None: - sync_client.broadcast( - ["personal_1", "personal_2"], - {"data": "data"}, - skip_history=False, - tags={"tag": "tag"}, - idempotency_key="idempotency_key", - ) - - -def test_subscribe(sync_client: Client) -> None: - sync_client.subscribe( - "user", - "personal_1", - info={"info": "info"}, - client="client", - session="session", - data={"data": "data"}, - recover_since=StreamPosition( - offset=1, - epoch="1", - ), - ) - - -def test_unsubscribe(sync_client: Client) -> None: - sync_client.unsubscribe( - user="user", - channel="personal_1", - session="session", - client="client", - ) - - -def test_presence(sync_client: Client) -> None: - sync_client.presence("personal_1") - - -def test_presence_stats(sync_client: Client) -> None: - sync_client.presence_stats("personal_1") - - -def test_history(sync_client: Client) -> None: - num_pubs = 10 - channel = "personal_" + uuid.uuid4().hex - for i in range(num_pubs): - sync_client.publish( - channel, - {"data": f"data {i}"}, - ) - - result = sync_client.history( - channel=channel, - limit=num_pubs, - reverse=False, - ) - assert isinstance(result.offset, int) - assert result.offset > 0 - assert len(result.publications) == num_pubs - assert result.publications[0].data == {"data": "data 0"} - - -def test_history_remove(sync_client: Client) -> None: - sync_client.history_remove("personal_1") - - -def test_info(sync_client: Client) -> None: - sync_client.info() - - -def test_channels(sync_client: Client) -> None: - sync_client.channels( - pattern="*", - ) - - -def test_disconnect(sync_client: Client) -> None: - sync_client.disconnect( - user="user", - client="client", - session="session", - whitelist=["personal_1"], - disconnect=Disconnect( - code=4000, - reason="reason", - ), - ) - - -def test_refresh(sync_client: Client) -> None: - sync_client.refresh( - user="user", - client="client", - session="session", - expire_at=1, - expired=True, - ) - - -def test_batch(sync_client: Client) -> None: - result = sync_client.batch( - commands=[ - PublishRequest( - channel="personal_1", - data={"data": "Second data"}, - ), - PublishRequest( - channel="personal_2", - data={"data": "First data"}, - ), - BroadcastRequest( - channels=["personal_1", "personal_2"], - data={"data": "Third data"}, - ), - PresenceRequest( - channel="personal_1", - ), - ] - ) - - num_expected_replies = 4 - assert len(result.replies) == num_expected_replies - assert result.replies[0].offset - assert result.replies[1].offset - assert result.replies[2].responses[0].result.offset - assert result.replies[2].responses[1].result.offset - assert result.replies[3].presence == {} - - -def test_error_publish(sync_client: Client) -> None: - with pytest.raises(CentApiResponseError, match="unknown channel") as exc_info: - sync_client.publish( - "undefined_channel:123", - {"data": "data"}, - ) - assert exc_info.value.code == UNKNOWN_CHANNEL_ERROR_CODE diff --git a/tests/test_validation.py b/tests/test_validation.py new file mode 100644 index 0000000..9b4dea9 --- /dev/null +++ b/tests/test_validation.py @@ -0,0 +1,233 @@ +import uuid +import pytest + +from cent import ( + AsyncClient, + Client, + CentApiResponseError, + PublishRequest, + BroadcastRequest, + PresenceRequest, + StreamPosition, + Disconnect, + SubscribeRequest, + BatchRequest, + UnsubscribeRequest, + PresenceStatsRequest, + HistoryRequest, + HistoryRemoveRequest, + InfoRequest, + ChannelsRequest, + DisconnectRequest, + RefreshRequest, + BatchResult, + HistoryResult, +) + +from tests.conftest import UNKNOWN_CHANNEL_ERROR_CODE + + +async def test_publish(sync_client: Client, async_client: AsyncClient) -> None: + request = PublishRequest( + channel="personal_1", + data={"data": "data"}, + skip_history=False, + tags={"tag": "tag"}, + idempotency_key="idempotency_key", + ) + result = sync_client.send(request) + assert result.offset + + result = await async_client.send(request) + assert result.offset + + +async def test_broadcast(sync_client: Client, async_client: AsyncClient) -> None: + channels = ["personal_1", "personal_2"] + request = BroadcastRequest( + channels=channels, + data={"data": "data"}, + skip_history=False, + tags={"tag": "tag"}, + idempotency_key="idempotency_key", + ) + result = sync_client.send(request) + assert len(result.responses) == len(channels) + + result = await async_client.send(request) + assert len(result.responses) == len(channels) + + +async def test_subscribe(sync_client: Client, async_client: AsyncClient) -> None: + request = SubscribeRequest( + user="user", + channel="personal_1", + client="client", + session="session", + data={"data": "data"}, + recover_since=StreamPosition( + offset=1, + epoch="1", + ), + ) + sync_client.send(request) + await async_client.send(request) + + +async def test_unsubscribe(sync_client: Client, async_client: AsyncClient) -> None: + request = UnsubscribeRequest( + user="user", + channel="personal_1", + session="session", + client="client", + ) + sync_client.send(request) + await async_client.send(request) + + +async def test_presence(sync_client: Client, async_client: AsyncClient) -> None: + request = PresenceRequest( + channel="personal_1", + ) + sync_client.send(request) + await async_client.send(request) + + +async def test_presence_stats(sync_client: Client, async_client: AsyncClient) -> None: + request = PresenceStatsRequest( + channel="personal_1", + ) + sync_client.send(request) + await async_client.send(request) + + +async def test_history(sync_client: Client, async_client: AsyncClient) -> None: + num_pubs = 10 + channel = "personal_" + uuid.uuid4().hex + for i in range(num_pubs): + sync_client.send( + PublishRequest( + channel=channel, + data={"data": f"data {i}"}, + ), + ) + + request = HistoryRequest( + channel=channel, + limit=num_pubs, + reverse=False, + ) + + def check_result(res: HistoryResult) -> None: + assert isinstance(res.offset, int) + assert res.offset > 0 + assert len(res.publications) == num_pubs + assert res.publications[0].data == {"data": "data 0"} + + result = sync_client.send(request) + check_result(result) + + result = await async_client.send(request) + check_result(result) + + +async def test_history_remove(sync_client: Client, async_client: AsyncClient) -> None: + request = HistoryRemoveRequest( + channel="personal_1", + ) + sync_client.send(request) + await async_client.send(request) + + +async def test_info(sync_client: Client, async_client: AsyncClient) -> None: + sync_client.send(InfoRequest()) + await async_client.send(InfoRequest()) + + +async def test_channels(sync_client: Client, async_client: AsyncClient) -> None: + request = ChannelsRequest(pattern="*") + + sync_client.send(request) + await async_client.send(request) + + +async def test_disconnect(sync_client: Client, async_client: AsyncClient) -> None: + request = DisconnectRequest( + user="user", + client="client", + session="session", + whitelist=["personal_1"], + disconnect=Disconnect( + code=4000, + reason="reason", + ), + ) + + sync_client.send(request) + await async_client.send(request) + + +async def test_refresh(sync_client: Client, async_client: AsyncClient) -> None: + request = RefreshRequest( + user="user", + client="client", + session="session", + expire_at=1, + expired=True, + ) + + sync_client.send(request) + await async_client.send(request) + + +async def test_batch(sync_client: Client, async_client: AsyncClient) -> None: + request = BatchRequest( + commands=[ + PublishRequest( + channel="personal_1", + data={"data": "Second data"}, + ), + PublishRequest( + channel="personal_2", + data={"data": "First data"}, + ), + BroadcastRequest( + channels=["personal_1", "personal_2"], + data={"data": "Third data"}, + ), + PresenceRequest( + channel="personal_1", + ), + ], + parallel=True, + ) + + def check_result(res: BatchResult) -> None: + num_expected_replies = 4 + assert len(res.replies) == num_expected_replies + assert res.replies[0].offset + assert res.replies[1].offset + assert res.replies[2].responses[0].result.offset + assert res.replies[2].responses[1].result.offset + assert res.replies[3].presence == {} + + result = sync_client.send(request) + check_result(result) + + result = await async_client.send(request) + check_result(result) + + +async def test_error_publish(sync_client: Client, async_client: AsyncClient) -> None: + request = PublishRequest( + channel="undefined:channel", + data={"data": "data"}, + ) + + with pytest.raises(CentApiResponseError, match="unknown channel") as exc_info: + sync_client.send(request) + assert exc_info.value.code == UNKNOWN_CHANNEL_ERROR_CODE + + with pytest.raises(CentApiResponseError, match="unknown channel") as exc_info: + await async_client.send(request) + assert exc_info.value.code == UNKNOWN_CHANNEL_ERROR_CODE From 34372bd6bca0460a15f3e963fa28f7fd890c7006 Mon Sep 17 00:00:00 2001 From: FZambia Date: Sat, 17 Feb 2024 13:56:23 +0200 Subject: [PATCH 33/55] fix batch --- cent/client/async_client.py | 1 - cent/client/sync_client.py | 1 - cent/dto.py | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/cent/client/async_client.py b/cent/client/async_client.py index 8d78fdf..d4dcddf 100644 --- a/cent/client/async_client.py +++ b/cent/client/async_client.py @@ -5,7 +5,6 @@ from cent.client.session import AiohttpSession from cent.dto import CentRequest - T = TypeVar("T") diff --git a/cent/client/sync_client.py b/cent/client/sync_client.py index 8de366a..fa6d676 100644 --- a/cent/client/sync_client.py +++ b/cent/client/sync_client.py @@ -5,7 +5,6 @@ from cent.client.session import RequestsSession from cent.dto import CentRequest - T = TypeVar("T") diff --git a/cent/dto.py b/cent/dto.py index 817d393..0799f5c 100644 --- a/cent/dto.py +++ b/cent/dto.py @@ -328,7 +328,7 @@ class BatchRequest(CentRequest[BatchResult]): __returning__ = BatchResult __api_method__ = "batch" - commands: List[CentRequest[Any]] + commands: List[Any] parallel: Optional[bool] = None From da13ecbb2a99c6772e674f9176ad7bd3cd91a9de Mon Sep 17 00:00:00 2001 From: FZambia Date: Sat, 17 Feb 2024 13:56:59 +0200 Subject: [PATCH 34/55] fix batch --- cent/dto.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cent/dto.py b/cent/dto.py index 0799f5c..c997207 100644 --- a/cent/dto.py +++ b/cent/dto.py @@ -328,7 +328,7 @@ class BatchRequest(CentRequest[BatchResult]): __returning__ = BatchResult __api_method__ = "batch" - commands: List[Any] + commands: List[CentRequest[CentResult]] parallel: Optional[bool] = None From d7980122b325c56d724f934b435001cacbc95235 Mon Sep 17 00:00:00 2001 From: FZambia Date: Sat, 17 Feb 2024 18:23:57 +0200 Subject: [PATCH 35/55] refactor --- benchmarks/test_publish.py | 18 ++-- cent/__init__.py | 10 +- cent/client/async_client.py | 23 +++-- cent/client/session/aiohttp.py | 24 ++--- cent/client/session/base_http.py | 67 +------------ cent/client/session/base_http_async.py | 22 +--- cent/client/session/base_http_sync.py | 22 +--- cent/client/session/requests.py | 21 ++-- cent/client/sync_client.py | 21 ++-- cent/dto.py | 133 +++++++++++++++++-------- cent/exceptions.py | 15 +-- tests/test_validation.py | 122 +++++++++++++++++------ 12 files changed, 264 insertions(+), 234 deletions(-) diff --git a/benchmarks/test_publish.py b/benchmarks/test_publish.py index 06ef867..b62f09f 100644 --- a/benchmarks/test_publish.py +++ b/benchmarks/test_publish.py @@ -1,22 +1,26 @@ import random from benchmarks.conftest import BenchmarkDecoratorType -from cent import AsyncClient, Client +from cent import AsyncClient, Client, PublishRequest def sync_requests(client: Client) -> None: channel_number = random.randint(0, 1000) # noqa: S311 - client.publish( - channel=f"personal_{channel_number}", - data={"message": "Hello world!"}, + client.send( + PublishRequest( + channel=f"personal_{channel_number}", + data={"message": "Hello world!"}, + ) ) async def async_requests(client: AsyncClient) -> None: channel_number = random.randint(0, 1000) # noqa: S311 - await client.publish( - channel=f"personal_{channel_number}", - data={"message": "Hello world!"}, + await client.send( + PublishRequest( + channel=f"personal_{channel_number}", + data={"message": "Hello world!"}, + ) ) diff --git a/cent/__init__.py b/cent/__init__.py index 1b9241c..22c9a34 100644 --- a/cent/__init__.py +++ b/cent/__init__.py @@ -3,6 +3,11 @@ AsyncClient, ) from cent.dto import ( + CentResult, + CentRequest, + Response, + BatchRequest, + BatchResult, BroadcastRequest, PublishRequest, SubscribeRequest, @@ -15,7 +20,6 @@ ChannelsRequest, DisconnectRequest, InfoRequest, - BatchRequest, PublishResult, BroadcastResult, SubscribeResult, @@ -28,7 +32,6 @@ ChannelsResult, DisconnectResult, InfoResult, - BatchResult, StreamPosition, ChannelOptionsOverride, Disconnect, @@ -58,6 +61,8 @@ "CentDecodeError", "CentError", "CentNetworkError", + "CentRequest", + "CentResult", "CentTransportError", "CentUnauthorizedError", "ChannelOptionsOverride", @@ -85,6 +90,7 @@ "PublishResult", "RefreshRequest", "RefreshResult", + "Response", "StreamPosition", "SubscribeRequest", "SubscribeResult", diff --git a/cent/client/async_client.py b/cent/client/async_client.py index d4dcddf..71ee27c 100644 --- a/cent/client/async_client.py +++ b/cent/client/async_client.py @@ -1,11 +1,9 @@ -from typing import Optional, Any, TypeVar +from typing import Optional, Any, cast from aiohttp import ClientSession from cent.client.session import AiohttpSession -from cent.dto import CentRequest - -T = TypeVar("T") +from cent.dto import CentRequest, CentResultType class AsyncClient: @@ -29,8 +27,21 @@ def __init__( session=session, ) - async def send(self, request: CentRequest[T], timeout: Optional[float] = None) -> T: - return await self._session(self._api_key, request, timeout=timeout) + async def send( + self, + request: CentRequest[CentResultType], + timeout: Optional[float] = None, + ) -> CentResultType: + method = request.get_method() + payload = request.to_json() + content = await self._session.make_request( + self._api_key, + method, + payload, + timeout=timeout, + ) + response = request.get_result(content) + return cast(CentResultType, response.result) async def close(self) -> None: await self._session.close() diff --git a/cent/client/session/aiohttp.py b/cent/client/session/aiohttp.py index 1cbab00..f8f6a92 100644 --- a/cent/client/session/aiohttp.py +++ b/cent/client/session/aiohttp.py @@ -1,10 +1,9 @@ import asyncio -from typing import Optional +from typing import Optional, Dict, Any from aiohttp import ClientSession, ClientError from cent.client.session.base_http_async import BaseHttpAsyncSession -from cent.dto import CentType, CentRequest, BatchRequest from cent.exceptions import CentNetworkError, CentTimeoutError @@ -34,19 +33,15 @@ async def close(self) -> None: async def make_request( self, api_key: str, - request: CentRequest[CentType], + method: str, + json_data: Dict[str, Any], timeout: Optional[float] = None, - ) -> CentType: + ) -> str: session = self._session if api_key: session.headers["X-API-Key"] = api_key - if isinstance(request, BatchRequest): - json_data = self.get_batch_json_data(request) - else: - json_data = request.model_dump(exclude_none=True) - - url = f"{self._base_url}/{request.__api_method__}" + url = f"{self._base_url}/{method}" try: async with session.post( @@ -57,19 +52,14 @@ async def make_request( raw_result = await resp.text() except asyncio.TimeoutError as error: raise CentTimeoutError( - request=request, message="Request timeout", ) from error except ClientError as error: raise CentNetworkError( - request=request, message=f"{type(error).__name__}: {error}", ) from error - return self.check_response( - request=request, - status_code=resp.status, - content=raw_result, - ) + self.check_status_code(status_code=resp.status) + return raw_result def __del__(self) -> None: if self._session and not self._session.closed: diff --git a/cent/client/session/base_http.py b/cent/client/session/base_http.py index 21c5bcf..b36c453 100644 --- a/cent/client/session/base_http.py +++ b/cent/client/session/base_http.py @@ -1,85 +1,22 @@ -import json from http import HTTPStatus -from typing import Any, Dict, List, cast - -from pydantic import ValidationError, TypeAdapter from cent.exceptions import ( - CentDecodeError, - CentApiResponseError, CentUnauthorizedError, CentTransportError, ) -from cent.dto import ( - CentRequest, - CentType, - Response, - BatchRequest, -) class BaseHttpSession: """Base class for HTTP sessions.""" @staticmethod - def get_batch_json_data(request: BatchRequest) -> Dict[str, List[Dict[str, Any]]]: - commands = [ - {command.__api_method__: command.model_dump(exclude_none=True)} - for command in request.commands - ] - return {"commands": commands} - - @staticmethod - def validate_batch( - request: BatchRequest, - json_replies: List[Dict[str, Any]], - ) -> Dict[str, Dict[str, List[Any]]]: - replies: List[CentRequest[Any]] = [] - for command_method, json_data in zip(request.commands, json_replies): - validated_request: CentRequest[Any] = TypeAdapter( - command_method.__returning__ - ).validate_python( - json_data[command_method.__api_method__], - ) - replies.append(validated_request) - return {"result": {"replies": replies}} - - def check_response( - self, - request: CentRequest[CentType], + def check_status_code( status_code: int, - content: str, - ) -> CentType: + ) -> None: if status_code == HTTPStatus.UNAUTHORIZED: raise CentUnauthorizedError if status_code != HTTPStatus.OK: raise CentTransportError( - request=request, status_code=status_code, ) - - try: - json_data = json.loads(content) - except Exception as err: - raise CentDecodeError from err - - if isinstance(request, BatchRequest): - json_data = self.validate_batch(request, json_data["replies"]) - - try: - response_type = Response[request.__returning__] # type: ignore - response = TypeAdapter(response_type).validate_python( - json_data, - ) - except ValidationError as err: - raise CentDecodeError from err - - if response.error: - raise CentApiResponseError( - request=request, - code=response.error.code, - message=response.error.message, - ) - - return cast(CentType, response.result) diff --git a/cent/client/session/base_http_async.py b/cent/client/session/base_http_async.py index b0fb057..0b83c0d 100644 --- a/cent/client/session/base_http_async.py +++ b/cent/client/session/base_http_async.py @@ -1,8 +1,7 @@ from abc import ABC, abstractmethod -from typing import Optional +from typing import Optional, Dict, Any from cent.client.session.base_http import BaseHttpSession -from cent.dto import CentType, CentRequest class BaseHttpAsyncSession(BaseHttpSession, ABC): @@ -16,21 +15,10 @@ async def close(self) -> None: async def make_request( self, api_key: str, - request: CentRequest[CentType], + method: str, + json_data: Dict[str, Any], timeout: Optional[float] = None, - ) -> CentType: + ) -> str: """ - Make request to centrifuge API. - - :param api_key: Centrifugo API key. - :param request: Centrifugo API request. - :param timeout: Request timeout. + Make request to Centrifugo HTTP API. """ - - async def __call__( - self, - api_key: str, - request: CentRequest[CentType], - timeout: Optional[float] = None, - ) -> CentType: - return await self.make_request(api_key, request, timeout) diff --git a/cent/client/session/base_http_sync.py b/cent/client/session/base_http_sync.py index 5770817..fd3018f 100644 --- a/cent/client/session/base_http_sync.py +++ b/cent/client/session/base_http_sync.py @@ -1,8 +1,7 @@ from abc import ABC, abstractmethod -from typing import Optional +from typing import Optional, Dict, Any from cent.client.session.base_http import BaseHttpSession -from cent.dto import CentType, CentRequest class BaseHttpSyncSession(BaseHttpSession, ABC): @@ -16,21 +15,10 @@ def close(self) -> None: def make_request( self, api_key: str, - request: CentRequest[CentType], + method: str, + json_data: Dict[str, Any], timeout: Optional[float] = None, - ) -> CentType: + ) -> str: """ - Make request to Centrifugo API. - - :param api_key: Centrifugo API key. - :param request: Centrifugo API request. - :param timeout: Request timeout. + Make request to Centrifugo HTTP API. """ - - def __call__( - self, - api_key: str, - request: CentRequest[CentType], - timeout: Optional[float] = None, - ) -> CentType: - return self.make_request(api_key, request, timeout) diff --git a/cent/client/session/requests.py b/cent/client/session/requests.py index 570095a..55830a4 100644 --- a/cent/client/session/requests.py +++ b/cent/client/session/requests.py @@ -1,10 +1,9 @@ -from typing import Optional +from typing import Optional, Dict, Any import requests from requests import Session from cent.client.session.base_http_sync import BaseHttpSyncSession -from cent.dto import CentType, CentRequest, BatchRequest from cent.exceptions import CentNetworkError, CentTimeoutError @@ -31,17 +30,14 @@ def close(self) -> None: def make_request( self, api_key: str, - request: CentRequest[CentType], + method: str, + json_data: Dict[str, Any], timeout: Optional[float] = None, - ) -> CentType: + ) -> str: if api_key: self._session.headers["X-API-Key"] = api_key - if isinstance(request, BatchRequest): - json_data = self.get_batch_json_data(request) - else: - json_data = request.model_dump(exclude_none=True) - url = f"{self._base_url}/{request.__api_method__}" + url = f"{self._base_url}/{method}" try: raw_result = self._session.post( @@ -51,19 +47,16 @@ def make_request( ) except requests.exceptions.Timeout as error: raise CentTimeoutError( - request=request, message="Request timeout", ) from error except requests.exceptions.ConnectionError as error: raise CentNetworkError( - request=request, message=f"{type(error).__name__}: {error}", ) from error - return self.check_response( - request=request, + self.check_status_code( status_code=raw_result.status_code, - content=raw_result.text, ) + return raw_result.text def __del__(self) -> None: self.close() diff --git a/cent/client/sync_client.py b/cent/client/sync_client.py index fa6d676..0da6798 100644 --- a/cent/client/sync_client.py +++ b/cent/client/sync_client.py @@ -1,11 +1,9 @@ -from typing import Optional, Any, TypeVar +from typing import Optional, Any, cast from requests import Session from cent.client.session import RequestsSession -from cent.dto import CentRequest - -T = TypeVar("T") +from cent.dto import CentRequest, CentResultType class Client: @@ -31,8 +29,19 @@ def __init__( session=session, ) - def send(self, request: CentRequest[T], timeout: Optional[float] = None) -> T: - return self._session(self._api_key, request, timeout=timeout) + def send( + self, + request: CentRequest[CentResultType], + timeout: Optional[float] = None, + ) -> CentResultType: + content = self._session.make_request( + self._api_key, + request.get_method(), + request.to_json(), + timeout=timeout, + ) + response = request.get_result(content) + return cast(CentResultType, response.result) def close(self) -> None: self._session.close() diff --git a/cent/dto.py b/cent/dto.py index c997207..dd32189 100644 --- a/cent/dto.py +++ b/cent/dto.py @@ -1,6 +1,9 @@ +import json from abc import ABC, abstractmethod from typing import TypeVar, Any, Generic, TYPE_CHECKING, ClassVar, Optional, List, Dict -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, ValidationError + +from cent.exceptions import CentDecodeError, CentApiResponseError class Error(BaseModel): @@ -8,15 +11,27 @@ class Error(BaseModel): message: str -CentType = TypeVar("CentType", bound=Any) +class CentResult(BaseModel, ABC): + model_config = ConfigDict( + use_enum_values=True, + extra="allow", + validate_assignment=True, + frozen=True, + populate_by_name=True, + arbitrary_types_allowed=True, + defer_build=True, + ) + + +CentResultType = TypeVar("CentResultType", bound=CentResult) -class Response(BaseModel, Generic[CentType]): +class Response(BaseModel, Generic[CentResultType]): error: Optional[Error] = None - result: Optional[CentType] = None + result: Optional[CentResultType] = None -class CentRequest(BaseModel, Generic[CentType], ABC): +class CentRequest(BaseModel, Generic[CentResultType], ABC): model_config = ConfigDict( extra="allow", populate_by_name=True, @@ -38,17 +53,39 @@ def __returning__(self) -> type: def __api_method__(self) -> str: pass + def to_json(self) -> Any: + return self.model_dump(exclude_none=True) -class CentResult(BaseModel, ABC): - model_config = ConfigDict( - use_enum_values=True, - extra="allow", - validate_assignment=True, - frozen=True, - populate_by_name=True, - arbitrary_types_allowed=True, - defer_build=True, - ) + def get_method(self) -> str: + return self.__api_method__ + + def get_result( + self, + content: str, + ) -> Response[CentResult]: + try: + json_data = json.loads(content) + except Exception as err: + raise CentDecodeError from err + + if isinstance(self, BatchRequest): + json_data = _validate_batch(self, json_data["replies"]) + + try: + response_type = Response[self.__returning__] # type: ignore + response = TypeAdapter(response_type).validate_python( + json_data, + ) + except ValidationError as err: + raise CentDecodeError from err + + if response.error: + raise CentApiResponseError( + code=response.error.code, + message=response.error.message, + ) + + return response class NestedModel(BaseModel, ABC): @@ -59,6 +96,48 @@ class NestedModel(BaseModel, ABC): ) +class BatchResult(CentResult): + """Batch response. + + Attributes: + replies: List of results from batch request. + """ + + replies: List[CentResult] + + +class BatchRequest(CentRequest[BatchResult]): + """Batch request.""" + + __returning__ = BatchResult + __api_method__ = "batch" + + requests: List[Any] + parallel: Optional[bool] = None + + def to_json(self) -> Any: + commands = [ + {request.__api_method__: request.model_dump(exclude_none=True)} + for request in self.requests + ] + return {"commands": commands, "parallel": bool(self.parallel)} + + +def _validate_batch( + request: BatchRequest, + json_replies: List[Dict[str, Any]], +) -> Dict[str, Dict[str, List[Any]]]: + replies: List[CentRequest[Any]] = [] + for command_method, json_data in zip(request.requests, json_replies): + validated_request: CentRequest[Any] = TypeAdapter( + command_method.__returning__ + ).validate_python( + json_data[command_method.__api_method__], + ) + replies.append(validated_request) + return {"result": {"replies": replies}} + + class Disconnect(NestedModel): """Disconnect data. @@ -199,16 +278,6 @@ class Node(CentResult): process: Optional[ProcessStats] = None -class BatchResult(CentResult): - """Batch response. - - Attributes: - replies: List of results from batch request. - """ - - replies: List[Any] - - class PublishResult(CentResult): """Publish result. @@ -318,20 +387,6 @@ class UnsubscribeResult(CentResult): """Unsubscribe result.""" -class BatchRequest(CentRequest[BatchResult]): - """Batch request. - - Attributes: - commands: List of commands to execute in batch. - """ - - __returning__ = BatchResult - __api_method__ = "batch" - - commands: List[CentRequest[CentResult]] - parallel: Optional[bool] = None - - class BroadcastRequest(CentRequest[BroadcastResult]): """Broadcast request. diff --git a/cent/exceptions.py b/cent/exceptions.py index 436d3ad..59df06f 100644 --- a/cent/exceptions.py +++ b/cent/exceptions.py @@ -1,6 +1,3 @@ -from cent.dto import CentType, CentRequest - - class CentError(Exception): """ Wrapper for all exceptions coming from this library. @@ -10,8 +7,7 @@ class CentError(Exception): class CentNetworkError(CentError): """CentNetworkError raised when Centrifugo is unreachable or not available.""" - def __init__(self, request: CentRequest[CentType], message: str) -> None: - self.request = request + def __init__(self, message: str) -> None: self.message = message def __str__(self) -> str: @@ -24,8 +20,7 @@ def __repr__(self) -> str: class CentTransportError(CentError): """CentTransportError raised when HTTP request results into non-200 status code.""" - def __init__(self, request: CentRequest[CentType], status_code: int): - self.request = request + def __init__(self, status_code: int): self.status_code = status_code def __str__(self) -> str: @@ -38,8 +33,7 @@ def __repr__(self) -> str: class CentTimeoutError(CentError): """CentTimeoutError raised when request is timed out""" - def __init__(self, request: CentRequest[CentType], message: str) -> None: - self.request = request + def __init__(self, message: str) -> None: self.message = message def __str__(self) -> str: @@ -67,8 +61,7 @@ class CentApiResponseError(CentError): any error as a result of API command execution. """ - def __init__(self, request: CentRequest[CentType], code: int, message: str) -> None: - self.request = request + def __init__(self, code: int, message: str) -> None: self.code = code self.message = message diff --git a/tests/test_validation.py b/tests/test_validation.py index 9b4dea9..8eb031f 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -1,4 +1,6 @@ import uuid +from typing import List, cast + import pytest from cent import ( @@ -11,7 +13,6 @@ StreamPosition, Disconnect, SubscribeRequest, - BatchRequest, UnsubscribeRequest, PresenceStatsRequest, HistoryRequest, @@ -20,13 +21,50 @@ ChannelsRequest, DisconnectRequest, RefreshRequest, - BatchResult, HistoryResult, + CentResult, + PublishResult, + BroadcastResult, + PresenceResult, + Response, + BatchRequest, ) + from tests.conftest import UNKNOWN_CHANNEL_ERROR_CODE +def test_serialization_none() -> None: + request = PublishRequest( + channel="personal_1", + data={"data": None}, + ) + assert request.to_json() == {"channel": "personal_1", "data": {"data": None}} + + +def test_serialization_batch() -> None: + requests = [ + PublishRequest( + channel="personal_1", + data={"data": "Second data"}, + ), + PublishRequest( + channel="personal_2", + data={"data": "First data"}, + ), + ] + batch_request = BatchRequest( + requests=requests, + ) + assert batch_request.to_json() == { + "commands": [ + {"publish": {"channel": "personal_1", "data": {"data": "Second data"}}}, + {"publish": {"channel": "personal_2", "data": {"data": "First data"}}}, + ], + "parallel": False, + } + + async def test_publish(sync_client: Client, async_client: AsyncClient) -> None: request = PublishRequest( channel="personal_1", @@ -44,6 +82,14 @@ async def test_publish(sync_client: Client, async_client: AsyncClient) -> None: async def test_broadcast(sync_client: Client, async_client: AsyncClient) -> None: channels = ["personal_1", "personal_2"] + + def check_result(res: BroadcastResult) -> None: + assert len(res.responses) == len(channels) + resp = res.responses[0] + assert resp.error is None + assert resp.result is not None + assert resp.result.offset + request = BroadcastRequest( channels=channels, data={"data": "data"}, @@ -52,10 +98,10 @@ async def test_broadcast(sync_client: Client, async_client: AsyncClient) -> None idempotency_key="idempotency_key", ) result = sync_client.send(request) - assert len(result.responses) == len(channels) + check_result(result) result = await async_client.send(request) - assert len(result.responses) == len(channels) + check_result(result) async def test_subscribe(sync_client: Client, async_client: AsyncClient) -> None: @@ -181,41 +227,51 @@ async def test_refresh(sync_client: Client, async_client: AsyncClient) -> None: async def test_batch(sync_client: Client, async_client: AsyncClient) -> None: + def check_result(res: List[CentResult]) -> None: + num_expected_replies = 4 + assert len(res) == num_expected_replies + assert cast(PublishResult, res[0]).offset + assert cast(PublishResult, res[1]).offset + broadcast_result = cast(BroadcastResult, res[2]) + num_expected_responses = 2 + assert len(broadcast_result.responses) == num_expected_responses + response_0 = broadcast_result.responses[0] + response_1 = broadcast_result.responses[1] + assert isinstance(response_0, Response) + assert isinstance(response_1, Response) + assert isinstance(response_0.result, PublishResult) + assert isinstance(response_1.result, PublishResult) + assert response_0.result.offset + assert response_1.result.offset + assert cast(PresenceResult, res[3]).presence == {} + + requests = [ + PublishRequest( + channel="personal_1", + data={"data": "Second data"}, + ), + PublishRequest( + channel="personal_2", + data={"data": "First data"}, + ), + BroadcastRequest( + channels=["personal_1", "personal_2"], + data={"data": "Third data"}, + ), + PresenceRequest( + channel="personal_1", + ), + ] + request = BatchRequest( - commands=[ - PublishRequest( - channel="personal_1", - data={"data": "Second data"}, - ), - PublishRequest( - channel="personal_2", - data={"data": "First data"}, - ), - BroadcastRequest( - channels=["personal_1", "personal_2"], - data={"data": "Third data"}, - ), - PresenceRequest( - channel="personal_1", - ), - ], - parallel=True, + requests=requests, ) - def check_result(res: BatchResult) -> None: - num_expected_replies = 4 - assert len(res.replies) == num_expected_replies - assert res.replies[0].offset - assert res.replies[1].offset - assert res.replies[2].responses[0].result.offset - assert res.replies[2].responses[1].result.offset - assert res.replies[3].presence == {} - result = sync_client.send(request) - check_result(result) + check_result(result.replies) result = await async_client.send(request) - check_result(result) + check_result(result.replies) async def test_error_publish(sync_client: Client, async_client: AsyncClient) -> None: From f3b5df12540cdda7afc68f2bff579980bc3502a8 Mon Sep 17 00:00:00 2001 From: FZambia Date: Sat, 17 Feb 2024 19:08:02 +0200 Subject: [PATCH 36/55] rename method --- cent/client/async_client.py | 2 +- cent/client/sync_client.py | 2 +- cent/dto.py | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cent/client/async_client.py b/cent/client/async_client.py index 71ee27c..a613228 100644 --- a/cent/client/async_client.py +++ b/cent/client/async_client.py @@ -40,7 +40,7 @@ async def send( payload, timeout=timeout, ) - response = request.get_result(content) + response = request.parse_response(content) return cast(CentResultType, response.result) async def close(self) -> None: diff --git a/cent/client/sync_client.py b/cent/client/sync_client.py index 0da6798..b1ceb58 100644 --- a/cent/client/sync_client.py +++ b/cent/client/sync_client.py @@ -40,7 +40,7 @@ def send( request.to_json(), timeout=timeout, ) - response = request.get_result(content) + response = request.parse_response(content) return cast(CentResultType, response.result) def close(self) -> None: diff --git a/cent/dto.py b/cent/dto.py index dd32189..b85a4c5 100644 --- a/cent/dto.py +++ b/cent/dto.py @@ -6,11 +6,6 @@ from cent.exceptions import CentDecodeError, CentApiResponseError -class Error(BaseModel): - code: int - message: str - - class CentResult(BaseModel, ABC): model_config = ConfigDict( use_enum_values=True, @@ -26,6 +21,11 @@ class CentResult(BaseModel, ABC): CentResultType = TypeVar("CentResultType", bound=CentResult) +class Error(BaseModel): + code: int + message: str + + class Response(BaseModel, Generic[CentResultType]): error: Optional[Error] = None result: Optional[CentResultType] = None @@ -59,7 +59,7 @@ def to_json(self) -> Any: def get_method(self) -> str: return self.__api_method__ - def get_result( + def parse_response( self, content: str, ) -> Response[CentResult]: From f47b9874a690cc96d1c5b039226abad504480172 Mon Sep 17 00:00:00 2001 From: FZambia Date: Sat, 17 Feb 2024 21:09:48 +0200 Subject: [PATCH 37/55] update readme --- README.md | 20 ++--- cent/__init__.py | 60 ++++++++++++++ cent/dto.py | 204 +++++++++++++++++++++++------------------------ 3 files changed, 172 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index 07479c0..c9ab81f 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@ pip install cent ## Usage -First of all, see the description of Centrifugo [server API](https://centrifugal.dev/docs/server/server_api) in the documentation. +First of all, see the description of Centrifugo [server API](https://centrifugal.dev/docs/server/server_api) in the documentation. This library also supports API extensions provided by Centrifugo PRO. -This library contains `Client` and `AsyncClient` to work with Centrifugo HTTP server API. Both clients have the same methods to work with Centrifugo API and raise the same top-level exceptions. +The library contains `Client` and `AsyncClient` to work with Centrifugo HTTP server API. Both clients have the same methods to work with Centrifugo API and raise the same top-level exceptions. ## Sync HTTP client @@ -38,13 +38,14 @@ Optional arguments: Example: ```python -from cent import Client +from cent import Client, PublishRequest api_url = "http://localhost:8000/api" api_key = "" client = Client(api_url, api_key) -result = client.publish("channel", {"input": "Hello world!"}) +request = PublishRequest(channel="channel", data={"input": "Hello world!"}) +result = client.send(request) print(result) ``` @@ -68,15 +69,15 @@ Example: ```python import asyncio -from cent import AsyncClient +from cent import AsyncClient, PublishRequest api_url = "http://localhost:8000/api" api_key = "" -client = AsyncClient(api_url, api_key) - async def main(): - result = await client.publish("channel", {"input": "Hello world!"}) + client = AsyncClient(api_url, api_key) + request = PublishRequest(channel="channel", data={"input": "Hello world!"}) + result = await client.send(request) print(result) if __name__ == "__main__": @@ -128,6 +129,7 @@ make bench Cent v5 contains the following notable changes compared to Cent v4: -* Constructor slightly changed, refer to the examples above. +* Client constructor slightly changed, refer to the examples above. +* To call desired API import and construct a request object (inherited from Pydantic `BaseModel`) and pass it to `send` method of the client. * Base exception class is now `CentError` instead of `CentException`, exceptions SDK raises were refactored. * To send multiple commands in one HTTP request SDK provides `batch` method. diff --git a/cent/__init__.py b/cent/__init__.py index 22c9a34..e05f9ca 100644 --- a/cent/__init__.py +++ b/cent/__init__.py @@ -40,6 +40,36 @@ Node, Publication, ClientInfo, + DeviceRegisterRequest, + DeviceRegisterResult, + DeviceUpdateRequest, + DeviceUpdateResult, + DeviceListRequest, + DeviceListResult, + DeviceRemoveRequest, + DeviceRemoveResult, + DeviceTopicListRequest, + DeviceTopicListResult, + UserTopicListRequest, + UserTopicListResult, + SendPushNotificationRequest, + SendPushNotificationResult, + PushNotification, + FcmPushNotification, + HmsPushNotification, + ApnsPushNotification, + Device, + DeviceFilter, + DeviceTopicFilter, + DeviceUserUpdate, + DeviceMetaUpdate, + DeviceTopicsUpdate, + DeviceTopicUpdateRequest, + DeviceTopicUpdateResult, + UpdatePushStatusRequest, + UpdatePushStatusResult, + CancelPushRequest, + CancelPushResult, ) from cent.exceptions import ( CentError, @@ -51,12 +81,15 @@ ) __all__ = ( + "ApnsPushNotification", "AsyncClient", "BatchRequest", "BatchResult", "BoolValue", "BroadcastRequest", "BroadcastResult", + "CancelPushRequest", + "CancelPushResult", "CentApiResponseError", "CentDecodeError", "CentError", @@ -70,13 +103,33 @@ "ChannelsResult", "Client", "ClientInfo", + "Device", + "DeviceFilter", + "DeviceListRequest", + "DeviceListResult", + "DeviceMetaUpdate", + "DeviceRegisterRequest", + "DeviceRegisterResult", + "DeviceRemoveRequest", + "DeviceRemoveResult", + "DeviceTopicFilter", + "DeviceTopicListRequest", + "DeviceTopicListResult", + "DeviceTopicUpdateRequest", + "DeviceTopicUpdateResult", + "DeviceTopicsUpdate", + "DeviceUpdateRequest", + "DeviceUpdateResult", + "DeviceUserUpdate", "Disconnect", "DisconnectRequest", "DisconnectResult", + "FcmPushNotification", "HistoryRemoveRequest", "HistoryRemoveResult", "HistoryRequest", "HistoryResult", + "HmsPushNotification", "InfoRequest", "InfoResult", "Node", @@ -88,12 +141,19 @@ "Publication", "PublishRequest", "PublishResult", + "PushNotification", "RefreshRequest", "RefreshResult", "Response", + "SendPushNotificationRequest", + "SendPushNotificationResult", "StreamPosition", "SubscribeRequest", "SubscribeResult", "UnsubscribeRequest", "UnsubscribeResult", + "UpdatePushStatusRequest", + "UpdatePushStatusResult", + "UserTopicListRequest", + "UserTopicListResult", ) diff --git a/cent/dto.py b/cent/dto.py index b85a4c5..9c9d33a 100644 --- a/cent/dto.py +++ b/cent/dto.py @@ -146,8 +146,8 @@ class Disconnect(NestedModel): reason (str): Disconnect reason. """ - code: int - reason: str + code: int = 0 + reason: str = "" class BoolValue(NestedModel): @@ -201,8 +201,8 @@ class ProcessStats(CentResult): rss (int): Process Resident Set Size (RSS) in bytes. """ - cpu: float = Field(default=0.0) - rss: int + cpu: float = 0.0 + rss: int = 0 class ClientInfo(CentResult): @@ -218,8 +218,8 @@ class ClientInfo(CentResult): settings or preferences related to the channel. """ - client: str - user: str + client: str = "" + user: str = "" conn_info: Optional[Any] = None chan_info: Optional[Any] = None @@ -234,7 +234,7 @@ class Publication(CentResult): """ data: Any - offset: int = Field(default=0) + offset: int = 0 tags: Optional[Dict[str, str]] = None @@ -246,7 +246,7 @@ class Metrics(CentResult): items (Dict[str, float]): metric values. """ - interval: float = Field(default=0.0) + interval: float = 0.0 items: Dict[str, float] @@ -268,12 +268,12 @@ class Node(CentResult): uid: str name: str - version: str - num_clients: int = Field(default=0) - num_subs: int = Field(default=0) - num_users: int = Field(default=0) - num_channels: int = Field(default=0) - uptime: int = Field(default=0) + version: str = "" + num_clients: int = 0 + num_subs: int = 0 + num_users: int = 0 + num_channels: int = 0 + uptime: int = 0 metrics: Optional[Metrics] = None process: Optional[ProcessStats] = None @@ -286,8 +286,8 @@ class PublishResult(CentResult): epoch: Epoch of current stream. """ - offset: Optional[int] = None - epoch: Optional[str] = None + offset: int = 0 + epoch: str = "" class BroadcastResult(CentResult): @@ -308,7 +308,7 @@ class ChannelInfoResult(CentResult): num_clients: Total number of connections currently subscribed to a channel. """ - num_clients: int = Field(default=0) + num_clients: int = 0 class ChannelsResult(CentResult): @@ -339,8 +339,8 @@ class HistoryResult(CentResult): """ publications: List[Publication] = Field(default_factory=list) - offset: Optional[int] = None - epoch: Optional[str] = None + offset: int = 0 + epoch: str = "" class InfoResult(CentResult): @@ -371,8 +371,8 @@ class PresenceStatsResult(CentResult): num_users: Total number of unique users in channel. """ - num_clients: int = Field(default=0) - num_users: int = Field(default=0) + num_clients: int = 0 + num_users: int = 0 class RefreshResult(CentResult): @@ -621,32 +621,30 @@ class UnsubscribeRequest(CentRequest[UnsubscribeResult]): class ConnectionTokenInfo(NestedModel): """Connection token info.""" - uid: str - issued_at: int + uid: Optional[str] = None + issued_at: Optional[int] = None class SubscriptionTokenInfo(NestedModel): """Subscription token info.""" - uid: str - issued_at: int + uid: Optional[str] = None + issued_at: Optional[int] = None class ChannelContext(NestedModel): """Channel context.""" - source: int + source: Optional[int] = None class ConnectionState(NestedModel): - """ - Connection state. - """ + """Connection state.""" - channels: Dict[str, ChannelContext] - connection_token: ConnectionTokenInfo - subscription_tokens: Dict[str, SubscriptionTokenInfo] - meta: Any + channels: Optional[Dict[str, ChannelContext]] = None + connection_token: Optional[ConnectionTokenInfo] = None + subscription_tokens: Optional[Dict[str, SubscriptionTokenInfo]] = None + meta: Optional[Any] = None class ConnectionInfo(NestedModel): @@ -656,8 +654,8 @@ class ConnectionInfo(NestedModel): app_version: Optional[str] = "" transport: str protocol: str - user: str - state: ConnectionState + user: Optional[str] = None + state: Optional[ConnectionState] = None class ConnectionsResult(CentResult): @@ -687,7 +685,6 @@ class UpdateUserStatusRequest(CentRequest[UpdateUserStatusResult]): __returning__ = UpdateUserStatusResult users: List[str] - state: str class UserStatus(NestedModel): @@ -696,9 +693,8 @@ class UserStatus(NestedModel): """ user: str - active: int - online: int - state: str + active: Optional[int] = None + online: Optional[int] = None class GetUserStatusResult(CentResult): @@ -751,7 +747,7 @@ class BlockUserRequest(CentRequest[BlockUserResult]): __api_method__ = "block_user" __returning__ = BlockUserResult - expire_at: int + expire_at: Optional[int] = None user: str @@ -786,7 +782,7 @@ class RevokeTokenRequest(CentRequest[RevokeTokenResult]): __api_method__ = "revoke_token" __returning__ = RevokeTokenResult - expire_at: int + expire_at: Optional[int] = None uid: str @@ -804,9 +800,9 @@ class InvalidateUserTokensRequest(CentRequest[InvalidateUserTokensResult]): __api_method__ = "invalidate_user_tokens" __returning__ = InvalidateUserTokensResult - expire_at: int + expire_at: Optional[int] = None user: str - issued_before: int + issued_before: Optional[int] = None channel: Optional[str] = None @@ -815,6 +811,8 @@ class DeviceRegisterResult(CentResult): Device register result. """ + id: str + class DeviceRegisterRequest(CentRequest[DeviceRegisterResult]): """ @@ -824,13 +822,13 @@ class DeviceRegisterRequest(CentRequest[DeviceRegisterResult]): __api_method__ = "device_register" __returning__ = DeviceRegisterResult - id: str + id: Optional[str] = None provider: str token: str platform: str - user: str - meta: Dict[str, str] - topics: List[str] + user: Optional[str] = None + meta: Optional[Dict[str, str]] = None + topics: Optional[List[str]] = None class DeviceUserUpdate(NestedModel): @@ -893,8 +891,8 @@ class DeviceRemoveRequest(CentRequest[DeviceRemoveResult]): __api_method__ = "device_remove" __returning__ = DeviceRemoveResult - ids: List[str] - users: List[str] + ids: Optional[List[str]] = None + users: Optional[List[str]] = None class DeviceFilter(NestedModel): @@ -902,11 +900,11 @@ class DeviceFilter(NestedModel): Device filter. """ - ids: List[str] - users: List[str] - topics: List[str] - providers: List[str] - platforms: List[str] + ids: Optional[List[str]] = None + users: Optional[List[str]] = None + topics: Optional[List[str]] = None + providers: Optional[List[str]] = None + platforms: Optional[List[str]] = None class Device(NestedModel): @@ -918,11 +916,11 @@ class Device(NestedModel): platform: str provider: str token: str - user: str + user: str = "" created_at: int updated_at: int - meta: Dict[str, str] - topics: List[str] + meta: Optional[Dict[str, str]] = None + topics: Optional[List[str]] = None class DeviceListResult(CentResult): @@ -931,8 +929,8 @@ class DeviceListResult(CentResult): """ items: List[Device] - next_cursor: str - total_count: int + next_cursor: Optional[str] = None + total_count: Optional[int] = None class DeviceListRequest(CentRequest[DeviceListResult]): @@ -943,12 +941,12 @@ class DeviceListRequest(CentRequest[DeviceListResult]): __api_method__ = "device_list" __returning__ = DeviceListResult - filter: DeviceFilter - include_total_count: bool - include_meta: bool - include_topics: bool - cursor: str - limit: int + filter: Optional[DeviceFilter] = None + include_total_count: Optional[bool] = None + include_meta: Optional[bool] = None + include_topics: Optional[bool] = None + cursor: Optional[str] = None + limit: Optional[int] = None class DeviceTopicFilter(NestedModel): @@ -956,12 +954,12 @@ class DeviceTopicFilter(NestedModel): Device topic filter. """ - device_ids: List[str] - device_providers: List[str] - device_platforms: List[str] - device_users: List[str] - topics: List[str] - topic_prefix: str + device_ids: Optional[List[str]] = None + device_providers: Optional[List[str]] = None + device_platforms: Optional[List[str]] = None + device_users: Optional[List[str]] = None + topics: Optional[List[str]] = None + topic_prefix: Optional[str] = None class DeviceTopic(NestedModel): @@ -980,8 +978,8 @@ class DeviceTopicListResult(CentResult): """ items: List[DeviceTopic] - next_cursor: str - total_count: int + next_cursor: Optional[str] = None + total_count: Optional[int] = None class DeviceTopicListRequest(CentRequest[DeviceTopicListResult]): @@ -992,11 +990,11 @@ class DeviceTopicListRequest(CentRequest[DeviceTopicListResult]): __api_method__ = "device_topic_list" __returning__ = DeviceTopicListResult - filter: DeviceTopicFilter - include_total_count: bool - include_device: bool - cursor: str - limit: int + filter: Optional[DeviceTopicFilter] = None + include_total_count: Optional[bool] = None + include_device: Optional[bool] = None + cursor: Optional[str] = None + limit: Optional[int] = None class UserTopicFilter(NestedModel): @@ -1004,9 +1002,9 @@ class UserTopicFilter(NestedModel): User topic filter. """ - users: List[str] - topics: List[str] - topic_prefix: str + users: Optional[List[str]] = None + topics: Optional[List[str]] = None + topic_prefix: Optional[str] = None class UserTopic(NestedModel): @@ -1025,8 +1023,8 @@ class UserTopicListResult(CentResult): """ items: List[UserTopic] - next_cursor: str - total_count: int + next_cursor: Optional[str] = None + total_count: Optional[int] = None class UserTopicListRequest(CentRequest[UserTopicListResult]): @@ -1037,10 +1035,10 @@ class UserTopicListRequest(CentRequest[UserTopicListResult]): __api_method__ = "user_topic_list" __returning__ = UserTopicListResult - filter: UserTopicFilter - include_total_count: bool - cursor: str - limit: int + filter: Optional[UserTopicFilter] = None + include_total_count: Optional[bool] = None + cursor: Optional[str] = None + limit: Optional[int] = None class DeviceTopicUpdateResult(CentResult): @@ -1086,14 +1084,14 @@ class PushRecipient(NestedModel): Push recipient. """ - filter: DeviceFilter - fcm_tokens: List[str] - fcm_topic: str - fcm_condition: str - hms_tokens: List[str] - hms_topic: str - hms_condition: str - apns_tokens: List[str] + filter: Optional[DeviceFilter] = None + fcm_tokens: Optional[List[str]] = None + fcm_topic: Optional[str] = None + fcm_condition: Optional[str] = None + hms_tokens: Optional[List[str]] = None + hms_topic: Optional[str] = None + hms_condition: Optional[str] = None + apns_tokens: Optional[List[str]] = None class FcmPushNotification(NestedModel): @@ -1117,7 +1115,7 @@ class ApnsPushNotification(NestedModel): APNS push notification. """ - headers: Dict[str, str] + headers: Optional[Dict[str, str]] = None payload: Any @@ -1126,10 +1124,10 @@ class PushNotification(NestedModel): Push notification. """ - fcm: FcmPushNotification - hms: HmsPushNotification - apns: ApnsPushNotification - expire_at: int + fcm: Optional[FcmPushNotification] = None + hms: Optional[HmsPushNotification] = None + apns: Optional[ApnsPushNotification] = None + expire_at: Optional[int] = None class SendPushNotificationResult(CentResult): @@ -1150,8 +1148,8 @@ class SendPushNotificationRequest(CentRequest[SendPushNotificationResult]): recipient: PushRecipient notification: PushNotification - uid: str - send_at: int + uid: Optional[str] = None + send_at: Optional[int] = None class UpdatePushStatusResult(CentResult): @@ -1171,7 +1169,7 @@ class UpdatePushStatusRequest(CentRequest[UpdatePushStatusResult]): uid: str status: str device_id: str - msg_id: str + msg_id: Optional[str] = None class CancelPushResult(CentResult): From bbc273a444cd2fe9cc613b20472a481b04fb9f35 Mon Sep 17 00:00:00 2001 From: FZambia Date: Sat, 17 Feb 2024 21:38:02 +0200 Subject: [PATCH 38/55] various cleanups and readme updates --- .github/workflows/test.yml | 6 +++++- Makefile | 8 +++---- README.md | 17 +++++++++++++-- cent/__init__.py | 42 +++++++++++++++++++++++++++++++++++++ cent/client/async_client.py | 2 +- cent/client/sync_client.py | 2 +- cent/dto.py | 8 +++---- tests/conftest.py | 11 ---------- 8 files changed, 72 insertions(+), 24 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6d2cecf..c52cc8a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,6 +42,10 @@ jobs: run: | make test - - name: Run linter + - name: Run lint run: | make lint-ci + + - name: Run mypy + run: | + make mypy diff --git a/Makefile b/Makefile index e83c3a9..66b9249 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,15 @@ -.PHONY: proto test lint lint-fix lint-ci bench +.PHONY: test lint lint-fix lint-ci mypy bench dev: pip install poetry poetry install -proto: - poetry run python -m grpc_tools.protoc -I . --python_betterproto_out=./cent/proto cent/proto/apiproto.proto - test: poetry run pytest -vv tests +mypy: + poetry run mypy cent tests benchmarks + lint: poetry run ruff . diff --git a/README.md b/README.md index c9ab81f..1e4bb6c 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@ pip install cent ## Usage -First of all, see the description of Centrifugo [server API](https://centrifugal.dev/docs/server/server_api) in the documentation. This library also supports API extensions provided by Centrifugo PRO. +First of all, see the description of Centrifugo [server API](https://centrifugal.dev/docs/server/server_api) in the documentation. This library also supports API extensions provided by Centrifugo PRO. In general, refer to [api.proto](https://github.com/centrifugal/centrifugo/blob/master/internal/apiproto/api.proto) Protobuf schema file as a source of truth about all available Centrifugo server APIs. Don't forget that Centrifugo supports both HTTP and GRPC API – so you can switch to GRPC by using `api.proto` file to generate stubs for communication. -The library contains `Client` and `AsyncClient` to work with Centrifugo HTTP server API. Both clients have the same methods to work with Centrifugo API and raise the same top-level exceptions. +This library contains `Client` and `AsyncClient` to work with Centrifugo HTTP server API. Both clients have the same methods to work with Centrifugo API and raise the same top-level exceptions. ## Sync HTTP client @@ -96,6 +96,19 @@ This library raises exceptions if sth goes wrong. All exceptions are subclasses * `CentDecodeError` - raised in case of server response decoding error * `CentApiResponseError` - raised in case of API response error (i.e. error returned by Centrifugo itself, you can inspect code and message returned by Centrifugo in this case) +## Using for async consumers + +You can use this library to constructs commands to Centrifugo over [async consumers](https://centrifugal.dev/docs/server/consumers). For example, to get proper method and payload for async publish: + +```python +from cent import PublishRequest + +request = PublishRequest(channel="channel", data={"input": "Hello world!"}) +method = request.get_api_method() +payload = request.to_json() +# use method and payload to construct async consumer event. +``` + ## For contributors ### Tests and benchmarks diff --git a/cent/__init__.py b/cent/__init__.py index e05f9ca..dec8ee1 100644 --- a/cent/__init__.py +++ b/cent/__init__.py @@ -70,6 +70,27 @@ UpdatePushStatusResult, CancelPushRequest, CancelPushResult, + UpdateUserStatusRequest, + UpdateUserStatusResult, + GetUserStatusRequest, + GetUserStatusResult, + UserStatus, + DeleteUserStatusRequest, + DeleteUserStatusResult, + BlockUserRequest, + BlockUserResult, + UnblockUserRequest, + UnblockUserResult, + RevokeTokenRequest, + RevokeTokenResult, + InvalidateUserTokensRequest, + InvalidateUserTokensResult, + ConnectionsRequest, + ConnectionsResult, + ConnectionState, + ConnectionTokenInfo, + SubscriptionTokenInfo, + ChannelContext, ) from cent.exceptions import ( CentError, @@ -85,6 +106,8 @@ "AsyncClient", "BatchRequest", "BatchResult", + "BlockUserRequest", + "BlockUserResult", "BoolValue", "BroadcastRequest", "BroadcastResult", @@ -98,11 +121,18 @@ "CentResult", "CentTransportError", "CentUnauthorizedError", + "ChannelContext", "ChannelOptionsOverride", "ChannelsRequest", "ChannelsResult", "Client", "ClientInfo", + "ConnectionState", + "ConnectionTokenInfo", + "ConnectionsRequest", + "ConnectionsResult", + "DeleteUserStatusRequest", + "DeleteUserStatusResult", "Device", "DeviceFilter", "DeviceListRequest", @@ -125,6 +155,8 @@ "DisconnectRequest", "DisconnectResult", "FcmPushNotification", + "GetUserStatusRequest", + "GetUserStatusResult", "HistoryRemoveRequest", "HistoryRemoveResult", "HistoryRequest", @@ -132,6 +164,8 @@ "HmsPushNotification", "InfoRequest", "InfoResult", + "InvalidateUserTokensRequest", + "InvalidateUserTokensResult", "Node", "PresenceRequest", "PresenceResult", @@ -145,15 +179,23 @@ "RefreshRequest", "RefreshResult", "Response", + "RevokeTokenRequest", + "RevokeTokenResult", "SendPushNotificationRequest", "SendPushNotificationResult", "StreamPosition", "SubscribeRequest", "SubscribeResult", + "SubscriptionTokenInfo", + "UnblockUserRequest", + "UnblockUserResult", "UnsubscribeRequest", "UnsubscribeResult", "UpdatePushStatusRequest", "UpdatePushStatusResult", + "UpdateUserStatusRequest", + "UpdateUserStatusResult", + "UserStatus", "UserTopicListRequest", "UserTopicListResult", ) diff --git a/cent/client/async_client.py b/cent/client/async_client.py index a613228..42def34 100644 --- a/cent/client/async_client.py +++ b/cent/client/async_client.py @@ -32,7 +32,7 @@ async def send( request: CentRequest[CentResultType], timeout: Optional[float] = None, ) -> CentResultType: - method = request.get_method() + method = request.get_api_method() payload = request.to_json() content = await self._session.make_request( self._api_key, diff --git a/cent/client/sync_client.py b/cent/client/sync_client.py index b1ceb58..4c1c420 100644 --- a/cent/client/sync_client.py +++ b/cent/client/sync_client.py @@ -36,7 +36,7 @@ def send( ) -> CentResultType: content = self._session.make_request( self._api_key, - request.get_method(), + request.get_api_method(), request.to_json(), timeout=timeout, ) diff --git a/cent/dto.py b/cent/dto.py index 9c9d33a..14823f0 100644 --- a/cent/dto.py +++ b/cent/dto.py @@ -56,7 +56,7 @@ def __api_method__(self) -> str: def to_json(self) -> Any: return self.model_dump(exclude_none=True) - def get_method(self) -> str: + def get_api_method(self) -> str: return self.__api_method__ def parse_response( @@ -650,11 +650,11 @@ class ConnectionState(NestedModel): class ConnectionInfo(NestedModel): """Connection info.""" - app_name: Optional[str] = "" - app_version: Optional[str] = "" + app_name: str = "" + app_version: str = "" transport: str protocol: str - user: Optional[str] = None + user: str = "" state: Optional[ConnectionState] = None diff --git a/tests/conftest.py b/tests/conftest.py index eb57d35..1a3301a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -31,14 +31,3 @@ async def async_client( client = AsyncClient(BASE_URL, API_KEY) yield client await client._session.close() - - -@pytest.fixture() -async def clients( - anyio_backend: Any, # noqa: ARG001 -) -> AsyncGenerator[Any, None]: - sync_client = Client(BASE_URL, API_KEY) - async_client = AsyncClient(BASE_URL, API_KEY) - yield sync_client, async_client - await async_client.close() - sync_client.close() From ae304ad6402806387378ecebbfa9d44284ca5073 Mon Sep 17 00:00:00 2001 From: FZambia Date: Sat, 17 Feb 2024 22:07:33 +0200 Subject: [PATCH 39/55] various cleanups --- README.md | 6 +++--- cent/client/async_client.py | 4 ++-- cent/client/sync_client.py | 4 ++-- cent/dto.py | 9 ++++++--- tests/test_validation.py | 14 +++++++++++--- 5 files changed, 24 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 1e4bb6c..eb05550 100644 --- a/README.md +++ b/README.md @@ -98,14 +98,14 @@ This library raises exceptions if sth goes wrong. All exceptions are subclasses ## Using for async consumers -You can use this library to constructs commands to Centrifugo over [async consumers](https://centrifugal.dev/docs/server/consumers). For example, to get proper method and payload for async publish: +You can use this library to constructs events for Centrifugo [async consumers](https://centrifugal.dev/docs/server/consumers). For example, to get proper method and payload for async publish: ```python from cent import PublishRequest request = PublishRequest(channel="channel", data={"input": "Hello world!"}) -method = request.get_api_method() -payload = request.to_json() +method = request.api_method +payload = request.api_payload # use method and payload to construct async consumer event. ``` diff --git a/cent/client/async_client.py b/cent/client/async_client.py index 42def34..056b174 100644 --- a/cent/client/async_client.py +++ b/cent/client/async_client.py @@ -32,8 +32,8 @@ async def send( request: CentRequest[CentResultType], timeout: Optional[float] = None, ) -> CentResultType: - method = request.get_api_method() - payload = request.to_json() + method = request.api_method + payload = request.api_payload content = await self._session.make_request( self._api_key, method, diff --git a/cent/client/sync_client.py b/cent/client/sync_client.py index 4c1c420..65135f0 100644 --- a/cent/client/sync_client.py +++ b/cent/client/sync_client.py @@ -36,8 +36,8 @@ def send( ) -> CentResultType: content = self._session.make_request( self._api_key, - request.get_api_method(), - request.to_json(), + request.api_method, + request.api_payload, timeout=timeout, ) response = request.parse_response(content) diff --git a/cent/dto.py b/cent/dto.py index 14823f0..828d514 100644 --- a/cent/dto.py +++ b/cent/dto.py @@ -53,10 +53,12 @@ def __returning__(self) -> type: def __api_method__(self) -> str: pass - def to_json(self) -> Any: + @property + def api_payload(self) -> Any: return self.model_dump(exclude_none=True) - def get_api_method(self) -> str: + @property + def api_method(self) -> str: return self.__api_method__ def parse_response( @@ -115,7 +117,8 @@ class BatchRequest(CentRequest[BatchResult]): requests: List[Any] parallel: Optional[bool] = None - def to_json(self) -> Any: + @property + def api_payload(self) -> Any: commands = [ {request.__api_method__: request.model_dump(exclude_none=True)} for request in self.requests diff --git a/tests/test_validation.py b/tests/test_validation.py index 8eb031f..37f7b3a 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -39,7 +39,7 @@ def test_serialization_none() -> None: channel="personal_1", data={"data": None}, ) - assert request.to_json() == {"channel": "personal_1", "data": {"data": None}} + assert request.api_payload == {"channel": "personal_1", "data": {"data": None}} def test_serialization_batch() -> None: @@ -53,10 +53,10 @@ def test_serialization_batch() -> None: data={"data": "First data"}, ), ] - batch_request = BatchRequest( + request = BatchRequest( requests=requests, ) - assert batch_request.to_json() == { + assert request.api_payload == { "commands": [ {"publish": {"channel": "personal_1", "data": {"data": "Second data"}}}, {"publish": {"channel": "personal_2", "data": {"data": "First data"}}}, @@ -65,6 +65,14 @@ def test_serialization_batch() -> None: } +def test_method() -> None: + request = PublishRequest( + channel="personal_1", + data={"data": None}, + ) + assert request.api_method == "publish" + + async def test_publish(sync_client: Client, async_client: AsyncClient) -> None: request = PublishRequest( channel="personal_1", From 96a2749aedfdfc146382094adf26c186925d5df9 Mon Sep 17 00:00:00 2001 From: FZambia Date: Sat, 17 Feb 2024 22:38:57 +0200 Subject: [PATCH 40/55] describe broadcast and batch behaviour --- README.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/README.md b/README.md index eb05550..aaffda8 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,46 @@ This library raises exceptions if sth goes wrong. All exceptions are subclasses * `CentDecodeError` - raised in case of server response decoding error * `CentApiResponseError` - raised in case of API response error (i.e. error returned by Centrifugo itself, you can inspect code and message returned by Centrifugo in this case) +Note, that `BroadcastRequest` and `BatchRequest` are quite special – since they contain multiple commands in one request, handling `CentApiResponseError` is still required, but not enough – you also need to manually iterate over the results to check for individual errors. For example, one publish command can fail while another one can succeed. For example: + +```python +from cent import * +c = Client("http://localhost:8000/api", "api_key") +req = BroadcastRequest(channels=["1", "2"], data={}) +c.send(req) +# BroadcastResult( +# responses=[ +# Response[PublishResult](error=None, result=PublishResult(offset=7, epoch='rqKx')), +# Response[PublishResult](error=None, result=PublishResult(offset=7, epoch='nUrf')) +# ] +# ) +req = BroadcastRequest(channels=["invalid:1", "2"], data={}) +c.send(req) +# BroadcastResult( +# responses=[ +# Response[PublishResult](error=Error(code=102, message='unknown channel'), result=None), +# Response[PublishResult](error=None, result=PublishResult(offset=8, epoch='nUrf')) +# ] +# ) +``` + +I.e. `cent` library does not raise exceptions for individual errors in `BroadcastRequest` or `BatchRequest`, only for top-level response error, for example, sending empty list of channels in broadcast: + +``` +req = BroadcastRequest(channels=[], data={}) +c.send(req) +Traceback (most recent call last): + File "", line 1, in + File "/Users/centrifugal/pycent/cent/client/sync_client.py", line 43, in send + response = request.parse_response(content) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Users/centrifugal/pycent/cent/dto.py", line 85, in parse_response + raise CentApiResponseError( +cent.exceptions.CentApiResponseError: Server API response error #107: bad request +``` + +So this all adds some complexity, but that's the trade-off for the performance and efficiency of these two methods. You can always write some convenient wrappers around `cent` library to handle errors in a way that suits your application. + ## Using for async consumers You can use this library to constructs events for Centrifugo [async consumers](https://centrifugal.dev/docs/server/consumers). For example, to get proper method and payload for async publish: From ace03f05587f47d864258c75cce1dec8b0d4cf01 Mon Sep 17 00:00:00 2001 From: FZambia Date: Sat, 17 Feb 2024 22:46:07 +0200 Subject: [PATCH 41/55] add types-requests --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 6f18ead..618fa58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ python = "^3.9" aiohttp = "^3" pydantic = "^2" requests = "^2" +types-requests = "^2" [tool.poetry.group.dev.dependencies] pre-commit = "^3.6.0" From c622b976806447ba5ac7a7cbe8d3897f5945dc31 Mon Sep 17 00:00:00 2001 From: FZambia Date: Sat, 17 Feb 2024 22:47:33 +0200 Subject: [PATCH 42/55] poetry lock --- poetry.lock | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index b454a49..4818b74 100644 --- a/poetry.lock +++ b/poetry.lock @@ -799,13 +799,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pytest" -version = "8.0.0" +version = "8.0.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.0.0-py3-none-any.whl", hash = "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"}, - {file = "pytest-8.0.0.tar.gz", hash = "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c"}, + {file = "pytest-8.0.1-py3-none-any.whl", hash = "sha256:3e4f16fe1c0a9dc9d9389161c127c3edc5d810c38d6793042fb81d9f48a59fca"}, + {file = "pytest-8.0.1.tar.gz", hash = "sha256:267f6563751877d772019b13aacbe4e860d73fe8f651f28112e9ac37de7513ae"}, ] [package.dependencies] @@ -991,6 +991,20 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "types-requests" +version = "2.31.0.20240125" +description = "Typing stubs for requests" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-requests-2.31.0.20240125.tar.gz", hash = "sha256:03a28ce1d7cd54199148e043b2079cdded22d6795d19a2c2a6791a4b2b5e2eb5"}, + {file = "types_requests-2.31.0.20240125-py3-none-any.whl", hash = "sha256:9592a9a4cb92d6d75d9b491a41477272b710e021011a2a3061157e2fb1f1a5d1"}, +] + +[package.dependencies] +urllib3 = ">=2" + [[package]] name = "typing-extensions" version = "4.9.0" @@ -1145,4 +1159,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "d77bf417174a1e7c0d3d8fddd47d1b5bf7c2f604315b48412f0fdb687593aa8c" +content-hash = "ae6d1bdb8a4c46ee131a7348a709d0021576a5c3725dc6424a7396a638be2321" From 988ad181cb572cf09bd3d32efc2169597ccc7da5 Mon Sep 17 00:00:00 2001 From: FZambia Date: Sat, 17 Feb 2024 23:59:42 +0200 Subject: [PATCH 43/55] update readme --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/README.md b/README.md index aaffda8..9d6c9ca 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,55 @@ payload = request.api_payload # use method and payload to construct async consumer event. ``` +## Using Broadcast and Batch + +To demonstrate the benefits of using `BroadcastRequest` and `BatchRequest` let's compare approaches. Let's say at some point in your app you need to publish the same message into 10k different channels. Let's compare sequential publish, batch publish and broadcast publish. Here is the code to do the comparison: + +```python +from cent import * +from time import time + + +def main(): + requests = [] + channels = [] + for i in range(10000): + channel = f"test_{i}" + requests.append(PublishRequest(channel=channel, data={"msg": "hello"})) + channels.append(channel) + batch = BatchRequest(requests=requests) + broadcast = BroadcastRequest(channels=channels, data={"msg": "hello"}) + + client = Client("http://localhost:8000/api", "api_key") + + start = time() + for request in requests: + client.send(request) + print("sequential", time() - start) + + start = time() + client.send(batch) + print("batch", time() - start) + + start = time() + client.send(broadcast) + print("broadcast", time() - start) + + +if __name__ == "__main__": + main() +``` + +On local machine, the output may look like this: + +``` +sequential 5.731332778930664 +batch 0.12313580513000488 +broadcast 0.06050515174865723 +``` + +So `BatchRequest` is much faster than sequential requests in this case, and `BroadcastRequest` is the fastest - publication to 10k Centrifugo channels took only 60ms. Because all the work is done in one network round-trip. In reality the difference will be even more significant because of network latency. + ## For contributors ### Tests and benchmarks From fceca825505986fc489fc424203ca02c3e3914e6 Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Sat, 2 Mar 2024 19:34:51 +0200 Subject: [PATCH 44/55] update dto --- cent/dto.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/cent/dto.py b/cent/dto.py index 828d514..6019148 100644 --- a/cent/dto.py +++ b/cent/dto.py @@ -830,6 +830,8 @@ class DeviceRegisterRequest(CentRequest[DeviceRegisterResult]): token: str platform: str user: Optional[str] = None + timezone: Optional[str] = None + language: Optional[str] = None meta: Optional[Dict[str, str]] = None topics: Optional[List[str]] = None @@ -842,6 +844,22 @@ class DeviceUserUpdate(NestedModel): user: str +class DeviceTimezoneUpdate(NestedModel): + """ + Device timezone update. + """ + + timezone: str + + +class DeviceLanguageUpdate(NestedModel): + """ + Device language update. + """ + + language: str + + class DeviceMetaUpdate(NestedModel): """ Device meta update. @@ -876,6 +894,8 @@ class DeviceUpdateRequest(CentRequest[DeviceUpdateResult]): ids: Optional[List[str]] = None users: Optional[List[str]] = None user_update: Optional[DeviceUserUpdate] = None + timezone_update: Optional[DeviceTimezoneUpdate] = None + language_update: Optional[DeviceLanguageUpdate] = None meta_update: Optional[DeviceMetaUpdate] = None topics_update: Optional[DeviceTopicsUpdate] = None @@ -1134,13 +1154,36 @@ class PushNotification(NestedModel): class SendPushNotificationResult(CentResult): - """ - Send push notification result. - """ - + """Send push notification result.""" uid: str +class PushLocalization(NestedModel): + translations: Dict[str, str] + + +class RateLimitPolicy(NestedModel): + rate: int + interval_ms: int + + +class PushRateLimitStrategy(NestedModel): + key: str + policies: List[RateLimitPolicy] + drop_if_rate_limited: bool + + +class PushTimeLimitStrategy(NestedModel): + send_after_time: str + send_before_time: str + no_tz_send_now: bool + + +class PushLimitStrategy(NestedModel): + rate_limit: Optional[PushRateLimitStrategy] = None + time_limit: Optional[PushTimeLimitStrategy] = None + + class SendPushNotificationRequest(CentRequest[SendPushNotificationResult]): """ Send push notification request. @@ -1153,6 +1196,12 @@ class SendPushNotificationRequest(CentRequest[SendPushNotificationResult]): notification: PushNotification uid: Optional[str] = None send_at: Optional[int] = None + analytics_uid: Optional[str] = None + optimize_for_reliability: Optional[bool] = None + limit_strategy: Optional[PushLimitStrategy] = None + localizations: Optional[Dict[str, PushLocalization]] = None + use_templating: Optional[bool] = None + use_meta: Optional[bool] = None class UpdatePushStatusResult(CentResult): @@ -1169,7 +1218,7 @@ class UpdatePushStatusRequest(CentRequest[UpdatePushStatusResult]): __api_method__ = "update_push_status" __returning__ = UpdatePushStatusResult - uid: str + alalytics_uid: str status: str device_id: str msg_id: Optional[str] = None From b97a5eb3a523edf0fd7fffd8b6550c63fc70e569 Mon Sep 17 00:00:00 2001 From: FZambia Date: Mon, 4 Mar 2024 17:55:41 +0200 Subject: [PATCH 45/55] add client api methods --- README.md | 13 +- benchmarks/test_publish.py | 4 +- cent/client/async_client.py | 295 ++++++++++++++++++++++++++++++++++- cent/client/sync_client.py | 296 +++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + tests/test_validation.py | 58 +++---- 6 files changed, 625 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 9d6c9ca..303ec0f 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ api_key = "" client = Client(api_url, api_key) request = PublishRequest(channel="channel", data={"input": "Hello world!"}) -result = client.send(request) +result = client._send(request) print(result) ``` @@ -100,9 +100,10 @@ Note, that `BroadcastRequest` and `BatchRequest` are quite special – since the ```python from cent import * + c = Client("http://localhost:8000/api", "api_key") req = BroadcastRequest(channels=["1", "2"], data={}) -c.send(req) +c._send(req) # BroadcastResult( # responses=[ # Response[PublishResult](error=None, result=PublishResult(offset=7, epoch='rqKx')), @@ -110,7 +111,7 @@ c.send(req) # ] # ) req = BroadcastRequest(channels=["invalid:1", "2"], data={}) -c.send(req) +c._send(req) # BroadcastResult( # responses=[ # Response[PublishResult](error=Error(code=102, message='unknown channel'), result=None), @@ -172,15 +173,15 @@ def main(): start = time() for request in requests: - client.send(request) + client._send(request) print("sequential", time() - start) start = time() - client.send(batch) + client._send(batch) print("batch", time() - start) start = time() - client.send(broadcast) + client._send(broadcast) print("broadcast", time() - start) diff --git a/benchmarks/test_publish.py b/benchmarks/test_publish.py index b62f09f..5d0e8cb 100644 --- a/benchmarks/test_publish.py +++ b/benchmarks/test_publish.py @@ -6,7 +6,7 @@ def sync_requests(client: Client) -> None: channel_number = random.randint(0, 1000) # noqa: S311 - client.send( + client._send( PublishRequest( channel=f"personal_{channel_number}", data={"message": "Hello world!"}, @@ -16,7 +16,7 @@ def sync_requests(client: Client) -> None: async def async_requests(client: AsyncClient) -> None: channel_number = random.randint(0, 1000) # noqa: S311 - await client.send( + await client.publish( PublishRequest( channel=f"personal_{channel_number}", data={"message": "Hello world!"}, diff --git a/cent/client/async_client.py b/cent/client/async_client.py index 056b174..e10f1e3 100644 --- a/cent/client/async_client.py +++ b/cent/client/async_client.py @@ -3,7 +3,74 @@ from aiohttp import ClientSession from cent.client.session import AiohttpSession -from cent.dto import CentRequest, CentResultType +from cent.dto import ( + CentRequest, + CentResultType, + PublishResult, + PublishRequest, + BroadcastRequest, + BroadcastResult, + BatchResult, + BatchRequest, + CancelPushResult, + CancelPushRequest, + UpdatePushStatusResult, + UpdatePushStatusRequest, + SendPushNotificationResult, + SendPushNotificationRequest, + UserTopicUpdateResult, + UserTopicUpdateRequest, + UserTopicListResult, + UserTopicListRequest, + DeviceTopicUpdateResult, + DeviceTopicUpdateRequest, + DeviceTopicListResult, + DeviceTopicListRequest, + DeviceListResult, + DeviceListRequest, + DeviceRemoveResult, + DeviceRemoveRequest, + DeviceUpdateResult, + DeviceUpdateRequest, + DeviceRegisterResult, + DeviceRegisterRequest, + InvalidateUserTokensResult, + InvalidateUserTokensRequest, + RevokeTokenResult, + RevokeTokenRequest, + UnblockUserResult, + UnblockUserRequest, + BlockUserResult, + BlockUserRequest, + DeleteUserStatusResult, + DeleteUserStatusRequest, + GetUserStatusResult, + GetUserStatusRequest, + UpdateUserStatusResult, + UpdateUserStatusRequest, + ConnectionsResult, + ConnectionsRequest, + ChannelsResult, + ChannelsRequest, + RefreshResult, + RefreshRequest, + InfoResult, + InfoRequest, + HistoryRemoveResult, + HistoryRemoveRequest, + HistoryResult, + HistoryRequest, + PresenceStatsResult, + PresenceStatsRequest, + PresenceResult, + PresenceRequest, + DisconnectResult, + DisconnectRequest, + UnsubscribeResult, + UnsubscribeRequest, + SubscribeResult, + SubscribeRequest, +) class AsyncClient: @@ -27,7 +94,7 @@ def __init__( session=session, ) - async def send( + async def _send( self, request: CentRequest[CentResultType], timeout: Optional[float] = None, @@ -43,6 +110,230 @@ async def send( response = request.parse_response(content) return cast(CentResultType, response.result) + async def publish( + self, + request: PublishRequest, + timeout: Optional[float] = None, + ) -> PublishResult: + return await self._send(request, timeout=timeout) + + async def broadcast( + self, + request: BroadcastRequest, + timeout: Optional[float] = None, + ) -> BroadcastResult: + return await self._send(request, timeout=timeout) + + async def subscribe( + self, + request: SubscribeRequest, + timeout: Optional[float] = None, + ) -> SubscribeResult: + return await self._send(request, timeout=timeout) + + async def unsubscribe( + self, + request: UnsubscribeRequest, + timeout: Optional[float] = None, + ) -> UnsubscribeResult: + return await self._send(request, timeout=timeout) + + async def disconnect( + self, + request: DisconnectRequest, + timeout: Optional[float] = None, + ) -> DisconnectResult: + return await self._send(request, timeout=timeout) + + async def presence( + self, + request: PresenceRequest, + timeout: Optional[float] = None, + ) -> PresenceResult: + return await self._send(request, timeout=timeout) + + async def presence_stats( + self, + request: PresenceStatsRequest, + timeout: Optional[float] = None, + ) -> PresenceStatsResult: + return await self._send(request, timeout=timeout) + + async def history( + self, + request: HistoryRequest, + timeout: Optional[float] = None, + ) -> HistoryResult: + return await self._send(request, timeout=timeout) + + async def history_remove( + self, + request: HistoryRemoveRequest, + timeout: Optional[float] = None, + ) -> HistoryRemoveResult: + return await self._send(request, timeout=timeout) + + async def info( + self, + request: InfoRequest, + timeout: Optional[float] = None, + ) -> InfoResult: + return await self._send(request, timeout=timeout) + + async def refresh( + self, + request: RefreshRequest, + timeout: Optional[float] = None, + ) -> RefreshResult: + return await self._send(request, timeout=timeout) + + async def channels( + self, + request: ChannelsRequest, + timeout: Optional[float] = None, + ) -> ChannelsResult: + return await self._send(request, timeout=timeout) + + async def connections( + self, + request: ConnectionsRequest, + timeout: Optional[float] = None, + ) -> ConnectionsResult: + return await self._send(request, timeout=timeout) + + async def update_user_status( + self, + request: UpdateUserStatusRequest, + timeout: Optional[float] = None, + ) -> UpdateUserStatusResult: + return await self._send(request, timeout=timeout) + + async def get_user_status( + self, + request: GetUserStatusRequest, + timeout: Optional[float] = None, + ) -> GetUserStatusResult: + return await self._send(request, timeout=timeout) + + async def delete_user_status( + self, + request: DeleteUserStatusRequest, + timeout: Optional[float] = None, + ) -> DeleteUserStatusResult: + return await self._send(request, timeout=timeout) + + async def block_user( + self, + request: BlockUserRequest, + timeout: Optional[float] = None, + ) -> BlockUserResult: + return await self._send(request, timeout=timeout) + + async def unblock_user( + self, + request: UnblockUserRequest, + timeout: Optional[float] = None, + ) -> UnblockUserResult: + return await self._send(request, timeout=timeout) + + async def revoke_token( + self, + request: RevokeTokenRequest, + timeout: Optional[float] = None, + ) -> RevokeTokenResult: + return await self._send(request, timeout=timeout) + + async def invalidate_user_tokens( + self, + request: InvalidateUserTokensRequest, + timeout: Optional[float] = None, + ) -> InvalidateUserTokensResult: + return await self._send(request, timeout=timeout) + + async def device_register( + self, + request: DeviceRegisterRequest, + timeout: Optional[float] = None, + ) -> DeviceRegisterResult: + return await self._send(request, timeout=timeout) + + async def device_update( + self, + request: DeviceUpdateRequest, + timeout: Optional[float] = None, + ) -> DeviceUpdateResult: + return await self._send(request, timeout=timeout) + + async def device_remove( + self, + request: DeviceRemoveRequest, + timeout: Optional[float] = None, + ) -> DeviceRemoveResult: + return await self._send(request, timeout=timeout) + + async def device_list( + self, + request: DeviceListRequest, + timeout: Optional[float] = None, + ) -> DeviceListResult: + return await self._send(request, timeout=timeout) + + async def device_topic_list( + self, + request: DeviceTopicListRequest, + timeout: Optional[float] = None, + ) -> DeviceTopicListResult: + return await self._send(request, timeout=timeout) + + async def device_topic_update( + self, + request: DeviceTopicUpdateRequest, + timeout: Optional[float] = None, + ) -> DeviceTopicUpdateResult: + return await self._send(request, timeout=timeout) + + async def user_topic_list( + self, + request: UserTopicListRequest, + timeout: Optional[float] = None, + ) -> UserTopicListResult: + return await self._send(request, timeout=timeout) + + async def user_topic_update( + self, + request: UserTopicUpdateRequest, + timeout: Optional[float] = None, + ) -> UserTopicUpdateResult: + return await self._send(request, timeout=timeout) + + async def send_push_notification( + self, + request: SendPushNotificationRequest, + timeout: Optional[float] = None, + ) -> SendPushNotificationResult: + return await self._send(request, timeout=timeout) + + async def update_push_status( + self, + request: UpdatePushStatusRequest, + timeout: Optional[float] = None, + ) -> UpdatePushStatusResult: + return await self._send(request, timeout=timeout) + + async def cancel_push( + self, + request: CancelPushRequest, + timeout: Optional[float] = None, + ) -> CancelPushResult: + return await self._send(request, timeout=timeout) + + async def batch( + self, + request: BatchRequest, + timeout: Optional[float] = None, + ) -> BatchResult: + return await self._send(request, timeout=timeout) + async def close(self) -> None: await self._session.close() diff --git a/cent/client/sync_client.py b/cent/client/sync_client.py index 65135f0..b80e313 100644 --- a/cent/client/sync_client.py +++ b/cent/client/sync_client.py @@ -1,9 +1,75 @@ from typing import Optional, Any, cast from requests import Session - from cent.client.session import RequestsSession -from cent.dto import CentRequest, CentResultType +from cent.dto import ( + CentRequest, + CentResultType, + BatchResult, + BatchRequest, + CancelPushResult, + CancelPushRequest, + UpdatePushStatusResult, + UpdatePushStatusRequest, + SendPushNotificationResult, + SendPushNotificationRequest, + UserTopicUpdateResult, + UserTopicUpdateRequest, + UserTopicListResult, + UserTopicListRequest, + DeviceTopicUpdateResult, + DeviceTopicUpdateRequest, + DeviceTopicListResult, + DeviceTopicListRequest, + DeviceListResult, + DeviceListRequest, + DeviceRemoveResult, + DeviceRemoveRequest, + DeviceUpdateResult, + DeviceUpdateRequest, + DeviceRegisterResult, + DeviceRegisterRequest, + InvalidateUserTokensResult, + InvalidateUserTokensRequest, + RevokeTokenResult, + RevokeTokenRequest, + UnblockUserResult, + UnblockUserRequest, + BlockUserResult, + BlockUserRequest, + DeleteUserStatusResult, + DeleteUserStatusRequest, + GetUserStatusResult, + GetUserStatusRequest, + UpdateUserStatusResult, + UpdateUserStatusRequest, + ConnectionsResult, + ConnectionsRequest, + ChannelsResult, + ChannelsRequest, + RefreshResult, + RefreshRequest, + InfoResult, + InfoRequest, + HistoryRemoveResult, + HistoryRemoveRequest, + HistoryResult, + HistoryRequest, + PresenceStatsResult, + PresenceStatsRequest, + PresenceResult, + PresenceRequest, + DisconnectResult, + DisconnectRequest, + UnsubscribeResult, + UnsubscribeRequest, + SubscribeResult, + SubscribeRequest, + BroadcastResult, + BroadcastRequest, + PublishResult, + PublishRequest, +) class Client: @@ -29,7 +95,7 @@ def __init__( session=session, ) - def send( + def _send( self, request: CentRequest[CentResultType], timeout: Optional[float] = None, @@ -43,6 +109,230 @@ def send( response = request.parse_response(content) return cast(CentResultType, response.result) + def publish( + self, + request: PublishRequest, + timeout: Optional[float] = None, + ) -> PublishResult: + return self._send(request, timeout=timeout) + + def broadcast( + self, + request: BroadcastRequest, + timeout: Optional[float] = None, + ) -> BroadcastResult: + return self._send(request, timeout=timeout) + + def subscribe( + self, + request: SubscribeRequest, + timeout: Optional[float] = None, + ) -> SubscribeResult: + return self._send(request, timeout=timeout) + + def unsubscribe( + self, + request: UnsubscribeRequest, + timeout: Optional[float] = None, + ) -> UnsubscribeResult: + return self._send(request, timeout=timeout) + + def disconnect( + self, + request: DisconnectRequest, + timeout: Optional[float] = None, + ) -> DisconnectResult: + return self._send(request, timeout=timeout) + + def presence( + self, + request: PresenceRequest, + timeout: Optional[float] = None, + ) -> PresenceResult: + return self._send(request, timeout=timeout) + + def presence_stats( + self, + request: PresenceStatsRequest, + timeout: Optional[float] = None, + ) -> PresenceStatsResult: + return self._send(request, timeout=timeout) + + def history( + self, + request: HistoryRequest, + timeout: Optional[float] = None, + ) -> HistoryResult: + return self._send(request, timeout=timeout) + + def history_remove( + self, + request: HistoryRemoveRequest, + timeout: Optional[float] = None, + ) -> HistoryRemoveResult: + return self._send(request, timeout=timeout) + + def info( + self, + request: InfoRequest, + timeout: Optional[float] = None, + ) -> InfoResult: + return self._send(request, timeout=timeout) + + def refresh( + self, + request: RefreshRequest, + timeout: Optional[float] = None, + ) -> RefreshResult: + return self._send(request, timeout=timeout) + + def channels( + self, + request: ChannelsRequest, + timeout: Optional[float] = None, + ) -> ChannelsResult: + return self._send(request, timeout=timeout) + + def connections( + self, + request: ConnectionsRequest, + timeout: Optional[float] = None, + ) -> ConnectionsResult: + return self._send(request, timeout=timeout) + + def update_user_status( + self, + request: UpdateUserStatusRequest, + timeout: Optional[float] = None, + ) -> UpdateUserStatusResult: + return self._send(request, timeout=timeout) + + def get_user_status( + self, + request: GetUserStatusRequest, + timeout: Optional[float] = None, + ) -> GetUserStatusResult: + return self._send(request, timeout=timeout) + + def delete_user_status( + self, + request: DeleteUserStatusRequest, + timeout: Optional[float] = None, + ) -> DeleteUserStatusResult: + return self._send(request, timeout=timeout) + + def block_user( + self, + request: BlockUserRequest, + timeout: Optional[float] = None, + ) -> BlockUserResult: + return self._send(request, timeout=timeout) + + def unblock_user( + self, + request: UnblockUserRequest, + timeout: Optional[float] = None, + ) -> UnblockUserResult: + return self._send(request, timeout=timeout) + + def revoke_token( + self, + request: RevokeTokenRequest, + timeout: Optional[float] = None, + ) -> RevokeTokenResult: + return self._send(request, timeout=timeout) + + def invalidate_user_tokens( + self, + request: InvalidateUserTokensRequest, + timeout: Optional[float] = None, + ) -> InvalidateUserTokensResult: + return self._send(request, timeout=timeout) + + def device_register( + self, + request: DeviceRegisterRequest, + timeout: Optional[float] = None, + ) -> DeviceRegisterResult: + return self._send(request, timeout=timeout) + + def device_update( + self, + request: DeviceUpdateRequest, + timeout: Optional[float] = None, + ) -> DeviceUpdateResult: + return self._send(request, timeout=timeout) + + def device_remove( + self, + request: DeviceRemoveRequest, + timeout: Optional[float] = None, + ) -> DeviceRemoveResult: + return self._send(request, timeout=timeout) + + def device_list( + self, + request: DeviceListRequest, + timeout: Optional[float] = None, + ) -> DeviceListResult: + return self._send(request, timeout=timeout) + + def device_topic_list( + self, + request: DeviceTopicListRequest, + timeout: Optional[float] = None, + ) -> DeviceTopicListResult: + return self._send(request, timeout=timeout) + + def device_topic_update( + self, + request: DeviceTopicUpdateRequest, + timeout: Optional[float] = None, + ) -> DeviceTopicUpdateResult: + return self._send(request, timeout=timeout) + + def user_topic_list( + self, + request: UserTopicListRequest, + timeout: Optional[float] = None, + ) -> UserTopicListResult: + return self._send(request, timeout=timeout) + + def user_topic_update( + self, + request: UserTopicUpdateRequest, + timeout: Optional[float] = None, + ) -> UserTopicUpdateResult: + return self._send(request, timeout=timeout) + + def send_push_notification( + self, + request: SendPushNotificationRequest, + timeout: Optional[float] = None, + ) -> SendPushNotificationResult: + return self._send(request, timeout=timeout) + + def update_push_status( + self, + request: UpdatePushStatusRequest, + timeout: Optional[float] = None, + ) -> UpdatePushStatusResult: + return self._send(request, timeout=timeout) + + def cancel_push( + self, + request: CancelPushRequest, + timeout: Optional[float] = None, + ) -> CancelPushResult: + return self._send(request, timeout=timeout) + + def batch( + self, + request: BatchRequest, + timeout: Optional[float] = None, + ) -> BatchResult: + return self._send(request, timeout=timeout) + def close(self) -> None: self._session.close() diff --git a/pyproject.toml b/pyproject.toml index 618fa58..70fa6cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,6 +74,7 @@ ignore = [ "PGH003", # use specific rule code when ignore "T201", "PLR0917", + "PLR0904", # Centrifugo has many API methods ] [tool.ruff.per-file-ignores] diff --git a/tests/test_validation.py b/tests/test_validation.py index 37f7b3a..49fc7c7 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -81,10 +81,10 @@ async def test_publish(sync_client: Client, async_client: AsyncClient) -> None: tags={"tag": "tag"}, idempotency_key="idempotency_key", ) - result = sync_client.send(request) + result = sync_client._send(request) assert result.offset - result = await async_client.send(request) + result = await async_client.publish(request) assert result.offset @@ -105,10 +105,10 @@ def check_result(res: BroadcastResult) -> None: tags={"tag": "tag"}, idempotency_key="idempotency_key", ) - result = sync_client.send(request) + result = sync_client._send(request) check_result(result) - result = await async_client.send(request) + result = await async_client.broadcast(request) check_result(result) @@ -124,8 +124,8 @@ async def test_subscribe(sync_client: Client, async_client: AsyncClient) -> None epoch="1", ), ) - sync_client.send(request) - await async_client.send(request) + sync_client._send(request) + await async_client.subscribe(request) async def test_unsubscribe(sync_client: Client, async_client: AsyncClient) -> None: @@ -135,31 +135,31 @@ async def test_unsubscribe(sync_client: Client, async_client: AsyncClient) -> No session="session", client="client", ) - sync_client.send(request) - await async_client.send(request) + sync_client._send(request) + await async_client.unsubscribe(request) async def test_presence(sync_client: Client, async_client: AsyncClient) -> None: request = PresenceRequest( channel="personal_1", ) - sync_client.send(request) - await async_client.send(request) + sync_client._send(request) + await async_client.presence(request) async def test_presence_stats(sync_client: Client, async_client: AsyncClient) -> None: request = PresenceStatsRequest( channel="personal_1", ) - sync_client.send(request) - await async_client.send(request) + sync_client._send(request) + await async_client.presence_stats(request) async def test_history(sync_client: Client, async_client: AsyncClient) -> None: num_pubs = 10 channel = "personal_" + uuid.uuid4().hex for i in range(num_pubs): - sync_client.send( + sync_client._send( PublishRequest( channel=channel, data={"data": f"data {i}"}, @@ -178,10 +178,10 @@ def check_result(res: HistoryResult) -> None: assert len(res.publications) == num_pubs assert res.publications[0].data == {"data": "data 0"} - result = sync_client.send(request) + result = sync_client._send(request) check_result(result) - result = await async_client.send(request) + result = await async_client.history(request) check_result(result) @@ -189,20 +189,20 @@ async def test_history_remove(sync_client: Client, async_client: AsyncClient) -> request = HistoryRemoveRequest( channel="personal_1", ) - sync_client.send(request) - await async_client.send(request) + sync_client._send(request) + await async_client.history_remove(request) async def test_info(sync_client: Client, async_client: AsyncClient) -> None: - sync_client.send(InfoRequest()) - await async_client.send(InfoRequest()) + sync_client._send(InfoRequest()) + await async_client.info(InfoRequest()) async def test_channels(sync_client: Client, async_client: AsyncClient) -> None: request = ChannelsRequest(pattern="*") - sync_client.send(request) - await async_client.send(request) + sync_client._send(request) + await async_client.channels(request) async def test_disconnect(sync_client: Client, async_client: AsyncClient) -> None: @@ -217,8 +217,8 @@ async def test_disconnect(sync_client: Client, async_client: AsyncClient) -> Non ), ) - sync_client.send(request) - await async_client.send(request) + sync_client._send(request) + await async_client.disconnect(request) async def test_refresh(sync_client: Client, async_client: AsyncClient) -> None: @@ -230,8 +230,8 @@ async def test_refresh(sync_client: Client, async_client: AsyncClient) -> None: expired=True, ) - sync_client.send(request) - await async_client.send(request) + sync_client._send(request) + await async_client.refresh(request) async def test_batch(sync_client: Client, async_client: AsyncClient) -> None: @@ -275,10 +275,10 @@ def check_result(res: List[CentResult]) -> None: requests=requests, ) - result = sync_client.send(request) + result = sync_client._send(request) check_result(result.replies) - result = await async_client.send(request) + result = await async_client.batch(request) check_result(result.replies) @@ -289,9 +289,9 @@ async def test_error_publish(sync_client: Client, async_client: AsyncClient) -> ) with pytest.raises(CentApiResponseError, match="unknown channel") as exc_info: - sync_client.send(request) + sync_client._send(request) assert exc_info.value.code == UNKNOWN_CHANNEL_ERROR_CODE with pytest.raises(CentApiResponseError, match="unknown channel") as exc_info: - await async_client.send(request) + await async_client.publish(request) assert exc_info.value.code == UNKNOWN_CHANNEL_ERROR_CODE From 41cdc04ff3a749a0366c568b34de43b58010b932 Mon Sep 17 00:00:00 2001 From: FZambia Date: Mon, 4 Mar 2024 17:59:06 +0200 Subject: [PATCH 46/55] update readme --- README.md | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 303ec0f..5dda6af 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ api_key = "" client = Client(api_url, api_key) request = PublishRequest(channel="channel", data={"input": "Hello world!"}) -result = client._send(request) +result = client.publish(request) print(result) ``` @@ -77,7 +77,7 @@ api_key = "" async def main(): client = AsyncClient(api_url, api_key) request = PublishRequest(channel="channel", data={"input": "Hello world!"}) - result = await client.send(request) + result = await client.publish(request) print(result) if __name__ == "__main__": @@ -103,7 +103,7 @@ from cent import * c = Client("http://localhost:8000/api", "api_key") req = BroadcastRequest(channels=["1", "2"], data={}) -c._send(req) +c.broadcast(req) # BroadcastResult( # responses=[ # Response[PublishResult](error=None, result=PublishResult(offset=7, epoch='rqKx')), @@ -111,7 +111,7 @@ c._send(req) # ] # ) req = BroadcastRequest(channels=["invalid:1", "2"], data={}) -c._send(req) +c.broadcast(req) # BroadcastResult( # responses=[ # Response[PublishResult](error=Error(code=102, message='unknown channel'), result=None), @@ -124,13 +124,9 @@ I.e. `cent` library does not raise exceptions for individual errors in `Broadcas ``` req = BroadcastRequest(channels=[], data={}) -c.send(req) +c.broadcast(req) Traceback (most recent call last): - File "", line 1, in - File "/Users/centrifugal/pycent/cent/client/sync_client.py", line 43, in send - response = request.parse_response(content) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/Users/centrifugal/pycent/cent/dto.py", line 85, in parse_response + ... raise CentApiResponseError( cent.exceptions.CentApiResponseError: Server API response error #107: bad request ``` @@ -160,28 +156,28 @@ from time import time def main(): - requests = [] + publish_requests = [] channels = [] for i in range(10000): channel = f"test_{i}" - requests.append(PublishRequest(channel=channel, data={"msg": "hello"})) + publish_requests.append(PublishRequest(channel=channel, data={"msg": "hello"})) channels.append(channel) - batch = BatchRequest(requests=requests) - broadcast = BroadcastRequest(channels=channels, data={"msg": "hello"}) + batch_request = BatchRequest(requests=publish_requests) + broadcast_request = BroadcastRequest(channels=channels, data={"msg": "hello"}) client = Client("http://localhost:8000/api", "api_key") start = time() - for request in requests: - client._send(request) + for request in publish_requests: + client.publish(request) print("sequential", time() - start) start = time() - client._send(batch) + client.batch(batch_request) print("batch", time() - start) start = time() - client._send(broadcast) + client.broadcast(broadcast_request) print("broadcast", time() - start) From 11bcb7fd700c3eebd22ba0d3ed3f6b021c45ee1f Mon Sep 17 00:00:00 2001 From: FZambia Date: Fri, 8 Mar 2024 19:22:00 +0200 Subject: [PATCH 47/55] fix field name in dto --- README.md | 3 +-- cent/dto.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5dda6af..bb37797 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,7 @@ pip install cent ## Centrifugo compatibility -**Cent v5 and higher works only with Centrifugo v5**. - +* **Cent v5 and higher works only with Centrifugo v5**. * If you need to work with Centrifugo v3, v4 => use Cent v4 * If you need to work with Centrifugo v2 => use Cent v3 diff --git a/cent/dto.py b/cent/dto.py index 6019148..233fa2a 100644 --- a/cent/dto.py +++ b/cent/dto.py @@ -1155,6 +1155,7 @@ class PushNotification(NestedModel): class SendPushNotificationResult(CentResult): """Send push notification result.""" + uid: str @@ -1218,7 +1219,7 @@ class UpdatePushStatusRequest(CentRequest[UpdatePushStatusResult]): __api_method__ = "update_push_status" __returning__ = UpdatePushStatusResult - alalytics_uid: str + analytics_uid: str status: str device_id: str msg_id: Optional[str] = None From cd7fe609568669c042b6b17eef8c8c8cc7df06d7 Mon Sep 17 00:00:00 2001 From: FZambia Date: Sat, 9 Mar 2024 09:28:41 +0200 Subject: [PATCH 48/55] more dto fixes --- cent/dto.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cent/dto.py b/cent/dto.py index 233fa2a..1b25c27 100644 --- a/cent/dto.py +++ b/cent/dto.py @@ -1171,13 +1171,13 @@ class RateLimitPolicy(NestedModel): class PushRateLimitStrategy(NestedModel): key: str policies: List[RateLimitPolicy] - drop_if_rate_limited: bool + drop_if_rate_limited: bool = False class PushTimeLimitStrategy(NestedModel): - send_after_time: str - send_before_time: str - no_tz_send_now: bool + send_after_time: str # use "%H:%M:%S" format, ex. "09:00:00" + send_before_time: str # use "%H:%M:%S" format, ex. "18:00:00" + no_tz_send_now: bool = False class PushLimitStrategy(NestedModel): @@ -1221,7 +1221,7 @@ class UpdatePushStatusRequest(CentRequest[UpdatePushStatusResult]): analytics_uid: str status: str - device_id: str + device_id: Optional[str] = None msg_id: Optional[str] = None From 8e4cfeb43b44134634be92c162a5d30422c87184 Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Sat, 9 Mar 2024 09:38:49 +0200 Subject: [PATCH 49/55] fix key --- cent/dto.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cent/dto.py b/cent/dto.py index 1b25c27..1f1ca7b 100644 --- a/cent/dto.py +++ b/cent/dto.py @@ -1169,7 +1169,7 @@ class RateLimitPolicy(NestedModel): class PushRateLimitStrategy(NestedModel): - key: str + key: Optional[str] = None policies: List[RateLimitPolicy] drop_if_rate_limited: bool = False From 8dcad06102ea38f8293c6b5e01bcb24752d6c08f Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Sat, 9 Mar 2024 09:50:22 +0200 Subject: [PATCH 50/55] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bb37797..e0294de 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,6 @@ make bench Cent v5 contains the following notable changes compared to Cent v4: * Client constructor slightly changed, refer to the examples above. -* To call desired API import and construct a request object (inherited from Pydantic `BaseModel`) and pass it to `send` method of the client. +* To call desired API import and construct a request object (inherited from Pydantic `BaseModel`) and then call corresponding method of client. This should feel very similar to how GRPC is usually structured. * Base exception class is now `CentError` instead of `CentException`, exceptions SDK raises were refactored. * To send multiple commands in one HTTP request SDK provides `batch` method. From a734205c405cb7b14cdcdfd0be0924ec832cd7e7 Mon Sep 17 00:00:00 2001 From: FZambia Date: Sat, 9 Mar 2024 16:54:29 +0200 Subject: [PATCH 51/55] be consistent in docstring style --- cent/client/async_client.py | 11 +++++++---- cent/client/sync_client.py | 11 +++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/cent/client/async_client.py b/cent/client/async_client.py index e10f1e3..de65a2f 100644 --- a/cent/client/async_client.py +++ b/cent/client/async_client.py @@ -82,10 +82,13 @@ def __init__( session: Optional[ClientSession] = None, ) -> None: """ - :param api_url: Centrifugo API URL - :param api_key: Centrifugo API key - :param timeout: Base timeout for all requests - :param session: Custom `aiohttp` session + Creates new AsyncClient instance. + + Args: + api_url (str): Centrifugo API URL. + api_key (str): Centrifugo API key. + timeout (float): Base timeout for all requests in seconds. + session (aiohttp.ClientSession): Custom `aiohttp` session. """ self._api_key = api_key self._session = AiohttpSession( diff --git a/cent/client/sync_client.py b/cent/client/sync_client.py index b80e313..ebd4061 100644 --- a/cent/client/sync_client.py +++ b/cent/client/sync_client.py @@ -81,10 +81,13 @@ def __init__( session: Optional[Session] = None, ) -> None: """ - :param api_url: Centrifugo API URL - :param api_key: Centrifugo API key - :param timeout: Base timeout for all requests. - :param session: Custom `requests` session. + Creates new Client instance. + + Args: + api_url (str): Centrifugo API URL. + api_key (str): Centrifugo API key. + timeout (float): Base timeout for all requests in seconds. + session (requests.Session): Custom `requests` session. """ self._api_url = api_url From 9efa92a4f23561607e8408ee0bd7cb3621b6fa8a Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Sun, 10 Mar 2024 08:32:52 +0200 Subject: [PATCH 52/55] tweaks in dto --- cent/dto.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/cent/dto.py b/cent/dto.py index 1f1ca7b..4656d4e 100644 --- a/cent/dto.py +++ b/cent/dto.py @@ -172,8 +172,8 @@ class StreamPosition(NestedModel): epoch (str): Epoch of current stream. """ - offset: int - epoch: str + offset: int = 0 + epoch: str = "" class ChannelOptionsOverride(NestedModel): @@ -696,8 +696,8 @@ class UserStatus(NestedModel): """ user: str - active: Optional[int] = None - online: Optional[int] = None + active: int = 0 + online: int = 0 class GetUserStatusResult(CentResult): @@ -936,12 +936,12 @@ class Device(NestedModel): """ id: str - platform: str - provider: str - token: str + platform: str = "" + provider: str = "" + token: str = "" user: str = "" - created_at: int - updated_at: int + created_at: int = 0 + updated_at: int = 0 meta: Optional[Dict[str, str]] = None topics: Optional[List[str]] = None @@ -1036,7 +1036,7 @@ class UserTopic(NestedModel): """ id: str - user: str + user: str = "" topic: str @@ -1171,13 +1171,13 @@ class RateLimitPolicy(NestedModel): class PushRateLimitStrategy(NestedModel): key: Optional[str] = None policies: List[RateLimitPolicy] - drop_if_rate_limited: bool = False + drop_if_rate_limited: Optional[bool] = False class PushTimeLimitStrategy(NestedModel): send_after_time: str # use "%H:%M:%S" format, ex. "09:00:00" send_before_time: str # use "%H:%M:%S" format, ex. "18:00:00" - no_tz_send_now: bool = False + no_tz_send_now: Optional[bool] = False class PushLimitStrategy(NestedModel): From de9457100c47e83eacfe45247655510162169a16 Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Fri, 15 Mar 2024 08:14:19 +0200 Subject: [PATCH 53/55] rename language to locale --- cent/dto.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cent/dto.py b/cent/dto.py index 4656d4e..43566f2 100644 --- a/cent/dto.py +++ b/cent/dto.py @@ -831,7 +831,7 @@ class DeviceRegisterRequest(CentRequest[DeviceRegisterResult]): platform: str user: Optional[str] = None timezone: Optional[str] = None - language: Optional[str] = None + locale: Optional[str] = None meta: Optional[Dict[str, str]] = None topics: Optional[List[str]] = None @@ -852,12 +852,12 @@ class DeviceTimezoneUpdate(NestedModel): timezone: str -class DeviceLanguageUpdate(NestedModel): +class DeviceLocaleUpdate(NestedModel): """ - Device language update. + Device locale update. """ - language: str + locale: str class DeviceMetaUpdate(NestedModel): @@ -895,7 +895,7 @@ class DeviceUpdateRequest(CentRequest[DeviceUpdateResult]): users: Optional[List[str]] = None user_update: Optional[DeviceUserUpdate] = None timezone_update: Optional[DeviceTimezoneUpdate] = None - language_update: Optional[DeviceLanguageUpdate] = None + locale_update: Optional[DeviceLocaleUpdate] = None meta_update: Optional[DeviceMetaUpdate] = None topics_update: Optional[DeviceTopicsUpdate] = None From 64f14a1f730ba9ee05fb27335b197a275fb35073 Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Sat, 16 Mar 2024 11:16:56 +0200 Subject: [PATCH 54/55] version 5.0.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 70fa6cf..dd5aec9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cent" -version = "5.0.0b1" +version = "5.0.0" description = "Python library to communicate with Centrifugo v5 server HTTP API" authors = ["Alexandr Emelin", "Katant Savelev"] license = "MIT" From c74ea797af5accf627d2a2b770d4a31dc24696c5 Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Sat, 16 Mar 2024 11:22:10 +0200 Subject: [PATCH 55/55] add Bogdan Evstratenko to authors --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dd5aec9..33229ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "cent" version = "5.0.0" description = "Python library to communicate with Centrifugo v5 server HTTP API" -authors = ["Alexandr Emelin", "Katant Savelev"] +authors = ["Alexandr Emelin", "Katant Savelev", "Bogdan Evstratenko"] license = "MIT" readme = 'README.md' classifiers = [