diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f2e6c52 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,24 @@ +root = true + +[*] +tab_width = 4 +end_of_line = lf +max_line_length = 99 +ij_visual_guides = 99 +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/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..363d435 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,25 @@ +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: Install poetry + run: pip install poetry + + - run: poetry config pypi-token.pypi "${{ secrets.PYPI_PASSWORD }}" + + - name: Publish package + run: poetry publish --build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..c52cc8a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,51 @@ +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 -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: | + make dev + + - name: Run tests + run: | + make test + + - name: Run lint + run: | + make lint-ci + + - name: Run mypy + run: | + make mypy 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/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d62f21b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +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 ] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..66b9249 --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +.PHONY: test lint lint-fix lint-ci mypy bench + +dev: + pip install poetry + poetry install + +test: + poetry run pytest -vv tests + +mypy: + poetry run mypy cent tests benchmarks + +lint: + poetry run ruff . + +lint-fix: + poetry run ruff . --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 1aa5f74..e0294de 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,4 @@ -CENT -==== - -Python tools to communicate with Centrifugo HTTP API. Python >= 3.3 supported. +Python SDK to communicate with Centrifugo v5 HTTP API. Python >= 3.9 supported. To install run: @@ -9,113 +6,228 @@ To install run: pip install cent ``` -### High-level library API +## Centrifugo compatibility + +* **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 + +## 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. 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. -First see [available API methods in documentation](https://centrifugal.dev/docs/server/server_api#http-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. -This library contains `Client` class to send messages to Centrifugo from your python-powered backend: +## Sync HTTP client ```python 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 for auth + +Optional arguments: + +* `timeout` (`float`) - base timeout for all requests in seconds, default is 10 seconds. +* `session` (`requests.Session`) - custom `requests` session to use. + +Example: + +```python +from cent import Client, PublishRequest + +api_url = "http://localhost:8000/api" +api_key = "" -url = "http://localhost:8000/api" -api_key = "XXX" +client = Client(api_url, api_key) +request = PublishRequest(channel="channel", data={"input": "Hello world!"}) +result = client.publish(request) +print(result) +``` + +## Async HTTP client + +```python +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 for auth -# initialize client instance. -client = Client(url, api_key=api_key, timeout=1) +Optional arguments: -# publish data into channel -channel = "public:chat" -data = {"input": "test"} -client.publish(channel, data) +* `timeout` (`float`) - base timeout for all requests in seconds, default is 10 seconds. +* `session` (`aiohttp.ClientSession`) - custom `aiohttp` session to use. + +Example: + +```python +import asyncio +from cent import AsyncClient, PublishRequest + +api_url = "http://localhost:8000/api" +api_key = "" + +async def main(): + client = AsyncClient(api_url, api_key) + request = PublishRequest(channel="channel", data={"input": "Hello world!"}) + result = await client.publish(request) + print(result) -# 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") +if __name__ == "__main__": + asyncio.run(main()) ``` -`publish`, `disconnect`, `unsubscribe`, `history_remove` return `None` in case of success. Each of this commands can raise an instance of `CentException`. +## Handling errors -I.e.: +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 (signal of invalid API key) +* `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 Client, CentException +from cent import * + +c = Client("http://localhost:8000/api", "api_key") +req = BroadcastRequest(channels=["1", "2"], data={}) +c.broadcast(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.broadcast(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: -client = Client("http://localhost:8000/api", api_key="XXX", timeout=1) -try: - client.publish("public:chat", {"input": "test"}) -except CentException: - # handle exception +``` +req = BroadcastRequest(channels=[], data={}) +c.broadcast(req) +Traceback (most recent call last): + ... + raise CentApiResponseError( +cent.exceptions.CentApiResponseError: Server API response error #107: bad request ``` -Depending on problem occurred exceptions can be: +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. -* RequestException – HTTP request to Centrifugo failed -* ResponseError - Centrifugo returned some error on request +## Using for async consumers -Both exceptions inherited from `CentException`. +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: -### Low-level library API: +```python +from cent import PublishRequest -To send lots of commands in one request: +request = PublishRequest(channel="channel", data={"input": "Hello world!"}) +method = request.api_method +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 Client, CentException +from cent import * +from time import time -client = Client("http://localhost:8000/api", api_key="XXX", timeout=1) -params = { - "channel": "python", - "data": "hello world" -} +def main(): + publish_requests = [] + channels = [] + for i in range(10000): + channel = f"test_{i}" + publish_requests.append(PublishRequest(channel=channel, data={"msg": "hello"})) + channels.append(channel) + batch_request = BatchRequest(requests=publish_requests) + broadcast_request = BroadcastRequest(channels=channels, data={"msg": "hello"}) -client.add("publish", params) + client = Client("http://localhost:8000/api", "api_key") + + start = time() + for request in publish_requests: + client.publish(request) + print("sequential", time() - start) + + start = time() + client.batch(batch_request) + print("batch", time() - start) + + start = time() + client.broadcast(broadcast_request) + print("broadcast", time() - start) + + +if __name__ == "__main__": + main() +``` + +On local machine, the output may look like this: -try: - result = client.send() -except CentException: - # handle exception -else: - print(result) ``` +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 -You can use `add` method to add several messages which will be sent. +### Tests and benchmarks -You'll get something like this in response: +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 ``` -I.e. list of single response to each command sent. So you need to inspect response on errors (if any) yourself. +And install dependencies: -### Client initialization arguments +```bash +make dev +``` -Required: +Then to run tests, run: -* address - Centrifugo HTTP API endpoint address +```bash +make test +``` -Optional: +To run benchmarks, run: -* `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. +```bash +make bench +``` -## For maintainer +## Migrate to Cent v5 -To release: +Cent v5 contains the following notable changes compared to Cent v4: -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/*` +* Client constructor slightly changed, refer to the examples above. +* 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. 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..6a149f1 --- /dev/null +++ b/benchmarks/conftest.py @@ -0,0 +1,49 @@ +import asyncio +import pytest_asyncio +from typing import ( + AsyncGenerator, + Callable, + Awaitable, + Optional, +) + +import pytest + +from cent import Client, AsyncClient + +API_URL = "http://localhost:8000/api" +API_KEY = "api_key" + +BenchmarkCoroType = Callable[[], Awaitable[None]] +BenchmarkType = Callable[[], Optional[Awaitable[None]]] +BenchmarkDecoratorType = Callable[[BenchmarkType], None] + + +@pytest.fixture() +def sync_client() -> Client: + return Client(API_URL, API_KEY) + + +@pytest_asyncio.fixture() +async def async_client( + # anyio_backend: Any, +) -> AsyncGenerator[AsyncClient, None]: + client = AsyncClient(API_URL, API_KEY) + yield client + await client.close() + + +# 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): + + @benchmark + def _(): # type: ignore + return event_loop.run_until_complete(func(*args, **kwargs)) + else: + benchmark(func, *args, **kwargs) + + return _wrapper diff --git a/benchmarks/test_publish.py b/benchmarks/test_publish.py new file mode 100644 index 0000000..5d0e8cb --- /dev/null +++ b/benchmarks/test_publish.py @@ -0,0 +1,39 @@ +import random + +from benchmarks.conftest import BenchmarkDecoratorType +from cent import AsyncClient, Client, PublishRequest + + +def sync_requests(client: Client) -> None: + channel_number = random.randint(0, 1000) # noqa: S311 + 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( + PublishRequest( + channel=f"personal_{channel_number}", + data={"message": "Hello world!"}, + ) + ) + + +def test_sync( + aio_benchmark: BenchmarkDecoratorType, + sync_client: Client, +) -> None: + @aio_benchmark + def _() -> None: + sync_requests(sync_client) + + +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 422cfb5..dec8ee1 100644 --- a/cent/__init__.py +++ b/cent/__init__.py @@ -1,2 +1,201 @@ -# coding: utf-8 -from .core import Client, CentException, RequestException, ResponseError, ClientNotEmpty +from .client import ( + Client, + AsyncClient, +) +from cent.dto import ( + CentResult, + CentRequest, + Response, + BatchRequest, + BatchResult, + BroadcastRequest, + PublishRequest, + SubscribeRequest, + UnsubscribeRequest, + PresenceRequest, + PresenceStatsRequest, + HistoryRequest, + HistoryRemoveRequest, + RefreshRequest, + ChannelsRequest, + DisconnectRequest, + InfoRequest, + PublishResult, + BroadcastResult, + SubscribeResult, + UnsubscribeResult, + PresenceResult, + PresenceStatsResult, + HistoryResult, + HistoryRemoveResult, + RefreshResult, + ChannelsResult, + DisconnectResult, + InfoResult, + StreamPosition, + ChannelOptionsOverride, + Disconnect, + BoolValue, + ProcessStats, + 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, + 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, + CentNetworkError, + CentTransportError, + CentUnauthorizedError, + CentDecodeError, + CentApiResponseError, +) + +__all__ = ( + "ApnsPushNotification", + "AsyncClient", + "BatchRequest", + "BatchResult", + "BlockUserRequest", + "BlockUserResult", + "BoolValue", + "BroadcastRequest", + "BroadcastResult", + "CancelPushRequest", + "CancelPushResult", + "CentApiResponseError", + "CentDecodeError", + "CentError", + "CentNetworkError", + "CentRequest", + "CentResult", + "CentTransportError", + "CentUnauthorizedError", + "ChannelContext", + "ChannelOptionsOverride", + "ChannelsRequest", + "ChannelsResult", + "Client", + "ClientInfo", + "ConnectionState", + "ConnectionTokenInfo", + "ConnectionsRequest", + "ConnectionsResult", + "DeleteUserStatusRequest", + "DeleteUserStatusResult", + "Device", + "DeviceFilter", + "DeviceListRequest", + "DeviceListResult", + "DeviceMetaUpdate", + "DeviceRegisterRequest", + "DeviceRegisterResult", + "DeviceRemoveRequest", + "DeviceRemoveResult", + "DeviceTopicFilter", + "DeviceTopicListRequest", + "DeviceTopicListResult", + "DeviceTopicUpdateRequest", + "DeviceTopicUpdateResult", + "DeviceTopicsUpdate", + "DeviceUpdateRequest", + "DeviceUpdateResult", + "DeviceUserUpdate", + "Disconnect", + "DisconnectRequest", + "DisconnectResult", + "FcmPushNotification", + "GetUserStatusRequest", + "GetUserStatusResult", + "HistoryRemoveRequest", + "HistoryRemoveResult", + "HistoryRequest", + "HistoryResult", + "HmsPushNotification", + "InfoRequest", + "InfoResult", + "InvalidateUserTokensRequest", + "InvalidateUserTokensResult", + "Node", + "PresenceRequest", + "PresenceResult", + "PresenceStatsRequest", + "PresenceStatsResult", + "ProcessStats", + "Publication", + "PublishRequest", + "PublishResult", + "PushNotification", + "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/__init__.py b/cent/client/__init__.py new file mode 100644 index 0000000..dee881a --- /dev/null +++ b/cent/client/__init__.py @@ -0,0 +1,7 @@ +from .sync_client import Client +from .async_client import AsyncClient + +__all__ = ( + "AsyncClient", + "Client", +) diff --git a/cent/client/async_client.py b/cent/client/async_client.py new file mode 100644 index 0000000..de65a2f --- /dev/null +++ b/cent/client/async_client.py @@ -0,0 +1,347 @@ +from typing import Optional, Any, cast + +from aiohttp import ClientSession + +from cent.client.session import AiohttpSession +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: + def __init__( + self, + api_url: str, + api_key: str, + timeout: Optional[float] = 10.0, + session: Optional[ClientSession] = None, + ) -> None: + """ + 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( + api_url, + timeout=timeout, + session=session, + ) + + async def _send( + self, + request: CentRequest[CentResultType], + timeout: Optional[float] = None, + ) -> CentResultType: + method = request.api_method + payload = request.api_payload + content = await self._session.make_request( + self._api_key, + method, + payload, + timeout=timeout, + ) + 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() + + async def __aenter__(self) -> "AsyncClient": + 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 new file mode 100644 index 0000000..b050430 --- /dev/null +++ b/cent/client/session/__init__.py @@ -0,0 +1,7 @@ +from .aiohttp import AiohttpSession +from .requests import RequestsSession + +__all__ = ( + "AiohttpSession", + "RequestsSession", +) diff --git a/cent/client/session/aiohttp.py b/cent/client/session/aiohttp.py new file mode 100644 index 0000000..f8f6a92 --- /dev/null +++ b/cent/client/session/aiohttp.py @@ -0,0 +1,68 @@ +import asyncio +from typing import Optional, Dict, Any + +from aiohttp import ClientSession, ClientError + +from cent.client.session.base_http_async import BaseHttpAsyncSession +from cent.exceptions import CentNetworkError, CentTimeoutError + + +class AiohttpSession(BaseHttpAsyncSession): + 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() + + 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, + api_key: str, + method: str, + json_data: Dict[str, Any], + timeout: Optional[float] = None, + ) -> str: + session = self._session + if api_key: + session.headers["X-API-Key"] = api_key + + url = f"{self._base_url}/{method}" + + 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 as error: + raise CentTimeoutError( + message="Request timeout", + ) from error + except ClientError as error: + raise CentNetworkError( + message=f"{type(error).__name__}: {error}", + ) from error + self.check_status_code(status_code=resp.status) + return raw_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_http.py b/cent/client/session/base_http.py new file mode 100644 index 0000000..b36c453 --- /dev/null +++ b/cent/client/session/base_http.py @@ -0,0 +1,22 @@ +from http import HTTPStatus + +from cent.exceptions import ( + CentUnauthorizedError, + CentTransportError, +) + + +class BaseHttpSession: + """Base class for HTTP sessions.""" + + @staticmethod + def check_status_code( + status_code: int, + ) -> None: + if status_code == HTTPStatus.UNAUTHORIZED: + raise CentUnauthorizedError + + if status_code != HTTPStatus.OK: + raise CentTransportError( + status_code=status_code, + ) diff --git a/cent/client/session/base_http_async.py b/cent/client/session/base_http_async.py new file mode 100644 index 0000000..0b83c0d --- /dev/null +++ b/cent/client/session/base_http_async.py @@ -0,0 +1,24 @@ +from abc import ABC, abstractmethod +from typing import Optional, Dict, Any + +from cent.client.session.base_http import BaseHttpSession + + +class BaseHttpAsyncSession(BaseHttpSession, ABC): + @abstractmethod + async def close(self) -> None: + """ + Close client session + """ + + @abstractmethod + async def make_request( + self, + api_key: str, + method: str, + json_data: Dict[str, Any], + timeout: Optional[float] = None, + ) -> str: + """ + Make request to Centrifugo HTTP API. + """ diff --git a/cent/client/session/base_http_sync.py b/cent/client/session/base_http_sync.py new file mode 100644 index 0000000..fd3018f --- /dev/null +++ b/cent/client/session/base_http_sync.py @@ -0,0 +1,24 @@ +from abc import ABC, abstractmethod +from typing import Optional, Dict, Any + +from cent.client.session.base_http import BaseHttpSession + + +class BaseHttpSyncSession(BaseHttpSession, ABC): + @abstractmethod + def close(self) -> None: + """ + Close client session + """ + + @abstractmethod + def make_request( + self, + api_key: str, + method: str, + json_data: Dict[str, Any], + timeout: Optional[float] = None, + ) -> str: + """ + Make request to Centrifugo HTTP API. + """ diff --git a/cent/client/session/requests.py b/cent/client/session/requests.py new file mode 100644 index 0000000..55830a4 --- /dev/null +++ b/cent/client/session/requests.py @@ -0,0 +1,62 @@ +from typing import Optional, Dict, Any + +import requests +from requests import Session + +from cent.client.session.base_http_sync import BaseHttpSyncSession +from cent.exceptions import CentNetworkError, CentTimeoutError + + +class RequestsSession(BaseHttpSyncSession): + 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._session: Session + if session: + self._session = session + else: + self._session = Session() + + def close(self) -> None: + if self._session is not None: + self._session.close() + + def make_request( + self, + api_key: str, + method: str, + json_data: Dict[str, Any], + timeout: Optional[float] = None, + ) -> str: + if api_key: + self._session.headers["X-API-Key"] = api_key + + url = f"{self._base_url}/{method}" + + try: + raw_result = self._session.post( + url=url, + json=json_data, + timeout=timeout or self._timeout, + ) + except requests.exceptions.Timeout as error: + raise CentTimeoutError( + message="Request timeout", + ) from error + except requests.exceptions.ConnectionError as error: + raise CentNetworkError( + message=f"{type(error).__name__}: {error}", + ) from error + self.check_status_code( + status_code=raw_result.status_code, + ) + return raw_result.text + + def __del__(self) -> None: + self.close() diff --git a/cent/client/sync_client.py b/cent/client/sync_client.py new file mode 100644 index 0000000..ebd4061 --- /dev/null +++ b/cent/client/sync_client.py @@ -0,0 +1,346 @@ +from typing import Optional, Any, cast + +from requests import Session +from cent.client.session import RequestsSession +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: + def __init__( + self, + api_url: str, + api_key: str, + timeout: Optional[float] = 10.0, + session: Optional[Session] = None, + ) -> None: + """ + 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 + self._api_key = api_key + self._session = RequestsSession( + api_url, + timeout=timeout, + session=session, + ) + + def _send( + self, + request: CentRequest[CentResultType], + timeout: Optional[float] = None, + ) -> CentResultType: + content = self._session.make_request( + self._api_key, + request.api_method, + request.api_payload, + timeout=timeout, + ) + 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() + + def __enter__(self) -> "Client": + return self + + def __exit__(self, *kwargs: Any) -> None: + self.close() diff --git a/cent/core.py b/cent/core.py deleted file mode 100644 index 5df8e2b..0000000 --- a/cent/core.py +++ /dev/null @@ -1,284 +0,0 @@ -# coding: utf-8 -import urllib.parse as urlparse -import sys -import json -import requests - - -def to_bytes(s): - return s.encode("latin-1") - - -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): - """ - Core class to communicate with Centrifugo. - """ - - def __init__(self, address, api_key="", timeout=1, - json_encoder=None, verify=True, - session=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.verify = verify - self.session = session or requests.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 _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 - try: - resp = self.session.post( - url, data=data, headers=headers, timeout=self.timeout, verify=self.verify) - except requests.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') - - 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() - 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() - 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() - 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() - return - - def disconnect(self, user, client=None): - self._check_empty() - self.add("disconnect", self.get_disconnect_params(user, client=client)) - self._send_one() - return - - def presence(self, channel): - self._check_empty() - self.add("presence", self.get_presence_params(channel)) - result = self._send_one() - 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() - 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() - 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() - return - - def channels(self, pattern=""): - self._check_empty() - self.add("channels", params=self.get_channels_params(pattern=pattern)) - result = self._send_one() - return result["channels"] - - def info(self): - self._check_empty() - self.add("info", self.get_info_params()) - result = self._send_one() - return result diff --git a/cent/dto.py b/cent/dto.py new file mode 100644 index 0000000..43566f2 --- /dev/null +++ b/cent/dto.py @@ -0,0 +1,1242 @@ +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, TypeAdapter, ValidationError + +from cent.exceptions import CentDecodeError, CentApiResponseError + + +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 Error(BaseModel): + code: int + message: str + + +class Response(BaseModel, Generic[CentResultType]): + error: Optional[Error] = None + result: Optional[CentResultType] = None + + +class CentRequest(BaseModel, Generic[CentResultType], 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 + + @property + def api_payload(self) -> Any: + return self.model_dump(exclude_none=True) + + @property + def api_method(self) -> str: + return self.__api_method__ + + def parse_response( + 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): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + arbitrary_types_allowed=True, + ) + + +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 + + @property + def api_payload(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. + + Attributes: + code (int): Disconnect code. + reason (str): Disconnect reason. + """ + + code: int = 0 + 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 = 0 + 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 = 0.0 + rss: int = 0 + + +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 = 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 = 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 = 0 + num_subs: int = 0 + num_users: int = 0 + num_channels: int = 0 + uptime: int = 0 + metrics: Optional[Metrics] = None + process: Optional[ProcessStats] = None + + +class PublishResult(CentResult): + """Publish result. + + Attributes: + offset: Offset of publication in history stream. + epoch: Epoch of current stream. + """ + + offset: int = 0 + epoch: str = "" + + +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 = 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: int = 0 + epoch: str = "" + + +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 = 0 + num_users: int = 0 + + +class RefreshResult(CentResult): + """Refresh result.""" + + +class SubscribeResult(CentResult): + """Subscribe result.""" + + +class UnsubscribeResult(CentResult): + """Unsubscribe result.""" + + +class BroadcastRequest(CentRequest[BroadcastResult]): + """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" + + 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 + + +class ChannelsRequest(CentRequest[ChannelsResult]): + """Channels request. + + Attributes: + pattern: Pattern to filter channels, we are using https://github.com/gobwas/glob + library for matching. + """ + + __returning__ = ChannelsResult + __api_method__ = "channels" + + pattern: Optional[str] = None + + +class DisconnectRequest(CentRequest[DisconnectResult]): + """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" + + user: str + client: Optional[str] = None + session: Optional[str] = None + whitelist: Optional[List[str]] = None + disconnect: Optional[Disconnect] = None + + +class HistoryRequest(CentRequest[HistoryResult]): + """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" + + channel: str + limit: Optional[int] = None + since: Optional[StreamPosition] = None + reverse: Optional[bool] = None + + +class HistoryRemoveRequest(CentRequest[HistoryRemoveResult]): + """History remove request. + + Attributes: + channel: Name of channel to remove history. + """ + + __returning__ = HistoryRemoveResult + __api_method__ = "history_remove" + + channel: str + + +class InfoRequest(CentRequest[InfoResult]): + """Info request.""" + + __returning__ = InfoResult + __api_method__ = "info" + + +class PresenceRequest(CentRequest[PresenceResult]): + """Presence request. + + Attributes: + channel: Name of channel to call presence from. + """ + + __returning__ = PresenceResult + __api_method__ = "presence" + + channel: str + + +class PresenceStatsRequest(CentRequest[PresenceStatsResult]): + """Presence request. + + Attributes: + channel: Name of channel to call presence from. + """ + + __returning__ = PresenceStatsResult + __api_method__ = "presence_stats" + + channel: str + + +class PublishRequest(CentRequest[PublishResult]): + """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" + + channel: str + data: Any + skip_history: Optional[bool] = None + tags: Optional[Dict[str, str]] = None + b64data: Optional[str] = None + idempotency_key: Optional[str] = None + + +class RefreshRequest(CentRequest[RefreshResult]): + """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" + + user: str + client: Optional[str] = None + session: Optional[str] = None + expired: Optional[bool] = None + expire_at: Optional[int] = None + + +class SubscribeRequest(CentRequest[SubscribeResult]): + """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" + + 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 + + +class UnsubscribeRequest(CentRequest[UnsubscribeResult]): + """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" + + user: str + channel: str + client: Optional[str] = None + session: Optional[str] = None + + +class ConnectionTokenInfo(NestedModel): + """Connection token info.""" + + uid: Optional[str] = None + issued_at: Optional[int] = None + + +class SubscriptionTokenInfo(NestedModel): + """Subscription token info.""" + + uid: Optional[str] = None + issued_at: Optional[int] = None + + +class ChannelContext(NestedModel): + """Channel context.""" + + source: Optional[int] = None + + +class ConnectionState(NestedModel): + """Connection state.""" + + 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): + """Connection info.""" + + app_name: str = "" + app_version: str = "" + transport: str + protocol: str + user: str = "" + state: Optional[ConnectionState] = None + + +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] + + +class UserStatus(NestedModel): + """ + User status. + """ + + user: str + active: int = 0 + online: int = 0 + + +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: Optional[int] = None + 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: Optional[int] = None + 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: Optional[int] = None + user: str + issued_before: Optional[int] = None + channel: Optional[str] = None + + +class DeviceRegisterResult(CentResult): + """ + Device register result. + """ + + id: str + + +class DeviceRegisterRequest(CentRequest[DeviceRegisterResult]): + """ + Device register request. + """ + + __api_method__ = "device_register" + __returning__ = DeviceRegisterResult + + id: Optional[str] = None + provider: str + token: str + platform: str + user: Optional[str] = None + timezone: Optional[str] = None + locale: Optional[str] = None + meta: Optional[Dict[str, str]] = None + topics: Optional[List[str]] = None + + +class DeviceUserUpdate(NestedModel): + """ + Device user update. + """ + + user: str + + +class DeviceTimezoneUpdate(NestedModel): + """ + Device timezone update. + """ + + timezone: str + + +class DeviceLocaleUpdate(NestedModel): + """ + Device locale update. + """ + + locale: 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 + timezone_update: Optional[DeviceTimezoneUpdate] = None + locale_update: Optional[DeviceLocaleUpdate] = 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: Optional[List[str]] = None + users: Optional[List[str]] = None + + +class DeviceFilter(NestedModel): + """ + Device filter. + """ + + 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): + """ + Device. + """ + + id: str + platform: str = "" + provider: str = "" + token: str = "" + user: str = "" + created_at: int = 0 + updated_at: int = 0 + meta: Optional[Dict[str, str]] = None + topics: Optional[List[str]] = None + + +class DeviceListResult(CentResult): + """ + Device list result. + """ + + items: List[Device] + next_cursor: Optional[str] = None + total_count: Optional[int] = None + + +class DeviceListRequest(CentRequest[DeviceListResult]): + """ + Device list request. + """ + + __api_method__ = "device_list" + __returning__ = DeviceListResult + + 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): + """ + Device topic filter. + """ + + 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): + """ + Device topic. + """ + + id: str + topic: str + device: Device + + +class DeviceTopicListResult(CentResult): + """ + Device topic list result. + """ + + items: List[DeviceTopic] + next_cursor: Optional[str] = None + total_count: Optional[int] = None + + +class DeviceTopicListRequest(CentRequest[DeviceTopicListResult]): + """ + Device topic list request. + """ + + __api_method__ = "device_topic_list" + __returning__ = DeviceTopicListResult + + 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): + """ + User topic filter. + """ + + users: Optional[List[str]] = None + topics: Optional[List[str]] = None + topic_prefix: Optional[str] = None + + +class UserTopic(NestedModel): + """ + User topic. + """ + + id: str + user: str = "" + topic: str + + +class UserTopicListResult(CentResult): + """ + User topic list result. + """ + + items: List[UserTopic] + next_cursor: Optional[str] = None + total_count: Optional[int] = None + + +class UserTopicListRequest(CentRequest[UserTopicListResult]): + """ + User topic list request. + """ + + __api_method__ = "user_topic_list" + __returning__ = UserTopicListResult + + filter: Optional[UserTopicFilter] = None + include_total_count: Optional[bool] = None + cursor: Optional[str] = None + limit: Optional[int] = None + + +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: 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): + """ + FCM push notification. + """ + + message: Any + + +class HmsPushNotification(NestedModel): + """ + HMS push notification. + """ + + message: Any + + +class ApnsPushNotification(NestedModel): + """ + APNS push notification. + """ + + headers: Optional[Dict[str, str]] = None + payload: Any + + +class PushNotification(NestedModel): + """ + Push notification. + """ + + fcm: Optional[FcmPushNotification] = None + hms: Optional[HmsPushNotification] = None + apns: Optional[ApnsPushNotification] = None + expire_at: Optional[int] = None + + +class SendPushNotificationResult(CentResult): + """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: Optional[str] = None + policies: List[RateLimitPolicy] + 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: Optional[bool] = False + + +class PushLimitStrategy(NestedModel): + rate_limit: Optional[PushRateLimitStrategy] = None + time_limit: Optional[PushTimeLimitStrategy] = None + + +class SendPushNotificationRequest(CentRequest[SendPushNotificationResult]): + """ + Send push notification request. + """ + + __api_method__ = "send_push_notification" + __returning__ = SendPushNotificationResult + + recipient: PushRecipient + 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): + """ + Update push status result. + """ + + +class UpdatePushStatusRequest(CentRequest[UpdatePushStatusResult]): + """ + Update push status request. + """ + + __api_method__ = "update_push_status" + __returning__ = UpdatePushStatusResult + + analytics_uid: str + status: str + device_id: Optional[str] = None + msg_id: Optional[str] = None + + +class CancelPushResult(CentResult): + """ + Cancel push result. + """ + + +class CancelPushRequest(CentRequest[CancelPushResult]): + """ + Cancel push request. + """ + + __returning__ = CancelPushResult + __api_method__ = "cancel_push" + + uid: str diff --git a/cent/exceptions.py b/cent/exceptions.py new file mode 100644 index 0000000..59df06f --- /dev/null +++ b/cent/exceptions.py @@ -0,0 +1,72 @@ +class CentError(Exception): + """ + Wrapper for all exceptions coming from this library. + """ + + +class CentNetworkError(CentError): + """CentNetworkError raised when Centrifugo is unreachable or not available.""" + + def __init__(self, message: str) -> None: + self.message = message + + def __str__(self) -> str: + return f"Network error - {self.message}" + + def __repr__(self) -> str: + return f"{type(self).__name__}('{self}')" + + +class CentTransportError(CentError): + """CentTransportError raised when HTTP request results into non-200 status code.""" + + def __init__(self, status_code: int): + 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, message: str) -> None: + 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): + """ + CentUnauthorizedError raised when Centrifugo returns 401 status code. + """ + + +class CentDecodeError(CentError): + """ + CentDecodeError raised when response from Centrifugo can't be decoded. + """ + + +class CentApiResponseError(CentError): + """ + CentApiResponseError raised when the response from Centrifugo server API contains + any error as a result of API command execution. + """ + + def __init__(self, code: int, message: str) -> None: + self.code = code + self.message = message + + def __str__(self) -> str: + return f"Server API response error #{self.code}: {self.message}" + + def __repr__(self) -> str: + return f"{type(self).__name__}('{self}')" diff --git a/cent/py.typed b/cent/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..4818b74 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1162 @@ +# This file is automatically @generated by Poetry 1.7.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] +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] +speedups = ["Brotli", "aiodns", "brotlicffi"] + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +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] +frozenlist = ">=1.1.0" + +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +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 = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.7" +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 = "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] +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 = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +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 = "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 = "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" +description = "Distribution utilities" +optional = false +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 = "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" +description = "A platform independent file lock." +optional = false +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] +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.34" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {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] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +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 = "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.5" +description = "multidict implementation" +optional = false +python-versions = ">=3.7" +files = [ + {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]] +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] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +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 = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +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] +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.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.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, +] + +[package.extras] +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" +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.1-py2.py3-none-any.whl", hash = "sha256:9fe989afcf095d2c4796ce7c553cf28d4d4a9b9346de3cda079bcf40748454a4"}, + {file = "pre_commit-3.6.1.tar.gz", hash = "sha256:c90961d8aa706f75d60935aba09469a6b0bcb8345f127c3fbee4bdc5f114cf4b"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +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.1" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {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.2" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.16.2" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {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] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pytest" +version = "8.0.1" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.0.1-py3-none-any.whl", hash = "sha256:3e4f16fe1c0a9dc9d9389161c127c3edc5d810c38d6793042fb81d9f48a59fca"}, + {file = "pytest-8.0.1.tar.gz", hash = "sha256:267f6563751877d772019b13aacbe4e860d73fe8f651f28112e9ac37de7513ae"}, +] + +[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-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" +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" +description = "YAML parser and emitter for Python" +optional = false +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_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"}, + {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 = "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" +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.1.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {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-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 = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +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 = "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" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +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 = "urllib3" +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.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 = "virtualenv" +version = "20.25.0" +description = "Virtual Python Environment builder" +optional = false +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 = "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.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "ae6d1bdb8a4c46ee131a7348a709d0021576a5c3725dc6424a7396a638be2321" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..33229ab --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,118 @@ +[tool.poetry] +name = "cent" +version = "5.0.0" +description = "Python library to communicate with Centrifugo v5 server HTTP API" +authors = ["Alexandr Emelin", "Katant Savelev", "Bogdan Evstratenko"] +license = "MIT" +readme = 'README.md' +classifiers = [ + "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.9" +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" +pytest = "^8" +pytest-benchmark = "^4.0.0" +pytest-asyncio = "^0.23.5" + +[tool.ruff] +preview = true +line-length = 99 +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 + "T201", + "PLR0917", + "PLR0904", # Centrifugo has many API methods +] + +[tool.ruff.per-file-ignores] +"tests/*" = ["S101", "PT012"] + +[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 +plugins = ["pydantic.mypy"] + +[tool.pydantic-mypy] +warn_required_dynamic_aliases = 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" diff --git a/setup.py b/setup.py deleted file mode 100644 index 4798733..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/__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..1a3301a --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,33 @@ +from typing import ( + Any, + AsyncGenerator, + Tuple, + Dict, +) + +import pytest + +from cent import Client, AsyncClient + +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": False} + + +@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_validation.py b/tests/test_validation.py new file mode 100644 index 0000000..49fc7c7 --- /dev/null +++ b/tests/test_validation.py @@ -0,0 +1,297 @@ +import uuid +from typing import List, cast + +import pytest + +from cent import ( + AsyncClient, + Client, + CentApiResponseError, + PublishRequest, + BroadcastRequest, + PresenceRequest, + StreamPosition, + Disconnect, + SubscribeRequest, + UnsubscribeRequest, + PresenceStatsRequest, + HistoryRequest, + HistoryRemoveRequest, + InfoRequest, + ChannelsRequest, + DisconnectRequest, + RefreshRequest, + 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.api_payload == {"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"}, + ), + ] + request = BatchRequest( + requests=requests, + ) + assert request.api_payload == { + "commands": [ + {"publish": {"channel": "personal_1", "data": {"data": "Second data"}}}, + {"publish": {"channel": "personal_2", "data": {"data": "First data"}}}, + ], + "parallel": False, + } + + +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", + 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.publish(request) + assert result.offset + + +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"}, + skip_history=False, + tags={"tag": "tag"}, + idempotency_key="idempotency_key", + ) + result = sync_client._send(request) + check_result(result) + + result = await async_client.broadcast(request) + check_result(result) + + +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.subscribe(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.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.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.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( + 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.history(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.history_remove(request) + + +async def test_info(sync_client: Client, async_client: AsyncClient) -> None: + 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.channels(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.disconnect(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.refresh(request) + + +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( + requests=requests, + ) + + result = sync_client._send(request) + check_result(result.replies) + + result = await async_client.batch(request) + check_result(result.replies) + + +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.publish(request) + assert exc_info.value.code == UNKNOWN_CHANNEL_ERROR_CODE