diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..af8b0ec --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,29 @@ +name: Build Python ๐Ÿ distribution ๐Ÿ“ฆ + +on: workflow_call + +jobs: + + build: + name: Build the Magic Python ๐Ÿง™โ€โ™‚๏ธ Distribution ๐Ÿ“ฆ + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Set up the Python Wizardry ๐Ÿง™โ€โ™‚๏ธ + uses: actions/setup-python@v5 + with: + python-version: "3.13" + - name: Install the Build Potion ๐Ÿงช + run: | + pip install build + - name: Concoct the Binary Wheel and Source Tarball ๐Ÿง™โ€โ™€๏ธ + run: | + python -m build + - name: Store the Magical Packages ๐Ÿงณ + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ \ No newline at end of file diff --git a/.github/workflows/publish-pypi.yaml b/.github/workflows/publish-pypi.yaml new file mode 100644 index 0000000..733f894 --- /dev/null +++ b/.github/workflows/publish-pypi.yaml @@ -0,0 +1,59 @@ +name: Publish Python ๐Ÿ distribution ๐Ÿ“ฆ to PyPI + +on: + workflow_dispatch: + +jobs: + build: + uses: ./.github/workflows/build.yaml + + publish-to-pypi: + name: Teleport Python ๐Ÿ Distribution ๐Ÿ“ฆ to PyPI + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/asgi-claim-validator + permissions: + id-token: write + + steps: + - name: Summon the Distribution Artifacts ๐Ÿง™โ€โ™‚๏ธ + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Cast the Publish Spell ๐Ÿ“ฆ to PyPI + uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 + + github-release: + name: Enchant the Python ๐Ÿ Distribution ๐Ÿ“ฆ with Sigstore and Upload to GitHub Release + needs: publish-to-pypi + runs-on: ubuntu-latest + + permissions: + contents: write + id-token: write + + steps: + - name: Summon the Distribution Artifacts ๐Ÿง™โ€โ™‚๏ธ + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Enchant the Artifacts with Sigstore โœจ + uses: sigstore/gh-action-sigstore-python@f514d46b907ebcd5bedc05145c03b69c1edd8b46 + with: + inputs: >- + ./dist/*.tar.gz + ./dist/*.whl + - name: Create the GitHub Release ๐ŸŽ‰ + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + gh release create "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --notes "" + - name: Upload the Enchanted Artifacts to GitHub Release ๐Ÿงณ + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + gh release upload "$GITHUB_REF_NAME" dist/** --repo "$GITHUB_REPOSITORY" \ No newline at end of file diff --git a/.github/workflows/publish-testpypi.yaml b/.github/workflows/publish-testpypi.yaml new file mode 100644 index 0000000..880dec9 --- /dev/null +++ b/.github/workflows/publish-testpypi.yaml @@ -0,0 +1,31 @@ +name: Publish Python ๐Ÿ distribution ๐Ÿ“ฆ to TestPyPI + +on: + push: + branches: + - main + +jobs: + build: + uses: ./.github/workflows/build.yaml + + publish-to-testpypi: + name: Teleport Python ๐Ÿ Distribution ๐Ÿ“ฆ to TestPyPI + needs: build + runs-on: ubuntu-latest + environment: + name: testpypi + url: https://test.pypi.org/p/asgi-claim-validator + permissions: + id-token: write + + steps: + - name: Summon the Distribution Artifacts ๐Ÿง™โ€โ™‚๏ธ + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Cast the Publish Spell ๐Ÿ“ฆ to TestPyPI + uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 + with: + repository-url: https://test.pypi.org/legacy/ \ No newline at end of file diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..cb88bbf --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,43 @@ +on: + pull_request: + push: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - 3.11 + - 3.12 + - 3.13 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v5 + with: + python-version: "${{ matrix.python-version }}" + - name: Create a Python Wonderland ๐Ÿโœจ + run: | + python -m venv .venv + source .venv/bin/activate + - name: Pimp My Pip ๐Ÿš€๐ŸŽฉ + run: | + source .venv/bin/activate + pip install --upgrade pip + - name: Dependency Party ๐ŸŽ‰๐Ÿ“ฆ + run: | + source .venv/bin/activate + pip install poetry + rm poetry.lock + poetry install --no-interaction --no-root + - name: Install the app ๐Ÿ“ฒ๐Ÿš€ + run: | + source .venv/bin/activate + poetry install --no-interaction + - name: Test Fest ๐ŸŽˆโœ… + run: | + source .venv/bin/activate + poetry run pytest -c pytest.ini \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e335453 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,74 @@ +# Contributing to ASGI Claim Validator + +Thanks for thinking about contributing to ASGI Request Duration! I welcome your help and can't wait to see what you'll bring to the table. + +## How to Contribute + +### Reporting Bugs + +Found a bug? Report it by opening an issue on my [GitHub Issues](https://github.com/feteu/asgi-claim-validator/issues) page. The more details, the better! + +### Feature Requests + +Got a brilliant idea for a new feature? Open an issue on the [GitHub Issues](https://github.com/feteu/asgi-claim-validator/issues) page. Let's collaborate to enhance this project together. + +### Submitting Changes + +1. **Fork it**: Click the "Fork" button. +2. **Clone it**: Clone your fork. + ```sh + git clone https://github.com/your-username/asgi-claim-validator.git + ``` +3. **Branch it**: Create a new branch. + ```sh + git checkout -b my-awesome-feature + ``` +4. **Change it**: Make your magic happen. +5. **Commit it**: Commit with style. + ```sh + git commit -m "My awesome changes" + ``` +6. **Push it**: Push to your fork. + ```sh + git push origin my-awesome-feature + ``` +7. **Pull it**: Open a pull request. Describe your awesomeness. + +### Setting Up the Development Environment + +1. **Clone the repository**: + ```sh + git clone https://github.com/feteu/asgi-claim-validator.git + cd asgi-claim-validator + ``` +2. **Install dependencies using Poetry**: + ```sh + poetry install + ``` + +### Running Tests + +Run the tests using `pytest`: +```sh +poetry run pytest +``` + +### Code Style + +Maintain a clean and consistent code style. Adhere to the existing conventions. If you notice any errors or areas for improvement, please correct them or let me know. I'm always eager to learn and improve. + +### Documentation + +Update the docs if you change or add something. Keep everyone in the loop. + +## Need Help? + +Open an issue on the [GitHub Issues](https://github.com/feteu/asgi-claim-validator/issues) page. I'll do my best to help. + +## Reviewing Pull Requests + +I'll review your pull request ASAP. Thanks for your patience! + +## Acknowledgements + +Thanks for contributing! Your support makes this project rock. \ No newline at end of file diff --git a/COPYRIGHT b/COPYRIGHT new file mode 100644 index 0000000..cf0df03 --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1,15 @@ +asgi-claim-validator +Copyright (C) 2024 Fabio Greco + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . \ No newline at end of file diff --git a/README.md b/README.md index 8194fbc..49fc451 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,123 @@ +[![PyPI - License](https://img.shields.io/pypi/l/asgi-claim-validator)](https://www.gnu.org/licenses/gpl-3.0) +[![PyPI - Version](https://img.shields.io/pypi/v/asgi-claim-validator.svg)](https://pypi.org/project/asgi-claim-validator/) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/asgi-claim-validator)](https://pypi.org/project/asgi-claim-validator/) +[![PyPI - Status](https://img.shields.io/pypi/status/asgi-claim-validator)](https://pypi.org/project/asgi-claim-validator/) +[![Dependencies](https://img.shields.io/librariesio/release/pypi/asgi-claim-validator)](https://libraries.io/pypi/asgi-claim-validator) +[![Last Commit](https://img.shields.io/github/last-commit/feteu/asgi-claim-validator)](https://github.com/feteu/asgi-claim-validator/commits/main) +[![Build Status build/testpypi](https://img.shields.io/github/actions/workflow/status/feteu/asgi-claim-validator/publish-testpypi.yaml?label=publish-testpypi)](https://github.com/feteu/asgi-claim-validator/actions/workflows/publish-testpypi.yaml) +[![Build Status build/pypi](https://img.shields.io/github/actions/workflow/status/feteu/asgi-claim-validator/publish-pypi.yaml?label=publish-pypi)](https://github.com/feteu/asgi-claim-validator/actions/workflows/publish-pypi.yaml) +[![Build Status test](https://img.shields.io/github/actions/workflow/status/feteu/asgi-claim-validator/test.yaml?label=test)](https://github.com/feteu/asgi-claim-validator/actions/workflows/test.yaml) + # asgi-claim-validator + A focused ASGI middleware for validating additional claims within JWT tokens to enhance token-based workflows. + +## Overview + +`asgi-claim-validator` is an ASGI middleware designed to validate additional claims within JWT tokens. Built in addition to the default JWT verification implementation of Connexion, it enhances token-based workflows by ensuring that specific claims are present and meet certain criteria before allowing access to protected endpoints. This middleware allows consumers to validate claims on an endpoint/method level and is compatible with popular ASGI frameworks such as Starlette, FastAPI, and Connexion. + +## Features + +- **Claim Validation**: Validate specific claims within JWT tokens, such as `sub`, `iss`, `aud`, `exp`, `iat`, and `nbf`. +- **Customizable Claims**: Define essential claims, allowed values, and whether blank values are permitted. +- **Path and Method Filtering**: Apply claim validation to specific paths and HTTP methods. +- **Exception Handling**: Integrate with custom exception handlers to provide meaningful error responses. +- **Logging**: Log validation errors for debugging and monitoring purposes. + +## Installation + +Install the package using pip: + +```sh +pip install asgi-claim-validator +``` + +## Usage + +### Basic Usage + +Here's an example of how to use `asgi-claim-validator` with Starlette: + +```python +from starlette.applications import Starlette +from starlette.requests import Request +from starlette.responses import JSONResponse +from starlette.routing import Route +from asgi_claim_validator import ClaimValidatorMiddleware + +async def secured_endpoint(request: Request) -> JSONResponse: + return JSONResponse({"message": "secured"}) + +app = Starlette(routes=[ + Route("/secured", secured_endpoint, methods=["GET"]), +]) + +app.add_middleware( + ClaimValidatorMiddleware, + claims_callable=lambda: { + "sub": "admin", + "iss": "https://example.com", + }, + secured={ + "^/secured$": { + "GET": { + "sub": { + "essential": True, + "allow_blank": False, + "values": ["admin"], + }, + "iss": { + "essential": True, + "allow_blank": False, + "values": ["https://example.com"], + }, + }, + } + }, +) +``` + +## Advanced Usage + +### Custom Exception Handlers + +Integrate `asgi-claim-validator` with custom exception handlers to provide meaningful error responses. Below are examples for Starlette and Connexion. Refer to the specific framework examples in the [examples](examples) directory for detailed implementation. + +### Middleware Configuration + +Configure the middleware with the following options: + +- **claims_callable**: A callable that returns the JWT claims to be validated. +- **secured**: A dictionary defining the paths and methods that require claim validation. +- **skipped**: A dictionary defining the paths and methods to be excluded from claim validation. +- **raise_on_unspecified_path**: Raise an exception if the path is not specified in the `secured` or `skipped` dictionaries. +- **raise_on_unspecified_method**: Raise an exception if the method is not specified for a secured path. + +### Claim Validation Options + +Configure claims with the following options: + +- **essential**: Indicates if the claim is essential (default: `False`). +- **allow_blank**: Indicates if blank values are allowed (default: `True`). +- **values**: A list of allowed values for the claim. + +## Examples + +### Starlette Example +Refer to the [app.py](examples/starlette/simple/app.py) file for a complete example using Starlette. + +### Connexion Example +Refer to the [app.py](examples/connexion/simple/app.py) file for a complete example using Connexion. + +## Testing +Run the tests using `pytest`: + +```sh +poetry run pytest +``` + +## Contributing +Contributions are welcome! Please refer to the [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines on how to contribute to this project. + +## License +This project is licensed under the GNU GPLv3 License. See the [LICENSE](LICENSE) file for more details. \ No newline at end of file diff --git a/asgi_claim_validator/__init__.py b/asgi_claim_validator/__init__.py new file mode 100644 index 0000000..349f1b3 --- /dev/null +++ b/asgi_claim_validator/__init__.py @@ -0,0 +1,28 @@ +from asgi_claim_validator.decorators import validate_claims_callable +from asgi_claim_validator.exceptions import ( + ClaimValidatorException, + InvalidClaimsTypeException, + InvalidClaimValueException, + MissingEssentialClaimException, + UnauthenticatedRequestException, + UnspecifiedMethodAuthenticationException, + UnspecifiedPathAuthenticationException, +) +from asgi_claim_validator.middleware import ClaimValidatorMiddleware +from asgi_claim_validator.types import SecuredCompiledType, SecuredType, SkippedCompiledType, SkippedType + +__all__ = ( + "ClaimValidatorException", + "ClaimValidatorMiddleware", + "InvalidClaimsTypeException", + "InvalidClaimValueException", + "MissingEssentialClaimException", + "SecuredCompiledType", + "SecuredType", + "SkippedCompiledType", + "SkippedType", + "UnauthenticatedRequestException", + "UnspecifiedMethodAuthenticationException", + "UnspecifiedPathAuthenticationException", + "validate_claims_callable", +) \ No newline at end of file diff --git a/asgi_claim_validator/constants.py b/asgi_claim_validator/constants.py new file mode 100644 index 0000000..6828112 --- /dev/null +++ b/asgi_claim_validator/constants.py @@ -0,0 +1,132 @@ +from re import escape +from asgi_claim_validator.types import SecuredType, SkippedType, ClaimsCallableType + +_DEFAULT_ANY_HTTP_METHODS: str = "*" +_DEFAULT_ALL_HTTP_METHODS: list[str] = [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE", +] +_DEFAULT_ALL_HTTP_METHODS_REGEX_GROUP: str = f"({'|'.join(map(escape, (*_DEFAULT_ALL_HTTP_METHODS, *_DEFAULT_ANY_HTTP_METHODS)))})" +_DEFAULT_CLAIMS_CALLABLE: ClaimsCallableType = lambda: dict() +_DEFAULT_RAISE_ON_INVALID_CLAIM: bool = True +_DEFAULT_RAISE_ON_INVALID_CLAIMS_TYPE: bool = True +_DEFAULT_RAISE_ON_MISSING_CLAIM: bool = True +_DEFAULT_RAISE_ON_UNAUTHENTICATED: bool = True +_DEFAULT_RAISE_ON_UNSPECIFIED_METHOD: bool = True +_DEFAULT_RAISE_ON_UNSPECIFIED_PATH: bool = True +_DEFAULT_RE_IGNORECASE: bool = False +_DEFAULT_SECURED: SecuredType = { + "^$": { + f"{_DEFAULT_ANY_HTTP_METHODS}": { + "sub": { + "essential": False, + "allow_blank": False, + }, + }, + }, +} +_DEFAULT_SKIPPED: SkippedType = { + "^$": [ + f"{_DEFAULT_ANY_HTTP_METHODS}", + ], +} +_DEFAULT_SKIPPED_JSON_SCHEMA: dict = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Skipped JSON Schema", + "description": "Schema for validating skipped configuration", + "type": "object", + "minProperties": 1, + "patternProperties": { + "^(.+)$": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "string", + "pattern": f"(?i:(^({_DEFAULT_ALL_HTTP_METHODS_REGEX_GROUP})$))", + }, + { + "type": "null", + }, + ], + }, + }, + }, + "additionalProperties": False, + "unevaluatedProperties": False, +} +_DEFAULT_SECURED_JSON_SCHEMA: dict = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Secured JSON Schema", + "description": "Schema for validating secured configuration", + "type": "object", + "minProperties": 1, + "patternProperties": { + "(^(.+)$)": { + "type": "object", + "minProperties": 1, + "patternProperties": { + f"(?i:(^({_DEFAULT_ALL_HTTP_METHODS_REGEX_GROUP})$))": { + "type": "object", + "minProperties": 1, + "patternProperties": { + "(^(.+)$)": { + "type": "object", + "minProperties": 1, + "properties": { + "essential": { + "oneOf": [ + { + "type": "boolean", + }, + ], + }, + "allow_blank": { + "oneOf": [ + { + "type": "boolean", + }, + { + "type": "null", + }, + ], + }, + "values": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "boolean", + }, + { + "type": "integer", + }, + { + "type": "string", + }, + ], + }, + }, + }, + "additionalProperties": False, + "unevaluatedProperties": False, + }, + }, + "additionalProperties": False, + "unevaluatedProperties": False, + }, + }, + "additionalProperties": False, + "unevaluatedProperties": False, + }, + }, + "additionalProperties": False, + "unevaluatedProperties": False, +} \ No newline at end of file diff --git a/asgi_claim_validator/decorators.py b/asgi_claim_validator/decorators.py new file mode 100644 index 0000000..4c59e0e --- /dev/null +++ b/asgi_claim_validator/decorators.py @@ -0,0 +1,52 @@ +from collections.abc import Callable +from jsonschema import validate +from jsonschema.exceptions import SchemaError, ValidationError +from logging import getLogger +from asgi_claim_validator.constants import ( + _DEFAULT_CLAIMS_CALLABLE, + _DEFAULT_SECURED_JSON_SCHEMA, + _DEFAULT_SKIPPED_JSON_SCHEMA, +) +from asgi_claim_validator.exceptions import ( + InvalidClaimsConfigurationException, + InvalidSecuredConfigurationException, + InvalidSkippedConfigurationException, +) + +log = getLogger(__name__) + +def validate_claims_callable() -> Callable: + def decorator(func) -> Callable: + def wrapper(self, *args, **kwargs) -> Callable: + claims = getattr(self, 'claims_callable', _DEFAULT_CLAIMS_CALLABLE) + if not isinstance(claims, Callable): + raise InvalidClaimsConfigurationException() + return func(self, *args, **kwargs) + return wrapper + return decorator + +def validate_secured() -> Callable: + def decorator(func) -> Callable: + def wrapper(self, *args, **kwargs) -> Callable: + secured = getattr(self, 'secured', None) + try: + validate(instance=secured, schema=_DEFAULT_SECURED_JSON_SCHEMA) + except (SchemaError, ValidationError) as e: + log.error(e) + raise InvalidSecuredConfigurationException() + return func(self, *args, **kwargs) + return wrapper + return decorator + +def validate_skipped() -> Callable: + def decorator(func) -> Callable: + def wrapper(self, *args, **kwargs) -> Callable: + skipped = getattr(self, 'skipped', None) + try: + validate(instance=skipped, schema=_DEFAULT_SKIPPED_JSON_SCHEMA) + except (SchemaError, ValidationError) as e: + log.error(e) + raise InvalidSkippedConfigurationException() + return func(self, *args, **kwargs) + return wrapper + return decorator \ No newline at end of file diff --git a/asgi_claim_validator/exceptions.py b/asgi_claim_validator/exceptions.py new file mode 100644 index 0000000..042fce7 --- /dev/null +++ b/asgi_claim_validator/exceptions.py @@ -0,0 +1,312 @@ +class ClaimValidatorException(Exception): + """Base exception for claim validator-related errors. + + This is the base class for all exceptions raised by the claim validator. + It provides a common interface for handling errors related to claim validation. + + Attributes: + detail (str): A detailed error message. + status (int): The HTTP status code. + title (str): A short HTTP status message. + """ + description: str = "A claim validator error occurred." + status: int = 400 + title: str = "Bad Request" + + def __init__(self, detail: str = description, status: int = status, title: str = title) -> None: + self.detail: str = detail + self.status: int = status + self.title: str = title + super().__init__(self.detail) + +class UnspecifiedMethodAuthenticationException(ClaimValidatorException): + """Exception raised when authentication is not specified for a method. + + This exception is used in the ClaimValidatorMiddleware to indicate that + the specified HTTP method does not have an associated authentication configuration. + It is raised when the `raise_on_unspecified_method` flag is set to True and + no matching secured method pattern is found for the current request method. + + Attributes: + method (str): The HTTP method of the request. + path (str): The path of the request. + detail (str): A detailed error message. + status (int): The HTTP status code. + title (str): A short HTTP status message. + """ + description: str = ( + "Authentication configuration missing for the specified method. " + "Ensure that an appropriate authentication definition is provided " + "for this method and try again." + ) + status: int = 401 + title: str = "Unauthorized" + + def __init__(self, method: str, path: str, detail: str = description, status: int = status, title: str = title) -> None: + self.method: str = method + self.path: str = path + self.detail: str = detail + self.status: int = status + self.title: str = title + super().__init__(self.detail, self.status, self.title) + + def __str__(self) -> str: + return f"Method authentication not specified {self.method} {self.path} ({self.detail})" + +class UnspecifiedPathAuthenticationException(ClaimValidatorException): + """Exception raised when authentication is not specified for a path. + + This exception is used in the ClaimValidatorMiddleware to indicate that + the specified path does not have an associated authentication configuration. + It is raised when the `raise_on_unspecified_path` flag is set to True and + no matching secured path pattern is found for the current request path. + + Attributes: + method (str): The HTTP method of the request. + path (str): The path of the request. + detail (str): A detailed error message. + status (int): The HTTP status code. + title (str): A short HTTP status message. + """ + description: str = ( + "Authentication configuration missing for the specified path. " + "Ensure that an appropriate authentication definition is provided " + "for this path and try again." + ) + status: int = 401 + title: str = "Unauthorized" + + def __init__(self, method: str, path: str, detail: str = description, status: int = status, title: str = title) -> None: + self.method: str = method + self.path: str = path + self.detail: str = detail + self.status: int = status + self.title: str = title + super().__init__(self.detail, self.status, self.title) + + def __str__(self) -> str: + return f"Path authentication not specified {self.method} {self.path} ({self.detail})" + +class UnauthenticatedRequestException(ClaimValidatorException): + """Exception raised when a request cannot be authenticated. + + This exception is used in the ClaimValidatorMiddleware to indicate that + the request could not be authenticated due to missing or invalid claims. + It is raised when the `raise_on_unauthenticated` flag is set to True and + the claims provided are insufficient for authentication. + + Attributes: + path (str): The path of the request. + method (str): The HTTP method of the request. + detail (str): A detailed error message. + status (int): The HTTP status code. + title (str): A short HTTP status message. + """ + description: str = ( + "The request could not be authenticated. Ensure that the necessary " + "claims are provided and try again." + ) + status: int = 401 + title: str = "Unauthorized" + + def __init__(self, path: str, method: str, detail: str = description, status: int = status, title: str = title) -> None: + self.path: str = path + self.method: str = method + self.detail: str = detail + self.status: int = status + self.title: str = title + super().__init__(self.detail, self.status, self.title) + + def __str__(self) -> str: + return f"Unauthenticated request {self.method} {self.path} ({self.detail})" + +class MissingEssentialClaimException(ClaimValidatorException): + """Exception raised when an essential claim is missing from the request. + + This exception is used in the ClaimValidatorMiddleware to indicate that + a required claim is missing from the JWT claims provided in the request. + It is raised when the `raise_on_missing_claim` flag is set to True and + a required claim is not found in the JWT claims. + + Attributes: + path (str): The path of the request. + method (str): The HTTP method of the request. + claims (str): The claims in the request. + detail (str): A detailed error message. + status (int): The HTTP status code. + title (str): A short HTTP status message. + """ + description: str = ( + "An essential claim is missing from the request. Ensure that the " + "necessary claims are provided and try again." + ) + status: int = 403 + title: str = "Forbidden" + + def __init__(self, path: str, method: str, claims: str, detail: str = description, status: int = status, title: str = title) -> None: + self.path: str = path + self.method: str = method + self.claims: str = claims + self.detail: str = detail + self.status: int = status + self.title: str = title + super().__init__(self.detail, self.status, self.title) + + def __str__(self) -> str: + return f"Missing essential claims in request {self.method} {self.path} {self.claims} ({self.detail})" + +class InvalidClaimValueException(ClaimValidatorException): + """Exception raised when a claim has an invalid value. + + This exception is used in the ClaimValidatorMiddleware to indicate that + a claim provided in the JWT claims has an invalid value. + It is raised when the `raise_on_invalid_claim` flag is set to True and + a claim is found to have an invalid value during validation. + + Attributes: + path (str): The path of the request. + method (str): The HTTP method of the request. + claims (str): The claims in the request. + detail (str): A detailed error message. + status (int): The HTTP status code. + title (str): A short HTTP status message. + """ + description: str = ( + "A claim has an invalid value. Ensure that the claims provided have " + "valid values and try again." + ) + status: int = 403 + title: str = "Forbidden" + + def __init__(self, path: str, method: str, claims: str, detail: str = description, status: int = status, title: str = title) -> None: + self.path: str = path + self.method: str = method + self.claims: str = claims + self.detail: str = detail + self.status: int = status + self.title: str = title + super().__init__(self.detail, self.status, self.title) + + def __str__(self) -> str: + return f"Invalid claims value in request {self.method} {self.path} {self.claims} ({self.detail})" + +class InvalidClaimsTypeException(ClaimValidatorException): + """Exception raised when the claims provided are not of the expected type. + + This exception is raised when the claims provided to the ClaimValidatorMiddleware + are not of the expected type. It indicates that the claims should be a dictionary + but are not. + + Attributes: + path (str): The path of the request. + method (str): The HTTP method of the request. + type_received (str): The type of the claims received. + type_expected (str): The expected type of the claims. + detail (str): A detailed error message. + status (int): The HTTP status code. + title (str): A short HTTP status message. + """ + description: str = ( + "The claims provided are not of the expected type. Ensure that the claims are " + "correctly formatted as a dictionary and try again." + ) + status: int = 400 + title: str = "Bad Request" + + def __init__(self, path: str, method: str, type_received: str, type_expected: str, detail: str = description, status: int = status, title: str = title) -> None: + self.path: str = path + self.method: str = method + self.type_received: str = type_received + self.type_expected: str = type_expected + self.detail: str = detail + self.status: int = status + self.title: str = title + super().__init__(self.detail, self.status, self.title) + + def __str__(self) -> str: + return f"Invalid claims type in request {self.method} {self.path} (received: {self.type_received}; expected: {self.type_expected}) ({self.detail})" + +class InvalidClaimsConfigurationException(ClaimValidatorException): + """Exception raised when the claims configuration is invalid. + + This exception is used to indicate that the claims callable provided + does not return an instance of Claims. It is raised during the initialization + of the ClaimValidatorMiddleware when the `claims` parameter is not callable + or does not return the expected type. + + Attributes: + detail (str): A detailed error message. + status (int): The HTTP status code. + title (str): A short HTTP status message. + """ + description: str = ( + "The claims callable must return an instance of Claims. Ensure that the " + "claims callable is correctly referenced and returns the expected type." + ) + status: int = 500 + title: str = "Internal Server Error" + + def __init__(self, detail: str = description, status: int = status, title: str = title) -> None: + self.detail: str = detail + self.status: int = status + self.title: str = title + super().__init__(self.detail, self.status, self.title) + + def __str__(self) -> str: + return f"Invalid claims callable: {self.detail}" + +class InvalidSecuredConfigurationException(ClaimValidatorException): + """Exception raised when the secured configuration is invalid. + + This exception is used to indicate that the secured dictionary provided + is not correctly formatted or contains invalid values. It is raised during + the initialization of the ClaimValidatorMiddleware. + + Attributes: + detail (str): A detailed error message. + status (int): The HTTP status code. + title (str): A short HTTP status message. + """ + description: str = ( + "The secured configuration is invalid. Ensure that the secured dictionary " + "is correctly formatted and contains valid values." + ) + status: int = 500 + title: str = "Internal Server Error" + + def __init__(self, detail: str = description, status: int = status, title: str = title) -> None: + self.detail: str = detail + self.status: int = status + self.title: str = title + super().__init__(self.detail, self.status, self.title) + + def __str__(self) -> str: + return f"Invalid secured configuration: {self.detail}" + +class InvalidSkippedConfigurationException(ClaimValidatorException): + """Exception raised when the skipped configuration is invalid. + + This exception is used to indicate that the skipped dictionary provided + is not correctly formatted or contains invalid values. It is raised during + the initialization of the ClaimValidatorMiddleware. + + Attributes: + detail (str): A detailed error message. + status (int): The HTTP status code. + title (str): A short HTTP status message. + """ + description: str = ( + "The skipped configuration is invalid. Ensure that the skipped dictionary " + "is correctly formatted and contains valid values." + ) + status: int = 500 + title: str = "Internal Server Error" + + def __init__(self, detail: str = description, status: int = status, title: str = title) -> None: + self.detail: str = detail + self.status: int = status + self.title: str = title + super().__init__(self.detail, self.status, self.title) + + def __str__(self) -> str: + return f"Invalid skipped configuration: {self.detail}" \ No newline at end of file diff --git a/asgi_claim_validator/middleware.py b/asgi_claim_validator/middleware.py new file mode 100644 index 0000000..72ec515 --- /dev/null +++ b/asgi_claim_validator/middleware.py @@ -0,0 +1,203 @@ +from dataclasses import dataclass, field +from joserfc.errors import InvalidClaimError, MissingClaimError +from joserfc.jwt import JWTClaimsRegistry +from logging import DEBUG, getLogger +from re import compile, error, IGNORECASE, NOFLAG, Pattern, RegexFlag +from starlette.types import ASGIApp, Receive, Scope, Send +from asgi_claim_validator.constants import ( + _DEFAULT_ANY_HTTP_METHODS, + _DEFAULT_CLAIMS_CALLABLE, + _DEFAULT_RAISE_ON_INVALID_CLAIM, + _DEFAULT_RAISE_ON_INVALID_CLAIMS_TYPE, + _DEFAULT_RAISE_ON_MISSING_CLAIM, + _DEFAULT_RAISE_ON_UNAUTHENTICATED, + _DEFAULT_RAISE_ON_UNSPECIFIED_METHOD, + _DEFAULT_RAISE_ON_UNSPECIFIED_PATH, + _DEFAULT_RE_IGNORECASE, + _DEFAULT_SECURED, + _DEFAULT_SKIPPED, +) +from asgi_claim_validator.decorators import ( + validate_claims_callable, + validate_secured, + validate_skipped, +) +from asgi_claim_validator.exceptions import ( + InvalidClaimsTypeException, + InvalidClaimValueException, + MissingEssentialClaimException, + UnauthenticatedRequestException, + UnspecifiedMethodAuthenticationException, + UnspecifiedPathAuthenticationException, +) +from asgi_claim_validator.types import ( + ClaimsCallableType, + SecuredCompiledType, + SecuredType, + SkippedCompiledType, + SkippedType, +) + +log = getLogger(__name__) + +@dataclass +class ClaimValidatorMiddleware: + """ + Middleware for validating JWT claims in ASGI applications. + + Attributes: + app (ASGIApp): The ASGI application. + claims_callable (ClaimsCallableType): A callable that returns the claims. + raise_on_invalid_claim (bool): Flag to raise an exception on invalid claims. + raise_on_invalid_claims_type (bool): Flag to raise an exception on invalid claims type. + raise_on_missing_claim (bool): Flag to raise an exception on missing claims. + raise_on_unauthenticated (bool): Flag to raise an exception on unauthenticated requests. + raise_on_unspecified_method (bool): Flag to raise an exception on unspecified methods. + raise_on_unspecified_path (bool): Flag to raise an exception on unspecified paths. + re_flags (RegexFlag): Regular expression flags. + re_ignorecase (bool): Flag to ignore case in regular expressions. + secured_compiled (SecuredCompiledType): Compiled secured paths and methods. + secured (SecuredType): Secured paths and methods. + skipped_compiled (SkippedCompiledType): Compiled skipped paths and methods. + skipped (SkippedType): Skipped paths and methods. + """ + app: ASGIApp + claims_callable: ClaimsCallableType = field(default=_DEFAULT_CLAIMS_CALLABLE) + raise_on_invalid_claim: bool = field(default=_DEFAULT_RAISE_ON_INVALID_CLAIM) + raise_on_invalid_claims_type: bool = field(default=_DEFAULT_RAISE_ON_INVALID_CLAIMS_TYPE) + raise_on_missing_claim: bool = field(default=_DEFAULT_RAISE_ON_MISSING_CLAIM) + raise_on_unauthenticated: bool = field(default=_DEFAULT_RAISE_ON_UNAUTHENTICATED) + raise_on_unspecified_method: bool = field(default=_DEFAULT_RAISE_ON_UNSPECIFIED_METHOD) + raise_on_unspecified_path: bool = field(default=_DEFAULT_RAISE_ON_UNSPECIFIED_PATH) + re_flags: RegexFlag = field(default=NOFLAG, init=False) + re_ignorecase: bool = field(default=_DEFAULT_RE_IGNORECASE) + secured_compiled: SecuredCompiledType = field(default_factory=dict, init=False) + secured: SecuredType = field(default_factory=lambda: _DEFAULT_SECURED) + skipped_compiled: SkippedCompiledType = field(default_factory=dict, init=False) + skipped: SkippedType = field(default_factory=lambda: _DEFAULT_SKIPPED) + + @validate_claims_callable() + @validate_secured() + @validate_skipped() + def __post_init__(self) -> None: + """ + Post-initialization method to compile regular expressions for secured and skipped paths. + """ + try: + self.re_flags = IGNORECASE if self.re_ignorecase else NOFLAG + # This code compiles regular expressions for each path in self.secured and associates them with a + # dictionary of HTTP methods in uppercase and their corresponding claims. + self.secured_compiled = { + compile(path, flags=self.re_flags): { + method.upper(): claims for method, claims in methods.items() + } for path, methods in self.secured.items() + } + # This code compiles regular expressions for each path in self.skipped and associates them with the + # corresponding HTTP methods in uppercase. + self.skipped_compiled = { + compile(path, flags=self.re_flags): set( + method.upper() for method in methods + ) for path, methods in self.skipped.items() + } + except error as e: + raise ValueError(f"Invalid regular expression in secured or skipped paths: {e}") + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + """ + ASGI application callable to validate JWT claims. + + Args: + scope (Scope): The ASGI scope. + receive (Receive): The ASGI receive callable. + send (Send): The ASGI send callable. + + Raises: + InvalidClaimValueException: If a claim value is invalid and raise_on_invalid_claim is True. + MissingEssentialClaimException: If a required claim is missing and raise_on_missing_claim is True. + UnauthenticatedRequestException: If the request is unauthenticated and raise_on_unauthenticated is True. + UnspecifiedMethodAuthenticationException: If the method is not specified and raise_on_unspecified_method is True. + UnspecifiedPathAuthenticationException: If the path is not specified and raise_on_unspecified_path is True. + """ + if scope["type"] not in ("http",): + await self.app(scope, receive, send) + return + + method = scope["method"].upper() + path = scope["path"] + claims = self.claims_callable() + + # Check if the request path matches any skipped path patterns and if the request method is allowed for that path. + # If both conditions are met, forward the request to the next middleware or application. + for p in self._search_patterns_in_string(path, self.skipped_compiled.keys()): + if any(sc_method in (method, _DEFAULT_ANY_HTTP_METHODS) for sc_method in self.skipped_compiled[p]): + await self.app(scope, receive, send) + return + + if self.raise_on_invalid_claims_type and not isinstance(claims, dict): + raise InvalidClaimsTypeException(path=path, method=method, type_received=type(claims), type_expected=dict) + + if self.raise_on_unauthenticated and not claims: + raise UnauthenticatedRequestException(path=path, method=method) + + # This dictionary comprehension filters the secured_compiled dictionary to include only those patterns + # that match the current URL path. It creates a new dictionary where the keys are the patterns that match the URL path. + filtered_patterns = { + p: self.secured_compiled[p] for p in self._search_patterns_in_string(path, self.secured_compiled.keys()) + } + + if self.raise_on_unspecified_path and not filtered_patterns: + raise UnspecifiedPathAuthenticationException(path=path, method=method) + + # This dictionary comprehension filters the previously created filtered_patterns dictionary to include only those methods + # that match the current HTTP method. It creates a new dictionary where the keys are the paths and the values + # are dictionaries of methods and their corresponding claims that match the current HTTP method. + filtered_patterns = { + fp_path: { + fp_method: fp_claims for fp_method, fp_claims in fp_methods.items() if fp_method in (method, _DEFAULT_ANY_HTTP_METHODS) + } for fp_path, fp_methods in filtered_patterns.items() + } + + if self.raise_on_unspecified_method and all(not fp_methods for fp_methods in filtered_patterns.values()): + raise UnspecifiedMethodAuthenticationException(path=path, method=method) + + # This block iterates over filtered patterns of paths and methods, validates JWT claims for each method, + # and logs any errors encountered during the validation process. + for fp_path, fp_methods in filtered_patterns.items(): + for fp_method, fp_claims in fp_methods.items(): + if log.isEnabledFor(DEBUG): + log.debug(f"path: {path} | method: {method} | claims: {claims}") + log.debug(f"fp_path: {fp_path} | fp_method: {fp_method} | fp_claims: {fp_claims}") + try: + claims_requests = JWTClaimsRegistry(**fp_claims) + claims_requests.validate(claims) + except MissingClaimError as e: + log.debug(f"Missing claim: {e}") + if self.raise_on_missing_claim: + raise MissingEssentialClaimException(path=path, method=method, claims=claims) + except InvalidClaimError as e: + log.debug(f"Invalid claim: {e}") + if self.raise_on_invalid_claim: + raise InvalidClaimValueException(path=path, method=method, claims=claims) + except Exception as e: + log.error(f"Unexpected error during claim validation: {e}") + raise + + await self.app(scope, receive, send) + return + + @staticmethod + def _search_patterns_in_string(s: str, patterns: list[Pattern]) -> list[Pattern]: + """ + Searches for patterns in a given string and returns the patterns that match. + + This method iterates over a list of compiled regular expression patterns and checks if each pattern matches the given string `s`. + The method returns a list of patterns that have at least one match in the string. + + Args: + s (str): The string to search within. + patterns (list[Pattern]): A list of compiled regular expression patterns to search for. + + Returns: + list[Pattern]: A list of patterns that match the string. + """ + return [p for p in patterns if p.search(s)] \ No newline at end of file diff --git a/asgi_claim_validator/types.py b/asgi_claim_validator/types.py new file mode 100644 index 0000000..9411910 --- /dev/null +++ b/asgi_claim_validator/types.py @@ -0,0 +1,10 @@ +from collections.abc import Callable +from joserfc.jwt import ClaimsOption, Claims +from re import Pattern + +SecuredCompiledType = dict[Pattern, dict[str, dict[str, ClaimsOption]]] +SecuredType = dict[str, dict[str, dict[str, ClaimsOption]]] +SkippedCompiledType = dict[Pattern, set[str]] +SkippedType = dict[str, list[str]] +ClaimsType = Claims +ClaimsCallableType = Callable[..., Claims] \ No newline at end of file diff --git a/examples/connexion/simple/api/blocked.py b/examples/connexion/simple/api/blocked.py new file mode 100644 index 0000000..47b945c --- /dev/null +++ b/examples/connexion/simple/api/blocked.py @@ -0,0 +1,7 @@ +from connexion.lifecycle import ConnexionResponse + +def search() -> ConnexionResponse: + data = {"status": "blocked"} + status_code = 200 + headers = {"Content-Type": "application/json"} + return data, status_code, headers \ No newline at end of file diff --git a/examples/connexion/simple/api/secured.py b/examples/connexion/simple/api/secured.py new file mode 100644 index 0000000..c2bdb70 --- /dev/null +++ b/examples/connexion/simple/api/secured.py @@ -0,0 +1,7 @@ +from connexion.lifecycle import ConnexionResponse + +def search() -> ConnexionResponse: + data = {"status": "secured"} + status_code = 200 + headers = {"Content-Type": "application/json"} + return data, status_code, headers \ No newline at end of file diff --git a/examples/connexion/simple/api/skipped.py b/examples/connexion/simple/api/skipped.py new file mode 100644 index 0000000..7bd949f --- /dev/null +++ b/examples/connexion/simple/api/skipped.py @@ -0,0 +1,7 @@ +from connexion.lifecycle import ConnexionResponse + +def search() -> ConnexionResponse: + data = {"status": "skipped"} + status_code = 200 + headers = {"Content-Type": "application/json"} + return data, status_code, headers \ No newline at end of file diff --git a/examples/connexion/simple/app.py b/examples/connexion/simple/app.py new file mode 100644 index 0000000..6ba1c97 --- /dev/null +++ b/examples/connexion/simple/app.py @@ -0,0 +1,59 @@ +from connexion import AsyncApp, RestyResolver +from connexion.exceptions import problem +from connexion.lifecycle import ConnexionRequest, ConnexionResponse +from connexion.middleware import MiddlewarePosition +from json import dumps +from time import time +from uvicorn import run +from asgi_claim_validator import ClaimValidatorMiddleware +from asgi_claim_validator.exceptions import ClaimValidatorException, UnspecifiedPathAuthenticationException + +JWT_LIFETIME_SECONDS = 3600 + +claim_validation_skipped = { + "^/api/1/openapi.json$": ["get"], + "^/api/1/skipped/?$": ["get"], + "^/api/1/ui/?$": ["get"], + "^/api/1/ui/.+$": ["get"], +} +claim_validation_secured = { + "^/api/1/secured$": { + "get": { + "sub" : { + "essential": True, + "allow_blank": False, + "values": ["admin"], + }, + "iss": { + "essential": True, + "allow_blank": False, + "values": ["https://example.com"], + }, + }, + } +} +claim_validation_claims_callable = lambda: { + "sub": "admin", + "iss": "https://example.com", + "aud": "https://example.com", + "exp": int(time() + JWT_LIFETIME_SECONDS), + "iat": int(time()), + "nbf": int(time()), +} + + +def claim_validator_custom_403(request: ConnexionRequest, exc: Exception) -> ConnexionResponse: + return ConnexionResponse(status_code=403, body=dumps({"error": "Forbidden"}), content_type="application/json") + +def claim_validator_error_handler(request: ConnexionRequest, exc: Exception) -> ConnexionResponse: + return problem(detail=exc.detail, status=exc.status, title=exc.title) + +app = AsyncApp(__name__, specification_dir="spec") +app.add_api("openapi.yaml", resolver=RestyResolver("api")) +app.add_middleware(ClaimValidatorMiddleware, MiddlewarePosition.BEFORE_SWAGGER, secured=claim_validation_secured, skipped=claim_validation_skipped, claims_callable=claim_validation_claims_callable) +app.add_error_handler(UnspecifiedPathAuthenticationException, claim_validator_custom_403) +app.add_error_handler(ClaimValidatorException, claim_validator_error_handler) + + +if __name__ == "__main__": + run(app, host='127.0.0.1', port=8000) \ No newline at end of file diff --git a/examples/connexion/simple/spec/openapi.yaml b/examples/connexion/simple/spec/openapi.yaml new file mode 100644 index 0000000..b70412f --- /dev/null +++ b/examples/connexion/simple/spec/openapi.yaml @@ -0,0 +1,86 @@ +openapi: "3.0.0" +info: + title: Connexion Simple Example + version: 1.0.0 +servers: + - url: /api/1/ +paths: + /blocked: + get: + summary: Blocked endpoint + tags: + - api + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: blocked + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /secured: + get: + summary: Secured endpoint + tags: + - api + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: secured + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /skipped: + get: + summary: Skipped endpoint + tags: + - api + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: skipped + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + +components: + schemas: + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string \ No newline at end of file diff --git a/examples/starlette/simple/app.py b/examples/starlette/simple/app.py new file mode 100644 index 0000000..4385e56 --- /dev/null +++ b/examples/starlette/simple/app.py @@ -0,0 +1,70 @@ +from collections.abc import Callable +from starlette.applications import Starlette +from starlette.requests import Request +from starlette.responses import JSONResponse +from starlette.routing import Route +from time import time +from uvicorn import run +from asgi_claim_validator import ClaimValidatorMiddleware, ClaimValidatorException + +JWT_LIFETIME_SECONDS = 3600 + +def claims_callable() -> Callable: + return lambda: { + "sub": "admin", + "iss": "https://example.com", + "aud": "https://example.com", + "exp": int(time() + JWT_LIFETIME_SECONDS), + "iat": int(time()), + "nbf": int(time()), + } + +async def blocked_endpoint(request: Request) -> JSONResponse: + return JSONResponse({"message": "blocked"}) + +async def secured_endpoint(request: Request) -> JSONResponse: + return JSONResponse({"message": "secured"}) + +async def skipped_endpoint(request: Request) -> JSONResponse: + return JSONResponse({"message": "skipped"}) + +async def claim_validator_error_handler(request: Request, exc: ClaimValidatorException) -> JSONResponse: + return JSONResponse({"error": f"{exc.title}"}, status_code=exc.status) + +routes = [ + Route("/blocked", blocked_endpoint, methods=["GET"]), + Route("/secured", secured_endpoint, methods=["GET"]), + Route("/skipped", skipped_endpoint, methods=["GET"]), +] + +exception_handlers = { + ClaimValidatorException: claim_validator_error_handler +} + +app = Starlette(routes=routes, exception_handlers=exception_handlers) +app.add_middleware( + ClaimValidatorMiddleware, + claims_callable=claims_callable, + secured={ + "^/secured$": { + "GET": { + "sub": { + "essential": True, + "allow_blank": False, + "values": ["admin"], + }, + "iss": { + "essential": True, + "allow_blank": False, + "values": ["https://example.com"], + }, + }, + } + }, + skipped={ + "^/skipped$": ["GET"], + }, +) + +if __name__ == "__main__": + run(app, host="127.0.0.1", port=8000) \ No newline at end of file diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..894ec0c --- /dev/null +++ b/poetry.lock @@ -0,0 +1,988 @@ +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. + +[[package]] +name = "anyio" +version = "4.8.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.9" +files = [ + {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, + {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "asgiref" +version = "3.8.1" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.8" +files = [ + {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, + {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, +] + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + +[[package]] +name = "attrs" +version = "24.3.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +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 = "certifi" +version = "2024.12.14" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, +] + +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +files = [ + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, +] + +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +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" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "connexion" +version = "3.2.0" +description = "Connexion - API first applications with OpenAPI/Swagger" +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "connexion-3.2.0-py3-none-any.whl", hash = "sha256:905950337d40f526fb4f2eed3b15fc15d8c367625921ab9442954a025d3e03b3"}, + {file = "connexion-3.2.0.tar.gz", hash = "sha256:0715d4a0393437aa2a48c144756360f9b5292635a05fd15c38cbbaf04ef5acb9"}, +] + +[package.dependencies] +asgiref = ">=3.4" +httpx = ">=0.23" +inflection = ">=0.3.1" +Jinja2 = ">=3.0.0" +jsonschema = ">=4.17.3" +python-multipart = ">=0.0.15" +PyYAML = ">=5.1" +requests = ">=2.27" +starlette = ">=0.35" +swagger-ui-bundle = {version = ">=1.1.0", optional = true, markers = "extra == \"swagger-ui\""} +typing-extensions = ">=4.6.1" +werkzeug = ">=2.2.1" + +[package.extras] +flask = ["a2wsgi (>=1.7)", "flask[async] (>=2.2)"] +mock = ["jsf (>=0.10.0)"] +swagger-ui = ["swagger-ui-bundle (>=1.1.0)"] +uvicorn = ["uvicorn[standard] (>=0.17.6)"] + +[[package]] +name = "cryptography" +version = "44.0.0" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.7" +files = [ + {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, + {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, + {file = "cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd"}, + {file = "cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, + {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, + {file = "cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c"}, + {file = "cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==44.0.0)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, + {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {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 = "inflection" +version = "0.5.1" +description = "A port of Ruby on Rails inflector to Python" +optional = false +python-versions = ">=3.5" +files = [ + {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"}, + {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jinja2" +version = "3.1.5" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +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 = "joserfc" +version = "1.0.1" +description = "The ultimate Python library for JOSE RFCs, including JWS, JWE, JWK, JWA, JWT" +optional = false +python-versions = ">=3.8" +files = [ + {file = "joserfc-1.0.1-py3-none-any.whl", hash = "sha256:ae16f56b4091181cab5148a75610bb40d2452db17d09169598605250fa40f5dd"}, + {file = "joserfc-1.0.1.tar.gz", hash = "sha256:c4507be82d681245f461710ffca1fa809fd288f49bc3ce4dba0b1c591700a686"}, +] + +[package.dependencies] +cryptography = "*" + +[package.extras] +drafts = ["pycryptodome"] + +[[package]] +name = "jsonschema" +version = "4.23.0" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, + {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rpds-py = ">=0.7.1" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] + +[[package]] +name = "jsonschema-specifications" +version = "2024.10.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.9" +files = [ + {file = "jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf"}, + {file = "jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +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 = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pytest" +version = "8.3.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.25.2" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.9" +files = [ + {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] +pytest = ">=8.2,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "python-multipart" +version = "0.0.20" +description = "A streaming multipart parser for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, + {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "referencing" +version = "0.35.1" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, + {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rpds-py" +version = "0.22.3" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "rpds_py-0.22.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6c7b99ca52c2c1752b544e310101b98a659b720b21db00e65edca34483259967"}, + {file = "rpds_py-0.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be2eb3f2495ba669d2a985f9b426c1797b7d48d6963899276d22f23e33d47e37"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70eb60b3ae9245ddea20f8a4190bd79c705a22f8028aaf8bbdebe4716c3fab24"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4041711832360a9b75cfb11b25a6a97c8fb49c07b8bd43d0d02b45d0b499a4ff"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64607d4cbf1b7e3c3c8a14948b99345eda0e161b852e122c6bb71aab6d1d798c"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e69b0a0e2537f26d73b4e43ad7bc8c8efb39621639b4434b76a3de50c6966e"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc27863442d388870c1809a87507727b799c8460573cfbb6dc0eeaef5a11b5ec"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e79dd39f1e8c3504be0607e5fc6e86bb60fe3584bec8b782578c3b0fde8d932c"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e0fa2d4ec53dc51cf7d3bb22e0aa0143966119f42a0c3e4998293a3dd2856b09"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fda7cb070f442bf80b642cd56483b5548e43d366fe3f39b98e67cce780cded00"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cff63a0272fcd259dcc3be1657b07c929c466b067ceb1c20060e8d10af56f5bf"}, + {file = "rpds_py-0.22.3-cp310-cp310-win32.whl", hash = "sha256:9bd7228827ec7bb817089e2eb301d907c0d9827a9e558f22f762bb690b131652"}, + {file = "rpds_py-0.22.3-cp310-cp310-win_amd64.whl", hash = "sha256:9beeb01d8c190d7581a4d59522cd3d4b6887040dcfc744af99aa59fef3e041a8"}, + {file = "rpds_py-0.22.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d20cfb4e099748ea39e6f7b16c91ab057989712d31761d3300d43134e26e165f"}, + {file = "rpds_py-0.22.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:68049202f67380ff9aa52f12e92b1c30115f32e6895cd7198fa2a7961621fc5a"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb4f868f712b2dd4bcc538b0a0c1f63a2b1d584c925e69a224d759e7070a12d5"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc51abd01f08117283c5ebf64844a35144a0843ff7b2983e0648e4d3d9f10dbb"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3cec041684de9a4684b1572fe28c7267410e02450f4561700ca5a3bc6695a2"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ef9d9da710be50ff6809fed8f1963fecdfecc8b86656cadfca3bc24289414b0"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59f4a79c19232a5774aee369a0c296712ad0e77f24e62cad53160312b1c1eaa1"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a60bce91f81ddaac922a40bbb571a12c1070cb20ebd6d49c48e0b101d87300d"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e89391e6d60251560f0a8f4bd32137b077a80d9b7dbe6d5cab1cd80d2746f648"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e3fb866d9932a3d7d0c82da76d816996d1667c44891bd861a0f97ba27e84fc74"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1352ae4f7c717ae8cba93421a63373e582d19d55d2ee2cbb184344c82d2ae55a"}, + {file = "rpds_py-0.22.3-cp311-cp311-win32.whl", hash = "sha256:b0b4136a252cadfa1adb705bb81524eee47d9f6aab4f2ee4fa1e9d3cd4581f64"}, + {file = "rpds_py-0.22.3-cp311-cp311-win_amd64.whl", hash = "sha256:8bd7c8cfc0b8247c8799080fbff54e0b9619e17cdfeb0478ba7295d43f635d7c"}, + {file = "rpds_py-0.22.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:27e98004595899949bd7a7b34e91fa7c44d7a97c40fcaf1d874168bb652ec67e"}, + {file = "rpds_py-0.22.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1978d0021e943aae58b9b0b196fb4895a25cc53d3956b8e35e0b7682eefb6d56"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:655ca44a831ecb238d124e0402d98f6212ac527a0ba6c55ca26f616604e60a45"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feea821ee2a9273771bae61194004ee2fc33f8ec7db08117ef9147d4bbcbca8e"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22bebe05a9ffc70ebfa127efbc429bc26ec9e9b4ee4d15a740033efda515cf3d"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3af6e48651c4e0d2d166dc1b033b7042ea3f871504b6805ba5f4fe31581d8d38"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ba3c290821343c192f7eae1d8fd5999ca2dc99994114643e2f2d3e6138b15"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:02fbb9c288ae08bcb34fb41d516d5eeb0455ac35b5512d03181d755d80810059"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f56a6b404f74ab372da986d240e2e002769a7d7102cc73eb238a4f72eec5284e"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0a0461200769ab3b9ab7e513f6013b7a97fdeee41c29b9db343f3c5a8e2b9e61"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8633e471c6207a039eff6aa116e35f69f3156b3989ea3e2d755f7bc41754a4a7"}, + {file = "rpds_py-0.22.3-cp312-cp312-win32.whl", hash = "sha256:593eba61ba0c3baae5bc9be2f5232430453fb4432048de28399ca7376de9c627"}, + {file = "rpds_py-0.22.3-cp312-cp312-win_amd64.whl", hash = "sha256:d115bffdd417c6d806ea9069237a4ae02f513b778e3789a359bc5856e0404cc4"}, + {file = "rpds_py-0.22.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ea7433ce7e4bfc3a85654aeb6747babe3f66eaf9a1d0c1e7a4435bbdf27fea84"}, + {file = "rpds_py-0.22.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6dd9412824c4ce1aca56c47b0991e65bebb7ac3f4edccfd3f156150c96a7bf25"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20070c65396f7373f5df4005862fa162db5d25d56150bddd0b3e8214e8ef45b4"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b09865a9abc0ddff4e50b5ef65467cd94176bf1e0004184eb915cbc10fc05c5"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3453e8d41fe5f17d1f8e9c383a7473cd46a63661628ec58e07777c2fff7196dc"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5d36399a1b96e1a5fdc91e0522544580dbebeb1f77f27b2b0ab25559e103b8b"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009de23c9c9ee54bf11303a966edf4d9087cd43a6003672e6aa7def643d06518"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1aef18820ef3e4587ebe8b3bc9ba6e55892a6d7b93bac6d29d9f631a3b4befbd"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f60bd8423be1d9d833f230fdbccf8f57af322d96bcad6599e5a771b151398eb2"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:62d9cfcf4948683a18a9aff0ab7e1474d407b7bab2ca03116109f8464698ab16"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9253fc214112405f0afa7db88739294295f0e08466987f1d70e29930262b4c8f"}, + {file = "rpds_py-0.22.3-cp313-cp313-win32.whl", hash = "sha256:fb0ba113b4983beac1a2eb16faffd76cb41e176bf58c4afe3e14b9c681f702de"}, + {file = "rpds_py-0.22.3-cp313-cp313-win_amd64.whl", hash = "sha256:c58e2339def52ef6b71b8f36d13c3688ea23fa093353f3a4fee2556e62086ec9"}, + {file = "rpds_py-0.22.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f82a116a1d03628a8ace4859556fb39fd1424c933341a08ea3ed6de1edb0283b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3dfcbc95bd7992b16f3f7ba05af8a64ca694331bd24f9157b49dadeeb287493b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59259dc58e57b10e7e18ce02c311804c10c5a793e6568f8af4dead03264584d1"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5725dd9cc02068996d4438d397e255dcb1df776b7ceea3b9cb972bdb11260a83"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99b37292234e61325e7a5bb9689e55e48c3f5f603af88b1642666277a81f1fbd"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27b1d3b3915a99208fee9ab092b8184c420f2905b7d7feb4aeb5e4a9c509b8a1"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f612463ac081803f243ff13cccc648578e2279295048f2a8d5eb430af2bae6e3"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f73d3fef726b3243a811121de45193c0ca75f6407fe66f3f4e183c983573e130"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3f21f0495edea7fdbaaa87e633a8689cd285f8f4af5c869f27bc8074638ad69c"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1e9663daaf7a63ceccbbb8e3808fe90415b0757e2abddbfc2e06c857bf8c5e2b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a76e42402542b1fae59798fab64432b2d015ab9d0c8c47ba7addddbaf7952333"}, + {file = "rpds_py-0.22.3-cp313-cp313t-win32.whl", hash = "sha256:69803198097467ee7282750acb507fba35ca22cc3b85f16cf45fb01cb9097730"}, + {file = "rpds_py-0.22.3-cp313-cp313t-win_amd64.whl", hash = "sha256:f5cf2a0c2bdadf3791b5c205d55a37a54025c6e18a71c71f82bb536cf9a454bf"}, + {file = "rpds_py-0.22.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:378753b4a4de2a7b34063d6f95ae81bfa7b15f2c1a04a9518e8644e81807ebea"}, + {file = "rpds_py-0.22.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3445e07bf2e8ecfeef6ef67ac83de670358abf2996916039b16a218e3d95e97e"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b2513ba235829860b13faa931f3b6846548021846ac808455301c23a101689d"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eaf16ae9ae519a0e237a0f528fd9f0197b9bb70f40263ee57ae53c2b8d48aeb3"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:583f6a1993ca3369e0f80ba99d796d8e6b1a3a2a442dd4e1a79e652116413091"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4617e1915a539a0d9a9567795023de41a87106522ff83fbfaf1f6baf8e85437e"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c150c7a61ed4a4f4955a96626574e9baf1adf772c2fb61ef6a5027e52803543"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fa4331c200c2521512595253f5bb70858b90f750d39b8cbfd67465f8d1b596d"}, + {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:214b7a953d73b5e87f0ebece4a32a5bd83c60a3ecc9d4ec8f1dca968a2d91e99"}, + {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f47ad3d5f3258bd7058d2d506852217865afefe6153a36eb4b6928758041d831"}, + {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f276b245347e6e36526cbd4a266a417796fc531ddf391e43574cf6466c492520"}, + {file = "rpds_py-0.22.3-cp39-cp39-win32.whl", hash = "sha256:bbb232860e3d03d544bc03ac57855cd82ddf19c7a07651a7c0fdb95e9efea8b9"}, + {file = "rpds_py-0.22.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfbc454a2880389dbb9b5b398e50d439e2e58669160f27b60e5eca11f68ae17c"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d48424e39c2611ee1b84ad0f44fb3b2b53d473e65de061e3f460fc0be5f1939d"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:24e8abb5878e250f2eb0d7859a8e561846f98910326d06c0d51381fed59357bd"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b232061ca880db21fa14defe219840ad9b74b6158adb52ddf0e87bead9e8493"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac0a03221cdb5058ce0167ecc92a8c89e8d0decdc9e99a2ec23380793c4dcb96"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb0c341fa71df5a4595f9501df4ac5abfb5a09580081dffbd1ddd4654e6e9123"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf9db5488121b596dbfc6718c76092fda77b703c1f7533a226a5a9f65248f8ad"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8db6b5b2d4491ad5b6bdc2bc7c017eec108acbf4e6785f42a9eb0ba234f4c9"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b3d504047aba448d70cf6fa22e06cb09f7cbd761939fdd47604f5e007675c24e"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e61b02c3f7a1e0b75e20c3978f7135fd13cb6cf551bf4a6d29b999a88830a338"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:e35ba67d65d49080e8e5a1dd40101fccdd9798adb9b050ff670b7d74fa41c566"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:26fd7cac7dd51011a245f29a2cc6489c4608b5a8ce8d75661bb4a1066c52dfbe"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:177c7c0fce2855833819c98e43c262007f42ce86651ffbb84f37883308cb0e7d"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bb47271f60660803ad11f4c61b42242b8c1312a31c98c578f79ef9387bbde21c"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:70fb28128acbfd264eda9bf47015537ba3fe86e40d046eb2963d75024be4d055"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44d61b4b7d0c2c9ac019c314e52d7cbda0ae31078aabd0f22e583af3e0d79723"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0e260eaf54380380ac3808aa4ebe2d8ca28b9087cf411649f96bad6900c728"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b25bc607423935079e05619d7de556c91fb6adeae9d5f80868dde3468657994b"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb6116dfb8d1925cbdb52595560584db42a7f664617a1f7d7f6e32f138cdf37d"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a63cbdd98acef6570c62b92a1e43266f9e8b21e699c363c0fef13bd530799c11"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b8f60e1b739a74bab7e01fcbe3dddd4657ec685caa04681df9d562ef15b625f"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2e8b55d8517a2fda8d95cb45d62a5a8bbf9dd0ad39c5b25c8833efea07b880ca"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:2de29005e11637e7a2361fa151f780ff8eb2543a0da1413bb951e9f14b699ef3"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:666ecce376999bf619756a24ce15bb14c5bfaf04bf00abc7e663ce17c3f34fe7"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5246b14ca64a8675e0a7161f7af68fe3e910e6b90542b4bfb5439ba752191df6"}, + {file = "rpds_py-0.22.3.tar.gz", hash = "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "starlette" +version = "0.45.2" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.9" +files = [ + {file = "starlette-0.45.2-py3-none-any.whl", hash = "sha256:4daec3356fb0cb1e723a5235e5beaf375d2259af27532958e2d79df549dad9da"}, + {file = "starlette-0.45.2.tar.gz", hash = "sha256:bba1831d15ae5212b22feab2f218bab6ed3cd0fc2dc1d4442443bb1ee52260e0"}, +] + +[package.dependencies] +anyio = ">=3.6.2,<5" + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] + +[[package]] +name = "swagger-ui-bundle" +version = "1.1.0" +description = "Swagger UI bundled for usage with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "swagger_ui_bundle-1.1.0-py3-none-any.whl", hash = "sha256:f7526f7bb99923e10594c54247265839bec97e96b0438561ac86faf40d40dd57"}, + {file = "swagger_ui_bundle-1.1.0.tar.gz", hash = "sha256:20673c3431c8733d5d1615ecf79d9acf30cff75202acaf21a7d9c7f489714529"}, +] + +[package.dependencies] +Jinja2 = ">=3.0.0,<4.0.0" + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +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.3.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +files = [ + {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, + {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "uvicorn" +version = "0.34.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.9" +files = [ + {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, + {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "werkzeug" +version = "3.1.3" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.9" +files = [ + {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, + {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.11,<4.0" +content-hash = "eeac36ed9ae4b9acd0b22a417e9b8879a1d28d4bef7db58ef6fbabe60d5bf0af" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6ad4fde --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,65 @@ +[tool.poetry] +name = "asgi-claim-validator" +version = "1.0.0" +description = "A focused ASGI middleware for validating additional claims within JWT tokens to enhance token-based workflows." +authors = ["Fabio Greco "] +maintainers = ["Fabio Greco "] +license = "GNU GPLv3" +readme = "README.md" +homepage = "https://github.com/feteu/asgi-claim-validator" +repository = "https://github.com/feteu/asgi-claim-validator" +keywords = [ + "asgi", + "async", + "claims", + "connexion", + "fastapi", + "jwt", + "middleware", + "request-duration", + "starlette", + "validator", +] +classifiers = [ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Web Environment', + 'Framework :: FastAPI', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Internet', + 'Topic :: Software Development :: Libraries :: Application Frameworks', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: Software Development :: Libraries', + 'Topic :: Software Development', + 'Topic :: Utilities', + 'Typing :: Typed', +] +packages = [ + { include = "asgi_claim_validator" }, +] + +[tool.poetry.dependencies] +python = ">=3.11,<4.0" +joserfc = "^1.0.1" +jsonschema = "^4.23.0" +starlette = "^0.45.2" + +[tool.poetry.group.dev.dependencies] +connexion = {version="*", extras=["swagger-ui"]} +httpx = "*" +jsonschema = "*" +pytest = "*" +pytest-asyncio = "*" +starlette = "*" +uvicorn = "*" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..2c1f30a --- /dev/null +++ b/pytest.ini @@ -0,0 +1,12 @@ +[pytest] +addopts = --strict-markers --maxfail=3 --tb=short -p no:cacheprovider --verbose +asyncio_default_fixture_loop_scope = function +asyncio_mode = auto +python_classes = Test* +python_files = test_*.py +python_functions = test_* +testpaths = tests +markers = + slow: marks tests as slow (deselect with '-m "not slow"') + smoke: marks tests as smoke tests + regression: marks tests as regression tests \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..4d21063 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,199 @@ +import pytest +from asgi_claim_validator.exceptions import ClaimValidatorException +from asgi_claim_validator.middleware import ClaimValidatorMiddleware +from collections.abc import Callable +from starlette.applications import Starlette +from starlette.requests import Request +from starlette.responses import JSONResponse +from starlette.routing import Route +from time import time + +@pytest.fixture +def claims_callable() -> Callable: + """Fixture for providing mock JWT claims callable.""" + return lambda: { + "sub": "admin", + "iss": "https://example.com", + "aud": "https://example.com", + "exp": int(time() + 3600), + "iat": int(time()), + "nbf": int(time()), + } + +async def blocked_endpoint(request: Request) -> JSONResponse: + return JSONResponse({"message": "blocked"}) + +async def secured_endpoint(request: Request) -> JSONResponse: + return JSONResponse({"message": "secured"}) + +async def skipped_endpoint(request: Request) -> JSONResponse: + return JSONResponse({"message": "skipped"}) + +async def claim_validator_error_handler(request: Request, exc: ClaimValidatorException) -> JSONResponse: + return JSONResponse({"error": f"{exc.title}"}, status_code=exc.status) + +@pytest.fixture +def app(claims_callable: Callable) -> Starlette: + routes = [ + Route("/blocked", blocked_endpoint, methods=["GET", "DELETE"]), + Route("/secured", secured_endpoint, methods=["GET", "DELETE"]), + Route("/skipped", skipped_endpoint, methods=["GET", "DELETE"]), + ] + app = Starlette(routes=routes, exception_handlers={Exception: claim_validator_error_handler}) + app.add_middleware( + ClaimValidatorMiddleware, + claims_callable=claims_callable, + secured={ + "^/secured$": { + "GET": { + "sub": { + "essential": True, + "allow_blank": False, + "values": ["admin"], + }, + "iss": { + "essential": True, + "allow_blank": False, + "values": ["https://example.com"], + }, + }, + } + }, + skipped={ + "^/skipped$": ["GET"], + }, + ) + return app + +@pytest.fixture +def valid_secured_configs_01() -> dict: + # default config + return { + "^/secured$": { + "GET": { + "sub": { + "essential": True, + "allow_blank": False, + "values": ["admin"], + }, + "iss": { + "essential": True, + "allow_blank": False, + "values": ["https://example.com"], + }, + }, + }, + } + +@pytest.fixture +def valid_secured_configs_02() -> dict: + # lowercased method, non essential claim, allow blank claim + return { + "^/secured$": { + "post": { + "aud": { + "essential": False, + "allow_blank": True, + "values": [], + }, + }, + }, + } + +@pytest.fixture +def valid_secured_configs_03() -> dict: + # cover all methods + return { + "^/secured$": { + "*": { + "sub": { + "essential": True, + "allow_blank": False, + "values": ["admin"], + }, + }, + }, + } + +@pytest.fixture +def valid_secured_configs_04() -> dict: + # covery all path and methods + return { + "^/.+$": { + "*": { + "sub": { + "essential": True, + "allow_blank": False, + "values": ["admin"], + }, + }, + }, + } + +@pytest.fixture +def invalid_secured_configs_01() -> dict: + return { + "^/secured$": { + "GET": {}, + }, + } + +@pytest.fixture +def invalid_secured_configs_02() -> dict: + return { + "^/secured$": { + "GET_": { + "sub": { + "essential": True, + "allow_blank": False, + "values": ["admin"], + }, + }, + }, + } + +@pytest.fixture +def valid_skipped_configs_01() -> dict: + return { + "^/skipped$": ["get"], + } + +@pytest.fixture +def valid_skipped_configs_02() -> dict: + return { + "^/skipped$": ["*"], + } + +@pytest.fixture +def valid_skipped_configs_03() -> dict: + return { + "^/skipped$": ["GET", "POST"], + } + +@pytest.fixture +def valid_skipped_configs_04() -> dict: + return { + "^/$": ["GET", "POST"], + "^/skipped$": ["*"], + } + +@pytest.fixture +def invalid_skipped_configs_01() -> dict: + # invalid method + return { + "^/skipped$": ["GET_"], + } + +@pytest.fixture +def invalid_skipped_configs_02() -> dict: + # invalid path + return { + "": ["GET"], + } + +@pytest.fixture +def invalid_skipped_configs_03() -> dict: + # invalid method object + return { + "^/skipped$": False, + } diff --git a/tests/test_decorators.py b/tests/test_decorators.py new file mode 100644 index 0000000..0ef9da6 --- /dev/null +++ b/tests/test_decorators.py @@ -0,0 +1,79 @@ +import pytest +from collections.abc import Callable +from asgi_claim_validator.constants import _DEFAULT_CLAIMS_CALLABLE, _DEFAULT_SECURED, _DEFAULT_SKIPPED +from asgi_claim_validator.decorators import validate_claims_callable, validate_secured, validate_skipped +from asgi_claim_validator.exceptions import InvalidClaimsConfigurationException, InvalidSecuredConfigurationException, InvalidSkippedConfigurationException +from asgi_claim_validator.types import SecuredType, SkippedType + +class TestClass: + def __init__(self, claims_callable: Callable = _DEFAULT_CLAIMS_CALLABLE, secured: SecuredType = _DEFAULT_SECURED, skipped: SkippedType = _DEFAULT_SKIPPED) -> None: + self.claims_callable = claims_callable + self.secured = secured + self.skipped = skipped + + @validate_claims_callable() + def test_validate_claims_callable(self, *args, **kwargs) -> bool: + return "OK" + + @validate_secured() + def test_validate_secured(self, *args, **kwargs) -> bool: + return "OK" + + @validate_skipped() + def test_validate_skipped(self, *args, **kwargs) -> bool: + return "OK" + +def test_validate_claims_callable_with_default_callable() -> None: + TC = TestClass() + result = TC.test_validate_claims_callable() + assert result == "OK" + +def test_validate_claims_callable_with_valid_callable(claims_callable: Callable) -> None: + claims_callable = claims_callable + TC = TestClass(claims_callable=claims_callable) + result = TC.test_validate_claims_callable() + assert result == "OK" + +def test_validate_claims_callable_with_invalid_callable(claims_callable: Callable) -> None: + claims_callable = None + with pytest.raises(InvalidClaimsConfigurationException): + TC = TestClass(claims_callable=claims_callable) + TC.test_validate_claims_callable() + +def test_validate_secured_with_default_config() -> None: + TC = TestClass() + result = TC.test_validate_secured() + assert result == "OK" + +@pytest.mark.parametrize("secured", [f"valid_secured_configs_{i:02d}" for i in range(1, 5)]) +def test_validate_secured_with_valid_config(secured: SecuredType, request: pytest.FixtureRequest) -> None: + secured = request.getfixturevalue(secured) + TC = TestClass(secured=secured) + result = TC.test_validate_secured() + assert result == "OK" + +@pytest.mark.parametrize("secured", [f"invalid_secured_configs_{i:02d}" for i in range(1, 3)]) +def test_validate_secured_with_invalid_config(secured: SecuredType, request: pytest.FixtureRequest) -> None: + secured = request.getfixturevalue(secured) + with pytest.raises(InvalidSecuredConfigurationException): + TC = TestClass(secured=secured) + TC.test_validate_secured() + +def test_validate_skipped_with_default_config() -> None: + TC = TestClass() + result = TC.test_validate_skipped() + assert result == "OK" + +@pytest.mark.parametrize("skipped", [f"valid_skipped_configs_{i:02d}" for i in range(1, 5)]) +def test_validate_skipped_with_valid_config(skipped: SkippedType, request: pytest.FixtureRequest) -> None: + skipped = request.getfixturevalue(skipped) + TC = TestClass(skipped=skipped) + result = TC.test_validate_skipped() + assert result == "OK" + +@pytest.mark.parametrize("skipped", [f"invalid_skipped_configs_{i:02d}" for i in range(1, 4)]) +def test_validate_skipped_with_invalid_config(skipped: SkippedType, request: pytest.FixtureRequest) -> None: + skipped = request.getfixturevalue(skipped) + with pytest.raises(InvalidSkippedConfigurationException): + TC = TestClass(skipped=skipped) + TC.test_validate_skipped() \ No newline at end of file diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py new file mode 100644 index 0000000..6b556f6 --- /dev/null +++ b/tests/test_exceptions.py @@ -0,0 +1,84 @@ +import pytest +from asgi_claim_validator.exceptions import ( + ClaimValidatorException, + InvalidClaimsConfigurationException, + InvalidClaimsTypeException, + InvalidClaimValueException, + MissingEssentialClaimException, + UnauthenticatedRequestException, + UnspecifiedMethodAuthenticationException, + UnspecifiedPathAuthenticationException, +) + +def test_claim_validator_exception() -> None: + with pytest.raises(ClaimValidatorException) as exc_info: + raise ClaimValidatorException("Test error") + exception = exc_info.value + assert exception.status == 400 + assert exception.title == "Bad Request" + assert str(exception) == "Test error" + +def test_unspecified_method_authentication_exception() -> None: + with pytest.raises(UnspecifiedMethodAuthenticationException) as exc_info: + raise UnspecifiedMethodAuthenticationException("GET", "/test") + exception = exc_info.value + assert exception.method == "GET" + assert exception.path == "/test" + assert exception.status == 401 + assert exception.title == "Unauthorized" + +def test_unspecified_path_authentication_exception() -> None: + with pytest.raises(UnspecifiedPathAuthenticationException) as exc_info: + raise UnspecifiedPathAuthenticationException("GET", "/test") + exception = exc_info.value + assert exception.method == "GET" + assert exception.path == "/test" + assert exception.status == 401 + assert exception.title == "Unauthorized" + +def test_unauthenticated_request_exception() -> None: + with pytest.raises(UnauthenticatedRequestException) as exc_info: + raise UnauthenticatedRequestException("/test", "GET") + exception = exc_info.value + assert exception.path == "/test" + assert exception.method == "GET" + assert exception.status == 401 + assert exception.title == "Unauthorized" + +def test_missing_essential_claim_exception() -> None: + with pytest.raises(MissingEssentialClaimException) as exc_info: + raise MissingEssentialClaimException("/test", "GET", "claims") + exception = exc_info.value + assert exception.path == "/test" + assert exception.method == "GET" + assert exception.claims == "claims" + assert exception.status == 403 + assert exception.title == "Forbidden" + +def test_invalid_claim_value_exception() -> None: + with pytest.raises(InvalidClaimValueException) as exc_info: + raise InvalidClaimValueException("/test", "GET", "claims") + exception = exc_info.value + assert exception.path == "/test" + assert exception.method == "GET" + assert exception.claims == "claims" + assert exception.status == 403 + assert exception.title == "Forbidden" + +def test_invalid_claims_type_exception() -> None: + with pytest.raises(InvalidClaimsTypeException) as exc_info: + raise InvalidClaimsTypeException("/test", "GET", "str", "dict") + exception = exc_info.value + assert exception.path == "/test" + assert exception.method == "GET" + assert exception.type_received == "str" + assert exception.type_expected == "dict" + assert exception.status == 400 + assert exception.title == "Bad Request" + +def test_invalid_claims_configuration_exception() -> None: + with pytest.raises(InvalidClaimsConfigurationException) as exc_info: + raise InvalidClaimsConfigurationException() + exception = exc_info.value + assert exception.status == 500 + assert exception.title == "Internal Server Error" \ No newline at end of file diff --git a/tests/test_middleware.py b/tests/test_middleware.py new file mode 100644 index 0000000..e55825e --- /dev/null +++ b/tests/test_middleware.py @@ -0,0 +1,128 @@ +import pytest +from httpx import AsyncClient, ASGITransport +from starlette.applications import Starlette +from asgi_claim_validator.exceptions import ( + InvalidClaimsTypeException, + InvalidClaimValueException, + MissingEssentialClaimException, + UnauthenticatedRequestException, + UnspecifiedMethodAuthenticationException, + UnspecifiedPathAuthenticationException, +) + +async def test_secured_endpoint(app: Starlette) -> None: + transport = ASGITransport(app=app, raise_app_exceptions=True) + async with AsyncClient(transport=transport, base_url="http://testserver") as client: + response = await client.get("/secured") + assert response.status_code == 200 + assert response.json() == {"message": "secured"} + +async def test_skipped_endpoint(app: Starlette) -> None: + transport = ASGITransport(app=app, raise_app_exceptions=True) + async with AsyncClient(transport=transport, base_url="http://testserver") as client: + response = await client.get("/skipped") + assert response.status_code == 200 + assert response.json() == {"message": "skipped"} + +async def test_blocked_endpoint(app: Starlette) -> None: + transport = ASGITransport(app=app, raise_app_exceptions=False) + async with AsyncClient(transport=transport, base_url="http://testserver") as client: + response = await client.get("/blocked") + assert response.status_code == 401 + assert response.json() == {"error": "Unauthorized"} + +async def test_invalid_claims_type_endpoint(app: Starlette) -> None: + app.user_middleware[0].kwargs["claims_callable"] = lambda: "not_a_dict" + transport = ASGITransport(app=app, raise_app_exceptions=False) + async with AsyncClient(transport=transport, base_url="http://testserver") as client: + response = await client.get("/secured") + assert response.status_code == 400 + assert response.json() == {"error": "Bad Request"} + +async def test_invalid_claims_type_exception(app: Starlette) -> None: + app.user_middleware[0].kwargs["claims_callable"] = lambda: "not_a_dict" + transport = ASGITransport(app=app, raise_app_exceptions=True) + async with AsyncClient(transport=transport, base_url="http://testserver") as client: + with pytest.raises(InvalidClaimsTypeException): + await client.get("/secured") + +async def test_unauthenticated_endpoint(app: Starlette) -> None: + app.user_middleware[0].kwargs["claims_callable"] = lambda: {} + transport = ASGITransport(app=app, raise_app_exceptions=False) + async with AsyncClient(transport=transport, base_url="http://testserver") as client: + response = await client.get("/secured") + assert response.status_code == 401 + assert response.json() == {"error": "Unauthorized"} + +async def test_unauthenticated_exception(app: Starlette) -> None: + app.user_middleware[0].kwargs["claims_callable"] = lambda: {} + transport = ASGITransport(app=app, raise_app_exceptions=True) + async with AsyncClient(transport=transport, base_url="http://testserver") as client: + with pytest.raises(UnauthenticatedRequestException): + await client.get("/secured") + +async def test_unspecified_path_authentication_endpoint(app: Starlette) -> None: + transport = ASGITransport(app=app, raise_app_exceptions=False) + async with AsyncClient(transport=transport, base_url="http://testserver") as client: + response = await client.get("/unspecified") + assert response.status_code == 401 + assert response.json() == {"error": "Unauthorized"} + +async def test_unspecified_path_authentication_exception(app: Starlette) -> None: + transport = ASGITransport(app=app, raise_app_exceptions=True) + async with AsyncClient(transport=transport, base_url="http://testserver") as client: + with pytest.raises(UnspecifiedPathAuthenticationException): + await client.get("/unspecified") + +async def test_unspecified_method_authentication_endpoint(app: Starlette) -> None: + transport = ASGITransport(app=app, raise_app_exceptions=False) + async with AsyncClient(transport=transport, base_url="http://testserver") as client: + response = await client.delete("/secured") + assert response.status_code == 401 + assert response.json() == {"error": "Unauthorized"} + +async def test_unspecified_method_authentication_exception(app: Starlette) -> None: + transport = ASGITransport(app=app, raise_app_exceptions=True) + async with AsyncClient(transport=transport, base_url="http://testserver") as client: + with pytest.raises(UnspecifiedMethodAuthenticationException): + await client.delete("/secured") + +async def test_missing_essential_claim_endpoint(app: Starlette) -> None: + app.user_middleware[0].kwargs["claims_callable"] = lambda: { + "sub": "admin", + } + transport = ASGITransport(app=app, raise_app_exceptions=False) + async with AsyncClient(transport=transport, base_url="http://testserver") as client: + response = await client.get("/secured") + assert response.status_code == 403 + assert response.json() == {"error": "Forbidden"} + +async def test_missing_essential_claim_exception(app: Starlette) -> None: + app.user_middleware[0].kwargs["claims_callable"] = lambda: { + "sub": "admin", + } + transport = ASGITransport(app=app, raise_app_exceptions=True) + async with AsyncClient(transport=transport, base_url="http://testserver") as client: + with pytest.raises(MissingEssentialClaimException): + await client.get("/secured") + +async def test_invalid_claim_value_endpoint(app: Starlette) -> None: + app.user_middleware[0].kwargs["claims_callable"] = lambda: { + "sub": "admin", + "iss": "https://wrong-example.com", + } + transport = ASGITransport(app=app, raise_app_exceptions=False) + async with AsyncClient(transport=transport, base_url="http://testserver") as client: + response = await client.get("/secured") + assert response.status_code == 403 + assert response.json() == {"error": "Forbidden"} + +async def test_invalid_claim_value_exception(app: Starlette) -> None: + app.user_middleware[0].kwargs["claims_callable"] = lambda: { + "sub": "admin", + "iss": "https://wrong-example.com", + } + transport = ASGITransport(app=app, raise_app_exceptions=True) + async with AsyncClient(transport=transport, base_url="http://testserver") as client: + with pytest.raises(InvalidClaimValueException): + await client.get("/secured") \ No newline at end of file