From 91e0e4315d5ae832fad5b8f437552242f8069df3 Mon Sep 17 00:00:00 2001 From: turall Date: Mon, 7 Oct 2024 10:09:06 +0400 Subject: [PATCH] V2 --- .flake8 | 4 - Makefile | 14 - README.md | 479 ++++++--- opa_client/__init__.py | 17 +- opa_client/base.py | 286 ++++++ opa_client/errors.py | 156 +-- opa_client/opa.py | 1130 +++++++++++----------- opa_client/opa_async.py | 604 ++++++++++++ opa_client/test/test_async_client.py | 118 +++ opa_client/test/test_integaration_opa.py | 123 +++ opa_client/test/test_opa.py | 236 ++--- opa_client/test/test_opa_client.py | 114 +++ poetry.lock | 1009 +++++++++++++++---- pyproject.toml | 29 +- 14 files changed, 3151 insertions(+), 1168 deletions(-) delete mode 100644 .flake8 delete mode 100644 Makefile create mode 100644 opa_client/base.py create mode 100644 opa_client/opa_async.py create mode 100644 opa_client/test/test_async_client.py create mode 100644 opa_client/test/test_integaration_opa.py create mode 100644 opa_client/test/test_opa_client.py diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 5643697..0000000 --- a/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -max_line_length = 100 -exclude = .venv,.mypy_cache,.pytest_cache -ignore = PT013,PT018,W503 \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index df69527..0000000 --- a/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -lint: - @echo - isort --diff -c --skip-glob '*.venv' . - @echo - blue --check --diff --color . - @echo - flake8 . - @echo - mypy --ignore-missing-imports . - - -format_code: - isort . - blue . diff --git a/README.md b/README.md index 4cb447b..7ab657e 100644 --- a/README.md +++ b/README.md @@ -1,238 +1,381 @@ -# Python Open Policy Agent (OPA) Client -[![MIT licensed](https://img.shields.io/github/license/Turall/OPA-python-client)](https://raw.githubusercontent.com/Turall/OPA-python-client/master/LICENSE) -[![GitHub stars](https://img.shields.io/github/stars/Turall/OPA-python-client.svg)](https://github.com/Turall/OPA-python-client/stargazers) -[![GitHub forks](https://img.shields.io/github/forks/Turall/OPA-python-client.svg)](https://github.com/Turall/OPA-python-client/network) -[![GitHub issues](https://img.shields.io/github/issues-raw/Turall/OPA-python-client)](https://github.com/Turall/OPA-python-client/issues) -[![Downloads](https://pepy.tech/badge/opa-python-client)](https://pepy.tech/project/opa-python-client) +# OpaClient - Open Policy Agent Python Client +OpaClient is a Python client library designed to interact with the [Open Policy Agent (OPA)](https://www.openpolicyagent.org/). It supports both **synchronous** and **asynchronous** requests, making it easy to manage policies, data, and evaluate rules in OPA servers. -See offical documentation page [Open Policy Agent](https://www.openpolicyagent.org/docs/latest/) +## Features +- **Manage Policies**: Create, update, retrieve, and delete policies. +- **Manage Data**: Create, update, retrieve, and delete data in OPA. +- **Evaluate Policies**: Use input data to evaluate policies and return decisions. +- **Synchronous & Asynchronous**: Choose between sync or async operations to suit your application. +- **SSL/TLS Support**: Communicate securely with SSL/TLS, including client certificates. +- **Customizable**: Use custom headers, timeouts, and other configurations. -### Installation ### +## Installation -```sh -$ pip install OPA-python-client -``` - -Alternatively, if you prefer to use `poetry` for package dependencies: +You can install the OpaClient package via `pip`: ```bash -$ poetry shell -$ poetry add OPA-python-client +pip install OPA-python-client ``` +## Quick Start - -## Usage Examples +### Synchronous Client Example ```python ->>> from opa_client.opa import OpaClient ->>> client = OpaClient() # default host='localhost', port=8181, version='v1' ->>> client.check_connection() -'Yes I"m here :)' ->>> test_policy = """ -... package play -... -... import data.testapi.testdata -... -... default hello = false -... -... hello { -... m := input.message -... testdata[i] == m -... } -... """ +from opa_client.opa import OpaClient + +# Initialize the OPA client +client = OpaClient(host='localhost', port=8181) ->>> client.update_opa_policy_fromstring(test_policy, "testpolicy") -True ->>> client.get_policies_list() -['testpolicy'] ->>> data = ["world", "hello"] ->>> client.update_or_create_opa_data(data, "testapi/testdata") -True ->>> check_data = {"input": {"message": "hello"}} ->>> client.check_permission(input_data=check_data, policy_name="testpolicy", rule_name="hello") -{'result': True} +# Check the OPA server connection +try: + print(client.check_connection()) # True +finally: + client.close_connection() ``` +or with client factory +```python +from opa_client import create_opa_client + +client = create_opa_client(host="localhost", port=8181) -### Connection to OPA service +``` + +Check OPA healthy. If you want check bundels or plugins, add query params for this. ```python from opa_client.opa import OpaClient -client = OpaClient() # default host='localhost', port=8181, version='v1' +client = OpaClient() + +print(client.check_health()) # response is True or False +print(client.check_health({"bundle": True})) # response is True or False +# If your diagnostic url different than default url, you can provide it. +print(client.check_health(diagnostic_url="http://localhost:8282/health")) # response is True or False +print(client.check_health(query={"bundle": True}, diagnostic_url="http://localhost:8282/health")) # response is True or False +``` + +### Asynchronous Client Example + +```python +import asyncio +from opa_client.opa_async import AsyncOpaClient + +async def main(): + async with AsyncOpaClient(host='localhost', port=8181) as client: + result = await client.check_connection() + print(result) + +# Run the async main function +asyncio.run(main()) +``` +or with clien factory + +```python +from opa_client import create_opa_client -client.check_connection() # response is Yes I'm here :) +client = create_opa_client(async_mode=True,host="localhost", port=8181) -# Ensure the connection is closed correctly by deleting the client -del client ``` +## Secure Connection with SSL/TLS + +You can use OpaClient with secure SSL/TLS connections, including mutual TLS (mTLS), by providing a client certificate and key. -### Connection to OPA service with SSL +### Synchronous Client with SSL/TLS ```python from opa_client.opa import OpaClient +# Path to your certificate and private key +cert_path = '/path/to/client_cert.pem' +key_path = '/path/to/client_key.pem' +# Initialize the OPA client with SSL/TLS client = OpaClient( - host="https://192.168.99.100", - port=8181, - version="v1", + host='your-opa-server.com', + port=443, # Typically for HTTPS ssl=True, - cert="/your/certificate/file/path/mycert.crt", + cert=(cert_path, key_path) # Provide the certificate and key as a tuple ) -client.check_connection() # response is Yes I'm here :) +# Check the OPA server connection +try: + result = client.check_connection() + print(result) +finally: + client.close_connection() +``` -del client +### Asynchronous Client with SSL/TLS + +```python +import asyncio +from opa_client.opa_async import AsyncOpaClient + +# Path to your certificate and private key +cert_path = '/path/to/client_cert.pem' +key_path = '/path/to/client_key.pem' + +async def main(): + # Initialize the OPA client with SSL/TLS + async with AsyncOpaClient( + host='your-opa-server.com', + port=443, # Typically for HTTPS + ssl=True, + cert=(cert_path, key_path) # Provide the certificate and key as a tuple + ) as client: + # Check the OPA server connection + result = await client.check_connection() + print(result) + +# Run the async main function +asyncio.run(main()) ``` +## Usage -### Update policy from rego file +### Policy Management + +#### Create or Update a Policy + +You can create or update a policy using the following syntax: + +- **Synchronous**: ```python -from opa_client.opa import OpaClient +policy_name = 'example_policy' +policy_content = ''' +package example -client = OpaClient() +default allow = false -client.update_opa_policy_fromfile("/your/path/filename.rego", endpoint="fromfile") # response is True +allow { + input.user == "admin" +} +''' + +client.update_policy_from_string(policy_content, policy_name) +``` -client.get_policies_list() # response is ["fromfile"] +- **Asynchronous**: -del client +```python +await client.update_policy_from_string(policy_content, policy_name) ``` +Or from url: -### Update policy from URL +- **Synchronous**: ```python -from opa_client.opa import OpaClient - -client = OpaClient() +policy_name = 'example_policy' +client.update_policy_from_url("http://opapolicyurlexample.test/example.rego", policy_name) -client.update_opa_policy_fromurl("http://opapolicyurlexample.test/example.rego", endpoint="fromurl") # response is True +``` -client.get_policies_list() # response is ["fromfile","fromurl"] +- **Asynchronous**: -del client +```python +await client.update_policy_from_url("http://opapolicyurlexample.test/example.rego", policy_name) ``` +Update policy from rego file -### Delete policy +```python +client.update_opa_policy_fromfile("/your/path/filename.rego", endpoint="fromfile") # response is True +client.get_policies_list() +``` +- **Asynchronous**: ```python -from opa_client.opa import OpaClient +await client.update_opa_policy_fromfile("/your/path/filename.rego", endpoint="fromfile") # response is True -client = OpaClient() +await client.get_policies_list() +``` -client.delete_opa_policy("fromfile") # response is True +#### Retrieve a Policy -client.get_policies_list() # response is [] +After creating a policy, you can retrieve it: -del client -``` +- **Synchronous**: -### Get raw data from OPA service +```python +policy = client.get_policy('example_policy') +print(policy) +# or +policies = client.get_policies_list() +print(policies) +``` +- **Asynchronous**: ```python -from opa_client.opa import OpaClient +policy = await client.get_policy('example_policy') +print(policy) -client = OpaClient() +# or +policies = await client.get_policies_list() +print(policies) +``` -print(client.get_opa_raw_data("testapi/testdata")) # response is {'result': ['world', 'hello']} +Save policy to file from OPA service -# You can use query params for additional info -# provenance - If parameter is true, response will include build/version info in addition to the result. -# metrics - Return query performance metrics in addition to result +```python +client.policy_to_file(policy_name="example_policy",path="/your/path",filename="example.rego") + +``` -print(client.get_opa_raw_data("userinfo",query_params={"provenance": True})) -# response is {'provenance': {'version': '0.25.2', 'build_commit': '4c6e524', 'build_timestamp': '2020-12-08T16:56:55Z', 'build_hostname': '3bb58334a5a9'}, 'result': {'user_roles': {'alice': ['admin'], 'bob': ['employee', 'billing'], 'eve': ['customer']}}} +- **Asynchronous**: -print(client.get_opa_raw_data("userinfo",query_params={"metrics": True})) +```python -# response is {'metrics': {'counter_server_query_cache_hit': 0, 'timer_rego_external_resolve_ns': 231, 'timer_rego_input_parse_ns': 381, 'timer_rego_query_compile_ns': 40173, 'timer_rego_query_eval_ns': 12674, 'timer_rego_query_parse_ns': 5692, 'timer_server_handler_ns': 83490}, 'result': {'user_roles': {'alice': ['admin'], 'bob': ['employee', 'billing'], 'eve': ['customer']}}} +await client.policy_to_file(policy_name="example_policy",path="/your/path",filename="example.rego") -del client ``` +Information about policy path and rules + +```python + +print(client.get_policies_info()) +#{'example_policy': {'path': 'http://localhost:8181/v1/data/example', 'rules': ['http://localhost:8181/v1/data/example/allow']}} -### Save policy to file from OPA service +``` +- **Asynchronous**: ```python -from opa_client.opa import OpaClient -client = OpaClient() +print(await client.get_policies_info()) +#{'example_policy': {'path': 'http://localhost:8181/v1/data/example', 'rules': ['http://localhost:8181/v1/data/example/allow']}} + +``` + +#### Delete a Policy -client.opa_policy_to_file(policy_name="fromurl",path="/your/path",filename="example.rego") # response is True +You can delete a policy by name: -del client +- **Synchronous**: + +```python +client.delete_policy('example_policy') ``` +- **Asynchronous**: + +```python +await client.delete_policy('example_policy') +``` -### Delete data from OPA service +### Data Management +#### Create or Update Data + +You can upload arbitrary data to OPA: + +- **Synchronous**: ```python -from opa_client.opa import OpaClient +data_name = 'users' +data_content = { + "users": [ + {"name": "alice", "role": "admin"}, + {"name": "bob", "role": "user"} + ] +} -client = OpaClient() +client.update_or_create_data(data_content, data_name) +``` -client.delete_opa_data("testapi") # response is True +- **Asynchronous**: -del client +```python +await client.update_or_create_data(data_content, data_name) ``` +#### Retrieve Data -### Information about policy path and rules +You can fetch the data stored in OPA: +- **Synchronous**: ```python -from opa_client.opa import OpaClient +data = client.get_data('users') +print(data) +# You can use query params for additional info +# provenance - If parameter is true, response will include build/version info in addition to the result. +# metrics - Return query performance metrics in addition to result +data = client.get_data('users',query_params={"provenance": True}) +print(data) # {'provenance': {'version': '0.68.0', 'build_commit': 'db53d77c482676fadd53bc67a10cf75b3d0ce00b', 'build_timestamp': '2024-08-29T15:23:19Z', 'build_hostname': '3aae2b82a15f'}, 'result': {'users': [{'name': 'alice', 'role': 'admin'}, {'name': 'bob', 'role': 'user'}]}} + -client = OpaClient() +data = client.get_data('users',query_params={"metrics": True}) +print(data) # {'metrics': {'counter_server_query_cache_hit': 0, 'timer_rego_external_resolve_ns': 7875, 'timer_rego_input_parse_ns': 875, 'timer_rego_query_compile_ns': 501083, 'timer_rego_query_eval_ns': 50250, 'timer_rego_query_parse_ns': 199917, 'timer_server_handler_ns': 1031291}, 'result': {'users': [{'name': 'alice', 'role': 'admin'}, {'name': 'bob', 'role': 'user'}]}} -client.get_policies_info() -# response is {'testpolicy': {'path': ['http://your-opa-service/v1/data/play'], 'rules': ['http://your-opa-service/v1/data/play/hello']} +``` -del client +- **Asynchronous**: + +```python +data = await client.get_data('users') +print(data) ``` +#### Delete Data -### Check permissions +To delete data from OPA: +- **Synchronous**: ```python -from opa_client.opa import OpaClient +client.delete_data('users') +``` -client = OpaClient() +- **Asynchronous**: -permission_you_want_check = {"input": {"message": "hello"}} -client.check_permission(input_data=permission_you_want_check, policy_name="testpolicy", rule_name="hello") +```python +await client.delete_data('users') +``` -# response is {'result': True} +### Policy Evaluation -# You can use query params for additional info -# provenance - If parameter is true, response will include build/version info in addition to the result. -# metrics - Return query performance metrics in addition to result +#### Check Permission (Policy Evaluation) + +You can evaluate policies with input data using `check_permission`. + +- **Synchronous**: -del client +```python +input_data = {"user": "admin"} +policy_name = 'example_policy' +rule_name = 'allow' + +result = client.check_permission(input_data, policy_name, rule_name) +print(result) ``` -### Queries a package rule with the given input data +- **Asynchronous**: ```python -from opa_client.opa import OpaClient +input_data = {"user": "admin"} +policy_name = 'example_policy' +rule_name = 'allow' -client = OpaClient() +result = await client.check_permission(input_data, policy_name, rule_name) +print(result) +``` + +Queries a package rule with the given input data + +```python rego = """ package play @@ -246,18 +389,41 @@ hello { """ check_data = {"message": "world"} -client.check_policy_rule(input_data=check_data, package_path="play", rule_name="hello") # response {'result': True} + +client.update_policy_from_string(rego, "test") +print(client.query_rule(input_data=check_data, package_path="play", rule_name="hello")) # {'result': True} + ``` -### Execute an Ad-hoc Query +- **Asynchronous**: ```python -from opa_client.opa import OpaClient -client = OpaClient() +rego = """ +package play -print(client.ad_hoc_query(query_params={"q": "data.userinfo.user_roles[name]"})) # response is {} +default hello = false +hello { + m := input.message + m == "world" +} +""" + +check_data = {"message": "world"} + +await client.update_policy_from_string(rego, "test") +print(await client.query_rule(input_data=check_data, package_path="play", rule_name="hello")) # {'result': True} + +``` + +### Ad-hoc Queries + +Execute ad-hoc queries directly: + +- **Synchronous**: + +```python data = { "user_roles": { "alice": [ @@ -272,38 +438,65 @@ data = { ] } } +input_data = {"user": "admin"} +client.update_or_create_data(data, "userinfo") -print(client.update_or_create_opa_data(data, "userinfo")) # response is True +result = client.ad_hoc_query(query="data.userinfo.user_roles[name]") +print(result) # {'result': [{'name': 'alice'}, {'name': 'bob'}, {'name': 'eve'}]} +``` -# execute query -print(client.ad_hoc_query(query_params={"q": "data.userinfo.user_roles[name]"})) -# response is {'result': [{'name': 'eve'}, {'name': 'alice'}, {'name': 'bob'}]} +- **Asynchronous**: -#you can send body request -print(client.ad_hoc_query(body={"query": "data.userinfo.user_roles[name] "})) -# response is {'result': [{'name': 'eve'}, {'name': 'alice'}, {'name': 'bob'}]} +```python +data = { + "user_roles": { + "alice": [ + "admin" + ], + "bob": [ + "employee", + "billing" + ], + "eve": [ + "customer" + ] + } +} +input_data = {"user": "admin"} +await client.update_or_create_data(data, "userinfo") + +result = await client.ad_hoc_query(query="data.userinfo.user_roles[name]") +print(result) # {'result': [{'name': 'alice'}, {'name': 'bob'}, {'name': 'eve'}]} ``` -### Check OPA healthy. If you want check bundels or plugins, add query params for this. +## API Reference -```python -from opa_client.opa import OpaClient +### Synchronous Client (OpaClient) -client = OpaClient() +- `check_connection()`: Verify connection to OPA server. +- `get_policies_list()`: Get a list of all policies. +- `get_policies_info()`: Returns information about each policy, including policy path and policy rules. +- `get_policy(policy_name)`: Fetch a specific policy. +- `policy_to_file(policy_name)`: Save an OPA policy to a file.. +- `update_policy_from_string(policy_content, policy_name)`: Upload or update a policy using its string content. +- `update_policy_from_url(url,endpoint)`: Update OPA policy by fetching it from a URL. +- `update_policy_from_file(filepath,endpoint)`: Update OPA policy using a policy file. +- `delete_policy(policy_name)`: Delete a specific policy. +- `update_or_create_data(data_content, data_name)`: Create or update data in OPA. +- `get_data(data_name)`: Retrieve data from OPA. +- `delete_data(data_name)`: Delete data from OPA. +- `check_permission(input_data, policy_name, rule_name)`: Evaluate a policy using input data. +- `query_rule(input_data, package_path, rule_name)`: Query a specific rule in a package. +- `ad_hoc_query(query, input_data)`: Run an ad-hoc query. -print(client.check_health()) # response is True or False -print(client.check_health({"bundle": True})) # response is True or False -# If your diagnostic url different than default url, you can provide it. -print(client.check_health(diagnostic_url="http://localhost:8282/health")) # response is True or False -print(client.check_health(query={"bundle": True}, diagnostic_url="http://localhost:8282/health")) # response is True or False -``` +### Asynchronous Client (AsyncOpaClient) +Same as the synchronous client, but all methods are asynchronous and must be awaited. -# Contributing +## Contributing -Fell free to open issue and send pull request. +Contributions are welcome! Feel free to open issues, fork the repo, and submit pull requests. -Thanks To [Contributors](https://github.com/Turall/OPA-python-client/graphs/contributors). -Contributions of any kind are welcome! +## License -Before you start please read [CONTRIBUTING](https://github.com/Turall/OPA-python-client/blob/master/CONTRIBUTING.md) +This project is licensed under the MIT License. diff --git a/opa_client/__init__.py b/opa_client/__init__.py index 45576be..4a86d39 100644 --- a/opa_client/__init__.py +++ b/opa_client/__init__.py @@ -1,7 +1,18 @@ """Initialize the OpaClient package.""" from .opa import OpaClient +from .opa_async import AsyncOpaClient -__all__ = [ - 'OpaClient', -] + +def create_opa_client(async_mode=False, *args, **kwargs): + if async_mode: + return AsyncOpaClient(*args, **kwargs) + else: + return OpaClient(*args, **kwargs) + + +__version__ = "2.0.0" +__author__ = "Tural Muradov" +__license__ = "MIT" + +__all__ = ["OpaClient", "create_opa_client", "AsyncOpaClient"] diff --git a/opa_client/base.py b/opa_client/base.py new file mode 100644 index 0000000..4b83d31 --- /dev/null +++ b/opa_client/base.py @@ -0,0 +1,286 @@ +import os +from typing import Dict, Optional, Union +from urllib.parse import urlencode + +from .errors import ( + FileError, + PathNotFoundError, +) + + +class BaseClient: + """ + Base class for OpaClient implementations. + + This class contains common logic shared between synchronous and asynchronous clients. + """ + + def __init__( + self, + host: str = "localhost", + port: int = 8181, + version: str = "v1", + ssl: bool = False, + cert: Optional[Union[str, tuple]] = None, + headers: Optional[dict] = None, + timeout: float = 1.5, + ): + if not isinstance(port, int): + raise TypeError("The port must be an integer") + + self.host = host.strip() + self.port = port + self.version = version + self.ssl = ssl + self.cert = cert + self.timeout = timeout + + self.schema = "https://" if ssl else "http://" + self.root_url = f"{self.schema}{self.host}:{self.port}/{self.version}" + + self.headers = headers + + self._session = None # Will be initialized in the subclass + self.retries = 2 + self.timeout = 1.5 + + def _build_url( + self, path: str, query_params: Dict[str, str] = None + ) -> str: + url = f"{self.root_url}/{path.lstrip('/')}" + if query_params: + url = f"{url}?{urlencode(query_params)}" + return url + + def _load_policy_from_file(self, filepath: str) -> str: + if not os.path.isfile(filepath): + raise FileError(f"'{filepath}' is not a valid file") + with open(filepath, "r", encoding="utf-8") as file: + return file.read() + + def _save_policy_to_file( + self, policy_raw: str, path: Optional[str], filename: str + ) -> bool: + full_path = os.path.join(path or "", filename) + try: + with open(full_path, "w", encoding="utf-8") as file: + file.write(policy_raw) + return True + except OSError as e: + raise PathNotFoundError(f"Failed to write to '{full_path}'") from e + + # Abstract methods to be implemented in subclasses + def close_connection(self): + raise NotImplementedError + + def check_connection(self) -> str: + raise NotImplementedError + + def _init_session(self): + raise NotImplementedError + + def check_health( + self, query: Dict[str, bool] = None, diagnostic_url: str = None + ) -> bool: + raise NotImplementedError + + def get_policies_list(self) -> list: + raise NotImplementedError + + def get_policies_info(self) -> dict: + raise NotImplementedError + + def update_policy_from_string( + self, new_policy: str, endpoint: str + ) -> bool: + raise NotImplementedError + + def update_policy_from_file(self, filepath: str, endpoint: str) -> bool: + raise NotImplementedError + + def update_policy_from_url(self, url: str, endpoint: str) -> bool: + raise NotImplementedError + + def update_or_create_data(self, new_data: dict, endpoint: str) -> bool: + raise NotImplementedError + + def get_data( + self, data_name: str = "", query_params: Dict[str, bool] = None + ) -> dict: + raise NotImplementedError + + def policy_to_file( + self, + policy_name: str, + path: Optional[str] = None, + filename: str = "opa_policy.rego", + ) -> bool: + raise NotImplementedError + + def get_policy(self, policy_name: str) -> dict: + raise NotImplementedError + + def delete_policy(self, policy_name: str) -> bool: + raise NotImplementedError + + def delete_data(self, data_name: str) -> bool: + raise NotImplementedError + + def check_permission( + self, + input_data: dict, + policy_name: str, + rule_name: str, + query_params: Dict[str, bool] = None, + ) -> dict: + raise NotImplementedError + + def query_rule( + self, + input_data: dict, + package_path: str, + rule_name: Optional[str] = None, + ) -> dict: + raise NotImplementedError + + def ad_hoc_query(self, query: str, input_data: dict = None) -> dict: + raise NotImplementedError + + +# class OpaClient(BaseOpaClient): +# """ +# Synchronous OpaClient implementation using requests. +# """ + +# def __init__(self, *args, **kwargs): +# super().__init__(*args, **kwargs) + +# self._init_session() + +# def _init_session(self): +# self._session = requests.Session() +# self._session.headers.update(self.headers) + +# if self.ssl: +# self._session.verify = self.cert if self.cert else True + +# # Optionally, configure retries and other session parameters + +# def close_connection(self): +# if self._session: +# self._session.close() +# self._session = None + +# def check_connection(self) -> str: +# url = self._build_url('policies/') +# try: +# response = self._session.get(url, timeout=self.timeout) +# response.raise_for_status() +# return "Yes, I'm here :)" +# except requests.exceptions.RequestException as e: +# raise ConnectionsError('Service unreachable', 'Check config and try again') from e + +# # Implement other synchronous methods similarly +# # For example: +# def get_policies_list(self) -> list: +# url = self._build_url('policies/') +# response = self._session.get(url, timeout=self.timeout) +# response.raise_for_status() +# policies = response.json().get('result', []) +# return [policy.get('id') for policy in policies if policy.get('id')] + +# # ... Rest of synchronous methods ... + + +# class AsyncOpaClient(BaseOpaClient): +# """ +# Asynchronous OpaClient implementation using aiohttp. +# """ + +# async def __aenter__(self): +# await self._init_session() +# return self + +# async def __aexit__(self, exc_type, exc_value, traceback): +# await self.close_connection() + +# async def _init_session(self): +# ssl_context = None + +# if self.ssl: +# ssl_context = ssl.create_default_context() +# if self.cert: +# if isinstance(self.cert, tuple): +# ssl_context.load_cert_chain(*self.cert) +# else: +# ssl_context.load_cert_chain(self.cert) +# else: +# ssl_context.load_default_certs() + +# connector = aiohttp.TCPConnector(ssl=ssl_context) + +# self._session = aiohttp.ClientSession( +# headers=self.headers, +# connector=connector, +# timeout=aiohttp.ClientTimeout(total=self.timeout), +# ) + +# async def close_connection(self): +# if self._session and not self._session.closed: +# await self._session.close() +# self._session = None + +# async def check_connection(self) -> str: +# url = self._build_url('policies/') +# try: +# async with self._session.get(url) as response: +# if response.status == 200: +# return "Yes, I'm here :)" +# else: +# raise ConnectionsError('Service unreachable', 'Check config and try again') +# except Exception as e: +# raise ConnectionsError('Service unreachable', 'Check config and try again') from e + +# # Implement other asynchronous methods similarly +# # For example: +# async def get_policies_list(self) -> list: +# url = self._build_url('policies/') +# async with self._session.get(url) as response: +# response.raise_for_status() +# policies = await response.json() +# result = policies.get('result', []) +# return [policy.get('id') for policy in result if policy.get('id')] + +# # ... Rest of asynchronous methods ... + + +# # Example usage: + +# # Synchronous client +# def sync_example(): +# client = OpaClient(host='localhost', port=8181) +# try: +# result = client.check_connection() +# print(result) +# policies = client.get_policies_list() +# print("Policies:", policies) +# finally: +# client.close_connection() + + +# # Asynchronous client +# async def async_example(): +# async with AsyncOpaClient(host='localhost', port=8181) as client: +# result = await client.check_connection() +# print(result) +# policies = await client.get_policies_list() +# print("Policies:", policies) + + +# if __name__ == '__main__': +# # Run synchronous example +# sync_example() + +# # Run asynchronous example +# import asyncio +# asyncio.run(async_example()) diff --git a/opa_client/errors.py b/opa_client/errors.py index f20f145..1ab44ba 100644 --- a/opa_client/errors.py +++ b/opa_client/errors.py @@ -1,106 +1,106 @@ class ConnectionsError(Exception): - def __init__(self, expression, message): - """ - expression -- input expression in which the error occurred - message -- explanation of the error - """ - self.expression = expression - self.message = message + def __init__(self, expression, message): + """ + expression -- input expression in which the error occurred + message -- explanation of the error + """ + self.expression = expression + self.message = message class QueryExecuteError(Exception): - def __init__(self, expression, message): - """ - expression -- input expression in which the error occurred - message -- explanation of the error - """ - self.expression = expression - self.message = message + def __init__(self, expression, message): + """ + expression -- input expression in which the error occurred + message -- explanation of the error + """ + self.expression = expression + self.message = message class PolicyNotFoundError(Exception): - def __init__(self, expression, message): - """ - expression -- input expression in which the error occurred - message -- explanation of the error - """ - self.expression = expression - self.message = message + def __init__(self, expression, message): + """ + expression -- input expression in which the error occurred + message -- explanation of the error + """ + self.expression = expression + self.message = message class CheckPermissionError(Exception): - def __init__(self, expression, message): - """ - expression -- input expression in which the error occurred - message -- explanation of the error - """ - self.expression = expression - self.message = message + def __init__(self, expression, message): + """ + expression -- input expression in which the error occurred + message -- explanation of the error + """ + self.expression = expression + self.message = message class DeleteDataError(Exception): - def __init__(self, expression, message): - """ - expression -- input expression in which the error occurred - message -- explanation of the error - """ - self.expression = expression - self.message = message + def __init__(self, expression, message): + """ + expression -- input expression in which the error occurred + message -- explanation of the error + """ + self.expression = expression + self.message = message class DeletePolicyError(Exception): - def __init__(self, expression, message): - """ - expression -- input expression in which the error occurred - message -- explanation of the error - """ - self.expression = expression - self.message = message + def __init__(self, expression, message): + """ + expression -- input expression in which the error occurred + message -- explanation of the error + """ + self.expression = expression + self.message = message class PathNotFoundError(Exception): - def __init__(self, expression, message): - """ - expression -- input expression in which the error occurred - message -- explanation of the error - """ - self.expression = expression - self.message = message + def __init__(self, expression, message): + """ + expression -- input expression in which the error occurred + message -- explanation of the error + """ + self.expression = expression + self.message = message class RegoParseError(Exception): - def __init__(self, expression, message): - """ - expression -- input expression in which the error occurred - message -- explanation of the error - """ - self.expression = expression - self.message = message + def __init__(self, expression, message): + """ + expression -- input expression in which the error occurred + message -- explanation of the error + """ + self.expression = expression + self.message = message class SSLError(Exception): - def __init__(self, expression, message): - """ - expression -- input expression in which the error occurred - message -- explanation of the error - """ - self.expression = expression - self.message = message + def __init__(self, expression, message): + """ + expression -- input expression in which the error occurred + message -- explanation of the error + """ + self.expression = expression + self.message = message class FileError(ValueError): - def __init__(self, expression, message): - """ - expression -- input expression in which the error occurred - message -- explanation of the error - """ - self.expression = expression - self.message = message - - -class TypeExecption(TypeError): - def __init__(self, expression): - """ - expression -- input expression in which the error occurred - """ - self.expression = expression + def __init__(self, expression, message): + """ + expression -- input expression in which the error occurred + message -- explanation of the error + """ + self.expression = expression + self.message = message + + +class TypeException(TypeError): + def __init__(self, expression): + """ + expression -- input expression in which the error occurred + """ + self.expression = expression diff --git a/opa_client/opa.py b/opa_client/opa.py index 6331636..28cf01b 100644 --- a/opa_client/opa.py +++ b/opa_client/opa.py @@ -1,599 +1,549 @@ -############################################ -# ________ ________ ________ # -# |\ __ \ |\ __ \ |\ __ \ # -# \ \ \|\ \ \ \ \|\ \ \ \ \|\ \ # -# \ \ \\\ \ \ \ ____\ \ \ __ \ # -# \ \ \\\ \ \ \ \___| \ \ \ \ \ # -# \ \_______\ \ \__\ \ \__\ \__\ # -# \|_______| \|__| \|__|\|__| # -############################################ - import json import os -from typing import Dict, Union +import threading +from typing import Dict, Optional from urllib.parse import urlencode import requests -import urllib3 -from user_agent import generate_user_agent +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry +from .base import BaseClient from .errors import ( - CheckPermissionError, - ConnectionsError, - DeleteDataError, - DeletePolicyError, - FileError, - PathNotFoundError, - PolicyNotFoundError, - QueryExecuteError, - RegoParseError, - SSLError, - TypeExecption, + CheckPermissionError, + ConnectionsError, + DeleteDataError, + DeletePolicyError, + FileError, + PathNotFoundError, + PolicyNotFoundError, + RegoParseError, + TypeException, ) -__version__ = '1.3.3' -__author__ = 'Tural Muradov' -__license__ = 'MIT' - - -class OpaClient: - """OpaClient client object to connect and manipulate OPA service. - ``` - The class object holds session information necesseary to connect OPA service. - param :: host : to connect OPA service ,defaults to localhost - type :: host: str - param :: port : to connect OPA service ,defaults to 8181 - type :: port : str or int - param :: version : provided REST API version by OPA,defaults to v1 - type :: version : str - param :: ssl : verify ssl certificates for https requests,defaults to False - type :: ssl : bool - param :: cert : path to client certificate information to use for mutual TLS authentification - type :: cert : str - param :: headers : dictionary of headers to send, defaults to None - ``` - """ - - def __init__( - self, - host: str = 'localhost', - port: int = 8181, - version: str = 'v1', - ssl: bool = False, - cert: Union[None, str] = None, - headers: Union[None, dict] = None, - **kwargs, - ): - host = host.lstrip() - self.__port = port - self.__version = version - self.__policy_root = '{}/policies/{}' - self.__data_root = '{}/data/{}' - self.__secure = False - self.__schema = 'http://' - self.retries = kwargs.get('retries', 2) - self.timeout = kwargs.get('timeout', 1.5) - - if not isinstance(self.__port, int): - raise TypeError('The port must be integer') - - if ssl: - self.__ssl = ssl - self.__cert = cert - self.__secure = True - self.__schema = 'https://' - - if not cert and ssl is True: - raise SSLError('ssl=True', 'Make sure you provide cert file') - - if host.startswith('https://'): - self.__host = host - self.__root_url = '{}:{}/{}'.format(self.__host, self.__port, self.__version) - - elif host.startswith('http://'): - self.__host = host - if self.__secure: - raise SSLError( - 'ssl=True', - 'With ssl enabled not possible to have connection with http', - ) - - self.__root_url = '{}:{}/{}'.format(self.__host, self.__port, self.__version) - - else: - self.__host = host - - self.__root_url = '{}{}:{}/{}'.format( - self.__schema, self.__host, self.__port, self.__version - ) - - if headers: - self.__headers = requests.utils.default_headers() - self.__headers.update({**headers}) - else: - self.__headers = requests.utils.default_headers() - - self.__headers.update({'User-Agent': generate_user_agent()}) - - if self.__secure: - self.__manager = urllib3.PoolManager( - cert_reqs='CERT_REQUIRED', - assert_hostname=False, - ca_certs=self.__cert, - headers=self.__headers, - ) - self.__session = self.__manager.request - else: - self.__manager = urllib3.PoolManager(headers=self.__headers) - self.__session = self.__manager.request - - def __del__(self): - self.close_connection() - - def close_connection(self): - """ - Close all currently open connections to the OPA server - """ - try: - self.__manager.clear() - except: # noqa: E722 - pass - - def check_connection(self): - """ - Checks whether established connection config True or not. - if not properly configured will raise an ConnectionError. - """ - - url = self.__policy_root.format(self.__root_url, '') - try: - response = self.__session('GET', url, retries=self.retries, timeout=self.timeout) - if response.status == 200: - return "Yes I'm here :)" - - except Exception: - raise ConnectionsError('service unreachable', 'check config and try again') - - raise ConnectionsError('service unreachable', 'check config and try again') - - def check_health(self, query: Dict[str, bool] = None, diagnostic_url: str = None) -> bool: - """ - Check OPA healthy. If you want check bundels or plugins, add query params for this. - If your diagnostic url different than default url, you can provide it. - ``` - param :: query : it is the url query string. default None - param :: diagnostic_url : OPA diagnostic url - - example: - print(client.check_health()) - print(client.check_health({"bundle": True})) - print(client.check_health(diagnostic_url="http://localhost:8282/health")) - print(client.check_health( - query={"bundle": True}, diagnostic_url="http://localhost:8282/health") - ) - """ - if diagnostic_url: - url = diagnostic_url - else: - url = '{}{}:{}/{}'.format(self.__schema, self.__host, self.__port, 'health') - if query: - url = self.prepare_args(url, query) - response = self.__session('GET', url, retries=self.retries, timeout=self.timeout) - if response.status == 200: - return True - return False - - def get_policies_list(self) -> list: - """Returns all OPA policies in the service""" - - return self.__get_policies_list() - - def get_policies_info(self) -> dict: - """ - Returns information about each policy, including - policy path and policy rules - """ - return self.__get_policies_info() - - def update_opa_policy_fromstring(self, new_policy: str, endpoint: str) -> bool: - """Write your rego policy with using python string type and update your OPA policies. - ``` - param :: new_policy : is the name of your new defined rego policy. - param :: endpoint : is the path of your new policy in OPA - - example: - new_policy= - package play - - default hello = false - - hello { - m := input.message - m == "world" - } - client = OpaClient() - client.update_opa_policy_fromstring(new_policy,'exampleapi') - ``` - """ - - return self.__update_opa_policy_fromstring(new_policy, endpoint) - - def update_opa_policy_fromfile(self, filepath: str, endpoint: str) -> bool: - """Write your rego policy to file and update your OPA policies.""" - - return self.__update_opa_policy_fromfile(filepath, endpoint) - - def update_opa_policy_fromurl(self, url: str, endpoint: str) -> bool: - """Update your OPA policies from internet.""" - - return self.__update_opa_policy_fromurl(url, endpoint) - - def update_or_create_opa_data(self, new_data: dict, endpoint: str) -> bool: - """Updates existing data or create new data for policy. - ``` - param :: new_data : name of defined data - type :: new_data : dict - param :: endpoint : is the path of your new data or existing one in OPA - type :: endpoint : str - example: - my_policy_list = [ - {"resource": "/api/someapi", "identity": "your_identity", "method": "PUT"}, - {"resource": "/api/someapi", "identity": "your_identity", "method": "GET"}, - ] - - client = OpaClient() - client.update_or_create_opa_data(my_policy_list,'exampledata/accesses') - ``` - """ - - return self.__update_opa_data(new_data, endpoint) - - def get_opa_raw_data(self, data_name: str = '', query_params: Dict[str, bool] = dict()) -> dict: - """Returns OPA raw data in string type - ``` - param :: data_name : OPA data name you want get - param :: query_params : query params in url for more information about metrics - ``` - """ - return self.__get_opa_raw_data(data_name, query_params) - - def opa_policy_to_file( - self, policy_name: str, path: Union[str, None] = None, filename: str = 'opa_policy.rego' - ): - """Write OPA service policy to the file. - ``` - param :: policy_name : name of OPA policy - type :: policy_name : str - - param :: path : path to save file ,default current path - type :: path : str - - param :: filename : name of the file,default opa_policy.rego - type :: filename : str - ``` - """ - return self.__opa_policy_to_file(policy_name, path, filename) - - def get_opa_policy(self, policy_name: str) -> dict: - """Returns full info about policy, provided OPA service""" - - return self.__get_opa_policy(policy_name) - - def delete_opa_policy(self, policy_name: str) -> bool: - """Deletes given OPA policy name""" - - return self.__delete_opa_policy(policy_name) - - def delete_opa_data(self, data_name: str) -> bool: - """Deletes given OPA policy data name""" - - return self.__delete_opa_data(data_name) - - def check_permission( - self, - input_data: dict, - policy_name: str, - rule_name: str, - query_params: Dict[str, bool] = dict(), - ) -> dict: - """ - ``` - params :: input_data : data which you want check permission - type :: input_data : dict - - params :: policy_name : the name of policy resource - type :: policy_name : str - - params :: rule_name : the name included in the policy - type :: rule_name : str - param :: query_params : query params in url for more information about metrics - type :: query_params : dict - ``` - """ - - return self.__check(input_data, policy_name, rule_name, query_params) - - def check_policy_rule(self, input_data: dict, package_path: str, rule_name: str = None) -> dict: - """ - Queries a package rule with the given input data - """ - - return self.__query(input_data, package_path, rule_name) - - def ad_hoc_query(self, *, query_params: Dict[str, str] = None, body: Dict[str, str] = None): - """Execute an ad-hoc query and return bindings for variables found in the query. - ``` - param :: query_params for sending query string in url - param :: body for sending query in request body - ``` - """ - - url = '{}{}:{}/{}/{}'.format(self.__schema, self.__host, self.__port, 'v1', 'query') - if body: - encoded_json = json.dumps(body).encode('utf-8') - response = self.__session( - 'POST', - url, - body=encoded_json, - retries=self.retries, - timeout=self.timeout, - ) - elif query_params: - url = self.prepare_args(url, query_params) - response = self.__session('GET', url, retries=self.retries, timeout=self.timeout) - data = json.loads(response.data.decode('utf-8')) - if response.status == 200: - return data - - raise QueryExecuteError(data.get('code'), data.get('message')) - - def prepare_args(self, url: str, query_params: dict) -> str: - if query_params: - query_params = urlencode(query_params) - url = url + '?' + query_params - return url - - def __get_opa_raw_data(self, data_name: str, query_params: Dict[str, bool]): - url = self.__data_root.format(self.__root_url, data_name) - url = self.prepare_args(url, query_params) - response = self.__session('GET', url, retries=self.retries, timeout=self.timeout) - code = response.status - response = json.loads(response.data.decode('utf-8')) - return response if code == 200 else (code, 'not found') - - def __update_opa_data(self, new_data: dict, endpoint: str): - url = self.__data_root.format(self.__root_url, endpoint) - - encoded_json = json.dumps(new_data).encode('utf-8') - response = self.__session( - 'PUT', - url, - body=encoded_json, - retries=self.retries, - timeout=self.timeout, - ) - code = response.status - return True if code == 204 else False - - def __update_opa_policy_fromfile(self, filepath: str, endpoint: str): - - if os.path.isfile(filepath): - with open(filepath, 'r') as rf: - return self.__update_opa_policy_fromstring(rf.read(), endpoint) - - raise FileError(f'{filepath}', 'is not a file, make sure you provide a file') - - def __update_opa_policy_fromstring(self, new_policy: str, endpoint: str) -> bool: - - if not isinstance(new_policy, str) or not isinstance(endpoint, str): - raise TypeExecption(f'{new_policy} is not string type') - - if new_policy: - url = self.__policy_root.format(self.__root_url, endpoint) - - response = self.__session( - 'PUT', - url, - body=new_policy.encode(), - headers=self.__headers, - retries=self.retries, - timeout=self.timeout, - ) - - if response.status == 200: - return True - - raise RegoParseError(response.status, json.loads(response.data.decode())) - - return False - - def __get_opa_policy(self, policy_name: str) -> dict: - url = self.__policy_root.format(self.__root_url, policy_name) - - response = self.__session('GET', url, retries=self.retries, timeout=self.timeout) - data = json.loads(response.data.decode('utf-8')) - if response.status == 200: - - return data - - raise PolicyNotFoundError(data.get('code'), data.get('message')) - - def __update_opa_policy_fromurl(self, url: str, endpoint: str) -> bool: - response = requests.get(url, headers=self.__headers) - return self.__update_opa_policy_fromstring(response.text, endpoint) - - def __opa_policy_to_file(self, policy_name: str, path: Union[str, None], filename: str) -> bool: - raw_policy = self.__get_opa_policy(policy_name) - if isinstance(raw_policy, dict): - try: - if path: - with open(f'{path}/{filename}', 'wb') as wr: - wr.write(raw_policy.get('result').get('raw').encode()) - else: - with open(filename, 'wb') as wr: - wr.write(raw_policy.get('result').get('raw').encode()) - return True - - except: # noqa: E722 - raise PathNotFoundError('error when write file', 'path not found') - - def __delete_opa_policy(self, policy_name: str) -> bool: - url = self.__policy_root.format(self.__root_url, policy_name) - - response = self.__session('DELETE', url, retries=self.retries, timeout=self.timeout) - data = json.loads(response.data.decode('utf-8')) - if response.status == 200: - return True - - raise DeletePolicyError(data.get('code'), data.get('message')) - - def __get_policies_list(self) -> list: - url = self.__policy_root.format(self.__root_url, '') - temp = [] - response = self.__session( - 'GET', url, retries=self.retries, timeout=self.timeout, headers=self.__headers - ) - - response = json.loads(response.data.decode()) - - for policy in response.get('result'): - if policy.get('id'): - temp.append(policy.get('id')) - - return temp - - def __delete_opa_data(self, data_name: str) -> bool: - url = self.__data_root.format(self.__root_url, data_name) - - response = self.__session('DELETE', url, retries=self.retries, timeout=self.timeout) - if response.data: - data = json.loads(response.data.decode('utf-8')) - if response.status == 204: - return True - - raise DeleteDataError(data.get('code'), data.get('message')) - - def __get_policies_info(self) -> dict: - url = self.__policy_root.format(self.__root_url, '') - policy = self.__session( - 'GET', url, retries=self.retries, timeout=self.timeout, headers=self.__headers - ) - - policy = json.loads(policy.data.decode()) - result = policy.get('result') - - temp_dict = {} - for policy in result: - temp_policy = [] - temp_rules = [] - temp_url = '' - permission_url = self.__root_url - for path in policy.get('ast').get('package').get('path'): - permission_url += '/' + path.get('value') - temp_policy.append(permission_url) - - rules = list(set( - [rule.get("head").get("name") for rule in policy.get("ast").get("rules")] - )) - for rule in rules: - temp_url = permission_url - temp_url += "/" + rule - temp_rules.append(temp_url) - - temp_dict[policy.get('id')] = {'path': temp_policy, 'rules': temp_rules} - - return temp_dict - - def __check( - self, input_data: dict, policy_name: str, rule_name: str, query_params: Dict[str, bool] - ) -> dict: - url = self.__policy_root.format(self.__root_url, policy_name) - - policy = self.__session( - 'GET', url, headers=self.__headers, retries=self.retries, timeout=self.timeout - ) - policy = json.loads(policy.data.decode('utf-8')) - result = policy.get('result') - find = False - permission_url = self.__root_url - if result: - for path in result.get('ast').get('package').get('path'): - permission_url += '/' + path.get('value') - - rules = [rule.get("head").get("name") for rule in result.get("ast").get("rules")] - if rule_name in rules: - permission_url += "/" + rule_name - find = True - - if find: - encoded_json = json.dumps(input_data).encode('utf-8') - permission_url = self.prepare_args(permission_url, query_params) - response = self.__session( - 'POST', - permission_url, - body=encoded_json, - retries=self.retries, - timeout=self.timeout, - ) - if response.data: - data = json.loads(response.data.decode('utf-8')) - return data - - raise CheckPermissionError(f'{rule_name} rule not found', 'policy or rule name not correct') - - def __query(self, input_data: dict, package_path: str, rule_name: str = None) -> dict: - if '.' in package_path: - package_path = package_path.replace('.', '/') - if rule_name: - package_path = package_path + '/' + rule_name - url = self.__data_root.format(self.__root_url, package_path) - - encoded_json = json.dumps({'input': input_data}).encode('utf-8') - response = self.__session( - 'POST', url, body=encoded_json, retries=self.retries, timeout=self.timeout - ) - if response.data: - data = json.loads(response.data.decode('utf-8')) - return data - - raise CheckPermissionError(f'{rule_name} rule not found', 'policy or rule name not correct') - - @property - def _host(self): - return self.__host - - @property - def _port(self): - return self.__port - - @property - def _version(self): - return self.__version - - @property - def _root_url(self): - return self.__root_url - - @property - def _schema(self): - return self.__schema - - @property - def _policy_root(self): - return self.__policy_root - - @property - def _data_root(self): - return self.__data_root - - @property - def _secure(self): - return self.__secure - - @property - def _ssl(self): - return self.__ssl - - @property - def _cert(self): - return self.__cert + +class OpaClient(BaseClient): + """ + OpaClient client object to connect and manipulate OPA service. + + Parameters: + host (str): Host to connect to OPA service, defaults to 'localhost'. + port (int): Port to connect to OPA service, defaults to 8181. + version (str): REST API version provided by OPA, defaults to 'v1'. + ssl (bool): Verify SSL certificates for HTTPS requests, defaults to False. + cert (Optional[str]): Path to client certificate for mutual TLS authentication. + headers (Optional[dict]): Dictionary of headers to send, defaults to None. + retries (int): Number of retries for failed requests, defaults to 2. + timeout (float): Timeout for requests in seconds, defaults to 1.5. + + Example: + client = OpaClient(host='opa.example.com', ssl=True, cert='/path/to/cert.pem') + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._lock = threading.Lock() + self._session = self._init_session() + + def _init_session(self) -> requests.Session: + session = requests.Session() + if self.headers: + session.headers.update(self.headers) + + # Configure retries + retries = Retry( + total=self.retries, + backoff_factor=0.3, + status_forcelist=(500, 502, 504), + ) + adapter = HTTPAdapter(max_retries=retries) + + session.mount("http://", adapter) + session.mount("https://", adapter) + + if self.ssl: + session.verify = self.cert + + return session + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close_connection() + + def close_connection(self): + """Close the session and release any resources.""" + with self._lock: + self._session.close() + + def check_connection(self) -> str: + """ + Checks whether the established connection is configured properly. + If not, raises a ConnectionsError. + + Returns: + str: Confirmation message if the connection is successful. + """ + url = f"{self.root_url}/policies/" + try: + response = self._session.get(url, timeout=self.timeout) + response.raise_for_status() + return True + except requests.exceptions.RequestException as e: + raise ConnectionsError( + "Service unreachable", "Check config and try again" + ) from e + + def check_health( + self, query: Dict[str, bool] = None, diagnostic_url: str = None + ) -> bool: + """ + Check if OPA is healthy. + + Parameters: + query (Dict[str, bool], optional): Query parameters for health check. + diagnostic_url (str, optional): Custom diagnostic URL. + + Returns: + bool: True if OPA is healthy, False otherwise. + """ + url = diagnostic_url or f"{self.schema}{self.host}:{self.port}/health" + if query: + url = f"{url}?{urlencode(query)}" + try: + response = self._session.get(url, timeout=self.timeout) + return response.status_code == 200 + except requests.exceptions.RequestException: + return False + + def get_policies_list(self) -> list: + """Returns all OPA policies in the service.""" + url = f"{self.root_url}/policies/" + response = self._session.get(url, timeout=self.timeout) + response.raise_for_status() + policies = response.json().get("result", []) + return [policy.get("id") for policy in policies if policy.get("id")] + + def get_policies_info(self) -> dict: + """ + Returns information about each policy, including + policy path and policy rules. + """ + url = f"{self.root_url}/policies/" + response = self._session.get(url, timeout=self.timeout) + response.raise_for_status() + policies = response.json().get("result", []) + policies_info = {} + + for policy in policies: + policy_id = policy.get("id") + ast = policy.get("ast", {}) + package_path = "/".join( + [ + p.get("value") + for p in ast.get("package", {}).get("path", []) + ] + ) + rules = list( + set( + rule.get("head", {}).get("name") + for rule in ast.get("rules", []) + ) + ) + policy_url = f"{self.root_url}/{package_path}" + rules_urls = [f"{policy_url}/{rule}" for rule in rules if rule] + policies_info[policy_id] = { + "path": policy_url, + "rules": rules_urls, + } + + return policies_info + + def update_policy_from_string( + self, new_policy: str, endpoint: str + ) -> bool: + """ + Update OPA policy using a policy string. + + Parameters: + new_policy (str): The new policy in Rego language. + endpoint (str): The policy endpoint in OPA. + + Returns: + bool: True if the policy was successfully updated. + """ + if not new_policy or not isinstance(new_policy, str): + raise TypeException("new_policy must be a non-empty string") + + url = f"{self.root_url}/policies/{endpoint}" + response = self._session.put( + url, + data=new_policy.encode("utf-8"), + headers={"Content-Type": "text/plain"}, + timeout=self.timeout, + ) + + if response.status_code == 200: + return True + else: + error = response.json() + raise RegoParseError(error.get("code"), error.get("message")) + + def update_policy_from_file(self, filepath: str, endpoint: str) -> bool: + """ + Update OPA policy using a policy file. + + Parameters: + filepath (str): Path to the policy file. + endpoint (str): The policy endpoint in OPA. + + Returns: + bool: True if the policy was successfully updated. + """ + if not os.path.isfile(filepath): + raise FileError(f"'{filepath}' is not a valid file") + + with open(filepath, "r", encoding="utf-8") as file: + policy_str = file.read() + + return self.update_policy_from_string(policy_str, endpoint) + + def update_policy_from_url(self, url: str, endpoint: str) -> bool: + """ + Update OPA policy by fetching it from a URL. + + Parameters: + url (str): URL to fetch the policy from. + endpoint (str): The policy endpoint in OPA. + + Returns: + bool: True if the policy was successfully updated. + """ + response = requests.get(url) + response.raise_for_status() + policy_str = response.text + return self.update_policy_from_string(policy_str, endpoint) + + def update_or_create_data(self, new_data: dict, endpoint: str) -> bool: + """ + Update or create OPA data. + + Parameters: + new_data (dict): The data to be updated or created. + endpoint (str): The data endpoint in OPA. + + Returns: + bool: True if the data was successfully updated or created. + """ + if not isinstance(new_data, dict): + raise TypeException("new_data must be a dictionary") + + url = f"{self.root_url}/data/{endpoint}" + response = self._session.put( + url, + json=new_data, + headers={"Content-Type": "application/json"}, + timeout=self.timeout, + ) + + if response.status_code == 204: + return True + else: + error = response.json() + raise RegoParseError(error.get("code"), error.get("message")) + + def get_data( + self, data_name: str = "", query_params: Dict[str, bool] = None + ) -> dict: + """ + Get OPA data. + + Parameters: + data_name (str, optional): The name of the data to retrieve. + query_params (Dict[str, bool], optional): Query parameters. + + Returns: + dict: The retrieved data. + """ + url = f"{self.root_url}/data/{data_name}" + if query_params: + url = f"{url}?{urlencode(query_params)}" + response = self._session.get(url, timeout=self.timeout) + if response.status_code == 200 and response.json().get("result"): + return response.json() + else: + error = response.json() + raise PolicyNotFoundError( + "PolicyNotFoundError", + error.get("message", "requested data not found"), + ) + + def policy_to_file( + self, + policy_name: str, + path: Optional[str] = None, + filename: str = "opa_policy.rego", + ) -> bool: + """ + Save an OPA policy to a file. + + Parameters: + policy_name (str): The name of the policy. + path (Optional[str]): The directory path to save the file. + filename (str): The name of the file. + + Returns: + bool: True if the policy was successfully saved. + """ + policy = self.get_policy(policy_name) + policy_raw = policy.get("result", {}).get("raw", "") + + if not policy_raw: + raise PolicyNotFoundError("Policy content is empty") + + full_path = os.path.join(path or "", filename) + + try: + with open(full_path, "w", encoding="utf-8") as file: + file.write(policy_raw) + return True + except OSError as e: + raise PathNotFoundError(f"Failed to write to '{full_path}'") from e + + def get_policy(self, policy_name: str) -> dict: + """ + Get a specific OPA policy. + + Parameters: + policy_name (str): The name of the policy. + + Returns: + dict: The policy data. + """ + url = f"{self.root_url}/policies/{policy_name}" + response = self._session.get(url, timeout=self.timeout) + if response.status_code == 200: + return response.json() + else: + error = response.json() + raise PolicyNotFoundError(error.get("code"), error.get("message")) + + def delete_policy(self, policy_name: str) -> bool: + """ + Delete an OPA policy. + + Parameters: + policy_name (str): The name of the policy. + + Returns: + bool: True if the policy was successfully deleted. + """ + url = f"{self.root_url}/policies/{policy_name}" + response = self._session.delete(url, timeout=self.timeout) + if response.status_code == 200: + return True + else: + error = response.json() + raise DeletePolicyError(error.get("code"), error.get("message")) + + def delete_data(self, data_name: str) -> bool: + """ + Delete OPA data. + + Parameters: + data_name (str): The name of the data. + + Returns: + bool: True if the data was successfully deleted. + """ + url = f"{self.root_url}/data/{data_name}" + response = self._session.delete(url, timeout=self.timeout) + if response.status_code == 204: + return True + else: + error = response.json() + raise DeleteDataError(error.get("code"), error.get("message")) + + def check_permission( + self, + input_data: dict, + policy_name: str, + rule_name: str, + query_params: Dict[str, bool] = None, + ) -> dict: + """ + Check permissions based on input data, policy name, and rule name. + + Parameters: + input_data (dict): The input data to check against the policy. + policy_name (str): The name of the policy. + rule_name (str): The name of the rule in the policy. + query_params (Dict[str, bool], optional): Query parameters. + + Returns: + dict: The result of the permission check. + """ + policy = self.get_policy(policy_name) + ast = policy.get("result", {}).get("ast", {}) + package_path = "/".join( + [p.get("value") for p in ast.get("package", {}).get("path", [])] + ) + rules = [ + rule.get("head", {}).get("name") for rule in ast.get("rules", []) + ] + + if rule_name not in rules: + raise CheckPermissionError( + f"Rule '{rule_name}' not found in policy '{policy_name}'" + ) + + url = f"{self.root_url}/{package_path}/{rule_name}" + if query_params: + url = f"{url}?{urlencode(query_params)}" + response = self._session.post( + url, json={"input": input_data}, timeout=self.timeout + ) + response.raise_for_status() + return response.json() + + def query_rule( + self, + input_data: dict, + package_path: str, + rule_name: Optional[str] = None, + ) -> dict: + """ + Query a specific rule in a package. + + Parameters: + input_data (dict): The input data for the query. + package_path (str): The package path. + rule_name (Optional[str]): The rule name. + + Returns: + dict: The result of the query. + """ + path = package_path.replace(".", "/") + if rule_name: + path = f"{path}/{rule_name}" + url = f"{self.root_url}/data/{path}" + + response = self._session.post( + url, json={"input": input_data}, timeout=self.timeout + ) + response.raise_for_status() + return response.json() + + def ad_hoc_query(self, query: str, input_data: dict = None) -> dict: + """ + Execute an ad-hoc query. + + Parameters: + query (str): The query string. + input_data (dict, optional): The input data for the query. + + Returns: + dict: The result of the query. + """ + url = f"{self.schema}{self.host}:{self.port}/v1/query" + params = {"q": query} + payload = {"input": input_data} if input_data else None + + response = self._session.get( + url, params=params, json=payload, timeout=self.timeout + ) + response.raise_for_status() + return response.json() + + # # Property methods for read-only access to certain attributes + # @property + # def host(self) -> str: + # return self._host + + # @host.setter + # def host(self, value: str): + # self._host = value + + # @property + # def port(self) -> int: + # return self._port + + # @port.setter + # def port(self, value: int): + # if not isinstance(value, int): + # raise TypeError('Port must be an integer') + # self._port = value + + # @property + # def version(self) -> str: + # return self._version + + # @version.setter + # def version(self, value: str): + # self._version = value + + # @property + # def schema(self) -> str: + # return self._schema + + # @schema.setter + # def schema(self, value: str): + # self._schema = value + + # @property + # def root_url(self) -> str: + # return self._root_url + + # @root_url.setter + # def root_url(self, value: str): + # self._root_url = value + + # @property + # def ssl(self) -> bool: + # return self._ssl + + # @ssl.setter + # def ssl(self, value: bool): + # self._ssl = value + + # @property + # def cert(self) -> Optional[str]: + # return self._cert + + # @cert.setter + # def cert(self, value: Optional[str]): + # self._cert = value + + # @property + # def headers(self) -> dict: + # return self._headers + + # @headers.setter + # def headers(self, value: dict): + # self._headers = value + + # @property + # def retries(self) -> int: + # return self._retries + + # @retries.setter + # def retries(self, value: int): + # self._retries = value + + # @property + # def timeout(self) -> float: + # return self._timeout + + # @timeout.setter + # def timeout(self, value: float): + # self._timeout = value + + +# Example usage: +if __name__ == "__main__": + client = OpaClient() + try: + print(client.check_connection()) + finally: + client.close_connection() diff --git a/opa_client/opa_async.py b/opa_client/opa_async.py new file mode 100644 index 0000000..7847ca2 --- /dev/null +++ b/opa_client/opa_async.py @@ -0,0 +1,604 @@ +import asyncio +import json +import os +import ssl +from typing import Dict, Optional, Union +from urllib.parse import urlencode + +import aiofiles +import aiohttp +from aiohttp import ClientSession, TCPConnector + +from .errors import ( + CheckPermissionError, + ConnectionsError, + DeleteDataError, + DeletePolicyError, + FileError, + PathNotFoundError, + PolicyNotFoundError, + RegoParseError, + TypeException, +) + + +class AsyncOpaClient: + """ + AsyncOpaClient client object to connect and manipulate OPA service asynchronously. + + Parameters: + host (str): Host to connect to OPA service, defaults to 'localhost'. + port (int): Port to connect to OPA service, defaults to 8181. + version (str): REST API version provided by OPA, defaults to 'v1'. + ssl (bool): Verify SSL certificates for HTTPS requests, defaults to False. + cert (Optional[str] or Tuple[str, str]): Path to client certificate or a tuple of (cert_file, key_file). + headers (Optional[dict]): Dictionary of headers to send, defaults to None. + timeout (float): Timeout for requests in seconds, defaults to 1.5. + + Example: + async with AsyncOpaClient(host='opa.example.com', ssl=True, cert='/path/to/cert.pem') as client: + await client.check_connection() + """ + + def __init__( + self, + host: str = "localhost", + port: int = 8181, + version: str = "v1", + ssl: bool = False, + cert: Optional[Union[str, tuple]] = None, + headers: Optional[dict] = None, + timeout: float = 1.5, + ): + if not isinstance(port, int): + raise TypeError("The port must be an integer") + + self.host = host.strip() + self.port = port + self.version = version + self.ssl = ssl + self.cert = cert + self.timeout = timeout + + self.schema = "https://" if ssl else "http://" + self.root_url = f"{self.schema}{self.host}:{self.port}/{self.version}" + + self.headers = headers + + # Initialize the session attributes + self._session: Optional[ClientSession] = None + self._connector = None # Will be initialized in _init_session + + async def __aenter__(self): + await self._init_session() + return self + + async def __aexit__(self, exc_type, exc_value, traceback): + await self.close_connection() + + async def _init_session(self): + ssl_context = None + + if self.ssl: + ssl_context = ssl.create_default_context() + + # If cert is provided, load the client certificate + if self.cert: + if isinstance(self.cert, tuple): + # Tuple of (cert_file, key_file) + ssl_context.load_cert_chain(*self.cert) + else: + # Single cert file (might contain both cert and key) + ssl_context.load_cert_chain(self.cert) + else: + # Verify default CA certificates + ssl_context.load_default_certs() + + self._connector = TCPConnector(ssl=ssl_context) + + self._session = aiohttp.ClientSession( + headers=self.headers, + connector=self._connector, + timeout=aiohttp.ClientTimeout(total=self.timeout), + ) + + async def close_connection(self): + """Close the session and release any resources.""" + if self._session and not self._session.closed: + await self._session.close() + self._session = None + + async def check_connection(self) -> str: + """ + Checks whether the established connection is configured properly. + If not, raises a ConnectionsError. + + Returns: + str: Confirmation message if the connection is successful. + """ + url = f"{self.root_url}/policies/" + try: + async with self._session.get(url) as response: + if response.status == 200: + return True + else: + raise ConnectionsError( + "Service unreachable", "Check config and try again" + ) + except Exception as e: + raise ConnectionsError( + "Service unreachable", "Check config and try again" + ) from e + + async def check_health( + self, query: Dict[str, bool] = None, diagnostic_url: str = None + ) -> bool: + """ + Check if OPA is healthy. + + Parameters: + query (Dict[str, bool], optional): Query parameters for health check. + diagnostic_url (str, optional): Custom diagnostic URL. + + Returns: + bool: True if OPA is healthy, False otherwise. + """ + url = diagnostic_url or f"{self.schema}{self.host}:{self.port}/health" + if query: + url = f"{url}?{urlencode(query)}" + try: + async with self._session.get(url) as response: + return response.status == 200 + except Exception: + return False + + async def get_policies_list(self) -> list: + """Returns all OPA policies in the service.""" + url = f"{self.root_url}/policies/" + async with self._session.get(url) as response: + response.raise_for_status() + policies = await response.json() + result = policies.get("result", []) + return [policy.get("id") for policy in result if policy.get("id")] + + async def get_policies_info(self) -> dict: + """ + Returns information about each policy, including + policy path and policy rules. + """ + url = f"{self.root_url}/policies/" + async with self._session.get(url) as response: + response.raise_for_status() + policies = await response.json() + result = policies.get("result", []) + policies_info = {} + + for policy in result: + policy_id = policy.get("id") + ast = policy.get("ast", {}) + package_path = "/".join( + [ + p.get("value") + for p in ast.get("package", {}).get("path", []) + ] + ) + rules = list( + set( + rule.get("head", {}).get("name") + for rule in ast.get("rules", []) + ) + ) + policy_url = f"{self.root_url}/{package_path}" + rules_urls = [f"{policy_url}/{rule}" for rule in rules if rule] + policies_info[policy_id] = { + "path": policy_url, + "rules": rules_urls, + } + + return policies_info + + async def update_policy_from_string( + self, new_policy: str, endpoint: str + ) -> bool: + """ + Update OPA policy using a policy string. + + Parameters: + new_policy (str): The new policy in Rego language. + endpoint (str): The policy endpoint in OPA. + + Returns: + bool: True if the policy was successfully updated. + """ + if not new_policy or not isinstance(new_policy, str): + raise TypeException("new_policy must be a non-empty string") + + url = f"{self.root_url}/policies/{endpoint}" + headers = self.headers.copy() if self.headers else {} + headers["Content-Type"] = "text/plain" + async with self._session.put( + url, data=new_policy.encode("utf-8"), headers=self.headers + ) as response: + if response.status == 200: + return True + else: + error = await response.json() + raise RegoParseError(error.get("code"), error.get("message")) + + async def update_policy_from_file( + self, filepath: str, endpoint: str + ) -> bool: + """ + Update OPA policy using a policy file. + + Parameters: + filepath (str): Path to the policy file. + endpoint (str): The policy endpoint in OPA. + + Returns: + bool: True if the policy was successfully updated. + """ + if not os.path.isfile(filepath): + raise FileError(f"'{filepath}' is not a valid file") + + async with aiofiles.open(filepath, "r", encoding="utf-8") as file: + policy_str = await file.read() + + return await self.update_policy_from_string(policy_str, endpoint) + + async def update_policy_from_url(self, url: str, endpoint: str) -> bool: + """ + Update OPA policy by fetching it from a URL. + + Parameters: + url (str): URL to fetch the policy from. + endpoint (str): The policy endpoint in OPA. + + Returns: + bool: True if the policy was successfully updated. + """ + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + response.raise_for_status() + policy_str = await response.text() + + return await self.update_policy_from_string(policy_str, endpoint) + + async def update_or_create_data( + self, new_data: dict, endpoint: str + ) -> bool: + """ + Update or create OPA data. + + Parameters: + new_data (dict): The data to be updated or created. + endpoint (str): The data endpoint in OPA. + + Returns: + bool: True if the data was successfully updated or created. + """ + if not isinstance(new_data, dict): + raise TypeException("new_data must be a dictionary") + + url = f"{self.root_url}/data/{endpoint}" + headers = self.headers.copy() if self.headers else {} + headers["Content-Type"] = "application/json" + async with self._session.put( + url, json=new_data, headers=headers + ) as response: + if response.status == 204: + return True + else: + error = await response.json() + raise RegoParseError(error.get("code"), error.get("message")) + + async def get_data( + self, data_name: str = "", query_params: Dict[str, bool] = None + ) -> dict: + """ + Get OPA data. + + Parameters: + data_name (str, optional): The name of the data to retrieve. + query_params (Dict[str, bool], optional): Query parameters. + + Returns: + dict: The retrieved data. + """ + url = f"{self.root_url}/data/{data_name}" + if query_params: + url = f"{url}?{urlencode(query_params)}" + async with self._session.get(url) as response: + if response.status == 200: + return await response.json() + else: + error = await response.json() + raise PolicyNotFoundError( + error.get("code"), error.get("message") + ) + + async def policy_to_file( + self, + policy_name: str, + path: Optional[str] = None, + filename: str = "opa_policy.rego", + ) -> bool: + """ + Save an OPA policy to a file. + + Parameters: + policy_name (str): The name of the policy. + path (Optional[str]): The directory path to save the file. + filename (str): The name of the file. + + Returns: + bool: True if the policy was successfully saved. + """ + policy = await self.get_policy(policy_name) + policy_raw = policy.get("result", {}).get("raw", "") + + if not policy_raw: + raise PolicyNotFoundError("Policy content is empty") + + full_path = os.path.join(path or "", filename) + + try: + async with aiofiles.open(full_path, "w", encoding="utf-8") as file: + await file.write(policy_raw) + return True + except OSError as e: + raise PathNotFoundError(f"Failed to write to '{full_path}'") from e + + async def get_policy(self, policy_name: str) -> dict: + """ + Get a specific OPA policy. + + Parameters: + policy_name (str): The name of the policy. + + Returns: + dict: The policy data. + """ + url = f"{self.root_url}/policies/{policy_name}" + async with self._session.get(url) as response: + if response.status == 200: + return await response.json() + else: + error = await response.json() + raise PolicyNotFoundError( + error.get("code"), error.get("message") + ) + + async def delete_policy(self, policy_name: str) -> bool: + """ + Delete an OPA policy. + + Parameters: + policy_name (str): The name of the policy. + + Returns: + bool: True if the policy was successfully deleted. + """ + url = f"{self.root_url}/policies/{policy_name}" + async with self._session.delete(url) as response: + if response.status == 200: + return True + else: + error = await response.json() + raise DeletePolicyError( + error.get("code"), error.get("message") + ) + + async def delete_data(self, data_name: str) -> bool: + """ + Delete OPA data. + + Parameters: + data_name (str): The name of the data. + + Returns: + bool: True if the data was successfully deleted. + """ + url = f"{self.root_url}/data/{data_name}" + async with self._session.delete(url) as response: + if response.status == 204: + return True + else: + error = await response.json() + raise DeleteDataError(error.get("code"), error.get("message")) + + async def check_permission( + self, + input_data: dict, + policy_name: str, + rule_name: str, + query_params: Dict[str, bool] = None, + ) -> dict: + """ + Check permissions based on input data, policy name, and rule name. + + Parameters: + input_data (dict): The input data to check against the policy. + policy_name (str): The name of the policy. + rule_name (str): The name of the rule in the policy. + query_params (Dict[str, bool], optional): Query parameters. + + Returns: + dict: The result of the permission check. + """ + policy = await self.get_policy(policy_name) + ast = policy.get("result", {}).get("ast", {}) + package_path = "/".join( + [p.get("value") for p in ast.get("package", {}).get("path", [])] + ) + rules = [ + rule.get("head", {}).get("name") for rule in ast.get("rules", []) + ] + + if rule_name not in rules: + raise CheckPermissionError( + f"Rule '{rule_name}' not found in policy '{policy_name}'" + ) + + url = f"{self.root_url}/data/{package_path}/{rule_name}" + if query_params: + url = f"{url}?{urlencode(query_params)}" + + async with self._session.post( + url, json={"input": input_data} + ) as response: + response.raise_for_status() + return await response.json() + + async def query_rule( + self, + input_data: dict, + package_path: str, + rule_name: Optional[str] = None, + ) -> dict: + """ + Query a specific rule in a package. + + Parameters: + input_data (dict): The input data for the query. + package_path (str): The package path. + rule_name (Optional[str]): The rule name. + + Returns: + dict: The result of the query. + """ + path = package_path.replace(".", "/") + if rule_name: + path = f"{path}/{rule_name}" + url = f"{self.root_url}/data/{path}" + + async with self._session.post( + url, json={"input": input_data} + ) as response: + response.raise_for_status() + return await response.json() + + async def ad_hoc_query(self, query: str, input_data: dict = None) -> dict: + """ + Execute an ad-hoc query. + + Parameters: + query (str): The query string. + input_data (dict, optional): The input data for the query. + + Returns: + dict: The result of the query. + """ + url = f"{self.schema}{self.host}:{self.port}/v1/query" + params = {"q": query} + payload = {"input": input_data} if input_data else None + + async with self._session.post( + url, params=params, json=payload + ) as response: + response.raise_for_status() + return await response.json() + + # Property methods for read-only access to certain attributes + @property + def host(self) -> str: + return self._host + + @host.setter + def host(self, value: str): + self._host = value + + @property + def port(self) -> int: + return self._port + + @port.setter + def port(self, value: int): + if not isinstance(value, int): + raise TypeError("Port must be an integer") + self._port = value + + @property + def version(self) -> str: + return self._version + + @version.setter + def version(self, value: str): + self._version = value + + @property + def schema(self) -> str: + return self._schema + + @schema.setter + def schema(self, value: str): + self._schema = value + + @property + def root_url(self) -> str: + return self._root_url + + @root_url.setter + def root_url(self, value: str): + self._root_url = value + + @property + def ssl(self) -> bool: + return self._ssl + + @ssl.setter + def ssl(self, value: bool): + self._ssl = value + + @property + def cert(self) -> Optional[str]: + return self._cert + + @cert.setter + def cert(self, value: Optional[str]): + self._cert = value + + @property + def headers(self) -> dict: + return self._headers + + @headers.setter + def headers(self, value: dict): + self._headers = value + + @property + def timeout(self) -> float: + return self._timeout + + @timeout.setter + def timeout(self, value: float): + self._timeout = value + + +# Example usage: +async def main(): + async with AsyncOpaClient() as client: + try: + result = await client.check_connection() + print(result) + finally: + await client.close_connection() + + +# Run the example +if __name__ == "__main__": + asyncio.run(main()) + + # Example usage: + async def main(): + async with AsyncOpaClient( + host="localhost", + port=8181, + ssl=True, + cert=("/path/to/cert.pem", "/path/to/key.pem"), + ) as client: + try: + result = await client.check_connection() + print(result) + finally: + await client.close_connection() diff --git a/opa_client/test/test_async_client.py b/opa_client/test/test_async_client.py new file mode 100644 index 0000000..c644de5 --- /dev/null +++ b/opa_client/test/test_async_client.py @@ -0,0 +1,118 @@ +import asyncio +import unittest +from unittest.mock import AsyncMock, Mock, patch + +from opa_client import create_opa_client +from opa_client.errors import ( + ConnectionsError, + DeleteDataError, + DeletePolicyError, + PolicyNotFoundError, + RegoParseError, +) + + +class TestAsyncOpaClient(unittest.IsolatedAsyncioTestCase): + async def asyncSetUp(self): + self.client = create_opa_client( + async_mode=True, host="localhost", port=8181 + ) + await self.client._init_session() + + async def asyncTearDown(self): + await self.client.close_connection() + + @patch("aiohttp.ClientSession.get") + async def test_check_connection_success(self, mock_get): + mock_response = AsyncMock() + mock_response.status = 200 + mock_get.return_value.__aenter__.return_value = mock_response + + result = await self.client.check_connection() + self.assertEqual(result, True) + mock_get.assert_called_once() + + @patch("aiohttp.ClientSession.get") + async def test_check_connection_failure(self, mock_get): + mock_response = AsyncMock() + mock_response.status = 500 + mock_get.return_value.__aenter__.return_value = mock_response + + with self.assertRaises(ConnectionsError): + await self.client.check_connection() + mock_get.assert_called_once() + + @patch("aiohttp.ClientSession.get") + async def test_get_policies_list(self, mock_get): + mock_response = AsyncMock() + mock_response.status = 200 + mock_response.raise_for_status = Mock() + mock_response.json = AsyncMock( + return_value={"result": [{"id": "policy1"}, {"id": "policy2"}]} + ) + mock_get.return_value.__aenter__.return_value = mock_response + + policies = await self.client.get_policies_list() + self.assertEqual(policies, ["policy1", "policy2"]) + mock_get.assert_called_once() + + @patch("aiohttp.ClientSession.put") + async def test_update_policy_from_string_success(self, mock_put): + mock_response = AsyncMock() + mock_response.status = 200 + mock_put.return_value.__aenter__.return_value = mock_response + + new_policy = "package example\n\ndefault allow = false" + result = await self.client.update_policy_from_string( + new_policy, "example" + ) + self.assertTrue(result) + mock_put.assert_called_once() + + @patch("aiohttp.ClientSession.put") + async def test_update_policy_from_string_failure(self, mock_put): + mock_response = AsyncMock() + mock_response.status = 400 + mock_response.json = AsyncMock( + return_value={ + "code": "invalid_parameter", + "message": "Parse error", + } + ) + mock_put.return_value.__aenter__.return_value = mock_response + + new_policy = "invalid policy" + with self.assertRaises(Exception) as context: + await self.client.update_policy_from_string(new_policy, "invalid") + + self.assertIsInstance(context.exception, RegoParseError) + mock_put.assert_called_once() + + @patch("aiohttp.ClientSession.delete") + async def test_delete_policy_success(self, mock_delete): + mock_response = AsyncMock() + mock_response.status = 200 + mock_delete.return_value.__aenter__.return_value = mock_response + + result = await self.client.delete_policy("policy1") + self.assertTrue(result) + mock_delete.assert_called_once() + + @patch("aiohttp.ClientSession.delete") + async def test_delete_policy_failure(self, mock_delete): + mock_response = AsyncMock() + mock_response.status = 404 + mock_response.json = AsyncMock( + return_value={"code": "not_found", "message": "Policy not found"} + ) + mock_delete.return_value.__aenter__.return_value = mock_response + + with self.assertRaises(DeletePolicyError): + await self.client.delete_policy("nonexistent_policy") + mock_delete.assert_called_once() + + # Add more test methods to cover other functionalities + + +if __name__ == "__main__": + unittest.main() diff --git a/opa_client/test/test_integaration_opa.py b/opa_client/test/test_integaration_opa.py new file mode 100644 index 0000000..e5f16ad --- /dev/null +++ b/opa_client/test/test_integaration_opa.py @@ -0,0 +1,123 @@ +import unittest + +from opa_client.errors import ( + ConnectionsError, + DeleteDataError, + DeletePolicyError, + PolicyNotFoundError, + RegoParseError, +) +from opa_client.opa import OpaClient + + +class TestIntegrationOpaClient(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.client = OpaClient(host="localhost", port=8181) + try: + cls.client.check_connection() + except ConnectionsError: + raise RuntimeError( + "OPA server is not running. Please start the OPA server before running tests." + ) + + @classmethod + def tearDownClass(cls): + cls.client.close_connection() + + def test_check_connection(self): + result = self.client.check_connection() + self.assertEqual(result, True) + + def test_policy_lifecycle(self): + # Define a sample policy + policy_name = "example_policy" + policy_content = """ + package example + + default allow = false + + allow { + input.user == "admin" + } + """ + + # Create/Update the policy + result = self.client.update_policy_from_string( + policy_content, policy_name + ) + self.assertTrue(result) + + # Retrieve the policy + policy = self.client.get_policy(policy_name) + self.assertIn("result", policy) + self.assertIn("raw", policy["result"]) + self.assertIn("package example", policy["result"]["raw"]) + + # Delete the policy + result = self.client.delete_policy(policy_name) + self.assertTrue(result) + + # Ensure the policy is deleted + with self.assertRaises(PolicyNotFoundError): + self.client.get_policy(policy_name) + + def test_data_lifecycle(self): + # Define sample data + data_name = "users" + data_content = { + "users": [ + {"name": "alice", "role": "admin"}, + {"name": "bob", "role": "user"}, + ] + } + + # Create/Update the data + result = self.client.update_or_create_data(data_content, data_name) + self.assertTrue(result) + + # Retrieve the data + data = self.client.get_data(data_name) + self.assertIn("result", data) + self.assertEqual(data["result"], data_content) + + # Delete the data + result = self.client.delete_data(data_name) + self.assertTrue(result) + + # Ensure the data is deleted + with self.assertRaises(PolicyNotFoundError): + self.client.get_data(data_name) + + def test_check_permission(self): + # Define a sample policy + policy_name = "authz" + policy_content = """ + package authz + + default allow = false + + allow { + input.user.role == "admin" + } + """ + + # Create the policy + self.client.update_policy_from_string(policy_content, policy_name) + + # Define sample input data + input_data = {"user": {"name": "alice", "role": "admin"}} + + # Check permission + result = self.client.check_permission(input_data, policy_name, "allow") + self.assertIn("result", result), result + self.assertTrue(result["result"]) + + # Clean up + self.client.delete_policy(policy_name) + + # Add more integration tests as needed + + +if __name__ == "__main__": + unittest.main() diff --git a/opa_client/test/test_opa.py b/opa_client/test/test_opa.py index 7bc2712..04a0b9c 100644 --- a/opa_client/test/test_opa.py +++ b/opa_client/test/test_opa.py @@ -1,108 +1,128 @@ -# -*- coding: utf-8 -*- -""" -Unit tests for the OpaClient. -""" - - -from unittest import TestCase - -from opa_client.errors import DeleteDataError, DeletePolicyError -from opa_client.opa import OpaClient - - -class TestClient(TestCase): - def setUp(self): - """Set up the test for OpaClient object""" - - self.myclient = OpaClient() - - def tearDown(self): - """Close the connection to the OPA server by deleting the client""" - del self.myclient - - def test_client(self): - """Set up the test for OpaClient object""" - - client = OpaClient('localhost', 8181, 'v1') - self.assertEqual('http://localhost:8181/v1', client._root_url) - - client = OpaClient('localhost', 8181, 'v1') - self.assertEqual('http://localhost:8181/v1', client._root_url) - - self.assertFalse(False, self.myclient._secure) - self.assertEqual('http://', self.myclient._schema) - self.assertEqual('v1', self.myclient._version) - self.assertEqual('localhost', self.myclient._host) - self.assertEqual(8181, self.myclient._port) - - def test_connection_to_opa(self): - self.assertEqual("Yes I'm here :)", self.myclient.check_connection()) - - def test_functions(self): - new_policy = """ - package test.policy - - import data.test.acl - import input - - default allow = false - - allow { - access := acl[input.user] - access[_] == input.access - } - - authorized_users[user] { - access := acl[user] - access[_] == input.access - } - """ - - _dict = { - 'test': { - 'path': ['http://localhost:8181/v1/data/test/policy'], - 'rules': [ - 'http://localhost:8181/v1/data/test/policy/allow', - 'http://localhost:8181/v1/data/test/policy/authorized_users' - ], - } - } - - my_policy_list = { - "alice": ["read","write"], - "bob": ["read"] - } - - self.assertEqual(list(), self.myclient.get_policies_list()) - self.assertEqual(dict(), self.myclient.get_policies_info()) - self.assertEqual(True, self.myclient.update_opa_policy_fromstring(new_policy, 'test')) - self.assertEqual(['test'], self.myclient.get_policies_list()) - - policy_info = self.myclient.get_policies_info() - self.assertEqual(_dict['test']['path'], policy_info['test']['path']) - for rule in _dict['test']['rules']: - self.assertIn(rule, policy_info['test']['rules']) - - self.assertTrue( - True, self.myclient.update_or_create_opa_data(my_policy_list, 'test/acl') - ) - - self.assertEqual(True, self.myclient.opa_policy_to_file('test')) - - value = {'result': {'acl': {'alice': ['read', 'write'], 'bob': ['read']}, 'policy': {'allow': False, 'authorized_users': []}}} - self.assertEqual(value, self.myclient.get_opa_raw_data('test')) - - _input_a = {"input": {"user": "alice", "access": "write"}} - _input_b = {"input": {"access": "read"}} - value_a = {"result": True} - value_b = {"result": ["alice", "bob"]} - self.assertEqual(value_a, self.myclient.check_permission(input_data=_input_a, policy_name="test", rule_name="allow")) - self.assertEqual(value_b, self.myclient.check_permission(input_data=_input_b, policy_name="test", rule_name="authorized_users")) - - self.assertTrue(True, self.myclient.delete_opa_policy('test')) - with self.assertRaises(DeletePolicyError): - self.myclient.delete_opa_policy('test') - - self.assertTrue(True, self.myclient.delete_opa_data('test/acl')) - with self.assertRaises(DeleteDataError): - self.myclient.delete_opa_data('test/acl') +# # -*- coding: utf-8 -*- +# """ +# Unit tests for the OpaClient. +# """ + + +# from unittest import TestCase + +# from opa_client.errors import DeleteDataError, DeletePolicyError +# from opa import OpaClient + + +# class TestClient(TestCase): +# def setUp(self): +# """Set up the test for OpaClient object""" + +# self.myclient = OpaClient() + +# def tearDown(self): +# """Close the connection to the OPA server by deleting the client""" +# del self.myclient + +# def test_client(self): +# """Set up the test for OpaClient object""" + +# client = OpaClient('localhost', 8181, 'v1') +# self.assertEqual('http://localhost:8181/v1', client._root_url) + +# client = OpaClient('localhost', 8181, 'v1') +# self.assertEqual('http://localhost:8181/v1', client._root_url) + +# self.assertEqual('http://', self.myclient._schema) +# self.assertEqual('v1', self.myclient._version) +# self.assertEqual('localhost', self.myclient._host) +# self.assertEqual(8181, self.myclient._port) + +# def test_connection_to_opa(self): +# self.assertEqual(True, self.myclient.check_connection()) + +# def test_functions(self): +# new_policy = """ +# package test.policy + +# import data.test.acl +# import input + +# default allow = false + +# allow { +# access := acl[input.user] +# access[_] == input.access +# } + +# authorized_users[user] { +# access := acl[user] +# access[_] == input.access +# } +# """ + +# _dict = { +# 'test': { +# 'path': 'http://localhost:8181/v1/data/test/policy', +# 'rules': [ +# 'http://localhost:8181/v1/data/test/policy/allow', +# 'http://localhost:8181/v1/data/test/policy/authorized_users' +# ], +# } +# } + +# my_policy_list = { +# "alice": ["read","write"], +# "bob": ["read"] +# } + +# self.assertEqual(list(), self.myclient.get_policies_list()) +# self.assertEqual(dict(), self.myclient.get_policies_info()) +# self.assertEqual(True, self.myclient.update_policy_from_string(new_policy, 'test')) +# self.assertEqual(['test'], self.myclient.get_policies_list()) + +# policy_info = self.myclient.get_policies_info() +# self.assertEqual(_dict['test']['path'], policy_info['test']['path']) +# for rule in _dict['test']['rules']: +# self.assertIn(rule, policy_info['test']['rules']) + +# self.assertTrue( +# True, self.myclient.update_or_create_data(my_policy_list, 'test/acl') +# ) + +# self.assertEqual(True, self.myclient.policy_to_file('test')) + +# value = {'result': {'acl': {'alice': ['read', 'write'], 'bob': ['read']}, 'policy': {'allow': False, 'authorized_users': []}}} +# self.assertEqual(value, self.myclient.get_data('test')) + +# _input_a = {"input": {"user": "alice", "access": "write"}} +# _input_b = {"input": {"access": "read"}} +# value_a = {"result": True} +# value_b = {"result": ["alice", "bob"]} +# self.assertEqual(value_a, self.myclient.check_permission(input_data=_input_a, policy_name="test", rule_name="allow")) +# self.assertEqual(value_b, self.myclient.check_permission(input_data=_input_b, policy_name="test", rule_name="authorized_users")) + +# self.assertTrue(True, self.myclient.delete_opa_policy('test')) +# with self.assertRaises(DeletePolicyError): +# self.myclient.delete_opa_policy('test') + +# self.assertTrue(True, self.myclient.delete_opa_data('test/acl')) +# with self.assertRaises(DeleteDataError): +# self.myclient.delete_opa_data('test/acl') + + +# # import threading + +# # def test_client_in_thread(client, policy_name): +# # try: +# # policies = client.get_policies_list() +# # print(f"Policies in thread {threading.current_thread().name}: {policies}") +# # except Exception as e: +# # print(f"Error in thread {threading.current_thread().name}: {e}") + +# # client = OpaClient() + +# # threads = [] +# # for i in range(5): +# # t = threading.Thread(target=test_client_in_thread, args=(client, f"policy_{i}")) +# # threads.append(t) +# # t.start() + +# # for t in threads: +# # t.join() diff --git a/opa_client/test/test_opa_client.py b/opa_client/test/test_opa_client.py new file mode 100644 index 0000000..573b232 --- /dev/null +++ b/opa_client/test/test_opa_client.py @@ -0,0 +1,114 @@ +import unittest +from unittest.mock import Mock, patch + +import requests + +from opa_client import create_opa_client +from opa_client.errors import ( + ConnectionsError, + DeleteDataError, + DeletePolicyError, + PolicyNotFoundError, + RegoParseError, +) + + +class TestOpaClient(unittest.TestCase): + def setUp(self): + self.client = create_opa_client(host="localhost", port=8181) + + def tearDown(self): + self.client.close_connection() + + @patch("requests.Session.get") + def test_check_connection_success(self, mock_get): + mock_response = Mock() + mock_response.status_code = 200 + mock_get.return_value = mock_response + + result = self.client.check_connection() + self.assertEqual(result, True) + mock_get.assert_called_once() + + @patch("requests.Session.get") + def test_check_connection_failure(self, mock_get): + mock_response = Mock() + mock_response.status_code = 500 + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError() + ) + mock_get.return_value = mock_response + with self.assertRaises(ConnectionsError): + self.client.check_connection() + mock_get.assert_called_once() + + @patch("requests.Session.get") + def test_get_policies_list(self, mock_get): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "result": [{"id": "policy1"}, {"id": "policy2"}] + } + mock_get.return_value = mock_response + + policies = self.client.get_policies_list() + self.assertEqual(policies, ["policy1", "policy2"]) + mock_get.assert_called_once() + + @patch("requests.Session.put") + def test_update_policy_from_string_success(self, mock_put): + mock_response = Mock() + mock_response.status_code = 200 + mock_put.return_value = mock_response + + new_policy = "package example\n\ndefault allow = false" + result = self.client.update_policy_from_string(new_policy, "example") + self.assertTrue(result) + mock_put.assert_called_once() + + @patch("requests.Session.put") + def test_update_policy_from_string_failure(self, mock_put): + mock_response = Mock() + mock_response.status_code = 400 + mock_response.json.return_value = { + "code": "invalid_parameter", + "message": "Parse error", + } + mock_put.return_value = mock_response + + new_policy = "invalid policy" + with self.assertRaises(Exception) as context: + self.client.update_policy_from_string(new_policy, "invalid") + + self.assertIsInstance(context.exception, RegoParseError) + mock_put.assert_called_once() + + @patch("requests.Session.delete") + def test_delete_policy_success(self, mock_delete): + mock_response = Mock() + mock_response.status_code = 200 + mock_delete.return_value = mock_response + + result = self.client.delete_policy("policy1") + self.assertTrue(result) + mock_delete.assert_called_once() + + @patch("requests.Session.delete") + def test_delete_policy_failure(self, mock_delete): + mock_response = Mock() + mock_response.status_code = 404 + mock_response.json.return_value = { + "code": "not_found", + "message": "Policy not found", + } + mock_delete.return_value = mock_response + + with self.assertRaises(DeletePolicyError): + self.client.delete_policy("nonexistent_policy") + mock_delete.assert_called_once() + + # Add more test methods to cover other functionalities + + +if __name__ == "__main__": + unittest.main() diff --git a/poetry.lock b/poetry.lock index bff0ebf..e1da6fd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,77 +1,421 @@ # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] -name = "black" -version = "22.1.0" -description = "The uncompromising code formatter." +name = "aiodns" +version = "3.2.0" +description = "Simple DNS resolver for asyncio" optional = false -python-versions = ">=3.6.2" +python-versions = "*" files = [ - {file = "black-22.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6"}, - {file = "black-22.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866"}, - {file = "black-22.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71"}, - {file = "black-22.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab"}, - {file = "black-22.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5"}, - {file = "black-22.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a"}, - {file = "black-22.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0"}, - {file = "black-22.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba"}, - {file = "black-22.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1"}, - {file = "black-22.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8"}, - {file = "black-22.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28"}, - {file = "black-22.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912"}, - {file = "black-22.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3"}, - {file = "black-22.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3"}, - {file = "black-22.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61"}, - {file = "black-22.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd"}, - {file = "black-22.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f"}, - {file = "black-22.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0"}, - {file = "black-22.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c"}, - {file = "black-22.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2"}, - {file = "black-22.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321"}, - {file = "black-22.1.0-py3-none-any.whl", hash = "sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d"}, - {file = "black-22.1.0.tar.gz", hash = "sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5"}, + {file = "aiodns-3.2.0-py3-none-any.whl", hash = "sha256:e443c0c27b07da3174a109fd9e736d69058d808f144d3c9d56dbd1776964c5f5"}, + {file = "aiodns-3.2.0.tar.gz", hash = "sha256:62869b23409349c21b072883ec8998316b234c9a9e36675756e8e317e8768f72"}, ] [package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = ">=1.1.0" -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} +pycares = ">=4.0.0" + +[[package]] +name = "aiofiles" +version = "24.1.0" +description = "File support for asyncio." +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, + {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.4.3" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohappyeyeballs-2.4.3-py3-none-any.whl", hash = "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572"}, + {file = "aiohappyeyeballs-2.4.3.tar.gz", hash = "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586"}, +] + +[[package]] +name = "aiohttp" +version = "3.10.9" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohttp-3.10.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8b3fb28a9ac8f2558760d8e637dbf27aef1e8b7f1d221e8669a1074d1a266bb2"}, + {file = "aiohttp-3.10.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:91aa966858593f64c8a65cdefa3d6dc8fe3c2768b159da84c1ddbbb2c01ab4ef"}, + {file = "aiohttp-3.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63649309da83277f06a15bbdc2a54fbe75efb92caa2c25bb57ca37762789c746"}, + {file = "aiohttp-3.10.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3e7fabedb3fe06933f47f1538df7b3a8d78e13d7167195f51ca47ee12690373"}, + {file = "aiohttp-3.10.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c070430fda1a550a1c3a4c2d7281d3b8cfc0c6715f616e40e3332201a253067"}, + {file = "aiohttp-3.10.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:51d0a4901b27272ae54e42067bc4b9a90e619a690b4dc43ea5950eb3070afc32"}, + {file = "aiohttp-3.10.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fec5fac7aea6c060f317f07494961236434928e6f4374e170ef50b3001e14581"}, + {file = "aiohttp-3.10.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:172ad884bb61ad31ed7beed8be776eb17e7fb423f1c1be836d5cb357a096bf12"}, + {file = "aiohttp-3.10.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d646fdd74c25bbdd4a055414f0fe32896c400f38ffbdfc78c68e62812a9e0257"}, + {file = "aiohttp-3.10.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e86260b76786c28acf0b5fe31c8dca4c2add95098c709b11e8c35b424ebd4f5b"}, + {file = "aiohttp-3.10.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d7cafc11d70fdd8801abfc2ff276744ae4cb39d8060b6b542c7e44e5f2cfc2"}, + {file = "aiohttp-3.10.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fc262c3df78c8ff6020c782d9ce02e4bcffe4900ad71c0ecdad59943cba54442"}, + {file = "aiohttp-3.10.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:482c85cf3d429844396d939b22bc2a03849cb9ad33344689ad1c85697bcba33a"}, + {file = "aiohttp-3.10.9-cp310-cp310-win32.whl", hash = "sha256:aeebd3061f6f1747c011e1d0b0b5f04f9f54ad1a2ca183e687e7277bef2e0da2"}, + {file = "aiohttp-3.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:fa430b871220dc62572cef9c69b41e0d70fcb9d486a4a207a5de4c1f25d82593"}, + {file = "aiohttp-3.10.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:16e6a51d8bc96b77f04a6764b4ad03eeef43baa32014fce71e882bd71302c7e4"}, + {file = "aiohttp-3.10.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8bd9125dd0cc8ebd84bff2be64b10fdba7dc6fd7be431b5eaf67723557de3a31"}, + {file = "aiohttp-3.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dcf354661f54e6a49193d0b5653a1b011ba856e0b7a76bda2c33e4c6892f34ea"}, + {file = "aiohttp-3.10.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42775de0ca04f90c10c5c46291535ec08e9bcc4756f1b48f02a0657febe89b10"}, + {file = "aiohttp-3.10.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d1e4185c5d7187684d41ebb50c9aeaaaa06ca1875f4c57593071b0409d2444"}, + {file = "aiohttp-3.10.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2695c61cf53a5d4345a43d689f37fc0f6d3a2dc520660aec27ec0f06288d1f9"}, + {file = "aiohttp-3.10.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a3f063b41cc06e8d0b3fcbbfc9c05b7420f41287e0cd4f75ce0a1f3d80729e6"}, + {file = "aiohttp-3.10.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d37f4718002863b82c6f391c8efd4d3a817da37030a29e2682a94d2716209de"}, + {file = "aiohttp-3.10.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2746d8994ebca1bdc55a1e998feff4e94222da709623bb18f6e5cfec8ec01baf"}, + {file = "aiohttp-3.10.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6f3c6648aa123bcd73d6f26607d59967b607b0da8ffcc27d418a4b59f4c98c7c"}, + {file = "aiohttp-3.10.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:558b3d223fd631ad134d89adea876e7fdb4c93c849ef195049c063ada82b7d08"}, + {file = "aiohttp-3.10.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4e6cb75f8ddd9c2132d00bc03c9716add57f4beff1263463724f6398b813e7eb"}, + {file = "aiohttp-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:608cecd8d58d285bfd52dbca5b6251ca8d6ea567022c8a0eaae03c2589cd9af9"}, + {file = "aiohttp-3.10.9-cp311-cp311-win32.whl", hash = "sha256:36d4fba838be5f083f5490ddd281813b44d69685db910907636bc5dca6322316"}, + {file = "aiohttp-3.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:8be1a65487bdfc285bd5e9baf3208c2132ca92a9b4020e9f27df1b16fab998a9"}, + {file = "aiohttp-3.10.9-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4fd16b30567c5b8e167923be6e027eeae0f20cf2b8a26b98a25115f28ad48ee0"}, + {file = "aiohttp-3.10.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:40ff5b7660f903dc587ed36ef08a88d46840182d9d4b5694e7607877ced698a1"}, + {file = "aiohttp-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4edc3fd701e2b9a0d605a7b23d3de4ad23137d23fc0dbab726aa71d92f11aaaf"}, + {file = "aiohttp-3.10.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e525b69ee8a92c146ae5b4da9ecd15e518df4d40003b01b454ad694a27f498b5"}, + {file = "aiohttp-3.10.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5002a02c17fcfd796d20bac719981d2fca9c006aac0797eb8f430a58e9d12431"}, + {file = "aiohttp-3.10.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4ceeae2fb8cabdd1b71c82bfdd39662473d3433ec95b962200e9e752fb70d0"}, + {file = "aiohttp-3.10.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6e395c3d1f773cf0651cd3559e25182eb0c03a2777b53b4575d8adc1149c6e9"}, + {file = "aiohttp-3.10.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbdb8def5268f3f9cd753a265756f49228a20ed14a480d151df727808b4531dd"}, + {file = "aiohttp-3.10.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f82ace0ec57c94aaf5b0e118d4366cff5889097412c75aa14b4fd5fc0c44ee3e"}, + {file = "aiohttp-3.10.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6ebdc3b3714afe1b134b3bbeb5f745eed3ecbcff92ab25d80e4ef299e83a5465"}, + {file = "aiohttp-3.10.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f9ca09414003c0e96a735daa1f071f7d7ed06962ef4fa29ceb6c80d06696d900"}, + {file = "aiohttp-3.10.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1298b854fd31d0567cbb916091be9d3278168064fca88e70b8468875ef9ff7e7"}, + {file = "aiohttp-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60ad5b8a7452c0f5645c73d4dad7490afd6119d453d302cd5b72b678a85d6044"}, + {file = "aiohttp-3.10.9-cp312-cp312-win32.whl", hash = "sha256:1a0ee6c0d590c917f1b9629371fce5f3d3f22c317aa96fbdcce3260754d7ea21"}, + {file = "aiohttp-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:c46131c6112b534b178d4e002abe450a0a29840b61413ac25243f1291613806a"}, + {file = "aiohttp-3.10.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2bd9f3eac515c16c4360a6a00c38119333901b8590fe93c3257a9b536026594d"}, + {file = "aiohttp-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8cc0d13b4e3b1362d424ce3f4e8c79e1f7247a00d792823ffd640878abf28e56"}, + {file = "aiohttp-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ba1a599255ad6a41022e261e31bc2f6f9355a419575b391f9655c4d9e5df5ff5"}, + {file = "aiohttp-3.10.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:776e9f3c9b377fcf097c4a04b241b15691e6662d850168642ff976780609303c"}, + {file = "aiohttp-3.10.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8debb45545ad95b58cc16c3c1cc19ad82cffcb106db12b437885dbee265f0ab5"}, + {file = "aiohttp-3.10.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2555e4949c8d8782f18ef20e9d39730d2656e218a6f1a21a4c4c0b56546a02e"}, + {file = "aiohttp-3.10.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c54dc329cd44f7f7883a9f4baaefe686e8b9662e2c6c184ea15cceee587d8d69"}, + {file = "aiohttp-3.10.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e709d6ac598c5416f879bb1bae3fd751366120ac3fa235a01de763537385d036"}, + {file = "aiohttp-3.10.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:17c272cfe7b07a5bb0c6ad3f234e0c336fb53f3bf17840f66bd77b5815ab3d16"}, + {file = "aiohttp-3.10.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0c21c82df33b264216abffff9f8370f303dab65d8eee3767efbbd2734363f677"}, + {file = "aiohttp-3.10.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9331dd34145ff105177855017920dde140b447049cd62bb589de320fd6ddd582"}, + {file = "aiohttp-3.10.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ac3196952c673822ebed8871cf8802e17254fff2a2ed4835d9c045d9b88c5ec7"}, + {file = "aiohttp-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2c33fa6e10bb7ed262e3ff03cc69d52869514f16558db0626a7c5c61dde3c29f"}, + {file = "aiohttp-3.10.9-cp313-cp313-win32.whl", hash = "sha256:a14e4b672c257a6b94fe934ee62666bacbc8e45b7876f9dd9502d0f0fe69db16"}, + {file = "aiohttp-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:a35ed3d03910785f7d9d6f5381f0c24002b2b888b298e6f941b2fc94c5055fcd"}, + {file = "aiohttp-3.10.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f392ef50e22c31fa49b5a46af7f983fa3f118f3eccb8522063bee8bfa6755f8"}, + {file = "aiohttp-3.10.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d1f5c9169e26db6a61276008582d945405b8316aae2bb198220466e68114a0f5"}, + {file = "aiohttp-3.10.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8d9d10d10ec27c0d46ddaecc3c5598c4db9ce4e6398ca872cdde0525765caa2f"}, + {file = "aiohttp-3.10.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d97273a52d7f89a75b11ec386f786d3da7723d7efae3034b4dda79f6f093edc1"}, + {file = "aiohttp-3.10.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d271f770b52e32236d945911b2082f9318e90ff835d45224fa9e28374303f729"}, + {file = "aiohttp-3.10.9-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7003f33f5f7da1eb02f0446b0f8d2ccf57d253ca6c2e7a5732d25889da82b517"}, + {file = "aiohttp-3.10.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6e00c8a92e7663ed2be6fcc08a2997ff06ce73c8080cd0df10cc0321a3168d7"}, + {file = "aiohttp-3.10.9-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a61df62966ce6507aafab24e124e0c3a1cfbe23c59732987fc0fd0d71daa0b88"}, + {file = "aiohttp-3.10.9-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:60555211a006d26e1a389222e3fab8cd379f28e0fbf7472ee55b16c6c529e3a6"}, + {file = "aiohttp-3.10.9-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:d15a29424e96fad56dc2f3abed10a89c50c099f97d2416520c7a543e8fddf066"}, + {file = "aiohttp-3.10.9-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:a19caae0d670771ea7854ca30df76f676eb47e0fd9b2ee4392d44708f272122d"}, + {file = "aiohttp-3.10.9-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:99f9678bf0e2b1b695e8028fedac24ab6770937932eda695815d5a6618c37e04"}, + {file = "aiohttp-3.10.9-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2914caa46054f3b5ff910468d686742ff8cff54b8a67319d75f5d5945fd0a13d"}, + {file = "aiohttp-3.10.9-cp38-cp38-win32.whl", hash = "sha256:0bc059ecbce835630e635879f5f480a742e130d9821fbe3d2f76610a6698ee25"}, + {file = "aiohttp-3.10.9-cp38-cp38-win_amd64.whl", hash = "sha256:e883b61b75ca6efc2541fcd52a5c8ccfe288b24d97e20ac08fdf343b8ac672ea"}, + {file = "aiohttp-3.10.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fcd546782d03181b0b1d20b43d612429a90a68779659ba8045114b867971ab71"}, + {file = "aiohttp-3.10.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:85711eec2d875cd88c7eb40e734c4ca6d9ae477d6f26bd2b5bb4f7f60e41b156"}, + {file = "aiohttp-3.10.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:02d1d6610588bcd743fae827bd6f2e47e0d09b346f230824b4c6fb85c6065f9c"}, + {file = "aiohttp-3.10.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3668d0c2a4d23fb136a753eba42caa2c0abbd3d9c5c87ee150a716a16c6deec1"}, + {file = "aiohttp-3.10.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7c071235a47d407b0e93aa6262b49422dbe48d7d8566e1158fecc91043dd948"}, + {file = "aiohttp-3.10.9-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac74e794e3aee92ae8f571bfeaa103a141e409863a100ab63a253b1c53b707eb"}, + {file = "aiohttp-3.10.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bbf94d4a0447705b7775417ca8bb8086cc5482023a6e17cdc8f96d0b1b5aba6"}, + {file = "aiohttp-3.10.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb0b2d5d51f96b6cc19e6ab46a7b684be23240426ae951dcdac9639ab111b45e"}, + {file = "aiohttp-3.10.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e83dfefb4f7d285c2d6a07a22268344a97d61579b3e0dce482a5be0251d672ab"}, + {file = "aiohttp-3.10.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f0a44bb40b6aaa4fb9a5c1ee07880570ecda2065433a96ccff409c9c20c1624a"}, + {file = "aiohttp-3.10.9-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c2b627d3c8982691b06d89d31093cee158c30629fdfebe705a91814d49b554f8"}, + {file = "aiohttp-3.10.9-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:03690541e4cc866eef79626cfa1ef4dd729c5c1408600c8cb9e12e1137eed6ab"}, + {file = "aiohttp-3.10.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ad3675c126f2a95bde637d162f8231cff6bc0bc9fbe31bd78075f9ff7921e322"}, + {file = "aiohttp-3.10.9-cp39-cp39-win32.whl", hash = "sha256:1321658f12b6caffafdc35cfba6c882cb014af86bef4e78c125e7e794dfb927b"}, + {file = "aiohttp-3.10.9-cp39-cp39-win_amd64.whl", hash = "sha256:9fdf5c839bf95fc67be5794c780419edb0dbef776edcfc6c2e5e2ffd5ee755fa"}, + {file = "aiohttp-3.10.9.tar.gz", hash = "sha256:143b0026a9dab07a05ad2dd9e46aa859bffdd6348ddc5967b42161168c24f857"}, +] + +[package.dependencies] +aiodns = {version = ">=3.2.0", optional = true, markers = "(sys_platform == \"linux\" or sys_platform == \"darwin\") and extra == \"speedups\""} +aiohappyeyeballs = ">=2.3.0" +aiosignal = ">=1.1.2" +async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +Brotli = {version = "*", optional = true, markers = "platform_python_implementation == \"CPython\" and extra == \"speedups\""} +brotlicffi = {version = "*", optional = true, markers = "platform_python_implementation != \"CPython\" and extra == \"speedups\""} +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.12.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns (>=3.2.0)", "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 = "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 = "24.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, +] [package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [[package]] -name = "blue" -version = "0.9.1" -description = "Blue -- Some folks like black but I prefer blue." +name = "brotli" +version = "1.1.0" +description = "Python bindings for the Brotli compression library" optional = false python-versions = "*" files = [ - {file = "blue-0.9.1-py3-none-any.whl", hash = "sha256:037742c072c58a2ff024f59fb9164257b907f97f8f862008db3b013d1f27cc22"}, - {file = "blue-0.9.1.tar.gz", hash = "sha256:76b4f26884a8425042356601d80773db6e0e14bebaa7a59d7c54bf8cef2e2af5"}, + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"}, + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"}, + {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"}, + {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"}, + {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"}, + {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"}, + {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"}, + {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"}, + {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"}, + {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"}, + {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"}, + {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"}, + {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"}, + {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"}, + {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"}, + {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"}, + {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"}, + {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"}, + {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"}, +] + +[[package]] +name = "brotlicffi" +version = "1.1.0.0" +description = "Python CFFI bindings to the Brotli library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2e4aeb0bd2540cb91b069dbdd54d458da8c4334ceaf2d25df2f4af576d6766ca"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b7b0033b0d37bb33009fb2fef73310e432e76f688af76c156b3594389d81391"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54a07bb2374a1eba8ebb52b6fafffa2afd3c4df85ddd38fcc0511f2bb387c2a8"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7901a7dc4b88f1c1475de59ae9be59799db1007b7d059817948d8e4f12e24e35"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce01c7316aebc7fce59da734286148b1d1b9455f89cf2c8a4dfce7d41db55c2d"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:246f1d1a90279bb6069de3de8d75a8856e073b8ff0b09dcca18ccc14cec85979"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc4bc5d82bc56ebd8b514fb8350cfac4627d6b0743382e46d033976a5f80fab6"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c26ecb14386a44b118ce36e546ce307f4810bc9598a6e6cb4f7fca725ae7e6"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca72968ae4eaf6470498d5c2887073f7efe3b1e7d7ec8be11a06a79cc810e990"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:add0de5b9ad9e9aa293c3aa4e9deb2b61e99ad6c1634e01d01d98c03e6a354cc"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b6068e0f3769992d6b622a1cd2e7835eae3cf8d9da123d7f51ca9c1e9c333e5"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8557a8559509b61e65083f8782329188a250102372576093c88930c875a69838"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a7ae37e5d79c5bdfb5b4b99f2715a6035e6c5bf538c3746abc8e26694f92f33"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391151ec86bb1c683835980f4816272a87eaddc46bb91cbf44f62228b84d8cca"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2f3711be9290f0453de8eed5275d93d286abe26b08ab4a35d7452caa1fef532f"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a807d760763e398bbf2c6394ae9da5815901aa93ee0a37bca5efe78d4ee3171"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa8ca0623b26c94fccc3a1fdd895be1743b838f3917300506d04aa3346fd2a14"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3de0cf28a53a3238b252aca9fed1593e9d36c1d116748013339f0949bfc84112"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6be5ec0e88a4925c91f3dea2bb0013b3a2accda6f77238f76a34a1ea532a1cb0"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d9eb71bb1085d996244439154387266fd23d6ad37161f6f52f1cd41dd95a3808"}, + {file = "brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13"}, ] [package.dependencies] -black = "22.1.0" -flake8 = ">=3.8,<5.0.0" +cffi = ">=1.0.0" [[package]] name = "certifi" -version = "2024.7.4" +version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "charset-normalizer" version = "3.3.2" @@ -171,20 +515,6 @@ files = [ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - [[package]] name = "colorama" version = "0.4.6" @@ -211,32 +541,105 @@ files = [ test = ["pytest (>=6)"] [[package]] -name = "flake8" -version = "4.0.1" -description = "the modular source code checker: pep8 pyflakes and co" +name = "frozenlist" +version = "1.4.1" +description = "A list-like structure which implements collections.abc.MutableSequence" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, - {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, + {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.dependencies] -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.8.0,<2.9.0" -pyflakes = ">=2.4.0,<2.5.0" - [[package]] name = "idna" -version = "3.7" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -249,82 +652,108 @@ files = [ ] [[package]] -name = "isort" -version = "5.13.2" -description = "A Python utility / library to sort Python imports." +name = "multidict" +version = "6.1.0" +description = "multidict implementation" optional = false -python-versions = ">=3.8.0" -files = [ - {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, - {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, -] - -[package.extras] -colors = ["colorama (>=0.4.6)"] - -[[package]] -name = "mccabe" -version = "0.6.1" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = "*" -files = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] - -[[package]] -name = "mypy" -version = "0.961" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "mypy-0.961-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:697540876638ce349b01b6786bc6094ccdaba88af446a9abb967293ce6eaa2b0"}, - {file = "mypy-0.961-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b117650592e1782819829605a193360a08aa99f1fc23d1d71e1a75a142dc7e15"}, - {file = "mypy-0.961-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bdd5ca340beffb8c44cb9dc26697628d1b88c6bddf5c2f6eb308c46f269bb6f3"}, - {file = "mypy-0.961-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3e09f1f983a71d0672bbc97ae33ee3709d10c779beb613febc36805a6e28bb4e"}, - {file = "mypy-0.961-cp310-cp310-win_amd64.whl", hash = "sha256:e999229b9f3198c0c880d5e269f9f8129c8862451ce53a011326cad38b9ccd24"}, - {file = "mypy-0.961-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b24be97351084b11582fef18d79004b3e4db572219deee0212078f7cf6352723"}, - {file = "mypy-0.961-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f4a21d01fc0ba4e31d82f0fff195682e29f9401a8bdb7173891070eb260aeb3b"}, - {file = "mypy-0.961-cp36-cp36m-win_amd64.whl", hash = "sha256:439c726a3b3da7ca84a0199a8ab444cd8896d95012c4a6c4a0d808e3147abf5d"}, - {file = "mypy-0.961-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5a0b53747f713f490affdceef835d8f0cb7285187a6a44c33821b6d1f46ed813"}, - {file = "mypy-0.961-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0e9f70df36405c25cc530a86eeda1e0867863d9471fe76d1273c783df3d35c2e"}, - {file = "mypy-0.961-cp37-cp37m-win_amd64.whl", hash = "sha256:b88f784e9e35dcaa075519096dc947a388319cb86811b6af621e3523980f1c8a"}, - {file = "mypy-0.961-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d5aaf1edaa7692490f72bdb9fbd941fbf2e201713523bdb3f4038be0af8846c6"}, - {file = "mypy-0.961-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9f5f5a74085d9a81a1f9c78081d60a0040c3efb3f28e5c9912b900adf59a16e6"}, - {file = "mypy-0.961-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f4b794db44168a4fc886e3450201365c9526a522c46ba089b55e1f11c163750d"}, - {file = "mypy-0.961-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:64759a273d590040a592e0f4186539858c948302c653c2eac840c7a3cd29e51b"}, - {file = "mypy-0.961-cp38-cp38-win_amd64.whl", hash = "sha256:63e85a03770ebf403291ec50097954cc5caf2a9205c888ce3a61bd3f82e17569"}, - {file = "mypy-0.961-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f1332964963d4832a94bebc10f13d3279be3ce8f6c64da563d6ee6e2eeda932"}, - {file = "mypy-0.961-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:006be38474216b833eca29ff6b73e143386f352e10e9c2fbe76aa8549e5554f5"}, - {file = "mypy-0.961-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9940e6916ed9371809b35b2154baf1f684acba935cd09928952310fbddaba648"}, - {file = "mypy-0.961-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a5ea0875a049de1b63b972456542f04643daf320d27dc592d7c3d9cd5d9bf950"}, - {file = "mypy-0.961-cp39-cp39-win_amd64.whl", hash = "sha256:1ece702f29270ec6af25db8cf6185c04c02311c6bb21a69f423d40e527b75c56"}, - {file = "mypy-0.961-py3-none-any.whl", hash = "sha256:03c6cc893e7563e7b2949b969e63f02c000b32502a1b4d1314cabe391aa87d66"}, - {file = "mypy-0.961.tar.gz", hash = "sha256:f730d56cb924d371c26b8eaddeea3cc07d78ff51c521c6d04899ac6904b75492"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"}, + {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"}, + {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, + {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, + {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, + {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, + {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"}, + {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"}, + {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"}, + {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"}, + {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"}, + {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"}, + {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"}, + {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, + {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, ] [package.dependencies] -mypy-extensions = ">=0.4.3" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=3.10" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -python2 = ["typed-ast (>=1.4.0,<2)"] -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"}, -] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} [[package]] name = "packaging" @@ -337,33 +766,6 @@ files = [ {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "platformdirs" -version = "4.2.2" -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.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, -] - -[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)"] -type = ["mypy (>=1.8)"] - [[package]] name = "pluggy" version = "1.5.0" @@ -380,36 +782,91 @@ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] -name = "pycodestyle" -version = "2.8.0" -description = "Python style guide checker" +name = "pycares" +version = "4.4.0" +description = "Python interface for c-ares" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.8" files = [ - {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, - {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, + {file = "pycares-4.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:24da119850841d16996713d9c3374ca28a21deee056d609fbbed29065d17e1f6"}, + {file = "pycares-4.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8f64cb58729689d4d0e78f0bfb4c25ce2f851d0274c0273ac751795c04b8798a"}, + {file = "pycares-4.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33e2a1120887e89075f7f814ec144f66a6ce06a54f5722ccefc62fbeda83cff"}, + {file = "pycares-4.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c680fef1b502ee680f8f0b95a41af4ec2c234e50e16c0af5bbda31999d3584bd"}, + {file = "pycares-4.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fff16b09042ba077f7b8aa5868d1d22456f0002574d0ba43462b10a009331677"}, + {file = "pycares-4.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:229a1675eb33bc9afb1fc463e73ee334950ccc485bc83a43f6ae5839fb4d5fa3"}, + {file = "pycares-4.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3aebc73e5ad70464f998f77f2da2063aa617cbd8d3e8174dd7c5b4518f967153"}, + {file = "pycares-4.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6ef64649eba56448f65e26546d85c860709844d2fc22ef14d324fe0b27f761a9"}, + {file = "pycares-4.4.0-cp310-cp310-win32.whl", hash = "sha256:4afc2644423f4eef97857a9fd61be9758ce5e336b4b0bd3d591238bb4b8b03e0"}, + {file = "pycares-4.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5ed4e04af4012f875b78219d34434a6d08a67175150ac1b79eb70ab585d4ba8c"}, + {file = "pycares-4.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bce8db2fc6f3174bd39b81405210b9b88d7b607d33e56a970c34a0c190da0490"}, + {file = "pycares-4.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9a0303428d013ccf5c51de59c83f9127aba6200adb7fd4be57eddb432a1edd2a"}, + {file = "pycares-4.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afb91792f1556f97be7f7acb57dc7756d89c5a87bd8b90363a77dbf9ea653817"}, + {file = "pycares-4.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b61579cecf1f4d616e5ea31a6e423a16680ab0d3a24a2ffe7bb1d4ee162477ff"}, + {file = "pycares-4.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7af06968cbf6851566e806bf3e72825b0e6671832a2cbe840be1d2d65350710"}, + {file = "pycares-4.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ceb12974367b0a68a05d52f4162b29f575d241bd53de155efe632bf2c943c7f6"}, + {file = "pycares-4.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2eeec144bcf6a7b6f2d74d6e70cbba7886a84dd373c886f06cb137a07de4954c"}, + {file = "pycares-4.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e3a6f7cfdfd11eb5493d6d632e582408c8f3b429f295f8799c584c108b28db6f"}, + {file = "pycares-4.4.0-cp311-cp311-win32.whl", hash = "sha256:34736a2ffaa9c08ca9c707011a2d7b69074bbf82d645d8138bba771479b2362f"}, + {file = "pycares-4.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:eb66c30eb11e877976b7ead13632082a8621df648c408b8e15cdb91a452dd502"}, + {file = "pycares-4.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fd644505a8cfd7f6584d33a9066d4e3d47700f050ef1490230c962de5dfb28c6"}, + {file = "pycares-4.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52084961262232ec04bd75f5043aed7e5d8d9695e542ff691dfef0110209f2d4"}, + {file = "pycares-4.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0c5368206057884cde18602580083aeaad9b860e2eac14fd253543158ce1e93"}, + {file = "pycares-4.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:112a4979c695b1c86f6782163d7dec58d57a3b9510536dcf4826550f9053dd9a"}, + {file = "pycares-4.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d186dafccdaa3409194c0f94db93c1a5d191145a275f19da6591f9499b8e7b8"}, + {file = "pycares-4.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:64965dc19c578a683ea73487a215a8897276224e004d50eeb21f0bc7a0b63c88"}, + {file = "pycares-4.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ed2a38e34bec6f2586435f6ff0bc5fe11d14bebd7ed492cf739a424e81681540"}, + {file = "pycares-4.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:94d6962db81541eb0396d2f0dfcbb18cdb8c8b251d165efc2d974ae652c547d4"}, + {file = "pycares-4.4.0-cp312-cp312-win32.whl", hash = "sha256:1168a48a834813aa80f412be2df4abaf630528a58d15c704857448b20b1675c0"}, + {file = "pycares-4.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:db24c4e7fea4a052c6e869cbf387dd85d53b9736cfe1ef5d8d568d1ca925e977"}, + {file = "pycares-4.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:21a5a0468861ec7df7befa69050f952da13db5427ae41ffe4713bc96291d1d95"}, + {file = "pycares-4.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:22c00bf659a9fa44d7b405cf1cd69b68b9d37537899898d8cbe5dffa4016b273"}, + {file = "pycares-4.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23aa3993a352491a47fcf17867f61472f32f874df4adcbb486294bd9fbe8abee"}, + {file = "pycares-4.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:813d661cbe2e37d87da2d16b7110a6860e93ddb11735c6919c8a3545c7b9c8d8"}, + {file = "pycares-4.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:77cf5a2fd5583c670de41a7f4a7b46e5cbabe7180d8029f728571f4d2e864084"}, + {file = "pycares-4.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3eaa6681c0a3e3f3868c77aca14b7760fed35fdfda2fe587e15c701950e7bc69"}, + {file = "pycares-4.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ad58e284a658a8a6a84af2e0b62f2f961f303cedfe551854d7bd40c3cbb61912"}, + {file = "pycares-4.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bfb89ca9e3d0a9b5332deeb666b2ede9d3469107742158f4aeda5ce032d003f4"}, + {file = "pycares-4.4.0-cp38-cp38-win32.whl", hash = "sha256:f36bdc1562142e3695555d2f4ac0cb69af165eddcefa98efc1c79495b533481f"}, + {file = "pycares-4.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:902461a92b6a80fd5041a2ec5235680c7cc35e43615639ec2a40e63fca2dfb51"}, + {file = "pycares-4.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7bddc6adba8f699728f7fc1c9ce8cef359817ad78e2ed52b9502cb5f8dc7f741"}, + {file = "pycares-4.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cb49d5805cd347c404f928c5ae7c35e86ba0c58ffa701dbe905365e77ce7d641"}, + {file = "pycares-4.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56cf3349fa3a2e67ed387a7974c11d233734636fe19facfcda261b411af14d80"}, + {file = "pycares-4.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bf2eaa83a5987e48fa63302f0fe7ce3275cfda87b34d40fef9ce703fb3ac002"}, + {file = "pycares-4.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82bba2ab77eb5addbf9758d514d9bdef3c1bfe7d1649a47bd9a0d55a23ef478b"}, + {file = "pycares-4.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c6a8bde63106f162fca736e842a916853cad3c8d9d137e11c9ffa37efa818b02"}, + {file = "pycares-4.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f5f646eec041db6ffdbcaf3e0756fb92018f7af3266138c756bb09d2b5baadec"}, + {file = "pycares-4.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9dc04c54c6ea615210c1b9e803d0e2d2255f87a3d5d119b6482c8f0dfa15b26b"}, + {file = "pycares-4.4.0-cp39-cp39-win32.whl", hash = "sha256:97892cced5794d721fb4ff8765764aa4ea48fe8b2c3820677505b96b83d4ef47"}, + {file = "pycares-4.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:917f08f0b5d9324e9a34211e68d27447c552b50ab967044776bbab7e42a553a2"}, + {file = "pycares-4.4.0.tar.gz", hash = "sha256:f47579d508f2f56eddd16ce72045782ad3b1b3b678098699e2b6a1b30733e1c2"}, ] +[package.dependencies] +cffi = ">=1.5.0" + +[package.extras] +idna = ["idna (>=2.1)"] + [[package]] -name = "pyflakes" -version = "2.4.0" -description = "passive checker of Python programs" +name = "pycparser" +version = "2.22" +description = "C parser in Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.8" files = [ - {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, - {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] [[package]] name = "pytest" -version = "7.4.4" +version = "8.3.3" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, - {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, ] [package.dependencies] @@ -417,11 +874,29 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.24.0" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, + {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, +] + +[package.dependencies] +pytest = ">=8.2,<9" [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "requests" @@ -445,25 +920,41 @@ socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" +name = "ruff" +version = "0.6.9" +description = "An extremely fast Python linter and code formatter, written in Rust." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = ">=3.7" files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"}, + {file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"}, + {file = "ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039"}, + {file = "ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d"}, + {file = "ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117"}, + {file = "ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93"}, + {file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"}, ] [[package]] name = "tomli" -version = "2.0.1" +version = "2.0.2" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, + {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, ] [[package]] @@ -479,13 +970,13 @@ files = [ [[package]] name = "urllib3" -version = "2.2.2" +version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, - {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, ] [package.extras] @@ -495,19 +986,111 @@ socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [[package]] -name = "user-agent" -version = "0.1.10" -description = "User-Agent generator" +name = "yarl" +version = "1.13.1" +description = "Yet another URL library" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "user_agent-0.1.10.tar.gz", hash = "sha256:b86537cb2a9d3bda0e2afcc654ec15b383502836877a67520654acadf73f1723"}, + {file = "yarl-1.13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:82e692fb325013a18a5b73a4fed5a1edaa7c58144dc67ad9ef3d604eccd451ad"}, + {file = "yarl-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df4e82e68f43a07735ae70a2d84c0353e58e20add20ec0af611f32cd5ba43fb4"}, + {file = "yarl-1.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec9dd328016d8d25702a24ee274932aebf6be9787ed1c28d021945d264235b3c"}, + {file = "yarl-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5820bd4178e6a639b3ef1db8b18500a82ceab6d8b89309e121a6859f56585b05"}, + {file = "yarl-1.13.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86c438ce920e089c8c2388c7dcc8ab30dfe13c09b8af3d306bcabb46a053d6f7"}, + {file = "yarl-1.13.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3de86547c820e4f4da4606d1c8ab5765dd633189791f15247706a2eeabc783ae"}, + {file = "yarl-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca53632007c69ddcdefe1e8cbc3920dd88825e618153795b57e6ebcc92e752a"}, + {file = "yarl-1.13.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4ee1d240b84e2f213565f0ec08caef27a0e657d4c42859809155cf3a29d1735"}, + {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c49f3e379177f4477f929097f7ed4b0622a586b0aa40c07ac8c0f8e40659a1ac"}, + {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5c5e32fef09ce101fe14acd0f498232b5710effe13abac14cd95de9c274e689e"}, + {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ab9524e45ee809a083338a749af3b53cc7efec458c3ad084361c1dbf7aaf82a2"}, + {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:b1481c048fe787f65e34cb06f7d6824376d5d99f1231eae4778bbe5c3831076d"}, + {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:31497aefd68036d8e31bfbacef915826ca2e741dbb97a8d6c7eac66deda3b606"}, + {file = "yarl-1.13.1-cp310-cp310-win32.whl", hash = "sha256:1fa56f34b2236f5192cb5fceba7bbb09620e5337e0b6dfe2ea0ddbd19dd5b154"}, + {file = "yarl-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:1bbb418f46c7f7355084833051701b2301092e4611d9e392360c3ba2e3e69f88"}, + {file = "yarl-1.13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:216a6785f296169ed52cd7dcdc2612f82c20f8c9634bf7446327f50398732a51"}, + {file = "yarl-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40c6e73c03a6befb85b72da213638b8aaa80fe4136ec8691560cf98b11b8ae6e"}, + {file = "yarl-1.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2430cf996113abe5aee387d39ee19529327205cda975d2b82c0e7e96e5fdabdc"}, + {file = "yarl-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fb4134cc6e005b99fa29dbc86f1ea0a298440ab6b07c6b3ee09232a3b48f495"}, + {file = "yarl-1.13.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:309c104ecf67626c033845b860d31594a41343766a46fa58c3309c538a1e22b2"}, + {file = "yarl-1.13.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f90575e9fe3aae2c1e686393a9689c724cd00045275407f71771ae5d690ccf38"}, + {file = "yarl-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d2e1626be8712333a9f71270366f4a132f476ffbe83b689dd6dc0d114796c74"}, + {file = "yarl-1.13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b66c87da3c6da8f8e8b648878903ca54589038a0b1e08dde2c86d9cd92d4ac9"}, + {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cf1ad338620249f8dd6d4b6a91a69d1f265387df3697ad5dc996305cf6c26fb2"}, + {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9915300fe5a0aa663c01363db37e4ae8e7c15996ebe2c6cce995e7033ff6457f"}, + {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:703b0f584fcf157ef87816a3c0ff868e8c9f3c370009a8b23b56255885528f10"}, + {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1d8e3ca29f643dd121f264a7c89f329f0fcb2e4461833f02de6e39fef80f89da"}, + {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7055bbade838d68af73aea13f8c86588e4bcc00c2235b4b6d6edb0dbd174e246"}, + {file = "yarl-1.13.1-cp311-cp311-win32.whl", hash = "sha256:a3442c31c11088e462d44a644a454d48110f0588de830921fd201060ff19612a"}, + {file = "yarl-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:81bad32c8f8b5897c909bf3468bf601f1b855d12f53b6af0271963ee67fff0d2"}, + {file = "yarl-1.13.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f452cc1436151387d3d50533523291d5f77c6bc7913c116eb985304abdbd9ec9"}, + {file = "yarl-1.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9cec42a20eae8bebf81e9ce23fb0d0c729fc54cf00643eb251ce7c0215ad49fe"}, + {file = "yarl-1.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d959fe96e5c2712c1876d69af0507d98f0b0e8d81bee14cfb3f6737470205419"}, + {file = "yarl-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8c837ab90c455f3ea8e68bee143472ee87828bff19ba19776e16ff961425b57"}, + {file = "yarl-1.13.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94a993f976cdcb2dc1b855d8b89b792893220db8862d1a619efa7451817c836b"}, + {file = "yarl-1.13.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b2442a415a5f4c55ced0fade7b72123210d579f7d950e0b5527fc598866e62c"}, + {file = "yarl-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fdbf0418489525231723cdb6c79e7738b3cbacbaed2b750cb033e4ea208f220"}, + {file = "yarl-1.13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b7f6e699304717fdc265a7e1922561b02a93ceffdaefdc877acaf9b9f3080b8"}, + {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bcd5bf4132e6a8d3eb54b8d56885f3d3a38ecd7ecae8426ecf7d9673b270de43"}, + {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2a93a4557f7fc74a38ca5a404abb443a242217b91cd0c4840b1ebedaad8919d4"}, + {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:22b739f99c7e4787922903f27a892744189482125cc7b95b747f04dd5c83aa9f"}, + {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2db874dd1d22d4c2c657807562411ffdfabec38ce4c5ce48b4c654be552759dc"}, + {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4feaaa4742517eaceafcbe74595ed335a494c84634d33961214b278126ec1485"}, + {file = "yarl-1.13.1-cp312-cp312-win32.whl", hash = "sha256:bbf9c2a589be7414ac4a534d54e4517d03f1cbb142c0041191b729c2fa23f320"}, + {file = "yarl-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:d07b52c8c450f9366c34aa205754355e933922c79135125541daae6cbf31c799"}, + {file = "yarl-1.13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:95c6737f28069153c399d875317f226bbdea939fd48a6349a3b03da6829fb550"}, + {file = "yarl-1.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cd66152561632ed4b2a9192e7f8e5a1d41e28f58120b4761622e0355f0fe034c"}, + {file = "yarl-1.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6a2acde25be0cf9be23a8f6cbd31734536a264723fca860af3ae5e89d771cd71"}, + {file = "yarl-1.13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a18595e6a2ee0826bf7dfdee823b6ab55c9b70e8f80f8b77c37e694288f5de1"}, + {file = "yarl-1.13.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a31d21089894942f7d9a8df166b495101b7258ff11ae0abec58e32daf8088813"}, + {file = "yarl-1.13.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45f209fb4bbfe8630e3d2e2052535ca5b53d4ce2d2026bed4d0637b0416830da"}, + {file = "yarl-1.13.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f722f30366474a99745533cc4015b1781ee54b08de73260b2bbe13316079851"}, + {file = "yarl-1.13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3bf60444269345d712838bb11cc4eadaf51ff1a364ae39ce87a5ca8ad3bb2c8"}, + {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:942c80a832a79c3707cca46bd12ab8aa58fddb34b1626d42b05aa8f0bcefc206"}, + {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:44b07e1690f010c3c01d353b5790ec73b2f59b4eae5b0000593199766b3f7a5c"}, + {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:396e59b8de7e4d59ff5507fb4322d2329865b909f29a7ed7ca37e63ade7f835c"}, + {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3bb83a0f12701c0b91112a11148b5217617982e1e466069d0555be9b372f2734"}, + {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c92b89bffc660f1274779cb6fbb290ec1f90d6dfe14492523a0667f10170de26"}, + {file = "yarl-1.13.1-cp313-cp313-win32.whl", hash = "sha256:269c201bbc01d2cbba5b86997a1e0f73ba5e2f471cfa6e226bcaa7fd664b598d"}, + {file = "yarl-1.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:1d0828e17fa701b557c6eaed5edbd9098eb62d8838344486248489ff233998b8"}, + {file = "yarl-1.13.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8be8cdfe20787e6a5fcbd010f8066227e2bb9058331a4eccddec6c0db2bb85b2"}, + {file = "yarl-1.13.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:08d7148ff11cb8e886d86dadbfd2e466a76d5dd38c7ea8ebd9b0e07946e76e4b"}, + {file = "yarl-1.13.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4afdf84610ca44dcffe8b6c22c68f309aff96be55f5ea2fa31c0c225d6b83e23"}, + {file = "yarl-1.13.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0d12fe78dcf60efa205e9a63f395b5d343e801cf31e5e1dda0d2c1fb618073d"}, + {file = "yarl-1.13.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298c1eecfd3257aa16c0cb0bdffb54411e3e831351cd69e6b0739be16b1bdaa8"}, + {file = "yarl-1.13.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c14c16831b565707149c742d87a6203eb5597f4329278446d5c0ae7a1a43928e"}, + {file = "yarl-1.13.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a9bacedbb99685a75ad033fd4de37129449e69808e50e08034034c0bf063f99"}, + {file = "yarl-1.13.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:658e8449b84b92a4373f99305de042b6bd0d19bf2080c093881e0516557474a5"}, + {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:373f16f38721c680316a6a00ae21cc178e3a8ef43c0227f88356a24c5193abd6"}, + {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:45d23c4668d4925688e2ea251b53f36a498e9ea860913ce43b52d9605d3d8177"}, + {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f7917697bcaa3bc3e83db91aa3a0e448bf5cde43c84b7fc1ae2427d2417c0224"}, + {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:5989a38ba1281e43e4663931a53fbf356f78a0325251fd6af09dd03b1d676a09"}, + {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:11b3ca8b42a024513adce810385fcabdd682772411d95bbbda3b9ed1a4257644"}, + {file = "yarl-1.13.1-cp38-cp38-win32.whl", hash = "sha256:dcaef817e13eafa547cdfdc5284fe77970b891f731266545aae08d6cce52161e"}, + {file = "yarl-1.13.1-cp38-cp38-win_amd64.whl", hash = "sha256:7addd26594e588503bdef03908fc207206adac5bd90b6d4bc3e3cf33a829f57d"}, + {file = "yarl-1.13.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a0ae6637b173d0c40b9c1462e12a7a2000a71a3258fa88756a34c7d38926911c"}, + {file = "yarl-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:576365c9f7469e1f6124d67b001639b77113cfd05e85ce0310f5f318fd02fe85"}, + {file = "yarl-1.13.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:78f271722423b2d4851cf1f4fa1a1c4833a128d020062721ba35e1a87154a049"}, + {file = "yarl-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d74f3c335cfe9c21ea78988e67f18eb9822f5d31f88b41aec3a1ec5ecd32da5"}, + {file = "yarl-1.13.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1891d69a6ba16e89473909665cd355d783a8a31bc84720902c5911dbb6373465"}, + {file = "yarl-1.13.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb382fd7b4377363cc9f13ba7c819c3c78ed97c36a82f16f3f92f108c787cbbf"}, + {file = "yarl-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c8854b9f80693d20cec797d8e48a848c2fb273eb6f2587b57763ccba3f3bd4b"}, + {file = "yarl-1.13.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbf2c3f04ff50f16404ce70f822cdc59760e5e2d7965905f0e700270feb2bbfc"}, + {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fb9f59f3848edf186a76446eb8bcf4c900fe147cb756fbbd730ef43b2e67c6a7"}, + {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ef9b85fa1bc91c4db24407e7c4da93a5822a73dd4513d67b454ca7064e8dc6a3"}, + {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:098b870c18f1341786f290b4d699504e18f1cd050ed179af8123fd8232513424"}, + {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:8c723c91c94a3bc8033dd2696a0f53e5d5f8496186013167bddc3fb5d9df46a3"}, + {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:44a4c40a6f84e4d5955b63462a0e2a988f8982fba245cf885ce3be7618f6aa7d"}, + {file = "yarl-1.13.1-cp39-cp39-win32.whl", hash = "sha256:84bbcdcf393139f0abc9f642bf03f00cac31010f3034faa03224a9ef0bb74323"}, + {file = "yarl-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:fc2931ac9ce9c61c9968989ec831d3a5e6fcaaff9474e7cfa8de80b7aff5a093"}, + {file = "yarl-1.13.1-py3-none-any.whl", hash = "sha256:6a5185ad722ab4dd52d5fb1f30dcc73282eb1ed494906a92d1a228d3f89607b0"}, + {file = "yarl-1.13.1.tar.gz", hash = "sha256:ec8cfe2295f3e5e44c51f57272afbd69414ae629ec7c6b27f5a410efc78b70a0"}, ] [package.dependencies] -six = "*" +idna = ">=2.0" +multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "3890bd5988ba42380a5b0035972309d477bc96573e7f3705efedcf92a816aa90" +content-hash = "0ee928b6044ff99d1fd6522e48131635092c41b487f890656143a2dd7135d74f" diff --git a/pyproject.toml b/pyproject.toml index 1288c13..86da289 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "opa-python-client" -version = "1.3.7" +version = "2.0.0" description = "Client for connection to the OPA service" authors = ["Tural Muradov "] license = "MIT" @@ -23,24 +23,23 @@ packages = [ [tool.poetry.dependencies] python = "^3.9" requests = "^2.32.3" -urllib3 = "^2.2.2" -certifi = "^2024.7.4" -user-agent = "^0.1.10" +aiohttp = {extras = ["speedups"], version = "^3.10.9"} +aiofiles = "^24.1.0" -[tool.poetry.dev-dependencies] -isort = "^5.10.1" -flake8 = "^4.0.1" -mypy = "^0.961" -blue = "^0.9.0" -pytest = "^7.1.2" +[tool.poetry.group.dev.dependencies] +pytest = "^8.3.3" +pytest-asyncio = "^0.24.0" +ruff = "^0.6.9" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" -[tool.isort] -profile = "black" -line_length = 100 -[tool.blue] -line-length = 100 +[tool.ruff] +line-length = 79 +exclude = [".venv","tests","migrations"] + +[tool.ruff.format] +indent-style = "tab" +docstring-code-format = true