├── .github └── workflows │ ├── poetry-publish.yml │ └── test-package.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENCE.md ├── README.md ├── opa_client ├── __init__.py ├── base.py ├── errors.py ├── opa.py ├── opa_async.py └── test │ ├── __init__.py │ ├── test_async_client.py │ ├── test_integaration_opa.py │ ├── test_opa.py │ └── test_opa_client.py ├── poetry.lock └── pyproject.toml /.github/workflows/poetry-publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload OPA-python-client 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-python@v1 13 | with: 14 | python-version: '3.8' 15 | architecture: x64 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install poetry 20 | - name: Build and publish 21 | run: | 22 | poetry version $(git describe --tags --abbrev=0) 23 | poetry build 24 | poetry publish --username ${{ secrets.PYPI_USERNAME }} --password ${{ secrets.PYPI_PASSWORD }} 25 | -------------------------------------------------------------------------------- /.github/workflows/test-package.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Turall/OPA-python-client/1cf1464bca03e1f306d3329029f17be9b44feece/.github/workflows/test-package.yml -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ### JetBrains template 3 | .idea/ 4 | 5 | ### Python template 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | testopa.py 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | .static_storage/ 61 | .media/ 62 | local_settings.py 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | .pytest_cache/ 111 | 112 | # VSCode 113 | .vscode/ 114 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to OPA-python-client 2 | 3 | We welcome contributions to [OPA-python-client](https://github.com/Turall/OPA-python-client) 4 | 5 | ## Issues 6 | 7 | Feel free to submit issues and enhancement requests. 8 | 9 | [OPA-python-client Issues](https://github.com/Turall/OPA-python-client/issues) 10 | 11 | ## Contributing 12 | 13 | Please refer to each project's style and contribution guidelines for submitting patches and additions. In general, we follow the "fork-and-pull" Git workflow. 14 | 15 | 1. **Fork** the repo on GitHub 16 | 2. **Clone** the project to your own machine 17 | 3. **Commit** changes to your own branch 18 | 4. **Push** your work 19 | 5. Submit a **Pull request** so that we can review your changes 20 | 21 | 22 | ## Testing 23 | ```sh 24 | $ docker run -it --rm -p 8181:8181 openpolicyagent/opa run --server --addr :8181 25 | $ pytest 26 | ``` 27 | 28 | NOTE: Be sure to merge the latest from "upstream" before making a pull request! 29 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tural Muradov Mohubbet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # OpaClient - Open Policy Agent Python Client 3 | [![MIT licensed](https://img.shields.io/github/license/Turall/OPA-python-client)](https://raw.githubusercontent.com/Turall/OPA-python-client/master/LICENSE) 4 | [![GitHub stars](https://img.shields.io/github/stars/Turall/OPA-python-client.svg)](https://github.com/Turall/OPA-python-client/stargazers) 5 | [![GitHub forks](https://img.shields.io/github/forks/Turall/OPA-python-client.svg)](https://github.com/Turall/OPA-python-client/network) 6 | [![GitHub issues](https://img.shields.io/github/issues-raw/Turall/OPA-python-client)](https://github.com/Turall/OPA-python-client/issues) 7 | [![Downloads](https://pepy.tech/badge/opa-python-client)](https://pepy.tech/project/opa-python-client) 8 | 9 | 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. 10 | 11 | ## Features 12 | 13 | - **Manage Policies**: Create, update, retrieve, and delete policies. 14 | - **Manage Data**: Create, update, retrieve, and delete data in OPA. 15 | - **Evaluate Policies**: Use input data to evaluate policies and return decisions. 16 | - **Synchronous & Asynchronous**: Choose between sync or async operations to suit your application. 17 | - **SSL/TLS Support**: Communicate securely with SSL/TLS, including client certificates. 18 | - **Customizable**: Use custom headers, timeouts, and other configurations. 19 | 20 | ## Installation 21 | 22 | You can install the OpaClient package via `pip`: 23 | 24 | ```bash 25 | pip install opa-python-client 26 | ``` 27 | 28 | ## Quick Start 29 | 30 | ### Synchronous Client Example 31 | 32 | ```python 33 | from opa_client.opa import OpaClient 34 | 35 | # Initialize the OPA client 36 | client = OpaClient(host='localhost', port=8181) 37 | 38 | # Check the OPA server connection 39 | try: 40 | print(client.check_connection()) # True 41 | finally: 42 | client.close_connection() 43 | ``` 44 | or with client factory 45 | 46 | ```python 47 | from opa_client import create_opa_client 48 | 49 | client = create_opa_client(host="localhost", port=8181) 50 | 51 | ``` 52 | 53 | Check OPA healthy. If you want check bundels or plugins, add query params for this. 54 | 55 | ```python 56 | from opa_client.opa import OpaClient 57 | 58 | client = OpaClient() 59 | 60 | print(client.check_health()) # response is True or False 61 | print(client.check_health({"bundle": True})) # response is True or False 62 | # If your diagnostic url different than default url, you can provide it. 63 | print(client.check_health(diagnostic_url="http://localhost:8282/health")) # response is True or False 64 | print(client.check_health(query={"bundle": True}, diagnostic_url="http://localhost:8282/health")) # response is True or False 65 | ``` 66 | 67 | ### Asynchronous Client Example 68 | 69 | ```python 70 | import asyncio 71 | from opa_client.opa_async import AsyncOpaClient 72 | 73 | async def main(): 74 | async with AsyncOpaClient(host='localhost', port=8181) as client: 75 | result = await client.check_connection() 76 | print(result) 77 | 78 | # Run the async main function 79 | asyncio.run(main()) 80 | ``` 81 | or with clien factory 82 | 83 | ```python 84 | from opa_client import create_opa_client 85 | 86 | client = create_opa_client(async_mode=True,host="localhost", port=8181) 87 | 88 | ``` 89 | 90 | ## Secure Connection with SSL/TLS 91 | 92 | You can use OpaClient with secure SSL/TLS connections, including mutual TLS (mTLS), by providing a client certificate and key. 93 | 94 | ### Synchronous Client with SSL/TLS 95 | 96 | ```python 97 | from opa_client.opa import OpaClient 98 | 99 | # Path to your certificate and private key 100 | cert_path = '/path/to/client_cert.pem' 101 | key_path = '/path/to/client_key.pem' 102 | 103 | # Initialize the OPA client with SSL/TLS 104 | client = OpaClient( 105 | host='your-opa-server.com', 106 | port=443, # Typically for HTTPS 107 | ssl=True, 108 | cert=(cert_path, key_path) # Provide the certificate and key as a tuple 109 | ) 110 | 111 | # Check the OPA server connection 112 | try: 113 | result = client.check_connection() 114 | print(result) 115 | finally: 116 | client.close_connection() 117 | ``` 118 | 119 | ### Asynchronous Client with SSL/TLS 120 | 121 | ```python 122 | import asyncio 123 | from opa_client.opa_async import AsyncOpaClient 124 | 125 | # Path to your certificate and private key 126 | cert_path = '/path/to/client_cert.pem' 127 | key_path = '/path/to/client_key.pem' 128 | 129 | async def main(): 130 | # Initialize the OPA client with SSL/TLS 131 | async with AsyncOpaClient( 132 | host='your-opa-server.com', 133 | port=443, # Typically for HTTPS 134 | ssl=True, 135 | cert=(cert_path, key_path) # Provide the certificate and key as a tuple 136 | ) as client: 137 | # Check the OPA server connection 138 | result = await client.check_connection() 139 | print(result) 140 | 141 | # Run the async main function 142 | asyncio.run(main()) 143 | ``` 144 | 145 | ## Usage 146 | 147 | ### Policy Management 148 | 149 | #### Create or Update a Policy 150 | 151 | You can create or update a policy using the following syntax: 152 | 153 | - **Synchronous**: 154 | 155 | ```python 156 | policy_name = 'example_policy' 157 | policy_content = ''' 158 | package example 159 | 160 | default allow = false 161 | 162 | allow { 163 | input.user == "admin" 164 | } 165 | ''' 166 | 167 | client.update_policy_from_string(policy_content, policy_name) 168 | ``` 169 | 170 | - **Asynchronous**: 171 | 172 | ```python 173 | await client.update_policy_from_string(policy_content, policy_name) 174 | ``` 175 | 176 | Or from url: 177 | 178 | - **Synchronous**: 179 | 180 | ```python 181 | policy_name = 'example_policy' 182 | 183 | client.update_policy_from_url("http://opapolicyurlexample.test/example.rego", policy_name) 184 | 185 | ``` 186 | 187 | - **Asynchronous**: 188 | 189 | ```python 190 | await client.update_policy_from_url("http://opapolicyurlexample.test/example.rego", policy_name) 191 | ``` 192 | 193 | Update policy from rego file 194 | 195 | ```python 196 | client.update_opa_policy_fromfile("/your/path/filename.rego", endpoint="fromfile") # response is True 197 | 198 | client.get_policies_list() 199 | ``` 200 | 201 | - **Asynchronous**: 202 | ```python 203 | await client.update_opa_policy_fromfile("/your/path/filename.rego", endpoint="fromfile") # response is True 204 | 205 | await client.get_policies_list() 206 | ``` 207 | 208 | #### Retrieve a Policy 209 | 210 | After creating a policy, you can retrieve it: 211 | 212 | - **Synchronous**: 213 | 214 | ```python 215 | policy = client.get_policy('example_policy') 216 | print(policy) 217 | # or 218 | policies = client.get_policies_list() 219 | print(policies) 220 | ``` 221 | 222 | - **Asynchronous**: 223 | 224 | ```python 225 | policy = await client.get_policy('example_policy') 226 | print(policy) 227 | 228 | # or 229 | policies = await client.get_policies_list() 230 | print(policies) 231 | ``` 232 | 233 | Save policy to file from OPA service 234 | 235 | ```python 236 | client.policy_to_file(policy_name="example_policy",path="/your/path",filename="example.rego") 237 | 238 | ``` 239 | 240 | - **Asynchronous**: 241 | 242 | ```python 243 | 244 | await client.policy_to_file(policy_name="example_policy",path="/your/path",filename="example.rego") 245 | 246 | ``` 247 | 248 | Information about policy path and rules 249 | 250 | ```python 251 | 252 | print(client.get_policies_info()) 253 | #{'example_policy': {'path': 'http://localhost:8181/v1/data/example', 'rules': ['http://localhost:8181/v1/data/example/allow']}} 254 | 255 | ``` 256 | - **Asynchronous**: 257 | 258 | 259 | ```python 260 | 261 | print(await client.get_policies_info()) 262 | #{'example_policy': {'path': 'http://localhost:8181/v1/data/example', 'rules': ['http://localhost:8181/v1/data/example/allow']}} 263 | 264 | ``` 265 | 266 | #### Delete a Policy 267 | 268 | You can delete a policy by name: 269 | 270 | - **Synchronous**: 271 | 272 | ```python 273 | client.delete_policy('example_policy') 274 | ``` 275 | 276 | - **Asynchronous**: 277 | 278 | ```python 279 | await client.delete_policy('example_policy') 280 | ``` 281 | 282 | ### Data Management 283 | 284 | #### Create or Update Data 285 | 286 | You can upload arbitrary data to OPA: 287 | 288 | - **Synchronous**: 289 | 290 | ```python 291 | data_name = 'users' 292 | data_content = { 293 | "users": [ 294 | {"name": "alice", "role": "admin"}, 295 | {"name": "bob", "role": "user"} 296 | ] 297 | } 298 | 299 | client.update_or_create_data(data_content, data_name) 300 | ``` 301 | 302 | - **Asynchronous**: 303 | 304 | ```python 305 | await client.update_or_create_data(data_content, data_name) 306 | ``` 307 | 308 | #### Retrieve Data 309 | 310 | You can fetch the data stored in OPA: 311 | 312 | - **Synchronous**: 313 | 314 | ```python 315 | data = client.get_data('users') 316 | print(data) 317 | # You can use query params for additional info 318 | # provenance - If parameter is true, response will include build/version info in addition to the result. 319 | # metrics - Return query performance metrics in addition to result 320 | data = client.get_data('users',query_params={"provenance": True}) 321 | 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'}]}} 322 | 323 | 324 | data = client.get_data('users',query_params={"metrics": True}) 325 | 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'}]}} 326 | 327 | 328 | ``` 329 | 330 | - **Asynchronous**: 331 | 332 | ```python 333 | data = await client.get_data('users') 334 | print(data) 335 | ``` 336 | 337 | #### Delete Data 338 | 339 | To delete data from OPA: 340 | 341 | - **Synchronous**: 342 | 343 | ```python 344 | client.delete_data('users') 345 | ``` 346 | 347 | - **Asynchronous**: 348 | 349 | ```python 350 | await client.delete_data('users') 351 | ``` 352 | 353 | ### Policy Evaluation 354 | 355 | #### Check Permission (Policy Evaluation) 356 | 357 | You can evaluate policies with input data using `check_permission`. 358 | 359 | - **Synchronous**: 360 | 361 | ```python 362 | input_data = {"user": "admin"} 363 | policy_name = 'example_policy' 364 | rule_name = 'allow' 365 | 366 | result = client.check_permission(input_data, policy_name, rule_name) 367 | print(result) 368 | ``` 369 | 370 | - **Asynchronous**: 371 | 372 | ```python 373 | input_data = {"user": "admin"} 374 | policy_name = 'example_policy' 375 | rule_name = 'allow' 376 | 377 | result = await client.check_permission(input_data, policy_name, rule_name) 378 | print(result) 379 | ``` 380 | 381 | Queries a package rule with the given input data 382 | 383 | ```python 384 | 385 | rego = """ 386 | package play 387 | 388 | default hello = false 389 | 390 | hello { 391 | m := input.message 392 | m == "world" 393 | } 394 | """ 395 | 396 | check_data = {"message": "world"} 397 | 398 | client.update_policy_from_string(rego, "test") 399 | print(client.query_rule(input_data=check_data, package_path="play", rule_name="hello")) # {'result': True} 400 | 401 | ``` 402 | 403 | - **Asynchronous**: 404 | 405 | ```python 406 | 407 | rego = """ 408 | package play 409 | 410 | default hello = false 411 | 412 | hello { 413 | m := input.message 414 | m == "world" 415 | } 416 | """ 417 | 418 | check_data = {"message": "world"} 419 | 420 | await client.update_policy_from_string(rego, "test") 421 | print(await client.query_rule(input_data=check_data, package_path="play", rule_name="hello")) # {'result': True} 422 | 423 | ``` 424 | 425 | ### Ad-hoc Queries 426 | 427 | Execute ad-hoc queries directly: 428 | 429 | - **Synchronous**: 430 | 431 | ```python 432 | data = { 433 | "user_roles": { 434 | "alice": [ 435 | "admin" 436 | ], 437 | "bob": [ 438 | "employee", 439 | "billing" 440 | ], 441 | "eve": [ 442 | "customer" 443 | ] 444 | } 445 | } 446 | input_data = {"user": "admin"} 447 | client.update_or_create_data(data, "userinfo") 448 | 449 | result = client.ad_hoc_query(query="data.userinfo.user_roles[name]") 450 | print(result) # {'result': [{'name': 'alice'}, {'name': 'bob'}, {'name': 'eve'}]} 451 | ``` 452 | 453 | - **Asynchronous**: 454 | 455 | ```python 456 | data = { 457 | "user_roles": { 458 | "alice": [ 459 | "admin" 460 | ], 461 | "bob": [ 462 | "employee", 463 | "billing" 464 | ], 465 | "eve": [ 466 | "customer" 467 | ] 468 | } 469 | } 470 | input_data = {"user": "admin"} 471 | await client.update_or_create_data(data, "userinfo") 472 | 473 | result = await client.ad_hoc_query(query="data.userinfo.user_roles[name]") 474 | print(result) # {'result': [{'name': 'alice'}, {'name': 'bob'}, {'name': 'eve'}]} 475 | ``` 476 | 477 | ## API Reference 478 | 479 | ### Synchronous Client (OpaClient) 480 | 481 | - `check_connection()`: Verify connection to OPA server. 482 | - `get_policies_list()`: Get a list of all policies. 483 | - `get_policies_info()`: Returns information about each policy, including policy path and policy rules. 484 | - `get_policy(policy_name)`: Fetch a specific policy. 485 | - `policy_to_file(policy_name)`: Save an OPA policy to a file.. 486 | - `update_policy_from_string(policy_content, policy_name)`: Upload or update a policy using its string content. 487 | - `update_policy_from_url(url,endpoint)`: Update OPA policy by fetching it from a URL. 488 | - `update_policy_from_file(filepath,endpoint)`: Update OPA policy using a policy file. 489 | - `delete_policy(policy_name)`: Delete a specific policy. 490 | - `update_or_create_data(data_content, data_name)`: Create or update data in OPA. 491 | - `get_data(data_name)`: Retrieve data from OPA. 492 | - `delete_data(data_name)`: Delete data from OPA. 493 | - `check_permission(input_data, policy_name, rule_name)`: Evaluate a policy using input data. 494 | - `query_rule(input_data, package_path, rule_name)`: Query a specific rule in a package. 495 | - `ad_hoc_query(query, input_data)`: Run an ad-hoc query. 496 | 497 | ### Asynchronous Client (AsyncOpaClient) 498 | 499 | Same as the synchronous client, but all methods are asynchronous and must be awaited. 500 | 501 | ## Contributing 502 | 503 | Contributions are welcome! Feel free to open issues, fork the repo, and submit pull requests. 504 | 505 | ## License 506 | 507 | This project is licensed under the MIT License. 508 | -------------------------------------------------------------------------------- /opa_client/__init__.py: -------------------------------------------------------------------------------- 1 | """Initialize the OpaClient package.""" 2 | 3 | from .opa import OpaClient 4 | from .opa_async import AsyncOpaClient 5 | 6 | 7 | def create_opa_client(async_mode=False, *args, **kwargs): 8 | if async_mode: 9 | return AsyncOpaClient(*args, **kwargs) 10 | else: 11 | return OpaClient(*args, **kwargs) 12 | 13 | 14 | __version__ = "2.0.2" 15 | __author__ = "Tural Muradov" 16 | __license__ = "MIT" 17 | 18 | __all__ = ["OpaClient", "create_opa_client", "AsyncOpaClient"] 19 | -------------------------------------------------------------------------------- /opa_client/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Dict, Optional, Union 3 | from urllib.parse import urlencode 4 | 5 | from .errors import ( 6 | FileError, 7 | PathNotFoundError, 8 | ) 9 | 10 | 11 | class BaseClient: 12 | """ 13 | Base class for OpaClient implementations. 14 | 15 | This class contains common logic shared between synchronous and asynchronous clients. 16 | """ 17 | 18 | def __init__( 19 | self, 20 | host: str = "localhost", 21 | port: int = 8181, 22 | version: str = "v1", 23 | ssl: bool = False, 24 | cert: Optional[Union[str, tuple]] = None, 25 | headers: Optional[dict] = None, 26 | timeout: float = 1.5, 27 | ): 28 | if not isinstance(port, int): 29 | raise TypeError("The port must be an integer") 30 | 31 | self.host = host.strip() 32 | self.port = port 33 | self.version = version 34 | self.ssl = ssl 35 | self.cert = cert 36 | self.timeout = timeout 37 | 38 | self.schema = "https://" if ssl else "http://" 39 | self.root_url = f"{self.schema}{self.host}:{self.port}/{self.version}" 40 | 41 | self.headers = headers 42 | 43 | self._session = None # Will be initialized in the subclass 44 | self.retries = 2 45 | self.timeout = 1.5 46 | 47 | def _build_url( 48 | self, path: str, query_params: Dict[str, str] = None 49 | ) -> str: 50 | url = f"{self.root_url}/{path.lstrip('/')}" 51 | if query_params: 52 | url = f"{url}?{urlencode(query_params)}" 53 | return url 54 | 55 | def _load_policy_from_file(self, filepath: str) -> str: 56 | if not os.path.isfile(filepath): 57 | raise FileError(f"'{filepath}' is not a valid file") 58 | with open(filepath, "r", encoding="utf-8") as file: 59 | return file.read() 60 | 61 | def _save_policy_to_file( 62 | self, policy_raw: str, path: Optional[str], filename: str 63 | ) -> bool: 64 | full_path = os.path.join(path or "", filename) 65 | try: 66 | with open(full_path, "w", encoding="utf-8") as file: 67 | file.write(policy_raw) 68 | return True 69 | except OSError as e: 70 | raise PathNotFoundError(f"Failed to write to '{full_path}'") from e 71 | 72 | # Abstract methods to be implemented in subclasses 73 | def close_connection(self): 74 | raise NotImplementedError 75 | 76 | def check_connection(self) -> str: 77 | raise NotImplementedError 78 | 79 | def _init_session(self): 80 | raise NotImplementedError 81 | 82 | def check_health( 83 | self, query: Dict[str, bool] = None, diagnostic_url: str = None 84 | ) -> bool: 85 | raise NotImplementedError 86 | 87 | def get_policies_list(self) -> list: 88 | raise NotImplementedError 89 | 90 | def get_policies_info(self) -> dict: 91 | raise NotImplementedError 92 | 93 | def update_policy_from_string( 94 | self, new_policy: str, endpoint: str 95 | ) -> bool: 96 | raise NotImplementedError 97 | 98 | def update_policy_from_file(self, filepath: str, endpoint: str) -> bool: 99 | raise NotImplementedError 100 | 101 | def update_policy_from_url(self, url: str, endpoint: str) -> bool: 102 | raise NotImplementedError 103 | 104 | def update_or_create_data(self, new_data: dict, endpoint: str) -> bool: 105 | raise NotImplementedError 106 | 107 | def get_data( 108 | self, data_name: str = "", query_params: Dict[str, bool] = None 109 | ) -> dict: 110 | raise NotImplementedError 111 | 112 | def policy_to_file( 113 | self, 114 | policy_name: str, 115 | path: Optional[str] = None, 116 | filename: str = "opa_policy.rego", 117 | ) -> bool: 118 | raise NotImplementedError 119 | 120 | def get_policy(self, policy_name: str) -> dict: 121 | raise NotImplementedError 122 | 123 | def delete_policy(self, policy_name: str) -> bool: 124 | raise NotImplementedError 125 | 126 | def delete_data(self, data_name: str) -> bool: 127 | raise NotImplementedError 128 | 129 | def check_permission( 130 | self, 131 | input_data: dict, 132 | policy_name: str, 133 | rule_name: str, 134 | query_params: Dict[str, bool] = None, 135 | ) -> dict: 136 | raise NotImplementedError 137 | 138 | def query_rule( 139 | self, 140 | input_data: dict, 141 | package_path: str, 142 | rule_name: Optional[str] = None, 143 | ) -> dict: 144 | raise NotImplementedError 145 | 146 | def ad_hoc_query(self, query: str, input_data: dict = None) -> dict: 147 | raise NotImplementedError 148 | -------------------------------------------------------------------------------- /opa_client/errors.py: -------------------------------------------------------------------------------- 1 | class ConnectionsError(Exception): 2 | def __init__(self, expression, message): 3 | """ 4 | expression -- input expression in which the error occurred 5 | message -- explanation of the error 6 | """ 7 | self.expression = expression 8 | self.message = message 9 | 10 | 11 | class QueryExecuteError(Exception): 12 | def __init__(self, expression, message): 13 | """ 14 | expression -- input expression in which the error occurred 15 | message -- explanation of the error 16 | """ 17 | self.expression = expression 18 | self.message = message 19 | 20 | 21 | class PolicyNotFoundError(Exception): 22 | def __init__(self, expression, message): 23 | """ 24 | expression -- input expression in which the error occurred 25 | message -- explanation of the error 26 | """ 27 | self.expression = expression 28 | self.message = message 29 | 30 | 31 | class CheckPermissionError(Exception): 32 | def __init__(self, expression, message): 33 | """ 34 | expression -- input expression in which the error occurred 35 | message -- explanation of the error 36 | """ 37 | self.expression = expression 38 | self.message = message 39 | 40 | 41 | class DeleteDataError(Exception): 42 | def __init__(self, expression, message): 43 | """ 44 | expression -- input expression in which the error occurred 45 | message -- explanation of the error 46 | """ 47 | self.expression = expression 48 | self.message = message 49 | 50 | 51 | class DeletePolicyError(Exception): 52 | def __init__(self, expression, message): 53 | """ 54 | expression -- input expression in which the error occurred 55 | message -- explanation of the error 56 | """ 57 | self.expression = expression 58 | self.message = message 59 | 60 | 61 | class PathNotFoundError(Exception): 62 | def __init__(self, expression, message): 63 | """ 64 | expression -- input expression in which the error occurred 65 | message -- explanation of the error 66 | """ 67 | self.expression = expression 68 | self.message = message 69 | 70 | 71 | class RegoParseError(Exception): 72 | def __init__(self, expression, message): 73 | """ 74 | expression -- input expression in which the error occurred 75 | message -- explanation of the error 76 | """ 77 | self.expression = expression 78 | self.message = message 79 | 80 | 81 | class SSLError(Exception): 82 | def __init__(self, expression, message): 83 | """ 84 | expression -- input expression in which the error occurred 85 | message -- explanation of the error 86 | """ 87 | self.expression = expression 88 | self.message = message 89 | 90 | 91 | class FileError(ValueError): 92 | def __init__(self, expression, message): 93 | """ 94 | expression -- input expression in which the error occurred 95 | message -- explanation of the error 96 | """ 97 | self.expression = expression 98 | self.message = message 99 | 100 | 101 | class TypeException(TypeError): 102 | def __init__(self, expression): 103 | """ 104 | expression -- input expression in which the error occurred 105 | """ 106 | self.expression = expression 107 | -------------------------------------------------------------------------------- /opa_client/opa.py: -------------------------------------------------------------------------------- 1 | import os 2 | import threading 3 | from typing import Dict, Optional 4 | from urllib.parse import urlencode 5 | 6 | import requests 7 | from requests.adapters import HTTPAdapter 8 | from urllib3.util.retry import Retry 9 | 10 | from .base import BaseClient 11 | from .errors import ( 12 | CheckPermissionError, 13 | ConnectionsError, 14 | DeleteDataError, 15 | DeletePolicyError, 16 | FileError, 17 | PathNotFoundError, 18 | PolicyNotFoundError, 19 | RegoParseError, 20 | TypeException, 21 | ) 22 | 23 | 24 | class OpaClient(BaseClient): 25 | """ 26 | OpaClient client object to connect and manipulate OPA service. 27 | 28 | Parameters: 29 | host (str): Host to connect to OPA service, defaults to 'localhost'. 30 | port (int): Port to connect to OPA service, defaults to 8181. 31 | version (str): REST API version provided by OPA, defaults to 'v1'. 32 | ssl (bool): Verify SSL certificates for HTTPS requests, defaults to False. 33 | cert (Optional[str]): Path to client certificate for mutual TLS authentication. 34 | headers (Optional[dict]): Dictionary of headers to send, defaults to None. 35 | retries (int): Number of retries for failed requests, defaults to 2. 36 | timeout (float): Timeout for requests in seconds, defaults to 1.5. 37 | 38 | Example: 39 | client = OpaClient(host='opa.example.com', ssl=True, cert='/path/to/cert.pem') 40 | """ 41 | 42 | def __init__(self, *args, **kwargs): 43 | super().__init__(*args, **kwargs) 44 | self._lock = threading.Lock() 45 | self._session = self._init_session() 46 | 47 | def _init_session(self) -> requests.Session: 48 | session = requests.Session() 49 | if self.headers: 50 | session.headers.update(self.headers) 51 | 52 | # Configure retries 53 | retries = Retry( 54 | total=self.retries, 55 | backoff_factor=0.3, 56 | status_forcelist=(500, 502, 504), 57 | ) 58 | adapter = HTTPAdapter(max_retries=retries) 59 | 60 | session.mount("http://", adapter) 61 | session.mount("https://", adapter) 62 | 63 | if self.ssl: 64 | session.verify = self.cert 65 | 66 | return session 67 | 68 | def __enter__(self): 69 | return self 70 | 71 | def __exit__(self, exc_type, exc_value, traceback): 72 | self.close_connection() 73 | 74 | def close_connection(self): 75 | """Close the session and release any resources.""" 76 | with self._lock: 77 | self._session.close() 78 | 79 | def check_connection(self) -> str: 80 | """ 81 | Checks whether the established connection is configured properly. 82 | If not, raises a ConnectionsError. 83 | 84 | Returns: 85 | str: Confirmation message if the connection is successful. 86 | """ 87 | url = f"{self.root_url}/policies/" 88 | try: 89 | response = self._session.get(url, timeout=self.timeout) 90 | response.raise_for_status() 91 | return True 92 | except requests.exceptions.RequestException as e: 93 | raise ConnectionsError( 94 | "Service unreachable", "Check config and try again" 95 | ) from e 96 | 97 | def check_health( 98 | self, query: Dict[str, bool] = None, diagnostic_url: str = None 99 | ) -> bool: 100 | """ 101 | Check if OPA is healthy. 102 | 103 | Parameters: 104 | query (Dict[str, bool], optional): Query parameters for health check. 105 | diagnostic_url (str, optional): Custom diagnostic URL. 106 | 107 | Returns: 108 | bool: True if OPA is healthy, False otherwise. 109 | """ 110 | url = diagnostic_url or f"{self.schema}{self.host}:{self.port}/health" 111 | if query: 112 | url = f"{url}?{urlencode(query)}" 113 | try: 114 | response = self._session.get(url, timeout=self.timeout) 115 | return response.status_code == 200 116 | except requests.exceptions.RequestException: 117 | return False 118 | 119 | def get_policies_list(self) -> list: 120 | """Returns all OPA policies in the service.""" 121 | url = f"{self.root_url}/policies/" 122 | response = self._session.get(url, timeout=self.timeout) 123 | response.raise_for_status() 124 | policies = response.json().get("result", []) 125 | return [policy.get("id") for policy in policies if policy.get("id")] 126 | 127 | def get_policies_info(self) -> dict: 128 | """ 129 | Returns information about each policy, including 130 | policy path and policy rules. 131 | """ 132 | url = f"{self.root_url}/policies/" 133 | response = self._session.get(url, timeout=self.timeout) 134 | response.raise_for_status() 135 | policies = response.json().get("result", []) 136 | policies_info = {} 137 | 138 | for policy in policies: 139 | policy_id = policy.get("id") 140 | ast = policy.get("ast", {}) 141 | package_path = "/".join( 142 | [ 143 | p.get("value") 144 | for p in ast.get("package", {}).get("path", []) 145 | ] 146 | ) 147 | rules = list( 148 | set( 149 | rule.get("head", {}).get("name") 150 | for rule in ast.get("rules", []) 151 | ) 152 | ) 153 | policy_url = f"{self.root_url}/{package_path}" 154 | rules_urls = [f"{policy_url}/{rule}" for rule in rules if rule] 155 | policies_info[policy_id] = { 156 | "path": policy_url, 157 | "rules": rules_urls, 158 | } 159 | 160 | return policies_info 161 | 162 | def update_policy_from_string( 163 | self, new_policy: str, endpoint: str 164 | ) -> bool: 165 | """ 166 | Update OPA policy using a policy string. 167 | 168 | Parameters: 169 | new_policy (str): The new policy in Rego language. 170 | endpoint (str): The policy endpoint in OPA. 171 | 172 | Returns: 173 | bool: True if the policy was successfully updated. 174 | """ 175 | if not new_policy or not isinstance(new_policy, str): 176 | raise TypeException("new_policy must be a non-empty string") 177 | 178 | url = f"{self.root_url}/policies/{endpoint}" 179 | response = self._session.put( 180 | url, 181 | data=new_policy.encode("utf-8"), 182 | headers={"Content-Type": "text/plain"}, 183 | timeout=self.timeout, 184 | ) 185 | 186 | if response.status_code == 200: 187 | return True 188 | else: 189 | error = response.json() 190 | raise RegoParseError(error.get("code"), error.get("message")) 191 | 192 | def update_policy_from_file(self, filepath: str, endpoint: str) -> bool: 193 | """ 194 | Update OPA policy using a policy file. 195 | 196 | Parameters: 197 | filepath (str): Path to the policy file. 198 | endpoint (str): The policy endpoint in OPA. 199 | 200 | Returns: 201 | bool: True if the policy was successfully updated. 202 | """ 203 | if not os.path.isfile(filepath): 204 | raise FileError("file_not_found",f"'{filepath}' is not a valid file") 205 | 206 | with open(filepath, "r", encoding="utf-8") as file: 207 | policy_str = file.read() 208 | 209 | return self.update_policy_from_string(policy_str, endpoint) 210 | 211 | def update_policy_from_url(self, url: str, endpoint: str) -> bool: 212 | """ 213 | Update OPA policy by fetching it from a URL. 214 | 215 | Parameters: 216 | url (str): URL to fetch the policy from. 217 | endpoint (str): The policy endpoint in OPA. 218 | 219 | Returns: 220 | bool: True if the policy was successfully updated. 221 | """ 222 | response = requests.get(url) 223 | response.raise_for_status() 224 | policy_str = response.text 225 | return self.update_policy_from_string(policy_str, endpoint) 226 | 227 | def update_or_create_data(self, new_data: dict, endpoint: str) -> bool: 228 | """ 229 | Update or create OPA data. 230 | 231 | Parameters: 232 | new_data (dict): The data to be updated or created. 233 | endpoint (str): The data endpoint in OPA. 234 | 235 | Returns: 236 | bool: True if the data was successfully updated or created. 237 | """ 238 | if not isinstance(new_data, dict): 239 | raise TypeException("new_data must be a dictionary") 240 | 241 | url = f"{self.root_url}/data/{endpoint}" 242 | response = self._session.put( 243 | url, 244 | json=new_data, 245 | headers={"Content-Type": "application/json"}, 246 | timeout=self.timeout, 247 | ) 248 | 249 | if response.status_code == 204: 250 | return True 251 | else: 252 | error = response.json() 253 | raise RegoParseError(error.get("code"), error.get("message")) 254 | 255 | def get_data( 256 | self, data_name: str = "", query_params: Dict[str, bool] = None 257 | ) -> dict: 258 | """ 259 | Get OPA data. 260 | 261 | Parameters: 262 | data_name (str, optional): The name of the data to retrieve. 263 | query_params (Dict[str, bool], optional): Query parameters. 264 | 265 | Returns: 266 | dict: The retrieved data. 267 | """ 268 | url = f"{self.root_url}/data/{data_name}" 269 | if query_params: 270 | url = f"{url}?{urlencode(query_params)}" 271 | response = self._session.get(url, timeout=self.timeout) 272 | if response.status_code == 200 and response.json().get("result"): 273 | return response.json() 274 | else: 275 | error = response.json() 276 | raise PolicyNotFoundError( 277 | "PolicyNotFoundError", 278 | error.get("message", "requested data not found"), 279 | ) 280 | 281 | def policy_to_file( 282 | self, 283 | policy_name: str, 284 | path: Optional[str] = None, 285 | filename: str = "opa_policy.rego", 286 | ) -> bool: 287 | """ 288 | Save an OPA policy to a file. 289 | 290 | Parameters: 291 | policy_name (str): The name of the policy. 292 | path (Optional[str]): The directory path to save the file. 293 | filename (str): The name of the file. 294 | 295 | Returns: 296 | bool: True if the policy was successfully saved. 297 | """ 298 | policy = self.get_policy(policy_name) 299 | policy_raw = policy.get("result", {}).get("raw", "") 300 | 301 | if not policy_raw: 302 | raise PolicyNotFoundError("resource_not_found", "Policy content is empty") 303 | 304 | full_path = os.path.join(path or "", filename) 305 | 306 | try: 307 | with open(full_path, "w", encoding="utf-8") as file: 308 | file.write(policy_raw) 309 | return True 310 | except OSError as e: 311 | raise PathNotFoundError("path_not_found", f"Failed to write to '{full_path}'") from e 312 | 313 | def get_policy(self, policy_name: str) -> dict: 314 | """ 315 | Get a specific OPA policy. 316 | 317 | Parameters: 318 | policy_name (str): The name of the policy. 319 | 320 | Returns: 321 | dict: The policy data. 322 | """ 323 | url = f"{self.root_url}/policies/{policy_name}" 324 | response = self._session.get(url, timeout=self.timeout) 325 | if response.status_code == 200: 326 | return response.json() 327 | else: 328 | error = response.json() 329 | raise PolicyNotFoundError(error.get("code"), error.get("message")) 330 | 331 | def delete_policy(self, policy_name: str) -> bool: 332 | """ 333 | Delete an OPA policy. 334 | 335 | Parameters: 336 | policy_name (str): The name of the policy. 337 | 338 | Returns: 339 | bool: True if the policy was successfully deleted. 340 | """ 341 | url = f"{self.root_url}/policies/{policy_name}" 342 | response = self._session.delete(url, timeout=self.timeout) 343 | if response.status_code == 200: 344 | return True 345 | else: 346 | error = response.json() 347 | raise DeletePolicyError(error.get("code"), error.get("message")) 348 | 349 | def delete_data(self, data_name: str) -> bool: 350 | """ 351 | Delete OPA data. 352 | 353 | Parameters: 354 | data_name (str): The name of the data. 355 | 356 | Returns: 357 | bool: True if the data was successfully deleted. 358 | """ 359 | url = f"{self.root_url}/data/{data_name}" 360 | response = self._session.delete(url, timeout=self.timeout) 361 | if response.status_code == 204: 362 | return True 363 | else: 364 | error = response.json() 365 | raise DeleteDataError(error.get("code"), error.get("message")) 366 | 367 | def check_permission( 368 | self, 369 | input_data: dict, 370 | policy_name: str, 371 | rule_name: str, 372 | query_params: Dict[str, bool] = None, 373 | ) -> dict: 374 | """ 375 | Check permissions based on input data, policy name, and rule name. 376 | 377 | Parameters: 378 | input_data (dict): The input data to check against the policy. 379 | policy_name (str): The name of the policy. 380 | rule_name (str): The name of the rule in the policy. 381 | query_params (Dict[str, bool], optional): Query parameters. 382 | 383 | Returns: 384 | dict: The result of the permission check. 385 | """ 386 | policy = self.get_policy(policy_name) 387 | ast = policy.get("result", {}).get("ast", {}) 388 | package_path = "/".join( 389 | [p.get("value") for p in ast.get("package", {}).get("path", [])] 390 | ) 391 | rules = [ 392 | rule.get("head", {}).get("name") for rule in ast.get("rules", []) 393 | ] 394 | 395 | if rule_name not in rules: 396 | raise CheckPermissionError( 397 | "resource_not_found", f"Rule '{rule_name}' not found in policy '{policy_name}'" 398 | ) 399 | 400 | url = f"{self.root_url}/{package_path}/{rule_name}" 401 | if query_params: 402 | url = f"{url}?{urlencode(query_params)}" 403 | response = self._session.post( 404 | url, json={"input": input_data}, timeout=self.timeout 405 | ) 406 | response.raise_for_status() 407 | return response.json() 408 | 409 | def query_rule( 410 | self, 411 | input_data: dict, 412 | package_path: str, 413 | rule_name: Optional[str] = None, 414 | ) -> dict: 415 | """ 416 | Query a specific rule in a package. 417 | 418 | Parameters: 419 | input_data (dict): The input data for the query. 420 | package_path (str): The package path. 421 | rule_name (Optional[str]): The rule name. 422 | 423 | Returns: 424 | dict: The result of the query. 425 | """ 426 | path = package_path.replace(".", "/") 427 | if rule_name: 428 | path = f"{path}/{rule_name}" 429 | url = f"{self.root_url}/data/{path}" 430 | 431 | response = self._session.post( 432 | url, json={"input": input_data}, timeout=self.timeout 433 | ) 434 | response.raise_for_status() 435 | return response.json() 436 | 437 | def ad_hoc_query(self, query: str, input_data: dict = None) -> dict: 438 | """ 439 | Execute an ad-hoc query. 440 | 441 | Parameters: 442 | query (str): The query string. 443 | input_data (dict, optional): The input data for the query. 444 | 445 | Returns: 446 | dict: The result of the query. 447 | """ 448 | url = f"{self.schema}{self.host}:{self.port}/v1/query" 449 | params = {"q": query} 450 | payload = {"input": input_data} if input_data else None 451 | 452 | response = self._session.get( 453 | url, params=params, json=payload, timeout=self.timeout 454 | ) 455 | response.raise_for_status() 456 | return response.json() 457 | 458 | 459 | # Example usage: 460 | if __name__ == "__main__": 461 | client = OpaClient() 462 | try: 463 | print(client.check_connection()) 464 | finally: 465 | client.close_connection() 466 | -------------------------------------------------------------------------------- /opa_client/opa_async.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import os 4 | import ssl 5 | from typing import Dict, Optional, Union 6 | from urllib.parse import urlencode 7 | 8 | import aiofiles 9 | import aiohttp 10 | from aiohttp import ClientSession, TCPConnector 11 | 12 | from .errors import ( 13 | CheckPermissionError, 14 | ConnectionsError, 15 | DeleteDataError, 16 | DeletePolicyError, 17 | FileError, 18 | PathNotFoundError, 19 | PolicyNotFoundError, 20 | RegoParseError, 21 | TypeException, 22 | ) 23 | 24 | 25 | class AsyncOpaClient: 26 | """ 27 | AsyncOpaClient client object to connect and manipulate OPA service asynchronously. 28 | 29 | Parameters: 30 | host (str): Host to connect to OPA service, defaults to 'localhost'. 31 | port (int): Port to connect to OPA service, defaults to 8181. 32 | version (str): REST API version provided by OPA, defaults to 'v1'. 33 | ssl (bool): Verify SSL certificates for HTTPS requests, defaults to False. 34 | cert (Optional[str] or Tuple[str, str]): Path to client certificate or a tuple of (cert_file, key_file). 35 | headers (Optional[dict]): Dictionary of headers to send, defaults to None. 36 | timeout (float): Timeout for requests in seconds, defaults to 1.5. 37 | 38 | Example: 39 | async with AsyncOpaClient(host='opa.example.com', ssl=True, cert='/path/to/cert.pem') as client: 40 | await client.check_connection() 41 | """ 42 | 43 | def __init__( 44 | self, 45 | host: str = "localhost", 46 | port: int = 8181, 47 | version: str = "v1", 48 | ssl: bool = False, 49 | cert: Optional[Union[str, tuple]] = None, 50 | headers: Optional[dict] = None, 51 | timeout: float = 1.5, 52 | ): 53 | if not isinstance(port, int): 54 | raise TypeError("The port must be an integer") 55 | 56 | self.host = host.strip() 57 | self.port = port 58 | self.version = version 59 | self.ssl = ssl 60 | self.cert = cert 61 | self.timeout = timeout 62 | 63 | self.schema = "https://" if ssl else "http://" 64 | self.root_url = f"{self.schema}{self.host}:{self.port}/{self.version}" 65 | 66 | self.headers = headers 67 | 68 | # Initialize the session attributes 69 | self._session: Optional[ClientSession] = None 70 | self._connector = None # Will be initialized in _init_session 71 | 72 | async def __aenter__(self): 73 | await self._init_session() 74 | return self 75 | 76 | async def __aexit__(self, exc_type, exc_value, traceback): 77 | await self.close_connection() 78 | 79 | async def _init_session(self): 80 | ssl_context = None 81 | 82 | if self.ssl: 83 | ssl_context = ssl.create_default_context() 84 | 85 | # If cert is provided, load the client certificate 86 | if self.cert: 87 | if isinstance(self.cert, tuple): 88 | # Tuple of (cert_file, key_file) 89 | ssl_context.load_cert_chain(*self.cert) 90 | else: 91 | # Single cert file (might contain both cert and key) 92 | ssl_context.load_cert_chain(self.cert) 93 | else: 94 | # Verify default CA certificates 95 | ssl_context.load_default_certs() 96 | 97 | self._connector = TCPConnector(ssl=ssl_context) 98 | 99 | self._session = aiohttp.ClientSession( 100 | headers=self.headers, 101 | connector=self._connector, 102 | timeout=aiohttp.ClientTimeout(total=self.timeout), 103 | ) 104 | 105 | async def close_connection(self): 106 | """Close the session and release any resources.""" 107 | if self._session and not self._session.closed: 108 | await self._session.close() 109 | self._session = None 110 | 111 | async def check_connection(self) -> str: 112 | """ 113 | Checks whether the established connection is configured properly. 114 | If not, raises a ConnectionsError. 115 | 116 | Returns: 117 | str: Confirmation message if the connection is successful. 118 | """ 119 | url = f"{self.root_url}/policies/" 120 | try: 121 | async with self._session.get(url) as response: 122 | if response.status == 200: 123 | return True 124 | else: 125 | raise ConnectionsError( 126 | "Service unreachable", "Check config and try again" 127 | ) 128 | except Exception as e: 129 | raise ConnectionsError( 130 | "Service unreachable", "Check config and try again" 131 | ) from e 132 | 133 | async def check_health( 134 | self, query: Dict[str, bool] = None, diagnostic_url: str = None 135 | ) -> bool: 136 | """ 137 | Check if OPA is healthy. 138 | 139 | Parameters: 140 | query (Dict[str, bool], optional): Query parameters for health check. 141 | diagnostic_url (str, optional): Custom diagnostic URL. 142 | 143 | Returns: 144 | bool: True if OPA is healthy, False otherwise. 145 | """ 146 | url = diagnostic_url or f"{self.schema}{self.host}:{self.port}/health" 147 | if query: 148 | url = f"{url}?{urlencode(query)}" 149 | try: 150 | async with self._session.get(url) as response: 151 | return response.status == 200 152 | except Exception: 153 | return False 154 | 155 | async def get_policies_list(self) -> list: 156 | """Returns all OPA policies in the service.""" 157 | url = f"{self.root_url}/policies/" 158 | async with self._session.get(url) as response: 159 | response.raise_for_status() 160 | policies = await response.json() 161 | result = policies.get("result", []) 162 | return [policy.get("id") for policy in result if policy.get("id")] 163 | 164 | async def get_policies_info(self) -> dict: 165 | """ 166 | Returns information about each policy, including 167 | policy path and policy rules. 168 | """ 169 | url = f"{self.root_url}/policies/" 170 | async with self._session.get(url) as response: 171 | response.raise_for_status() 172 | policies = await response.json() 173 | result = policies.get("result", []) 174 | policies_info = {} 175 | 176 | for policy in result: 177 | policy_id = policy.get("id") 178 | ast = policy.get("ast", {}) 179 | package_path = "/".join( 180 | [ 181 | p.get("value") 182 | for p in ast.get("package", {}).get("path", []) 183 | ] 184 | ) 185 | rules = list( 186 | set( 187 | rule.get("head", {}).get("name") 188 | for rule in ast.get("rules", []) 189 | ) 190 | ) 191 | policy_url = f"{self.root_url}/{package_path}" 192 | rules_urls = [f"{policy_url}/{rule}" for rule in rules if rule] 193 | policies_info[policy_id] = { 194 | "path": policy_url, 195 | "rules": rules_urls, 196 | } 197 | 198 | return policies_info 199 | 200 | async def update_policy_from_string( 201 | self, new_policy: str, endpoint: str 202 | ) -> bool: 203 | """ 204 | Update OPA policy using a policy string. 205 | 206 | Parameters: 207 | new_policy (str): The new policy in Rego language. 208 | endpoint (str): The policy endpoint in OPA. 209 | 210 | Returns: 211 | bool: True if the policy was successfully updated. 212 | """ 213 | if not new_policy or not isinstance(new_policy, str): 214 | raise TypeException("new_policy must be a non-empty string") 215 | 216 | url = f"{self.root_url}/policies/{endpoint}" 217 | headers = self.headers.copy() if self.headers else {} 218 | headers["Content-Type"] = "text/plain" 219 | async with self._session.put( 220 | url, data=new_policy.encode("utf-8"), headers=self.headers 221 | ) as response: 222 | if response.status == 200: 223 | return True 224 | else: 225 | error = await response.json() 226 | raise RegoParseError(error.get("code"), error.get("message")) 227 | 228 | async def update_policy_from_file( 229 | self, filepath: str, endpoint: str 230 | ) -> bool: 231 | """ 232 | Update OPA policy using a policy file. 233 | 234 | Parameters: 235 | filepath (str): Path to the policy file. 236 | endpoint (str): The policy endpoint in OPA. 237 | 238 | Returns: 239 | bool: True if the policy was successfully updated. 240 | """ 241 | if not os.path.isfile(filepath): 242 | raise FileError(f"'{filepath}' is not a valid file") 243 | 244 | async with aiofiles.open(filepath, "r", encoding="utf-8") as file: 245 | policy_str = await file.read() 246 | 247 | return await self.update_policy_from_string(policy_str, endpoint) 248 | 249 | async def update_policy_from_url(self, url: str, endpoint: str) -> bool: 250 | """ 251 | Update OPA policy by fetching it from a URL. 252 | 253 | Parameters: 254 | url (str): URL to fetch the policy from. 255 | endpoint (str): The policy endpoint in OPA. 256 | 257 | Returns: 258 | bool: True if the policy was successfully updated. 259 | """ 260 | async with aiohttp.ClientSession() as session: 261 | async with session.get(url) as response: 262 | response.raise_for_status() 263 | policy_str = await response.text() 264 | 265 | return await self.update_policy_from_string(policy_str, endpoint) 266 | 267 | async def update_or_create_data( 268 | self, new_data: dict, endpoint: str 269 | ) -> bool: 270 | """ 271 | Update or create OPA data. 272 | 273 | Parameters: 274 | new_data (dict): The data to be updated or created. 275 | endpoint (str): The data endpoint in OPA. 276 | 277 | Returns: 278 | bool: True if the data was successfully updated or created. 279 | """ 280 | if not isinstance(new_data, dict): 281 | raise TypeException("new_data must be a dictionary") 282 | 283 | url = f"{self.root_url}/data/{endpoint}" 284 | headers = self.headers.copy() if self.headers else {} 285 | headers["Content-Type"] = "application/json" 286 | async with self._session.put( 287 | url, json=new_data, headers=headers 288 | ) as response: 289 | if response.status == 204: 290 | return True 291 | else: 292 | error = await response.json() 293 | raise RegoParseError(error.get("code"), error.get("message")) 294 | 295 | async def get_data( 296 | self, data_name: str = "", query_params: Dict[str, bool] = None 297 | ) -> dict: 298 | """ 299 | Get OPA data. 300 | 301 | Parameters: 302 | data_name (str, optional): The name of the data to retrieve. 303 | query_params (Dict[str, bool], optional): Query parameters. 304 | 305 | Returns: 306 | dict: The retrieved data. 307 | """ 308 | url = f"{self.root_url}/data/{data_name}" 309 | if query_params: 310 | url = f"{url}?{urlencode(query_params)}" 311 | async with self._session.get(url) as response: 312 | if response.status == 200: 313 | return await response.json() 314 | else: 315 | error = await response.json() 316 | raise PolicyNotFoundError( 317 | error.get("code"), error.get("message") 318 | ) 319 | 320 | async def policy_to_file( 321 | self, 322 | policy_name: str, 323 | path: Optional[str] = None, 324 | filename: str = "opa_policy.rego", 325 | ) -> bool: 326 | """ 327 | Save an OPA policy to a file. 328 | 329 | Parameters: 330 | policy_name (str): The name of the policy. 331 | path (Optional[str]): The directory path to save the file. 332 | filename (str): The name of the file. 333 | 334 | Returns: 335 | bool: True if the policy was successfully saved. 336 | """ 337 | policy = await self.get_policy(policy_name) 338 | policy_raw = policy.get("result", {}).get("raw", "") 339 | 340 | if not policy_raw: 341 | raise PolicyNotFoundError("Policy content is empty") 342 | 343 | full_path = os.path.join(path or "", filename) 344 | 345 | try: 346 | async with aiofiles.open(full_path, "w", encoding="utf-8") as file: 347 | await file.write(policy_raw) 348 | return True 349 | except OSError as e: 350 | raise PathNotFoundError(f"Failed to write to '{full_path}'") from e 351 | 352 | async def get_policy(self, policy_name: str) -> dict: 353 | """ 354 | Get a specific OPA policy. 355 | 356 | Parameters: 357 | policy_name (str): The name of the policy. 358 | 359 | Returns: 360 | dict: The policy data. 361 | """ 362 | url = f"{self.root_url}/policies/{policy_name}" 363 | async with self._session.get(url) as response: 364 | if response.status == 200: 365 | return await response.json() 366 | else: 367 | error = await response.json() 368 | raise PolicyNotFoundError( 369 | error.get("code"), error.get("message") 370 | ) 371 | 372 | async def delete_policy(self, policy_name: str) -> bool: 373 | """ 374 | Delete an OPA policy. 375 | 376 | Parameters: 377 | policy_name (str): The name of the policy. 378 | 379 | Returns: 380 | bool: True if the policy was successfully deleted. 381 | """ 382 | url = f"{self.root_url}/policies/{policy_name}" 383 | async with self._session.delete(url) as response: 384 | if response.status == 200: 385 | return True 386 | else: 387 | error = await response.json() 388 | raise DeletePolicyError( 389 | error.get("code"), error.get("message") 390 | ) 391 | 392 | async def delete_data(self, data_name: str) -> bool: 393 | """ 394 | Delete OPA data. 395 | 396 | Parameters: 397 | data_name (str): The name of the data. 398 | 399 | Returns: 400 | bool: True if the data was successfully deleted. 401 | """ 402 | url = f"{self.root_url}/data/{data_name}" 403 | async with self._session.delete(url) as response: 404 | if response.status == 204: 405 | return True 406 | else: 407 | error = await response.json() 408 | raise DeleteDataError(error.get("code"), error.get("message")) 409 | 410 | async def check_permission( 411 | self, 412 | input_data: dict, 413 | policy_name: str, 414 | rule_name: str, 415 | query_params: Dict[str, bool] = None, 416 | ) -> dict: 417 | """ 418 | Check permissions based on input data, policy name, and rule name. 419 | 420 | Parameters: 421 | input_data (dict): The input data to check against the policy. 422 | policy_name (str): The name of the policy. 423 | rule_name (str): The name of the rule in the policy. 424 | query_params (Dict[str, bool], optional): Query parameters. 425 | 426 | Returns: 427 | dict: The result of the permission check. 428 | """ 429 | policy = await self.get_policy(policy_name) 430 | ast = policy.get("result", {}).get("ast", {}) 431 | package_path = "/".join( 432 | [p.get("value") for p in ast.get("package", {}).get("path", [])] 433 | ) 434 | rules = [ 435 | rule.get("head", {}).get("name") for rule in ast.get("rules", []) 436 | ] 437 | 438 | if rule_name not in rules: 439 | raise CheckPermissionError( 440 | f"Rule '{rule_name}' not found in policy '{policy_name}'" 441 | ) 442 | 443 | url = f"{self.root_url}/data/{package_path}/{rule_name}" 444 | if query_params: 445 | url = f"{url}?{urlencode(query_params)}" 446 | 447 | async with self._session.post( 448 | url, json={"input": input_data} 449 | ) as response: 450 | response.raise_for_status() 451 | return await response.json() 452 | 453 | async def query_rule( 454 | self, 455 | input_data: dict, 456 | package_path: str, 457 | rule_name: Optional[str] = None, 458 | ) -> dict: 459 | """ 460 | Query a specific rule in a package. 461 | 462 | Parameters: 463 | input_data (dict): The input data for the query. 464 | package_path (str): The package path. 465 | rule_name (Optional[str]): The rule name. 466 | 467 | Returns: 468 | dict: The result of the query. 469 | """ 470 | path = package_path.replace(".", "/") 471 | if rule_name: 472 | path = f"{path}/{rule_name}" 473 | url = f"{self.root_url}/data/{path}" 474 | 475 | async with self._session.post( 476 | url, json={"input": input_data} 477 | ) as response: 478 | response.raise_for_status() 479 | return await response.json() 480 | 481 | async def ad_hoc_query(self, query: str, input_data: dict = None) -> dict: 482 | """ 483 | Execute an ad-hoc query. 484 | 485 | Parameters: 486 | query (str): The query string. 487 | input_data (dict, optional): The input data for the query. 488 | 489 | Returns: 490 | dict: The result of the query. 491 | """ 492 | url = f"{self.schema}{self.host}:{self.port}/v1/query" 493 | params = {"q": query} 494 | payload = {"input": input_data} if input_data else None 495 | 496 | async with self._session.post( 497 | url, params=params, json=payload 498 | ) as response: 499 | response.raise_for_status() 500 | return await response.json() 501 | 502 | # Property methods for read-only access to certain attributes 503 | @property 504 | def host(self) -> str: 505 | return self._host 506 | 507 | @host.setter 508 | def host(self, value: str): 509 | self._host = value 510 | 511 | @property 512 | def port(self) -> int: 513 | return self._port 514 | 515 | @port.setter 516 | def port(self, value: int): 517 | if not isinstance(value, int): 518 | raise TypeError("Port must be an integer") 519 | self._port = value 520 | 521 | @property 522 | def version(self) -> str: 523 | return self._version 524 | 525 | @version.setter 526 | def version(self, value: str): 527 | self._version = value 528 | 529 | @property 530 | def schema(self) -> str: 531 | return self._schema 532 | 533 | @schema.setter 534 | def schema(self, value: str): 535 | self._schema = value 536 | 537 | @property 538 | def root_url(self) -> str: 539 | return self._root_url 540 | 541 | @root_url.setter 542 | def root_url(self, value: str): 543 | self._root_url = value 544 | 545 | @property 546 | def ssl(self) -> bool: 547 | return self._ssl 548 | 549 | @ssl.setter 550 | def ssl(self, value: bool): 551 | self._ssl = value 552 | 553 | @property 554 | def cert(self) -> Optional[str]: 555 | return self._cert 556 | 557 | @cert.setter 558 | def cert(self, value: Optional[str]): 559 | self._cert = value 560 | 561 | @property 562 | def headers(self) -> dict: 563 | return self._headers 564 | 565 | @headers.setter 566 | def headers(self, value: dict): 567 | self._headers = value 568 | 569 | @property 570 | def timeout(self) -> float: 571 | return self._timeout 572 | 573 | @timeout.setter 574 | def timeout(self, value: float): 575 | self._timeout = value 576 | 577 | 578 | # Example usage: 579 | async def main(): 580 | async with AsyncOpaClient() as client: 581 | try: 582 | result = await client.check_connection() 583 | print(result) 584 | finally: 585 | await client.close_connection() 586 | 587 | 588 | # Run the example 589 | if __name__ == "__main__": 590 | asyncio.run(main()) 591 | 592 | # Example usage: 593 | async def main(): 594 | async with AsyncOpaClient( 595 | host="localhost", 596 | port=8181, 597 | ssl=True, 598 | cert=("/path/to/cert.pem", "/path/to/key.pem"), 599 | ) as client: 600 | try: 601 | result = await client.check_connection() 602 | print(result) 603 | finally: 604 | await client.close_connection() 605 | -------------------------------------------------------------------------------- /opa_client/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Turall/OPA-python-client/1cf1464bca03e1f306d3329029f17be9b44feece/opa_client/test/__init__.py -------------------------------------------------------------------------------- /opa_client/test/test_async_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import unittest 3 | from unittest.mock import AsyncMock, Mock, patch 4 | 5 | from opa_client import create_opa_client 6 | from opa_client.errors import ( 7 | ConnectionsError, 8 | DeleteDataError, 9 | DeletePolicyError, 10 | PolicyNotFoundError, 11 | RegoParseError, 12 | ) 13 | 14 | 15 | class TestAsyncOpaClient(unittest.IsolatedAsyncioTestCase): 16 | async def asyncSetUp(self): 17 | self.client = create_opa_client( 18 | async_mode=True, host="localhost", port=8181 19 | ) 20 | await self.client._init_session() 21 | 22 | async def asyncTearDown(self): 23 | await self.client.close_connection() 24 | 25 | @patch("aiohttp.ClientSession.get") 26 | async def test_check_connection_success(self, mock_get): 27 | mock_response = AsyncMock() 28 | mock_response.status = 200 29 | mock_get.return_value.__aenter__.return_value = mock_response 30 | 31 | result = await self.client.check_connection() 32 | self.assertEqual(result, True) 33 | mock_get.assert_called_once() 34 | 35 | @patch("aiohttp.ClientSession.get") 36 | async def test_check_connection_failure(self, mock_get): 37 | mock_response = AsyncMock() 38 | mock_response.status = 500 39 | mock_get.return_value.__aenter__.return_value = mock_response 40 | 41 | with self.assertRaises(ConnectionsError): 42 | await self.client.check_connection() 43 | mock_get.assert_called_once() 44 | 45 | @patch("aiohttp.ClientSession.get") 46 | async def test_get_policies_list(self, mock_get): 47 | mock_response = AsyncMock() 48 | mock_response.status = 200 49 | mock_response.raise_for_status = Mock() 50 | mock_response.json = AsyncMock( 51 | return_value={"result": [{"id": "policy1"}, {"id": "policy2"}]} 52 | ) 53 | mock_get.return_value.__aenter__.return_value = mock_response 54 | 55 | policies = await self.client.get_policies_list() 56 | self.assertEqual(policies, ["policy1", "policy2"]) 57 | mock_get.assert_called_once() 58 | 59 | @patch("aiohttp.ClientSession.put") 60 | async def test_update_policy_from_string_success(self, mock_put): 61 | mock_response = AsyncMock() 62 | mock_response.status = 200 63 | mock_put.return_value.__aenter__.return_value = mock_response 64 | 65 | new_policy = "package example\n\ndefault allow = false" 66 | result = await self.client.update_policy_from_string( 67 | new_policy, "example" 68 | ) 69 | self.assertTrue(result) 70 | mock_put.assert_called_once() 71 | 72 | @patch("aiohttp.ClientSession.put") 73 | async def test_update_policy_from_string_failure(self, mock_put): 74 | mock_response = AsyncMock() 75 | mock_response.status = 400 76 | mock_response.json = AsyncMock( 77 | return_value={ 78 | "code": "invalid_parameter", 79 | "message": "Parse error", 80 | } 81 | ) 82 | mock_put.return_value.__aenter__.return_value = mock_response 83 | 84 | new_policy = "invalid policy" 85 | with self.assertRaises(Exception) as context: 86 | await self.client.update_policy_from_string(new_policy, "invalid") 87 | 88 | self.assertIsInstance(context.exception, RegoParseError) 89 | mock_put.assert_called_once() 90 | 91 | @patch("aiohttp.ClientSession.delete") 92 | async def test_delete_policy_success(self, mock_delete): 93 | mock_response = AsyncMock() 94 | mock_response.status = 200 95 | mock_delete.return_value.__aenter__.return_value = mock_response 96 | 97 | result = await self.client.delete_policy("policy1") 98 | self.assertTrue(result) 99 | mock_delete.assert_called_once() 100 | 101 | @patch("aiohttp.ClientSession.delete") 102 | async def test_delete_policy_failure(self, mock_delete): 103 | mock_response = AsyncMock() 104 | mock_response.status = 404 105 | mock_response.json = AsyncMock( 106 | return_value={"code": "not_found", "message": "Policy not found"} 107 | ) 108 | mock_delete.return_value.__aenter__.return_value = mock_response 109 | 110 | with self.assertRaises(DeletePolicyError): 111 | await self.client.delete_policy("nonexistent_policy") 112 | mock_delete.assert_called_once() 113 | 114 | # Add more test methods to cover other functionalities 115 | 116 | 117 | if __name__ == "__main__": 118 | unittest.main() 119 | -------------------------------------------------------------------------------- /opa_client/test/test_integaration_opa.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from opa_client.errors import ( 4 | ConnectionsError, 5 | DeleteDataError, 6 | DeletePolicyError, 7 | PolicyNotFoundError, 8 | RegoParseError, 9 | ) 10 | from opa_client.opa import OpaClient 11 | 12 | 13 | class TestIntegrationOpaClient(unittest.TestCase): 14 | @classmethod 15 | def setUpClass(cls): 16 | cls.client = OpaClient(host="localhost", port=8181) 17 | try: 18 | cls.client.check_connection() 19 | except ConnectionsError: 20 | raise RuntimeError( 21 | "OPA server is not running. Please start the OPA server before running tests." 22 | ) 23 | 24 | @classmethod 25 | def tearDownClass(cls): 26 | cls.client.close_connection() 27 | 28 | def test_check_connection(self): 29 | result = self.client.check_connection() 30 | self.assertEqual(result, True) 31 | 32 | def test_policy_lifecycle(self): 33 | # Define a sample policy 34 | policy_name = "example_policy" 35 | policy_content = """ 36 | package example 37 | 38 | default allow = false 39 | 40 | allow { 41 | input.user == "admin" 42 | } 43 | """ 44 | 45 | # Create/Update the policy 46 | result = self.client.update_policy_from_string( 47 | policy_content, policy_name 48 | ) 49 | self.assertTrue(result) 50 | 51 | # Retrieve the policy 52 | policy = self.client.get_policy(policy_name) 53 | self.assertIn("result", policy) 54 | self.assertIn("raw", policy["result"]) 55 | self.assertIn("package example", policy["result"]["raw"]) 56 | 57 | # Delete the policy 58 | result = self.client.delete_policy(policy_name) 59 | self.assertTrue(result) 60 | 61 | # Ensure the policy is deleted 62 | with self.assertRaises(PolicyNotFoundError): 63 | self.client.get_policy(policy_name) 64 | 65 | def test_data_lifecycle(self): 66 | # Define sample data 67 | data_name = "users" 68 | data_content = { 69 | "users": [ 70 | {"name": "alice", "role": "admin"}, 71 | {"name": "bob", "role": "user"}, 72 | ] 73 | } 74 | 75 | # Create/Update the data 76 | result = self.client.update_or_create_data(data_content, data_name) 77 | self.assertTrue(result) 78 | 79 | # Retrieve the data 80 | data = self.client.get_data(data_name) 81 | self.assertIn("result", data) 82 | self.assertEqual(data["result"], data_content) 83 | 84 | # Delete the data 85 | result = self.client.delete_data(data_name) 86 | self.assertTrue(result) 87 | 88 | # Ensure the data is deleted 89 | with self.assertRaises(PolicyNotFoundError): 90 | self.client.get_data(data_name) 91 | 92 | def test_check_permission(self): 93 | # Define a sample policy 94 | policy_name = "authz" 95 | policy_content = """ 96 | package authz 97 | 98 | default allow = false 99 | 100 | allow { 101 | input.user.role == "admin" 102 | } 103 | """ 104 | 105 | # Create the policy 106 | self.client.update_policy_from_string(policy_content, policy_name) 107 | 108 | # Define sample input data 109 | input_data = {"user": {"name": "alice", "role": "admin"}} 110 | 111 | # Check permission 112 | result = self.client.check_permission(input_data, policy_name, "allow") 113 | self.assertIn("result", result), result 114 | self.assertTrue(result["result"]) 115 | 116 | # Clean up 117 | self.client.delete_policy(policy_name) 118 | 119 | # Add more integration tests as needed 120 | 121 | 122 | if __name__ == "__main__": 123 | unittest.main() 124 | -------------------------------------------------------------------------------- /opa_client/test/test_opa.py: -------------------------------------------------------------------------------- 1 | # # -*- coding: utf-8 -*- 2 | # """ 3 | # Unit tests for the OpaClient. 4 | # """ 5 | 6 | 7 | # from unittest import TestCase 8 | 9 | # from opa_client.errors import DeleteDataError, DeletePolicyError 10 | # from opa import OpaClient 11 | 12 | 13 | # class TestClient(TestCase): 14 | # def setUp(self): 15 | # """Set up the test for OpaClient object""" 16 | 17 | # self.myclient = OpaClient() 18 | 19 | # def tearDown(self): 20 | # """Close the connection to the OPA server by deleting the client""" 21 | # del self.myclient 22 | 23 | # def test_client(self): 24 | # """Set up the test for OpaClient object""" 25 | 26 | # client = OpaClient('localhost', 8181, 'v1') 27 | # self.assertEqual('http://localhost:8181/v1', client._root_url) 28 | 29 | # client = OpaClient('localhost', 8181, 'v1') 30 | # self.assertEqual('http://localhost:8181/v1', client._root_url) 31 | 32 | # self.assertEqual('http://', self.myclient._schema) 33 | # self.assertEqual('v1', self.myclient._version) 34 | # self.assertEqual('localhost', self.myclient._host) 35 | # self.assertEqual(8181, self.myclient._port) 36 | 37 | # def test_connection_to_opa(self): 38 | # self.assertEqual(True, self.myclient.check_connection()) 39 | 40 | # def test_functions(self): 41 | # new_policy = """ 42 | # package test.policy 43 | 44 | # import data.test.acl 45 | # import input 46 | 47 | # default allow = false 48 | 49 | # allow { 50 | # access := acl[input.user] 51 | # access[_] == input.access 52 | # } 53 | 54 | # authorized_users[user] { 55 | # access := acl[user] 56 | # access[_] == input.access 57 | # } 58 | # """ 59 | 60 | # _dict = { 61 | # 'test': { 62 | # 'path': 'http://localhost:8181/v1/data/test/policy', 63 | # 'rules': [ 64 | # 'http://localhost:8181/v1/data/test/policy/allow', 65 | # 'http://localhost:8181/v1/data/test/policy/authorized_users' 66 | # ], 67 | # } 68 | # } 69 | 70 | # my_policy_list = { 71 | # "alice": ["read","write"], 72 | # "bob": ["read"] 73 | # } 74 | 75 | # self.assertEqual(list(), self.myclient.get_policies_list()) 76 | # self.assertEqual(dict(), self.myclient.get_policies_info()) 77 | # self.assertEqual(True, self.myclient.update_policy_from_string(new_policy, 'test')) 78 | # self.assertEqual(['test'], self.myclient.get_policies_list()) 79 | 80 | # policy_info = self.myclient.get_policies_info() 81 | # self.assertEqual(_dict['test']['path'], policy_info['test']['path']) 82 | # for rule in _dict['test']['rules']: 83 | # self.assertIn(rule, policy_info['test']['rules']) 84 | 85 | # self.assertTrue( 86 | # True, self.myclient.update_or_create_data(my_policy_list, 'test/acl') 87 | # ) 88 | 89 | # self.assertEqual(True, self.myclient.policy_to_file('test')) 90 | 91 | # value = {'result': {'acl': {'alice': ['read', 'write'], 'bob': ['read']}, 'policy': {'allow': False, 'authorized_users': []}}} 92 | # self.assertEqual(value, self.myclient.get_data('test')) 93 | 94 | # _input_a = {"input": {"user": "alice", "access": "write"}} 95 | # _input_b = {"input": {"access": "read"}} 96 | # value_a = {"result": True} 97 | # value_b = {"result": ["alice", "bob"]} 98 | # self.assertEqual(value_a, self.myclient.check_permission(input_data=_input_a, policy_name="test", rule_name="allow")) 99 | # self.assertEqual(value_b, self.myclient.check_permission(input_data=_input_b, policy_name="test", rule_name="authorized_users")) 100 | 101 | # self.assertTrue(True, self.myclient.delete_opa_policy('test')) 102 | # with self.assertRaises(DeletePolicyError): 103 | # self.myclient.delete_opa_policy('test') 104 | 105 | # self.assertTrue(True, self.myclient.delete_opa_data('test/acl')) 106 | # with self.assertRaises(DeleteDataError): 107 | # self.myclient.delete_opa_data('test/acl') 108 | 109 | 110 | # # import threading 111 | 112 | # # def test_client_in_thread(client, policy_name): 113 | # # try: 114 | # # policies = client.get_policies_list() 115 | # # print(f"Policies in thread {threading.current_thread().name}: {policies}") 116 | # # except Exception as e: 117 | # # print(f"Error in thread {threading.current_thread().name}: {e}") 118 | 119 | # # client = OpaClient() 120 | 121 | # # threads = [] 122 | # # for i in range(5): 123 | # # t = threading.Thread(target=test_client_in_thread, args=(client, f"policy_{i}")) 124 | # # threads.append(t) 125 | # # t.start() 126 | 127 | # # for t in threads: 128 | # # t.join() 129 | -------------------------------------------------------------------------------- /opa_client/test/test_opa_client.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock, patch 3 | 4 | import requests 5 | 6 | from opa_client import create_opa_client 7 | from opa_client.errors import ( 8 | ConnectionsError, 9 | DeleteDataError, 10 | DeletePolicyError, 11 | PolicyNotFoundError, 12 | RegoParseError, 13 | ) 14 | 15 | 16 | class TestOpaClient(unittest.TestCase): 17 | def setUp(self): 18 | self.client = create_opa_client(host="localhost", port=8181) 19 | 20 | def tearDown(self): 21 | self.client.close_connection() 22 | 23 | @patch("requests.Session.get") 24 | def test_check_connection_success(self, mock_get): 25 | mock_response = Mock() 26 | mock_response.status_code = 200 27 | mock_get.return_value = mock_response 28 | 29 | result = self.client.check_connection() 30 | self.assertEqual(result, True) 31 | mock_get.assert_called_once() 32 | 33 | @patch("requests.Session.get") 34 | def test_check_connection_failure(self, mock_get): 35 | mock_response = Mock() 36 | mock_response.status_code = 500 37 | mock_response.raise_for_status.side_effect = ( 38 | requests.exceptions.HTTPError() 39 | ) 40 | mock_get.return_value = mock_response 41 | with self.assertRaises(ConnectionsError): 42 | self.client.check_connection() 43 | mock_get.assert_called_once() 44 | 45 | @patch("requests.Session.get") 46 | def test_get_policies_list(self, mock_get): 47 | mock_response = Mock() 48 | mock_response.status_code = 200 49 | mock_response.json.return_value = { 50 | "result": [{"id": "policy1"}, {"id": "policy2"}] 51 | } 52 | mock_get.return_value = mock_response 53 | 54 | policies = self.client.get_policies_list() 55 | self.assertEqual(policies, ["policy1", "policy2"]) 56 | mock_get.assert_called_once() 57 | 58 | @patch("requests.Session.put") 59 | def test_update_policy_from_string_success(self, mock_put): 60 | mock_response = Mock() 61 | mock_response.status_code = 200 62 | mock_put.return_value = mock_response 63 | 64 | new_policy = "package example\n\ndefault allow = false" 65 | result = self.client.update_policy_from_string(new_policy, "example") 66 | self.assertTrue(result) 67 | mock_put.assert_called_once() 68 | 69 | @patch("requests.Session.put") 70 | def test_update_policy_from_string_failure(self, mock_put): 71 | mock_response = Mock() 72 | mock_response.status_code = 400 73 | mock_response.json.return_value = { 74 | "code": "invalid_parameter", 75 | "message": "Parse error", 76 | } 77 | mock_put.return_value = mock_response 78 | 79 | new_policy = "invalid policy" 80 | with self.assertRaises(Exception) as context: 81 | self.client.update_policy_from_string(new_policy, "invalid") 82 | 83 | self.assertIsInstance(context.exception, RegoParseError) 84 | mock_put.assert_called_once() 85 | 86 | @patch("requests.Session.delete") 87 | def test_delete_policy_success(self, mock_delete): 88 | mock_response = Mock() 89 | mock_response.status_code = 200 90 | mock_delete.return_value = mock_response 91 | 92 | result = self.client.delete_policy("policy1") 93 | self.assertTrue(result) 94 | mock_delete.assert_called_once() 95 | 96 | @patch("requests.Session.delete") 97 | def test_delete_policy_failure(self, mock_delete): 98 | mock_response = Mock() 99 | mock_response.status_code = 404 100 | mock_response.json.return_value = { 101 | "code": "not_found", 102 | "message": "Policy not found", 103 | } 104 | mock_delete.return_value = mock_response 105 | 106 | with self.assertRaises(DeletePolicyError): 107 | self.client.delete_policy("nonexistent_policy") 108 | mock_delete.assert_called_once() 109 | 110 | # Add more test methods to cover other functionalities 111 | 112 | 113 | if __name__ == "__main__": 114 | unittest.main() 115 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "aiodns" 5 | version = "3.2.0" 6 | description = "Simple DNS resolver for asyncio" 7 | optional = false 8 | python-versions = "*" 9 | files = [ 10 | {file = "aiodns-3.2.0-py3-none-any.whl", hash = "sha256:e443c0c27b07da3174a109fd9e736d69058d808f144d3c9d56dbd1776964c5f5"}, 11 | {file = "aiodns-3.2.0.tar.gz", hash = "sha256:62869b23409349c21b072883ec8998316b234c9a9e36675756e8e317e8768f72"}, 12 | ] 13 | 14 | [package.dependencies] 15 | pycares = ">=4.0.0" 16 | 17 | [[package]] 18 | name = "aiofiles" 19 | version = "24.1.0" 20 | description = "File support for asyncio." 21 | optional = false 22 | python-versions = ">=3.8" 23 | files = [ 24 | {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, 25 | {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, 26 | ] 27 | 28 | [[package]] 29 | name = "aiohappyeyeballs" 30 | version = "2.4.3" 31 | description = "Happy Eyeballs for asyncio" 32 | optional = false 33 | python-versions = ">=3.8" 34 | files = [ 35 | {file = "aiohappyeyeballs-2.4.3-py3-none-any.whl", hash = "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572"}, 36 | {file = "aiohappyeyeballs-2.4.3.tar.gz", hash = "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586"}, 37 | ] 38 | 39 | [[package]] 40 | name = "aiohttp" 41 | version = "3.10.9" 42 | description = "Async http client/server framework (asyncio)" 43 | optional = false 44 | python-versions = ">=3.8" 45 | files = [ 46 | {file = "aiohttp-3.10.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8b3fb28a9ac8f2558760d8e637dbf27aef1e8b7f1d221e8669a1074d1a266bb2"}, 47 | {file = "aiohttp-3.10.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:91aa966858593f64c8a65cdefa3d6dc8fe3c2768b159da84c1ddbbb2c01ab4ef"}, 48 | {file = "aiohttp-3.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63649309da83277f06a15bbdc2a54fbe75efb92caa2c25bb57ca37762789c746"}, 49 | {file = "aiohttp-3.10.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3e7fabedb3fe06933f47f1538df7b3a8d78e13d7167195f51ca47ee12690373"}, 50 | {file = "aiohttp-3.10.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c070430fda1a550a1c3a4c2d7281d3b8cfc0c6715f616e40e3332201a253067"}, 51 | {file = "aiohttp-3.10.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:51d0a4901b27272ae54e42067bc4b9a90e619a690b4dc43ea5950eb3070afc32"}, 52 | {file = "aiohttp-3.10.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fec5fac7aea6c060f317f07494961236434928e6f4374e170ef50b3001e14581"}, 53 | {file = "aiohttp-3.10.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:172ad884bb61ad31ed7beed8be776eb17e7fb423f1c1be836d5cb357a096bf12"}, 54 | {file = "aiohttp-3.10.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d646fdd74c25bbdd4a055414f0fe32896c400f38ffbdfc78c68e62812a9e0257"}, 55 | {file = "aiohttp-3.10.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e86260b76786c28acf0b5fe31c8dca4c2add95098c709b11e8c35b424ebd4f5b"}, 56 | {file = "aiohttp-3.10.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d7cafc11d70fdd8801abfc2ff276744ae4cb39d8060b6b542c7e44e5f2cfc2"}, 57 | {file = "aiohttp-3.10.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fc262c3df78c8ff6020c782d9ce02e4bcffe4900ad71c0ecdad59943cba54442"}, 58 | {file = "aiohttp-3.10.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:482c85cf3d429844396d939b22bc2a03849cb9ad33344689ad1c85697bcba33a"}, 59 | {file = "aiohttp-3.10.9-cp310-cp310-win32.whl", hash = "sha256:aeebd3061f6f1747c011e1d0b0b5f04f9f54ad1a2ca183e687e7277bef2e0da2"}, 60 | {file = "aiohttp-3.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:fa430b871220dc62572cef9c69b41e0d70fcb9d486a4a207a5de4c1f25d82593"}, 61 | {file = "aiohttp-3.10.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:16e6a51d8bc96b77f04a6764b4ad03eeef43baa32014fce71e882bd71302c7e4"}, 62 | {file = "aiohttp-3.10.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8bd9125dd0cc8ebd84bff2be64b10fdba7dc6fd7be431b5eaf67723557de3a31"}, 63 | {file = "aiohttp-3.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dcf354661f54e6a49193d0b5653a1b011ba856e0b7a76bda2c33e4c6892f34ea"}, 64 | {file = "aiohttp-3.10.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42775de0ca04f90c10c5c46291535ec08e9bcc4756f1b48f02a0657febe89b10"}, 65 | {file = "aiohttp-3.10.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d1e4185c5d7187684d41ebb50c9aeaaaa06ca1875f4c57593071b0409d2444"}, 66 | {file = "aiohttp-3.10.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2695c61cf53a5d4345a43d689f37fc0f6d3a2dc520660aec27ec0f06288d1f9"}, 67 | {file = "aiohttp-3.10.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a3f063b41cc06e8d0b3fcbbfc9c05b7420f41287e0cd4f75ce0a1f3d80729e6"}, 68 | {file = "aiohttp-3.10.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d37f4718002863b82c6f391c8efd4d3a817da37030a29e2682a94d2716209de"}, 69 | {file = "aiohttp-3.10.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2746d8994ebca1bdc55a1e998feff4e94222da709623bb18f6e5cfec8ec01baf"}, 70 | {file = "aiohttp-3.10.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6f3c6648aa123bcd73d6f26607d59967b607b0da8ffcc27d418a4b59f4c98c7c"}, 71 | {file = "aiohttp-3.10.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:558b3d223fd631ad134d89adea876e7fdb4c93c849ef195049c063ada82b7d08"}, 72 | {file = "aiohttp-3.10.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4e6cb75f8ddd9c2132d00bc03c9716add57f4beff1263463724f6398b813e7eb"}, 73 | {file = "aiohttp-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:608cecd8d58d285bfd52dbca5b6251ca8d6ea567022c8a0eaae03c2589cd9af9"}, 74 | {file = "aiohttp-3.10.9-cp311-cp311-win32.whl", hash = "sha256:36d4fba838be5f083f5490ddd281813b44d69685db910907636bc5dca6322316"}, 75 | {file = "aiohttp-3.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:8be1a65487bdfc285bd5e9baf3208c2132ca92a9b4020e9f27df1b16fab998a9"}, 76 | {file = "aiohttp-3.10.9-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4fd16b30567c5b8e167923be6e027eeae0f20cf2b8a26b98a25115f28ad48ee0"}, 77 | {file = "aiohttp-3.10.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:40ff5b7660f903dc587ed36ef08a88d46840182d9d4b5694e7607877ced698a1"}, 78 | {file = "aiohttp-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4edc3fd701e2b9a0d605a7b23d3de4ad23137d23fc0dbab726aa71d92f11aaaf"}, 79 | {file = "aiohttp-3.10.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e525b69ee8a92c146ae5b4da9ecd15e518df4d40003b01b454ad694a27f498b5"}, 80 | {file = "aiohttp-3.10.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5002a02c17fcfd796d20bac719981d2fca9c006aac0797eb8f430a58e9d12431"}, 81 | {file = "aiohttp-3.10.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4ceeae2fb8cabdd1b71c82bfdd39662473d3433ec95b962200e9e752fb70d0"}, 82 | {file = "aiohttp-3.10.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6e395c3d1f773cf0651cd3559e25182eb0c03a2777b53b4575d8adc1149c6e9"}, 83 | {file = "aiohttp-3.10.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbdb8def5268f3f9cd753a265756f49228a20ed14a480d151df727808b4531dd"}, 84 | {file = "aiohttp-3.10.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f82ace0ec57c94aaf5b0e118d4366cff5889097412c75aa14b4fd5fc0c44ee3e"}, 85 | {file = "aiohttp-3.10.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6ebdc3b3714afe1b134b3bbeb5f745eed3ecbcff92ab25d80e4ef299e83a5465"}, 86 | {file = "aiohttp-3.10.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f9ca09414003c0e96a735daa1f071f7d7ed06962ef4fa29ceb6c80d06696d900"}, 87 | {file = "aiohttp-3.10.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1298b854fd31d0567cbb916091be9d3278168064fca88e70b8468875ef9ff7e7"}, 88 | {file = "aiohttp-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60ad5b8a7452c0f5645c73d4dad7490afd6119d453d302cd5b72b678a85d6044"}, 89 | {file = "aiohttp-3.10.9-cp312-cp312-win32.whl", hash = "sha256:1a0ee6c0d590c917f1b9629371fce5f3d3f22c317aa96fbdcce3260754d7ea21"}, 90 | {file = "aiohttp-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:c46131c6112b534b178d4e002abe450a0a29840b61413ac25243f1291613806a"}, 91 | {file = "aiohttp-3.10.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2bd9f3eac515c16c4360a6a00c38119333901b8590fe93c3257a9b536026594d"}, 92 | {file = "aiohttp-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8cc0d13b4e3b1362d424ce3f4e8c79e1f7247a00d792823ffd640878abf28e56"}, 93 | {file = "aiohttp-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ba1a599255ad6a41022e261e31bc2f6f9355a419575b391f9655c4d9e5df5ff5"}, 94 | {file = "aiohttp-3.10.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:776e9f3c9b377fcf097c4a04b241b15691e6662d850168642ff976780609303c"}, 95 | {file = "aiohttp-3.10.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8debb45545ad95b58cc16c3c1cc19ad82cffcb106db12b437885dbee265f0ab5"}, 96 | {file = "aiohttp-3.10.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2555e4949c8d8782f18ef20e9d39730d2656e218a6f1a21a4c4c0b56546a02e"}, 97 | {file = "aiohttp-3.10.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c54dc329cd44f7f7883a9f4baaefe686e8b9662e2c6c184ea15cceee587d8d69"}, 98 | {file = "aiohttp-3.10.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e709d6ac598c5416f879bb1bae3fd751366120ac3fa235a01de763537385d036"}, 99 | {file = "aiohttp-3.10.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:17c272cfe7b07a5bb0c6ad3f234e0c336fb53f3bf17840f66bd77b5815ab3d16"}, 100 | {file = "aiohttp-3.10.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0c21c82df33b264216abffff9f8370f303dab65d8eee3767efbbd2734363f677"}, 101 | {file = "aiohttp-3.10.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9331dd34145ff105177855017920dde140b447049cd62bb589de320fd6ddd582"}, 102 | {file = "aiohttp-3.10.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ac3196952c673822ebed8871cf8802e17254fff2a2ed4835d9c045d9b88c5ec7"}, 103 | {file = "aiohttp-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2c33fa6e10bb7ed262e3ff03cc69d52869514f16558db0626a7c5c61dde3c29f"}, 104 | {file = "aiohttp-3.10.9-cp313-cp313-win32.whl", hash = "sha256:a14e4b672c257a6b94fe934ee62666bacbc8e45b7876f9dd9502d0f0fe69db16"}, 105 | {file = "aiohttp-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:a35ed3d03910785f7d9d6f5381f0c24002b2b888b298e6f941b2fc94c5055fcd"}, 106 | {file = "aiohttp-3.10.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f392ef50e22c31fa49b5a46af7f983fa3f118f3eccb8522063bee8bfa6755f8"}, 107 | {file = "aiohttp-3.10.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d1f5c9169e26db6a61276008582d945405b8316aae2bb198220466e68114a0f5"}, 108 | {file = "aiohttp-3.10.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8d9d10d10ec27c0d46ddaecc3c5598c4db9ce4e6398ca872cdde0525765caa2f"}, 109 | {file = "aiohttp-3.10.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d97273a52d7f89a75b11ec386f786d3da7723d7efae3034b4dda79f6f093edc1"}, 110 | {file = "aiohttp-3.10.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d271f770b52e32236d945911b2082f9318e90ff835d45224fa9e28374303f729"}, 111 | {file = "aiohttp-3.10.9-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7003f33f5f7da1eb02f0446b0f8d2ccf57d253ca6c2e7a5732d25889da82b517"}, 112 | {file = "aiohttp-3.10.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6e00c8a92e7663ed2be6fcc08a2997ff06ce73c8080cd0df10cc0321a3168d7"}, 113 | {file = "aiohttp-3.10.9-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a61df62966ce6507aafab24e124e0c3a1cfbe23c59732987fc0fd0d71daa0b88"}, 114 | {file = "aiohttp-3.10.9-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:60555211a006d26e1a389222e3fab8cd379f28e0fbf7472ee55b16c6c529e3a6"}, 115 | {file = "aiohttp-3.10.9-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:d15a29424e96fad56dc2f3abed10a89c50c099f97d2416520c7a543e8fddf066"}, 116 | {file = "aiohttp-3.10.9-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:a19caae0d670771ea7854ca30df76f676eb47e0fd9b2ee4392d44708f272122d"}, 117 | {file = "aiohttp-3.10.9-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:99f9678bf0e2b1b695e8028fedac24ab6770937932eda695815d5a6618c37e04"}, 118 | {file = "aiohttp-3.10.9-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2914caa46054f3b5ff910468d686742ff8cff54b8a67319d75f5d5945fd0a13d"}, 119 | {file = "aiohttp-3.10.9-cp38-cp38-win32.whl", hash = "sha256:0bc059ecbce835630e635879f5f480a742e130d9821fbe3d2f76610a6698ee25"}, 120 | {file = "aiohttp-3.10.9-cp38-cp38-win_amd64.whl", hash = "sha256:e883b61b75ca6efc2541fcd52a5c8ccfe288b24d97e20ac08fdf343b8ac672ea"}, 121 | {file = "aiohttp-3.10.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fcd546782d03181b0b1d20b43d612429a90a68779659ba8045114b867971ab71"}, 122 | {file = "aiohttp-3.10.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:85711eec2d875cd88c7eb40e734c4ca6d9ae477d6f26bd2b5bb4f7f60e41b156"}, 123 | {file = "aiohttp-3.10.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:02d1d6610588bcd743fae827bd6f2e47e0d09b346f230824b4c6fb85c6065f9c"}, 124 | {file = "aiohttp-3.10.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3668d0c2a4d23fb136a753eba42caa2c0abbd3d9c5c87ee150a716a16c6deec1"}, 125 | {file = "aiohttp-3.10.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7c071235a47d407b0e93aa6262b49422dbe48d7d8566e1158fecc91043dd948"}, 126 | {file = "aiohttp-3.10.9-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac74e794e3aee92ae8f571bfeaa103a141e409863a100ab63a253b1c53b707eb"}, 127 | {file = "aiohttp-3.10.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bbf94d4a0447705b7775417ca8bb8086cc5482023a6e17cdc8f96d0b1b5aba6"}, 128 | {file = "aiohttp-3.10.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb0b2d5d51f96b6cc19e6ab46a7b684be23240426ae951dcdac9639ab111b45e"}, 129 | {file = "aiohttp-3.10.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e83dfefb4f7d285c2d6a07a22268344a97d61579b3e0dce482a5be0251d672ab"}, 130 | {file = "aiohttp-3.10.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f0a44bb40b6aaa4fb9a5c1ee07880570ecda2065433a96ccff409c9c20c1624a"}, 131 | {file = "aiohttp-3.10.9-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c2b627d3c8982691b06d89d31093cee158c30629fdfebe705a91814d49b554f8"}, 132 | {file = "aiohttp-3.10.9-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:03690541e4cc866eef79626cfa1ef4dd729c5c1408600c8cb9e12e1137eed6ab"}, 133 | {file = "aiohttp-3.10.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ad3675c126f2a95bde637d162f8231cff6bc0bc9fbe31bd78075f9ff7921e322"}, 134 | {file = "aiohttp-3.10.9-cp39-cp39-win32.whl", hash = "sha256:1321658f12b6caffafdc35cfba6c882cb014af86bef4e78c125e7e794dfb927b"}, 135 | {file = "aiohttp-3.10.9-cp39-cp39-win_amd64.whl", hash = "sha256:9fdf5c839bf95fc67be5794c780419edb0dbef776edcfc6c2e5e2ffd5ee755fa"}, 136 | {file = "aiohttp-3.10.9.tar.gz", hash = "sha256:143b0026a9dab07a05ad2dd9e46aa859bffdd6348ddc5967b42161168c24f857"}, 137 | ] 138 | 139 | [package.dependencies] 140 | aiodns = {version = ">=3.2.0", optional = true, markers = "(sys_platform == \"linux\" or sys_platform == \"darwin\") and extra == \"speedups\""} 141 | aiohappyeyeballs = ">=2.3.0" 142 | aiosignal = ">=1.1.2" 143 | async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} 144 | attrs = ">=17.3.0" 145 | Brotli = {version = "*", optional = true, markers = "platform_python_implementation == \"CPython\" and extra == \"speedups\""} 146 | brotlicffi = {version = "*", optional = true, markers = "platform_python_implementation != \"CPython\" and extra == \"speedups\""} 147 | frozenlist = ">=1.1.1" 148 | multidict = ">=4.5,<7.0" 149 | yarl = ">=1.12.0,<2.0" 150 | 151 | [package.extras] 152 | speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] 153 | 154 | [[package]] 155 | name = "aiosignal" 156 | version = "1.3.1" 157 | description = "aiosignal: a list of registered asynchronous callbacks" 158 | optional = false 159 | python-versions = ">=3.7" 160 | files = [ 161 | {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, 162 | {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, 163 | ] 164 | 165 | [package.dependencies] 166 | frozenlist = ">=1.1.0" 167 | 168 | [[package]] 169 | name = "async-timeout" 170 | version = "4.0.3" 171 | description = "Timeout context manager for asyncio programs" 172 | optional = false 173 | python-versions = ">=3.7" 174 | files = [ 175 | {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, 176 | {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, 177 | ] 178 | 179 | [[package]] 180 | name = "attrs" 181 | version = "24.2.0" 182 | description = "Classes Without Boilerplate" 183 | optional = false 184 | python-versions = ">=3.7" 185 | files = [ 186 | {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, 187 | {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, 188 | ] 189 | 190 | [package.extras] 191 | benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] 192 | cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] 193 | dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] 194 | docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] 195 | tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] 196 | tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] 197 | 198 | [[package]] 199 | name = "brotli" 200 | version = "1.1.0" 201 | description = "Python bindings for the Brotli compression library" 202 | optional = false 203 | python-versions = "*" 204 | files = [ 205 | {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"}, 206 | {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"}, 207 | {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3"}, 208 | {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d"}, 209 | {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e"}, 210 | {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"}, 211 | {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80"}, 212 | {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"}, 213 | {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"}, 214 | {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"}, 215 | {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"}, 216 | {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"}, 217 | {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"}, 218 | {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6"}, 219 | {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd"}, 220 | {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf"}, 221 | {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61"}, 222 | {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327"}, 223 | {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd"}, 224 | {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"}, 225 | {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"}, 226 | {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"}, 227 | {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"}, 228 | {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"}, 229 | {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"}, 230 | {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"}, 231 | {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"}, 232 | {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91"}, 233 | {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408"}, 234 | {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0"}, 235 | {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc"}, 236 | {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"}, 237 | {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"}, 238 | {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"}, 239 | {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"}, 240 | {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"}, 241 | {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"}, 242 | {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"}, 243 | {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"}, 244 | {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112"}, 245 | {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064"}, 246 | {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914"}, 247 | {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"}, 248 | {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"}, 249 | {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"}, 250 | {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"}, 251 | {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"}, 252 | {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"}, 253 | {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985"}, 254 | {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60"}, 255 | {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a"}, 256 | {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84"}, 257 | {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643"}, 258 | {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"}, 259 | {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"}, 260 | {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"}, 261 | {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"}, 262 | {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"}, 263 | {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"}, 264 | {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208"}, 265 | {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7"}, 266 | {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751"}, 267 | {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48"}, 268 | {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619"}, 269 | {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97"}, 270 | {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"}, 271 | {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"}, 272 | {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"}, 273 | {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"}, 274 | {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"}, 275 | {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"}, 276 | {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f"}, 277 | {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9"}, 278 | {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf"}, 279 | {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac"}, 280 | {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578"}, 281 | {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474"}, 282 | {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"}, 283 | {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"}, 284 | {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"}, 285 | {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"}, 286 | {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"}, 287 | {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"}, 288 | ] 289 | 290 | [[package]] 291 | name = "brotlicffi" 292 | version = "1.1.0.0" 293 | description = "Python CFFI bindings to the Brotli library" 294 | optional = false 295 | python-versions = ">=3.7" 296 | files = [ 297 | {file = "brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851"}, 298 | {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b"}, 299 | {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814"}, 300 | {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820"}, 301 | {file = "brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb"}, 302 | {file = "brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613"}, 303 | {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2e4aeb0bd2540cb91b069dbdd54d458da8c4334ceaf2d25df2f4af576d6766ca"}, 304 | {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b7b0033b0d37bb33009fb2fef73310e432e76f688af76c156b3594389d81391"}, 305 | {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54a07bb2374a1eba8ebb52b6fafffa2afd3c4df85ddd38fcc0511f2bb387c2a8"}, 306 | {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"}, 307 | {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce01c7316aebc7fce59da734286148b1d1b9455f89cf2c8a4dfce7d41db55c2d"}, 308 | {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:246f1d1a90279bb6069de3de8d75a8856e073b8ff0b09dcca18ccc14cec85979"}, 309 | {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc4bc5d82bc56ebd8b514fb8350cfac4627d6b0743382e46d033976a5f80fab6"}, 310 | {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c26ecb14386a44b118ce36e546ce307f4810bc9598a6e6cb4f7fca725ae7e6"}, 311 | {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"}, 312 | {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:add0de5b9ad9e9aa293c3aa4e9deb2b61e99ad6c1634e01d01d98c03e6a354cc"}, 313 | {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b6068e0f3769992d6b622a1cd2e7835eae3cf8d9da123d7f51ca9c1e9c333e5"}, 314 | {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8557a8559509b61e65083f8782329188a250102372576093c88930c875a69838"}, 315 | {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a7ae37e5d79c5bdfb5b4b99f2715a6035e6c5bf538c3746abc8e26694f92f33"}, 316 | {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"}, 317 | {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2f3711be9290f0453de8eed5275d93d286abe26b08ab4a35d7452caa1fef532f"}, 318 | {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a807d760763e398bbf2c6394ae9da5815901aa93ee0a37bca5efe78d4ee3171"}, 319 | {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa8ca0623b26c94fccc3a1fdd895be1743b838f3917300506d04aa3346fd2a14"}, 320 | {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3de0cf28a53a3238b252aca9fed1593e9d36c1d116748013339f0949bfc84112"}, 321 | {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"}, 322 | {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d9eb71bb1085d996244439154387266fd23d6ad37161f6f52f1cd41dd95a3808"}, 323 | {file = "brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13"}, 324 | ] 325 | 326 | [package.dependencies] 327 | cffi = ">=1.0.0" 328 | 329 | [[package]] 330 | name = "certifi" 331 | version = "2024.8.30" 332 | description = "Python package for providing Mozilla's CA Bundle." 333 | optional = false 334 | python-versions = ">=3.6" 335 | files = [ 336 | {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, 337 | {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, 338 | ] 339 | 340 | [[package]] 341 | name = "cffi" 342 | version = "1.17.1" 343 | description = "Foreign Function Interface for Python calling C code." 344 | optional = false 345 | python-versions = ">=3.8" 346 | files = [ 347 | {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, 348 | {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, 349 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, 350 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, 351 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, 352 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, 353 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, 354 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, 355 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, 356 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, 357 | {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, 358 | {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, 359 | {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, 360 | {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, 361 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, 362 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, 363 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, 364 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, 365 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, 366 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, 367 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, 368 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, 369 | {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, 370 | {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, 371 | {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, 372 | {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, 373 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, 374 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, 375 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, 376 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, 377 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, 378 | {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, 379 | {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, 380 | {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, 381 | {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, 382 | {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, 383 | {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, 384 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, 385 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, 386 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, 387 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, 388 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, 389 | {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, 390 | {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, 391 | {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, 392 | {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, 393 | {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, 394 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, 395 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, 396 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, 397 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, 398 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, 399 | {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, 400 | {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, 401 | {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, 402 | {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, 403 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, 404 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, 405 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, 406 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, 407 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, 408 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, 409 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, 410 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, 411 | {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, 412 | {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, 413 | {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, 414 | ] 415 | 416 | [package.dependencies] 417 | pycparser = "*" 418 | 419 | [[package]] 420 | name = "charset-normalizer" 421 | version = "3.3.2" 422 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 423 | optional = false 424 | python-versions = ">=3.7.0" 425 | files = [ 426 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 427 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 428 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 429 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 430 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 431 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 432 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 433 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 434 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 435 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 436 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 437 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 438 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 439 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 440 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 441 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 442 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 443 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 444 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 445 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 446 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 447 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 448 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 449 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 450 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 451 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 452 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 453 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 454 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 455 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 456 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 457 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 458 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 459 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 460 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 461 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 462 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 463 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 464 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 465 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 466 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 467 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 468 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 469 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 470 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 471 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 472 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 473 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 474 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 475 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 476 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 477 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 478 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 479 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 480 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 481 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 482 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 483 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 484 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 485 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 486 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 487 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 488 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 489 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 490 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 491 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 492 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 493 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 494 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 495 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 496 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 497 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 498 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 499 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 500 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 501 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 502 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 503 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 504 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 505 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 506 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 507 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 508 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 509 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 510 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 511 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 512 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 513 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 514 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 515 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 516 | ] 517 | 518 | [[package]] 519 | name = "colorama" 520 | version = "0.4.6" 521 | description = "Cross-platform colored terminal text." 522 | optional = false 523 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 524 | files = [ 525 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 526 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 527 | ] 528 | 529 | [[package]] 530 | name = "exceptiongroup" 531 | version = "1.2.2" 532 | description = "Backport of PEP 654 (exception groups)" 533 | optional = false 534 | python-versions = ">=3.7" 535 | files = [ 536 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 537 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 538 | ] 539 | 540 | [package.extras] 541 | test = ["pytest (>=6)"] 542 | 543 | [[package]] 544 | name = "frozenlist" 545 | version = "1.4.1" 546 | description = "A list-like structure which implements collections.abc.MutableSequence" 547 | optional = false 548 | python-versions = ">=3.8" 549 | files = [ 550 | {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, 551 | {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, 552 | {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, 553 | {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, 554 | {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, 555 | {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, 556 | {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, 557 | {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"}, 558 | {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, 559 | {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, 560 | {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, 561 | {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, 562 | {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, 563 | {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, 564 | {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, 565 | {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, 566 | {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, 567 | {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, 568 | {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, 569 | {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, 570 | {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, 571 | {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, 572 | {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"}, 573 | {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, 574 | {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, 575 | {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, 576 | {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, 577 | {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, 578 | {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, 579 | {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, 580 | {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, 581 | {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, 582 | {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, 583 | {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, 584 | {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, 585 | {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, 586 | {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, 587 | {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"}, 588 | {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, 589 | {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, 590 | {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, 591 | {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, 592 | {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, 593 | {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, 594 | {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, 595 | {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, 596 | {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, 597 | {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, 598 | {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, 599 | {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, 600 | {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, 601 | {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, 602 | {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"}, 603 | {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, 604 | {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, 605 | {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, 606 | {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, 607 | {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, 608 | {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, 609 | {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, 610 | {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, 611 | {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, 612 | {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, 613 | {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, 614 | {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, 615 | {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, 616 | {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, 617 | {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"}, 618 | {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, 619 | {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, 620 | {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, 621 | {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, 622 | {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, 623 | {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, 624 | {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, 625 | {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, 626 | {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, 627 | ] 628 | 629 | [[package]] 630 | name = "idna" 631 | version = "3.10" 632 | description = "Internationalized Domain Names in Applications (IDNA)" 633 | optional = false 634 | python-versions = ">=3.6" 635 | files = [ 636 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 637 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 638 | ] 639 | 640 | [package.extras] 641 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 642 | 643 | [[package]] 644 | name = "iniconfig" 645 | version = "2.0.0" 646 | description = "brain-dead simple config-ini parsing" 647 | optional = false 648 | python-versions = ">=3.7" 649 | files = [ 650 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 651 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 652 | ] 653 | 654 | [[package]] 655 | name = "multidict" 656 | version = "6.1.0" 657 | description = "multidict implementation" 658 | optional = false 659 | python-versions = ">=3.8" 660 | files = [ 661 | {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, 662 | {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, 663 | {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"}, 664 | {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"}, 665 | {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"}, 666 | {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"}, 667 | {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"}, 668 | {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"}, 669 | {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"}, 670 | {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"}, 671 | {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"}, 672 | {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"}, 673 | {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"}, 674 | {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"}, 675 | {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"}, 676 | {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, 677 | {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, 678 | {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, 679 | {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, 680 | {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, 681 | {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, 682 | {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, 683 | {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, 684 | {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, 685 | {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, 686 | {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, 687 | {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, 688 | {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, 689 | {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, 690 | {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, 691 | {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, 692 | {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, 693 | {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, 694 | {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, 695 | {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, 696 | {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, 697 | {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, 698 | {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, 699 | {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, 700 | {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, 701 | {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, 702 | {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, 703 | {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, 704 | {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, 705 | {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, 706 | {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"}, 707 | {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"}, 708 | {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"}, 709 | {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"}, 710 | {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"}, 711 | {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"}, 712 | {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"}, 713 | {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"}, 714 | {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"}, 715 | {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"}, 716 | {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"}, 717 | {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"}, 718 | {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"}, 719 | {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"}, 720 | {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"}, 721 | {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"}, 722 | {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"}, 723 | {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"}, 724 | {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"}, 725 | {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"}, 726 | {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"}, 727 | {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"}, 728 | {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"}, 729 | {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"}, 730 | {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"}, 731 | {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"}, 732 | {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"}, 733 | {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"}, 734 | {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"}, 735 | {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"}, 736 | {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"}, 737 | {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"}, 738 | {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"}, 739 | {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"}, 740 | {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"}, 741 | {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"}, 742 | {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"}, 743 | {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"}, 744 | {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"}, 745 | {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"}, 746 | {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"}, 747 | {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"}, 748 | {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"}, 749 | {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"}, 750 | {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"}, 751 | {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, 752 | {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, 753 | ] 754 | 755 | [package.dependencies] 756 | typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} 757 | 758 | [[package]] 759 | name = "packaging" 760 | version = "24.1" 761 | description = "Core utilities for Python packages" 762 | optional = false 763 | python-versions = ">=3.8" 764 | files = [ 765 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 766 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 767 | ] 768 | 769 | [[package]] 770 | name = "pluggy" 771 | version = "1.5.0" 772 | description = "plugin and hook calling mechanisms for python" 773 | optional = false 774 | python-versions = ">=3.8" 775 | files = [ 776 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 777 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 778 | ] 779 | 780 | [package.extras] 781 | dev = ["pre-commit", "tox"] 782 | testing = ["pytest", "pytest-benchmark"] 783 | 784 | [[package]] 785 | name = "pycares" 786 | version = "4.4.0" 787 | description = "Python interface for c-ares" 788 | optional = false 789 | python-versions = ">=3.8" 790 | files = [ 791 | {file = "pycares-4.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:24da119850841d16996713d9c3374ca28a21deee056d609fbbed29065d17e1f6"}, 792 | {file = "pycares-4.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8f64cb58729689d4d0e78f0bfb4c25ce2f851d0274c0273ac751795c04b8798a"}, 793 | {file = "pycares-4.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33e2a1120887e89075f7f814ec144f66a6ce06a54f5722ccefc62fbeda83cff"}, 794 | {file = "pycares-4.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c680fef1b502ee680f8f0b95a41af4ec2c234e50e16c0af5bbda31999d3584bd"}, 795 | {file = "pycares-4.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fff16b09042ba077f7b8aa5868d1d22456f0002574d0ba43462b10a009331677"}, 796 | {file = "pycares-4.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:229a1675eb33bc9afb1fc463e73ee334950ccc485bc83a43f6ae5839fb4d5fa3"}, 797 | {file = "pycares-4.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3aebc73e5ad70464f998f77f2da2063aa617cbd8d3e8174dd7c5b4518f967153"}, 798 | {file = "pycares-4.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6ef64649eba56448f65e26546d85c860709844d2fc22ef14d324fe0b27f761a9"}, 799 | {file = "pycares-4.4.0-cp310-cp310-win32.whl", hash = "sha256:4afc2644423f4eef97857a9fd61be9758ce5e336b4b0bd3d591238bb4b8b03e0"}, 800 | {file = "pycares-4.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5ed4e04af4012f875b78219d34434a6d08a67175150ac1b79eb70ab585d4ba8c"}, 801 | {file = "pycares-4.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bce8db2fc6f3174bd39b81405210b9b88d7b607d33e56a970c34a0c190da0490"}, 802 | {file = "pycares-4.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9a0303428d013ccf5c51de59c83f9127aba6200adb7fd4be57eddb432a1edd2a"}, 803 | {file = "pycares-4.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afb91792f1556f97be7f7acb57dc7756d89c5a87bd8b90363a77dbf9ea653817"}, 804 | {file = "pycares-4.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b61579cecf1f4d616e5ea31a6e423a16680ab0d3a24a2ffe7bb1d4ee162477ff"}, 805 | {file = "pycares-4.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7af06968cbf6851566e806bf3e72825b0e6671832a2cbe840be1d2d65350710"}, 806 | {file = "pycares-4.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ceb12974367b0a68a05d52f4162b29f575d241bd53de155efe632bf2c943c7f6"}, 807 | {file = "pycares-4.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2eeec144bcf6a7b6f2d74d6e70cbba7886a84dd373c886f06cb137a07de4954c"}, 808 | {file = "pycares-4.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e3a6f7cfdfd11eb5493d6d632e582408c8f3b429f295f8799c584c108b28db6f"}, 809 | {file = "pycares-4.4.0-cp311-cp311-win32.whl", hash = "sha256:34736a2ffaa9c08ca9c707011a2d7b69074bbf82d645d8138bba771479b2362f"}, 810 | {file = "pycares-4.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:eb66c30eb11e877976b7ead13632082a8621df648c408b8e15cdb91a452dd502"}, 811 | {file = "pycares-4.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fd644505a8cfd7f6584d33a9066d4e3d47700f050ef1490230c962de5dfb28c6"}, 812 | {file = "pycares-4.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52084961262232ec04bd75f5043aed7e5d8d9695e542ff691dfef0110209f2d4"}, 813 | {file = "pycares-4.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0c5368206057884cde18602580083aeaad9b860e2eac14fd253543158ce1e93"}, 814 | {file = "pycares-4.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:112a4979c695b1c86f6782163d7dec58d57a3b9510536dcf4826550f9053dd9a"}, 815 | {file = "pycares-4.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d186dafccdaa3409194c0f94db93c1a5d191145a275f19da6591f9499b8e7b8"}, 816 | {file = "pycares-4.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:64965dc19c578a683ea73487a215a8897276224e004d50eeb21f0bc7a0b63c88"}, 817 | {file = "pycares-4.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ed2a38e34bec6f2586435f6ff0bc5fe11d14bebd7ed492cf739a424e81681540"}, 818 | {file = "pycares-4.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:94d6962db81541eb0396d2f0dfcbb18cdb8c8b251d165efc2d974ae652c547d4"}, 819 | {file = "pycares-4.4.0-cp312-cp312-win32.whl", hash = "sha256:1168a48a834813aa80f412be2df4abaf630528a58d15c704857448b20b1675c0"}, 820 | {file = "pycares-4.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:db24c4e7fea4a052c6e869cbf387dd85d53b9736cfe1ef5d8d568d1ca925e977"}, 821 | {file = "pycares-4.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:21a5a0468861ec7df7befa69050f952da13db5427ae41ffe4713bc96291d1d95"}, 822 | {file = "pycares-4.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:22c00bf659a9fa44d7b405cf1cd69b68b9d37537899898d8cbe5dffa4016b273"}, 823 | {file = "pycares-4.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23aa3993a352491a47fcf17867f61472f32f874df4adcbb486294bd9fbe8abee"}, 824 | {file = "pycares-4.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:813d661cbe2e37d87da2d16b7110a6860e93ddb11735c6919c8a3545c7b9c8d8"}, 825 | {file = "pycares-4.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:77cf5a2fd5583c670de41a7f4a7b46e5cbabe7180d8029f728571f4d2e864084"}, 826 | {file = "pycares-4.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3eaa6681c0a3e3f3868c77aca14b7760fed35fdfda2fe587e15c701950e7bc69"}, 827 | {file = "pycares-4.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ad58e284a658a8a6a84af2e0b62f2f961f303cedfe551854d7bd40c3cbb61912"}, 828 | {file = "pycares-4.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bfb89ca9e3d0a9b5332deeb666b2ede9d3469107742158f4aeda5ce032d003f4"}, 829 | {file = "pycares-4.4.0-cp38-cp38-win32.whl", hash = "sha256:f36bdc1562142e3695555d2f4ac0cb69af165eddcefa98efc1c79495b533481f"}, 830 | {file = "pycares-4.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:902461a92b6a80fd5041a2ec5235680c7cc35e43615639ec2a40e63fca2dfb51"}, 831 | {file = "pycares-4.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7bddc6adba8f699728f7fc1c9ce8cef359817ad78e2ed52b9502cb5f8dc7f741"}, 832 | {file = "pycares-4.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cb49d5805cd347c404f928c5ae7c35e86ba0c58ffa701dbe905365e77ce7d641"}, 833 | {file = "pycares-4.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56cf3349fa3a2e67ed387a7974c11d233734636fe19facfcda261b411af14d80"}, 834 | {file = "pycares-4.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bf2eaa83a5987e48fa63302f0fe7ce3275cfda87b34d40fef9ce703fb3ac002"}, 835 | {file = "pycares-4.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82bba2ab77eb5addbf9758d514d9bdef3c1bfe7d1649a47bd9a0d55a23ef478b"}, 836 | {file = "pycares-4.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c6a8bde63106f162fca736e842a916853cad3c8d9d137e11c9ffa37efa818b02"}, 837 | {file = "pycares-4.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f5f646eec041db6ffdbcaf3e0756fb92018f7af3266138c756bb09d2b5baadec"}, 838 | {file = "pycares-4.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9dc04c54c6ea615210c1b9e803d0e2d2255f87a3d5d119b6482c8f0dfa15b26b"}, 839 | {file = "pycares-4.4.0-cp39-cp39-win32.whl", hash = "sha256:97892cced5794d721fb4ff8765764aa4ea48fe8b2c3820677505b96b83d4ef47"}, 840 | {file = "pycares-4.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:917f08f0b5d9324e9a34211e68d27447c552b50ab967044776bbab7e42a553a2"}, 841 | {file = "pycares-4.4.0.tar.gz", hash = "sha256:f47579d508f2f56eddd16ce72045782ad3b1b3b678098699e2b6a1b30733e1c2"}, 842 | ] 843 | 844 | [package.dependencies] 845 | cffi = ">=1.5.0" 846 | 847 | [package.extras] 848 | idna = ["idna (>=2.1)"] 849 | 850 | [[package]] 851 | name = "pycparser" 852 | version = "2.22" 853 | description = "C parser in Python" 854 | optional = false 855 | python-versions = ">=3.8" 856 | files = [ 857 | {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, 858 | {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, 859 | ] 860 | 861 | [[package]] 862 | name = "pytest" 863 | version = "8.3.3" 864 | description = "pytest: simple powerful testing with Python" 865 | optional = false 866 | python-versions = ">=3.8" 867 | files = [ 868 | {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, 869 | {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, 870 | ] 871 | 872 | [package.dependencies] 873 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 874 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 875 | iniconfig = "*" 876 | packaging = "*" 877 | pluggy = ">=1.5,<2" 878 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 879 | 880 | [package.extras] 881 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 882 | 883 | [[package]] 884 | name = "pytest-asyncio" 885 | version = "0.24.0" 886 | description = "Pytest support for asyncio" 887 | optional = false 888 | python-versions = ">=3.8" 889 | files = [ 890 | {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, 891 | {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, 892 | ] 893 | 894 | [package.dependencies] 895 | pytest = ">=8.2,<9" 896 | 897 | [package.extras] 898 | docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] 899 | testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] 900 | 901 | [[package]] 902 | name = "requests" 903 | version = "2.32.3" 904 | description = "Python HTTP for Humans." 905 | optional = false 906 | python-versions = ">=3.8" 907 | files = [ 908 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 909 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 910 | ] 911 | 912 | [package.dependencies] 913 | certifi = ">=2017.4.17" 914 | charset-normalizer = ">=2,<4" 915 | idna = ">=2.5,<4" 916 | urllib3 = ">=1.21.1,<3" 917 | 918 | [package.extras] 919 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 920 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 921 | 922 | [[package]] 923 | name = "ruff" 924 | version = "0.6.9" 925 | description = "An extremely fast Python linter and code formatter, written in Rust." 926 | optional = false 927 | python-versions = ">=3.7" 928 | files = [ 929 | {file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"}, 930 | {file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"}, 931 | {file = "ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c"}, 932 | {file = "ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e"}, 933 | {file = "ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577"}, 934 | {file = "ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829"}, 935 | {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5"}, 936 | {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7"}, 937 | {file = "ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f"}, 938 | {file = "ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa"}, 939 | {file = "ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb"}, 940 | {file = "ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0"}, 941 | {file = "ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625"}, 942 | {file = "ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039"}, 943 | {file = "ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d"}, 944 | {file = "ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117"}, 945 | {file = "ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93"}, 946 | {file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"}, 947 | ] 948 | 949 | [[package]] 950 | name = "tomli" 951 | version = "2.0.2" 952 | description = "A lil' TOML parser" 953 | optional = false 954 | python-versions = ">=3.8" 955 | files = [ 956 | {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, 957 | {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, 958 | ] 959 | 960 | [[package]] 961 | name = "typing-extensions" 962 | version = "4.12.2" 963 | description = "Backported and Experimental Type Hints for Python 3.8+" 964 | optional = false 965 | python-versions = ">=3.8" 966 | files = [ 967 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 968 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 969 | ] 970 | 971 | [[package]] 972 | name = "urllib3" 973 | version = "2.2.3" 974 | description = "HTTP library with thread-safe connection pooling, file post, and more." 975 | optional = false 976 | python-versions = ">=3.8" 977 | files = [ 978 | {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, 979 | {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, 980 | ] 981 | 982 | [package.extras] 983 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 984 | h2 = ["h2 (>=4,<5)"] 985 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 986 | zstd = ["zstandard (>=0.18.0)"] 987 | 988 | [[package]] 989 | name = "yarl" 990 | version = "1.13.1" 991 | description = "Yet another URL library" 992 | optional = false 993 | python-versions = ">=3.8" 994 | files = [ 995 | {file = "yarl-1.13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:82e692fb325013a18a5b73a4fed5a1edaa7c58144dc67ad9ef3d604eccd451ad"}, 996 | {file = "yarl-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df4e82e68f43a07735ae70a2d84c0353e58e20add20ec0af611f32cd5ba43fb4"}, 997 | {file = "yarl-1.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec9dd328016d8d25702a24ee274932aebf6be9787ed1c28d021945d264235b3c"}, 998 | {file = "yarl-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5820bd4178e6a639b3ef1db8b18500a82ceab6d8b89309e121a6859f56585b05"}, 999 | {file = "yarl-1.13.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86c438ce920e089c8c2388c7dcc8ab30dfe13c09b8af3d306bcabb46a053d6f7"}, 1000 | {file = "yarl-1.13.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3de86547c820e4f4da4606d1c8ab5765dd633189791f15247706a2eeabc783ae"}, 1001 | {file = "yarl-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca53632007c69ddcdefe1e8cbc3920dd88825e618153795b57e6ebcc92e752a"}, 1002 | {file = "yarl-1.13.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4ee1d240b84e2f213565f0ec08caef27a0e657d4c42859809155cf3a29d1735"}, 1003 | {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c49f3e379177f4477f929097f7ed4b0622a586b0aa40c07ac8c0f8e40659a1ac"}, 1004 | {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5c5e32fef09ce101fe14acd0f498232b5710effe13abac14cd95de9c274e689e"}, 1005 | {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ab9524e45ee809a083338a749af3b53cc7efec458c3ad084361c1dbf7aaf82a2"}, 1006 | {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:b1481c048fe787f65e34cb06f7d6824376d5d99f1231eae4778bbe5c3831076d"}, 1007 | {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:31497aefd68036d8e31bfbacef915826ca2e741dbb97a8d6c7eac66deda3b606"}, 1008 | {file = "yarl-1.13.1-cp310-cp310-win32.whl", hash = "sha256:1fa56f34b2236f5192cb5fceba7bbb09620e5337e0b6dfe2ea0ddbd19dd5b154"}, 1009 | {file = "yarl-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:1bbb418f46c7f7355084833051701b2301092e4611d9e392360c3ba2e3e69f88"}, 1010 | {file = "yarl-1.13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:216a6785f296169ed52cd7dcdc2612f82c20f8c9634bf7446327f50398732a51"}, 1011 | {file = "yarl-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40c6e73c03a6befb85b72da213638b8aaa80fe4136ec8691560cf98b11b8ae6e"}, 1012 | {file = "yarl-1.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2430cf996113abe5aee387d39ee19529327205cda975d2b82c0e7e96e5fdabdc"}, 1013 | {file = "yarl-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fb4134cc6e005b99fa29dbc86f1ea0a298440ab6b07c6b3ee09232a3b48f495"}, 1014 | {file = "yarl-1.13.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:309c104ecf67626c033845b860d31594a41343766a46fa58c3309c538a1e22b2"}, 1015 | {file = "yarl-1.13.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f90575e9fe3aae2c1e686393a9689c724cd00045275407f71771ae5d690ccf38"}, 1016 | {file = "yarl-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d2e1626be8712333a9f71270366f4a132f476ffbe83b689dd6dc0d114796c74"}, 1017 | {file = "yarl-1.13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b66c87da3c6da8f8e8b648878903ca54589038a0b1e08dde2c86d9cd92d4ac9"}, 1018 | {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cf1ad338620249f8dd6d4b6a91a69d1f265387df3697ad5dc996305cf6c26fb2"}, 1019 | {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9915300fe5a0aa663c01363db37e4ae8e7c15996ebe2c6cce995e7033ff6457f"}, 1020 | {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:703b0f584fcf157ef87816a3c0ff868e8c9f3c370009a8b23b56255885528f10"}, 1021 | {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1d8e3ca29f643dd121f264a7c89f329f0fcb2e4461833f02de6e39fef80f89da"}, 1022 | {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7055bbade838d68af73aea13f8c86588e4bcc00c2235b4b6d6edb0dbd174e246"}, 1023 | {file = "yarl-1.13.1-cp311-cp311-win32.whl", hash = "sha256:a3442c31c11088e462d44a644a454d48110f0588de830921fd201060ff19612a"}, 1024 | {file = "yarl-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:81bad32c8f8b5897c909bf3468bf601f1b855d12f53b6af0271963ee67fff0d2"}, 1025 | {file = "yarl-1.13.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f452cc1436151387d3d50533523291d5f77c6bc7913c116eb985304abdbd9ec9"}, 1026 | {file = "yarl-1.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9cec42a20eae8bebf81e9ce23fb0d0c729fc54cf00643eb251ce7c0215ad49fe"}, 1027 | {file = "yarl-1.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d959fe96e5c2712c1876d69af0507d98f0b0e8d81bee14cfb3f6737470205419"}, 1028 | {file = "yarl-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8c837ab90c455f3ea8e68bee143472ee87828bff19ba19776e16ff961425b57"}, 1029 | {file = "yarl-1.13.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94a993f976cdcb2dc1b855d8b89b792893220db8862d1a619efa7451817c836b"}, 1030 | {file = "yarl-1.13.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b2442a415a5f4c55ced0fade7b72123210d579f7d950e0b5527fc598866e62c"}, 1031 | {file = "yarl-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fdbf0418489525231723cdb6c79e7738b3cbacbaed2b750cb033e4ea208f220"}, 1032 | {file = "yarl-1.13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b7f6e699304717fdc265a7e1922561b02a93ceffdaefdc877acaf9b9f3080b8"}, 1033 | {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bcd5bf4132e6a8d3eb54b8d56885f3d3a38ecd7ecae8426ecf7d9673b270de43"}, 1034 | {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2a93a4557f7fc74a38ca5a404abb443a242217b91cd0c4840b1ebedaad8919d4"}, 1035 | {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:22b739f99c7e4787922903f27a892744189482125cc7b95b747f04dd5c83aa9f"}, 1036 | {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2db874dd1d22d4c2c657807562411ffdfabec38ce4c5ce48b4c654be552759dc"}, 1037 | {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4feaaa4742517eaceafcbe74595ed335a494c84634d33961214b278126ec1485"}, 1038 | {file = "yarl-1.13.1-cp312-cp312-win32.whl", hash = "sha256:bbf9c2a589be7414ac4a534d54e4517d03f1cbb142c0041191b729c2fa23f320"}, 1039 | {file = "yarl-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:d07b52c8c450f9366c34aa205754355e933922c79135125541daae6cbf31c799"}, 1040 | {file = "yarl-1.13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:95c6737f28069153c399d875317f226bbdea939fd48a6349a3b03da6829fb550"}, 1041 | {file = "yarl-1.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cd66152561632ed4b2a9192e7f8e5a1d41e28f58120b4761622e0355f0fe034c"}, 1042 | {file = "yarl-1.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6a2acde25be0cf9be23a8f6cbd31734536a264723fca860af3ae5e89d771cd71"}, 1043 | {file = "yarl-1.13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a18595e6a2ee0826bf7dfdee823b6ab55c9b70e8f80f8b77c37e694288f5de1"}, 1044 | {file = "yarl-1.13.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a31d21089894942f7d9a8df166b495101b7258ff11ae0abec58e32daf8088813"}, 1045 | {file = "yarl-1.13.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45f209fb4bbfe8630e3d2e2052535ca5b53d4ce2d2026bed4d0637b0416830da"}, 1046 | {file = "yarl-1.13.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f722f30366474a99745533cc4015b1781ee54b08de73260b2bbe13316079851"}, 1047 | {file = "yarl-1.13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3bf60444269345d712838bb11cc4eadaf51ff1a364ae39ce87a5ca8ad3bb2c8"}, 1048 | {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:942c80a832a79c3707cca46bd12ab8aa58fddb34b1626d42b05aa8f0bcefc206"}, 1049 | {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:44b07e1690f010c3c01d353b5790ec73b2f59b4eae5b0000593199766b3f7a5c"}, 1050 | {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:396e59b8de7e4d59ff5507fb4322d2329865b909f29a7ed7ca37e63ade7f835c"}, 1051 | {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3bb83a0f12701c0b91112a11148b5217617982e1e466069d0555be9b372f2734"}, 1052 | {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c92b89bffc660f1274779cb6fbb290ec1f90d6dfe14492523a0667f10170de26"}, 1053 | {file = "yarl-1.13.1-cp313-cp313-win32.whl", hash = "sha256:269c201bbc01d2cbba5b86997a1e0f73ba5e2f471cfa6e226bcaa7fd664b598d"}, 1054 | {file = "yarl-1.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:1d0828e17fa701b557c6eaed5edbd9098eb62d8838344486248489ff233998b8"}, 1055 | {file = "yarl-1.13.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8be8cdfe20787e6a5fcbd010f8066227e2bb9058331a4eccddec6c0db2bb85b2"}, 1056 | {file = "yarl-1.13.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:08d7148ff11cb8e886d86dadbfd2e466a76d5dd38c7ea8ebd9b0e07946e76e4b"}, 1057 | {file = "yarl-1.13.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4afdf84610ca44dcffe8b6c22c68f309aff96be55f5ea2fa31c0c225d6b83e23"}, 1058 | {file = "yarl-1.13.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0d12fe78dcf60efa205e9a63f395b5d343e801cf31e5e1dda0d2c1fb618073d"}, 1059 | {file = "yarl-1.13.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298c1eecfd3257aa16c0cb0bdffb54411e3e831351cd69e6b0739be16b1bdaa8"}, 1060 | {file = "yarl-1.13.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c14c16831b565707149c742d87a6203eb5597f4329278446d5c0ae7a1a43928e"}, 1061 | {file = "yarl-1.13.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a9bacedbb99685a75ad033fd4de37129449e69808e50e08034034c0bf063f99"}, 1062 | {file = "yarl-1.13.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:658e8449b84b92a4373f99305de042b6bd0d19bf2080c093881e0516557474a5"}, 1063 | {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:373f16f38721c680316a6a00ae21cc178e3a8ef43c0227f88356a24c5193abd6"}, 1064 | {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:45d23c4668d4925688e2ea251b53f36a498e9ea860913ce43b52d9605d3d8177"}, 1065 | {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f7917697bcaa3bc3e83db91aa3a0e448bf5cde43c84b7fc1ae2427d2417c0224"}, 1066 | {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:5989a38ba1281e43e4663931a53fbf356f78a0325251fd6af09dd03b1d676a09"}, 1067 | {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:11b3ca8b42a024513adce810385fcabdd682772411d95bbbda3b9ed1a4257644"}, 1068 | {file = "yarl-1.13.1-cp38-cp38-win32.whl", hash = "sha256:dcaef817e13eafa547cdfdc5284fe77970b891f731266545aae08d6cce52161e"}, 1069 | {file = "yarl-1.13.1-cp38-cp38-win_amd64.whl", hash = "sha256:7addd26594e588503bdef03908fc207206adac5bd90b6d4bc3e3cf33a829f57d"}, 1070 | {file = "yarl-1.13.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a0ae6637b173d0c40b9c1462e12a7a2000a71a3258fa88756a34c7d38926911c"}, 1071 | {file = "yarl-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:576365c9f7469e1f6124d67b001639b77113cfd05e85ce0310f5f318fd02fe85"}, 1072 | {file = "yarl-1.13.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:78f271722423b2d4851cf1f4fa1a1c4833a128d020062721ba35e1a87154a049"}, 1073 | {file = "yarl-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d74f3c335cfe9c21ea78988e67f18eb9822f5d31f88b41aec3a1ec5ecd32da5"}, 1074 | {file = "yarl-1.13.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1891d69a6ba16e89473909665cd355d783a8a31bc84720902c5911dbb6373465"}, 1075 | {file = "yarl-1.13.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb382fd7b4377363cc9f13ba7c819c3c78ed97c36a82f16f3f92f108c787cbbf"}, 1076 | {file = "yarl-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c8854b9f80693d20cec797d8e48a848c2fb273eb6f2587b57763ccba3f3bd4b"}, 1077 | {file = "yarl-1.13.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbf2c3f04ff50f16404ce70f822cdc59760e5e2d7965905f0e700270feb2bbfc"}, 1078 | {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fb9f59f3848edf186a76446eb8bcf4c900fe147cb756fbbd730ef43b2e67c6a7"}, 1079 | {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ef9b85fa1bc91c4db24407e7c4da93a5822a73dd4513d67b454ca7064e8dc6a3"}, 1080 | {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:098b870c18f1341786f290b4d699504e18f1cd050ed179af8123fd8232513424"}, 1081 | {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:8c723c91c94a3bc8033dd2696a0f53e5d5f8496186013167bddc3fb5d9df46a3"}, 1082 | {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:44a4c40a6f84e4d5955b63462a0e2a988f8982fba245cf885ce3be7618f6aa7d"}, 1083 | {file = "yarl-1.13.1-cp39-cp39-win32.whl", hash = "sha256:84bbcdcf393139f0abc9f642bf03f00cac31010f3034faa03224a9ef0bb74323"}, 1084 | {file = "yarl-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:fc2931ac9ce9c61c9968989ec831d3a5e6fcaaff9474e7cfa8de80b7aff5a093"}, 1085 | {file = "yarl-1.13.1-py3-none-any.whl", hash = "sha256:6a5185ad722ab4dd52d5fb1f30dcc73282eb1ed494906a92d1a228d3f89607b0"}, 1086 | {file = "yarl-1.13.1.tar.gz", hash = "sha256:ec8cfe2295f3e5e44c51f57272afbd69414ae629ec7c6b27f5a410efc78b70a0"}, 1087 | ] 1088 | 1089 | [package.dependencies] 1090 | idna = ">=2.0" 1091 | multidict = ">=4.0" 1092 | 1093 | [metadata] 1094 | lock-version = "2.0" 1095 | python-versions = "^3.9" 1096 | content-hash = "0ee928b6044ff99d1fd6522e48131635092c41b487f890656143a2dd7135d74f" 1097 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "opa-python-client" 3 | version = "2.0.2" 4 | description = "Client for connection to the OPA service" 5 | authors = ["Tural Muradov "] 6 | license = "MIT" 7 | readme = "README.md" 8 | homepage = "https://github.com/Turall/OPA-python-client" 9 | repository = "https://github.com/Turall/OPA-python-client" 10 | classifiers = [ 11 | "Intended Audience :: Developers", 12 | "Programming Language :: Python", 13 | "Programming Language :: Python :: 3", 14 | "Programming Language :: Python :: 3.9", 15 | "Programming Language :: Python :: 3 :: Only", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | ] 19 | packages = [ 20 | {include = "opa_client"} 21 | ] 22 | 23 | [tool.poetry.dependencies] 24 | python = "^3.9" 25 | requests = "^2.32.3" 26 | aiohttp = {extras = ["speedups"], version = "^3.10.9"} 27 | aiofiles = "^24.1.0" 28 | 29 | [tool.poetry.group.dev.dependencies] 30 | pytest = "^8.3.3" 31 | pytest-asyncio = "^0.24.0" 32 | ruff = "^0.6.9" 33 | 34 | [build-system] 35 | requires = ["poetry-core>=1.0.0"] 36 | build-backend = "poetry.core.masonry.api" 37 | 38 | 39 | [tool.ruff] 40 | line-length = 79 41 | exclude = [".venv","tests","migrations"] 42 | 43 | [tool.ruff.format] 44 | indent-style = "tab" 45 | docstring-code-format = true 46 | --------------------------------------------------------------------------------