diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..c5523ef --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,43 @@ +name: website + +# build the documentation whenever there are new commits on main +on: + push: + branches: + - main + +# security: restrict permissions for CI jobs. +permissions: + contents: read + +jobs: + # Build the documentation and upload the static HTML files as an artifact. + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + - run: pip install -e . + - run: pip install pdoc + - run: pdoc -d google pyoutlineapi --output-dir docs + + - uses: actions/upload-pages-artifact@v3 + with: + path: docs/ + + # Deploy the artifact to GitHub pages. + # This is a separate job so that only actions/deploy-pages has the necessary permissions. + deploy: + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.github/workflows/python_tests.yml b/.github/workflows/python_tests.yml index 1b3d5b1..d7e81d5 100644 --- a/.github/workflows/python_tests.yml +++ b/.github/workflows/python_tests.yml @@ -5,38 +5,88 @@ on: branches: [ "main", "development" ] pull_request: branches: [ "main" ] + schedule: + - cron: '0 0 * * 0' # Weekly security scan permissions: contents: read + pull-requests: write + security-events: write # Required for security findings jobs: - build: + test: + name: Run Tests runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: [ "3.10", "3.11", "3.12", "3.13" ] + steps: - uses: actions/checkout@v4 - - name: Set up Python 3.12 - uses: actions/setup-python@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: Install Poetry - run: | - curl -sSL https://install.python-poetry.org | python3 - - echo "export PATH=$HOME/.local/bin:$PATH" >> $GITHUB_ENV + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v4 + with: + path: .venv + key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }} + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction + + - name: Run tests run: | - poetry install - - name: Install flake8 - run: | - poetry run pip install flake8 - - name: Lint with flake8 + poetry run pytest --cov=./ --cov-report=xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + fail_ci_if_error: true + + security: + name: Security Checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + cache: 'pip' + + - name: Install security tools run: | - poetry run flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - poetry run flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with pytest + python -m pip install safety + + - name: Run safety check run: | - poetry run pytest --cov - - name: Upload results to Codecov - uses: codecov/codecov-action@v4 + safety check + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: python + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 with: - token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file + category: "/language:python" \ No newline at end of file diff --git a/.gitignore b/.gitignore index bd51b7a..4c2e18c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,21 +7,28 @@ __pycache__/ dist/ build/ *.egg-info/ +MANIFEST # C extensions *.so -# Distribution / packaging +# Virtual environment .Python env/ venv/ ENV/ +.env/ +.venv/ env.bak/ venv.bak/ +pythonenv* +.python-version + +# Package files *.egg +*.whl # PyInstaller -# Usually these files are written by a python script from a .spec file *.manifest *.spec @@ -30,56 +37,99 @@ pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ .coverage +.coverage.* coverage.xml *.cover *.py,cover +.pytest_cache/ nosetests.xml test_*.xml -*.tox/ -*.nox/ -*.coverage -*.hypothesis/ -*.pytest_cache/ - -# Pytest -.cache +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django *.log +local_settings.py +db.sqlite3 +db.sqlite3-journal -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json +# Flask +instance/ +.webassets-cache -# IDEs and editors -.vscode/ -.idea/ -*.swp -*.swo +# Scrapy +.scrapy + +# Sphinx documentation +docs/_build/ +doc/_build/ + +# PyBuilder +target/ # Jupyter Notebook .ipynb_checkpoints +*.ipynb -# Pyre type checker -.pyre/ +# IPython +profile_default/ +ipython_config.py -# Virtual environment -.venv/ +# pyenv + +# Celery +celerybeat-schedule +celerybeat.pid +# SageMath +*.sage.py -# Environment variables +# Environments .env .env.* +.venv -# macOS -.DS_Store +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre +.pyre/ -# Windows +# pytype +.pytype/ + +# Cython debug symbols +cython_debug/ + +# IDE settings +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db Thumbs.db Desktop.ini -# Miscellaneous -*.orig -*.bak -*.tmp -/main.py +# Project specific +logs/ +tmp/ +temp/ + +main.py \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7d25659 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,55 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.2.0] - 2024-01-10 + +### Added + +- New asynchronous client `AsyncOutlineClient` using `aiohttp` +- Comprehensive type hints and overloads for better IDE support +- New methods for server management: + - `rename_server()` - Change server name + - `set_hostname()` - Configure server hostname + - `get_metrics_status()` - Check metrics collection status + - `set_metrics_status()` - Enable/disable metrics collection +- Support for different metrics periods (DAILY, WEEKLY, MONTHLY) +- Extended options for access key creation (method, encryption settings) +- Improved error handling with detailed error messages +- Context manager support with async `__aenter__` and `__aexit__` + +### Changed + +- Complete rewrite of the client to support asynchronous operations +- Enhanced error hierarchy with `OutlineError` base class +- Improved request handling with automatic session management +- More flexible SSL/TLS certificate verification +- Better JSON response parsing and validation +- Updated type annotations to use modern Python typing features + +### Removed + +- Synchronous client implementation (migrated to async) +- Direct requests-based HTTP handling + +## [0.1.2] - 2024-01-09 + +### Added + +- Initial release with synchronous client +- Basic Outline VPN server management features: + - Server information retrieval + - Access key management (create, list, delete) + - Data limit management + - Server port configuration + - Basic metrics retrieval +- Pydantic models for data validation +- Support for custom certificate verification +- Optional JSON response format + +[0.2.0]: https://github.com/username/repo/compare/v0.1.2...v0.2.0 + +[0.1.2]: https://github.com/username/repo/releases/tag/v0.1.2 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 547cf05..26f9945 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ # Contributing to PyOutlineAPI Thank you for considering contributing to PyOutlineAPI! Whether you have suggestions, bug reports, or code improvements, -your input is valuable. Here are some guidelines to help you contribute effectively: +your input is valuable. ## How to Contribute @@ -9,87 +9,204 @@ your input is valuable. Here are some guidelines to help you contribute effectiv If you encounter any issues or bugs, please follow these steps: -1. **Search for Existing Issues**: Check the [Issues](https://github.com/orenlab/pyoutlineapi/issues) section to see - if your issue has already been reported. +1. **Search for Existing Issues**: Check the [Issues](https://github.com/orenlab/pyoutlineapi/issues) section to see if + your issue has already been reported. 2. **Create a New Issue**: If you don't find an existing - issue, [open a new issue](https://github.com/orenlab/pyoutlineapi/issues/new) with a clear description of the - problem. Include: + issue, [open a new issue](https://github.com/orenlab/pyoutlineapi/issues/new) with: - A descriptive title - Steps to reproduce the issue - Expected and actual results - - Any relevant code snippets or logs + - Python version (3.10+) and PyOutlineAPI version + - Any relevant code snippets or error messages + - Environment details (OS, Outline server version) ### Suggesting Enhancements -If you have an idea for a new feature or improvement: +For new feature or improvement suggestions: -1. **Check Existing Feature Requests**: Look through the [Issues](https://github.com/orenlab/pyoutlineapi/issues) to - see if a similar feature has already been suggested. +1. **Check Existing Feature Requests**: Review the [Issues](https://github.com/orenlab/pyoutlineapi/issues) to see if + similar features have been suggested. 2. **Open a New Feature Request**: [Submit a new feature request](https://github.com/orenlab/pyoutlineapi/issues/new) with: - A descriptive title - - A detailed description of the proposed feature + - Detailed description of the proposed feature - Use cases and benefits - - Any related documentation or examples + - Example code or API design if applicable ### Contributing Code To contribute code: -1. **Fork the Repository**: Create a fork of the repository on GitHub. -2. **Clone Your Fork**: Clone your fork locally. +1. **Fork and Clone**: ```bash git clone https://github.com/orenlab/pyoutlineapi.git + cd pyoutlineapi ``` -3. **Create a New Branch**: Create a new branch for your changes. + +2. **Set Up Development Environment**: ```bash - git checkout -b my-feature-branch + # Install Poetry if you haven't already + curl -sSL https://install.python-poetry.org | python3 - + + # Install dependencies + poetry install + + # Activate virtual environment + poetry shell ``` -4. **Make Your Changes**: Implement your changes, ensuring to follow existing code styles and conventions. -5. **Write Tests**: Add or update tests to cover your changes. -6. **Run Tests**: Ensure all tests pass before submitting a pull request. -7. **Commit and Push**: Commit your changes and push them to your fork. + +3. **Create a Feature Branch**: ```bash - git add . - git commit -m "Add new feature or fix bug" - git push origin my-feature-branch + git checkout -b feature/your-feature-name + # or + git checkout -b fix/issue-description ``` -8. **Submit a Pull Request**: Open a pull request from your branch to the `main` branch of the original repository. - Provide a clear description of your changes and any relevant details. -## Code of Conduct +4. **Make Your Changes**: + - Follow the existing code structure + - Use type hints consistently (Python 3.10+ typing features) + - Add docstrings with examples (see existing code) + - Update tests if needed -Please adhere to our [Code of Conduct](CODE_OF_CONDUCT.md) in all interactions. Respectful and constructive -communication is essential for a positive and productive community. +5. **Test Your Changes**: + ```bash + # Run tests with coverage + poetry run pytest -## Style Guide + # Type checking + poetry run mypy pyoutlineapi -Follow these guidelines to ensure consistency in the codebase: + # Code formatting + poetry run black pyoutlineapi tests -- **Code Style**: Adhere to [PEP 8](https://pep8.org/) for Python code style. -- **Documentation**: Update documentation as needed to reflect code changes. Use clear, concise language and proper - formatting. -- **Commit Messages**: Write clear and descriptive commit messages. Use the following format: - ``` - [type]: [short summary] + # Linting + poetry run flake8 pyoutlineapi tests + ``` - [longer description, if necessary] +6. **Submit a Pull Request**: + - Write a clear PR description + - Link related issues + - Include any necessary documentation updates + +## Code Style Guidelines + +We follow strict coding standards to maintain consistency: + +### Python Style + +- Follow [PEP 8](https://pep8.org/) conventions +- Use modern type hints (Python 3.10+) +- Maximum line length: 88 characters (Black default) +- Use descriptive variable names + +### Documentation + +- Use Google-style docstrings with type information +- For non-private methods, include examples in docstrings (see existing code) +- Example: + ```python + async def create_access_key( + self, + *, + name: Optional[str] = None, + port: Optional[int] = None, + ) -> Union[JsonDict, AccessKey]: + """ + Create a new access key. + + Args: + name: Optional key name + port: Optional port number (1-65535) + + Returns: + New access key details + + Examples: + >>> async with AsyncOutlineClient(...) as client: + ... key = await client.create_access_key(name="User 1") + ... print(f"Created key: {key.access_url}") + """ ``` - Types of commits might include: - - `feat`: A new feature - - `fix`: A bug fix - - `docs`: Documentation changes - - `style`: Code style improvements (non-functional changes) - - `refactor`: Code refactoring (no functional changes) - - `test`: Adding or updating tests - - `chore`: Other changes (e.g., build process, CI configuration) +### Testing + +- Write unit tests for new features +- Use pytest fixtures and parametrize when appropriate +- Mock external dependencies +- Maintain high test coverage (enforced by pytest-cov) + +### Commit Messages + +Follow the [Conventional Commits](https://www.conventionalcommits.org/) specification: + +``` +(): + +[optional body] + +[optional footer] +``` + +Types: + +- `feat`: New feature +- `fix`: Bug fix +- `docs`: Documentation changes +- `style`: Code style/formatting changes +- `refactor`: Code refactoring +- `test`: Adding/updating tests +- `chore`: Maintenance tasks + +Example: + +``` +feat(client): add support for custom encryption methods + +- Added method parameter to create_access_key +- Updated documentation with examples +- Added unit tests for new functionality + +Closes #123 +``` + +## Development Setup + +1. **Required Dependencies**: + - Python 3.10 or higher + - Poetry for package management + - Outline server (for integration testing) + +2. **Development Tools**: + All development dependencies are managed by Poetry and include: + - pytest-cov for test coverage + - black for code formatting + - mypy for type checking + - flake8 for linting + +3. **Environment Variables for Testing**: + ```bash + OUTLINE_API_URL=https://your-server:port/secret + OUTLINE_CERT_SHA256=your-cert-fingerprint + ``` + +## Project Configuration + +Key project settings are managed in `pyproject.toml`, including: + +- Python version requirement (3.10+) +- Dependencies: + - pydantic (^2.9.2) + - aiohttp (^3.11.11) +- Development dependencies for testing and code quality +- Pytest configuration with coverage reporting ## Contact -For any questions or additional information, feel free to reach out: +- **Issues**: [GitHub Issues](https://github.com/orenlab/pyoutlineapi/issues) +- **Email**: `pytelemonbot@mail.ru` + +## License -- **Email**: pytelemonbot@mail.ru -- **GitHub Issues**: [Link](https://github.com/orenlab/pyoutlineapi/issues) +By contributing, you agree that your contributions will be licensed under the MIT License. Thank you for contributing to PyOutlineAPI! \ No newline at end of file diff --git a/README.md b/README.md index 9d0a92d..b5c6af6 100644 --- a/README.md +++ b/README.md @@ -1,129 +1,183 @@ # PyOutlineAPI -`pyoutlineapi` is a Python package designed to interact with the Outline VPN Server API, providing robust data -validation through Pydantic models. This ensures reliable and secure API interactions, making it an excellent choice for -integrating with bots and other automated systems that require accurate and secure communication. +A modern, async-first Python client for the Outline VPN Server API with comprehensive data validation through Pydantic +models. [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=orenlab_pyoutlineapi&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=orenlab_pyoutlineapi&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi) [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=orenlab_pyoutlineapi&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi) [![tests](https://github.com/orenlab/pyoutlineapi/actions/workflows/python_tests.yml/badge.svg)](https://github.com/orenlab/pyoutlineapi/actions/workflows/python_tests.yml) +[![codecov](https://codecov.io/gh/orenlab/pyoutlineapi/branch/main/graph/badge.svg?token=D0MPKCKFJQ)](https://codecov.io/gh/orenlab/pyoutlineapi) +![PyPI - Downloads](https://img.shields.io/pypi/dm/pyoutlineapi) ## Features -- **Server Management**: Retrieve server information, update hostnames, manage ports, and more. -- **Access Key Management**: Create, list, rename, and delete access keys, as well as set data limits. -- **Metrics**: Enable or disable metrics sharing and retrieve data transfer metrics. -- **Experimental Endpoints**: Access and manage experimental features of the Outline Server API. - -## Quick Start - -To get started with `pyoutlineapi`, follow these steps: - -1. Install the package using pip or Poetry. -2. Initialize the `PyOutlineWrapper` client with your Outline VPN server URL and certificate fingerprint. -3. Use the provided methods to interact with the server and access keys. - -See the examples below for more detailed information. +- **Async-First Design**: Built with modern async/await patterns for optimal performance +- **Type Safety**: Full typing support with runtime validation via Pydantic +- **Comprehensive API Coverage**: Support for all Outline VPN Server + API [endpoints](https://github.com/Jigsaw-Code/outline-server/blob/master/src/shadowbox/server/api.yml) +- **Error Handling**: Robust error handling with custom exception types +- **SSL/TLS Security**: Certificate fingerprint verification for enhanced security +- **Flexible Response Format**: Choose between Pydantic models or JSON responses +- **Data Transfer Metrics**: Built-in support for monitoring server and key usage +- **Context Manager Support**: Clean resource management with async context managers ## Installation -You can install PyOutlineAPI via [PyPI](https://pypi.org/project/pyoutlineapi/) using pip: +Install via pip: ```bash pip install pyoutlineapi ``` -Or via [Poetry](https://python-poetry.org/): +Or using Poetry: ```bash poetry add pyoutlineapi ``` -## Basic Operations +## Quick Start -### Initialize the Client +Here's a simple example to get you started: ```python -from pyoutlineapi.client import PyOutlineWrapper -from pyoutlineapi.models import DataLimit - -# Initialize the API client -api_url = "https://your-outline-url.com" -cert_sha256 = "your-cert-sha256-fingerprint" -# Set "verify_tls" to False if using a self-signed certificate. -# Set "json_format" to True if answers need to be returned in JSON format. Defaults to False - Pydantic models will be returned. -api_client = PyOutlineWrapper(api_url=api_url, cert_sha256=cert_sha256, verify_tls=False, json_format=True) +import asyncio +from pyoutlineapi import AsyncOutlineClient + + +async def main(): + async with AsyncOutlineClient( + api_url="https://your-outline-server:port/api", + cert_sha256="your-certificate-fingerprint" + ) as client: + # Get server info + server = await client.get_server_info() + print(f"Connected to {server.name} running version {server.version}") + + # Create a new access key + key = await client.create_access_key(name="TestUser") + print(f"Created key: {key.access_url}") + + +if __name__ == "__main__": + asyncio.run(main()) ``` -### Retrieve Server Information +## Detailed Usage + +### Client Configuration + +The client can be configured with several options: ```python -server_info = api_client.get_server_info() -print("Server Information:", server_info) +from pyoutlineapi import AsyncOutlineClient + +client = AsyncOutlineClient( + api_url="https://your-outline-server:port/api", + cert_sha256="your-certificate-fingerprint", + json_format=True, # Return JSON instead of Pydantic models + timeout=30.0 # Request timeout in seconds +) ``` -### Create a New Access Key +### Managing Access Keys + +Create and manage access keys: ```python -# Create a new access key with default values -new_access_key = api_client.create_access_key() -# Create a new access key with custom values -new_access_key = api_client.create_access_key(name="my_access_key", password="secure_password", port=8080) -print("New Access Key:", new_access_key) -``` +from pyoutlineapi import AsyncOutlineClient, DataLimit -### Delete Access Key -```python -success = api_client.delete_access_key("example-key-id") -print("Access Key Deleted Successfully" if success else "Failed to Delete Access Key") +async def manage_keys(): + async with AsyncOutlineClient(...) as client: + # Create a key with data limit + key = await client.create_access_key( + name="Limited User", + port=8388, + limit=DataLimit(bytes=5 * 1024 ** 3) # 5 GB limit + ) + + # List all keys + keys = await client.get_access_keys() + for key in keys.access_keys: + print(f"Key {key.id}: {key.name or 'unnamed'}") + + # Modify a key + await client.rename_access_key(1, "New Name") + await client.set_access_key_data_limit(1, 10 * 1024 ** 3) # 10 GB + + # Delete a key + await client.delete_access_key(1) ``` -## Additional Functions +### Server Management -### Update Server Port +Configure server settings: ```python -update_success = api_client.update_server_port(9090) -print("Server Port Updated:", update_success) + +from pyoutlineapi import AsyncOutlineClient + + +async def configure_server(): + async with AsyncOutlineClient(...) as client: + # Update server name + await client.rename_server("My VPN Server") + + # Set hostname for access keys + await client.set_hostname("vpn.example.com") + + # Configure default port for new keys + await client.set_default_port(8388) ``` -### Set Data Limit for an Access Key +### Metrics Collection + +Monitor server usage: ```python -data_limit = api_client.set_access_key_data_limit("example-key-id", DataLimit(bytes=50000000)) -print("Data Limit Set:", data_limit) +from pyoutlineapi import AsyncOutlineClient, MetricsPeriod + + +async def get_metrics(): + async with AsyncOutlineClient(...) as client: + # Enable metrics collection + await client.set_metrics_status(True) + + # Get transfer metrics + metrics = await client.get_transfer_metrics(MetricsPeriod.MONTHLY) + for user_id, bytes_transferred in metrics.bytes_transferred_by_user_id.items(): + print(f"User {user_id}: {bytes_transferred / 1024 ** 3:.2f} GB") ``` -### Retrieve Metrics +## Error Handling + +The client provides custom exceptions for different error scenarios: ```python -metrics_data = api_client.get_metrics() -print("Metrics Data:", metrics_data) -``` +from pyoutlineapi import AsyncOutlineClient, OutlineError, APIError -## Contributing -We welcome contributions to PyOutlineAPI! Please follow the guidelines outlined in -the [CONTRIBUTING.md](https://github.com/orenlab/pyoutlineapi/blob/main/CONTRIBUTING.md) file. +async def handle_errors(): + try: + async with AsyncOutlineClient(...) as client: + await client.get_server_info() + except APIError as e: + print(f"API error: {e}") + except OutlineError as e: + print(f"Client error: {e}") +``` -## License +## Contributing -PyOutlineAPI is licensed under the MIT [License](https://github.com/orenlab/pyoutlineapi/blob/main/LICENSE). See the -LICENSE file for more details. +We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details on how to submit pull +requests, report issues, and contribute to the project. -## Frequently Asked Questions (FAQ) +## Security -___ -**How to use self-signed certificates?** +If you discover any security-related issues, please email `pytelemonbot@mail.ru` instead of using the issue tracker. -Set the `verify_tls` parameter to `False` when initializing the client. -___ -___ -**How to change the response format to Pydantic models?** +## License -Set the `json_format` parameter to `False` when initializing the client if you need to receive responses in Pydantic -models. -___ \ No newline at end of file +PyOutlineAPI is open-sourced software licensed under the [MIT license](LICENSE). \ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..f272e85 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/docs/pyoutlineapi.html b/docs/pyoutlineapi.html new file mode 100644 index 0000000..6ce28e0 --- /dev/null +++ b/docs/pyoutlineapi.html @@ -0,0 +1,2954 @@ + + + + + + + pyoutlineapi API documentation + + + + + + + + + +
+
+

+pyoutlineapi

+ +

PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API.

+ +

Copyright (c) 2025 Denis Rozhnovskiy pytelemonbot@mail.ru +All rights reserved.

+ +

This software is licensed under the MIT License.

+ +
You can find the full license text at:
+ +
+

https://opensource.org/licenses/MIT

+
+ +
Source code repository:
+ +
+

https://github.com/orenlab/pyoutlineapi

+
+
+ + + + + +
 1"""
+ 2PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API.
+ 3
+ 4Copyright (c) 2025 Denis Rozhnovskiy <pytelemonbot@mail.ru>
+ 5All rights reserved.
+ 6
+ 7This software is licensed under the MIT License.
+ 8You can find the full license text at:
+ 9    https://opensource.org/licenses/MIT
+10
+11Source code repository:
+12    https://github.com/orenlab/pyoutlineapi
+13"""
+14
+15import sys
+16from typing import TYPE_CHECKING
+17
+18if sys.version_info < (3, 10):
+19    raise RuntimeError("PyOutlineAPI requires Python 3.10 or higher")
+20
+21from .client import AsyncOutlineClient, OutlineError, APIError
+22
+23if TYPE_CHECKING:
+24    from .models import (
+25        AccessKey,
+26        AccessKeyCreateRequest,
+27        AccessKeyList,
+28        DataLimit,
+29        ErrorResponse,
+30        ExperimentalMetrics,
+31        MetricsPeriod,
+32        MetricsStatusResponse,
+33        Server,
+34        ServerMetrics,
+35    )
+36
+37__version__: str = "0.2.0"
+38__author__ = "Denis Rozhnovskiy"
+39__email__ = "pytelemonbot@mail.ru"
+40__license__ = "MIT"
+41
+42PUBLIC_API = [
+43    "AsyncOutlineClient",
+44    "OutlineError",
+45    "APIError",
+46    "AccessKey",
+47    "AccessKeyCreateRequest",
+48    "AccessKeyList",
+49    "DataLimit",
+50    "ErrorResponse",
+51    "ExperimentalMetrics",
+52    "MetricsPeriod",
+53    "MetricsStatusResponse",
+54    "Server",
+55    "ServerMetrics",
+56]
+57
+58__all__ = PUBLIC_API
+59
+60# Actual imports for runtime
+61from .models import (
+62    AccessKey,
+63    AccessKeyCreateRequest,
+64    AccessKeyList,
+65    DataLimit,
+66    ErrorResponse,
+67    ExperimentalMetrics,
+68    MetricsPeriod,
+69    MetricsStatusResponse,
+70    Server,
+71    ServerMetrics,
+72)
+
+ + +
+
+ +
+ + class + AsyncOutlineClient: + + + +
+ +
 72class AsyncOutlineClient:
+ 73    """
+ 74    Asynchronous client for the Outline VPN Server API.
+ 75
+ 76    Args:
+ 77        api_url: Base URL for the Outline server API
+ 78        cert_sha256: SHA-256 fingerprint of the server's TLS certificate
+ 79        json_format: Return raw JSON instead of Pydantic models
+ 80        timeout: Request timeout in seconds
+ 81
+ 82    Examples:
+ 83        >>> async def doo_something():
+ 84        ...     async with AsyncOutlineClient(
+ 85        ...         "https://example.com:1234/secret",
+ 86        ...         "ab12cd34..."
+ 87        ...     ) as client:
+ 88        ...         server_info = await client.get_server_info()
+ 89    """
+ 90
+ 91    def __init__(
+ 92            self,
+ 93            api_url: str,
+ 94            cert_sha256: str,
+ 95            *,
+ 96            json_format: bool = True,
+ 97            timeout: float = 30.0,
+ 98    ) -> None:
+ 99        self._api_url = api_url.rstrip("/")
+100        self._cert_sha256 = cert_sha256
+101        self._json_format = json_format
+102        self._timeout = aiohttp.ClientTimeout(total=timeout)
+103        self._ssl_context: Optional[Fingerprint] = None
+104        self._session: Optional[aiohttp.ClientSession] = None
+105
+106    async def __aenter__(self) -> AsyncOutlineClient:
+107        """Set up client session for context manager."""
+108        self._session = aiohttp.ClientSession(
+109            timeout=self._timeout,
+110            raise_for_status=False,
+111            connector=aiohttp.TCPConnector(ssl=self._get_ssl_context()),
+112        )
+113        return self
+114
+115    async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
+116        """Clean up client session."""
+117        if self._session:
+118            await self._session.close()
+119            self._session = None
+120
+121    @overload
+122    async def _parse_response(
+123            self,
+124            response: ClientResponse,
+125            model: type[BaseModel],
+126            json_format: Literal[True],
+127    ) -> JsonDict:
+128        ...
+129
+130    @overload
+131    async def _parse_response(
+132            self,
+133            response: ClientResponse,
+134            model: type[BaseModel],
+135            json_format: Literal[False],
+136    ) -> BaseModel:
+137        ...
+138
+139    @overload
+140    async def _parse_response(
+141            self, response: ClientResponse, model: type[BaseModel], json_format: bool
+142    ) -> Union[JsonDict, BaseModel]:
+143        ...
+144
+145    @ensure_context
+146    async def _parse_response(
+147            self, response: ClientResponse, model: type[BaseModel], json_format: bool = True
+148    ) -> ResponseType:
+149        """
+150        Parse and validate API response data.
+151
+152        Args:
+153            response: API response to parse
+154            model: Pydantic model for validation
+155            json_format: Whether to return raw JSON
+156
+157        Returns:
+158            Validated response data
+159
+160        Raises:
+161            ValueError: If response validation fails
+162        """
+163        try:
+164            data = await response.json()
+165            validated = model.model_validate(data)
+166            return validated.model_dump() if json_format else validated
+167        except aiohttp.ContentTypeError as e:
+168            raise ValueError("Invalid response format") from e
+169        except Exception as e:
+170            raise ValueError(f"Validation error: {e}") from e
+171
+172    @staticmethod
+173    async def _handle_error_response(response: ClientResponse) -> None:
+174        """Handle error responses from the API."""
+175        try:
+176            error_data = await response.json()
+177            error = ErrorResponse.model_validate(error_data)
+178            raise APIError(f"{error.code}: {error.message}", response.status)
+179        except ValueError:
+180            raise APIError(
+181                f"HTTP {response.status}: {response.reason}", response.status
+182            )
+183
+184    @ensure_context
+185    async def _request(
+186            self,
+187            method: str,
+188            endpoint: str,
+189            *,
+190            json: Any = None,
+191            params: Optional[dict[str, Any]] = None,
+192    ) -> Any:
+193        """Make an API request."""
+194        url = self._build_url(endpoint)
+195
+196        async with self._session.request(
+197                method,
+198                url,
+199                json=json,
+200                params=params,
+201                raise_for_status=False,
+202        ) as response:
+203            if response.status >= 400:
+204                await self._handle_error_response(response)
+205
+206            if response.status == 204:
+207                return True
+208
+209            try:
+210                await response.json()
+211                return response
+212            except aiohttp.ContentTypeError:
+213                return await response.text()
+214            except Exception as e:
+215                raise APIError(f"Failed to parse response: {e}", response.status)
+216
+217    def _build_url(self, endpoint: str) -> str:
+218        """Build and validate the full URL for the API request."""
+219        if not isinstance(endpoint, str):
+220            raise ValueError("Endpoint must be a string")
+221
+222        url = f"{self._api_url}/{endpoint.lstrip('/')}"
+223        parsed_url = urlparse(url)
+224
+225        if not all([parsed_url.scheme, parsed_url.netloc]):
+226            raise ValueError(f"Invalid URL: {url}")
+227
+228        return url
+229
+230    def _get_ssl_context(self) -> Optional[Fingerprint]:
+231        """Create an SSL context if a certificate fingerprint is provided."""
+232        if not self._cert_sha256:
+233            return None
+234
+235        try:
+236            return Fingerprint(binascii.unhexlify(self._cert_sha256))
+237        except binascii.Error as e:
+238            raise ValueError(f"Invalid certificate SHA256: {self._cert_sha256}") from e
+239        except Exception as e:
+240            raise OutlineError("Failed to create SSL context") from e
+241
+242    async def get_server_info(self) -> Union[JsonDict, Server]:
+243        """
+244        Get server information.
+245
+246        Returns:
+247            Server information including name, ID, and configuration.
+248
+249        Examples:
+250            >>> async def doo_something():
+251            ...     async with AsyncOutlineClient(
+252            ...         "https://example.com:1234/secret",
+253            ...         "ab12cd34..."
+254            ...     ) as client:
+255            ...         server = await client.get_server_info()
+256            ...         print(f"Server {server.name} running version {server.version}")
+257        """
+258        response = await self._request("GET", "server")
+259        return await self._parse_response(
+260            response, Server, json_format=self._json_format
+261        )
+262
+263    async def rename_server(self, name: str) -> bool:
+264        """
+265        Rename the server.
+266
+267        Args:
+268            name: New server name
+269
+270        Returns:
+271            True if successful
+272
+273        Examples:
+274            >>> async def doo_something():
+275            ...     async with AsyncOutlineClient(
+276            ...         "https://example.com:1234/secret",
+277            ...         "ab12cd34..."
+278            ...     ) as client:
+279            ...     success = await client.rename_server("My VPN Server")
+280            ...     if success:
+281            ...         print("Server renamed successfully")
+282        """
+283        return await self._request("PUT", "name", json={"name": name})
+284
+285    async def set_hostname(self, hostname: str) -> bool:
+286        """
+287        Set server hostname for access keys.
+288
+289        Args:
+290            hostname: New hostname or IP address
+291
+292        Returns:
+293            True if successful
+294
+295        Raises:
+296            APIError: If hostname is invalid
+297
+298        Examples:
+299            >>> async def doo_something():
+300            ...     async with AsyncOutlineClient(
+301            ...         "https://example.com:1234/secret",
+302            ...         "ab12cd34..."
+303            ...     ) as client:
+304            ...         await client.set_hostname("vpn.example.com")
+305            ...         # Or use IP address
+306            ...         await client.set_hostname("203.0.113.1")
+307        """
+308        return await self._request(
+309            "PUT", "server/hostname-for-access-keys", json={"hostname": hostname}
+310        )
+311
+312    async def set_default_port(self, port: int) -> bool:
+313        """
+314        Set default port for new access keys.
+315
+316        Args:
+317            port: Port number (1025-65535)
+318
+319        Returns:
+320            True if successful
+321
+322        Raises:
+323            APIError: If port is invalid or in use
+324
+325        Examples:
+326            >>> async def doo_something():
+327            ...     async with AsyncOutlineClient(
+328            ...         "https://example.com:1234/secret",
+329            ...         "ab12cd34..."
+330            ...     ) as client:
+331            ...         await client.set_default_port(8388)
+332
+333        """
+334        if port < 1025 or port > 65535:
+335            raise ValueError("Privileged ports are not allowed. Use range: 1025-65535")
+336
+337        return await self._request(
+338            "PUT", "server/port-for-new-access-keys", json={"port": port}
+339        )
+340
+341    async def get_metrics_status(self) -> dict[str, Any] | BaseModel:
+342        """
+343        Get whether metrics collection is enabled.
+344
+345        Returns:
+346            Current metrics collection status
+347
+348        Examples:
+349            >>> async def doo_something():
+350            ...     async with AsyncOutlineClient(
+351            ...         "https://example.com:1234/secret",
+352            ...         "ab12cd34..."
+353            ...     ) as client:
+354            ...         if await client.get_metrics_status():
+355            ...             print("Metrics collection is enabled")
+356        """
+357        response = await self._request("GET", "metrics/enabled")
+358        data = await self._parse_response(
+359            response, MetricsStatusResponse, json_format=self._json_format
+360        )
+361        return data
+362
+363    async def set_metrics_status(self, enabled: bool) -> bool:
+364        """
+365        Enable or disable metrics collection.
+366
+367        Args:
+368            enabled: Whether to enable metrics
+369
+370        Returns:
+371            True if successful
+372
+373        Examples:
+374            >>> async def doo_something():
+375            ...     async with AsyncOutlineClient(
+376            ...         "https://example.com:1234/secret",
+377            ...         "ab12cd34..."
+378            ...     ) as client:
+379            ...         # Enable metrics
+380            ...         await client.set_metrics_status(True)
+381            ...         # Check new status
+382            ...         is_enabled = await client.get_metrics_status()
+383        """
+384        return await self._request(
+385            "PUT", "metrics/enabled", json={"metricsEnabled": enabled}
+386        )
+387
+388    async def get_transfer_metrics(
+389            self, period: MetricsPeriod = MetricsPeriod.MONTHLY
+390    ) -> Union[JsonDict, ServerMetrics]:
+391        """
+392        Get transfer metrics for specified period.
+393
+394        Args:
+395            period: Time period for metrics (DAILY, WEEKLY, or MONTHLY)
+396
+397        Returns:
+398            Transfer metrics data for each access key
+399
+400        Examples:
+401            >>> async def doo_something():
+402            ...     async with AsyncOutlineClient(
+403            ...         "https://example.com:1234/secret",
+404            ...         "ab12cd34..."
+405            ...     ) as client:
+406            ...         # Get monthly metrics
+407            ...         metrics = await client.get_transfer_metrics()
+408            ...         # Or get daily metrics
+409            ...         daily = await client.get_transfer_metrics(MetricsPeriod.DAILY)
+410            ...         for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items():
+411            ...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")
+412        """
+413        response = await self._request(
+414            "GET", "metrics/transfer", params={"period": period.value}
+415        )
+416        return await self._parse_response(
+417            response, ServerMetrics, json_format=self._json_format
+418        )
+419
+420    async def create_access_key(
+421            self,
+422            *,
+423            name: Optional[str] = None,
+424            password: Optional[str] = None,
+425            port: Optional[int] = None,
+426            method: Optional[str] = None,
+427            limit: Optional[DataLimit] = None,
+428    ) -> Union[JsonDict, AccessKey]:
+429        """
+430        Create a new access key.
+431
+432        Args:
+433            name: Optional key name
+434            password: Optional password
+435            port: Optional port number (1-65535)
+436            method: Optional encryption method
+437            limit: Optional data transfer limit
+438
+439        Returns:
+440            New access key details
+441
+442        Examples:
+443            >>> async def doo_something():
+444            ...     async with AsyncOutlineClient(
+445            ...         "https://example.com:1234/secret",
+446            ...         "ab12cd34..."
+447            ...     ) as client:
+448            ...         # Create basic key
+449            ...         key = await client.create_access_key(name="User 1")
+450            ...
+451            ...         # Create key with data limit
+452            ...         _limit = DataLimit(bytes=5 * 1024**3)  # 5 GB
+453            ...         key = await client.create_access_key(
+454            ...             name="Limited User",
+455            ...             port=8388,
+456            ...             limit=_limit
+457            ...         )
+458            ...         print(f"Created key: {key.access_url}")
+459        """
+460        request = AccessKeyCreateRequest(
+461            name=name, password=password, port=port, method=method, limit=limit
+462        )
+463        response = await self._request(
+464            "POST", "access-keys", json=request.model_dump(exclude_none=True)
+465        )
+466        return await self._parse_response(
+467            response, AccessKey, json_format=self._json_format
+468        )
+469
+470    async def get_access_keys(self) -> Union[JsonDict, AccessKeyList]:
+471        """
+472        Get all access keys.
+473
+474        Returns:
+475            List of all access keys
+476
+477        Examples:
+478            >>> async def doo_something():
+479            ...     async with AsyncOutlineClient(
+480            ...         "https://example.com:1234/secret",
+481            ...         "ab12cd34..."
+482            ...     ) as client:
+483            ...         keys = await client.get_access_keys()
+484            ...         for key in keys.access_keys:
+485            ...             print(f"Key {key.id}: {key.name or 'unnamed'}")
+486            ...             if key.data_limit:
+487            ...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")
+488        """
+489        response = await self._request("GET", "access-keys")
+490        return await self._parse_response(
+491            response, AccessKeyList, json_format=self._json_format
+492        )
+493
+494    async def get_access_key(self, key_id: int) -> Union[JsonDict, AccessKey]:
+495        """
+496        Get specific access key.
+497
+498        Args:
+499            key_id: Access key ID
+500
+501        Returns:
+502            Access key details
+503
+504        Raises:
+505            APIError: If key doesn't exist
+506
+507        Examples:
+508            >>> async def doo_something():
+509            ...     async with AsyncOutlineClient(
+510            ...         "https://example.com:1234/secret",
+511            ...         "ab12cd34..."
+512            ...     ) as client:
+513            ...         key = await client.get_access_key(1)
+514            ...         print(f"Port: {key.port}")
+515            ...         print(f"URL: {key.access_url}")
+516        """
+517        response = await self._request("GET", f"access-keys/{key_id}")
+518        return await self._parse_response(
+519            response, AccessKey, json_format=self._json_format
+520        )
+521
+522    async def rename_access_key(self, key_id: int, name: str) -> bool:
+523        """
+524        Rename access key.
+525
+526        Args:
+527            key_id: Access key ID
+528            name: New name
+529
+530        Returns:
+531            True if successful
+532
+533        Raises:
+534            APIError: If key doesn't exist
+535
+536        Examples:
+537            >>> async def doo_something():
+538            ...     async with AsyncOutlineClient(
+539            ...         "https://example.com:1234/secret",
+540            ...         "ab12cd34..."
+541            ...     ) as client:
+542            ...         # Rename key
+543            ...         await client.rename_access_key(1, "Alice")
+544            ...
+545            ...         # Verify new name
+546            ...         key = await client.get_access_key(1)
+547            ...         assert key.name == "Alice"
+548        """
+549        return await self._request(
+550            "PUT", f"access-keys/{key_id}/name", json={"name": name}
+551        )
+552
+553    async def delete_access_key(self, key_id: int) -> bool:
+554        """
+555        Delete access key.
+556
+557        Args:
+558            key_id: Access key ID
+559
+560        Returns:
+561            True if successful
+562
+563        Raises:
+564            APIError: If key doesn't exist
+565
+566        Examples:
+567            >>> async def doo_something():
+568            ...     async with AsyncOutlineClient(
+569            ...         "https://example.com:1234/secret",
+570            ...         "ab12cd34..."
+571            ...     ) as client:
+572            ...         if await client.delete_access_key(1):
+573            ...             print("Key deleted")
+574
+575        """
+576        return await self._request("DELETE", f"access-keys/{key_id}")
+577
+578    async def set_access_key_data_limit(self, key_id: int, bytes_limit: int) -> bool:
+579        """
+580        Set data transfer limit for access key.
+581
+582        Args:
+583            key_id: Access key ID
+584            bytes_limit: Limit in bytes (must be positive)
+585
+586        Returns:
+587            True if successful
+588
+589        Raises:
+590            APIError: If key doesn't exist or limit is invalid
+591
+592        Examples:
+593            >>> async def doo_something():
+594            ...     async with AsyncOutlineClient(
+595            ...         "https://example.com:1234/secret",
+596            ...         "ab12cd34..."
+597            ...     ) as client:
+598            ...         # Set 5 GB limit
+599            ...         limit = 5 * 1024**3  # 5 GB in bytes
+600            ...         await client.set_access_key_data_limit(1, limit)
+601            ...
+602            ...         # Verify limit
+603            ...         key = await client.get_access_key(1)
+604            ...         assert key.data_limit and key.data_limit.bytes == limit
+605        """
+606        return await self._request(
+607            "PUT",
+608            f"access-keys/{key_id}/data-limit",
+609            json={"limit": {"bytes": bytes_limit}},
+610        )
+611
+612    async def remove_access_key_data_limit(self, key_id: int) -> bool:
+613        """
+614        Remove data transfer limit from access key.
+615
+616        Args:
+617            key_id: Access key ID
+618
+619        Returns:
+620            True if successful
+621
+622        Raises:
+623            APIError: If key doesn't exist
+624        """
+625        return await self._request("DELETE", f"access-keys/{key_id}/data-limit")
+626
+627    @property
+628    def session(self):
+629        return self._session
+
+ + +

Asynchronous client for the Outline VPN Server API.

+ +
Arguments:
+ +
    +
  • api_url: Base URL for the Outline server API
  • +
  • cert_sha256: SHA-256 fingerprint of the server's TLS certificate
  • +
  • json_format: Return raw JSON instead of Pydantic models
  • +
  • timeout: Request timeout in seconds
  • +
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         server_info = await client.get_server_info()
+
+
+
+
+ + +
+ +
+ + AsyncOutlineClient( api_url: str, cert_sha256: str, *, json_format: bool = True, timeout: float = 30.0) + + + +
+ +
 91    def __init__(
+ 92            self,
+ 93            api_url: str,
+ 94            cert_sha256: str,
+ 95            *,
+ 96            json_format: bool = True,
+ 97            timeout: float = 30.0,
+ 98    ) -> None:
+ 99        self._api_url = api_url.rstrip("/")
+100        self._cert_sha256 = cert_sha256
+101        self._json_format = json_format
+102        self._timeout = aiohttp.ClientTimeout(total=timeout)
+103        self._ssl_context: Optional[Fingerprint] = None
+104        self._session: Optional[aiohttp.ClientSession] = None
+
+ + + + +
+
+ +
+ + async def + get_server_info(self) -> Union[dict[str, Any], Server]: + + + +
+ +
242    async def get_server_info(self) -> Union[JsonDict, Server]:
+243        """
+244        Get server information.
+245
+246        Returns:
+247            Server information including name, ID, and configuration.
+248
+249        Examples:
+250            >>> async def doo_something():
+251            ...     async with AsyncOutlineClient(
+252            ...         "https://example.com:1234/secret",
+253            ...         "ab12cd34..."
+254            ...     ) as client:
+255            ...         server = await client.get_server_info()
+256            ...         print(f"Server {server.name} running version {server.version}")
+257        """
+258        response = await self._request("GET", "server")
+259        return await self._parse_response(
+260            response, Server, json_format=self._json_format
+261        )
+
+ + +

Get server information.

+ +
Returns:
+ +
+

Server information including name, ID, and configuration.

+
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         server = await client.get_server_info()
+...         print(f"Server {server.name} running version {server.version}")
+
+
+
+
+ + +
+
+ +
+ + async def + rename_server(self, name: str) -> bool: + + + +
+ +
263    async def rename_server(self, name: str) -> bool:
+264        """
+265        Rename the server.
+266
+267        Args:
+268            name: New server name
+269
+270        Returns:
+271            True if successful
+272
+273        Examples:
+274            >>> async def doo_something():
+275            ...     async with AsyncOutlineClient(
+276            ...         "https://example.com:1234/secret",
+277            ...         "ab12cd34..."
+278            ...     ) as client:
+279            ...     success = await client.rename_server("My VPN Server")
+280            ...     if success:
+281            ...         print("Server renamed successfully")
+282        """
+283        return await self._request("PUT", "name", json={"name": name})
+
+ + +

Rename the server.

+ +
Arguments:
+ +
    +
  • name: New server name
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...     success = await client.rename_server("My VPN Server")
+...     if success:
+...         print("Server renamed successfully")
+
+
+
+
+ + +
+
+ +
+ + async def + set_hostname(self, hostname: str) -> bool: + + + +
+ +
285    async def set_hostname(self, hostname: str) -> bool:
+286        """
+287        Set server hostname for access keys.
+288
+289        Args:
+290            hostname: New hostname or IP address
+291
+292        Returns:
+293            True if successful
+294
+295        Raises:
+296            APIError: If hostname is invalid
+297
+298        Examples:
+299            >>> async def doo_something():
+300            ...     async with AsyncOutlineClient(
+301            ...         "https://example.com:1234/secret",
+302            ...         "ab12cd34..."
+303            ...     ) as client:
+304            ...         await client.set_hostname("vpn.example.com")
+305            ...         # Or use IP address
+306            ...         await client.set_hostname("203.0.113.1")
+307        """
+308        return await self._request(
+309            "PUT", "server/hostname-for-access-keys", json={"hostname": hostname}
+310        )
+
+ + +

Set server hostname for access keys.

+ +
Arguments:
+ +
    +
  • hostname: New hostname or IP address
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Raises:
+ +
    +
  • APIError: If hostname is invalid
  • +
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         await client.set_hostname("vpn.example.com")
+...         # Or use IP address
+...         await client.set_hostname("203.0.113.1")
+
+
+
+
+ + +
+
+ +
+ + async def + set_default_port(self, port: int) -> bool: + + + +
+ +
312    async def set_default_port(self, port: int) -> bool:
+313        """
+314        Set default port for new access keys.
+315
+316        Args:
+317            port: Port number (1025-65535)
+318
+319        Returns:
+320            True if successful
+321
+322        Raises:
+323            APIError: If port is invalid or in use
+324
+325        Examples:
+326            >>> async def doo_something():
+327            ...     async with AsyncOutlineClient(
+328            ...         "https://example.com:1234/secret",
+329            ...         "ab12cd34..."
+330            ...     ) as client:
+331            ...         await client.set_default_port(8388)
+332
+333        """
+334        if port < 1025 or port > 65535:
+335            raise ValueError("Privileged ports are not allowed. Use range: 1025-65535")
+336
+337        return await self._request(
+338            "PUT", "server/port-for-new-access-keys", json={"port": port}
+339        )
+
+ + +

Set default port for new access keys.

+ +
Arguments:
+ +
    +
  • port: Port number (1025-65535)
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Raises:
+ +
    +
  • APIError: If port is invalid or in use
  • +
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         await client.set_default_port(8388)
+
+
+
+
+ + +
+
+ +
+ + async def + get_metrics_status(self) -> dict[str, typing.Any] | pydantic.main.BaseModel: + + + +
+ +
341    async def get_metrics_status(self) -> dict[str, Any] | BaseModel:
+342        """
+343        Get whether metrics collection is enabled.
+344
+345        Returns:
+346            Current metrics collection status
+347
+348        Examples:
+349            >>> async def doo_something():
+350            ...     async with AsyncOutlineClient(
+351            ...         "https://example.com:1234/secret",
+352            ...         "ab12cd34..."
+353            ...     ) as client:
+354            ...         if await client.get_metrics_status():
+355            ...             print("Metrics collection is enabled")
+356        """
+357        response = await self._request("GET", "metrics/enabled")
+358        data = await self._parse_response(
+359            response, MetricsStatusResponse, json_format=self._json_format
+360        )
+361        return data
+
+ + +

Get whether metrics collection is enabled.

+ +
Returns:
+ +
+

Current metrics collection status

+
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         if await client.get_metrics_status():
+...             print("Metrics collection is enabled")
+
+
+
+
+ + +
+
+ +
+ + async def + set_metrics_status(self, enabled: bool) -> bool: + + + +
+ +
363    async def set_metrics_status(self, enabled: bool) -> bool:
+364        """
+365        Enable or disable metrics collection.
+366
+367        Args:
+368            enabled: Whether to enable metrics
+369
+370        Returns:
+371            True if successful
+372
+373        Examples:
+374            >>> async def doo_something():
+375            ...     async with AsyncOutlineClient(
+376            ...         "https://example.com:1234/secret",
+377            ...         "ab12cd34..."
+378            ...     ) as client:
+379            ...         # Enable metrics
+380            ...         await client.set_metrics_status(True)
+381            ...         # Check new status
+382            ...         is_enabled = await client.get_metrics_status()
+383        """
+384        return await self._request(
+385            "PUT", "metrics/enabled", json={"metricsEnabled": enabled}
+386        )
+
+ + +

Enable or disable metrics collection.

+ +
Arguments:
+ +
    +
  • enabled: Whether to enable metrics
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         # Enable metrics
+...         await client.set_metrics_status(True)
+...         # Check new status
+...         is_enabled = await client.get_metrics_status()
+
+
+
+
+ + +
+
+ +
+ + async def + get_transfer_metrics( self, period: MetricsPeriod = <MetricsPeriod.MONTHLY: 'monthly'>) -> Union[dict[str, Any], ServerMetrics]: + + + +
+ +
388    async def get_transfer_metrics(
+389            self, period: MetricsPeriod = MetricsPeriod.MONTHLY
+390    ) -> Union[JsonDict, ServerMetrics]:
+391        """
+392        Get transfer metrics for specified period.
+393
+394        Args:
+395            period: Time period for metrics (DAILY, WEEKLY, or MONTHLY)
+396
+397        Returns:
+398            Transfer metrics data for each access key
+399
+400        Examples:
+401            >>> async def doo_something():
+402            ...     async with AsyncOutlineClient(
+403            ...         "https://example.com:1234/secret",
+404            ...         "ab12cd34..."
+405            ...     ) as client:
+406            ...         # Get monthly metrics
+407            ...         metrics = await client.get_transfer_metrics()
+408            ...         # Or get daily metrics
+409            ...         daily = await client.get_transfer_metrics(MetricsPeriod.DAILY)
+410            ...         for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items():
+411            ...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")
+412        """
+413        response = await self._request(
+414            "GET", "metrics/transfer", params={"period": period.value}
+415        )
+416        return await self._parse_response(
+417            response, ServerMetrics, json_format=self._json_format
+418        )
+
+ + +

Get transfer metrics for specified period.

+ +
Arguments:
+ +
    +
  • period: Time period for metrics (DAILY, WEEKLY, or MONTHLY)
  • +
+ +
Returns:
+ +
+

Transfer metrics data for each access key

+
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         # Get monthly metrics
+...         metrics = await client.get_transfer_metrics()
+...         # Or get daily metrics
+...         daily = await client.get_transfer_metrics(MetricsPeriod.DAILY)
+...         for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items():
+...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")
+
+
+
+
+ + +
+
+ +
+ + async def + create_access_key( self, *, name: Optional[str] = None, password: Optional[str] = None, port: Optional[int] = None, method: Optional[str] = None, limit: Optional[DataLimit] = None) -> Union[dict[str, Any], AccessKey]: + + + +
+ +
420    async def create_access_key(
+421            self,
+422            *,
+423            name: Optional[str] = None,
+424            password: Optional[str] = None,
+425            port: Optional[int] = None,
+426            method: Optional[str] = None,
+427            limit: Optional[DataLimit] = None,
+428    ) -> Union[JsonDict, AccessKey]:
+429        """
+430        Create a new access key.
+431
+432        Args:
+433            name: Optional key name
+434            password: Optional password
+435            port: Optional port number (1-65535)
+436            method: Optional encryption method
+437            limit: Optional data transfer limit
+438
+439        Returns:
+440            New access key details
+441
+442        Examples:
+443            >>> async def doo_something():
+444            ...     async with AsyncOutlineClient(
+445            ...         "https://example.com:1234/secret",
+446            ...         "ab12cd34..."
+447            ...     ) as client:
+448            ...         # Create basic key
+449            ...         key = await client.create_access_key(name="User 1")
+450            ...
+451            ...         # Create key with data limit
+452            ...         _limit = DataLimit(bytes=5 * 1024**3)  # 5 GB
+453            ...         key = await client.create_access_key(
+454            ...             name="Limited User",
+455            ...             port=8388,
+456            ...             limit=_limit
+457            ...         )
+458            ...         print(f"Created key: {key.access_url}")
+459        """
+460        request = AccessKeyCreateRequest(
+461            name=name, password=password, port=port, method=method, limit=limit
+462        )
+463        response = await self._request(
+464            "POST", "access-keys", json=request.model_dump(exclude_none=True)
+465        )
+466        return await self._parse_response(
+467            response, AccessKey, json_format=self._json_format
+468        )
+
+ + +

Create a new access key.

+ +
Arguments:
+ +
    +
  • name: Optional key name
  • +
  • password: Optional password
  • +
  • port: Optional port number (1-65535)
  • +
  • method: Optional encryption method
  • +
  • limit: Optional data transfer limit
  • +
+ +
Returns:
+ +
+

New access key details

+
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         # Create basic key
+...         key = await client.create_access_key(name="User 1")
+...
+...         # Create key with data limit
+...         _limit = DataLimit(bytes=5 * 1024**3)  # 5 GB
+...         key = await client.create_access_key(
+...             name="Limited User",
+...             port=8388,
+...             limit=_limit
+...         )
+...         print(f"Created key: {key.access_url}")
+
+
+
+
+ + +
+
+ +
+ + async def + get_access_keys(self) -> Union[dict[str, Any], AccessKeyList]: + + + +
+ +
470    async def get_access_keys(self) -> Union[JsonDict, AccessKeyList]:
+471        """
+472        Get all access keys.
+473
+474        Returns:
+475            List of all access keys
+476
+477        Examples:
+478            >>> async def doo_something():
+479            ...     async with AsyncOutlineClient(
+480            ...         "https://example.com:1234/secret",
+481            ...         "ab12cd34..."
+482            ...     ) as client:
+483            ...         keys = await client.get_access_keys()
+484            ...         for key in keys.access_keys:
+485            ...             print(f"Key {key.id}: {key.name or 'unnamed'}")
+486            ...             if key.data_limit:
+487            ...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")
+488        """
+489        response = await self._request("GET", "access-keys")
+490        return await self._parse_response(
+491            response, AccessKeyList, json_format=self._json_format
+492        )
+
+ + +

Get all access keys.

+ +
Returns:
+ +
+

List of all access keys

+
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         keys = await client.get_access_keys()
+...         for key in keys.access_keys:
+...             print(f"Key {key.id}: {key.name or 'unnamed'}")
+...             if key.data_limit:
+...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")
+
+
+
+
+ + +
+
+ +
+ + async def + get_access_key( self, key_id: int) -> Union[dict[str, Any], AccessKey]: + + + +
+ +
494    async def get_access_key(self, key_id: int) -> Union[JsonDict, AccessKey]:
+495        """
+496        Get specific access key.
+497
+498        Args:
+499            key_id: Access key ID
+500
+501        Returns:
+502            Access key details
+503
+504        Raises:
+505            APIError: If key doesn't exist
+506
+507        Examples:
+508            >>> async def doo_something():
+509            ...     async with AsyncOutlineClient(
+510            ...         "https://example.com:1234/secret",
+511            ...         "ab12cd34..."
+512            ...     ) as client:
+513            ...         key = await client.get_access_key(1)
+514            ...         print(f"Port: {key.port}")
+515            ...         print(f"URL: {key.access_url}")
+516        """
+517        response = await self._request("GET", f"access-keys/{key_id}")
+518        return await self._parse_response(
+519            response, AccessKey, json_format=self._json_format
+520        )
+
+ + +

Get specific access key.

+ +
Arguments:
+ +
    +
  • key_id: Access key ID
  • +
+ +
Returns:
+ +
+

Access key details

+
+ +
Raises:
+ +
    +
  • APIError: If key doesn't exist
  • +
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         key = await client.get_access_key(1)
+...         print(f"Port: {key.port}")
+...         print(f"URL: {key.access_url}")
+
+
+
+
+ + +
+
+ +
+ + async def + rename_access_key(self, key_id: int, name: str) -> bool: + + + +
+ +
522    async def rename_access_key(self, key_id: int, name: str) -> bool:
+523        """
+524        Rename access key.
+525
+526        Args:
+527            key_id: Access key ID
+528            name: New name
+529
+530        Returns:
+531            True if successful
+532
+533        Raises:
+534            APIError: If key doesn't exist
+535
+536        Examples:
+537            >>> async def doo_something():
+538            ...     async with AsyncOutlineClient(
+539            ...         "https://example.com:1234/secret",
+540            ...         "ab12cd34..."
+541            ...     ) as client:
+542            ...         # Rename key
+543            ...         await client.rename_access_key(1, "Alice")
+544            ...
+545            ...         # Verify new name
+546            ...         key = await client.get_access_key(1)
+547            ...         assert key.name == "Alice"
+548        """
+549        return await self._request(
+550            "PUT", f"access-keys/{key_id}/name", json={"name": name}
+551        )
+
+ + +

Rename access key.

+ +
Arguments:
+ +
    +
  • key_id: Access key ID
  • +
  • name: New name
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Raises:
+ +
    +
  • APIError: If key doesn't exist
  • +
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         # Rename key
+...         await client.rename_access_key(1, "Alice")
+...
+...         # Verify new name
+...         key = await client.get_access_key(1)
+...         assert key.name == "Alice"
+
+
+
+
+ + +
+
+ +
+ + async def + delete_access_key(self, key_id: int) -> bool: + + + +
+ +
553    async def delete_access_key(self, key_id: int) -> bool:
+554        """
+555        Delete access key.
+556
+557        Args:
+558            key_id: Access key ID
+559
+560        Returns:
+561            True if successful
+562
+563        Raises:
+564            APIError: If key doesn't exist
+565
+566        Examples:
+567            >>> async def doo_something():
+568            ...     async with AsyncOutlineClient(
+569            ...         "https://example.com:1234/secret",
+570            ...         "ab12cd34..."
+571            ...     ) as client:
+572            ...         if await client.delete_access_key(1):
+573            ...             print("Key deleted")
+574
+575        """
+576        return await self._request("DELETE", f"access-keys/{key_id}")
+
+ + +

Delete access key.

+ +
Arguments:
+ +
    +
  • key_id: Access key ID
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Raises:
+ +
    +
  • APIError: If key doesn't exist
  • +
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         if await client.delete_access_key(1):
+...             print("Key deleted")
+
+
+
+
+ + +
+
+ +
+ + async def + set_access_key_data_limit(self, key_id: int, bytes_limit: int) -> bool: + + + +
+ +
578    async def set_access_key_data_limit(self, key_id: int, bytes_limit: int) -> bool:
+579        """
+580        Set data transfer limit for access key.
+581
+582        Args:
+583            key_id: Access key ID
+584            bytes_limit: Limit in bytes (must be positive)
+585
+586        Returns:
+587            True if successful
+588
+589        Raises:
+590            APIError: If key doesn't exist or limit is invalid
+591
+592        Examples:
+593            >>> async def doo_something():
+594            ...     async with AsyncOutlineClient(
+595            ...         "https://example.com:1234/secret",
+596            ...         "ab12cd34..."
+597            ...     ) as client:
+598            ...         # Set 5 GB limit
+599            ...         limit = 5 * 1024**3  # 5 GB in bytes
+600            ...         await client.set_access_key_data_limit(1, limit)
+601            ...
+602            ...         # Verify limit
+603            ...         key = await client.get_access_key(1)
+604            ...         assert key.data_limit and key.data_limit.bytes == limit
+605        """
+606        return await self._request(
+607            "PUT",
+608            f"access-keys/{key_id}/data-limit",
+609            json={"limit": {"bytes": bytes_limit}},
+610        )
+
+ + +

Set data transfer limit for access key.

+ +
Arguments:
+ +
    +
  • key_id: Access key ID
  • +
  • bytes_limit: Limit in bytes (must be positive)
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Raises:
+ +
    +
  • APIError: If key doesn't exist or limit is invalid
  • +
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         # Set 5 GB limit
+...         limit = 5 * 1024**3  # 5 GB in bytes
+...         await client.set_access_key_data_limit(1, limit)
+...
+...         # Verify limit
+...         key = await client.get_access_key(1)
+...         assert key.data_limit and key.data_limit.bytes == limit
+
+
+
+
+ + +
+
+ +
+ + async def + remove_access_key_data_limit(self, key_id: int) -> bool: + + + +
+ +
612    async def remove_access_key_data_limit(self, key_id: int) -> bool:
+613        """
+614        Remove data transfer limit from access key.
+615
+616        Args:
+617            key_id: Access key ID
+618
+619        Returns:
+620            True if successful
+621
+622        Raises:
+623            APIError: If key doesn't exist
+624        """
+625        return await self._request("DELETE", f"access-keys/{key_id}/data-limit")
+
+ + +

Remove data transfer limit from access key.

+ +
Arguments:
+ +
    +
  • key_id: Access key ID
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Raises:
+ +
    +
  • APIError: If key doesn't exist
  • +
+
+ + +
+
+ +
+ session + + + +
+ +
627    @property
+628    def session(self):
+629        return self._session
+
+ + + + +
+
+
+ +
+ + class + OutlineError(builtins.Exception): + + + +
+ +
48class OutlineError(Exception):
+49    """Base exception for Outline client errors."""
+
+ + +

Base exception for Outline client errors.

+
+ + +
+
+ +
+ + class + APIError(pyoutlineapi.OutlineError): + + + +
+ +
52class APIError(OutlineError):
+53    """Raised when API requests fail."""
+54
+55    def __init__(self, message: str, status_code: Optional[int] = None) -> None:
+56        super().__init__(message)
+57        self.status_code = status_code
+
+ + +

Raised when API requests fail.

+
+ + +
+ +
+ + APIError(message: str, status_code: Optional[int] = None) + + + +
+ +
55    def __init__(self, message: str, status_code: Optional[int] = None) -> None:
+56        super().__init__(message)
+57        self.status_code = status_code
+
+ + + + +
+
+
+ status_code + + +
+ + + + +
+
+
+ +
+ + class + AccessKey(pydantic.main.BaseModel): + + + +
+ +
42class AccessKey(BaseModel):
+43    """Access key details."""
+44
+45    id: int
+46    name: Optional[str] = None
+47    password: str
+48    port: int = Field(gt=0, lt=65536)
+49    method: str
+50    access_url: str = Field(alias="accessUrl")
+51    data_limit: Optional[DataLimit] = Field(None, alias="dataLimit")
+
+ + +

Access key details.

+
+ + +
+
+ id: int + + +
+ + + + +
+
+
+ name: Optional[str] + + +
+ + + + +
+
+
+ password: str + + +
+ + + + +
+
+
+ port: int + + +
+ + + + +
+
+
+ method: str + + +
+ + + + +
+
+
+ access_url: str + + +
+ + + + +
+
+
+ data_limit: Optional[DataLimit] + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + AccessKeyCreateRequest(pydantic.main.BaseModel): + + + +
+ +
119class AccessKeyCreateRequest(BaseModel):
+120    """
+121    Request parameters for creating an access key.
+122    Per OpenAPI: /access-keys POST request body
+123    """
+124
+125    name: Optional[str] = None
+126    method: Optional[str] = None
+127    password: Optional[str] = None
+128    port: Optional[int] = Field(None, gt=0, lt=65536)
+129    limit: Optional[DataLimit] = None
+
+ + +

Request parameters for creating an access key. +Per OpenAPI: /access-keys POST request body

+
+ + +
+
+ name: Optional[str] + + +
+ + + + +
+
+
+ method: Optional[str] + + +
+ + + + +
+
+
+ password: Optional[str] + + +
+ + + + +
+
+
+ port: Optional[int] + + +
+ + + + +
+
+
+ limit: Optional[DataLimit] + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + AccessKeyList(pydantic.main.BaseModel): + + + +
+ +
54class AccessKeyList(BaseModel):
+55    """List of access keys."""
+56
+57    access_keys: list[AccessKey] = Field(alias="accessKeys")
+
+ + +

List of access keys.

+
+ + +
+
+ access_keys: list[AccessKey] + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + DataLimit(pydantic.main.BaseModel): + + + +
+ +
30class DataLimit(BaseModel):
+31    """Data transfer limit configuration."""
+32
+33    bytes: int = Field(gt=0)
+34
+35    @field_validator("bytes")
+36    def validate_bytes(cls, v: int) -> int:
+37        if v < 0:
+38            raise ValueError("bytes must be positive")
+39        return v
+
+ + +

Data transfer limit configuration.

+
+ + +
+
+ bytes: int + + +
+ + + + +
+
+ +
+
@field_validator('bytes')
+ + def + validate_bytes(cls, v: int) -> int: + + + +
+ +
35    @field_validator("bytes")
+36    def validate_bytes(cls, v: int) -> int:
+37        if v < 0:
+38            raise ValueError("bytes must be positive")
+39        return v
+
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + ErrorResponse(pydantic.main.BaseModel): + + + +
+ +
138class ErrorResponse(BaseModel):
+139    """
+140    Error response structure
+141    Per OpenAPI: 404 and 400 responses
+142    """
+143
+144    code: str
+145    message: str
+
+ + +

Error response structure +Per OpenAPI: 404 and 400 responses

+
+ + +
+
+ code: str + + +
+ + + + +
+
+
+ message: str + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + ExperimentalMetrics(pydantic.main.BaseModel): + + + +
+ +
 93class ExperimentalMetrics(BaseModel):
+ 94    """
+ 95    Experimental metrics data structure
+ 96    Per OpenAPI: /experimental/server/metrics endpoint
+ 97    """
+ 98
+ 99    server: list[ServerMetric]
+100    access_keys: list[AccessKeyMetric] = Field(alias="accessKeys")
+
+ + +

Experimental metrics data structure +Per OpenAPI: /experimental/server/metrics endpoint

+
+ + +
+
+ server: list[pyoutlineapi.models.ServerMetric] + + +
+ + + + +
+
+
+ access_keys: list[pyoutlineapi.models.AccessKeyMetric] + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + MetricsPeriod(builtins.str, enum.Enum): + + + +
+ +
22class MetricsPeriod(str, Enum):
+23    """Time periods for metrics collection."""
+24
+25    DAILY = "daily"
+26    WEEKLY = "weekly"
+27    MONTHLY = "monthly"
+
+ + +

Time periods for metrics collection.

+
+ + +
+
+ DAILY = +<MetricsPeriod.DAILY: 'daily'> + + +
+ + + + +
+
+
+ WEEKLY = +<MetricsPeriod.WEEKLY: 'weekly'> + + +
+ + + + +
+
+
+ MONTHLY = +<MetricsPeriod.MONTHLY: 'monthly'> + + +
+ + + + +
+
+
+ +
+ + class + MetricsStatusResponse(pydantic.main.BaseModel): + + + +
+ +
132class MetricsStatusResponse(BaseModel):
+133    """Response for /metrics/enabled endpoint"""
+134
+135    metrics_enabled: bool = Field(alias="metricsEnabled")
+
+ + +

Response for /metrics/enabled endpoint

+
+ + +
+
+ metrics_enabled: bool + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + Server(pydantic.main.BaseModel): + + + +
+ +
103class Server(BaseModel):
+104    """
+105    Server information.
+106    Per OpenAPI: /server endpoint schema
+107    """
+108
+109    name: str
+110    server_id: str = Field(alias="serverId")
+111    metrics_enabled: bool = Field(alias="metricsEnabled")
+112    created_timestamp_ms: int = Field(alias="createdTimestampMs")
+113    version: str
+114    port_for_new_access_keys: int = Field(alias="portForNewAccessKeys", gt=0, lt=65536)
+115    hostname_for_access_keys: Optional[str] = Field(None, alias="hostnameForAccessKeys")
+116    access_key_data_limit: Optional[DataLimit] = Field(None, alias="accessKeyDataLimit")
+
+ + +

Server information. +Per OpenAPI: /server endpoint schema

+
+ + +
+
+ name: str + + +
+ + + + +
+
+
+ server_id: str + + +
+ + + + +
+
+
+ metrics_enabled: bool + + +
+ + + + +
+
+
+ created_timestamp_ms: int + + +
+ + + + +
+
+
+ version: str + + +
+ + + + +
+
+
+ port_for_new_access_keys: int + + +
+ + + + +
+
+
+ hostname_for_access_keys: Optional[str] + + +
+ + + + +
+
+
+ access_key_data_limit: Optional[DataLimit] + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + ServerMetrics(pydantic.main.BaseModel): + + + +
+ +
60class ServerMetrics(BaseModel):
+61    """
+62    Server metrics data for data transferred per access key
+63    Per OpenAPI: /metrics/transfer endpoint
+64    """
+65
+66    bytes_transferred_by_user_id: dict[str, int] = Field(
+67        alias="bytesTransferredByUserId"
+68    )
+
+ + +

Server metrics data for data transferred per access key +Per OpenAPI: /metrics/transfer endpoint

+
+ + +
+
+ bytes_transferred_by_user_id: dict[str, int] + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ + \ No newline at end of file diff --git a/docs/search.js b/docs/search.js new file mode 100644 index 0000000..88a45c7 --- /dev/null +++ b/docs/search.js @@ -0,0 +1,46 @@ +window.pdocSearch = (function(){ +/** elasticlunr - http://weixsong.github.io * Copyright (C) 2017 Oliver Nightingale * Copyright (C) 2017 Wei Song * MIT Licensed */!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();oPyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API.

\n\n

Copyright (c) 2025 Denis Rozhnovskiy pytelemonbot@mail.ru\nAll rights reserved.

\n\n

This software is licensed under the MIT License.

\n\n
You can find the full license text at:
\n\n
\n

https://opensource.org/licenses/MIT

\n
\n\n
Source code repository:
\n\n
\n

https://github.com/orenlab/pyoutlineapi

\n
\n"}, {"fullname": "pyoutlineapi.AsyncOutlineClient", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient", "kind": "class", "doc": "

Asynchronous client for the Outline VPN Server API.

\n\n
Arguments:
\n\n
    \n
  • api_url: Base URL for the Outline server API
  • \n
  • cert_sha256: SHA-256 fingerprint of the server's TLS certificate
  • \n
  • json_format: Return raw JSON instead of Pydantic models
  • \n
  • timeout: Request timeout in seconds
  • \n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         server_info = await client.get_server_info()\n
\n
\n
\n"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.__init__", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.__init__", "kind": "function", "doc": "

\n", "signature": "(\tapi_url: str,\tcert_sha256: str,\t*,\tjson_format: bool = True,\ttimeout: float = 30.0)"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_server_info", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_server_info", "kind": "function", "doc": "

Get server information.

\n\n
Returns:
\n\n
\n

Server information including name, ID, and configuration.

\n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         server = await client.get_server_info()\n...         print(f"Server {server.name} running version {server.version}")\n
\n
\n
\n", "signature": "(self) -> Union[dict[str, Any], pyoutlineapi.models.Server]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.rename_server", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.rename_server", "kind": "function", "doc": "

Rename the server.

\n\n
Arguments:
\n\n
    \n
  • name: New server name
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...     success = await client.rename_server("My VPN Server")\n...     if success:\n...         print("Server renamed successfully")\n
\n
\n
\n", "signature": "(self, name: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_hostname", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_hostname", "kind": "function", "doc": "

Set server hostname for access keys.

\n\n
Arguments:
\n\n
    \n
  • hostname: New hostname or IP address
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Raises:
\n\n
    \n
  • APIError: If hostname is invalid
  • \n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         await client.set_hostname("vpn.example.com")\n...         # Or use IP address\n...         await client.set_hostname("203.0.113.1")\n
\n
\n
\n", "signature": "(self, hostname: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_default_port", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_default_port", "kind": "function", "doc": "

Set default port for new access keys.

\n\n
Arguments:
\n\n
    \n
  • port: Port number (1025-65535)
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Raises:
\n\n
    \n
  • APIError: If port is invalid or in use
  • \n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         await client.set_default_port(8388)\n
\n
\n
\n", "signature": "(self, port: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_metrics_status", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_metrics_status", "kind": "function", "doc": "

Get whether metrics collection is enabled.

\n\n
Returns:
\n\n
\n

Current metrics collection status

\n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         if await client.get_metrics_status():\n...             print("Metrics collection is enabled")\n
\n
\n
\n", "signature": "(self) -> dict[str, typing.Any] | pydantic.main.BaseModel:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_metrics_status", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_metrics_status", "kind": "function", "doc": "

Enable or disable metrics collection.

\n\n
Arguments:
\n\n
    \n
  • enabled: Whether to enable metrics
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Enable metrics\n...         await client.set_metrics_status(True)\n...         # Check new status\n...         is_enabled = await client.get_metrics_status()\n
\n
\n
\n", "signature": "(self, enabled: bool) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_transfer_metrics", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_transfer_metrics", "kind": "function", "doc": "

Get transfer metrics for specified period.

\n\n
Arguments:
\n\n
    \n
  • period: Time period for metrics (DAILY, WEEKLY, or MONTHLY)
  • \n
\n\n
Returns:
\n\n
\n

Transfer metrics data for each access key

\n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Get monthly metrics\n...         metrics = await client.get_transfer_metrics()\n...         # Or get daily metrics\n...         daily = await client.get_transfer_metrics(MetricsPeriod.DAILY)\n...         for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items():\n...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")\n
\n
\n
\n", "signature": "(\tself,\tperiod: pyoutlineapi.models.MetricsPeriod = <MetricsPeriod.MONTHLY: 'monthly'>) -> Union[dict[str, Any], pyoutlineapi.models.ServerMetrics]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.create_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.create_access_key", "kind": "function", "doc": "

Create a new access key.

\n\n
Arguments:
\n\n
    \n
  • name: Optional key name
  • \n
  • password: Optional password
  • \n
  • port: Optional port number (1-65535)
  • \n
  • method: Optional encryption method
  • \n
  • limit: Optional data transfer limit
  • \n
\n\n
Returns:
\n\n
\n

New access key details

\n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Create basic key\n...         key = await client.create_access_key(name="User 1")\n...\n...         # Create key with data limit\n...         _limit = DataLimit(bytes=5 * 1024**3)  # 5 GB\n...         key = await client.create_access_key(\n...             name="Limited User",\n...             port=8388,\n...             limit=_limit\n...         )\n...         print(f"Created key: {key.access_url}")\n
\n
\n
\n", "signature": "(\tself,\t*,\tname: Optional[str] = None,\tpassword: Optional[str] = None,\tport: Optional[int] = None,\tmethod: Optional[str] = None,\tlimit: Optional[pyoutlineapi.models.DataLimit] = None) -> Union[dict[str, Any], pyoutlineapi.models.AccessKey]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_access_keys", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_access_keys", "kind": "function", "doc": "

Get all access keys.

\n\n
Returns:
\n\n
\n

List of all access keys

\n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         keys = await client.get_access_keys()\n...         for key in keys.access_keys:\n...             print(f"Key {key.id}: {key.name or 'unnamed'}")\n...             if key.data_limit:\n...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")\n
\n
\n
\n", "signature": "(self) -> Union[dict[str, Any], pyoutlineapi.models.AccessKeyList]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_access_key", "kind": "function", "doc": "

Get specific access key.

\n\n
Arguments:
\n\n
    \n
  • key_id: Access key ID
  • \n
\n\n
Returns:
\n\n
\n

Access key details

\n
\n\n
Raises:
\n\n
    \n
  • APIError: If key doesn't exist
  • \n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         key = await client.get_access_key(1)\n...         print(f"Port: {key.port}")\n...         print(f"URL: {key.access_url}")\n
\n
\n
\n", "signature": "(\tself,\tkey_id: int) -> Union[dict[str, Any], pyoutlineapi.models.AccessKey]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.rename_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.rename_access_key", "kind": "function", "doc": "

Rename access key.

\n\n
Arguments:
\n\n
    \n
  • key_id: Access key ID
  • \n
  • name: New name
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Raises:
\n\n
    \n
  • APIError: If key doesn't exist
  • \n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Rename key\n...         await client.rename_access_key(1, "Alice")\n...\n...         # Verify new name\n...         key = await client.get_access_key(1)\n...         assert key.name == "Alice"\n
\n
\n
\n", "signature": "(self, key_id: int, name: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.delete_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.delete_access_key", "kind": "function", "doc": "

Delete access key.

\n\n
Arguments:
\n\n
    \n
  • key_id: Access key ID
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Raises:
\n\n
    \n
  • APIError: If key doesn't exist
  • \n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         if await client.delete_access_key(1):\n...             print("Key deleted")\n
\n
\n
\n", "signature": "(self, key_id: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_access_key_data_limit", "kind": "function", "doc": "

Set data transfer limit for access key.

\n\n
Arguments:
\n\n
    \n
  • key_id: Access key ID
  • \n
  • bytes_limit: Limit in bytes (must be positive)
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Raises:
\n\n
    \n
  • APIError: If key doesn't exist or limit is invalid
  • \n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Set 5 GB limit\n...         limit = 5 * 1024**3  # 5 GB in bytes\n...         await client.set_access_key_data_limit(1, limit)\n...\n...         # Verify limit\n...         key = await client.get_access_key(1)\n...         assert key.data_limit and key.data_limit.bytes == limit\n
\n
\n
\n", "signature": "(self, key_id: int, bytes_limit: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.remove_access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.remove_access_key_data_limit", "kind": "function", "doc": "

Remove data transfer limit from access key.

\n\n
Arguments:
\n\n
    \n
  • key_id: Access key ID
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Raises:
\n\n
    \n
  • APIError: If key doesn't exist
  • \n
\n", "signature": "(self, key_id: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.session", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.session", "kind": "variable", "doc": "

\n"}, {"fullname": "pyoutlineapi.OutlineError", "modulename": "pyoutlineapi", "qualname": "OutlineError", "kind": "class", "doc": "

Base exception for Outline client errors.

\n", "bases": "builtins.Exception"}, {"fullname": "pyoutlineapi.APIError", "modulename": "pyoutlineapi", "qualname": "APIError", "kind": "class", "doc": "

Raised when API requests fail.

\n", "bases": "pyoutlineapi.client.OutlineError"}, {"fullname": "pyoutlineapi.APIError.__init__", "modulename": "pyoutlineapi", "qualname": "APIError.__init__", "kind": "function", "doc": "

\n", "signature": "(message: str, status_code: Optional[int] = None)"}, {"fullname": "pyoutlineapi.APIError.status_code", "modulename": "pyoutlineapi", "qualname": "APIError.status_code", "kind": "variable", "doc": "

\n"}, {"fullname": "pyoutlineapi.AccessKey", "modulename": "pyoutlineapi", "qualname": "AccessKey", "kind": "class", "doc": "

Access key details.

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKey.id", "modulename": "pyoutlineapi", "qualname": "AccessKey.id", "kind": "variable", "doc": "

\n", "annotation": ": int"}, {"fullname": "pyoutlineapi.AccessKey.name", "modulename": "pyoutlineapi", "qualname": "AccessKey.name", "kind": "variable", "doc": "

\n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKey.password", "modulename": "pyoutlineapi", "qualname": "AccessKey.password", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.port", "modulename": "pyoutlineapi", "qualname": "AccessKey.port", "kind": "variable", "doc": "

\n", "annotation": ": int"}, {"fullname": "pyoutlineapi.AccessKey.method", "modulename": "pyoutlineapi", "qualname": "AccessKey.method", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.access_url", "modulename": "pyoutlineapi", "qualname": "AccessKey.access_url", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.data_limit", "modulename": "pyoutlineapi", "qualname": "AccessKey.data_limit", "kind": "variable", "doc": "

\n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.AccessKey.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKey.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest", "kind": "class", "doc": "

Request parameters for creating an access key.\nPer OpenAPI: /access-keys POST request body

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.name", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.name", "kind": "variable", "doc": "

\n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.method", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.method", "kind": "variable", "doc": "

\n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.password", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.password", "kind": "variable", "doc": "

\n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.port", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.port", "kind": "variable", "doc": "

\n", "annotation": ": Optional[int]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.limit", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.limit", "kind": "variable", "doc": "

\n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.AccessKeyList", "modulename": "pyoutlineapi", "qualname": "AccessKeyList", "kind": "class", "doc": "

List of access keys.

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKeyList.access_keys", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.access_keys", "kind": "variable", "doc": "

\n", "annotation": ": list[pyoutlineapi.models.AccessKey]"}, {"fullname": "pyoutlineapi.AccessKeyList.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.DataLimit", "modulename": "pyoutlineapi", "qualname": "DataLimit", "kind": "class", "doc": "

Data transfer limit configuration.

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.DataLimit.bytes", "modulename": "pyoutlineapi", "qualname": "DataLimit.bytes", "kind": "variable", "doc": "

\n", "annotation": ": int"}, {"fullname": "pyoutlineapi.DataLimit.validate_bytes", "modulename": "pyoutlineapi", "qualname": "DataLimit.validate_bytes", "kind": "function", "doc": "

\n", "signature": "(cls, v: int) -> int:", "funcdef": "def"}, {"fullname": "pyoutlineapi.DataLimit.model_config", "modulename": "pyoutlineapi", "qualname": "DataLimit.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ErrorResponse", "modulename": "pyoutlineapi", "qualname": "ErrorResponse", "kind": "class", "doc": "

Error response structure\nPer OpenAPI: 404 and 400 responses

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ErrorResponse.code", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.code", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.ErrorResponse.message", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.message", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.ErrorResponse.model_config", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ExperimentalMetrics", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics", "kind": "class", "doc": "

Experimental metrics data structure\nPer OpenAPI: /experimental/server/metrics endpoint

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.server", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.server", "kind": "variable", "doc": "

\n", "annotation": ": list[pyoutlineapi.models.ServerMetric]"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.access_keys", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.access_keys", "kind": "variable", "doc": "

\n", "annotation": ": list[pyoutlineapi.models.AccessKeyMetric]"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.model_config", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.MetricsPeriod", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod", "kind": "class", "doc": "

Time periods for metrics collection.

\n", "bases": "builtins.str, enum.Enum"}, {"fullname": "pyoutlineapi.MetricsPeriod.DAILY", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod.DAILY", "kind": "variable", "doc": "

\n", "default_value": "<MetricsPeriod.DAILY: 'daily'>"}, {"fullname": "pyoutlineapi.MetricsPeriod.WEEKLY", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod.WEEKLY", "kind": "variable", "doc": "

\n", "default_value": "<MetricsPeriod.WEEKLY: 'weekly'>"}, {"fullname": "pyoutlineapi.MetricsPeriod.MONTHLY", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod.MONTHLY", "kind": "variable", "doc": "

\n", "default_value": "<MetricsPeriod.MONTHLY: 'monthly'>"}, {"fullname": "pyoutlineapi.MetricsStatusResponse", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse", "kind": "class", "doc": "

Response for /metrics/enabled endpoint

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.MetricsStatusResponse.metrics_enabled", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse.metrics_enabled", "kind": "variable", "doc": "

\n", "annotation": ": bool"}, {"fullname": "pyoutlineapi.MetricsStatusResponse.model_config", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.Server", "modulename": "pyoutlineapi", "qualname": "Server", "kind": "class", "doc": "

Server information.\nPer OpenAPI: /server endpoint schema

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.Server.name", "modulename": "pyoutlineapi", "qualname": "Server.name", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.server_id", "modulename": "pyoutlineapi", "qualname": "Server.server_id", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.metrics_enabled", "modulename": "pyoutlineapi", "qualname": "Server.metrics_enabled", "kind": "variable", "doc": "

\n", "annotation": ": bool"}, {"fullname": "pyoutlineapi.Server.created_timestamp_ms", "modulename": "pyoutlineapi", "qualname": "Server.created_timestamp_ms", "kind": "variable", "doc": "

\n", "annotation": ": int"}, {"fullname": "pyoutlineapi.Server.version", "modulename": "pyoutlineapi", "qualname": "Server.version", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.port_for_new_access_keys", "modulename": "pyoutlineapi", "qualname": "Server.port_for_new_access_keys", "kind": "variable", "doc": "

\n", "annotation": ": int"}, {"fullname": "pyoutlineapi.Server.hostname_for_access_keys", "modulename": "pyoutlineapi", "qualname": "Server.hostname_for_access_keys", "kind": "variable", "doc": "

\n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.Server.access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "Server.access_key_data_limit", "kind": "variable", "doc": "

\n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.Server.model_config", "modulename": "pyoutlineapi", "qualname": "Server.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ServerMetrics", "modulename": "pyoutlineapi", "qualname": "ServerMetrics", "kind": "class", "doc": "

Server metrics data for data transferred per access key\nPer OpenAPI: /metrics/transfer endpoint

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.bytes_transferred_by_user_id", "kind": "variable", "doc": "

\n", "annotation": ": dict[str, int]"}, {"fullname": "pyoutlineapi.ServerMetrics.model_config", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}]; + + // mirrored in build-search-index.js (part 1) + // Also split on html tags. this is a cheap heuristic, but good enough. + elasticlunr.tokenizer.setSeperator(/[\s\-.;&_'"=,()]+|<[^>]*>/); + + let searchIndex; + if (docs._isPrebuiltIndex) { + console.info("using precompiled search index"); + searchIndex = elasticlunr.Index.load(docs); + } else { + console.time("building search index"); + // mirrored in build-search-index.js (part 2) + searchIndex = elasticlunr(function () { + this.pipeline.remove(elasticlunr.stemmer); + this.pipeline.remove(elasticlunr.stopWordFilter); + this.addField("qualname"); + this.addField("fullname"); + this.addField("annotation"); + this.addField("default_value"); + this.addField("signature"); + this.addField("bases"); + this.addField("doc"); + this.setRef("fullname"); + }); + for (let doc of docs) { + searchIndex.addDoc(doc); + } + console.timeEnd("building search index"); + } + + return (term) => searchIndex.search(term, { + fields: { + qualname: {boost: 4}, + fullname: {boost: 2}, + annotation: {boost: 2}, + default_value: {boost: 2}, + signature: {boost: 2}, + bases: {boost: 2}, + doc: {boost: 1}, + }, + expand: true + }); +})(); \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 247d31d..c8a0c01 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,130 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand. + +[[package]] +name = "aiohappyeyeballs" +version = "2.4.4" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"}, + {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"}, +] + +[[package]] +name = "aiohttp" +version = "3.11.11" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohttp-3.11.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a60804bff28662cbcf340a4d61598891f12eea3a66af48ecfdc975ceec21e3c8"}, + {file = "aiohttp-3.11.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b4fa1cb5f270fb3eab079536b764ad740bb749ce69a94d4ec30ceee1b5940d5"}, + {file = "aiohttp-3.11.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:731468f555656767cda219ab42e033355fe48c85fbe3ba83a349631541715ba2"}, + {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb23d8bb86282b342481cad4370ea0853a39e4a32a0042bb52ca6bdde132df43"}, + {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f047569d655f81cb70ea5be942ee5d4421b6219c3f05d131f64088c73bb0917f"}, + {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd7659baae9ccf94ae5fe8bfaa2c7bc2e94d24611528395ce88d009107e00c6d"}, + {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af01e42ad87ae24932138f154105e88da13ce7d202a6de93fafdafb2883a00ef"}, + {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5854be2f3e5a729800bac57a8d76af464e160f19676ab6aea74bde18ad19d438"}, + {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6526e5fb4e14f4bbf30411216780c9967c20c5a55f2f51d3abd6de68320cc2f3"}, + {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:85992ee30a31835fc482468637b3e5bd085fa8fe9392ba0bdcbdc1ef5e9e3c55"}, + {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:88a12ad8ccf325a8a5ed80e6d7c3bdc247d66175afedbe104ee2aaca72960d8e"}, + {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0a6d3fbf2232e3a08c41eca81ae4f1dff3d8f1a30bae415ebe0af2d2458b8a33"}, + {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84a585799c58b795573c7fa9b84c455adf3e1d72f19a2bf498b54a95ae0d194c"}, + {file = "aiohttp-3.11.11-cp310-cp310-win32.whl", hash = "sha256:bfde76a8f430cf5c5584553adf9926534352251d379dcb266ad2b93c54a29745"}, + {file = "aiohttp-3.11.11-cp310-cp310-win_amd64.whl", hash = "sha256:0fd82b8e9c383af11d2b26f27a478640b6b83d669440c0a71481f7c865a51da9"}, + {file = "aiohttp-3.11.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ba74ec819177af1ef7f59063c6d35a214a8fde6f987f7661f4f0eecc468a8f76"}, + {file = "aiohttp-3.11.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4af57160800b7a815f3fe0eba9b46bf28aafc195555f1824555fa2cfab6c1538"}, + {file = "aiohttp-3.11.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffa336210cf9cd8ed117011085817d00abe4c08f99968deef0013ea283547204"}, + {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b8fe282183e4a3c7a1b72f5ade1094ed1c6345a8f153506d114af5bf8accd9"}, + {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af41686ccec6a0f2bdc66686dc0f403c41ac2089f80e2214a0f82d001052c03"}, + {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70d1f9dde0e5dd9e292a6d4d00058737052b01f3532f69c0c65818dac26dc287"}, + {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:249cc6912405917344192b9f9ea5cd5b139d49e0d2f5c7f70bdfaf6b4dbf3a2e"}, + {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0eb98d90b6690827dcc84c246811feeb4e1eea683c0eac6caed7549be9c84665"}, + {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec82bf1fda6cecce7f7b915f9196601a1bd1a3079796b76d16ae4cce6d0ef89b"}, + {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9fd46ce0845cfe28f108888b3ab17abff84ff695e01e73657eec3f96d72eef34"}, + {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd176afcf8f5d2aed50c3647d4925d0db0579d96f75a31e77cbaf67d8a87742d"}, + {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ec2aa89305006fba9ffb98970db6c8221541be7bee4c1d027421d6f6df7d1ce2"}, + {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:92cde43018a2e17d48bb09c79e4d4cb0e236de5063ce897a5e40ac7cb4878773"}, + {file = "aiohttp-3.11.11-cp311-cp311-win32.whl", hash = "sha256:aba807f9569455cba566882c8938f1a549f205ee43c27b126e5450dc9f83cc62"}, + {file = "aiohttp-3.11.11-cp311-cp311-win_amd64.whl", hash = "sha256:ae545f31489548c87b0cced5755cfe5a5308d00407000e72c4fa30b19c3220ac"}, + {file = "aiohttp-3.11.11-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e595c591a48bbc295ebf47cb91aebf9bd32f3ff76749ecf282ea7f9f6bb73886"}, + {file = "aiohttp-3.11.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ea1b59dc06396b0b424740a10a0a63974c725b1c64736ff788a3689d36c02d2"}, + {file = "aiohttp-3.11.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8811f3f098a78ffa16e0ea36dffd577eb031aea797cbdba81be039a4169e242c"}, + {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7227b87a355ce1f4bf83bfae4399b1f5bb42e0259cb9405824bd03d2f4336a"}, + {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d40f9da8cabbf295d3a9dae1295c69975b86d941bc20f0a087f0477fa0a66231"}, + {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffb3dc385f6bb1568aa974fe65da84723210e5d9707e360e9ecb51f59406cd2e"}, + {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8f5f7515f3552d899c61202d99dcb17d6e3b0de777900405611cd747cecd1b8"}, + {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3499c7ffbfd9c6a3d8d6a2b01c26639da7e43d47c7b4f788016226b1e711caa8"}, + {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8e2bf8029dbf0810c7bfbc3e594b51c4cc9101fbffb583a3923aea184724203c"}, + {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6212a60e5c482ef90f2d788835387070a88d52cf6241d3916733c9176d39eab"}, + {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d119fafe7b634dbfa25a8c597718e69a930e4847f0b88e172744be24515140da"}, + {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:6fba278063559acc730abf49845d0e9a9e1ba74f85f0ee6efd5803f08b285853"}, + {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92fc484e34b733704ad77210c7957679c5c3877bd1e6b6d74b185e9320cc716e"}, + {file = "aiohttp-3.11.11-cp312-cp312-win32.whl", hash = "sha256:9f5b3c1ed63c8fa937a920b6c1bec78b74ee09593b3f5b979ab2ae5ef60d7600"}, + {file = "aiohttp-3.11.11-cp312-cp312-win_amd64.whl", hash = "sha256:1e69966ea6ef0c14ee53ef7a3d68b564cc408121ea56c0caa2dc918c1b2f553d"}, + {file = "aiohttp-3.11.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:541d823548ab69d13d23730a06f97460f4238ad2e5ed966aaf850d7c369782d9"}, + {file = "aiohttp-3.11.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:929f3ed33743a49ab127c58c3e0a827de0664bfcda566108989a14068f820194"}, + {file = "aiohttp-3.11.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0882c2820fd0132240edbb4a51eb8ceb6eef8181db9ad5291ab3332e0d71df5f"}, + {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63de12e44935d5aca7ed7ed98a255a11e5cb47f83a9fded7a5e41c40277d104"}, + {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa54f8ef31d23c506910c21163f22b124facb573bff73930735cf9fe38bf7dff"}, + {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a344d5dc18074e3872777b62f5f7d584ae4344cd6006c17ba12103759d407af3"}, + {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7fb429ab1aafa1f48578eb315ca45bd46e9c37de11fe45c7f5f4138091e2f1"}, + {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c341c7d868750e31961d6d8e60ff040fb9d3d3a46d77fd85e1ab8e76c3e9a5c4"}, + {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed9ee95614a71e87f1a70bc81603f6c6760128b140bc4030abe6abaa988f1c3d"}, + {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de8d38f1c2810fa2a4f1d995a2e9c70bb8737b18da04ac2afbf3971f65781d87"}, + {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a9b7371665d4f00deb8f32208c7c5e652059b0fda41cf6dbcac6114a041f1cc2"}, + {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:620598717fce1b3bd14dd09947ea53e1ad510317c85dda2c9c65b622edc96b12"}, + {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf8d9bfee991d8acc72d060d53860f356e07a50f0e0d09a8dfedea1c554dd0d5"}, + {file = "aiohttp-3.11.11-cp313-cp313-win32.whl", hash = "sha256:9d73ee3725b7a737ad86c2eac5c57a4a97793d9f442599bea5ec67ac9f4bdc3d"}, + {file = "aiohttp-3.11.11-cp313-cp313-win_amd64.whl", hash = "sha256:c7a06301c2fb096bdb0bd25fe2011531c1453b9f2c163c8031600ec73af1cc99"}, + {file = "aiohttp-3.11.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3e23419d832d969f659c208557de4a123e30a10d26e1e14b73431d3c13444c2e"}, + {file = "aiohttp-3.11.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21fef42317cf02e05d3b09c028712e1d73a9606f02467fd803f7c1f39cc59add"}, + {file = "aiohttp-3.11.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1f21bb8d0235fc10c09ce1d11ffbd40fc50d3f08a89e4cf3a0c503dc2562247a"}, + {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1642eceeaa5ab6c9b6dfeaaa626ae314d808188ab23ae196a34c9d97efb68350"}, + {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2170816e34e10f2fd120f603e951630f8a112e1be3b60963a1f159f5699059a6"}, + {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8be8508d110d93061197fd2d6a74f7401f73b6d12f8822bbcd6d74f2b55d71b1"}, + {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4eed954b161e6b9b65f6be446ed448ed3921763cc432053ceb606f89d793927e"}, + {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6c9af134da4bc9b3bd3e6a70072509f295d10ee60c697826225b60b9959acdd"}, + {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:44167fc6a763d534a6908bdb2592269b4bf30a03239bcb1654781adf5e49caf1"}, + {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:479b8c6ebd12aedfe64563b85920525d05d394b85f166b7873c8bde6da612f9c"}, + {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:10b4ff0ad793d98605958089fabfa350e8e62bd5d40aa65cdc69d6785859f94e"}, + {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b540bd67cfb54e6f0865ceccd9979687210d7ed1a1cc8c01f8e67e2f1e883d28"}, + {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1dac54e8ce2ed83b1f6b1a54005c87dfed139cf3f777fdc8afc76e7841101226"}, + {file = "aiohttp-3.11.11-cp39-cp39-win32.whl", hash = "sha256:568c1236b2fde93b7720f95a890741854c1200fba4a3471ff48b2934d2d93fd3"}, + {file = "aiohttp-3.11.11-cp39-cp39-win_amd64.whl", hash = "sha256:943a8b052e54dfd6439fd7989f67fc6a7f2138d0a2cf0a7de5f18aa4fe7eb3b1"}, + {file = "aiohttp-3.11.11.tar.gz", hash = "sha256:bb49c7f1e6ebf3821a42d81d494f538107610c3a705987f53068546b0e90303e"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.3.0" +aiosignal = ">=1.1.2" +async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] + +[[package]] +name = "aiosignal" +version = "1.3.2" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"}, + {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" [[package]] name = "annotated-types" @@ -6,127 +132,115 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] [[package]] -name = "certifi" -version = "2024.8.30" -description = "Python package for providing Mozilla's CA Bundle." +name = "async-timeout" +version = "5.0.1" +description = "Timeout context manager for asyncio programs" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version < \"3.11\"" files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.3.2" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, ] +[[package]] +name = "attrs" +version = "24.3.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, + {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + +[[package]] +name = "black" +version = "24.10.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, + {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, + {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, + {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, + {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, + {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, + {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, + {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, + {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, + {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, + {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, + {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, + {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, + {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, + {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, + {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, + {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, + {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, + {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, + {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, + {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, + {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -134,83 +248,74 @@ files = [ [[package]] name = "coverage" -version = "7.6.1" +version = "7.6.10" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, - {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, - {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, - {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, - {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, - {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, - {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, - {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, - {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, - {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, - {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, - {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, - {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, - {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, - {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, - {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, - {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, - {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, + {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, + {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, + {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, + {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, + {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, + {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, + {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, + {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, + {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, + {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, + {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, + {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, + {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, + {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, + {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, + {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, + {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, + {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, ] [package.dependencies] @@ -225,6 +330,8 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -233,45 +340,495 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "flake8" +version = "7.1.1" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +groups = ["main", "dev"] +files = [ + {file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"}, + {file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.12.0,<2.13.0" +pyflakes = ">=3.2.0,<3.3.0" + +[[package]] +name = "frozenlist" +version = "1.5.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5"}, + {file = "frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb"}, + {file = "frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf"}, + {file = "frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942"}, + {file = "frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f"}, + {file = "frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8"}, + {file = "frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03"}, + {file = "frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c"}, + {file = "frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e"}, + {file = "frozenlist-1.5.0-cp38-cp38-win32.whl", hash = "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723"}, + {file = "frozenlist-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c"}, + {file = "frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3"}, + {file = "frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0"}, + {file = "frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3"}, + {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"}, +] + [[package]] name = "idna" -version = "3.8" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ - {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, - {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "jinja2" +version = "3.1.5" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +groups = ["main", "dev"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "multidict" +version = "6.1.0" +description = "multidict implementation" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"}, + {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"}, + {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, + {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, + {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, + {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, + {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"}, + {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"}, + {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"}, + {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"}, + {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"}, + {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"}, + {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"}, + {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, + {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "mypy" +version = "1.14.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"}, + {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"}, + {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"}, + {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"}, + {file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"}, + {file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"}, + {file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"}, + {file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"}, + {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"}, + {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"}, + {file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"}, + {file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"}, + {file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"}, + {file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"}, + {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"}, + {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"}, + {file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"}, + {file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"}, + {file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"}, + {file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"}, + {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"}, + {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"}, + {file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"}, + {file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"}, + {file = "mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31"}, + {file = "mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6"}, + {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319"}, + {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac"}, + {file = "mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b"}, + {file = "mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837"}, + {file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"}, + {file = "mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc"}, + {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9"}, + {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb"}, + {file = "mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60"}, + {file = "mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c"}, + {file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"}, + {file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"}, +] + +[package.dependencies] +mypy_extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + [[package]] name = "packaging" -version = "24.1" +version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "pdoc" +version = "15.0.1" +description = "API Documentation for Python Projects" +optional = false +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "pdoc-15.0.1-py3-none-any.whl", hash = "sha256:fd437ab8eb55f9b942226af7865a3801e2fb731665199b74fd9a44737dbe20f9"}, + {file = "pdoc-15.0.1.tar.gz", hash = "sha256:3b08382c9d312243ee6c2a1813d0ff517a6ab84d596fa2c6c6b5255b17c3d666"}, ] +[package.dependencies] +Jinja2 = ">=2.11.0" +MarkupSafe = ">=1.1.1" +pygments = ">=2.12.0" + +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + [[package]] name = "pluggy" version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -281,138 +838,281 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "propcache" +version = "0.2.1" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6"}, + {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2"}, + {file = "propcache-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b"}, + {file = "propcache-0.2.1-cp310-cp310-win32.whl", hash = "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4"}, + {file = "propcache-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e"}, + {file = "propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034"}, + {file = "propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518"}, + {file = "propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246"}, + {file = "propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30"}, + {file = "propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6"}, + {file = "propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1"}, + {file = "propcache-0.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6a9a8c34fb7bb609419a211e59da8887eeca40d300b5ea8e56af98f6fbbb1541"}, + {file = "propcache-0.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae1aa1cd222c6d205853b3013c69cd04515f9d6ab6de4b0603e2e1c33221303e"}, + {file = "propcache-0.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:accb6150ce61c9c4b7738d45550806aa2b71c7668c6942f17b0ac182b6142fd4"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eee736daafa7af6d0a2dc15cc75e05c64f37fc37bafef2e00d77c14171c2097"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7a31fc1e1bd362874863fdeed71aed92d348f5336fd84f2197ba40c59f061bd"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba4cfa1052819d16699e1d55d18c92b6e094d4517c41dd231a8b9f87b6fa681"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f089118d584e859c62b3da0892b88a83d611c2033ac410e929cb6754eec0ed16"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:781e65134efaf88feb447e8c97a51772aa75e48b794352f94cb7ea717dedda0d"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31f5af773530fd3c658b32b6bdc2d0838543de70eb9a2156c03e410f7b0d3aae"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a7a078f5d37bee6690959c813977da5291b24286e7b962e62a94cec31aa5188b"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cea7daf9fc7ae6687cf1e2c049752f19f146fdc37c2cc376e7d0032cf4f25347"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b3489ff1ed1e8315674d0775dc7d2195fb13ca17b3808721b54dbe9fd020faf"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9403db39be1393618dd80c746cb22ccda168efce239c73af13c3763ef56ffc04"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5d97151bc92d2b2578ff7ce779cdb9174337390a535953cbb9452fb65164c587"}, + {file = "propcache-0.2.1-cp39-cp39-win32.whl", hash = "sha256:9caac6b54914bdf41bcc91e7eb9147d331d29235a7c967c150ef5df6464fd1bb"}, + {file = "propcache-0.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:92fc4500fcb33899b05ba73276dfb684a20d31caa567b7cb5252d48f896a91b1"}, + {file = "propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54"}, + {file = "propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64"}, +] + +[[package]] +name = "pycodestyle" +version = "2.12.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, + {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, +] + [[package]] name = "pydantic" -version = "2.8.2" +version = "2.10.5" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, - {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, + {file = "pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53"}, + {file = "pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff"}, ] [package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.20.1" -typing-extensions = [ - {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, - {version = ">=4.6.1", markers = "python_version < \"3.13\""}, -] +annotated-types = ">=0.6.0" +pydantic-core = "2.27.2" +typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.20.1" +version = "2.27.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, - {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, - {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, - {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, - {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, - {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, - {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, - {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, - {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, - {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, - {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, - {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, - {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, - {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, + {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, ] [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pyflakes" +version = "3.2.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, + {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, +] + +[[package]] +name = "pygments" +version = "2.19.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pytest" -version = "8.3.2" +version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, - {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, ] [package.dependencies] @@ -427,67 +1127,111 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] -name = "pytest-cov" -version = "5.0.0" -description = "Pytest plugin for measuring coverage." +name = "pytest-asyncio" +version = "0.25.2" +description = "Pytest support for asyncio" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, - {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, + {file = "pytest_asyncio-0.25.2-py3-none-any.whl", hash = "sha256:0d0bb693f7b99da304a0634afc0a4b19e49d5e0de2d670f38dc4bfa5727c5075"}, + {file = "pytest_asyncio-0.25.2.tar.gz", hash = "sha256:3f8ef9a98f45948ea91a0ed3dc4268b5326c0e7bce73892acc654df4262ad45f"}, ] [package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" +pytest = ">=8.2,<9" [package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] -name = "requests" -version = "2.32.3" -description = "Python HTTP for Humans." +name = "pytest-cov" +version = "5.0.0" +description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, ] [package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" [package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] -name = "requests-toolbelt" -version = "1.0.0" -description = "A utility belt for advanced users of python-requests" +name = "ruff" +version = "0.3.7" +description = "An extremely fast Python linter and code formatter, written in Rust." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.7" +groups = ["dev"] files = [ - {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, - {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, + {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce"}, + {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d28bdf3d7dc71dd46929fafeec98ba89b7c3550c3f0978e36389b5631b793663"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:379b67d4f49774ba679593b232dcd90d9e10f04d96e3c8ce4a28037ae473f7bb"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c060aea8ad5ef21cdfbbe05475ab5104ce7827b639a78dd55383a6e9895b7c51"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ebf8f615dde968272d70502c083ebf963b6781aacd3079081e03b32adfe4d58a"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d48098bd8f5c38897b03604f5428901b65e3c97d40b3952e38637b5404b739a2"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da8a4fda219bf9024692b1bc68c9cff4b80507879ada8769dc7e985755d662ea"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c44e0149f1d8b48c4d5c33d88c677a4aa22fd09b1683d6a7ff55b816b5d074f"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3050ec0af72b709a62ecc2aca941b9cd479a7bf2b36cc4562f0033d688e44fa1"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a29cc38e4c1ab00da18a3f6777f8b50099d73326981bb7d182e54a9a21bb4ff7"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b15cc59c19edca917f51b1956637db47e200b0fc5e6e1878233d3a938384b0b"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e491045781b1e38b72c91247cf4634f040f8d0cb3e6d3d64d38dcf43616650b4"}, + {file = "ruff-0.3.7-py3-none-win32.whl", hash = "sha256:bc931de87593d64fad3a22e201e55ad76271f1d5bfc44e1a1887edd0903c7d9f"}, + {file = "ruff-0.3.7-py3-none-win_amd64.whl", hash = "sha256:5ef0e501e1e39f35e03c2acb1d1238c595b8bb36cf7a170e7c1df1b73da00e74"}, + {file = "ruff-0.3.7-py3-none-win_arm64.whl", hash = "sha256:789e144f6dc7019d1f92a812891c645274ed08af6037d11fc65fcbc183b7d59f"}, + {file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"}, ] -[package.dependencies] -requests = ">=2.0.1,<3.0.0" - [[package]] name = "tomli" -version = "2.0.1" +version = "2.2.1" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_full_version <= \"3.11.0a6\"" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] @@ -496,29 +1240,110 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] -name = "urllib3" -version = "2.2.2" -description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "yarl" +version = "1.18.3" +description = "Yet another URL library" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, - {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690"}, + {file = "yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6"}, + {file = "yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a"}, + {file = "yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1"}, + {file = "yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285"}, + {file = "yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2"}, + {file = "yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8"}, + {file = "yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d"}, + {file = "yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c"}, + {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04"}, + {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719"}, + {file = "yarl-1.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1"}, + {file = "yarl-1.18.3-cp39-cp39-win32.whl", hash = "sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5"}, + {file = "yarl-1.18.3-cp39-cp39-win_amd64.whl", hash = "sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9"}, + {file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"}, + {file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"}, ] -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.0" [metadata] -lock-version = "2.0" -python-versions = ">=3.10" -content-hash = "f19125c79d2d577a626ea1de0922e09f9614ab609e313f5a5f98e67a14a454e2" +lock-version = "2.1" +python-versions = ">=3.10,<4.0" +content-hash = "31e3d0112e6f1a25d2bb15d81a2cc1f9606f67a07a93414b44e8139ecc180a62" diff --git a/pyoutlineapi/__init__.py b/pyoutlineapi/__init__.py new file mode 100644 index 0000000..58dfac9 --- /dev/null +++ b/pyoutlineapi/__init__.py @@ -0,0 +1,72 @@ +""" +PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. + +Copyright (c) 2025 Denis Rozhnovskiy +All rights reserved. + +This software is licensed under the MIT License. +You can find the full license text at: + https://opensource.org/licenses/MIT + +Source code repository: + https://github.com/orenlab/pyoutlineapi +""" + +import sys +from typing import TYPE_CHECKING + +if sys.version_info < (3, 10): + raise RuntimeError("PyOutlineAPI requires Python 3.10 or higher") + +from .client import AsyncOutlineClient, OutlineError, APIError + +if TYPE_CHECKING: + from .models import ( + AccessKey, + AccessKeyCreateRequest, + AccessKeyList, + DataLimit, + ErrorResponse, + ExperimentalMetrics, + MetricsPeriod, + MetricsStatusResponse, + Server, + ServerMetrics, + ) + +__version__: str = "0.2.0" +__author__ = "Denis Rozhnovskiy" +__email__ = "pytelemonbot@mail.ru" +__license__ = "MIT" + +PUBLIC_API = [ + "AsyncOutlineClient", + "OutlineError", + "APIError", + "AccessKey", + "AccessKeyCreateRequest", + "AccessKeyList", + "DataLimit", + "ErrorResponse", + "ExperimentalMetrics", + "MetricsPeriod", + "MetricsStatusResponse", + "Server", + "ServerMetrics", +] + +__all__ = PUBLIC_API + +# Actual imports for runtime +from .models import ( + AccessKey, + AccessKeyCreateRequest, + AccessKeyList, + DataLimit, + ErrorResponse, + ExperimentalMetrics, + MetricsPeriod, + MetricsStatusResponse, + Server, + ServerMetrics, +) diff --git a/pyoutlineapi/client.py b/pyoutlineapi/client.py index c29ea7c..a9971fe 100644 --- a/pyoutlineapi/client.py +++ b/pyoutlineapi/client.py @@ -1,211 +1,635 @@ -from typing import Union, Optional, Type +""" +PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. -import requests -from pydantic import BaseModel, ValidationError -from requests_toolbelt.adapters.fingerprint import FingerprintAdapter +Copyright (c) 2025 Denis Rozhnovskiy +All rights reserved. -from pyoutlineapi.exceptions import APIError -from pyoutlineapi.models import DataLimit, ServerPort, Metrics, AccessKeyList, AccessKey, AccessKeyCreateRequest, Server +This software is licensed under the MIT License. +You can find the full license text at: + https://opensource.org/licenses/MIT +Source code repository: + https://github.com/orenlab/pyoutlineapi +""" + +from __future__ import annotations + +import binascii +from functools import wraps +from typing import ( + Any, + Literal, + TypeAlias, + Union, + overload, + Optional, + ParamSpec, + TypeVar, + Callable, +) +from urllib.parse import urlparse + +import aiohttp +from aiohttp import ClientResponse, Fingerprint +from pydantic import BaseModel + +from .models import ( + AccessKey, + AccessKeyCreateRequest, + AccessKeyList, + DataLimit, + ErrorResponse, + MetricsPeriod, + MetricsStatusResponse, + Server, + ServerMetrics, +) + +# Type variables for decorator +P = ParamSpec("P") +T = TypeVar("T") + +# Type aliases +JsonDict: TypeAlias = dict[str, Any] +ResponseType = Union[JsonDict, BaseModel] + + +class OutlineError(Exception): + """Base exception for Outline client errors.""" -class PyOutlineWrapper: - """ - Class for interacting with the Outline VPN Server API. - This class provides methods to interact with the Outline VPN Server, including: +class APIError(OutlineError): + """Raised when API requests fail.""" - - Retrieving server information - - Creating, listing, and deleting access keys - - Updating server ports - - Setting and removing data limits for access keys - - Retrieving metrics + def __init__(self, message: str, status_code: Optional[int] = None) -> None: + super().__init__(message) + self.status_code = status_code - The class uses the `requests` library for making HTTP requests and `pydantic` for data validation. - Responses can be returned either as Pydantic models or in JSON format, depending on the `json_format` parameter. + +def ensure_context(func: Callable[P, T]) -> Callable[P, T]: + """Decorator to ensure client session is initialized.""" + + @wraps(func) + async def wrapper(self: AsyncOutlineClient, *args: P.args, **kwargs: P.kwargs) -> T: + if not self._session or self._session.closed: + raise RuntimeError("Client session is not initialized or already closed.") + return await func(self, *args, **kwargs) + + return wrapper + + +class AsyncOutlineClient: + """ + Asynchronous client for the Outline VPN Server API. + + Args: + api_url: Base URL for the Outline server API + cert_sha256: SHA-256 fingerprint of the server's TLS certificate + json_format: Return raw JSON instead of Pydantic models + timeout: Request timeout in seconds + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... server_info = await client.get_server_info() """ - def __init__(self, api_url: str, cert_sha256: str, verify_tls: bool = True, json_format: bool = True): + def __init__( + self, + api_url: str, + cert_sha256: str, + *, + json_format: bool = True, + timeout: float = 30.0, + ) -> None: + self._api_url = api_url.rstrip("/") + self._cert_sha256 = cert_sha256 + self._json_format = json_format + self._timeout = aiohttp.ClientTimeout(total=timeout) + self._ssl_context: Optional[Fingerprint] = None + self._session: Optional[aiohttp.ClientSession] = None + + async def __aenter__(self) -> AsyncOutlineClient: + """Set up client session for context manager.""" + self._session = aiohttp.ClientSession( + timeout=self._timeout, + raise_for_status=False, + connector=aiohttp.TCPConnector(ssl=self._get_ssl_context()), + ) + return self + + async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + """Clean up client session.""" + if self._session: + await self._session.close() + self._session = None + + @overload + async def _parse_response( + self, + response: ClientResponse, + model: type[BaseModel], + json_format: Literal[True], + ) -> JsonDict: ... + + @overload + async def _parse_response( + self, + response: ClientResponse, + model: type[BaseModel], + json_format: Literal[False], + ) -> BaseModel: ... + + @overload + async def _parse_response( + self, response: ClientResponse, model: type[BaseModel], json_format: bool + ) -> Union[JsonDict, BaseModel]: ... + + @ensure_context + async def _parse_response( + self, response: ClientResponse, model: type[BaseModel], json_format: bool = True + ) -> ResponseType: """ - Initializes the PyOutlineWrapper with the given API URL, certificate fingerprint, and options for TLS verification - and response format. + Parse and validate API response data. Args: - api_url (str): The base URL of the Outline VPN Server API. - cert_sha256 (str): The SHA-256 fingerprint of the server's certificate. - verify_tls (bool, optional): Whether to verify the server's TLS certificate. Defaults to True. - json_format (bool, optional): Whether to return responses in JSON format. Defaults to True. + response: API response to parse + model: Pydantic model for validation + json_format: Whether to return raw JSON + + Returns: + Validated response data + + Raises: + ValueError: If response validation fails """ - self._api_url = api_url - self._cert_sha256 = cert_sha256 - self._verify_tls = verify_tls - self._json_format = json_format - self._session = requests.Session() - self._session.mount(self._api_url, FingerprintAdapter(self._cert_sha256)) + try: + data = await response.json() + validated = model.model_validate(data) + return validated.model_dump() if json_format else validated + except aiohttp.ContentTypeError as e: + raise ValueError("Invalid response format") from e + except Exception as e: + raise ValueError(f"Validation error: {e}") from e + + @staticmethod + async def _handle_error_response(response: ClientResponse) -> None: + """Handle error responses from the API.""" + try: + error_data = await response.json() + error = ErrorResponse.model_validate(error_data) + raise APIError(f"{error.code}: {error.message}", response.status) + except ValueError: + raise APIError( + f"HTTP {response.status}: {response.reason}", response.status + ) + + @ensure_context + async def _request( + self, + method: str, + endpoint: str, + *, + json: Any = None, + params: Optional[dict[str, Any]] = None, + ) -> Any: + """Make an API request.""" + url = self._build_url(endpoint) + + async with self._session.request( + method, + url, + json=json, + params=params, + raise_for_status=False, + ) as response: + if response.status >= 400: + await self._handle_error_response(response) + + if response.status == 204: + return True + + try: + await response.json() + return response + except aiohttp.ContentTypeError: + return await response.text() + except Exception as e: + raise APIError(f"Failed to parse response: {e}", response.status) + + def _build_url(self, endpoint: str) -> str: + """Build and validate the full URL for the API request.""" + if not isinstance(endpoint, str): + raise ValueError("Endpoint must be a string") + + url = f"{self._api_url}/{endpoint.lstrip('/')}" + parsed_url = urlparse(url) + + if not all([parsed_url.scheme, parsed_url.netloc]): + raise ValueError(f"Invalid URL: {url}") + + return url + + def _get_ssl_context(self) -> Optional[Fingerprint]: + """Create an SSL context if a certificate fingerprint is provided.""" + if not self._cert_sha256: + return None + + try: + return Fingerprint(binascii.unhexlify(self._cert_sha256)) + except binascii.Error as e: + raise ValueError(f"Invalid certificate SHA256: {self._cert_sha256}") from e + except Exception as e: + raise OutlineError("Failed to create SSL context") from e + + async def get_server_info(self) -> Union[JsonDict, Server]: + """ + Get server information. + + Returns: + Server information including name, ID, and configuration. + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... server = await client.get_server_info() + ... print(f"Server {server.name} running version {server.version}") + """ + response = await self._request("GET", "server") + return await self._parse_response( + response, Server, json_format=self._json_format + ) + + async def rename_server(self, name: str) -> bool: + """ + Rename the server. + + Args: + name: New server name - def _request(self, method: str, endpoint: str, json_data=None) -> requests.Response: + Returns: + True if successful + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... success = await client.rename_server("My VPN Server") + ... if success: + ... print("Server renamed successfully") """ - Makes an HTTP request to the API. + return await self._request("PUT", "name", json={"name": name}) + + async def set_hostname(self, hostname: str) -> bool: + """ + Set server hostname for access keys. Args: - method (str): The HTTP method to use (e.g., 'GET', 'POST', 'PUT', 'DELETE'). - endpoint (str): The API endpoint to call. - json_data (optional): The JSON data to send with the request. + hostname: New hostname or IP address Returns: - requests.Response: The HTTP response object. + True if successful Raises: - APIError: If the request fails or the response status is not successful. + APIError: If hostname is invalid + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... await client.set_hostname("vpn.example.com") + ... # Or use IP address + ... await client.set_hostname("203.0.113.1") """ - url = f"{self._api_url}/{endpoint}" - try: - response = self._session.request( - method, - url, - json=json_data, - verify=self._verify_tls, - timeout=15 - ) - response.raise_for_status() - return response - except requests.RequestException as exception: - raise APIError(f"Request to {url} failed: {exception}") + return await self._request( + "PUT", "server/hostname-for-access-keys", json={"hostname": hostname} + ) - def _parse_response(self, response: requests.Response, model: Type[BaseModel]) -> Union[BaseModel, str]: + async def set_default_port(self, port: int) -> bool: """ - Parses the response from the API. + Set default port for new access keys. Args: - response (requests.Response): The HTTP response object. - model (Type[BaseModel]): The Pydantic model to validate the response data. + port: Port number (1025-65535) Returns: - Union[BaseModel, str]: The validated data as a Pydantic model or a JSON string, depending on the json_format parameter. + True if successful Raises: - ValidationError: If the response data does not match the Pydantic model. + APIError: If port is invalid or in use + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... await client.set_default_port(8388) + """ - try: - json_data = response.json() - data = model.model_validate(json_data) - return data.model_dump_json() if self._json_format else data - except ValidationError as e: - raise ValidationError(f"Validation error: {e}") + if port < 1025 or port > 65535: + raise ValueError("Privileged ports are not allowed. Use range: 1025-65535") + + return await self._request( + "PUT", "server/port-for-new-access-keys", json={"port": port} + ) - def get_server_info(self) -> Union[Server, str]: + async def get_metrics_status(self) -> dict[str, Any] | BaseModel: """ - Retrieves information about the Outline VPN server. + Get whether metrics collection is enabled. Returns: - Union[Server, str]: The server information as a Pydantic model or a JSON string. + Current metrics collection status + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... if await client.get_metrics_status(): + ... print("Metrics collection is enabled") """ - response = self._request("GET", "server") - return self._parse_response(response, Server) + response = await self._request("GET", "metrics/enabled") + data = await self._parse_response( + response, MetricsStatusResponse, json_format=self._json_format + ) + return data - def create_access_key(self, name: Optional[str] = None, password: Optional[str] = None, - port: Optional[int] = None) -> Union[AccessKey, str]: + async def set_metrics_status(self, enabled: bool) -> bool: """ - Creates a new access key. + Enable or disable metrics collection. Args: - name (Optional[str]): The name of the access key. - password (Optional[str]): The password for the access key. - port (Optional[int]): The port for the access key. + enabled: Whether to enable metrics Returns: - Union[AccessKey, str]: The created access key as a Pydantic model or a JSON string. + True if successful + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... # Enable metrics + ... await client.set_metrics_status(True) + ... # Check new status + ... is_enabled = await client.get_metrics_status() """ - request_data = { - "name": name, - "password": password, - "port": port, - } - request_data = {key: value for key, value in request_data.items() if value is not None} + return await self._request( + "PUT", "metrics/enabled", json={"metricsEnabled": enabled} + ) - if request_data: - request_data = AccessKeyCreateRequest(**request_data).model_dump() + async def get_transfer_metrics( + self, period: MetricsPeriod = MetricsPeriod.MONTHLY + ) -> Union[JsonDict, ServerMetrics]: + """ + Get transfer metrics for specified period. - response = self._request("POST", "access-keys", json_data=request_data) - return self._parse_response(response, AccessKey) + Args: + period: Time period for metrics (DAILY, WEEKLY, or MONTHLY) - def get_access_keys(self) -> Union[AccessKeyList, str]: + Returns: + Transfer metrics data for each access key + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... # Get monthly metrics + ... metrics = await client.get_transfer_metrics() + ... # Or get daily metrics + ... daily = await client.get_transfer_metrics(MetricsPeriod.DAILY) + ... for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items(): + ... print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB") + """ + response = await self._request( + "GET", "metrics/transfer", params={"period": period.value} + ) + return await self._parse_response( + response, ServerMetrics, json_format=self._json_format + ) + + async def create_access_key( + self, + *, + name: Optional[str] = None, + password: Optional[str] = None, + port: Optional[int] = None, + method: Optional[str] = None, + limit: Optional[DataLimit] = None, + ) -> Union[JsonDict, AccessKey]: """ - Retrieves a list of all access keys. + Create a new access key. + + Args: + name: Optional key name + password: Optional password + port: Optional port number (1-65535) + method: Optional encryption method + limit: Optional data transfer limit Returns: - Union[AccessKeyList, str]: The list of access keys as a Pydantic model or a JSON string. + New access key details + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... # Create basic key + ... key = await client.create_access_key(name="User 1") + ... + ... # Create key with data limit + ... _limit = DataLimit(bytes=5 * 1024**3) # 5 GB + ... key = await client.create_access_key( + ... name="Limited User", + ... port=8388, + ... limit=_limit + ... ) + ... print(f"Created key: {key.access_url}") """ - response = self._request("GET", "access-keys") - return self._parse_response(response, AccessKeyList) + request = AccessKeyCreateRequest( + name=name, password=password, port=port, method=method, limit=limit + ) + response = await self._request( + "POST", "access-keys", json=request.model_dump(exclude_none=True) + ) + return await self._parse_response( + response, AccessKey, json_format=self._json_format + ) + + async def get_access_keys(self) -> Union[JsonDict, AccessKeyList]: + """ + Get all access keys. - def delete_access_key(self, key_id: str) -> bool: + Returns: + List of all access keys + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... keys = await client.get_access_keys() + ... for key in keys.access_keys: + ... print(f"Key {key.id}: {key.name or 'unnamed'}") + ... if key.data_limit: + ... print(f" Limit: {key.data_limit.bytes / 1024**3:.1f} GB") """ - Deletes an access key by its ID. + response = await self._request("GET", "access-keys") + return await self._parse_response( + response, AccessKeyList, json_format=self._json_format + ) + + async def get_access_key(self, key_id: int) -> Union[JsonDict, AccessKey]: + """ + Get specific access key. Args: - key_id (str): The ID of the access key to delete. + key_id: Access key ID Returns: - bool: True if the access key was successfully deleted, False otherwise. + Access key details + + Raises: + APIError: If key doesn't exist + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... key = await client.get_access_key(1) + ... print(f"Port: {key.port}") + ... print(f"URL: {key.access_url}") """ - response = self._request("DELETE", f"access-keys/{key_id}") - return response.status_code == 204 + response = await self._request("GET", f"access-keys/{key_id}") + return await self._parse_response( + response, AccessKey, json_format=self._json_format + ) - def update_server_port(self, port: int) -> bool: + async def rename_access_key(self, key_id: int, name: str) -> bool: """ - Updates the port for new access keys on the server. + Rename access key. Args: - port (int): The new port number. + key_id: Access key ID + name: New name Returns: - bool: True if the port was successfully updated, False otherwise. + True if successful Raises: - APIError: If the port is already in use. + APIError: If key doesn't exist + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... # Rename key + ... await client.rename_access_key(1, "Alice") + ... + ... # Verify new name + ... key = await client.get_access_key(1) + ... assert key.name == "Alice" """ - verified_port = ServerPort(port=port) - response = self._request("PUT", "server/port-for-new-access-keys", {"port": verified_port.port}) - if response.status_code == 409: - raise APIError(f"Port {verified_port.port} is already in use") - return response.status_code == 204 + return await self._request( + "PUT", f"access-keys/{key_id}/name", json={"name": name} + ) - def set_access_key_data_limit(self, key_id: str, limit: DataLimit) -> bool: + async def delete_access_key(self, key_id: int) -> bool: """ - Sets a data limit for an access key. + Delete access key. Args: - key_id (str): The ID of the access key. - limit (DataLimit): The data limit to set. + key_id: Access key ID Returns: - bool: True if the data limit was successfully set, False otherwise. + True if successful + + Raises: + APIError: If key doesn't exist + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... if await client.delete_access_key(1): + ... print("Key deleted") + """ - response = self._request("PUT", f"access-keys/{key_id}/data-limit", {"bytes": limit.bytes}) - return response.status_code == 204 + return await self._request("DELETE", f"access-keys/{key_id}") - def get_metrics(self) -> Union[Metrics, str]: + async def set_access_key_data_limit(self, key_id: int, bytes_limit: int) -> bool: """ - Retrieves transfer metrics from the server. + Set data transfer limit for access key. + + Args: + key_id: Access key ID + bytes_limit: Limit in bytes (must be positive) Returns: - Union[Metrics, str]: The metrics data as a Pydantic model or a JSON string. + True if successful + + Raises: + APIError: If key doesn't exist or limit is invalid + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... # Set 5 GB limit + ... limit = 5 * 1024**3 # 5 GB in bytes + ... await client.set_access_key_data_limit(1, limit) + ... + ... # Verify limit + ... key = await client.get_access_key(1) + ... assert key.data_limit and key.data_limit.bytes == limit """ - response = self._request("GET", "metrics/transfer") - return self._parse_response(response, Metrics) + return await self._request( + "PUT", + f"access-keys/{key_id}/data-limit", + json={"limit": {"bytes": bytes_limit}}, + ) - def remove_access_key_data_limit(self, key_id: str) -> bool: + async def remove_access_key_data_limit(self, key_id: int) -> bool: """ - Removes the data limit for an access key. + Remove data transfer limit from access key. Args: - key_id (str): The ID of the access key. + key_id: Access key ID Returns: - bool: True if the data limit was successfully removed, False otherwise. - """ - response = self._request("DELETE", f"access-keys/{key_id}/data-limit") - return response.status_code == 204 + True if successful + Raises: + APIError: If key doesn't exist + """ + return await self._request("DELETE", f"access-keys/{key_id}/data-limit") -__all__ = ["PyOutlineWrapper"] + @property + def session(self): + return self._session diff --git a/pyoutlineapi/exceptions.py b/pyoutlineapi/exceptions.py deleted file mode 100644 index ee2d807..0000000 --- a/pyoutlineapi/exceptions.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -Copyright (c) 2024 Denis Rozhnovskiy - -This file is part of the PyOutlineAPI project. - -PyOutlineAPI is a Python package for interacting with the Outline VPN Server. - -Licensed under the MIT License. See the LICENSE file for more details. - -""" - - -class APIError(Exception): - """Base class for all API-related errors.""" - - def __init__(self, message: str): - super().__init__(message) - self.message = message - - def __str__(self): - return self.message - - -class HTTPError(APIError): - """Raised for HTTP-related errors (e.g., HTTP status codes).""" - - def __init__(self, status_code: int, message: str): - self.status_code = status_code - super().__init__(message) - - def __str__(self): - return f"HTTP error occurred: {self.status_code} - {self.message}" - - -class RequestError(APIError): - """Raised for request-related errors (e.g., connection issues).""" - - def __init__(self, message: str): - super().__init__(f"An error occurred while requesting data: {message}") - - -class ValidationError(APIError): - """Raised for validation errors when processing API responses.""" - - def __init__(self, message: str): - super().__init__(f"Validation error occurred: {message}") diff --git a/pyoutlineapi/logger.py b/pyoutlineapi/logger.py deleted file mode 100644 index 8f65453..0000000 --- a/pyoutlineapi/logger.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -Copyright (c) 2024 Denis Rozhnovskiy - -This file is part of the PyOutlineAPI project. - -PyOutlineAPI is a Python package for interacting with the Outline VPN Server. - -Licensed under the MIT License. See the LICENSE file for more details. - -""" - -import logging - - -def setup_logger(name: str) -> logging.Logger: - """ - Set up a logger with a specified name. - - Args: - name (str): The name of the logger. - - Returns: - logging.Logger: Configured logger instance. - """ - logger = logging.getLogger(name) - logger.setLevel(logging.DEBUG) # Set the base logging level - - # Create a console handler with a specific format - ch = logging.StreamHandler() - ch.setLevel(logging.DEBUG) # Set the level for the console handler - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - ch.setFormatter(formatter) - - # Add the handler to the logger - logger.addHandler(ch) - - return logger diff --git a/pyoutlineapi/models.py b/pyoutlineapi/models.py index b7ae056..3ad3bce 100644 --- a/pyoutlineapi/models.py +++ b/pyoutlineapi/models.py @@ -1,139 +1,144 @@ """ -Copyright (c) 2024 Denis Rozhnovskiy +PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. -This file is part of the PyOutlineAPI project. +Copyright (c) 2025 Denis Rozhnovskiy +All rights reserved. -PyOutlineAPI is a Python package for interacting with the Outline VPN Server. +This software is licensed under the MIT License. +You can find the full license text at: + https://opensource.org/licenses/MIT -Licensed under the MIT License. See the LICENSE file for more details. +Source code repository: + https://github.com/orenlab/pyoutlineapi """ -from typing import Optional, List, Dict +from enum import Enum +from typing import Optional -from pydantic import BaseModel, Field, constr, field_validator +from pydantic import BaseModel, Field, field_validator -class Server(BaseModel): - """ - Model for server information. - - Attributes: - name (str): The name of the server. - serverId (str): The unique identifier for the server. - metricsEnabled (bool): Indicates if metrics collection is enabled. - createdTimestampMs (int): The timestamp when the server was created, must be non-negative. - portForNewAccessKeys (int): The port used for new access keys, must be between 1 and 65535. - """ - name: str - serverId: str - metricsEnabled: bool - createdTimestampMs: int = Field(ge=0, description="Timestamp must be non-negative") - portForNewAccessKeys: int = Field(ge=1, le=65535, description="Port must be between 1 and 65535") +class MetricsPeriod(str, Enum): + """Time periods for metrics collection.""" + + DAILY = "daily" + WEEKLY = "weekly" + MONTHLY = "monthly" class DataLimit(BaseModel): - """ - Model for data limit information. + """Data transfer limit configuration.""" - Attributes: - bytes (int): The data limit in bytes, must be non-negative. - """ - bytes: int = Field(ge=0, description="Data limit in bytes must be non-negative") + bytes: int = Field(gt=0) + + @field_validator("bytes") + def validate_bytes(cls, v: int) -> int: + if v < 0: + raise ValueError("bytes must be positive") + return v class AccessKey(BaseModel): - """ - Model for access key information. - - Attributes: - id (str): The unique identifier for the access key. - name (str): The name of the access key. - password (str): The password for the access key, must not be empty. - port (int): The port used by the access key, must be between 1 and 65535. - method (str): The encryption method used by the access key. - accessUrl (str): The URL used to access the server, must not be empty. - """ - id: str - name: str - password: str = Field(..., min_length=1, description="Password must not be empty") - port: int = Field(ge=1, le=65535, description="Port must be between 1 and 65535") + """Access key details.""" + + id: int + name: Optional[str] = None + password: str + port: int = Field(gt=0, lt=65536) method: str - accessUrl: str = Field(..., min_length=1, description="Access URL must not be empty") + access_url: str = Field(alias="accessUrl") + data_limit: Optional[DataLimit] = Field(None, alias="dataLimit") -class ServerPort(BaseModel): - """ - Model for server port information. +class AccessKeyList(BaseModel): + """List of access keys.""" - Attributes: - port (int): The port used by the server, must be between 1 and 65535. - """ - port: int = Field(ge=1, le=65535, description="Port must be between 1 and 65535") + access_keys: list[AccessKey] = Field(alias="accessKeys") -class AccessKeyCreateRequest(BaseModel): +class ServerMetrics(BaseModel): """ - Model for creating access key information. - - Attributes: - name (Optional[str]): The name of the access key (optional). - password (Optional[str]): The password for the access key (optional). - port (Optional[int]): The port used by the access key, must be between 0 and 65535 (optional). + Server metrics data for data transferred per access key + Per OpenAPI: /metrics/transfer endpoint """ - name: Optional[str] - password: Optional[str] - port: Optional[int] = Field(ge=0, le=65535, description="Port must be between 0 and 65535") + bytes_transferred_by_user_id: dict[str, int] = Field( + alias="bytesTransferredByUserId" + ) -class AccessKeyList(BaseModel): - """ - Model for access key list information. - Attributes: - accessKeys (List[AccessKey]): A list of access keys. - """ - accessKeys: List[AccessKey] +class TunnelData(BaseModel): + seconds: int + +class TransferData(BaseModel): + bytes: int -class MetricsEnabled(BaseModel): + +class ServerMetric(BaseModel): + location: str + asn: Optional[int] = None + as_org: Optional[str] = Field(None, alias="asOrg") + tunnel_time: TunnelData = Field(alias="tunnelTime") + data_transferred: TransferData = Field(alias="dataTransferred") + + +class AccessKeyMetric(BaseModel): + access_key_id: int = Field(alias="accessKeyId") + tunnel_time: TunnelData = Field(alias="tunnelTime") + data_transferred: TransferData = Field(alias="dataTransferred") + + +class ExperimentalMetrics(BaseModel): """ - Model for metrics enabled information. + Experimental metrics data structure + Per OpenAPI: /experimental/server/metrics endpoint + """ + + server: list[ServerMetric] + access_keys: list[AccessKeyMetric] = Field(alias="accessKeys") + - Attributes: - enabled (bool): Indicates if metrics collection is enabled. +class Server(BaseModel): + """ + Server information. + Per OpenAPI: /server endpoint schema """ - enabled: bool + + name: str + server_id: str = Field(alias="serverId") + metrics_enabled: bool = Field(alias="metricsEnabled") + created_timestamp_ms: int = Field(alias="createdTimestampMs") + version: str + port_for_new_access_keys: int = Field(alias="portForNewAccessKeys", gt=0, lt=65536) + hostname_for_access_keys: Optional[str] = Field(None, alias="hostnameForAccessKeys") + access_key_data_limit: Optional[DataLimit] = Field(None, alias="accessKeyDataLimit") -class Metrics(BaseModel): +class AccessKeyCreateRequest(BaseModel): """ - Model for metrics information. + Request parameters for creating an access key. + Per OpenAPI: /access-keys POST request body + """ + + name: Optional[str] = None + method: Optional[str] = None + password: Optional[str] = None + port: Optional[int] = Field(None, gt=0, lt=65536) + limit: Optional[DataLimit] = None - Attributes: - bytesTransferredByUserId (Dict[str, int]): A dictionary mapping user IDs to the number of bytes transferred. - User IDs must be non-empty strings, and byte values must be non-negative. - Methods: - validate_bytes_transferred: Validates that all byte values in the dictionary are non-negative. +class MetricsStatusResponse(BaseModel): + """Response for /metrics/enabled endpoint""" + + metrics_enabled: bool = Field(alias="metricsEnabled") + + +class ErrorResponse(BaseModel): """ - bytesTransferredByUserId: Dict[constr(min_length=1), int] = Field( - description="User IDs must be non-empty strings and byte values must be non-negative") - - @field_validator("bytesTransferredByUserId") - def validate_bytes_transferred(cls, value: Dict[str, int]) -> Dict[str, int]: - """ - Validate that all byte values in the dictionary are non-negative. - - Args: - value (Dict[str, int]): The dictionary to validate. - - Returns: - Dict[str, int]: The validated dictionary. - - Raises: - ValueError: If any byte value is negative. - """ - for user_id, bytes_transferred in value.items(): - if bytes_transferred < 0: - raise ValueError(f"Transferred bytes for user {user_id} must be non-negative") - return value + Error response structure + Per OpenAPI: 404 and 400 responses + """ + + code: str + message: str diff --git a/pyproject.toml b/pyproject.toml index bbfe5c8..c6eb33c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,34 +1,76 @@ [tool.poetry] name = "pyoutlineapi" -version = "0.1.2" -description = "A Python package to interact with the Outline VPN Server API" +version = "0.2.0" +description = "A modern, async-first Python client for the Outline VPN Server API with comprehensive data validation through Pydantic models." authors = ["Denis Rozhnovskiy "] readme = "README.md" license = "MIT" packages = [{ include = "pyoutlineapi" }] -keywords = ["outline", "vpn", "api", "manager", "wrapper"] +keywords = ["outline", "vpn", "api", "manager", "wrapper", "asyncio"] +documentation = "https://github.com/orenlab/pyoutlineapi/blob/main/README.md" classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", + "Intended Audience :: Developers", + "Topic :: Internet :: WWW/HTTP :: HTTP Servers", + "Topic :: Security", + "Topic :: Internet :: Proxy Servers", + "Programming Language :: Python :: 3 :: Only", + "Typing :: Typed", + "Development Status :: 5 - Production/Stable", + "Framework :: AsyncIO", + "Framework :: aiohttp", + "Framework :: Pydantic", ] [tool.poetry.dependencies] -python = ">=3.10" -pydantic = "^2.0.0" -requests = "^2.32.3" -requests-toolbelt = "^1.0.0" +python = ">=3.10,<4.0" +pydantic = "^2.9.2" +aiohttp = "^3.11.11" +flake8 = "^7.1.1" [tool.poetry.group.dev.dependencies] +flake8 = "^7.1.1" +pytest = "^8.3.4" +pytest-asyncio = "^0.25.2" pytest-cov = "^5.0.0" +black = "^24.10.0" +mypy = "^1.0.0" +ruff = "^0.3.0" +pdoc = "^15.0.1" [tool.pytest.ini_options] -addopts = "--cov=pyoutlineapi --cov-report=term" +asyncio_mode = "auto" +testpaths = ["tests"] +python_files = ["test_*.py"] +addopts = "-v --cov=pyoutlineapi --cov-report=html --cov-report=xml" + +[tool.black] +line-length = 88 +target-version = ["py310", "py311", "py312"] +include = '\.pyi?$' + +[tool.mypy] +python_version = "3.10" +strict = true +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true + +[tool.ruff] +line-length = 88 +target-version = "py310" +lint.select = ["E", "F", "B", "I"] [tool.poetry.urls] homepage = "https://github.com/orenlab/pyoutlineapi" -repository = "https://github.com/orenlab/pyoutlineapi" +documentation = "https://github.com/orenlab/pyoutlineapi/blob/main/README.md" +changelog = "https://github.com/orenlab/pyoutlineapi/blob/main/CHANGELOG.md" [build-system] requires = ["poetry-core>=1.3.0"] diff --git a/tests/test_client.py b/tests/test_client.py deleted file mode 100644 index 27c94f9..0000000 --- a/tests/test_client.py +++ /dev/null @@ -1,133 +0,0 @@ -import unittest -from unittest.mock import patch, Mock - -import requests - -from pyoutlineapi.client import PyOutlineWrapper -from pyoutlineapi.exceptions import APIError -from pyoutlineapi.models import Server, DataLimit - - -class TestPyOutlineWrapper(unittest.TestCase): - - def setUp(self): - self.api_url = "https://example.com" - self.cert_sha256 = "dummy-sha256" - self.wrapper = PyOutlineWrapper(api_url=self.api_url, cert_sha256=self.cert_sha256) - - @patch("pyoutlineapi.client.requests.Session.request") - def test_request_success(self, mock_request): - mock_response = Mock() - mock_response.status_code = 200 - mock_response.json.return_value = {} - mock_request.return_value = mock_response - - response = self.wrapper._request("GET", "test-endpoint") - self.assertEqual(response.status_code, 200) - - @patch("pyoutlineapi.client.requests.Session.request") - def test_request_failure(self, mock_request): - mock_request.side_effect = requests.RequestException("Connection error") - - with self.assertRaises(APIError): - self.wrapper._request("GET", "test-endpoint") - - @patch("pyoutlineapi.client.requests.Response.json") - def test_parse_response_success(self, mock_json): - mock_json.return_value = { - "name": "Test Server", - "serverId": "12345", - "metricsEnabled": True, - "createdTimestampMs": 1609459200000, - "portForNewAccessKeys": 8080 - } - - response = Mock() - response.json = mock_json - - result = self.wrapper._parse_response(response, Server) - self.assertIsInstance(result, str) # JSON format is True by default - - @patch.object(PyOutlineWrapper, '_request') - def test_get_server_info(self, mock_request): - mock_request.return_value.json.return_value = { - "name": "Test Server", - "serverId": "12345", - "metricsEnabled": True, - "createdTimestampMs": 1609459200000, - "portForNewAccessKeys": 8080 - } - - result = self.wrapper.get_server_info() - self.assertIsInstance(result, str) - - @patch.object(PyOutlineWrapper, '_request') - def test_create_access_key(self, mock_request): - mock_request.return_value.json.return_value = { - "id": "test_id", - "name": "Test Access Key", - "password": "secret", - "port": 8080, - "method": "aes-256-gcm", - "accessUrl": "ss://..." - } - - result = self.wrapper.create_access_key(name="Test Access Key", password="secret", port=8080) - self.assertIsInstance(result, str) - - @patch.object(PyOutlineWrapper, '_request') - def test_get_access_keys(self, mock_request): - mock_request.return_value.json.return_value = { - "accessKeys": [{ - "id": "test_id", - "name": "Test Access Key", - "password": "secret", - "port": 8080, - "method": "aes-256-gcm", - "accessUrl": "ss://..." - }] - } - - result = self.wrapper.get_access_keys() - self.assertIsInstance(result, str) - - @patch.object(PyOutlineWrapper, '_request') - def test_delete_access_key(self, mock_request): - mock_request.return_value.status_code = 204 - - result = self.wrapper.delete_access_key("test_id") - self.assertTrue(result) - - @patch.object(PyOutlineWrapper, '_request') - def test_update_server_port(self, mock_request): - mock_request.return_value.status_code = 204 - - result = self.wrapper.update_server_port(8080) - self.assertTrue(result) - - @patch.object(PyOutlineWrapper, '_request') - def test_set_access_key_data_limit(self, mock_request): - mock_request.return_value.status_code = 204 - - data_limit = DataLimit(bytes=1000000) - result = self.wrapper.set_access_key_data_limit("test_id", data_limit) - self.assertTrue(result) - - @patch.object(PyOutlineWrapper, '_request') - def test_remove_access_key_data_limit(self, mock_request): - mock_request.return_value.status_code = 204 - - result = self.wrapper.remove_access_key_data_limit("test_id") - self.assertTrue(result) - - @patch.object(PyOutlineWrapper, '_request') - def test_get_metrics(self, mock_request): - mock_request.return_value.json.return_value = { - "bytesTransferredByUserId": { - "user1": 1000, - "user2": 2000 - } - } - - result = self.wrapper.get_metrics() - self.assertIsInstance(result, str) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py deleted file mode 100644 index aed32ff..0000000 --- a/tests/test_exceptions.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -Copyright (c) 2024 Denis Rozhnovskiy - -This file is part of the PyOutlineAPI project. - -PyOutlineAPI is a Python package for interacting with the Outline VPN Server. - -Licensed under the MIT License. See the LICENSE file for more details. - -""" - -import unittest -from pyoutlineapi.exceptions import APIError, HTTPError, RequestError, ValidationError - -class TestAPIError(unittest.TestCase): - def test_api_error_initialization(self): - """Test initialization of APIError.""" - error = APIError("Test API Error") - self.assertEqual(str(error), "Test API Error") - self.assertEqual(error.message, "Test API Error") - -class TestHTTPError(unittest.TestCase): - def test_http_error_initialization(self): - """Test initialization of HTTPError.""" - error = HTTPError(404, "Not Found") - self.assertEqual(str(error), "HTTP error occurred: 404 - Not Found") - self.assertEqual(error.status_code, 404) - self.assertEqual(error.message, "Not Found") - -class TestRequestError(unittest.TestCase): - def test_request_error_initialization(self): - """Test initialization of RequestError.""" - error = RequestError("Connection failed") - self.assertEqual(str(error), "An error occurred while requesting data: Connection failed") - -class TestValidationError(unittest.TestCase): - def test_validation_error_initialization(self): - """Test initialization of ValidationError.""" - error = ValidationError("Invalid data format") - self.assertEqual(str(error), "Validation error occurred: Invalid data format") - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/test_logger.py b/tests/test_logger.py deleted file mode 100644 index a3f6f69..0000000 --- a/tests/test_logger.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Copyright (c) 2024 Denis Rozhnovskiy - -This file is part of the PyOutlineAPI project. - -PyOutlineAPI is a Python package for interacting with the Outline VPN Server. - -Licensed under the MIT License. See the LICENSE file for more details. - -""" -import io -import logging -import unittest -from logging import StreamHandler - -from pyoutlineapi import logger - - -class TestLoggerSetup(unittest.TestCase): - def setUp(self): - """Setup for test cases.""" - self.logger_name = 'test_logger' - self.logger = logger.setup_logger(self.logger_name) - self.log_stream = io.StringIO() - # Redirect log output to the StringIO object - handler = StreamHandler(self.log_stream) - handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) - self.logger.addHandler(handler) - self.logger.setLevel(logging.DEBUG) - - def tearDown(self): - """Teardown for test cases.""" - self.logger.handlers.clear() - - def test_logger_name(self): - """Test if the logger has the correct name.""" - self.assertEqual(self.logger.name, self.logger_name) - - def test_logger_level(self): - """Test if the logger's level is set to DEBUG.""" - self.assertEqual(self.logger.level, logging.DEBUG) - - def test_logging_format(self): - """Test if the logging format is correct.""" - self.logger.info('Test message') - log_contents = self.log_stream.getvalue() - self.assertIn('Test message', log_contents) - self.assertIn('INFO', log_contents) - self.assertIn(self.logger_name, log_contents) - self.assertIn(' - ', log_contents) - self.assertIn('-', log_contents) # Ensure the format contains '-' - - def test_logger_output(self): - """Test if the log message is correctly output.""" - self.logger.info('Test log output') - log_contents = self.log_stream.getvalue() - self.assertIn('Test log output', log_contents) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_models.py b/tests/test_models.py deleted file mode 100644 index 26e99d4..0000000 --- a/tests/test_models.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Copyright (c) 2024 Denis Rozhnovskiy - -This file is part of the PyOutlineAPI project. - -PyOutlineAPI is a Python package for interacting with the Outline VPN Server. - -Licensed under the MIT License. See the LICENSE file for more details. -""" - -import unittest - -from pydantic import ValidationError - -from pyoutlineapi.models import Server, ServerPort, DataLimit, AccessKey, Metrics - - -class TestPyOutlineModels(unittest.TestCase): - - def test_server_model_invalid_timestamp(self): - """Test that Server model raises ValidationError for invalid timestamp.""" - with self.assertRaises(ValidationError): - Server( - name="Test Server", - serverId="server-id", - metricsEnabled=True, - createdTimestampMs=-1609459200000, # Invalid negative timestamp - portForNewAccessKeys=12345 - ) - - def test_server_port_invalid_range(self): - """Test that ServerPort model raises ValidationError for port out of range.""" - with self.assertRaises(ValidationError): - ServerPort(port=70000) # Port number out of valid range - - def test_data_limit_negative_bytes(self): - """Test that DataLimit model raises ValidationError for negative bytes.""" - with self.assertRaises(ValidationError): - DataLimit(bytes=-1) # Negative value not allowed - - def test_access_key_invalid_port(self): - """Test that AccessKey model raises ValidationError for invalid port.""" - with self.assertRaises(ValidationError): - AccessKey( - id="access-key-id", - name="test-key", - password="test-password", - port=70000, # Invalid port number - method="aes-256-cfb", - accessUrl="ss://example" - ) - - def test_metrics_invalid_data(self): - """Test that Metrics model raises ValidationError for invalid dictionary data.""" - with self.assertRaises(ValidationError): - Metrics(bytesTransferredByUserId={"user1": -100}) # Negative bytes transferred - - def test_access_key_empty_password(self): - """Test that AccessKey model raises ValidationError for empty password.""" - with self.assertRaises(ValidationError): - AccessKey( - id="access-key-id", - name="test-key", - password="", # Empty password - port=12345, - method="aes-256-cfb", - accessUrl="ss://example" - ) - - def test_access_key_invalid_url(self): - """Test that AccessKey model raises ValidationError for invalid accessUrl.""" - with self.assertRaises(ValidationError): - AccessKey( - id="access-key-id", - name="test-key", - password="test-password", - port=12345, - method="aes-256-cfb", - accessUrl="" # Invalid access URL - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_outline_api.py b/tests/test_outline_api.py new file mode 100644 index 0000000..1dd880d --- /dev/null +++ b/tests/test_outline_api.py @@ -0,0 +1,225 @@ +from datetime import datetime, timezone +from typing import AsyncGenerator, Dict +from unittest.mock import MagicMock + +import pytest +from aiohttp import ClientSession + +from pyoutlineapi import AsyncOutlineClient, APIError +from pyoutlineapi.models import DataLimit + +# Constants for testing +TEST_API_URL = "https://example.com:1234/secret" +TEST_CERT_SHA256 = "ab12cd34ef56gh78ij90kl12mn34op56qr78st90uvwxyzabcdef123456" +TEST_SERVER_ID = "server-id-123" +TEST_SERVER_NAME = "Test Server" + + +class MockResponse: + """Mock response that can be used as an async context manager.""" + + def __init__(self, status: int = 200, data: dict = None): + self.status = status + self._data = data or {} + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + pass + + async def json(self): + return self._data + + +@pytest.fixture +def server_info() -> Dict: + """Base server information fixture.""" + return { + "name": TEST_SERVER_NAME, + "serverId": TEST_SERVER_ID, + "metricsEnabled": True, + "createdTimestampMs": int(datetime.now(timezone.utc).timestamp() * 1000), + "version": "1.0.0", + "portForNewAccessKeys": 8388, + "hostnameForAccessKeys": "vpn.example.com", + "accessKeyDataLimit": None, + } + + +@pytest.fixture +def access_key_data() -> Dict: + """Base access key data fixture.""" + return { + "id": 1, + "name": "Test Key", + "password": "test-password", + "port": 8388, + "method": "chacha20-ietf-poly1305", + "accessUrl": "ss://test-url", + "dataLimit": None, + } + + +@pytest.fixture +def access_key_list_data(access_key_data) -> Dict: + """Access key list fixture.""" + return {"accessKeys": [access_key_data]} + + +@pytest.fixture +def metrics_data() -> Dict: + """Server metrics fixture.""" + return { + "bytesTransferredByUserId": { + "1": 1024 * 1024 * 100, # 100 MB + "2": 1024 * 1024 * 200, # 200 MB + } + } + + +@pytest.fixture +async def client() -> AsyncGenerator[AsyncOutlineClient, None]: + """Fixture for AsyncOutlineClient with mocked session.""" + client = AsyncOutlineClient(TEST_API_URL, TEST_CERT_SHA256, json_format=True) + + # Create mock session + mock_session = MagicMock(spec=ClientSession) + mock_session.closed = False + client._session = mock_session + + yield client + + # Cleanup + if client.session and not client.session.closed: + await client.session.close() + + +@pytest.fixture +def mock_successful_response(): + """Fixture for successful API responses.""" + + def configure_response(data: dict): + return MockResponse(status=200, data=data) + + return configure_response + + +@pytest.fixture +def mock_error_response(): + """Fixture for error API responses.""" + + def configure_error(status_code: int, error_code: str, message: str): + return MockResponse( + status=status_code, data={"code": error_code, "message": message} + ) + + return configure_error + + +# Test case helpers +async def assert_request_called_with( + client: AsyncOutlineClient, + method: str, + endpoint: str, + json: dict = None, + params: dict = None, +): + """Helper to verify request parameters.""" + expected_url = f"{TEST_API_URL}/{endpoint.lstrip('/')}" + client.session.request.assert_called_once_with( + method, expected_url, json=json, params=params, raise_for_status=False + ) + + +@pytest.mark.asyncio +async def test_get_server_info( + client: AsyncOutlineClient, server_info: Dict, mock_successful_response +): + """Test get_server_info method.""" + # Configure mock response + client._session.request.return_value = mock_successful_response(server_info) + + # Make request + result = await client.get_server_info() + + # Verify request + await assert_request_called_with(client, "GET", "server") + + # Verify response + assert isinstance(result, dict) + assert result["name"] == TEST_SERVER_NAME + assert result["server_id"] == TEST_SERVER_ID + + +@pytest.mark.asyncio +async def test_create_access_key( + client: AsyncOutlineClient, access_key_data: Dict, mock_successful_response +): + """Test create_access_key method.""" + # Configure mock response + client._session.request.return_value = mock_successful_response(access_key_data) + + # Test data + key_name = "New Key" + port = 8389 + data_limit = DataLimit(bytes=1024 * 1024 * 1024) # 1 GB + + # Make request + result = await client.create_access_key(name=key_name, port=port, limit=data_limit) + + # Verify request + await assert_request_called_with( + client, + "POST", + "access-keys", + json={"name": key_name, "port": port, "limit": {"bytes": data_limit.bytes}}, + ) + + # Verify response + assert isinstance(result, dict) + assert result["id"] == access_key_data["id"] + + +@pytest.mark.asyncio +async def test_get_metrics( + client: AsyncOutlineClient, metrics_data: Dict, mock_successful_response +): + """Test get_transfer_metrics method.""" + # Configure mock response + client._session.request.return_value = mock_successful_response(metrics_data) + + # Make request + result = await client.get_transfer_metrics() + + # Verify request + await assert_request_called_with( + client, "GET", "metrics/transfer", params={"period": "monthly"} + ) + + # Verify response + assert isinstance(result, dict) + assert "bytes_transferred_by_user_id" in result + assert ( + result["bytes_transferred_by_user_id"]["1"] + == metrics_data["bytesTransferredByUserId"]["1"] + ) + + +@pytest.mark.asyncio +async def test_error_handling(client: AsyncOutlineClient, mock_error_response): + """Test API error handling.""" + # Configure error response + error_code = "forbidden" + error_message = "Access denied" + client._session.request.return_value = mock_error_response( + 403, error_code, error_message + ) + + # Verify error is raised + with pytest.raises(APIError) as exc_info: + await client.get_server_info() + + assert exc_info.value.status_code == 403 + assert error_code in str(exc_info.value) + assert error_message in str(exc_info.value)