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.
[](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi)
[](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi)
[](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi)
[](https://github.com/orenlab/pyoutlineapi/actions/workflows/python_tests.yml)
+[](https://codecov.io/gh/orenlab/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
+
+
+
+
+
+ View Source
+
+ 1 """
+ 2 PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API.
+ 3
+ 4 Copyright (c) 2025 Denis Rozhnovskiy <pytelemonbot@mail.ru>
+ 5 All rights reserved.
+ 6
+ 7 This software is licensed under the MIT License.
+ 8 You can find the full license text at:
+ 9 https://opensource.org/licenses/MIT
+10
+11 Source code repository:
+12 https://github.com/orenlab/pyoutlineapi
+13 """
+14
+15 import sys
+16 from typing import TYPE_CHECKING
+17
+18 if sys . version_info < ( 3 , 10 ):
+19 raise RuntimeError ( "PyOutlineAPI requires Python 3.10 or higher" )
+20
+21 from .client import AsyncOutlineClient , OutlineError , APIError
+22
+23 if 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
+42 PUBLIC_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
+61 from .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 :
+
+ View Source
+
+
+
+ 72 class 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 )
+
+ View Source
+
+
+
+
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 ] :
+
+
View Source
+
+
+
+
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 :
+
+ View Source
+
+
+
+
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:
+
+
+
+
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 :
+
+ View Source
+
+
+
+
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 :
+
+ View Source
+
+
+
+
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 :
+
+ View Source
+
+
+
+
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 :
+
+ View Source
+
+
+
+
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 ()
+
+
+
+
+
+
+
+
+
+
+
+
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 ] :
+
+
View Source
+
+
+
+
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 ] :
+
+
View Source
+
+
+
+
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 ] :
+
+
View Source
+
+
+
+
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:
+
+
+
+
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 :
+
+ View Source
+
+
+
+
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 :
+
+ View Source
+
+
+
+
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:
+
+
+
+
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 :
+
+ View Source
+
+
+
+
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 :
+
+ View Source
+
+
+
+
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:
+
+
+
+
Returns:
+
+
+ True if successful
+
+
+
Raises:
+
+
+APIError: If key doesn't exist
+
+
+
+
+
+
+
+
+ session
+
+ View Source
+
+
+
+
627 @property
+628 def session ( self ):
+629 return self . _session
+
+
+
+
+
+
+
+
+
+
+
+ class
+ OutlineError (builtins.Exception ):
+
+ View Source
+
+
+
+ 48 class OutlineError ( Exception ):
+49 """Base exception for Outline client errors."""
+
+
+
+ Base exception for Outline client errors.
+
+
+
+
+
+
+
+
+ 52 class 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 )
+
+ View Source
+
+
+
+
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 ):
+
+ View Source
+
+
+
+ 42 class 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" )
+
+
+
+
+
+
+
+
+ id : int
+
+
+
+
+
+
+
+
+
+
+ name : Optional[str]
+
+
+
+
+
+
+
+
+
+
+ password : str
+
+
+
+
+
+
+
+
+
+
+ port : int
+
+
+
+
+
+
+
+
+
+
+ method : str
+
+
+
+
+
+
+
+
+
+
+ access_url : str
+
+
+
+
+
+
+
+
+
+
+
+ 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 ):
+
+ View Source
+
+
+
+ 119 class 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]
+
+
+
+
+
+
+
+
+
+
+
+ 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 ):
+
+ View Source
+
+
+
+ 54 class AccessKeyList ( BaseModel ):
+55 """List of access keys."""
+56
+57 access_keys : list [ AccessKey ] = Field ( alias = "accessKeys" )
+
+
+
+
+
+
+
+
+
+ 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 ):
+
+ View Source
+
+
+
+ 30 class 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 :
+
+
View Source
+
+
+
+
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 ):
+
+ View Source
+
+
+
+ 138 class 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 ):
+
+ View Source
+
+
+
+ 93 class 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 ):
+
+ View Source
+
+
+
+ 22 class MetricsPeriod ( str , Enum ):
+23 """Time periods for metrics collection."""
+24
+25 DAILY = "daily"
+26 WEEKLY = "weekly"
+27 MONTHLY = "monthly"
+
+
+
+ Time periods for metrics collection.
+
+
+
+
+
+
+
+
+
+
+
+ class
+ MetricsStatusResponse (pydantic.main.BaseModel ):
+
+ View Source
+
+
+
+ 132 class 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 ):
+
+ View Source
+
+
+
+ 103 class 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 ):
+
+ View Source
+
+
+
+ 60 class 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\nCopyright (c) 2025 Denis Rozhnovskiy pytelemonbot@mail.ru \nAll rights reserved.
\n\nThis software is licensed under the MIT License.
\n\nYou can find the full license text at: \n\n\n https://opensource.org/licenses/MIT
\n \n\nSource 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\nArguments: \n\n\napi_url: Base URL for the Outline server API \ncert_sha256: SHA-256 fingerprint of the server's TLS certificate \njson_format: Return raw JSON instead of Pydantic models \ntimeout: Request timeout in seconds \n \n\nExamples: \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\nReturns: \n\n\n Server information including name, ID, and configuration.
\n \n\nExamples: \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\nArguments: \n\n\nname: New server name \n \n\nReturns: \n\n\n True if successful
\n \n\nExamples: \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\nArguments: \n\n\nhostname: New hostname or IP address \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If hostname is invalid \n \n\nExamples: \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\nArguments: \n\n\nport: Port number (1025-65535) \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If port is invalid or in use \n \n\nExamples: \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\nReturns: \n\n\n Current metrics collection status
\n \n\nExamples: \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\nArguments: \n\n\nenabled: Whether to enable metrics \n \n\nReturns: \n\n\n True if successful
\n \n\nExamples: \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\nArguments: \n\n\nperiod: Time period for metrics (DAILY, WEEKLY, or MONTHLY) \n \n\nReturns: \n\n\n Transfer metrics data for each access key
\n \n\nExamples: \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\nArguments: \n\n\nname: Optional key name \npassword: Optional password \nport: Optional port number (1-65535) \nmethod: Optional encryption method \nlimit: Optional data transfer limit \n \n\nReturns: \n\n\n New access key details
\n \n\nExamples: \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\nReturns: \n\n\n List of all access keys
\n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \n \n\nReturns: \n\n\n Access key details
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \nname: New name \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \nbytes_limit: Limit in bytes (must be positive) \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist or limit is invalid \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: 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)