diff --git a/.env b/.env new file mode 100644 index 0000000..d30e91c --- /dev/null +++ b/.env @@ -0,0 +1,35 @@ +# DATABASE +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_USER=fh_user +POSTGRES_PASSWORD=fh_password +POSTGRES_DATABASE=test_fh_db +POSTGRES_DSN=postgres://fh_user:fh_password@localhost:5432/test_fh_db + +# REDIS +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_USER=default +REDIS_PASSWORD=password +REDIS_DATABASE=0 +REDIS_DSN=redis://default:password@localhost:6379/0 + +# RABBITMQ +RABBITMQ_HOST=localhost +RABBITMQ_PORT=5672 +RABBITMQ_USER=user +RABBITMQ_PASSWORD=password +RABBITMQ_VHOST=test +RABBITMQ_DSN=amqp://user:password@localhost:5672/test + +# KAFKA +KAFKA_BOOTSTRAP_SERVERS=localhost:9094,localhost:9095 + +# MONGO +MONGO_HOST=localhost +MONGO_PORT=27017 +MONGO_USER=root +MONGO_PASSWORD=root +MONGO_AUTH_SOURCE=admin +MONGO_DATABASE=test +MONGO_DSN=mongodb://root:root@localhost:27017/test?authSource=admin diff --git a/.github/workflows/1_test.yml b/.github/workflows/1_test.yml new file mode 100644 index 0000000..f400cdb --- /dev/null +++ b/.github/workflows/1_test.yml @@ -0,0 +1,105 @@ +name: Tests + +on: + workflow_dispatch: + push: + branches: ["main"] + tags-ignore: ["**"] + pull_request: + +concurrency: + group: check-${{ github.ref }} + cancel-in-progress: true + +jobs: + + pre-commit: + runs-on: ubuntu-latest + if: github.event.repository.fork == false + steps: + + - uses: actions/checkout@v4.2.2 + + - name: Install uv + uses: astral-sh/setup-uv@v4.2.0 + + - name: Set up Python 3.10 + run: uv python install 3.10 + + - name: Install the project + run: uv sync --all-extras --dev + + - uses: tox-dev/action-pre-commit-uv@v1.0.1 + env: + SKIP: "detect-aws-credentials,no-commit-to-branch" + + unit-tests: + needs: [pre-commit] + name: test with ${{ matrix.python-version }} on ${{ matrix.os }} with ${{ matrix.pydantic-version }} + runs-on: ${{ matrix.os }} + if: github.event.repository.fork == false + strategy: + fail-fast: false + matrix: + python-version: + - "3.13" + - "3.12" + - "3.11" + - "3.10" + pydantic-version: + - "pydantic-v1" + - "pydantic-v2" + os: + - ubuntu-latest + # - macos-latest + - windows-latest + + steps: + - uses: actions/checkout@v4.2.2 + with: + fetch-depth: 0 + - name: Install the latest version of uv + + uses: astral-sh/setup-uv@v4.2.0 + with: + enable-cache: true + cache-dependency-glob: "pyproject.toml" + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Add .local/bin to Windows PATH + if: runner.os == 'Windows' + shell: bash + run: echo "$USERPROFILE/.local/bin" >> $GITHUB_PATH + + - name: Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + + - name: Run Imports Tests + run: | + uv sync + uv pip install pytest pytest-asyncio pytest-cov greenlet + uv run pytest --cov --cov-append -m 'imports' tests/unit/test_imports.py + uv sync --group=dev --all-extras + + - name: Install Pydantic v1 + if: matrix.pydantic-version == 'pydantic-v1' + run: uv pip install "pydantic>=1.10.19,<2.0.0" + + - name: Install Pydantic v2 + if: matrix.pydantic-version == 'pydantic-v2' + run: uv pip install "pydantic>=2.10.3,<3.0.0" + + - name: Run Unit Tests + run: | + uv run pytest --cov --cov-append -m 'unit' + uv run coverage xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5.1.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + env_vars: OS,PYTHON + fail_ci_if_error: true + flags: unittests + name: codecov-umbrella + verbose: true diff --git a/.github/workflows/2_bump.yml b/.github/workflows/2_bump.yml new file mode 100644 index 0000000..40a8f33 --- /dev/null +++ b/.github/workflows/2_bump.yml @@ -0,0 +1,29 @@ +name: Bump version + +on: + workflow_dispatch: + +jobs: + bump-version: + if: "!startsWith(github.event.head_commit.message, 'bump:')" + runs-on: ubuntu-latest + name: "Bump version and create changelog with commitizen" + steps: + - name: Check out + uses: actions/checkout@v4.2.2 + with: + token: "${{ secrets.PERSONAL_ACCESS_TOKEN }}" + fetch-depth: 0 + + - name: Create bump and changelog + uses: commitizen-tools/commitizen-action@0.23.0 + with: + github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + + - name: Release + uses: softprops/action-gh-release@v1 + with: + body_path: "CHANGELOG.md" + tag_name: ${{ env.REVISION }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/3_docs.yml b/.github/workflows/3_docs.yml new file mode 100644 index 0000000..8dd451c --- /dev/null +++ b/.github/workflows/3_docs.yml @@ -0,0 +1,32 @@ +name: Docs + +on: + push: + tags: + - "*.*.*" + paths: + - 'docs/**' + - 'CHANGELOG.md' + - 'mkdocs.yml' + - 'requirements-docs.txt' + +jobs: + build: + runs-on: ubuntu-latest + if: github.event.repository.fork == false + steps: + + - uses: actions/checkout@v4.2.2 + + - name: Install uv + uses: astral-sh/setup-uv@v4.2.0 + + - name: Set up Python 3.13 + run: uv python install 3.13 + + - run: uv sync --group=docs + + - name: Deploy to GitHub Pages + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: uv run mkdocs gh-deploy --force diff --git a/.github/workflows/4_pythonpublish.yaml b/.github/workflows/4_pythonpublish.yaml new file mode 100644 index 0000000..772ae9b --- /dev/null +++ b/.github/workflows/4_pythonpublish.yaml @@ -0,0 +1,33 @@ +name: Upload Python Package + +on: + push: + tags: + - "*.*.*" + +jobs: + PyPi: + runs-on: ubuntu-latest + if: github.event.repository.fork == false + steps: + - uses: actions/checkout@v4.2.2 + with: + fetch-depth: 0 + + - name: Install uv + uses: astral-sh/setup-uv@v4.2.0 + + - name: Set up Python 3.12 + run: uv python install 3.12 + + - name: Install the project + run: uv sync --group=dev --group=docs --all-extras + + - name: Build the project + run: uv build + + - name: Publish To PyPi + env: + UV_PUBLISH_USERNAME: __token__ + UV_PUBLISH_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: uv publish diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f0d5ef7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,178 @@ +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + + +# Gitlab CI coverage report +report.xml +*_report.xml + +.copier-answers.yml +.gitlab-ci.yml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7ec4fb1 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,98 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +exclude: ^(poetry.lock|.vscode/) +default_language_version: + python: python3.10 + +repos: + + - repo: local + hooks: + - id: forbidden-files + name: forbidden files + entry: found Copier update rejection files; review them and remove them + language: fail + files: "\\.rej$" + - id: ruff-format + name: "ruff format" + entry: ruff format --force-exclude + args: ["--preview"] + types_or: [python, pyi] + language: python + - id: ruff-lint + name: "ruff lint" + entry: ruff check --force-exclude + args: ["--fix", "--exit-non-zero-on-fix", "--preview"] + types_or: [python, pyi] + language: python + - id: mypy + name: "mypy" + entry: mypy + args: ["--config-file", "pyproject.toml", "--install-types", "--non-interactive", "--show-traceback"] + types_or: [python, pyi] + language: python + exclude: "(?x)^( + tests/.* + )$" + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-added-large-files + - id: check-ast + - id: check-builtin-literals + - id: check-case-conflict + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-json + - id: check-merge-conflict + args: [--assume-in-merge] + - id: check-shebang-scripts-are-executable + - id: check-symlinks + - id: check-toml + - id: check-vcs-permalinks + - id: check-xml + - id: check-yaml + exclude: "(?x)^( + mkdocs.yml + )$" + - id: debug-statements + - id: destroyed-symlinks + - id: detect-aws-credentials + - id: detect-private-key + exclude: "(?x)^( + certs/key.key| + certs/ca.key + )$" + - id: end-of-file-fixer + - id: file-contents-sorter + - id: fix-byte-order-marker + - id: fix-encoding-pragma + args: ['--remove'] + - id: forbid-new-submodules + - id: mixed-line-ending + - id: name-tests-test + args: ['--pytest-test-first'] + exclude: "(?x)^( + tests/utils.py + )$" + - id: no-commit-to-branch + args: [--branch, main] + - id: pretty-format-json + - id: requirements-txt-fixer + - id: sort-simple-yaml + - id: trailing-whitespace + + - repo: https://github.com/astral-sh/uv-pre-commit + # uv version. + rev: 0.5.7 + hooks: + # Update the uv lockfile + - id: uv-lock + - id: uv-export + + - repo: https://github.com/Lucas-C/pre-commit-hooks-safety + rev: v1.3.3 + hooks: + - id: python-safety-dependencies-check + files: requirements.txt diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..c9ddef5 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "ms-python.python", + "charliermarsh.ruff", + "ms-python.mypy-type-checker", + "KnisterPeter.vscode-commitizen", + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e3f6923 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,32 @@ +{ + "files.trimTrailingWhitespace": true, + "files.exclude": { + "**/.ruff_cache": true, + "**/.mypy_cache": true, + "**/.pytest_cache": true, + "**/.tox": true, + ".coverage": true, + "**/coverage.xml": true, + "**/htmlcov": true, + }, + "editor.rulers": [120, 132], + "editor.renderWhitespace": "all", + "ruff.lint.args": [ + "--preview" + ], + "ruff.lineLength": 120, + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": {"source.organizeImports": "explicit"}, + "editor.formatOnType": true, + "editor.tabSize": 4, + "editor.insertSpaces": true, + }, + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "yaml.customTags": ["!reference sequence"], + "mypy-type-checker.args": ["--config-file", "pyproject.toml", "--install-types"], + "mypy-type-checker.importStrategy": "fromEnvironment", + "mypy-type-checker.reportingScope": "workspace", +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bc57b11 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Vladislav Shepilov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..89ab110 --- /dev/null +++ b/Makefile @@ -0,0 +1,95 @@ +.PHONY: help bash install-hooks update-uv lint test + +# colors +GREEN = $(shell tput -Txterm setaf 2) +YELLOW = $(shell tput -Txterm setaf 3) +WHITE = $(shell tput -Txterm setaf 7) +RESET = $(shell tput -Txterm sgr0) +GRAY = $(shell tput -Txterm setaf 6) +TARGET_MAX_CHAR_NUM = 20 + +# Help + +## Shows help. +help: + @echo 'DEBUG_MODE: ${DEBUG_MODE}' + @echo '' + @echo 'Usage:' + @echo '' + @echo ' ${YELLOW}make${RESET} ${GREEN}${RESET}' + @echo '' + @echo 'Targets:' + @awk '/^[a-zA-Z\-\_]+:/ { \ + helpMessage = match(lastLine, /^## (.*)/); \ + if (helpMessage) { \ + if (index(lastLine, "|") != 0) { \ + stage = substr(lastLine, index(lastLine, "|") + 1); \ + printf "\n ${GRAY}%s: \n\n", stage; \ + } \ + helpCommand = substr($$1, 0, index($$1, ":")-1); \ + helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ + if (index(lastLine, "|") != 0) { \ + helpMessage = substr(helpMessage, 0, index(helpMessage, "|")-1); \ + } \ + printf " ${YELLOW}%-$(TARGET_MAX_CHAR_NUM)s${RESET} ${GREEN}%s${RESET}\n", helpCommand, helpMessage; \ + } \ + } \ + { lastLine = $$0 }' $(MAKEFILE_LIST) + @echo '' + +.DEFAULT_GOAL := help + +# Common + +## Run bash environment | Common +bash: + @source .venv/bin/activate + +# Development + +## Install pre-commit hooks. | Development +install-hooks: + pre-commit install --hook-type pre-commit + pre-commit install --hook-type commit-msg + pre-commit install --install-hooks + +## Update uv dependencies +update-uv: + @uv sync --all-extras --upgrade + +## Run linters +lint: + @uv run pre-commit run --all-files + +## Run imports tests +tests-imports: + @uv sync + @uv pip install pytest pytest-asyncio pytest-cov greenlet + @uv run pytest --cov --cov-append -m 'imports' tests/unit/test_imports.py + +## Run integration tests +tests-integration: + @docker compose up -d + @echo "Waiting for services to start..." + @sleep 15s + @uv sync --group=dev --all-extras + @uv run pytest --cov --cov-append -m 'integration' + @echo "Stopping services..." + @docker compose down --remove-orphans --volumes + +## Run unit tests +tests-unit: + @uv run pytest --cov --cov-append -m 'unit' + +## Run all tests +tests-all: + @rm -rf .coverage + @make tests-imports + @make tests-integration + @make tests-unit + @uv run coverage report + @uv sync --group=dev --group=docs --all-extras + +## Serve documentation locally +serve-docs: + @uv run mkdocs serve diff --git a/README.md b/README.md new file mode 100644 index 0000000..b938900 --- /dev/null +++ b/README.md @@ -0,0 +1,157 @@ +

+ FastHealthcheck +

+ +Framework agnostic health checks with integrations for most popular ASGI frameworks: [FastAPI](https://github.com/fastapi/fastapi) / [Faststream](https://github.com/airtai/faststream) / [Litestar](https://github.com/litestar-org/litestar) to help you to implement the [Health Check API](https://microservices.io/patterns/observability/health-check-api.html) pattern + +--- + +

+ + + Test Passing + + + + Coverage + + + + Downloads + + + + Package version + + + + Supported Python versions + + + + License + + +

+ +--- + +## Installation + +With `pip`: +```bash +pip install fast-healthcheck +``` + +With `poetry`: +```bash +poetry add fast-healthcheck +``` + +With `uv`: +```bash +uv add fast-healthcheck +``` + +## Quick Start + +Examples: +- [FastAPI example](./examples/fastapi_example) +- [Faststream example](./examples/faststream_example) +- [Litestar example](./examples/litestar_example) + +```python +import asyncio +import os +import time + +from fastapi import FastAPI + +from fast_healthchecks.checks.function import FunctionHealthCheck +from fast_healthchecks.checks.kafka import KafkaHealthCheck +from fast_healthchecks.checks.mongo import MongoHealthCheck +from fast_healthchecks.checks.postgresql.asyncpg import PostgreSQLAsyncPGHealthCheck +from fast_healthchecks.checks.postgresql.psycopg import PostgreSQLPsycopgHealthCheck +from fast_healthchecks.checks.rabbitmq import RabbitMQHealthCheck +from fast_healthchecks.checks.redis import RedisHealthCheck +from fast_healthchecks.checks.url import UrlHealthCheck +from fast_healthchecks.integrations.fastapi import HealthcheckRouter, Probe + + +def sync_dummy_check() -> bool: + time.sleep(0.1) + return True + + +async def async_dummy_check() -> bool: + await asyncio.sleep(0.1) + return True + + +app = FastAPI() +app.include_router( + HealthcheckRouter( + Probe( + name="liveness", + checks=[ + FunctionHealthCheck(func=sync_dummy_check, name="Sync dummy"), + ], + ), + Probe( + name="readiness", + checks=[ + KafkaHealthCheck( + bootstrap_servers=os.environ["KAFKA_BOOTSTRAP_SERVERS"], + name="Kafka", + ), + MongoHealthCheck.from_dsn(os.environ["MONGO_DSN"], name="Mongo"), + PostgreSQLAsyncPGHealthCheck.from_dsn(os.environ["POSTGRES_DSN"], name="PostgreSQL asyncpg"), + PostgreSQLPsycopgHealthCheck.from_dsn(os.environ["POSTGRES_DSN"], name="PostgreSQL psycopg"), + RabbitMQHealthCheck.from_dsn(os.environ["RABBITMQ_DSN"], name="RabbitMQ"), + RedisHealthCheck.from_dsn(os.environ["REDIS_DSN"], name="Redis"), + UrlHealthCheck(url="https://httpbin.org/status/200", name="URL 200"), + ], + ), + Probe( + name="startup", + checks=[ + FunctionHealthCheck(func=async_dummy_check, name="Async dummy"), + ], + ), + debug=True, + prefix="/health", + ), +) +``` + +## Development + +### Setup environment + +```bash +git clone https://github.com/shepilov-vladislav/fast-healthchecks.git +cd fast-healthchecks +uv sync --group=dev --group=docs --all-extras +``` + +### Run linters + +```bash +make lint +``` + +### Run all tests + +```bash +make tests-all +``` + +### Serve documentation + +```bash +make serve-docs +``` + +## License + +This project is licensed under the terms of the MIT license. diff --git a/certs/ca.crt b/certs/ca.crt new file mode 100644 index 0000000..d00ca34 --- /dev/null +++ b/certs/ca.crt @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIUbFpLgi/L/vHM9j0yPmF5d9KmTWowDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDEyMDUxMTUzMzFaFw0zNDEy +MDMxMTUzMzFaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCvP+nxquwPcjDvvSc4KhK8Y1IarghdnyVnief5mW9c +mvrA7DJ/SBfyMNJrbfOA8Mdp9AzVXUvh47ipUmPmuQ+uAwhz1NRLqGDGo9K2cENE +41O76U9f3fdURSOBjvWIQEeOqgvus4Vvd3kyrfhNajJtqrYAmyqHzPyWn1HBwyCW +FAXLWiXYb2wV7zu6K1IYFhYXRp1MrpwrlcZ47Si2oOwZfmReMY7njXPj8FoKeU80 +r4yEYL51uL9GzTwh+jZx65Zl3DWvKjCdhsvtEB2U2cAnk8fxD64G6aOtLtzTtmGE +Xxm1QjE8nUIj5EjBHkH+tgqDO9rcTnheuYSuGeItOrNNZ5B2ywSafQ2sWHjKBl1E +TNa+4hwmmaDxc53sP7lOLzz30hDA6rEdGqMM/D0UuCgTTHih4QpKM0ctEdTTBR35 +8D7x7az467N+msnTZiVvrAwNCEsWwiMszCsKKHG4dseLcmu9TTRlSkQAMFKLBguF +7U5DjDmpuCIETspaYYjoz8mNtKS7/g+dYawd9FhNepfIa6RnasbfNH/C2pQbIxf9 +41sf6AQVQZTiSwVeOPQr+Wn5WPHUZ8uM37nhWhrKRU012w4dKoluG64knfJdBuSq +CAr69q5MU67zOLBK0DskRSoNS4g8BX4A3iplKNg9MqiKR8Vbg3qTVKY6UFDW3a+r +HwIDAQABo1MwUTAdBgNVHQ4EFgQUwv0Z5vmgPGbRLcUfe68zYsjEXtIwHwYDVR0j +BBgwFoAUwv0Z5vmgPGbRLcUfe68zYsjEXtIwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAgEApSh6WMMvLi0FZs/u1j4c018RjsBYcDM5Ug8PQkm4vC0m +ZrWE3VVo8MvVmWhps83PfzCwvmhkAnnzkWk0WWwIlQij0X3Je2ejQ6FJfhubi9lT +UAORsrd8SUJbx5SCkLGoBx2OVeeXE+CZcG2yEMoGZzSgHLHNvk3jl4DOAgbo0YjS +mJxuIRMbC4KNhW/JOJfIxCiPk2Od00EHXVhiAxedpdJ5dkiV8XH+2aL9KNeVRXPX +XQPX8K/itFKONcmh8W1hthlKY+jJIkPP7XpyMr8E0BaVuKB2GoA3oiqY88wVCMpD +V0ZYbJG4bdX8Fb7DTACXo+ORFTrWFKbwg8ASCQB0OkPMqA2JAaPR2NhT2zX22Auh +9UiPcEc2HP5ozivGNeBAfylWW7SZVFcpzzm5lRokGPQJwO5dDcG1AyL5TPfwaTet +met/mjTFKlJxbR8xf+oMGGS1jZ5qcMh+IJ0BTvYFeDiLp2Tl8yJ5zbSmR/iIqDwS +GxSdvGe9I6moi3QQOJJ2TrjPpFnNOvSc3kIPvj8JjMwerkCVtDK1J41chdH9WoXJ +ODjewC6bZ5y5OzyHa54q/8GVzhs6MESDJMhOMCqWP27Knlij+tSjpFQm9GyNfjjp +m0eGfqD2sKqEUK6+OVwJiPK6V95tRTviUF7QT8TDnaDUEtpMwznzmWTEnW0KZaE= +-----END CERTIFICATE----- diff --git a/certs/ca.key b/certs/ca.key new file mode 100644 index 0000000..0003322 --- /dev/null +++ b/certs/ca.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCvP+nxquwPcjDv +vSc4KhK8Y1IarghdnyVnief5mW9cmvrA7DJ/SBfyMNJrbfOA8Mdp9AzVXUvh47ip +UmPmuQ+uAwhz1NRLqGDGo9K2cENE41O76U9f3fdURSOBjvWIQEeOqgvus4Vvd3ky +rfhNajJtqrYAmyqHzPyWn1HBwyCWFAXLWiXYb2wV7zu6K1IYFhYXRp1MrpwrlcZ4 +7Si2oOwZfmReMY7njXPj8FoKeU80r4yEYL51uL9GzTwh+jZx65Zl3DWvKjCdhsvt +EB2U2cAnk8fxD64G6aOtLtzTtmGEXxm1QjE8nUIj5EjBHkH+tgqDO9rcTnheuYSu +GeItOrNNZ5B2ywSafQ2sWHjKBl1ETNa+4hwmmaDxc53sP7lOLzz30hDA6rEdGqMM +/D0UuCgTTHih4QpKM0ctEdTTBR358D7x7az467N+msnTZiVvrAwNCEsWwiMszCsK +KHG4dseLcmu9TTRlSkQAMFKLBguF7U5DjDmpuCIETspaYYjoz8mNtKS7/g+dYawd +9FhNepfIa6RnasbfNH/C2pQbIxf941sf6AQVQZTiSwVeOPQr+Wn5WPHUZ8uM37nh +WhrKRU012w4dKoluG64knfJdBuSqCAr69q5MU67zOLBK0DskRSoNS4g8BX4A3ipl +KNg9MqiKR8Vbg3qTVKY6UFDW3a+rHwIDAQABAoICABAKqrB7c9ZKHp6jUua6OzLR +aJ+WlJ91ROhAYGKhn+b7LL7iIBE0mTSLMYex7ds8rxRMyavyOVL5FFszdn+VKxFD +p89qiPBP/mPQdSZMCmxQ3sZRqfldiRlGpuRiIKmTMLmnaSY1ep5kckyoThVQBkOx +n61YhsEdi3WCKeqxoNb8CDfADbzNHji3yGDXPFGGHAmPZjCxvwviTuOc2eA1xMbk +oe1ZXfpmIViZFLTmu9BXzWYEsQp3mdKyULHPhJJS/VZfnO5mz0JsJ1iQ5BRPBl7Z +ETFIvSZW1quwoXgjtrN2PRUxdO5WespsSBidW28kXLv8i6Ek8bHCC5ogNmrrd6QU +BgNdbrUvuLi0Nwttyjxv1KSPr4wiPUjwwtc833xNvLCiW2dFJL4SlMJtX/J5BuRg +s7PU9UupVxrJVTvLHADQEbJMaB1BJ83Fj5ho10GTcvww1n9d8ZwGQzrE37W9crk8 +moRU7v9wrzekUo3a6sKs5k0qWlM7uLn+jIS+AQdXqhip/ZWkRk6bdXgYNjtU+dnr +3staV3/DRfxX21VG02mofAnU/Pb359GyZQYo8o5IH9GVdrNy+eNK7CLHQX7YHhoz +M8O7GLB7J/BGvSdxJdYXjhT78rEZGYUdMwmtnxXjQWz3xTh58JVKggthXQy307RB +ZPn1S7hpF8h3zrjFAXCRAoIBAQD1lCR86iUAO6GClosKxwmd/zeM/899Zm7FQ9CA +9n9O3+EdWkC0sxO2BPqswDkNHcVY7b38bNoVf0fu/GLLTJBHxt1PsQMv64ndKTFn +OCCvI6IYlo6Oj5Hr0NeCcjahahR6vN6R430Jr7C9s6Adx8lzZFEMip7FJeqUBh6B +aYK8VyWcvtr6e27DNwm0LitCoJK6pJBKfvaSJx4Mxu04RN16aG7uBRdyLiOHcGJp +/i5IUEok6/h6FDTYITRZdG6Ei3LndU6v1G9Gq2RD0HUzkYCKfyfpgzIETL+DnzZR +EoQ2/QqohP7PqPh+NSPTvymapdjua5sJKoXM6pldty9aHg5pAoIBAQC2r7+H7FhH +x+kVTsfYOX1LPOeiFu2sJjuVpzhOXrsGC3sHq0sk4S4UJFKoJdpNvWOasTss5iCs +XSmb3hDgxDrXhDoUjpn5L6gKVtnhPYR4gocxpkUvT2nkcCxp5JizKeK0cJGvsdia +w35dqNpfV0FrZ9/zTwtfuCIf4aqXdX/oHUInFEfy+x3F14vr6aFgqytgUL2EGebk +7Zd0KF4W1aqXj4vvQD3TjL3mwc9i4V6pB9v0ubuYe3YJlXICuqq3SRv0wCeWjhhQ +nmgovZJhNArrVqE/9shipVGXRiD41OuSyuTMp3rd9tz4wi3w7JtjE7dRs7xelqXX +RMIfZksNLsxHAoIBAGSmLr4ziK6rweovoRTttndW3oGfZn7SuJuIy6/PVyYQg8bv +8o0cx/tV1xduQPOrO/LSnYcTZd5hqC2+qw7/djK0woei2NePBZXrCCBx1JNzW0AQ +lKTBGuE3WtxPyywkufgD6ISKY/jQVPOq5vjNpTbx6nXlamUKwTWhvGb1w2tFnFi9 +sCnw5NiFeiiqs3g/L2PnhmvB1XTZK2u5LAhf0RYWL0DGPXHCjzU3Tl56mqVworK3 +M8N0/KOIGFiBa7pPHOzYG0PdIS4pmJJioWZdP/2DV+xQpPM2MVfUrQJVQHL5CqE+ +wOCDNDUlumVSd862Ik61M3lyQBxYGjtalMGuh+ECggEAB2R9P6bUu7LfP7l8ZMeO +xmikhu6el4TEjH3DzYgP0WLNi7XmyItELhR6M8u0VckAKtZZKqv1ToRGlsQZsr6+ +EtKK9yH+IRNInYJ+NpsirS27AstTqWJxSokvgul5NGbRgbO8cXuk5D5c8rVOgOUr +BImjilj8gNcWqmubV52tm1rzvlkXwEFhJwxd4SIHIb5Ldw5NXPcfXMwwgMbRZ+Ml +kBSC+R+EYPclCB13ouyzw+tJf1G0wardT+34OxwKpHgU5YaRE4qUeU6vsFZbggt1 +FajfxZLa8QL3lkOEOg2DquEC+TUdx7Who39YFJO+hffaRzgau0klVNy3bkXZa6Ml +KQKCAQEAktQm0NNicdQ88cpbocd0O0SsR8EwsSPW7vA95FbXf9H+YtaoGP7Z3fiO +h+v9Fm1GBMkuDKTv5bP7ZtA+HLq1J9d8FU7gcUVo1zWHkzmflyKxYR07Zb2ZJMAR +5m/GY8zGehZJzh04cYijtg6FPTbDoW3iamb1fuIvC/MVvkTQrK1HInpNh0SJUZ2n +L2VNfVdFFojdEppPO6gNeKAiEn4XBUv8euwfeybdZUzgV0MwuPz/oDN+2Y7dvtPm +ZIYEsULG9o9S6ZOIRVJOLy1fVaBMTXDCXLfGpdw2pu+QJ7tzvxGLLw26MSUkPWQ6 ++Oq7Vx2UC8kl6/ZGY1n2HjpjdjQ2Qg== +-----END PRIVATE KEY----- diff --git a/certs/ca.srl b/certs/ca.srl new file mode 100644 index 0000000..46ec87f --- /dev/null +++ b/certs/ca.srl @@ -0,0 +1 @@ +143B6FB8E49CFF276C646967B16D2F8E5CE2AF28 diff --git a/certs/cert.crt b/certs/cert.crt new file mode 100644 index 0000000..5bb76cb --- /dev/null +++ b/certs/cert.crt @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIUFDtvuOSc/ydsZGlnsW0vjlzirygwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDEyMDUxMTU0MDFaFw0yNTEy +MDUxMTU0MDFaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQDd5qfWSLTP8t6Ut44xvKwJPoqAZphuVUq+nDkWx8rP +XnDbKZbTjL4Zb/vhqU8SBW9BpjSpxRjXRtQUsH7qeX/m2cvX3BOcr6fvCTVkgxmy +U60G2h2/PX99M/3x2Zd9jDpgKqSBdbR9N3saIeFxD8mTC0qLSt0R9PlousYlItfN +2vm8OObOXcJvBP8gYLpNhiT0LP+PlcU/PBYPYzoVs4f4U5odj6RUbJLU6x8QSaza +npg/FUi43VYzEBewjKcRQQck/4GaH30ceWVs9IQY9t1zlJDO078ssY8YZ/jsrD4p +Hwt1y2+ui0fa+F40b59mXXB9440i4UK5fik3wiUb3L5p1MxsUA9P8fRhapjP6C8Q +q8zLoKIXNdMFRntbbA+xfFiXr0ONr/6E2Bd1EnMo9jZuECUN3yDFDRY2bQm9v/tk +70ZZuJEgTc5GgrU386N8kmVInzcUf0ri1pD6UKV4cqA1ajkhT9rcPFRNxrE6gpuj +1sLN0Htjl4k2FcI8LYeXp0tm7S+HK4eF4gWUaF29u+TJVsbNr1dImvAihcSMRogl +PnFeZu72gAQU/lYrMF8EElo7eiC0gWSQNquufN7jrNbLGPefY9X/qSfu3h592ETa +RSpA7nqVMJNvOp/qBQMMVcBdI0OJ8ov4QSObZZNCKaBZH1zpDwEY1qoFaSpXXo6H +dQIDAQABo0IwQDAdBgNVHQ4EFgQUT2eNIEIpFlUEGgaKvA+tTc9KMKUwHwYDVR0j +BBgwFoAUwv0Z5vmgPGbRLcUfe68zYsjEXtIwDQYJKoZIhvcNAQELBQADggIBAJ32 +JlHqAlvhxrgYFpwmBMyDCcdrik7u+sZOYp/DARNbUX31gPfQa1PMtQw5jvMDTtJy +0UcQBldYHNbr+U7OJ+ueVh0UJ3sX2VHj39sshT5OAwbiK6+cja5ifzzLXCUTVrQj +LouulUUXASa1xx/r8YYJIJsEY5EUfajy9Um4/o8Fob9X9PeZljb5Bl04gF82V5xS +b8tr+2JJdgDUl0krwwpl1LfnUqmHzFSg8eIe4cMgF3gHkbtuINgDIKW9u7amLJm+ +MRWHUSdqXJ9GK3NgFGRfxX3PnXmC+/XiZVj/mfdSfw7Fwv1IwZqpoUQjRZKL6+P7 +2+J5qZ70DScjG84RJNWm3e6LRw0OracDORhs7OGdCtrJOC2i9ctsid0Z2Z110hT7 +cWcAQbkA6E5GbovyzXMY3H75cGKnIgWBgsnlbpw95bU+eE+YyeULdNJs1CkBHQyQ +Qqy2gbCG6WF/PkI7UrvFxUiGqQ6HTbppBQbKPYJ09taDTyCzN/YgAM/B4KqTJOOl +RdLcSl7beklxS2IVwSGshDzrn3H2bXgjGewr9Sa7CvYt1QZDs5YuNfwIxxks2W7c +Ix66D+3sENozpR1QDZ1IQxYzPKb3P997kYHsSyaNGdaQCTdV9cxln1zwcGPuNahA +hSB8QXy1QdyYY1veAu4KTabTwS2zwMkURtPFsrS9 +-----END CERTIFICATE----- diff --git a/certs/cert.csr b/certs/cert.csr new file mode 100644 index 0000000..178547c --- /dev/null +++ b/certs/cert.csr @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIEijCCAnICAQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx +ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAN3mp9ZItM/y3pS3jjG8rAk+ioBmmG5VSr6cORbH +ys9ecNspltOMvhlv++GpTxIFb0GmNKnFGNdG1BSwfup5f+bZy9fcE5yvp+8JNWSD +GbJTrQbaHb89f30z/fHZl32MOmAqpIF1tH03exoh4XEPyZMLSotK3RH0+Wi6xiUi +183a+bw45s5dwm8E/yBguk2GJPQs/4+VxT88Fg9jOhWzh/hTmh2PpFRsktTrHxBJ +rNqemD8VSLjdVjMQF7CMpxFBByT/gZoffRx5ZWz0hBj23XOUkM7Tvyyxjxhn+Oys +PikfC3XLb66LR9r4XjRvn2ZdcH3jjSLhQrl+KTfCJRvcvmnUzGxQD0/x9GFqmM/o +LxCrzMugohc10wVGe1tsD7F8WJevQ42v/oTYF3UScyj2Nm4QJQ3fIMUNFjZtCb2/ ++2TvRlm4kSBNzkaCtTfzo3ySZUifNxR/SuLWkPpQpXhyoDVqOSFP2tw8VE3GsTqC +m6PWws3Qe2OXiTYVwjwth5enS2btL4crh4XiBZRoXb275MlWxs2vV0ia8CKFxIxG +iCU+cV5m7vaABBT+ViswXwQSWjt6ILSBZJA2q6583uOs1ssY959j1f+pJ+7eHn3Y +RNpFKkDuepUwk286n+oFAwxVwF0jQ4nyi/hBI5tlk0IpoFkfXOkPARjWqgVpKlde +jod1AgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAgEAJvlHLAc1r5Jh4toXqxKSvfhx +siW6508V6kVVMjGc29e3Zp1iKNbMR50dVFPVi1utkOLcv4XRrxuNObAmH0qUbhan +rIm0Y3vPLqiFFSI14nTxMjFvqANfaFJ3YGK40QeOcTWdIqaBe6wIjpT0ey7cAZ9u +5OhETFOd6JEfo+KqX0Yq/30KaYp3ZXSu7CKMd03BY8vAgD2A2uHo8vdGhSRtsB/v +KnuT2RDCqg66rdbqM0geDM+bDN05dqYUfZqyBVnT9ImgpE/MhKQPSyMyak7oHqhy +LZCrC25BIEL2T1ZDl4ebiT9MperKkZFdMhQh88yvARncxhJOxfE3kyY03zbd7Atd +OPa+DGgZ5r4gU/gmc7A4u3KYge5tRmcqx9AVGYALRtmC0wUvz8rUDPwhyhThfSDC +OG3wRdH7iI2Si090XMi2PyyJ4YjVW8rGKz3mvqPXHDZ+7FD5dCBAFN/bl8gtsCuR +RTKqkiCEOGJLj0GcVXZPOp5+7E+mCLT5RNQWRmoJberihh7W1WT8sjTcbpC6qufS +iiYWIlxCnQsAuAY5LqVdFLMKZ6N3VTvxhj4GxpZd8ij49yG9tDZzf1cUmp5KwMrf +iue9wB3XjXjm0PGirjvtvTXD85URVMCRDmk2sZbJ1L3En3E68G2rKGOjdXFnDvXg +1H1xYHqa70hITq5Zpdc= +-----END CERTIFICATE REQUEST----- diff --git a/certs/key.key b/certs/key.key new file mode 100644 index 0000000..6e588c8 --- /dev/null +++ b/certs/key.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDd5qfWSLTP8t6U +t44xvKwJPoqAZphuVUq+nDkWx8rPXnDbKZbTjL4Zb/vhqU8SBW9BpjSpxRjXRtQU +sH7qeX/m2cvX3BOcr6fvCTVkgxmyU60G2h2/PX99M/3x2Zd9jDpgKqSBdbR9N3sa +IeFxD8mTC0qLSt0R9PlousYlItfN2vm8OObOXcJvBP8gYLpNhiT0LP+PlcU/PBYP +YzoVs4f4U5odj6RUbJLU6x8QSazanpg/FUi43VYzEBewjKcRQQck/4GaH30ceWVs +9IQY9t1zlJDO078ssY8YZ/jsrD4pHwt1y2+ui0fa+F40b59mXXB9440i4UK5fik3 +wiUb3L5p1MxsUA9P8fRhapjP6C8Qq8zLoKIXNdMFRntbbA+xfFiXr0ONr/6E2Bd1 +EnMo9jZuECUN3yDFDRY2bQm9v/tk70ZZuJEgTc5GgrU386N8kmVInzcUf0ri1pD6 +UKV4cqA1ajkhT9rcPFRNxrE6gpuj1sLN0Htjl4k2FcI8LYeXp0tm7S+HK4eF4gWU +aF29u+TJVsbNr1dImvAihcSMRoglPnFeZu72gAQU/lYrMF8EElo7eiC0gWSQNquu +fN7jrNbLGPefY9X/qSfu3h592ETaRSpA7nqVMJNvOp/qBQMMVcBdI0OJ8ov4QSOb +ZZNCKaBZH1zpDwEY1qoFaSpXXo6HdQIDAQABAoICAEg+pVwttbiSUQdIL6Jf0/76 +fqtJO82INVqTkD6rc4tKKyIfizx68RVlETOqJNUwMcXE8BZp1imYpMnLoLaEMjEd +rbEstLHpuponfFuqFz6o4Yd+kfrGcfB4cfBAsIKumf7fQ0nm4Yl2+7xJVZWy1yTp +oy5whEMpZ95CGOrUSkB6T56JRBPiEMCGdu26sE03JGbfE6FS2LI6xM/jtXCFT/p4 +dY+0SYM3CiMKHcX2xrEyu0ymiFOvtDXRwnS3hlkmu8W+7hoYsGoJ2Ay+Gxfpn7XW +o8LCy9YoRPdkOnYLqf1HXzrNriG8tPtEq58UzGfOeiZyZGv7vPaZbZ/6tIw6tT8y +CYKaIIlgWO1d50xL+9hNw1FA4ziqG4aouDRgMFNNxtXL3LfnZCcCLSUekB3KUVgc +N2KHkVKbyj+KsJyiRCCye7UikwFv0t2u+plqGTjKBwc9lgIJTx0n9fjzfKxfOsA0 +inMg9Ak6BeoBjVLRHUEuqILmoNWWQviHzFJGvPuZtfRnrPAC5TODBSp8uymoyfg+ +e+Tl2n6PKGDJEO/bfdfcTKNAqnVSAhRaD5vi+tMr8xUGf7Yh8Zib004WU17A+5+i +NB1BhY/XjeOhNrCD6wTo73Y5FCT1m0n0LReTMrjRnOdbqCQLh+Hy/5lJRSw7x+aC +fA54Yq5HJ/bCZtGEJdotAoIBAQD9xDzuECbAsOTfZkyrxrFPtZLHXzGWyU+0w3bw +lQFeqCsw5qey2igYel2965liGbmAfBMBMUgq5A+UaDKkmQLc3DOgQoMyUNQ15CTO +inzJA/S4YkkDb9LqQjKCDIavMFw6XJAgALStHDttPC1SiU7vARp9nczkjswF+L7Z +arSuhhD2wGA89es+/o8jIoForDbB7yfxVq29EwTHQgEx0/AoaastQy03S/SMXMSz +uX289+qsaltAzZ8H7V3xMUC8gbFXSFdDxoGJekgr0XA28Km0G1PICMm5sX2aZU4K +k55a2/yrwSrLztn13N7QwJcDM2kU3bBHOi+VebPazwDIH4TXAoIBAQDf2p8KOuqm +CTEjg+FYK1LTm1G0uM5acYSppNOe2k48TmTOepusG3ePWBZix77ehY/pqaSUDdEG +FV1dlyATKBXm1VGn9Oq/gpyL8krXzJ8j6CU12L8oevDud0AnH2tBdjbNqaC7PgLe +eci2ZrvJ3D3MXniIOT0Dbf4w5CLW4yLvus/tFWxanqEBTJEJIDxQTEXWUD8BwxMX +FSjD1A7lptA93odPJpv8esPU2YW/GDkkObcE3rbQ/PEkO/hXWYq/KQx9KuEE8oZ5 +EPGiZl9E1JHYsYtlgFLmbKhD0f2Bil1iBIgZ7m2+qFH1ZocbrJrkuSUjplwPnmwR +8zCqVDyHTMCTAoIBAQCQxT8tYTF6hHBNsoLTDItjOeb+rqQPrdE3Arv8DEW3xqC9 +SRkqw6JUNJr+GkZq3NojHqWI7KCLN4hb1gXuOQyVC1q4drl6Hvqxs/H7kq4Vz5fx +CME2oLjmw9UktBiyIRi8gsoGN+DbUVvluYrxpzvMxghi2X9mdMCYN4xJZMKnPfy3 +iJBetrz6NydZl/nTyUuD3/gdiqGbWBpFwrYDwzEjHQV20Pger8pXuSTOk2fUQmsy +6YodsjCjyRrq2npgEG0nqjF+jOShlY1O0jD+ZtWp1l05pSnQMh3B00Fub0DL/Oxs +38qWcu+Nf+/tj7GXNeEg8kf6motC7ydYEPgVM0YJAoIBAGUwlT3xngqKEy1juyS5 +CMrg5hFUjOszb22kNYkUU9NM+KKhp+cnz2b7wbrLYkuCgqh0aBIJINioJbldzED8 +mNHs0emje84wQ4W7c/uS4sk/cjqiDN9Gm8ygGV7WBAzmXCWhrSeXA4L/+CwDypWY +OlM8zS1++kannQUKy3jp2ewWPVGFoqJgHJXSv6kpajo+ED+trJ2mSeXgSbokDMvh +GTcudWnhIRgDgqyf26ajU4k9ka7g4jEcdgEUHtGVh3OcIaofDPLIkuL9Ns1bi2s4 +z+jtcP7kABVrPrDRps+89TOOqttV/UP3IH1W6HIpFyeXTeOMmwbwbRe8H96PD4F1 +v4MCggEBAL3T7MKSF3oxV0KDOFWAyqDlMk0pZDVJQRV7Fu0VkOPjrjFts7Q0P07V +RSxYHyz7xWQFoJKcBFB/7S2q7aV4WBpY2myMduraykGaaTsS5KGBkYrV+0fMYmZ1 +Y9ELxaP67N3ACsP19JYJB2QEnoboEXsSSmMSAnb5ZiL996pyFhCkU/zd/0Aqrsj8 +nQTj/zZcSIbUcDiUM1mmefFduMRGtjYE4WgNDSDRPjAAfIlQcCx8BY+tZ4/XQm4g +bF+sKqMMSbz0B/mAOD/Ln8i7UvYsPJCm3pXGjp0fOlusRNYO2EZqACn1EdJm8pFY +xnfTXfBUcwChYj88fJJTP2bM4r+XAms= +-----END PRIVATE KEY----- diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f792454 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,78 @@ +services: + + postgres: + container_name: dev_fast_healthchecks_postgres + image: postgres:17-alpine + ports: ['${POSTGRES_PORT}:5432'] + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DATABASE} + + redis: + container_name: dev_fast_healthchecks_redis + image: redis:7-alpine + restart: on-failure + ports: ['6379:6379'] + command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"] + + rabbitmq: + container_name: dev_fast_healthchecks_rabbitmq + image: rabbitmq:4-alpine + restart: always + ports: + - '${RABBITMQ_PORT}:5672' + environment: + RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER} + RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD} + RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST} + + zookeeper: + container_name: dev_fast_healthchecks_zookeeper + image: confluentinc/cp-zookeeper:7.8.0 + hostname: zookeeper + ports: + - "2181:2181" + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + + kafka1: + container_name: dev_fast_healthchecks_kafka1 + image: confluentinc/cp-kafka:7.8.0 + hostname: kafka1 + depends_on: + - zookeeper + ports: + - "9094:9094" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka1:9092,OUTSIDE://localhost:9094 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,OUTSIDE:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL + + kafka2: + container_name: dev_fast_healthchecks_kafka2 + image: confluentinc/cp-kafka:7.8.0 + hostname: kafka2 + depends_on: + - zookeeper + ports: + - "9095:9095" + environment: + KAFKA_BROKER_ID: 2 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka2:9093,OUTSIDE://localhost:9095 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,OUTSIDE:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL + + mongo: + image: mongo + restart: always + environment: + MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER} + MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD} + MONGO_INITDB_DATABASE: ${MONGO_DATABASE} + ports: + - "${MONGO_PORT}:27017" diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..363dd95 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,12 @@ +# API Reference + +::: fast_healthchecks.models + options: + heading_level: 3 + show_root_heading: false + +::: fast_healthchecks.integrations.fastapi + +::: fast_healthchecks.integrations.faststream + +::: fast_healthchecks.integrations.litestar diff --git a/docs/changelog.md b/docs/changelog.md new file mode 120000 index 0000000..04c99a5 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1 @@ +../CHANGELOG.md \ No newline at end of file diff --git a/docs/img/black.svg b/docs/img/black.svg new file mode 100644 index 0000000..5988e1a --- /dev/null +++ b/docs/img/black.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/img/favicon.ico b/docs/img/favicon.ico new file mode 100644 index 0000000..e031f4f Binary files /dev/null and b/docs/img/favicon.ico differ diff --git a/docs/img/logo.svg b/docs/img/logo.svg new file mode 100644 index 0000000..db3f5ef --- /dev/null +++ b/docs/img/logo.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..2a5816c --- /dev/null +++ b/docs/index.md @@ -0,0 +1,69 @@ +

+ FastHealthcheck +

+ +Framework agnostic health checks with integrations for most popular ASGI frameworks: [FastAPI](https://github.com/fastapi/fastapi) / [Faststream](https://github.com/airtai/faststream) / [Litestar](https://github.com/litestar-org/litestar) to help you to implement the [Health Check API](https://microservices.io/patterns/observability/health-check-api.html) pattern + +--- + +## Installation + +With `pip`: +```bash +pip install fast-healthcheck +``` + +With `poetry`: +```bash +poetry add fast-healthcheck +``` + +With `uv`: +```bash +uv add fast-healthcheck +``` + +## Usage + +The easier way to use this package is to use the **`health`** function. + +Create the health check endpoint dynamically using different conditions. +Each condition is a callable, and you can even have dependencies inside of it: + +=== "examples/probes.py" + + ```python + {% + include-markdown "../examples/probes.py" + %} + ``` + +=== "FastAPI" + + ```python + {% + include-markdown "../examples/fastapi_example/main.py" + %} + ``` + +=== "Faststream" + + ```python + {% + include-markdown "../examples/faststream_example/main.py" + %} + ``` + +=== "Litestar" + + ```python + {% + include-markdown "../examples/litestar_example/main.py" + %} + ``` + +You can find examples for each framework here: + +- [FastAPI example](./examples/fastapi_example) +- [Faststream example](./examples/faststream_example) +- [Litestar example](./examples/litestar_example) diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/fastapi_example/__init__.py b/examples/fastapi_example/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/fastapi_example/main.py b/examples/fastapi_example/main.py new file mode 100644 index 0000000..2a8392e --- /dev/null +++ b/examples/fastapi_example/main.py @@ -0,0 +1,98 @@ +from fastapi import FastAPI, status + +from examples.probes import ( + LIVENESS_CHECKS, + READINESS_CHECKS, + READINESS_CHECKS_FAIL, + READINESS_CHECKS_SUCCESS, + STARTUP_CHECKS, + custom_handler, +) +from fast_healthchecks.integrations.fastapi import HealthcheckRouter, Probe + +app_integration = FastAPI() +app_integration.include_router( + HealthcheckRouter( + Probe( + name="liveness", + checks=LIVENESS_CHECKS, + ), + Probe( + name="readiness", + checks=READINESS_CHECKS, + ), + Probe( + name="startup", + checks=STARTUP_CHECKS, + ), + debug=True, + prefix="/health", + ), +) + +app_success = FastAPI() +app_success.include_router( + HealthcheckRouter( + Probe( + name="liveness", + checks=[], + ), + Probe( + name="readiness", + checks=READINESS_CHECKS_SUCCESS, + ), + Probe( + name="startup", + checks=[], + ), + debug=True, + prefix="/health", + ), +) + +app_fail = FastAPI() +app_fail.include_router( + HealthcheckRouter( + Probe( + name="liveness", + checks=[], + ), + Probe( + name="readiness", + checks=READINESS_CHECKS_FAIL, + ), + Probe( + name="startup", + checks=[], + ), + debug=True, + prefix="/health", + ), +) + +app_custom = FastAPI() +app_custom.include_router( + HealthcheckRouter( + Probe( + name="liveness", + checks=[], + summary="Check if the application is alive", + ), + Probe( + name="readiness", + checks=READINESS_CHECKS_SUCCESS, + summary="Check if the application is ready", + ), + Probe( + name="startup", + checks=[], + summary="Check if the application is starting up", + ), + success_handler=custom_handler, + failure_handler=custom_handler, + success_status=status.HTTP_200_OK, + failure_status=status.HTTP_503_SERVICE_UNAVAILABLE, + debug=True, + prefix="/custom_health", + ), +) diff --git a/examples/faststream_example/__init__.py b/examples/faststream_example/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/faststream_example/main.py b/examples/faststream_example/main.py new file mode 100644 index 0000000..50e8daf --- /dev/null +++ b/examples/faststream_example/main.py @@ -0,0 +1,84 @@ +import os +from http import HTTPStatus + +from faststream.asgi import AsgiFastStream +from faststream.kafka import KafkaBroker + +from examples.probes import ( + LIVENESS_CHECKS, + READINESS_CHECKS, + READINESS_CHECKS_FAIL, + READINESS_CHECKS_SUCCESS, + STARTUP_CHECKS, + custom_handler, +) +from fast_healthchecks.integrations.faststream import Probe, health + +broker = KafkaBroker(os.environ["KAFKA_BOOTSTRAP_SERVERS"].split(",")) +app_integration = AsgiFastStream( + broker, + asgi_routes=[ + *health( + Probe(name="liveness", checks=LIVENESS_CHECKS), + Probe(name="readiness", checks=READINESS_CHECKS), + Probe(name="startup", checks=STARTUP_CHECKS), + debug=False, + prefix="/health", + ), + ], +) + +app_success = AsgiFastStream( + broker, + asgi_routes=[ + *health( + Probe(name="liveness", checks=[]), + Probe(name="readiness", checks=READINESS_CHECKS_SUCCESS), + Probe(name="startup", checks=[]), + debug=False, + prefix="/health", + ), + ], +) + +app_fail = AsgiFastStream( + broker, + asgi_routes=[ + *health( + Probe(name="liveness", checks=[]), + Probe(name="readiness", checks=READINESS_CHECKS_FAIL), + Probe(name="startup", checks=[]), + debug=False, + prefix="/health", + ), + ], +) + +app_custom = AsgiFastStream( + broker, + asgi_routes=[ + *health( + Probe( + name="liveness", + checks=[], + summary="Check if the application is alive", + ), + Probe( + name="readiness", + checks=READINESS_CHECKS_SUCCESS, + summary="Check if the application is ready", + ), + Probe( + name="startup", + checks=[], + summary="Check if the application is starting up", + ), + success_handler=custom_handler, + failure_handler=custom_handler, + success_status=HTTPStatus.OK, + failure_status=HTTPStatus.SERVICE_UNAVAILABLE, + debug=True, + prefix="/custom_health", + ), + ], +) diff --git a/examples/litestar_example/__init__.py b/examples/litestar_example/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/litestar_example/main.py b/examples/litestar_example/main.py new file mode 100644 index 0000000..26e35a1 --- /dev/null +++ b/examples/litestar_example/main.py @@ -0,0 +1,76 @@ +from litestar import Litestar +from litestar.status_codes import HTTP_200_OK, HTTP_503_SERVICE_UNAVAILABLE + +from examples.probes import ( + LIVENESS_CHECKS, + READINESS_CHECKS, + READINESS_CHECKS_FAIL, + READINESS_CHECKS_SUCCESS, + STARTUP_CHECKS, + custom_handler, +) +from fast_healthchecks.integrations.litestar import Probe, health + +app_integration = Litestar( + route_handlers=[ + *health( + Probe(name="liveness", checks=LIVENESS_CHECKS), + Probe(name="readiness", checks=READINESS_CHECKS), + Probe(name="startup", checks=STARTUP_CHECKS), + debug=False, + prefix="/health", + ), + ], +) + +app_success = Litestar( + route_handlers=[ + *health( + Probe(name="liveness", checks=[]), + Probe(name="readiness", checks=READINESS_CHECKS_SUCCESS), + Probe(name="startup", checks=[]), + debug=False, + prefix="/health", + ), + ], +) + +app_fail = Litestar( + route_handlers=[ + *health( + Probe(name="liveness", checks=[]), + Probe(name="readiness", checks=READINESS_CHECKS_FAIL), + Probe(name="startup", checks=[]), + debug=False, + prefix="/health", + ), + ], +) + +app_custom = Litestar( + route_handlers=[ + *health( + Probe( + name="liveness", + checks=[], + summary="Check if the application is alive", + ), + Probe( + name="readiness", + checks=READINESS_CHECKS_SUCCESS, + summary="Check if the application is ready", + ), + Probe( + name="startup", + checks=[], + summary="Check if the application is starting up", + ), + success_handler=custom_handler, + failure_handler=custom_handler, + success_status=HTTP_200_OK, + failure_status=HTTP_503_SERVICE_UNAVAILABLE, + debug=True, + prefix="/custom_health", + ), + ], +) diff --git a/examples/probes.py b/examples/probes.py new file mode 100644 index 0000000..2ecd78c --- /dev/null +++ b/examples/probes.py @@ -0,0 +1,71 @@ +import asyncio +import os +import time +from pathlib import Path +from typing import Any + +from dotenv import load_dotenv + +from fast_healthchecks.checks import Check +from fast_healthchecks.checks.function import FunctionHealthCheck +from fast_healthchecks.checks.kafka import KafkaHealthCheck +from fast_healthchecks.checks.mongo import MongoHealthCheck +from fast_healthchecks.checks.postgresql.asyncpg import PostgreSQLAsyncPGHealthCheck +from fast_healthchecks.checks.postgresql.psycopg import PostgreSQLPsycopgHealthCheck +from fast_healthchecks.checks.rabbitmq import RabbitMQHealthCheck +from fast_healthchecks.checks.redis import RedisHealthCheck +from fast_healthchecks.checks.url import UrlHealthCheck +from fast_healthchecks.integrations.base import ProbeAsgiResponse + +load_dotenv(Path(__file__).parent.parent / ".env") + + +def sync_dummy_check() -> bool: + time.sleep(0.1) + return True + + +async def async_dummy_check() -> bool: + await asyncio.sleep(0.1) + return True + + +async def async_dummy_check_fail() -> bool: + msg = "Failed" + raise Exception(msg) from None # noqa: TRY002 + await asyncio.sleep(0.1) + return False + + +LIVENESS_CHECKS: list[Check] = [ + FunctionHealthCheck(func=sync_dummy_check, name="Sync dummy"), +] + +READINESS_CHECKS: list[Check] = [ + KafkaHealthCheck( + bootstrap_servers=os.environ["KAFKA_BOOTSTRAP_SERVERS"], + name="Kafka", + ), + MongoHealthCheck.from_dsn(os.environ["MONGO_DSN"], name="Mongo"), + PostgreSQLAsyncPGHealthCheck.from_dsn(os.environ["POSTGRES_DSN"], name="PostgreSQL asyncpg"), + PostgreSQLPsycopgHealthCheck.from_dsn(os.environ["POSTGRES_DSN"], name="PostgreSQL psycopg"), + RabbitMQHealthCheck.from_dsn(os.environ["RABBITMQ_DSN"], name="RabbitMQ"), + RedisHealthCheck.from_dsn(os.environ["REDIS_DSN"], name="Redis"), + UrlHealthCheck(url="https://httpbin.org/status/200", name="URL 200"), +] + +STARTUP_CHECKS: list[Check] = [ + FunctionHealthCheck(func=async_dummy_check, name="Async dummy"), +] + +READINESS_CHECKS_SUCCESS: list[Check] = [ + FunctionHealthCheck(func=async_dummy_check, name="Async dummy"), +] +READINESS_CHECKS_FAIL: list[Check] = [ + FunctionHealthCheck(func=async_dummy_check_fail, name="Async dummy fail"), +] + + +async def custom_handler(response: ProbeAsgiResponse) -> Any: # noqa: ANN401, RUF029 + """Custom handler for probes.""" + return response.data diff --git a/fast_healthchecks/__init__.py b/fast_healthchecks/__init__.py new file mode 100644 index 0000000..5f065c4 --- /dev/null +++ b/fast_healthchecks/__init__.py @@ -0,0 +1,3 @@ +"""Fast Healthchecks.""" + +__version__ = "0.0.0" diff --git a/fast_healthchecks/checks/__init__.py b/fast_healthchecks/checks/__init__.py new file mode 100644 index 0000000..389880d --- /dev/null +++ b/fast_healthchecks/checks/__init__.py @@ -0,0 +1,14 @@ +"""Module containing all the health checks.""" + +from typing import TypeAlias + +from fast_healthchecks.checks._base import HealthCheck, HealthCheckDSN +from fast_healthchecks.models import HealthCheckResult + +Check: TypeAlias = HealthCheck[HealthCheckResult] | HealthCheckDSN[HealthCheckResult] + +__all__ = ( + "Check", + "HealthCheck", + "HealthCheckDSN", +) diff --git a/fast_healthchecks/checks/_base.py b/fast_healthchecks/checks/_base.py new file mode 100644 index 0000000..cc65296 --- /dev/null +++ b/fast_healthchecks/checks/_base.py @@ -0,0 +1,75 @@ +"""This module contains the base classes for all health checks.""" + +from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeAlias, TypeVar + +from fast_healthchecks.compat import PYDANTIC_INSTALLED, PYDANTIC_V2 +from fast_healthchecks.models import HealthCheckResult + +if TYPE_CHECKING: + from pydantic import AmqpDsn, KafkaDsn, MongoDsn, PostgresDsn, RedisDsn +else: + AmqpDsn: TypeAlias = str + KafkaDsn: TypeAlias = str + MongoDsn: TypeAlias = str + PostgresDsn: TypeAlias = str + RedisDsn: TypeAlias = str + +if PYDANTIC_INSTALLED: + if PYDANTIC_V2: + from pydantic import TypeAdapter + else: # pragma: no cover + from pydantic import parse_obj_as + +SupportedDsns: TypeAlias = AmqpDsn | KafkaDsn | MongoDsn | PostgresDsn | RedisDsn + +T_co = TypeVar("T_co", bound=HealthCheckResult, covariant=True) + +__all__ = ( + "DEFAULT_HC_TIMEOUT", + "HealthCheck", + "HealthCheckDSN", +) + + +DEFAULT_HC_TIMEOUT: float = 5.0 + + +class HealthCheck(Protocol[T_co]): + """Base class for health checks.""" + + async def __call__(self) -> T_co: ... + + +class HealthCheckDSN(HealthCheck[T_co], Generic[T_co]): + """Base class for health checks that can be created from a DSN.""" + + @classmethod + def from_dsn( + cls, + dsn: Any, # noqa: ANN401 + *, + name: str = "Service", + timeout: float = DEFAULT_HC_TIMEOUT, + ) -> "HealthCheckDSN[T_co]": + raise NotImplementedError + + @classmethod + def check_pydantinc_installed(cls) -> None: + """Check if Pydantic is installed.""" + if not PYDANTIC_INSTALLED: + msg = "Pydantic is not installed" + raise RuntimeError(msg) from None + + @classmethod + def validate_dsn(cls, dsn: str | SupportedDsns, type_: type[SupportedDsns]) -> str: + """Validate the DSN.""" + if not PYDANTIC_INSTALLED: + type_(dsn) # type: ignore[arg-type] + return str(dsn) + + if isinstance(dsn, type_): + return str(dsn) + + if PYDANTIC_V2: + return str(TypeAdapter(type_).validate_python(dsn)) + return str(parse_obj_as(type_, dsn)) # pragma: no cover diff --git a/fast_healthchecks/checks/function.py b/fast_healthchecks/checks/function.py new file mode 100644 index 0000000..9d9776f --- /dev/null +++ b/fast_healthchecks/checks/function.py @@ -0,0 +1,87 @@ +"""This module provides a health check class for functions. + +Classes: + FunctionHealthCheck: A class to perform health checks on a function. + +Usage: + The FunctionHealthCheck class can be used to perform health checks on a function by calling it. + +Example: + def my_function(): + return True + + health_check = FunctionHealthCheck(func=my_function) + result = await health_check() + print(result.healthy) +""" + +import asyncio +import functools +from collections.abc import Callable +from traceback import format_exc +from typing import Any + +from fast_healthchecks.checks._base import DEFAULT_HC_TIMEOUT, HealthCheck +from fast_healthchecks.models import HealthCheckResult + + +class FunctionHealthCheck(HealthCheck[HealthCheckResult]): + """A class to perform health checks on a function. + + Attributes: + _args: The arguments to pass to the function. + _func: The function to perform the health check on. + _kwargs: The keyword arguments to pass to the function. + _name: The name of the health check. + _timeout: The timeout for the health check. + """ + + __slots__ = ("_args", "_func", "_kwargs", "_name", "_timeout") + + _func: Callable[..., Any] + _args: tuple[Any, ...] + _kwargs: dict[str, Any] + _timeout: float + _name: str + + def __init__( + self, + *, + func: Callable[..., Any], + args: tuple[Any, ...] = (), + kwargs: dict[str, Any] | None = None, + timeout: float = DEFAULT_HC_TIMEOUT, + name: str = "Function", + ) -> None: + """Initializes the FunctionHealthCheck class. + + Args: + func: The function to perform the health check on. + args: The arguments to pass to the function. + kwargs: The keyword arguments to pass to the function. + timeout: The timeout for the health check. + name: The name of the health check. + """ + self._func = func + self._args = args or () + self._kwargs = kwargs or {} + self._timeout = timeout + self._name = name + + async def __call__(self) -> HealthCheckResult: + """Performs the health check on the function. + + Returns: + A HealthCheckResult object. + """ + try: + task: asyncio.Future[Any] + if asyncio.iscoroutinefunction(self._func): + task = self._func(*self._args, **self._kwargs) + else: + loop = asyncio.get_event_loop() + task = loop.run_in_executor(None, functools.partial(self._func, *self._args, **self._kwargs)) + await asyncio.wait_for(task, timeout=self._timeout) + return HealthCheckResult(name=self._name, healthy=True) + except BaseException: # noqa: BLE001 + return HealthCheckResult(name=self._name, healthy=False, error_details=format_exc()) diff --git a/fast_healthchecks/checks/kafka.py b/fast_healthchecks/checks/kafka.py new file mode 100644 index 0000000..13d6beb --- /dev/null +++ b/fast_healthchecks/checks/kafka.py @@ -0,0 +1,139 @@ +"""This module provides a health check class for Kafka. + +Classes: + KafkaHealthCheck: A class to perform health checks on Kafka. + +Usage: + The KafkaHealthCheck class can be used to perform health checks on Kafka by calling it. + +Example: + health_check = KafkaHealthCheck( + bootstrap_servers="localhost:9092", + security_protocol="PLAINTEXT", + ) + result = await health_check() + print(result.healthy) +""" + +import ssl +from traceback import format_exc +from typing import Literal, TypeAlias + +from fast_healthchecks.checks._base import DEFAULT_HC_TIMEOUT, HealthCheck +from fast_healthchecks.compat import PYDANTIC_INSTALLED +from fast_healthchecks.models import HealthCheckResult + +IMPORT_ERROR_MSG = "aiokafka is not installed. Install it with `pip install aiokafka`." + +try: + from aiokafka import AIOKafkaClient +except ImportError as exc: + raise ImportError(IMPORT_ERROR_MSG) from exc + + +if PYDANTIC_INSTALLED: + from pydantic import KafkaDsn +else: # pragma: no cover + KafkaDsn: TypeAlias = str # type: ignore[no-redef] + +SecurityProtocol: TypeAlias = Literal["SSL", "PLAINTEXT", "SASL_PLAINTEXT", "SASL_SSL"] +SaslMechanism: TypeAlias = Literal["PLAIN", "GSSAPI", "SCRAM-SHA-256", "SCRAM-SHA-512", "OAUTHBEARER"] + + +class KafkaHealthCheck(HealthCheck[HealthCheckResult]): + """A class to perform health checks on Kafka. + + Attributes: + _bootstrap_servers: The Kafka bootstrap servers. + _name: The name of the health check. + _sasl_mechanism: The SASL mechanism to use. + _sasl_plain_password: The SASL plain password. + _sasl_plain_username: The SASL plain username. + _security_protocol: The security protocol to use. + _ssl_context: The SSL context to use. + _timeout: The timeout for the health check. + """ + + __slots__ = ( + "_bootstrap_servers", + "_name", + "_sasl_mechanism", + "_sasl_plain_password", + "_sasl_plain_username", + "_security_protocol", + "_ssl_context", + "_timeout", + ) + + _bootstrap_servers: str + _ssl_context: ssl.SSLContext | None + _security_protocol: SecurityProtocol + _sasl_mechanism: SaslMechanism + _sasl_plain_username: str | None + _sasl_plain_password: str | None + _timeout: float + _name: str + + def __init__( # noqa: PLR0913 + self, + *, + bootstrap_servers: str, + ssl_context: ssl.SSLContext | None = None, + security_protocol: SecurityProtocol = "PLAINTEXT", + sasl_mechanism: SaslMechanism = "PLAIN", + sasl_plain_username: str | None = None, + sasl_plain_password: str | None = None, + timeout: float = DEFAULT_HC_TIMEOUT, + name: str = "Kafka", + ) -> None: + """Initializes the KafkaHealthCheck class. + + Args: + bootstrap_servers: The Kafka bootstrap servers. + ssl_context: The SSL context to use. + security_protocol: The security protocol to use. + sasl_mechanism: The SASL mechanism to use. + sasl_plain_username: The SASL plain username. + sasl_plain_password: The SASL plain password. + timeout: The timeout for the health check. + name: The name of the health check. + """ + self._bootstrap_servers = bootstrap_servers + self._ssl_context = ssl_context + if security_protocol not in {"SSL", "PLAINTEXT", "SASL_PLAINTEXT", "SASL_SSL"}: + msg = f"Invalid security protocol: {security_protocol}" + raise ValueError(msg) from None + self._security_protocol = security_protocol + if sasl_mechanism not in {"PLAIN", "GSSAPI", "SCRAM-SHA-256", "SCRAM-SHA-512", "OAUTHBEARER"}: + msg = f"Invalid SASL mechanism: {sasl_mechanism}" + raise ValueError(msg) from None + self._sasl_mechanism = sasl_mechanism + self._sasl_plain_username = sasl_plain_username + self._sasl_plain_password = sasl_plain_password + self._timeout = timeout + self._name = name + + async def __call__(self) -> HealthCheckResult: + """Performs the health check on Kafka. + + Returns: + A HealthCheckResult object. + """ + client = AIOKafkaClient( + bootstrap_servers=self._bootstrap_servers, + client_id="fast_healthchecks", + request_timeout_ms=int(self._timeout * 1000), + ssl_context=self._ssl_context, + security_protocol=self._security_protocol, + sasl_mechanism=self._sasl_mechanism, + sasl_plain_username=self._sasl_plain_username, + sasl_plain_password=self._sasl_plain_password, + ) + try: + await client.bootstrap() + await client.check_version() + return HealthCheckResult(name=self._name, healthy=True) + except BaseException: # noqa: BLE001 + return HealthCheckResult(name=self._name, healthy=False, error_details=format_exc()) + finally: + await client.close() diff --git a/fast_healthchecks/checks/mongo.py b/fast_healthchecks/checks/mongo.py new file mode 100644 index 0000000..b420a9e --- /dev/null +++ b/fast_healthchecks/checks/mongo.py @@ -0,0 +1,191 @@ +"""This module provides a health check class for MongoDB. + +Classes: + MongoHealthCheck: A class to perform health checks on MongoDB. + +Usage: + The MongoHealthCheck class can be used to perform health checks on MongoDB by calling it. + +Example: + health_check = MongoHealthCheck( + host="localhost", + port=27017 + ) + result = await health_check() + print(result.healthy) +""" + +import logging +from traceback import format_exc +from typing import TYPE_CHECKING, Any, TypeAlias, TypedDict +from urllib.parse import ParseResult, unquote, urlparse + +from fast_healthchecks.checks._base import DEFAULT_HC_TIMEOUT, HealthCheckDSN +from fast_healthchecks.compat import PYDANTIC_INSTALLED +from fast_healthchecks.models import HealthCheckResult + +IMPORT_ERROR_MSG = "motor is not installed. Install it with `pip install motor`." + +try: + from motor.motor_asyncio import AsyncIOMotorClient +except ImportError as exc: + raise ImportError(IMPORT_ERROR_MSG) from exc + +if TYPE_CHECKING: + from collections.abc import Mapping + + +if PYDANTIC_INSTALLED: + from pydantic import MongoDsn +else: # pragma: no cover + MongoDsn: TypeAlias = str # type: ignore[no-redef] + +logger = logging.getLogger(__name__) + + +class ParseDSNResult(TypedDict, total=True): + """A dictionary containing the results of parsing a DSN.""" + + parse_result: ParseResult + authSource: str + + +class MongoHealthCheck(HealthCheckDSN[HealthCheckResult]): + """A class to perform health checks on MongoDB. + + Attributes: + _auth_source: The MongoDB authentication source. + _database: The MongoDB database to use. + _host: The MongoDB host. + _name: The name of the health check. + _password: The MongoDB password. + _port: The MongoDB port. + _timeout: The timeout for the health check. + _user: The MongoDB user. + """ + + __slots__ = ( + "_auth_source", + "_database", + "_host", + "_name", + "_password", + "_port", + "_timeout", + "_user", + ) + + _host: str + _port: int + _user: str | None + _password: str | None + _database: str | None + _auth_source: str + _timeout: float + _name: str + + def __init__( # noqa: PLR0913 + self, + *, + host: str = "localhost", + port: int = 27017, + user: str | None = None, + password: str | None = None, + database: str | None = None, + auth_source: str = "admin", + timeout: float = DEFAULT_HC_TIMEOUT, + name: str = "MongoDB", + ) -> None: + """Initializes the MongoHealthCheck class. + + Args: + host: The MongoDB host. + port: The MongoDB port + user: The MongoDB user. + password: The MongoDB password. + database: The MongoDB database to use. + auth_source: The MongoDB authentication source. + timeout: The timeout for the health check. + name: The name of the health check. + """ + self._host = host + self._port = port + self._user = user + self._password = password + self._database = database + self._auth_source = auth_source + self._timeout = timeout + self._name = name + + @classmethod + def parse_dsn(cls, dsn: str) -> ParseDSNResult: + """Parse the DSN and return the results. + + Args: + dsn (str): The DSN to parse. + + Returns: + ParseDSNResult: The results of parsing the DSN. + """ + parse_result: ParseResult = urlparse(dsn) + query = ( + {k: unquote(v) for k, v in (q.split("=") for q in parse_result.query.split("&"))} + if parse_result.query + else {} + ) + return {"parse_result": parse_result, "authSource": query.get("authSource", "admin")} + + @classmethod + def from_dsn( + cls, + dsn: "MongoDsn | str", + *, + name: str = "MongoDB", + timeout: float = DEFAULT_HC_TIMEOUT, + ) -> "MongoHealthCheck": + """Creates a MongoHealthCheck instance from a DSN. + + Args: + dsn (MongoDsn | str): The DSN for the PostgreSQL database. + name (str): The name of the health check. + timeout (float): The timeout for the connection. + + Returns: + MongoHealthCheck: The health check instance. + """ + dsn = cls.validate_dsn(dsn, type_=MongoDsn) + parsed_dsn = cls.parse_dsn(dsn) + parse_result = parsed_dsn["parse_result"] + return cls( + host=parse_result.hostname or "localhost", + port=parse_result.port or 27017, + user=parse_result.username, + password=parse_result.password, + database=parse_result.path.lstrip("/") or None, + auth_source=parsed_dsn["authSource"], + timeout=timeout, + name=name, + ) + + async def __call__(self) -> HealthCheckResult: + """Performs the health check on MongoDB. + + Returns: + A HealthCheckResult object. + """ + client: AsyncIOMotorClient[Mapping[str, Any]] = AsyncIOMotorClient( + host=self._host, + port=self._port, + username=self._user, + password=self._password, + authSource=self._auth_source, + serverSelectionTimeoutMS=int(self._timeout * 1000), + ) + database = client[self._database] if self._database else client[self._auth_source] + try: + res = await database.command("ping") + return HealthCheckResult(name=self._name, healthy=res == {"ok": 1.0}) + except BaseException: # noqa: BLE001 + return HealthCheckResult(name=self._name, healthy=False, error_details=format_exc()) + finally: + client.close() diff --git a/fast_healthchecks/checks/postgresql/__init__.py b/fast_healthchecks/checks/postgresql/__init__.py new file mode 100644 index 0000000..20c2f80 --- /dev/null +++ b/fast_healthchecks/checks/postgresql/__init__.py @@ -0,0 +1 @@ +"""Module for PostgreSQL health checks.""" diff --git a/fast_healthchecks/checks/postgresql/asyncpg.py b/fast_healthchecks/checks/postgresql/asyncpg.py new file mode 100644 index 0000000..cc1dd75 --- /dev/null +++ b/fast_healthchecks/checks/postgresql/asyncpg.py @@ -0,0 +1,183 @@ +"""This module provides a health check class for PostgreSQL using asyncpg. + +Classes: + PostgreSQLAsyncPGHealthCheck: A class to perform health checks on a PostgreSQL database using asyncpg. + +Usage: + The PostgreSQLAsyncPGHealthCheck class can be used to perform health checks on a PostgreSQL database by + connecting to the database and executing a simple query. + +Example: + health_check = PostgreSQLAsyncPGHealthCheck( + host="localhost", + port=5432, + user="username", + password="password", + database="dbname" + ) + # or + health_check = PostgreSQLAsyncPGHealthCheck.from_dsn( + "postgresql://username:password@localhost:5432/dbname", + ) + result = await health_check() + print(result.healthy) +""" + +import ssl +from traceback import format_exc +from typing import TYPE_CHECKING, TypeAlias + +from fast_healthchecks.checks._base import DEFAULT_HC_TIMEOUT +from fast_healthchecks.checks.postgresql.base import BasePostgreSQLHealthCheck +from fast_healthchecks.compat import PYDANTIC_INSTALLED +from fast_healthchecks.models import HealthCheckResult + +IMPORT_ERROR_MSG = "asyncpg is not installed. Install it with `pip install asyncpg`." + +try: + import asyncpg +except ImportError as exc: + raise ImportError(IMPORT_ERROR_MSG) from exc + +if TYPE_CHECKING: + from asyncpg.connection import Connection + + +if PYDANTIC_INSTALLED: + from pydantic import PostgresDsn +else: # pragma: no cover + PostgresDsn: TypeAlias = str # type: ignore[no-redef] + + +class PostgreSQLAsyncPGHealthCheck(BasePostgreSQLHealthCheck[HealthCheckResult]): + """Health check class for PostgreSQL using asyncpg. + + Attributes: + _name (str): The name of the health check. + _host (str): The hostname of the PostgreSQL server. + _port (int): The port number of the PostgreSQL server. + _user (str | None): The username for authentication. + _password (str | None): The password for authentication. + _database (str | None): The database name. + _ssl (ssl.SSLContext | None): The SSL context for secure connections. + _direct_tls (bool): Whether to use direct TLS. + _timeout (float): The timeout for the connection. + """ + + __slots__ = ( + "_database", + "_direct_tls", + "_host", + "_name", + "_password", + "_port", + "_ssl", + "_timeout", + "_user", + ) + + _host: str + _port: int + _user: str | None + _password: str | None + _database: str | None + _ssl: ssl.SSLContext | None + _direct_tls: bool + _timeout: float + _name: str + + def __init__( # noqa: PLR0913 + self, + *, + host: str, + port: int, + user: str | None = None, + password: str | None = None, + database: str | None = None, + ssl: ssl.SSLContext | None = None, + direct_tls: bool = False, + timeout: float = DEFAULT_HC_TIMEOUT, + name: str = "PostgreSQL", + ) -> None: + """Initializes the PostgreSQLAsyncPGHealthCheck instance. + + Args: + host (str): The hostname of the PostgreSQL server. + port (int): The port number of the PostgreSQL server. + user (str | None): The username for authentication. + password (str | None): The password for authentication. + database (str | None): The database name. + timeout (float): The timeout for the connection. + ssl (ssl.SSLContext | None): The SSL context for secure connections. + direct_tls (bool): Whether to use direct TLS. + name (str): The name of the health check. + """ + self._host = host + self._port = port + self._user = user + self._password = password + self._database = database + self._ssl = ssl + self._direct_tls = direct_tls + self._timeout = timeout + self._name = name + + @classmethod + def from_dsn( + cls, + dsn: "PostgresDsn | str", + *, + name: str = "PostgreSQL", + timeout: float = DEFAULT_HC_TIMEOUT, + ) -> "PostgreSQLAsyncPGHealthCheck": + """Creates a PostgreSQLAsyncPGHealthCheck instance from a DSN. + + Args: + dsn (PostgresDsn | str): The DSN for the PostgreSQL database. + name (str): The name of the health check. + timeout (float): The timeout for the connection. + + Returns: + PostgreSQLAsyncPGHealthCheck: The health check instance. + """ + dsn = cls.validate_dsn(dsn, type_=PostgresDsn) + parsed_dsn = cls.parse_dsn(dsn) + parse_result = parsed_dsn["parse_result"] + sslctx = parsed_dsn["sslctx"] + return cls( + host=parse_result.hostname or "localhost", + port=parse_result.port or 5432, + user=parse_result.username, + password=parse_result.password, + database=parse_result.path.lstrip("/"), + ssl=sslctx, + timeout=timeout, + name=name, + ) + + async def __call__(self) -> HealthCheckResult: + """Performs the health check. + + Returns: + HealthCheckResult: The result of the health check. + """ + connection: Connection | None = None + try: + connection = await asyncpg.connect( + host=self._host, + port=self._port, + user=self._user, + password=self._password, + database=self._database, + timeout=self._timeout, + ssl=self._ssl, + direct_tls=self._direct_tls, + ) + async with connection.transaction(readonly=True): + healthy: bool = bool(await connection.fetchval("SELECT 1")) + return HealthCheckResult(name=self._name, healthy=healthy) + except BaseException: # noqa: BLE001 + return HealthCheckResult(name=self._name, healthy=False, error_details=format_exc()) + finally: + if connection is not None and not connection.is_closed(): + await connection.close(timeout=self._timeout) diff --git a/fast_healthchecks/checks/postgresql/base.py b/fast_healthchecks/checks/postgresql/base.py new file mode 100644 index 0000000..4687fb9 --- /dev/null +++ b/fast_healthchecks/checks/postgresql/base.py @@ -0,0 +1,144 @@ +"""This module contains the base class for PostgreSQL health checks.""" + +import ssl +from functools import lru_cache +from typing import Generic, Literal, TypeAlias, TypedDict, cast +from urllib.parse import ParseResult, unquote, urlparse + +from fast_healthchecks.checks._base import HealthCheckDSN, T_co + +SslMode: TypeAlias = Literal["disable", "allow", "prefer", "require", "verify-ca", "verify-full"] + + +class ParseDSNResult(TypedDict, total=True): + """A dictionary containing the results of parsing a DSN.""" + + parse_result: ParseResult + sslmode: SslMode + sslcert: str | None + sslkey: str | None + sslrootcert: str | None + sslctx: ssl.SSLContext | None + + +@lru_cache +def create_ssl_context( + sslmode: SslMode, + sslcert: str | None, + sslkey: str | None, + sslrootcert: str | None, +) -> ssl.SSLContext | None: + """Create an SSL context from the query parameters. + + Args: + sslmode (SslMode): The SSL mode to use. + sslcert (str | None): The path to the SSL certificate. + sslkey (str | None): The path to the SSL key. + sslrootcert (str | None): The path to the SSL root certificate. + + Returns: + ssl.SSLContext | None: The SSL context. + """ + sslctx: ssl.SSLContext | None = None + match sslmode: + case "disable": + sslctx = None + case "allow": + sslctx = None + case "prefer": + sslctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) + case "require": + sslctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) + sslctx.check_hostname = False + sslctx.verify_mode = ssl.CERT_NONE + case "verify-ca": + sslctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=sslrootcert) + sslctx.check_hostname = False + sslctx.verify_mode = ssl.CERT_REQUIRED + case "verify-full": + if sslcert is None: + msg = "sslcert is required for verify-full" + raise ValueError(msg) from None + sslctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=sslrootcert) + sslctx.check_hostname = True + sslctx.load_cert_chain( + certfile=sslcert, + keyfile=sslkey, + ) + case _: # pragma: no cover + msg = f"Unsupported sslmode: {sslmode}" + raise ValueError(msg) from None + return sslctx + + +class BasePostgreSQLHealthCheck(HealthCheckDSN[T_co], Generic[T_co]): + """Base class for PostgreSQL health checks.""" + + @classmethod + def create_ssl_context_from_query( + cls, + sslmode: SslMode, + sslcert: str | None, + sslkey: str | None, + sslrootcert: str | None, + ) -> ssl.SSLContext | None: + """Create an SSL context from the query parameters. + + Args: + sslmode (SslMode): The SSL mode to use. + sslcert (str | None): The path to the SSL certificate. + sslkey (str | None): The path to the SSL key. + sslrootcert (str | None): The path to the SSL root certificate. + + Returns: + ssl.SSLContext | None: The SSL context. + """ + return create_ssl_context(sslmode, sslcert, sslkey, sslrootcert) + + @classmethod + def validate_sslmode(cls, mode: str) -> SslMode: + """Validate the SSL mode. + + Args: + mode (str): The SSL mode to validate. + + Returns: + SslMode: The validated SSL mode. + + Raises: + ValueError: If the SSL mode is invalid. + """ + if mode not in {"disable", "allow", "prefer", "require", "verify-ca", "verify-full"}: + msg = f"Invalid sslmode: {mode}" + raise ValueError(msg) from None + return cast("SslMode", mode) + + @classmethod + def parse_dsn(cls, dsn: str) -> ParseDSNResult: + """Parse the DSN and return the results. + + Args: + dsn (str): The DSN to parse. + + Returns: + ParseDSNResult: The results of parsing the DSN. + """ + parse_result: ParseResult = urlparse(dsn) + query = ( + {k: unquote(v) for k, v in (q.split("=") for q in parse_result.query.split("&"))} + if parse_result.query + else {} + ) + sslmode: SslMode = cls.validate_sslmode(query.get("sslmode", "disable")) + sslcert: str | None = query.get("sslcert") + sslkey: str | None = query.get("sslkey") + sslrootcert: str | None = query.get("sslrootcert") + sslctx: ssl.SSLContext | None = cls.create_ssl_context_from_query(sslmode, sslcert, sslkey, sslrootcert) + return { + "parse_result": parse_result, + "sslmode": sslmode, + "sslcert": sslcert, + "sslkey": sslkey, + "sslrootcert": sslrootcert, + "sslctx": sslctx, + } diff --git a/fast_healthchecks/checks/postgresql/psycopg.py b/fast_healthchecks/checks/postgresql/psycopg.py new file mode 100644 index 0000000..d2839bf --- /dev/null +++ b/fast_healthchecks/checks/postgresql/psycopg.py @@ -0,0 +1,199 @@ +"""The module contains a health check for PostgreSQL using psycopg. + +Classes: + PostgreSQLPsycopgHealthCheck: A class for health checking PostgreSQL using psycopg. + +Usage: + The PostgreSQLPsycopgHealthCheck class can be used to perform health checks on a PostgreSQL database by + connecting to the database and executing a simple query. + +Example: + health_check = PostgreSQLPsycopgHealthCheck( + host="localhost", + port=5432, + user="username", + password="password", + database="dbname" + ) + # or + health_check = PostgreSQLPsycopgHealthCheck.from_dsn( + "postgresql://username:password@localhost:5432/dbname", + ) + result = await health_check() + print(result.healthy) +""" + +from traceback import format_exc +from typing import TYPE_CHECKING, TypeAlias + +from fast_healthchecks.checks._base import DEFAULT_HC_TIMEOUT +from fast_healthchecks.checks.postgresql.base import BasePostgreSQLHealthCheck, SslMode +from fast_healthchecks.compat import PYDANTIC_INSTALLED +from fast_healthchecks.models import HealthCheckResult + +IMPORT_ERROR_MSG = "psycopg is not installed. Install it with `pip install psycopg`." + +try: + import psycopg +except ImportError as exc: + raise ImportError(IMPORT_ERROR_MSG) from exc + +if TYPE_CHECKING: + from psycopg import AsyncConnection + + +if PYDANTIC_INSTALLED: + from pydantic import PostgresDsn +else: # pragma: no cover + PostgresDsn: TypeAlias = str # type: ignore[no-redef] + + +class PostgreSQLPsycopgHealthCheck(BasePostgreSQLHealthCheck[HealthCheckResult]): + """Health check class for PostgreSQL using psycopg. + + Attributes: + _name (str): The name of the health check. + _host (str): The hostname of the PostgreSQL server. + _port (int): The port number of the PostgreSQL server. + _user (str | None): The username for authentication. + _password (str | None): The password for authentication. + _database (str | None): The database name. + _sslmode (SslMode | None): The SSL mode to use for the connection. + _sslcert (str | None): The path to the SSL certificate file. + _sslkey (str | None): The path to the SSL key file. + _sslrootcert (str | None): The path to the SSL root certificate file. + _timeout (float): The timeout for the health check. + """ + + __slots__ = ( + "_database", + "_host", + "_name", + "_password", + "_port", + "_sslcert", + "_sslkey", + "_sslmode", + "_sslrootcert", + "_timeout", + "_user", + ) + + _host: str + _port: int + _user: str | None + _password: str | None + _database: str | None + _sslmode: SslMode | None + _sslcert: str | None + _sslkey: str | None + _sslrootcert: str | None + _timeout: float + _name: str + + def __init__( # noqa: PLR0913 + self, + *, + host: str, + port: int, + user: str | None = None, + password: str | None = None, + database: str | None = None, + sslmode: SslMode | None = None, + sslcert: str | None = None, + sslkey: str | None = None, + sslrootcert: str | None = None, + timeout: float = DEFAULT_HC_TIMEOUT, + name: str = "PostgreSQL", + ) -> None: + """Initializes the PostgreSQLPsycopgHealthCheck instance. + + Args: + host (str): The hostname of the PostgreSQL server. + port (int): The port number of the PostgreSQL server. + user (str | None): The username for authentication. + password (str | None): The password for authentication. + database (str | None): The database name. + timeout (float): The timeout for the health check. + sslmode (SslMode | None): The SSL mode to use for the connection. + sslcert (str | None): The path to the SSL certificate file. + sslkey (str | None): The path to the SSL key file. + sslrootcert (str | None): The path to the SSL root certificate file. + name (str): The name of the health check. + """ + self._host = host + self._port = port + self._user = user + self._password = password + self._database = database + self._sslmode = sslmode + self._sslcert = sslcert + self._sslkey = sslkey + self._sslrootcert = sslrootcert + self._timeout = timeout + self._name = name + + @classmethod + def from_dsn( + cls, + dsn: "PostgresDsn | str", + *, + name: str = "PostgreSQL", + timeout: float = DEFAULT_HC_TIMEOUT, + ) -> "PostgreSQLPsycopgHealthCheck": + """Creates a PostgreSQLPsycopgHealthCheck instance from a DSN. + + Args: + dsn (PostgresDsn | str): The DSN for the PostgreSQL connection. + name (str): The name of the health check. + timeout (float): The timeout for the health check. + + Returns: + PostgreSQLPsycopgHealthCheck: The health check instance. + """ + dsn = cls.validate_dsn(dsn, type_=PostgresDsn) + parsed_dsn = cls.parse_dsn(dsn) + parse_result = parsed_dsn["parse_result"] + return cls( + host=parse_result.hostname or "localhost", + port=parse_result.port or 5432, + user=parse_result.username, + password=parse_result.password, + database=parse_result.path.lstrip("/"), + sslmode=parsed_dsn["sslmode"], + sslcert=parsed_dsn["sslcert"], + sslkey=parsed_dsn["sslkey"], + sslrootcert=parsed_dsn["sslrootcert"], + timeout=timeout, + name=name, + ) + + async def __call__(self) -> HealthCheckResult: + """Performs the health check. + + Returns: + HealthCheckResult: The result of the health check. + """ + connection: AsyncConnection | None = None + try: + connection = await psycopg.AsyncConnection.connect( + host=self._host, + port=self._port, + user=self._user, + password=self._password, + dbname=self._database, + sslmode=self._sslmode, + sslcert=self._sslcert, + sslkey=self._sslkey, + sslrootcert=self._sslrootcert, + ) + async with connection.cursor() as cursor: + await cursor.execute("SELECT 1") + healthy: bool = bool(await cursor.fetchone()) + return HealthCheckResult(name=self._name, healthy=healthy) + except BaseException: # noqa: BLE001 + return HealthCheckResult(name=self._name, healthy=False, error_details=format_exc()) + finally: + if connection is not None and not connection.closed: + await connection.cancel_safe(timeout=self._timeout) + await connection.close() diff --git a/fast_healthchecks/checks/rabbitmq.py b/fast_healthchecks/checks/rabbitmq.py new file mode 100644 index 0000000..fb673d6 --- /dev/null +++ b/fast_healthchecks/checks/rabbitmq.py @@ -0,0 +1,168 @@ +"""This module provides a health check class for RabbitMQ. + +Classes: + RabbitMQHealthCheck: A class to perform health checks on RabbitMQ. + +Usage: + The RabbitMQHealthCheck class can be used to perform health checks on RabbitMQ by calling it. + +Example: + health_check = RabbitMQHealthCheck( + host="localhost", + port=5672, + username="guest", + password="guest", + ) + result = await health_check() + print(result.healthy) +""" + +from traceback import format_exc +from typing import TypeAlias, TypedDict +from urllib.parse import ParseResult, urlparse + +from fast_healthchecks.checks._base import DEFAULT_HC_TIMEOUT, HealthCheckDSN +from fast_healthchecks.compat import PYDANTIC_INSTALLED +from fast_healthchecks.models import HealthCheckResult + +IMPORT_ERROR_MSG = "aio-pika is not installed. Install it with `pip install aio-pika`." + +try: + import aio_pika +except ImportError as exc: + raise ImportError(IMPORT_ERROR_MSG) from exc + +if PYDANTIC_INSTALLED: + from pydantic import AmqpDsn +else: # pragma: no cover + AmqpDsn: TypeAlias = str # type: ignore[no-redef] + + +class ParseDSNResult(TypedDict, total=True): + """A dictionary containing the results of parsing a DSN.""" + + parse_result: ParseResult + + +class RabbitMQHealthCheck(HealthCheckDSN[HealthCheckResult]): + """A class to perform health checks on RabbitMQ. + + Attributes: + _host: The RabbitMQ host. + _name: The name of the health check. + _password: The RabbitMQ password. + _port: The RabbitMQ port. + _secure: Whether to use a secure connection. + _timeout: The timeout for the health check. + _user: The RabbitMQ user. + _vhost: The RabbitMQ virtual host. + """ + + __slots__ = ("_host", "_name", "_password", "_port", "_secure", "_timeout", "_user") + + _host: str + _port: int + _secure: bool + _user: str + _vhost: str + _password: str + _timeout: float + _name: str + + def __init__( # noqa: PLR0913 + self, + *, + host: str, + user: str, + password: str, + port: int = 5672, + vhost: str = "/", + secure: bool = False, + timeout: float = DEFAULT_HC_TIMEOUT, + name: str = "RabbitMQ", + ) -> None: + """Initializes the RabbitMQHealthCheck class. + + Args: + host: The RabbitMQ host + user: The RabbitMQ user + password: The RabbitMQ password + port: The RabbitMQ port + vhost: The RabbitMQ virtual host + secure: Whether to use a secure connection + timeout: The timeout for the health check + name: The name of the health check + """ + self._host = host + self._user = user + self._password = password + self._port = port + self._vhost = vhost + self._secure = secure + self._timeout = timeout + self._name = name + + @classmethod + def parse_dsn(cls, dsn: str) -> ParseDSNResult: + """Parse the DSN and return the results. + + Args: + dsn (str): The DSN to parse. + + Returns: + ParseDSNResult: The results of parsing the DSN. + """ + parse_result: ParseResult = urlparse(dsn) + return {"parse_result": parse_result} + + @classmethod + def from_dsn( + cls, + dsn: "AmqpDsn | str", + *, + name: str = "RabbitMQ", + timeout: float = DEFAULT_HC_TIMEOUT, + ) -> "RabbitMQHealthCheck": + """Creates a RabbitMQHealthCheck instance from a DSN. + + Args: + dsn: The DSN to create the RabbitMQHealthCheck instance from. + name: The name of the health check. + timeout: The timeout for the health check. + + Returns: + A RabbitMQHealthCheck instance. + """ + dsn = cls.validate_dsn(dsn, type_=AmqpDsn) + parsed_dsn = cls.parse_dsn(dsn) + parse_result = parsed_dsn["parse_result"] + return RabbitMQHealthCheck( + host=parse_result.hostname or "localhost", + user=parse_result.username or "guest", + password=parse_result.password or "guest", + port=parse_result.port or 5672, + vhost=parse_result.path.lstrip("/") or "/", + secure=parse_result.scheme == "amqps", + timeout=timeout, + name=name, + ) + + async def __call__(self) -> HealthCheckResult: + """Performs the health check on RabbitMQ. + + Returns: + A HealthCheckResult object. + """ + try: + async with await aio_pika.connect_robust( + host=self._host, + port=self._port, + login=self._user, + password=self._password, + ssl=self._secure, + virtualhost=self._vhost, + timeout=self._timeout, + ): + return HealthCheckResult(name=self._name, healthy=True) + except BaseException: # noqa: BLE001 + return HealthCheckResult(name=self._name, healthy=False, error_details=format_exc()) diff --git a/fast_healthchecks/checks/redis.py b/fast_healthchecks/checks/redis.py new file mode 100644 index 0000000..5b28780 --- /dev/null +++ b/fast_healthchecks/checks/redis.py @@ -0,0 +1,173 @@ +"""This module provides a health check class for Redis. + +Classes: + RedisHealthCheck: A class to perform health checks on Redis. + +Usage: + The RedisHealthCheck class can be used to perform health checks on Redis by calling it. + +Example: + health_check = RedisHealthCheck( + host="localhost", + port=6379, + ) + result = await health_check() + print(result.healthy) +""" + +from traceback import format_exc +from typing import TYPE_CHECKING, TypeAlias, TypedDict + +from fast_healthchecks.checks._base import DEFAULT_HC_TIMEOUT, HealthCheckDSN +from fast_healthchecks.compat import PYDANTIC_INSTALLED +from fast_healthchecks.models import HealthCheckResult + +IMPORT_ERROR_MSG = "redis is not installed. Install it with `pip install redis`." + +try: + from redis.asyncio import Redis + from redis.asyncio.connection import parse_url +except ImportError as exc: + raise ImportError(IMPORT_ERROR_MSG) from exc + +if TYPE_CHECKING: + from redis.asyncio.connection import ConnectKwargs + + +if PYDANTIC_INSTALLED: + from pydantic import RedisDsn +else: # pragma: no cover + RedisDsn: TypeAlias = str # type: ignore[no-redef] + + +class ParseDSNResult(TypedDict, total=True): + """A dictionary containing the results of parsing a DSN.""" + + parse_result: "ConnectKwargs" + + +class RedisHealthCheck(HealthCheckDSN[HealthCheckResult]): + """A class to perform health checks on Redis. + + Attributes: + _database: The database to connect to. + _host: The host to connect to. + _name: The name of the health check. + _password: The password to authenticate with. + _port: The port to connect to. + _timeout: The timeout for the connection. + _user: The user to authenticate with. + """ + + __slots__ = ( + "_database", + "_host", + "_name", + "_password", + "_port", + "_timeout", + "_user", + ) + + _host: str + _port: int + _database: str | int + _user: str | None + _password: str | None + _timeout: float | None + _name: str + + def __init__( # noqa: PLR0913 + self, + *, + host: str = "localhost", + port: int = 6379, + database: str | int = 0, + user: str | None = None, + password: str | None = None, + timeout: float | None = DEFAULT_HC_TIMEOUT, + name: str = "Redis", + ) -> None: + """Initialize the RedisHealthCheck class. + + Args: + host: The host to connect to. + port: The port to connect to. + database: The database to connect to. + user: The user to authenticate with. + password: The password to authenticate with. + timeout: The timeout for the connection. + name: The name of the health check. + """ + self._host = host + self._port = port + self._database = database + self._user = user + self._password = password + self._timeout = timeout + self._name = name + + @classmethod + def parse_dsn(cls, dsn: str) -> ParseDSNResult: + """Parse the DSN and return the results. + + Args: + dsn (str): The DSN to parse. + + Returns: + ParseDSNResult: The results of parsing the DSN. + """ + parse_result: "ConnectKwargs" = parse_url(str(dsn)) # noqa: UP037 + return {"parse_result": parse_result} + + @classmethod + def from_dsn( + cls, + dsn: "RedisDsn | str", + *, + name: str = "Redis", + timeout: float = DEFAULT_HC_TIMEOUT, + ) -> "RedisHealthCheck": + """Create a RedisHealthCheck instance from a DSN. + + Args: + dsn: The DSN to connect to. + name: The name of the health check. + timeout: The timeout for the connection. + + Returns: + A RedisHealthCheck instance. + """ + dsn = cls.validate_dsn(dsn, type_=RedisDsn) + parsed_dsn = cls.parse_dsn(dsn) + parse_result = parsed_dsn["parse_result"] + return RedisHealthCheck( + host=parse_result.get("host", "localhost"), + port=parse_result.get("port", 6379), + database=parse_result.get("db", 0), + user=parse_result.get("username"), + password=parse_result.get("password"), + timeout=timeout, + name=name, + ) + + async def __call__(self) -> HealthCheckResult: + """Perform a health check on Redis. + + Returns: + A HealthCheckResult instance. + """ + try: + async with Redis( + host=self._host, + port=self._port, + db=self._database, + username=self._user, + password=self._password, + socket_timeout=self._timeout, + single_connection_client=True, + ) as redis: + healthy: bool = await redis.ping() + return HealthCheckResult(name=self._name, healthy=healthy) + except BaseException: # noqa: BLE001 + return HealthCheckResult(name=self._name, healthy=False, error_details=format_exc()) diff --git a/fast_healthchecks/checks/url.py b/fast_healthchecks/checks/url.py new file mode 100644 index 0000000..76c69d8 --- /dev/null +++ b/fast_healthchecks/checks/url.py @@ -0,0 +1,120 @@ +"""This module provides a health check class for URLs. + +Classes: + UrlHealthCheck: A class to perform health checks on URLs. + +Usage: + The UrlHealthCheck class can be used to perform health checks on URLs by calling it. + +Example: + health_check = UrlHealthCheck( + url="https://www.google.com", + ) + result = await health_check() + print(result.healthy) +""" + +from http import HTTPStatus +from traceback import format_exc +from typing import TYPE_CHECKING + +from fast_healthchecks.checks._base import DEFAULT_HC_TIMEOUT, HealthCheck +from fast_healthchecks.models import HealthCheckResult + +IMPORT_ERROR_MSG = "httpx is not installed. Install it with `pip install httpx`." + +try: + from httpx import AsyncClient, AsyncHTTPTransport, BasicAuth, Response +except ImportError as exc: + raise ImportError(IMPORT_ERROR_MSG) from exc + +if TYPE_CHECKING: + from httpx._types import URLTypes + + +class UrlHealthCheck(HealthCheck[HealthCheckResult]): + """A class to perform health checks on URLs. + + Attributes: + _name: The name of the health check. + _password: The password to authenticate with. + _timeout: The timeout for the connection. + _url: The URL to connect to. + _username: The user to authenticate with. + _verify_ssl: Whether to verify the SSL certificate. + """ + + __slots__ = ( + "_auth", + "_follow_redirects", + "_name", + "_password", + "_timeout", + "_transport", + "_url", + "_username", + "_verify_ssl", + ) + + _url: "URLTypes" + _username: str | None + _password: str | None + _auth: BasicAuth | None + _verify_ssl: bool + _transport: AsyncHTTPTransport | None + _follow_redirects: bool + _timeout: float + _name: str + + def __init__( # noqa: PLR0913, D417 + self, + *, + url: "URLTypes", + username: str | None = None, + password: str | None = None, + verify_ssl: bool = True, + follow_redirects: bool = True, + timeout: float = DEFAULT_HC_TIMEOUT, + name: str = "HTTP", + ) -> None: + """Initializes the health check. + + Args: + url: The URL to connect to. + username: The user to authenticate with. + password: The password to authenticate with. + verify_ssl: Whether to verify the SSL certificate. + timeout: The timeout for the connection. + name: The name of the health check. + """ + self._url = url + self._username = username + self._password = password + self._auth = BasicAuth(self._username, self._password or "") if self._username else None + self._verify_ssl = verify_ssl + self._transport = AsyncHTTPTransport(verify=self._verify_ssl) if self._verify_ssl else None + self._follow_redirects = follow_redirects + self._timeout = timeout + self._name = name + + async def __call__(self) -> HealthCheckResult: + """Performs the health check. + + Returns: + A HealthCheckResult object with the result of the health check. + """ + try: + async with AsyncClient( + auth=self._auth, + timeout=self._timeout, + transport=self._transport, + follow_redirects=self._follow_redirects, + ) as client: + response: Response = await client.get(self._url) + if response.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR or ( + self._username and response.status_code in {HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN} + ): + response.raise_for_status() + return HealthCheckResult(name=self._name, healthy=response.is_success) + except BaseException: # noqa: BLE001 + return HealthCheckResult(name=self._name, healthy=False, error_details=format_exc()) diff --git a/fast_healthchecks/compat.py b/fast_healthchecks/compat.py new file mode 100644 index 0000000..c00f663 --- /dev/null +++ b/fast_healthchecks/compat.py @@ -0,0 +1,21 @@ +"""Module to check compatibility with Pydantic.""" + +PYDANTIC_INSTALLED: bool +PYDANTIC_VERSION: str | None +PYDANTIC_V2: bool + +try: + from pydantic.version import VERSION as PYDANTIC_VERSION + + PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.") # type: ignore[union-attr] + PYDANTIC_INSTALLED = True +except ImportError: + PYDANTIC_INSTALLED = False + PYDANTIC_VERSION = None + PYDANTIC_V2 = False + +__all__ = ( + "PYDANTIC_INSTALLED", + "PYDANTIC_V2", + "PYDANTIC_VERSION", +) diff --git a/fast_healthchecks/integrations/__init__.py b/fast_healthchecks/integrations/__init__.py new file mode 100644 index 0000000..11a5ff2 --- /dev/null +++ b/fast_healthchecks/integrations/__init__.py @@ -0,0 +1 @@ +"""Module for integrations with external frameworks.""" diff --git a/fast_healthchecks/integrations/base.py b/fast_healthchecks/integrations/base.py new file mode 100644 index 0000000..c0e74fb --- /dev/null +++ b/fast_healthchecks/integrations/base.py @@ -0,0 +1,200 @@ +"""Base classes for integrations.""" + +import asyncio +import json +import re +from collections.abc import Awaitable, Callable, Iterable +from dataclasses import asdict +from http import HTTPStatus +from typing import Any, NamedTuple, TypeAlias + +from fast_healthchecks.checks import Check +from fast_healthchecks.models import HealthcheckReport, HealthCheckResult + +HandlerType: TypeAlias = Callable[["ProbeAsgiResponse"], Awaitable[dict[str, str]]] + + +class Probe(NamedTuple): + """A probe is a collection of health checks that can be run together. + + Args: + name: The name of the probe. + checks: An iterable of health checks to run. + summary: A summary of the probe. If not provided, a default summary will be generated. + """ + + name: str + checks: Iterable[Check] + summary: str | None = None + + @property + def endpoint_summary(self) -> str: + """Return a summary for the endpoint. + + If a summary is provided, it will be used. Otherwise, a default summary will be generated. + """ + if self.summary: + return self.summary + title = re.sub( + pattern=r"[^a-z0-9]+", + repl=" ", + string=self.name.lower().capitalize(), + flags=re.IGNORECASE, + ) + return f"{title} probe" + + +class ProbeAsgiResponse(NamedTuple): + """A response from an ASGI probe. + + Args: + body: The body of the response. + status_code: The status code of the response. + """ + + data: dict[str, str] + healthy: bool + + +async def default_handler(response: ProbeAsgiResponse) -> Any: # noqa: ANN401 + """Default handler for health check route. + + Args: + response: The response from the probe. + + Returns: + The response data. + """ + + +class ProbeAsgi: + """An ASGI probe. + + Args: + probe: The probe to run. + success_handler: The handler to use for successful responses. + failure_handler: The handler to use for failed responses. + success_status: The status code to use for successful responses. + failure_status: The status code to use for failed responses. + debug: Whether to include debug information in the response. + """ + + __slots__ = ( + "_debug", + "_exclude_fields", + "_failure_handler", + "_failure_status", + "_map_handler", + "_map_status", + "_probe", + "_success_handler", + "_success_status", + ) + + _probe: Probe + _success_handler: HandlerType + _failure_handler: HandlerType + _success_status: int + _failure_status: int + _debug: bool + _exclude_fields: set[str] + _map_status: dict[bool, int] + _map_handler: dict[bool, HandlerType] + + def __init__( # noqa: PLR0913 + self, + probe: Probe, + *, + success_handler: HandlerType = default_handler, + failure_handler: HandlerType = default_handler, + success_status: int = HTTPStatus.NO_CONTENT, + failure_status: int = HTTPStatus.SERVICE_UNAVAILABLE, + debug: bool = False, + ) -> None: + """Initialize the ASGI probe.""" + self._probe = probe + self._success_handler = success_handler + self._failure_handler = failure_handler + self._success_status = success_status + self._failure_status = failure_status + self._debug = debug + self._exclude_fields: set[str] = {"allow_partial_failure", "error_details"} if not debug else set() + self._map_status: dict[bool, int] = {True: success_status, False: failure_status} + self._map_handler: dict[bool, HandlerType] = {True: success_handler, False: failure_handler} + + async def __call__(self) -> tuple[bytes, dict[str, str] | None, int]: + """Run the probe. + + Returns: + A tuple containing the response body, headers, and status code. + """ + tasks = [check() for check in self._probe.checks] + results: list[HealthCheckResult] = await asyncio.gather(*tasks) + report = HealthcheckReport(results=results) + response = ProbeAsgiResponse( + data=asdict( + report, + dict_factory=lambda x: {k: v for (k, v) in x if k not in self._exclude_fields}, + ), + healthy=report.healthy, + ) + + content_needed = not ( + (response.healthy and self._success_status < HTTPStatus.OK) + or self._success_status + in { + HTTPStatus.NO_CONTENT, + HTTPStatus.NOT_MODIFIED, + } + ) + + content = b"" + headers = None + if content_needed: + handler = self._map_handler[response.healthy] + content_ = await handler(response) + content = json.dumps( + content_, + ensure_ascii=False, + allow_nan=False, + indent=None, + separators=(",", ":"), + ).encode("utf-8") + headers = { + "content-type": "application/json", + "content-length": str(len(content)), + } + + return content, headers, self._map_status[response.healthy] + + +def make_probe_asgi( # noqa: PLR0913 + probe: Probe, + *, + success_handler: HandlerType = default_handler, + failure_handler: HandlerType = default_handler, + success_status: int = HTTPStatus.NO_CONTENT, + failure_status: int = HTTPStatus.SERVICE_UNAVAILABLE, + debug: bool = False, +) -> Callable[[], Awaitable[Any]]: + """Create an ASGI probe from a probe. + + Args: + probe: The probe to create the ASGI probe from. + success_handler: The handler to use for successful responses. + failure_handler: The handler to use for failed responses. + success_status: The status code to use for successful responses. + failure_status: The status code to use for failed responses. + debug: Whether to include debug information in the response. + + Returns: + An ASGI probe. + """ + return ProbeAsgi( + probe, + success_handler=success_handler, + failure_handler=failure_handler, + success_status=success_status, + failure_status=failure_status, + debug=debug, + ) diff --git a/fast_healthchecks/integrations/fastapi.py b/fast_healthchecks/integrations/fastapi.py new file mode 100644 index 0000000..2795c5c --- /dev/null +++ b/fast_healthchecks/integrations/fastapi.py @@ -0,0 +1,73 @@ +"""FastAPI integration for health checks.""" + +from typing import Any + +from fastapi import APIRouter, status +from fastapi.responses import Response + +from fast_healthchecks.integrations.base import HandlerType, Probe, default_handler, make_probe_asgi + + +class HealthcheckRouter(APIRouter): + """A router for health checks. + + Args: + probes: An iterable of probes to run. + debug: Whether to include the probes in the schema. Defaults to False. + """ + + def __init__( # noqa: PLR0913 + self, + *probes: Probe, + success_handler: HandlerType = default_handler, + failure_handler: HandlerType = default_handler, + success_status: int = status.HTTP_204_NO_CONTENT, + failure_status: int = status.HTTP_503_SERVICE_UNAVAILABLE, + debug: bool = False, + prefix: str = "/health", + **kwargs: dict[str, Any], + ) -> None: + """Initialize the router.""" + kwargs["prefix"] = prefix # type: ignore[assignment] + kwargs["tags"] = ["Healthchecks"] # type: ignore[assignment] + super().__init__(**kwargs) # type: ignore[arg-type] + for probe in probes: + self._add_probe_route( + probe, + success_handler=success_handler, + failure_handler=failure_handler, + success_status=success_status, + failure_status=failure_status, + debug=debug, + ) + + def _add_probe_route( # noqa: PLR0913 + self, + probe: Probe, + *, + success_handler: HandlerType = default_handler, + failure_handler: HandlerType = default_handler, + success_status: int = status.HTTP_204_NO_CONTENT, + failure_status: int = status.HTTP_503_SERVICE_UNAVAILABLE, + debug: bool = False, + ) -> None: + probe_handler = make_probe_asgi( + probe, + success_handler=success_handler, + failure_handler=failure_handler, + success_status=success_status, + failure_status=failure_status, + debug=debug, + ) + + async def handle_request() -> Response: + content, headers, status_code = await probe_handler() + return Response(content=content, status_code=status_code, headers=headers) + + self.add_api_route( + path=f"/{probe.name}", + endpoint=handle_request, + status_code=success_status, + summary=probe.endpoint_summary, + include_in_schema=debug, + ) diff --git a/fast_healthchecks/integrations/faststream.py b/fast_healthchecks/integrations/faststream.py new file mode 100644 index 0000000..0649435 --- /dev/null +++ b/fast_healthchecks/integrations/faststream.py @@ -0,0 +1,64 @@ +"""FastStream integration for health checks.""" + +from collections.abc import Iterable +from http import HTTPStatus +from typing import TYPE_CHECKING + +from faststream.asgi.handlers import get +from faststream.asgi.response import AsgiResponse + +from fast_healthchecks.integrations.base import HandlerType, Probe, default_handler, make_probe_asgi + +if TYPE_CHECKING: + from faststream.asgi.types import ASGIApp, Scope + + +def _add_probe_route( # noqa: PLR0913 + probe: Probe, + *, + success_handler: HandlerType = default_handler, + failure_handler: HandlerType = default_handler, + success_status: int = HTTPStatus.NO_CONTENT, + failure_status: int = HTTPStatus.SERVICE_UNAVAILABLE, + debug: bool = False, + prefix: str = "/health", +) -> tuple[str, "ASGIApp"]: + probe_handler = make_probe_asgi( + probe, + success_handler=success_handler, + failure_handler=failure_handler, + success_status=success_status, + failure_status=failure_status, + debug=debug, + ) + + @get + async def handle_request(scope: "Scope") -> AsgiResponse: # noqa: ARG001 + content, headers, status_code = await probe_handler() + return AsgiResponse(content, status_code=status_code, headers=headers) + + return f"{prefix.removesuffix('/')}/{probe.name.removeprefix('/')}", handle_request + + +def health( # noqa: PLR0913 + *probes: Probe, + success_handler: HandlerType = default_handler, + failure_handler: HandlerType = default_handler, + success_status: int = HTTPStatus.NO_CONTENT, + failure_status: int = HTTPStatus.SERVICE_UNAVAILABLE, + debug: bool = False, + prefix: str = "/health", +) -> Iterable[tuple[str, "ASGIApp"]]: + """Make list of routes for healthchecks.""" + return [ + _add_probe_route( + probe, + success_handler=success_handler, + failure_handler=failure_handler, + success_status=success_status, + failure_status=failure_status, + debug=debug, + prefix=prefix, + ) + for probe in probes + ] diff --git a/fast_healthchecks/integrations/litestar.py b/fast_healthchecks/integrations/litestar.py new file mode 100644 index 0000000..c915c73 --- /dev/null +++ b/fast_healthchecks/integrations/litestar.py @@ -0,0 +1,65 @@ +"""FastAPI integration for health checks.""" + +from collections.abc import Iterable +from http import HTTPStatus + +from litestar import Response, get +from litestar.handlers.http_handlers import HTTPRouteHandler + +from fast_healthchecks.integrations.base import HandlerType, Probe, default_handler, make_probe_asgi + + +def _add_probe_route( # noqa: PLR0913 + probe: Probe, + *, + success_handler: HandlerType = default_handler, + failure_handler: HandlerType = default_handler, + success_status: int = HTTPStatus.NO_CONTENT, + failure_status: int = HTTPStatus.SERVICE_UNAVAILABLE, + debug: bool = False, + prefix: str = "/health", +) -> HTTPRouteHandler: + probe_handler = make_probe_asgi( + probe, + success_handler=success_handler, + failure_handler=failure_handler, + success_status=success_status, + failure_status=failure_status, + debug=debug, + ) + + @get( + path=f"{prefix.removesuffix('/')}/{probe.name.removeprefix('/')}", + name=probe.name, + operation_id=f"health:{probe.name}", + summary=probe.summary, + ) + async def handle_request() -> Response[bytes]: + content, headers, status_code = await probe_handler() + return Response(content, headers=headers, status_code=status_code) + + return handle_request + + +def health( # noqa: PLR0913 + *probes: Probe, + success_handler: HandlerType = default_handler, + failure_handler: HandlerType = default_handler, + success_status: int = HTTPStatus.NO_CONTENT, + failure_status: int = HTTPStatus.SERVICE_UNAVAILABLE, + debug: bool = False, + prefix: str = "/health", +) -> Iterable[HTTPRouteHandler]: + """Make list of routes for healthchecks.""" + return [ + _add_probe_route( + probe, + success_handler=success_handler, + failure_handler=failure_handler, + success_status=success_status, + failure_status=failure_status, + debug=debug, + prefix=prefix, + ) + for probe in probes + ] diff --git a/fast_healthchecks/models.py b/fast_healthchecks/models.py new file mode 100644 index 0000000..8fcc8c2 --- /dev/null +++ b/fast_healthchecks/models.py @@ -0,0 +1,49 @@ +"""Models for healthchecks.""" + +from dataclasses import dataclass + +__all__ = ( + "HealthCheckResult", + "HealthcheckReport", +) + + +@dataclass +class HealthCheckResult: + """Result of a healthcheck. + + Attributes: + name: Name of the healthcheck. + healthy: Whether the healthcheck passed. + error_details: Details of the error if the healthcheck failed. + """ + + name: str + healthy: bool + error_details: str | None = None + + def __str__(self) -> str: + """Return a string representation of the result.""" + return f"{self.name}: {'healthy' if self.healthy else 'unhealthy'}" + + +@dataclass +class HealthcheckReport: + """Report of healthchecks. + + Attributes: + healthy: Whether all healthchecks passed. + results: List of healthcheck results. + """ + + results: list[HealthCheckResult] + allow_partial_failure: bool = False + + def __str__(self) -> str: + """Return a string representation of the report.""" + return "\n".join(str(result) for result in self.results) + + @property + def healthy(self) -> bool: + """Return whether all healthchecks passed.""" + return all(result.healthy for result in self.results) or self.allow_partial_failure diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..1b4ecc3 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,66 @@ +site_name: Fast Healthchecks +site_description: Healthcheck for most popular ASGI frameworks +site_url: https://github.com/shepilov-vladislav/fast-healthchecks +site_author: Vladislav Shepilov +copyright: Copyright © 2024 Vladislav Shepilov + +repo_name: fast-healthchecks +repo_url: https://github.com/shepilov-vladislav/fast-healthchecks +edit_uri: edit/main/docs/ + +plugins: + - search + - mkdocstrings + - include-markdown + +theme: + name: material + favicon: img/favicon.ico + logo: img/logo.svg + icon: + repo: fontawesome/brands/github + + palette: + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: slate + primary: green + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: default + primary: green + toggle: + icon: material/brightness-4 + name: Switch to system preference + + features: + - search.suggest + - search.highlight + - content.tabs.link + - content.code.copy + language: en + +markdown_extensions: + - attr_list + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - admonition + - pymdownx.details + - pymdownx.tabbed: + alternate_style: true + + +nav: + - Home: index.md + - API Reference: api.md + - Changelog: changelog.md diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..35100c4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,271 @@ +[project] +name = "fast-healthchecks" +version = "0.0.0" +description = "FastHealthchecks" +readme = "README.md" +license = { file = "LICENSE" } +authors = [ + {name = "Vladislav Shepilov", email = "shepilov.v@protonmail.com"}, +] +maintainers = [ + {name = "Vladislav Shepilov", email = "shepilov.v@protonmail.com"}, +] +keywords = [ + "healthcheck", "library", + "fastapi", "starlette", "faststream", "litestar", + "asyncpg", "psycopg", "redis", "aio-pika", "httpx", "aiokafka", "motor", +] +classifiers = [ + "Development Status :: 1 - Planning", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "License :: OSI Approved :: MIT License", +] +requires-python = "<4.0.0,>=3.10.0" +dependencies = [] + +[tool.setuptools] +packages = ["fast_healthchecks"] + +[project.urls] +homepage = "https://github.com/shepilov-vladislav/fast-healthchecks" +documentation = "https://shepilov-vladislav.github.io/fast-healthchecks/" +source = "https://github.com/shepilov-vladislav/fast-healthchecks" +tracker = "https://github.com/shepilov-vladislav/fast-healthchecks/issues" + +[project.optional-dependencies] +pydantic = ["pydantic>=2.10.3,<3.0.0"] +asyncpg = ["asyncpg>=0.30.0,<1.0.0"] +psycopg = ["psycopg>=3.2.3,<4.0.0"] +redis = ["redis>=5.2.1,<6.0.0"] +aio-pika = ["aio-pika>=9.5.3,<10.0.0"] +httpx = ["httpx>=0.28.1,<1.0.0"] +aiokafka = ["aiokafka>=0.12.0,<1.0.0"] +motor = ["motor>=3.6.0,<4.0.0"] +fastapi = ["fastapi[standard]>=0.115.6,<1.0.0"] +faststream = ["faststream>=0.5.33,<1.0.0"] +litestar = ["litestar>=2.13.0,<3.0.0"] +msgspec = ["msgspec>=0.18.6,<1.0.0"] + +[dependency-groups] +dev = [ + "pre-commit>=4.0.1,<5.0.0", + "ruff>=0.8.2,<1.0.0", + "mypy>=1.13.0,<2.0.0", + "mypy-extensions>=1.0.0,<2.0.0", + "pytest>=8.3.4,<9.0.0", + "pytest-cov>=6.0.0,<7.0.0", + "pytest-asyncio>=0.24.0,<1.0.0", + "pytest-deadfixtures>=2.2.1,<3.0.0", + "greenlet>=3.1.1,<4.0.0", + "tox>=4.23.2,<5.0.0", + "tox-uv>=1.16.1,<2.0.0", + "python-dotenv>=1.0.1,<2.0.0", + "types-redis>=4.6.0.20241004,<5.0.0.0", + "pytest-vcr>=1.0.2", +] +docs = [ + "mkdocs-include-markdown-plugin>=7.1.2", + "mkdocs>=1.6.1,<2.0.0", + "mkdocs-material>=9.5.47,<10.0.0", + "mkdocstrings[python]>=0.27.0,<1.0.0", + "pymdown-extensions>=10.12,<11.0", +] + +[tool.uv] +default-groups = [] + +[tool.uv.sources] +msgspec = { git = "https://github.com/jcrist/msgspec.git", rev = "main" } + +[tool.ruff] +line-length = 120 +# Assume Python 3.10 to be compatible with typing. +target-version = "py310" +# Exclude a variety of commonly ignored directories. +exclude = [ + ".git", + ".ruff_cache", + ".mypy_cache", + ".tox", +] + +[tool.ruff.format] +# Like Black, use double quotes for strings. +quote-style = "double" +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +[tool.ruff.lint] +select = [ + "F", # pyflakes + "W", # pycodestyle warnings + "E", # pycodestyle errors + "C90", # mccabe + "I", # isort + "N", # pep8-naming + "D", # pydocstyle, disabled because of we are using other docstring style + "UP", # pyupgrade + "YTT", # flake8-2020 + "ANN", # flake8-annotations + "ASYNC", # flake8-async + "S", # flake8-bandit + "BLE", # flake8-blind-except + "FBT", # flake8-boolean-trap + "B", # flake8-bugbear + "A", # flake8-builtins + "COM", # flake8-commas + "CPY", # flake8-copyright + "C4", # flake8-comprehensions + "DTZ", # flake8-datetimez + "T10", # flake8-debugger + "DJ", # flake8-django + "EM", # flake8-errmsg + "EXE", # flake8-executable + "FA", # flake8-future-annotations + "ISC", # flake8-implicit-str-concat + "ICN", # flake8-import-conventions + "G", # flake8-logging-format + "INP", # flake8-no-pep420 + "PIE", # flake8-pie + "T20", # flake8-print + "PYI", # flake8-pyi + "PT", # flake8-pytest + "Q", # flake8-quotes + "RSE", # flake8-raise + "RET", # flake8-return + "SLF", # flake8-self + "SLOT", # flake8-slots + "SIM", # flake8-simplify + "TID", # flake8-tidy-imports + "TCH", # flake8-type-checking + "INT", # flake8-gettext + "ARG", # flake8-unused-arguments + "PTH", # flake8-use-pathlib + "TD", # flake8-todos + "FIX", # flake8-fixme + "ERA", # eradicate + "PD", # pandas-vet + "PGH", # pygrep-hooks + "PL", # Pylint + "TRY", # tryceratops + "FLY", # flynt + "NPY", # NumPy-specific rules + "AIR", # Airflow + "PERF", # Perflint + "FURB", # refurb + "LOG", # flake8-logging + "RUF", # Ruff-specific rules +] +ignore = [ + "E501", # line too long, handled by ruff-format + "CPY001", # Missing copyright notice at top of file +] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.lint.flake8-tidy-imports] +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"pydantic_core.core_schema.FieldValidationInfo".msg = "FieldValidationInfo` is deprecated, use `ValidationInfo` instead." + +[tool.ruff.lint.per-file-ignores] +"!fast_healthchecks/**.py" = ["D", "S", "SLF"] + +[tool.pytest.ini_options] +pythonpath = [ + "." +] +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "session" +minversion = "8.0" +addopts = "-ra -q" +filterwarnings = [ + "error", + "error:::fast_healthchecks", + "ignore:Failing to pass a value to the 'type_params' parameter of 'typing.ForwardRef._evaluate':DeprecationWarning", +] +markers = """ + integration: mark a test as an integration test + unit: mark a test as a unit test + imports: mark a test as an imports test +""" + +[tool.mypy] +ignore_missing_imports = true +check_untyped_defs = true +disallow_any_generics = true +disallow_untyped_defs = true +follow_imports = "silent" +strict_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +show_error_codes = true + +[[tool.mypy.overrides]] +disable_error_code = ["arg-type", "attr-defined"] +module = "tests.*" +ignore_missing_imports = true +check_untyped_defs = true + +[tool.coverage.run] +source = ["fast_healthchecks"] + +[tool.coverage.report] +precision = 1 +include = ["fast_healthchecks/*"] +omit = [ + "tests/*", +] +exclude_lines = [ + # Have to re-enable the standard pragma + "pragma: no cover", + # Don't check obviously not implemented + "raise NotImplementedError", + # We don't really care what happens if fail + "except ImportError:", + # Don't check for typing-only code + "if TYPE_CHECKING:", + # Don't check for code that only runs itself + "if __name__ == .__main__.:", +] +show_missing = true + +[tool.tox] +legacy_tox_ini = """ + [tox] + requires = + tox>=4 + isolated_build = true + envlist = py{310,311,312,313}-pydantic-{v1,v2} + + [testenv] + deps = + pydantic1: pydantic>=1.10.19,<2.0.0 + pydantic2: pydantic>=2.10.3,<3.0.0 + extras = ["test"] + commands = + pytest --dead-fixtures + pytest --junitxml={envname}_report.xml --cov --cov-append -m 'not imports and not integration' +""" + +[tool.commitizen] +name = "cz_conventional_commits" +tag_format = "$version" +version_scheme = "semver" +version_provider = "pep621" +update_changelog_on_bump = true +major_version_zero = false +version_files = [ + "pyproject.toml:version", + "fast_healthchecks/__init__.py:__version__", +] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1022e75 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# This file was autogenerated by uv via the following command: +# uv export --frozen --output-file=requirements.txt diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..6183265 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,9 @@ +sonar.projectKey=${env.SONAR_PROJECT_KEY} +sonar.qualitygate.wait=true +sonar.scm.provider=git +sonar.python.version=3.10 +sonar.python.coverage.reportPaths=coverage.xml +sonar.sources=fast_healthchecks/ +sonar.tests=tests/ +sonar.coverage.exclusions=tests/** +sonar.projectVersion=${env.CI_COMMIT_SHORT_SHA} diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/checks/__init__.py b/tests/integration/checks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/checks/postgresql/__init__.py b/tests/integration/checks/postgresql/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/checks/postgresql/conftest.py b/tests/integration/checks/postgresql/conftest.py new file mode 100644 index 0000000..d133fe0 --- /dev/null +++ b/tests/integration/checks/postgresql/conftest.py @@ -0,0 +1,33 @@ +from typing import Any, TypedDict + +import pytest + + +class BasePostgreSQLConfig(TypedDict, total=True): + host: str + port: int + user: str | None + password: str | None + database: str | None + + +@pytest.fixture(scope="session", name="base_postgresql_config") +def fixture_base_postgresql_config(env_config: dict[str, Any]) -> BasePostgreSQLConfig: + result: BasePostgreSQLConfig = { + "host": "localhost", + "port": 5432, + "user": None, + "password": None, + "database": None, + } + for key in ("host", "port", "user", "password", "database"): + value = env_config.get(f"POSTGRES_{key.upper()}") + match key: + case "port": + if value is not None: + result[key] = int(value) + case _: + if value is not None: + result[key] = str(value) + + return result diff --git a/tests/integration/checks/postgresql/test_asyncpg.py b/tests/integration/checks/postgresql/test_asyncpg.py new file mode 100644 index 0000000..83c1ea9 --- /dev/null +++ b/tests/integration/checks/postgresql/test_asyncpg.py @@ -0,0 +1,56 @@ +import ssl + +import pytest + +from fast_healthchecks.checks.postgresql.asyncpg import PostgreSQLAsyncPGHealthCheck +from fast_healthchecks.models import HealthCheckResult +from tests.integration.checks.postgresql.conftest import BasePostgreSQLConfig + +pytestmark = pytest.mark.integration + + +class AsyncPGConfig(BasePostgreSQLConfig, total=True): + ssl: ssl.SSLContext | None + direct_tls: bool | None + + +@pytest.fixture(scope="session", name="asyncpg_config") +def fixture_asyncpg_config(base_postgresql_config: BasePostgreSQLConfig) -> AsyncPGConfig: + return { + **base_postgresql_config, + "ssl": None, + "direct_tls": None, + } + + +@pytest.mark.asyncio +async def test_postgresql_asyncpg_check_success(asyncpg_config: AsyncPGConfig) -> None: + check = PostgreSQLAsyncPGHealthCheck(**asyncpg_config) + result = await check() + assert result == HealthCheckResult(name="PostgreSQL", healthy=True, error_details=None) + + +@pytest.mark.asyncio +async def test_postgresql_asyncpg_check_failure(asyncpg_config: AsyncPGConfig) -> None: + config = { + **asyncpg_config, + "host": "localhost2", + } + check = PostgreSQLAsyncPGHealthCheck(**config) + result = await check() + assert result.healthy is False + assert result.error_details is not None + assert "nodename nor servname provided, or not known" in result.error_details + + +@pytest.mark.asyncio +async def test_postgresql_asyncpg_check_connection_error(asyncpg_config: AsyncPGConfig) -> None: + config = { + **asyncpg_config, + "port": 6432, + } + check = PostgreSQLAsyncPGHealthCheck(**config) + result = await check() + assert result.healthy is False + assert result.error_details is not None + assert "Connect call failed" in result.error_details diff --git a/tests/integration/checks/postgresql/test_psycopg.py b/tests/integration/checks/postgresql/test_psycopg.py new file mode 100644 index 0000000..37e8b62 --- /dev/null +++ b/tests/integration/checks/postgresql/test_psycopg.py @@ -0,0 +1,54 @@ +import pytest + +from fast_healthchecks.checks.postgresql.psycopg import PostgreSQLPsycopgHealthCheck +from fast_healthchecks.models import HealthCheckResult +from tests.integration.checks.postgresql.conftest import BasePostgreSQLConfig + +pytestmark = pytest.mark.integration + + +class PsycopgConfig(BasePostgreSQLConfig, total=True): + sslmode: str | None + sslrootcert: str | None + + +@pytest.fixture(scope="session", name="psycopg_config") +def fixture_psycopg_config(base_postgresql_config: BasePostgreSQLConfig) -> PsycopgConfig: + return { + **base_postgresql_config, + "sslmode": None, + "sslrootcert": None, + } + + +@pytest.mark.asyncio +async def test_postgresql_psycopg_check_success(psycopg_config: PsycopgConfig) -> None: + check = PostgreSQLPsycopgHealthCheck(**psycopg_config) + result = await check() + assert result == HealthCheckResult(name="PostgreSQL", healthy=True, error_details=None) + + +@pytest.mark.asyncio +async def test_postgresql_psycopg_check_failure(psycopg_config: PsycopgConfig) -> None: + config = { + **psycopg_config, + "host": "localhost2", + } + check = PostgreSQLPsycopgHealthCheck(**config) + result = await check() + assert result.healthy is False + assert result.error_details is not None + assert "nodename nor servname provided, or not known" in result.error_details + + +@pytest.mark.asyncio +async def test_postgresql_psycopg_check_connection_error(psycopg_config: PsycopgConfig) -> None: + config = { + **psycopg_config, + "port": 6432, + } + check = PostgreSQLPsycopgHealthCheck(**config) + result = await check() + assert result.healthy is False + assert result.error_details is not None + assert "connection failed" in result.error_details diff --git a/tests/integration/checks/test_function.py b/tests/integration/checks/test_function.py new file mode 100644 index 0000000..57f46b3 --- /dev/null +++ b/tests/integration/checks/test_function.py @@ -0,0 +1,3 @@ +import pytest + +pytestmark = pytest.mark.integration diff --git a/tests/integration/checks/test_kafka.py b/tests/integration/checks/test_kafka.py new file mode 100644 index 0000000..797070a --- /dev/null +++ b/tests/integration/checks/test_kafka.py @@ -0,0 +1,60 @@ +from typing import Any, TypedDict + +import pytest + +from fast_healthchecks.checks.kafka import KafkaHealthCheck +from fast_healthchecks.models import HealthCheckResult + +pytestmark = pytest.mark.integration + + +class KafkaConfig(TypedDict, total=True): + bootstrap_servers: str + + +@pytest.fixture(scope="session", name="kafka_config") +def fixture_kafka_config(env_config: dict[str, Any]) -> KafkaConfig: + result: KafkaConfig = { + "bootstrap_servers": "localhost:9092", + } + for key in ("bootstrap_servers",): + value = env_config.get(f"KAFKA_{key.upper()}") + match key: + case _: + if value is not None: + result[key] = str(value) + + return result + + +@pytest.mark.asyncio +async def test_kafka_check_success(kafka_config: KafkaConfig) -> None: + check = KafkaHealthCheck(**kafka_config) + result = await check() + assert result == HealthCheckResult(name="Kafka", healthy=True, error_details=None) + + +@pytest.mark.asyncio +async def test_kafka_check_failure(kafka_config: KafkaConfig) -> None: + config = { + **kafka_config, + "bootstrap_servers": "localhost2:9093", + } + check = KafkaHealthCheck(**config) + result = await check() + assert result.healthy is False + assert result.error_details is not None + assert "Unable to bootstrap from" in result.error_details + + +@pytest.mark.asyncio +async def test_kafka_check_connection_error(kafka_config: KafkaConfig) -> None: + config = { + **kafka_config, + "bootstrap_servers": "localhost:9092", + } + check = KafkaHealthCheck(**config) + result = await check() + assert result.healthy is False + assert result.error_details is not None + assert "Unable to bootstrap from" in result.error_details diff --git a/tests/integration/checks/test_mongo.py b/tests/integration/checks/test_mongo.py new file mode 100644 index 0000000..d1b0111 --- /dev/null +++ b/tests/integration/checks/test_mongo.py @@ -0,0 +1,73 @@ +from typing import Any, TypedDict + +import pytest + +from fast_healthchecks.checks.mongo import MongoHealthCheck +from fast_healthchecks.models import HealthCheckResult + +pytestmark = pytest.mark.integration + + +class MongoConfig(TypedDict, total=True): + host: str + port: int + user: str | None + password: str | None + database: str | None + auth_source: str | None + + +@pytest.fixture(scope="session", name="mongo_config") +def fixture_mongo_config(env_config: dict[str, Any]) -> MongoConfig: + result: MongoConfig = { + "host": "localhost", + "port": 27017, + "user": None, + "password": None, + "database": None, + "auth_source": "admin", + } + for key in ("host", "port", "user", "password", "database", "auth_source"): + value = env_config.get(f"MONGO_{key.upper()}") + match key: + case "port": + if value is not None: + result[key] = int(value) + case _: + if value is not None: + result[key] = str(value) + + return result + + +@pytest.mark.asyncio +async def test_mongo_check_success(mongo_config: MongoConfig) -> None: + check = MongoHealthCheck(**mongo_config) + result = await check() + assert result == HealthCheckResult(name="MongoDB", healthy=True, error_details=None) + + +@pytest.mark.asyncio +async def test_mongo_check_failure(mongo_config: MongoConfig) -> None: + config = { + **mongo_config, + "host": "localhost2", + } + check = MongoHealthCheck(**config) + result = await check() + assert result.healthy is False + assert result.error_details is not None + assert "nodename nor servname provided, or not known" in result.error_details + + +@pytest.mark.asyncio +async def test_mongo_check_connection_error(mongo_config: MongoConfig) -> None: + config = { + **mongo_config, + "port": 27018, + } + check = MongoHealthCheck(**config) + result = await check() + assert result.healthy is False + assert result.error_details is not None + assert "Connection refused" in result.error_details diff --git a/tests/integration/checks/test_rabbitmq.py b/tests/integration/checks/test_rabbitmq.py new file mode 100644 index 0000000..3c592ac --- /dev/null +++ b/tests/integration/checks/test_rabbitmq.py @@ -0,0 +1,71 @@ +from typing import Any, TypedDict + +import pytest + +from fast_healthchecks.checks.rabbitmq import RabbitMQHealthCheck +from fast_healthchecks.models import HealthCheckResult + +pytestmark = pytest.mark.integration + + +class RabbitMqConfig(TypedDict, total=True): + host: str + port: int + user: str | None + password: str | None + vhost: str | None + + +@pytest.fixture(scope="session", name="rabbitmq_config") +def fixture_rabbitmq_config(env_config: dict[str, Any]) -> RabbitMqConfig: + result: RabbitMqConfig = { + "host": "localhost", + "port": 5672, + "user": None, + "password": None, + "vhost": "/", + } + for key in ("host", "port", "user", "password", "vhost"): + value = env_config.get(f"RABBITMQ_{key.upper()}") + match key: + case "port": + if value is not None: + result[key] = int(value) + case _: + if value is not None: + result[key] = str(value) + + return result + + +@pytest.mark.asyncio +async def test_rabbitmq_check_success(rabbitmq_config: RabbitMqConfig) -> None: + check = RabbitMQHealthCheck(**rabbitmq_config) + result = await check() + assert result == HealthCheckResult(name="RabbitMQ", healthy=True, error_details=None) + + +@pytest.mark.asyncio +async def test_rabbitmq_check_failure(rabbitmq_config: RabbitMqConfig) -> None: + config = { + **rabbitmq_config, + "host": "localhost2", + } + check = RabbitMQHealthCheck(**config) + result = await check() + assert result.healthy is False + assert result.error_details is not None + assert "nodename nor servname provided, or not known" in result.error_details + + +@pytest.mark.asyncio +async def test_rabbitmq_check_connection_error(rabbitmq_config: RabbitMqConfig) -> None: + config = { + **rabbitmq_config, + "port": 5673, + } + check = RabbitMQHealthCheck(**config) + result = await check() + assert result.healthy is False + assert result.error_details is not None + assert "Connect call failed" in result.error_details diff --git a/tests/integration/checks/test_redis.py b/tests/integration/checks/test_redis.py new file mode 100644 index 0000000..ee64661 --- /dev/null +++ b/tests/integration/checks/test_redis.py @@ -0,0 +1,71 @@ +from typing import Any, TypedDict + +import pytest + +from fast_healthchecks.checks.redis import RedisHealthCheck +from fast_healthchecks.models import HealthCheckResult + +pytestmark = pytest.mark.integration + + +class RedisConfig(TypedDict, total=True): + host: str + port: int + user: str | None + password: str | None + database: str | int + + +@pytest.fixture(scope="session", name="redis_config") +def fixture_redis_config(env_config: dict[str, Any]) -> RedisConfig: + result: RedisConfig = { + "host": "localhost", + "port": 6379, + "user": None, + "password": None, + "database": 0, + } + for key in ("host", "port", "user", "password", "database"): + value = env_config.get(f"REDIS_{key.upper()}") + match key: + case "port": + if value is not None: + result[key] = int(value) + case _: + if value is not None: + result[key] = str(value) + + return result + + +@pytest.mark.asyncio +async def test_redis_check_success(redis_config: RedisConfig) -> None: + check = RedisHealthCheck(**redis_config) + result = await check() + assert result == HealthCheckResult(name="Redis", healthy=True, error_details=None) + + +@pytest.mark.asyncio +async def test_redis_check_failure(redis_config: RedisConfig) -> None: + config = { + **redis_config, + "host": "localhost2", + } + check = RedisHealthCheck(**config) + result = await check() + assert result.healthy is False + assert result.error_details is not None + assert "nodename nor servname provided, or not known" in result.error_details + + +@pytest.mark.asyncio +async def test_redis_check_connection_error(redis_config: RedisConfig) -> None: + config = { + **redis_config, + "port": 6380, + } + check = RedisHealthCheck(**config) + result = await check() + assert result.healthy is False + assert result.error_details is not None + assert "Connect call failed" in result.error_details diff --git a/tests/integration/checks/test_url.py b/tests/integration/checks/test_url.py new file mode 100644 index 0000000..a4ff9a2 --- /dev/null +++ b/tests/integration/checks/test_url.py @@ -0,0 +1,64 @@ +import pytest + +from fast_healthchecks.checks.url import UrlHealthCheck +from fast_healthchecks.models import HealthCheckResult + +pytestmark = pytest.mark.integration + + +@pytest.mark.asyncio +async def test_url_health_check_success() -> None: + check = UrlHealthCheck( + name="test_check", + url="https://httpbin.org/status/200", + ) + result = await check() + assert result == HealthCheckResult(name="test_check", healthy=True) + + +@pytest.mark.asyncio +async def test_url_health_check_failure() -> None: + check = UrlHealthCheck( + name="test_check", + url="https://httpbin.org/status/500", + ) + result = await check() + assert result.healthy is False + assert "500 INTERNAL SERVER ERROR" in result.error_details + + +@pytest.mark.asyncio +async def test_url_health_check_with_basic_auth_success() -> None: + check = UrlHealthCheck( + name="test_check", + url="https://httpbin.org/basic-auth/user/passwd", + username="user", + password="passwd", + ) + result = await check() + assert result == HealthCheckResult(name="test_check", healthy=True) + + +@pytest.mark.asyncio +async def test_url_health_check_with_basic_auth_failure() -> None: + check = UrlHealthCheck( + name="test_check", + url="https://httpbin.org/basic-auth/user/passwd", + username="user", + password="wrong_passwd", + ) + result = await check() + assert result.healthy is False + assert "401 UNAUTHORIZED" in result.error_details + + +@pytest.mark.asyncio +async def test_url_health_check_with_timeout() -> None: + check = UrlHealthCheck( + name="test_check", + url="https://httpbin.org/delay/5", + timeout=1, + ) + result = await check() + assert result.healthy is False + assert "Timeout" in result.error_details diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 0000000..6065fa3 --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,13 @@ +import os +from typing import Any + +import pytest +from dotenv import dotenv_values + + +@pytest.fixture(scope="session", name="env_config") +def fixture_env_config() -> dict[str, Any]: + return { + **dotenv_values(".env"), # load shared default test environment variables + **os.environ, # override loaded values with environment variables + } diff --git a/tests/integration/integrations/__init__.py b/tests/integration/integrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/integrations/test_fastapi.py b/tests/integration/integrations/test_fastapi.py new file mode 100644 index 0000000..68dccc1 --- /dev/null +++ b/tests/integration/integrations/test_fastapi.py @@ -0,0 +1,49 @@ +import json + +import pytest +from fastapi import status +from fastapi.testclient import TestClient + +from examples.fastapi_example.main import app_custom, app_fail, app_integration + +pytestmark = pytest.mark.integration + +client = TestClient(app_integration) + + +def test_liveness_probe() -> None: + response = client.get("/health/liveness") + assert response.status_code == status.HTTP_204_NO_CONTENT + assert response.content == b"" + + +def test_readiness_probe() -> None: + response = client.get("/health/readiness") + assert response.status_code == status.HTTP_204_NO_CONTENT + assert response.content == b"" + + +def test_startup_probe() -> None: + response = client.get("/health/startup") + assert response.status_code == status.HTTP_204_NO_CONTENT + assert response.content == b"" + + +def test_readiness_probe_fail() -> None: + client_fail = TestClient(app_fail) + response = client_fail.get("/health/readiness") + assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE + assert response.content == b"" + + +def test_custom_handler() -> None: + client_custom = TestClient(app_custom) + response = client_custom.get("/custom_health/readiness") + assert response.status_code == status.HTTP_200_OK + assert response.content == json.dumps( + {"results": [{"name": "Async dummy", "healthy": True, "error_details": None}], "allow_partial_failure": False}, + ensure_ascii=False, + allow_nan=False, + indent=None, + separators=(",", ":"), + ).encode("utf-8") diff --git a/tests/integration/integrations/test_faststream.py b/tests/integration/integrations/test_faststream.py new file mode 100644 index 0000000..a1f7104 --- /dev/null +++ b/tests/integration/integrations/test_faststream.py @@ -0,0 +1,49 @@ +import json +from http import HTTPStatus + +import pytest +from starlette.testclient import TestClient + +from examples.faststream_example.main import app_custom, app_fail, app_integration + +pytestmark = pytest.mark.integration + +client = TestClient(app_integration) + + +def test_liveness_probe() -> None: + response = client.get("/health/liveness") + assert response.status_code == HTTPStatus.NO_CONTENT + assert response.content == b"" + + +def test_readiness_probe() -> None: + response = client.get("/health/readiness") + assert response.status_code == HTTPStatus.NO_CONTENT + assert response.content == b"" + + +def test_startup_probe() -> None: + response = client.get("/health/startup") + assert response.status_code == HTTPStatus.NO_CONTENT + assert response.content == b"" + + +def test_readiness_probe_fail() -> None: + client_fail = TestClient(app_fail) + response = client_fail.get("/health/readiness") + assert response.status_code == HTTPStatus.SERVICE_UNAVAILABLE + assert response.content == b"" + + +def test_custom_handler() -> None: + client_custom = TestClient(app_custom) + response = client_custom.get("/custom_health/readiness") + assert response.status_code == HTTPStatus.OK + assert response.content == json.dumps( + {"results": [{"name": "Async dummy", "healthy": True, "error_details": None}], "allow_partial_failure": False}, + ensure_ascii=False, + allow_nan=False, + indent=None, + separators=(",", ":"), + ).encode("utf-8") diff --git a/tests/integration/integrations/test_litestar.py b/tests/integration/integrations/test_litestar.py new file mode 100644 index 0000000..74854cc --- /dev/null +++ b/tests/integration/integrations/test_litestar.py @@ -0,0 +1,51 @@ +import json + +import pytest +from litestar.status_codes import HTTP_200_OK, HTTP_204_NO_CONTENT, HTTP_503_SERVICE_UNAVAILABLE +from litestar.testing import TestClient + +from examples.litestar_example.main import app_custom, app_fail, app_integration + +app_integration.debug = True +pytestmark = pytest.mark.integration + + +def test_liveness_probe() -> None: + with TestClient(app=app_integration) as client: + response = client.get("/health/liveness") + assert response.status_code == HTTP_204_NO_CONTENT + assert response.content == b"" + + +def test_readiness_probe() -> None: + with TestClient(app=app_integration) as client: + response = client.get("/health/readiness") + assert response.status_code == HTTP_204_NO_CONTENT + assert response.content == b"" + + +def test_startup_probe() -> None: + with TestClient(app=app_integration) as client: + response = client.get("/health/startup") + assert response.status_code == HTTP_204_NO_CONTENT + assert response.content == b"" + + +def test_readiness_probe_fail() -> None: + with TestClient(app=app_fail) as client: + response = client.get("/health/readiness") + assert response.status_code == HTTP_503_SERVICE_UNAVAILABLE + assert response.content == b"" + + +def test_custom_handler() -> None: + with TestClient(app=app_custom) as client: + response = client.get("/custom_health/readiness") + assert response.status_code == HTTP_200_OK + assert response.content == json.dumps( + {"results": [{"name": "Async dummy", "healthy": True, "error_details": None}], "allow_partial_failure": False}, + ensure_ascii=False, + allow_nan=False, + indent=None, + separators=(",", ":"), + ).encode("utf-8") diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/checks/__init__.py b/tests/unit/checks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/checks/cassettes/test_url_health_check_failure.yaml b/tests/unit/checks/cassettes/test_url_health_check_failure.yaml new file mode 100644 index 0000000..24f7131 --- /dev/null +++ b/tests/unit/checks/cassettes/test_url_health_check_failure.yaml @@ -0,0 +1,38 @@ +interactions: +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - httpbin.org + user-agent: + - python-httpx/0.28.1 + method: GET + uri: https://httpbin.org/status/500 + response: + body: + string: '' + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Origin: + - '*' + Connection: + - keep-alive + Content-Length: + - '0' + Content-Type: + - text/html; charset=utf-8 + Date: + - Sat, 07 Dec 2024 13:20:09 GMT + Server: + - gunicorn/19.9.0 + status: + code: 500 + message: INTERNAL SERVER ERROR +version: 1 diff --git a/tests/unit/checks/cassettes/test_url_health_check_success.yaml b/tests/unit/checks/cassettes/test_url_health_check_success.yaml new file mode 100644 index 0000000..c412fd6 --- /dev/null +++ b/tests/unit/checks/cassettes/test_url_health_check_success.yaml @@ -0,0 +1,38 @@ +interactions: +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - httpbin.org + user-agent: + - python-httpx/0.28.1 + method: GET + uri: https://httpbin.org/status/200 + response: + body: + string: '' + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Origin: + - '*' + Connection: + - keep-alive + Content-Length: + - '0' + Content-Type: + - text/html; charset=utf-8 + Date: + - Sat, 07 Dec 2024 13:20:08 GMT + Server: + - gunicorn/19.9.0 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/unit/checks/cassettes/test_url_health_check_with_basic_auth_failure.yaml b/tests/unit/checks/cassettes/test_url_health_check_with_basic_auth_failure.yaml new file mode 100644 index 0000000..d49810c --- /dev/null +++ b/tests/unit/checks/cassettes/test_url_health_check_with_basic_auth_failure.yaml @@ -0,0 +1,40 @@ +interactions: +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - Basic dXNlcjp3cm9uZ19wYXNzd2Q= + connection: + - keep-alive + host: + - httpbin.org + user-agent: + - python-httpx/0.28.1 + method: GET + uri: https://httpbin.org/basic-auth/user/passwd + response: + body: + string: '' + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Origin: + - '*' + Connection: + - keep-alive + Content-Length: + - '0' + Date: + - Sat, 07 Dec 2024 13:20:12 GMT + Server: + - gunicorn/19.9.0 + WWW-Authenticate: + - Basic realm="Fake Realm" + status: + code: 401 + message: UNAUTHORIZED +version: 1 diff --git a/tests/unit/checks/cassettes/test_url_health_check_with_basic_auth_success.yaml b/tests/unit/checks/cassettes/test_url_health_check_with_basic_auth_success.yaml new file mode 100644 index 0000000..b5d7145 --- /dev/null +++ b/tests/unit/checks/cassettes/test_url_health_check_with_basic_auth_success.yaml @@ -0,0 +1,40 @@ +interactions: +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - Basic dXNlcjpwYXNzd2Q= + connection: + - keep-alive + host: + - httpbin.org + user-agent: + - python-httpx/0.28.1 + method: GET + uri: https://httpbin.org/basic-auth/user/passwd + response: + body: + string: "{\n \"authenticated\": true, \n \"user\": \"user\"\n}\n" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Origin: + - '*' + Connection: + - keep-alive + Content-Length: + - '47' + Content-Type: + - application/json + Date: + - Sat, 07 Dec 2024 13:20:10 GMT + Server: + - gunicorn/19.9.0 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/unit/checks/postgresql/__init__.py b/tests/unit/checks/postgresql/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/checks/postgresql/test_asyncpg.py b/tests/unit/checks/postgresql/test_asyncpg.py new file mode 100644 index 0000000..2dfce5c --- /dev/null +++ b/tests/unit/checks/postgresql/test_asyncpg.py @@ -0,0 +1,835 @@ +import ssl +from typing import Any +from unittest.mock import MagicMock, patch +from urllib.parse import ParseResult, unquote, urlparse + +import pytest +from asyncpg import Connection + +from fast_healthchecks.checks.postgresql.asyncpg import PostgreSQLAsyncPGHealthCheck +from fast_healthchecks.checks.postgresql.base import create_ssl_context +from tests.utils import ( + TEST_SSLCERT, + TEST_SSLKEY, + TEST_SSLROOTCERT, + create_temp_files, +) + +pytestmark = pytest.mark.unit + + +def to_dict(obj: PostgreSQLAsyncPGHealthCheck) -> dict[str, Any]: + return { + "host": obj._host, + "port": obj._port, + "user": obj._user, + "password": obj._password, + "database": obj._database, + "ssl": obj._ssl, + "direct_tls": obj._direct_tls, + "timeout": obj._timeout, + "name": obj._name, + } + + +@pytest.mark.parametrize( + ("params", "expected", "exception"), + [ + ( + {}, + "missing 2 required keyword-only arguments: 'host' and 'port'", + TypeError, + ), + ( + { + "host": "localhost", + }, + "missing 1 required keyword-only argument: 'port'", + TypeError, + ), + ( + { + "host": "localhost", + "port": 5432, + }, + { + "host": "localhost", + "port": 5432, + "user": None, + "password": None, + "database": None, + "ssl": None, + "direct_tls": False, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": None, + "database": None, + "ssl": None, + "direct_tls": False, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": None, + "ssl": None, + "direct_tls": False, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "ssl": None, + "direct_tls": False, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "ssl": ("verify-full", TEST_SSLCERT, TEST_SSLKEY, TEST_SSLROOTCERT), + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "ssl": ("verify-full", TEST_SSLCERT, TEST_SSLKEY, TEST_SSLROOTCERT), + "direct_tls": False, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "ssl": ("verify-full", TEST_SSLCERT, TEST_SSLKEY, TEST_SSLROOTCERT), + "direct_tls": True, + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "ssl": ("verify-full", TEST_SSLCERT, TEST_SSLKEY, TEST_SSLROOTCERT), + "direct_tls": True, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "ssl": ("verify-full", TEST_SSLCERT, TEST_SSLKEY, TEST_SSLROOTCERT), + "direct_tls": True, + "timeout": 10.0, + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "ssl": ("verify-full", TEST_SSLCERT, TEST_SSLKEY, TEST_SSLROOTCERT), + "direct_tls": True, + "timeout": 10.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "ssl": ("verify-full", TEST_SSLCERT, TEST_SSLKEY, TEST_SSLROOTCERT), + "direct_tls": True, + "timeout": 10.0, + "name": "test", + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "ssl": ("verify-full", TEST_SSLCERT, TEST_SSLKEY, TEST_SSLROOTCERT), + "direct_tls": True, + "timeout": 10.0, + "name": "test", + }, + None, + ), + ], +) +def test__init(params: dict[str, Any], expected: dict[str, Any] | str, exception: type[BaseException] | None) -> None: + files_1 = list(params.get("ssl", ()) or ()) + files_2 = list(expected.get("ssl", ()) or ()) if exception is None else [] + files = [] + files += files_1[1:] if files_1 else [] + files += files_2[1:] if files_2 else [] + files = set(files) + with create_temp_files(files): + if "ssl" in params and params["ssl"] is not None: + params["ssl"] = create_ssl_context(*params["ssl"]) + if "ssl" in expected and expected["ssl"] is not None: + expected["ssl"] = create_ssl_context(*expected["ssl"]) + if exception is not None: + with pytest.raises(exception, match=expected): + PostgreSQLAsyncPGHealthCheck(**params) + else: + obj = PostgreSQLAsyncPGHealthCheck(**params) + assert to_dict(obj) == expected + + +@pytest.mark.parametrize( + ("args", "kwargs", "expected", "exception"), + [ + ( + ("postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=broken",), + {}, + "Invalid sslmode: broken", + ValueError, + ), + ( + ("postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=disable",), + {}, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": None, + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + (f"postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=disable&sslcert={TEST_SSLCERT}",), + {}, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": None, + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ( + f"postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=disable&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}", + ), + {}, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": None, + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ( + f"postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=disable&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}&sslrootcert={TEST_SSLROOTCERT}", + ), + {}, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": None, + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ("postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=allow",), + {}, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": None, + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + (f"postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=allow&sslcert={TEST_SSLCERT}",), + {}, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": None, + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ( + f"postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=allow&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}", + ), + {}, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": None, + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ( + f"postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=allow&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}&sslrootcert={TEST_SSLROOTCERT}", + ), + {}, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": None, + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ("postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=prefer",), + {}, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": ("prefer", None, None, None), + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + (f"postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=prefer&sslcert={TEST_SSLCERT}",), + {}, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": ("prefer", TEST_SSLCERT, None, None), + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ( + f"postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=prefer&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}", + ), + {}, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": ("prefer", TEST_SSLCERT, TEST_SSLKEY, None), + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ( + f"postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=prefer&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}&sslrootcert={TEST_SSLROOTCERT}", + ), + {}, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": ("prefer", TEST_SSLCERT, TEST_SSLKEY, TEST_SSLROOTCERT), + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ("postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=require",), + {}, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": ("require", None, None, None), + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + (f"postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=require&sslcert={TEST_SSLCERT}",), + {}, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": ("require", TEST_SSLCERT, None, None), + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ( + f"postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=require&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}", + ), + {}, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": ("require", TEST_SSLCERT, TEST_SSLKEY, None), + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ( + f"postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=require&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}&sslrootcert={TEST_SSLROOTCERT}", + ), + {}, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": ("require", TEST_SSLCERT, TEST_SSLKEY, TEST_SSLROOTCERT), + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ("postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=verify-ca",), + {}, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": ("verify-ca", None, None, None), + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + (f"postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=verify-ca&sslcert={TEST_SSLCERT}",), + {}, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": ("verify-ca", TEST_SSLCERT, None, None), + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ( + f"postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=verify-ca&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}", + ), + {}, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": ("verify-ca", TEST_SSLCERT, TEST_SSLKEY, None), + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ( + f"postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=verify-ca&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}&sslrootcert={TEST_SSLROOTCERT}", + ), + {}, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": ("verify-ca", TEST_SSLCERT, TEST_SSLKEY, TEST_SSLROOTCERT), + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ("postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=verify-full",), + {}, + "sslcert is required for verify-full", + ValueError, + ), + ( + (f"postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=verify-full&sslcert={TEST_SSLCERT}",), + {}, + "\\[SSL\\] PEM lib \\(_ssl.c:\\d+\\)", + ssl.SSLError, + ), + ( + ( + f"postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=verify-full&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}", + ), + {}, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": ("verify-full", unquote(TEST_SSLCERT), unquote(TEST_SSLKEY), None), + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ( + f"postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=verify-full&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}&sslrootcert={TEST_SSLROOTCERT}", + ), + {}, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": ("verify-full", unquote(TEST_SSLCERT), unquote(TEST_SSLKEY), unquote(TEST_SSLROOTCERT)), + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ( + f"postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=verify-full&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}&sslrootcert={TEST_SSLROOTCERT}", + ), + { + "timeout": 10.0, + }, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": ("verify-full", unquote(TEST_SSLCERT), unquote(TEST_SSLKEY), unquote(TEST_SSLROOTCERT)), + "user": "postgres", + "timeout": 10.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ( + f"postgresql+asyncpg://postgres:pass@localhost:5432/db?sslmode=verify-full&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}&sslrootcert={TEST_SSLROOTCERT}", + ), + { + "timeout": 10.0, + "name": "test", + }, + { + "database": "db", + "direct_tls": False, + "host": "localhost", + "password": "pass", + "port": 5432, + "ssl": ("verify-full", unquote(TEST_SSLCERT), unquote(TEST_SSLKEY), unquote(TEST_SSLROOTCERT)), + "user": "postgres", + "timeout": 10.0, + "name": "test", + }, + None, + ), + ], +) +def test_from_dsn( + args: tuple[Any, ...], + kwargs: dict[str, Any], + expected: dict[str, Any] | str, + exception: type[BaseException] | None, +) -> None: + parse_result: ParseResult = urlparse(args[0]) + query = {k: unquote(v) for k, v in (q.split("=") for q in parse_result.query.split("&"))} + files = [y for x, y in query.items() if x in {"sslcert", "sslkey", "sslrootcert"}] + + if exception is not None: + with pytest.raises(exception, match=expected), create_temp_files(files): + PostgreSQLAsyncPGHealthCheck.from_dsn(*args, **kwargs) + else: + with create_temp_files(files): + check = PostgreSQLAsyncPGHealthCheck.from_dsn(*args, **kwargs) + if "ssl" in expected and expected["ssl"] is not None: + expected["ssl"] = create_ssl_context(*expected["ssl"]) + assert to_dict(check) == expected + + +@pytest.mark.asyncio +async def test_asyncpg_connect_args_kwargs() -> None: + with create_temp_files([TEST_SSLCERT, TEST_SSLKEY, TEST_SSLROOTCERT]): + test_ssl_context = create_ssl_context("verify-full", TEST_SSLCERT, TEST_SSLKEY, TEST_SSLROOTCERT) + health_check = PostgreSQLAsyncPGHealthCheck( + host="localhost2", + port=6432, + user="user", + password="password", + database="db", + ssl=test_ssl_context, + direct_tls=True, + timeout=1.5, + name="test", + ) + Connection_mock = MagicMock(spec=Connection) # noqa: N806 + Connection_mock.is_closed.return_value = False + with patch( + "fast_healthchecks.checks.postgresql.asyncpg.asyncpg.connect", + return_value=Connection_mock, + ) as asyncpg_connect_mock: + await health_check() + asyncpg_connect_mock.assert_called_once_with( + host="localhost2", + port=6432, + user="user", + password="password", + database="db", + timeout=1.5, + ssl=test_ssl_context, + direct_tls=True, + ) + asyncpg_connect_mock.assert_awaited_once_with( + host="localhost2", + port=6432, + user="user", + password="password", + database="db", + timeout=1.5, + ssl=test_ssl_context, + direct_tls=True, + ) + Connection_mock.transaction.assert_called_once_with(readonly=True) + Connection_mock.fetchval.assert_called_once_with("SELECT 1") + Connection_mock.fetchval.assert_awaited_once_with("SELECT 1") + Connection_mock.is_closed.assert_called_once_with() + Connection_mock.close.assert_called_once_with(timeout=1.5) + Connection_mock.close.assert_awaited_once_with(timeout=1.5) + + +@pytest.mark.asyncio +async def test__call_success() -> None: + with create_temp_files([TEST_SSLCERT, TEST_SSLKEY, TEST_SSLROOTCERT]): + test_ssl_context = create_ssl_context("verify-full", TEST_SSLCERT, TEST_SSLKEY, TEST_SSLROOTCERT) + health_check = PostgreSQLAsyncPGHealthCheck( + host="localhost2", + port=6432, + user="user", + password="password", + database="db", + ssl=test_ssl_context, + direct_tls=True, + timeout=1.5, + name="test", + ) + Connection_mock = MagicMock(spec=Connection) # noqa: N806 + Connection_mock.is_closed.return_value = False + Connection_mock.fetchval.return_value = 1 + with patch( + "fast_healthchecks.checks.postgresql.asyncpg.asyncpg.connect", + return_value=Connection_mock, + ) as asyncpg_connect_mock: + result = await health_check() + assert result.healthy is True + assert result.name == "test" + asyncpg_connect_mock.assert_called_once_with( + host="localhost2", + port=6432, + user="user", + password="password", + database="db", + timeout=1.5, + ssl=test_ssl_context, + direct_tls=True, + ) + Connection_mock.transaction.assert_called_once_with(readonly=True) + Connection_mock.fetchval.assert_called_once_with("SELECT 1") + Connection_mock.is_closed.assert_called_once_with() + Connection_mock.close.assert_called_once_with(timeout=1.5) + + +@pytest.mark.asyncio +async def test__call_failure() -> None: + with create_temp_files([TEST_SSLCERT, TEST_SSLKEY, TEST_SSLROOTCERT]): + test_ssl_context = create_ssl_context("verify-full", TEST_SSLCERT, TEST_SSLKEY, TEST_SSLROOTCERT) + health_check = PostgreSQLAsyncPGHealthCheck( + host="localhost2", + port=6432, + user="user", + password="password", + database="db", + ssl=test_ssl_context, + direct_tls=True, + timeout=1.5, + name="test", + ) + Connection_mock = MagicMock(spec=Connection) # noqa: N806 + Connection_mock.is_closed.return_value = False + Connection_mock.fetchval.side_effect = Exception("Database error") + with patch( + "fast_healthchecks.checks.postgresql.asyncpg.asyncpg.connect", + return_value=Connection_mock, + ) as asyncpg_connect_mock: + result = await health_check() + assert result.healthy is False + assert result.name == "test" + assert "Database error" in result.error_details + asyncpg_connect_mock.assert_called_once_with( + host="localhost2", + port=6432, + user="user", + password="password", + database="db", + timeout=1.5, + ssl=test_ssl_context, + direct_tls=True, + ) + Connection_mock.transaction.assert_called_once_with(readonly=True) + Connection_mock.fetchval.assert_called_once_with("SELECT 1") + Connection_mock.is_closed.assert_called_once_with() + Connection_mock.close.assert_called_once_with(timeout=1.5) diff --git a/tests/unit/checks/postgresql/test_psycopg.py b/tests/unit/checks/postgresql/test_psycopg.py new file mode 100644 index 0000000..0da97ec --- /dev/null +++ b/tests/unit/checks/postgresql/test_psycopg.py @@ -0,0 +1,1290 @@ +import ssl +from typing import Any +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch +from urllib.parse import ParseResult, unquote, urlparse + +import pytest +from psycopg.connection_async import AsyncConnection, AsyncCursor + +from fast_healthchecks.checks.postgresql.base import create_ssl_context +from fast_healthchecks.checks.postgresql.psycopg import PostgreSQLPsycopgHealthCheck +from tests.utils import ( + TEST_SSLCERT, + TEST_SSLKEY, + TEST_SSLROOTCERT, + create_temp_files, +) + +pytestmark = pytest.mark.unit + + +def to_dict(obj: PostgreSQLPsycopgHealthCheck) -> dict[str, Any]: + return { + "host": obj._host, + "port": obj._port, + "user": obj._user, + "password": obj._password, + "database": obj._database, + "sslmode": obj._sslmode, + "sslcert": obj._sslcert, + "sslkey": obj._sslkey, + "sslrootcert": obj._sslrootcert, + "timeout": obj._timeout, + "name": obj._name, + } + + +@pytest.mark.parametrize( + ("params", "expected", "exception"), + [ + ( + {}, + "missing 2 required keyword-only arguments: 'host' and 'port'", + TypeError, + ), + ( + { + "host": "localhost", + }, + "missing 1 required keyword-only argument: 'port'", + TypeError, + ), + ( + { + "host": "localhost", + "port": 5432, + }, + { + "host": "localhost", + "port": 5432, + "user": None, + "password": None, + "database": None, + "sslmode": None, + "sslcert": None, + "sslkey": None, + "sslrootcert": None, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": None, + "database": None, + "sslmode": None, + "sslcert": None, + "sslkey": None, + "sslrootcert": None, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": None, + "sslmode": None, + "sslcert": None, + "sslkey": None, + "sslrootcert": None, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": None, + "sslcert": None, + "sslkey": None, + "sslrootcert": None, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "disable", + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "disable", + "sslcert": None, + "sslkey": None, + "sslrootcert": None, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "disable", + "sslcert": TEST_SSLCERT, + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "disable", + "sslcert": TEST_SSLCERT, + "sslkey": None, + "sslrootcert": None, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "disable", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "disable", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslrootcert": None, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "disable", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslrootcert": TEST_SSLROOTCERT, + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "disable", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslrootcert": TEST_SSLROOTCERT, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "allow", + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "allow", + "sslcert": None, + "sslkey": None, + "sslrootcert": None, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "allow", + "sslcert": TEST_SSLCERT, + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "allow", + "sslcert": TEST_SSLCERT, + "sslkey": None, + "sslrootcert": None, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "allow", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "allow", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslrootcert": None, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "allow", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslrootcert": TEST_SSLROOTCERT, + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "allow", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslrootcert": TEST_SSLROOTCERT, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "prefer", + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "prefer", + "sslcert": None, + "sslkey": None, + "sslrootcert": None, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "prefer", + "sslcert": TEST_SSLCERT, + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "prefer", + "sslcert": TEST_SSLCERT, + "sslkey": None, + "sslrootcert": None, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "prefer", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "prefer", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslrootcert": None, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "prefer", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslrootcert": TEST_SSLROOTCERT, + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "prefer", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslrootcert": TEST_SSLROOTCERT, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "require", + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "require", + "sslcert": None, + "sslkey": None, + "sslrootcert": None, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "require", + "sslcert": TEST_SSLCERT, + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "require", + "sslcert": TEST_SSLCERT, + "sslkey": None, + "sslrootcert": None, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "require", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "require", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslrootcert": None, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "require", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslrootcert": TEST_SSLROOTCERT, + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "require", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslrootcert": TEST_SSLROOTCERT, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "verify-ca", + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "verify-ca", + "sslcert": None, + "sslkey": None, + "sslrootcert": None, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "verify-ca", + "sslcert": TEST_SSLCERT, + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "verify-ca", + "sslcert": TEST_SSLCERT, + "sslkey": None, + "sslrootcert": None, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "verify-ca", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "verify-ca", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslrootcert": None, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "verify-ca", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslrootcert": TEST_SSLROOTCERT, + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "verify-ca", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslrootcert": TEST_SSLROOTCERT, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "verify-full", + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "verify-full", + "sslcert": None, + "sslkey": None, + "sslrootcert": None, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "verify-full", + "sslcert": TEST_SSLCERT, + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "verify-full", + "sslcert": TEST_SSLCERT, + "sslkey": None, + "sslrootcert": None, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "verify-full", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "verify-full", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslrootcert": None, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "verify-full", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslrootcert": TEST_SSLROOTCERT, + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "verify-full", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslrootcert": TEST_SSLROOTCERT, + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "verify-full", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslrootcert": TEST_SSLROOTCERT, + "timeout": 10.0, + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "verify-full", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslrootcert": TEST_SSLROOTCERT, + "timeout": 10.0, + "name": "PostgreSQL", + }, + None, + ), + ( + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "verify-full", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslrootcert": TEST_SSLROOTCERT, + "timeout": 10.0, + "name": "test", + }, + { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "pass", + "database": "db", + "sslmode": "verify-full", + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslrootcert": TEST_SSLROOTCERT, + "timeout": 10.0, + "name": "test", + }, + None, + ), + ], +) +def test__init(params: dict[str, Any], expected: dict[str, Any] | str, exception: type[BaseException] | None) -> None: + if exception is not None: + with pytest.raises(exception, match=expected): + PostgreSQLPsycopgHealthCheck(**params) + else: + obj = PostgreSQLPsycopgHealthCheck(**params) + assert to_dict(obj) == expected + + +@pytest.mark.parametrize( + ("args", "kwargs", "expected", "exception"), + [ + ( + ("postgresql+psycopg://postgres:pass@localhost:5432/db?sslmode=broken",), + {}, + "Invalid sslmode: broken", + ValueError, + ), + ( + ("postgresql+psycopg://postgres:pass@localhost:5432/db?sslmode=disable",), + {}, + { + "database": "db", + "host": "localhost", + "password": "pass", + "port": 5432, + "sslcert": None, + "sslkey": None, + "sslmode": "disable", + "sslrootcert": None, + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + (f"postgresql+psycopg://postgres:pass@localhost:5432/db?sslmode=disable&sslcert={TEST_SSLCERT}",), + {}, + { + "database": "db", + "host": "localhost", + "password": "pass", + "port": 5432, + "sslcert": unquote(TEST_SSLCERT), + "sslkey": None, + "sslmode": "disable", + "sslrootcert": None, + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ( + f"postgresql+psycopg://postgres:pass@localhost:5432/db?sslmode=disable&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}", + ), + {}, + { + "database": "db", + "host": "localhost", + "password": "pass", + "port": 5432, + "sslcert": unquote(TEST_SSLCERT), + "sslkey": unquote(TEST_SSLKEY), + "sslmode": "disable", + "sslrootcert": None, + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ( + f"postgresql+psycopg://postgres:pass@localhost:5432/db?sslmode=disable&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}&sslrootcert={TEST_SSLROOTCERT}", + ), + {}, + { + "database": "db", + "host": "localhost", + "password": "pass", + "port": 5432, + "sslcert": unquote(TEST_SSLCERT), + "sslkey": unquote(TEST_SSLKEY), + "sslmode": "disable", + "sslrootcert": unquote(TEST_SSLROOTCERT), + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ("postgresql+psycopg://postgres:pass@localhost:5432/db?sslmode=allow",), + {}, + { + "database": "db", + "host": "localhost", + "password": "pass", + "port": 5432, + "sslcert": None, + "sslkey": None, + "sslmode": "allow", + "sslrootcert": None, + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + (f"postgresql+psycopg://postgres:pass@localhost:5432/db?sslmode=allow&sslcert={TEST_SSLCERT}",), + {}, + { + "database": "db", + "host": "localhost", + "password": "pass", + "port": 5432, + "sslcert": TEST_SSLCERT, + "sslkey": None, + "sslmode": "allow", + "sslrootcert": None, + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ( + f"postgresql+psycopg://postgres:pass@localhost:5432/db?sslmode=allow&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}", + ), + {}, + { + "database": "db", + "host": "localhost", + "password": "pass", + "port": 5432, + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslmode": "allow", + "sslrootcert": None, + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ( + f"postgresql+psycopg://postgres:pass@localhost:5432/db?sslmode=allow&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}&sslrootcert={TEST_SSLROOTCERT}", + ), + {}, + { + "database": "db", + "host": "localhost", + "password": "pass", + "port": 5432, + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslmode": "allow", + "sslrootcert": TEST_SSLROOTCERT, + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ("postgresql+psycopg://postgres:pass@localhost:5432/db?sslmode=prefer",), + {}, + { + "database": "db", + "host": "localhost", + "password": "pass", + "port": 5432, + "sslcert": None, + "sslkey": None, + "sslmode": "prefer", + "sslrootcert": None, + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + (f"postgresql+psycopg://postgres:pass@localhost:5432/db?sslmode=prefer&sslcert={TEST_SSLCERT}",), + {}, + { + "database": "db", + "host": "localhost", + "password": "pass", + "port": 5432, + "sslcert": TEST_SSLCERT, + "sslkey": None, + "sslmode": "prefer", + "sslrootcert": None, + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ( + f"postgresql+psycopg://postgres:pass@localhost:5432/db?sslmode=prefer&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}", + ), + {}, + { + "database": "db", + "host": "localhost", + "password": "pass", + "port": 5432, + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslmode": "prefer", + "sslrootcert": None, + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ( + f"postgresql+psycopg://postgres:pass@localhost:5432/db?sslmode=prefer&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}&sslrootcert={TEST_SSLROOTCERT}", + ), + {}, + { + "database": "db", + "host": "localhost", + "password": "pass", + "port": 5432, + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslmode": "prefer", + "sslrootcert": TEST_SSLROOTCERT, + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ("postgresql+psycopg://postgres:pass@localhost:5432/db?sslmode=verify-full",), + {}, + "sslcert is required for verify-full", + ValueError, + ), + ( + (f"postgresql+psycopg://postgres:pass@localhost:5432/db?sslmode=verify-full&sslcert={TEST_SSLCERT}",), + {}, + "\\[SSL\\] PEM lib \\(_ssl.c:\\d+\\)", + ssl.SSLError, + ), + ( + ( + f"postgresql+psycopg://postgres:pass@localhost:5432/db?sslmode=verify-full&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}", + ), + {}, + { + "database": "db", + "host": "localhost", + "password": "pass", + "port": 5432, + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslmode": "verify-full", + "sslrootcert": None, + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ( + f"postgresql+psycopg://postgres:pass@localhost:5432/db?sslmode=verify-full&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}&sslrootcert={TEST_SSLROOTCERT}", + ), + {}, + { + "database": "db", + "host": "localhost", + "password": "pass", + "port": 5432, + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslmode": "verify-full", + "sslrootcert": TEST_SSLROOTCERT, + "user": "postgres", + "timeout": 5.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ( + f"postgresql+psycopg://postgres:pass@localhost:5432/db?sslmode=verify-full&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}&sslrootcert={TEST_SSLROOTCERT}", + ), + { + "timeout": 10.0, + }, + { + "database": "db", + "host": "localhost", + "password": "pass", + "port": 5432, + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslmode": "verify-full", + "sslrootcert": TEST_SSLROOTCERT, + "user": "postgres", + "timeout": 10.0, + "name": "PostgreSQL", + }, + None, + ), + ( + ( + f"postgresql+psycopg://postgres:pass@localhost:5432/db?sslmode=verify-full&sslcert={TEST_SSLCERT}&sslkey={TEST_SSLKEY}&sslrootcert={TEST_SSLROOTCERT}", + ), + { + "timeout": 10.0, + "name": "test", + }, + { + "database": "db", + "host": "localhost", + "password": "pass", + "port": 5432, + "sslcert": TEST_SSLCERT, + "sslkey": TEST_SSLKEY, + "sslmode": "verify-full", + "sslrootcert": TEST_SSLROOTCERT, + "user": "postgres", + "timeout": 10.0, + "name": "test", + }, + None, + ), + ], +) +def test_from_dsn( + args: tuple[Any, ...], + kwargs: dict[str, Any], + expected: dict[str, Any] | str, + exception: type[BaseException] | None, +) -> None: + parse_result: ParseResult = urlparse(args[0]) + query = {k: unquote(v) for k, v in (q.split("=") for q in parse_result.query.split("&"))} + files = [y for x, y in query.items() if x in {"sslcert", "sslkey", "sslrootcert"}] + + if exception is not None: + with pytest.raises(exception, match=expected), create_temp_files(files): + PostgreSQLPsycopgHealthCheck.from_dsn(*args, **kwargs) + else: + with create_temp_files(files): + check = PostgreSQLPsycopgHealthCheck.from_dsn(*args, **kwargs) + if "ssl" in expected and expected["ssl"] is not None: + expected["ssl"] = create_ssl_context(*expected["ssl"]) + assert to_dict(check) == expected + + +@pytest.mark.asyncio +async def test_psycopg_AsyncConnection_connect_args_kwargs() -> None: # noqa: N802 + with create_temp_files([TEST_SSLCERT, TEST_SSLKEY, TEST_SSLROOTCERT]): + health_check = PostgreSQLPsycopgHealthCheck( + host="localhost2", + port=6432, + user="user", + password="password", + database="db", + sslmode="verify-full", + sslcert=TEST_SSLCERT, + sslkey=TEST_SSLKEY, + sslrootcert=TEST_SSLROOTCERT, + timeout=1.5, + name="test", + ) + AsyncConnection_mock = MagicMock(spec=AsyncConnection) # noqa: N806 + closed = PropertyMock(return_value=False) + type(AsyncConnection_mock).closed = closed + AsyncCursor_mock = MagicMock(spec=AsyncCursor) # noqa: N806 + AsyncConnection_mock.cursor.return_value = AsyncCursor_mock + AsyncCursor_mock.__aenter__.return_value = AsyncCursor_mock + with patch( + "fast_healthchecks.checks.postgresql.psycopg.psycopg.AsyncConnection.connect", + return_value=AsyncConnection_mock, + ) as asyncpg_connect_mock: + await health_check() + asyncpg_connect_mock.assert_called_once_with( + host="localhost2", + port=6432, + user="user", + password="password", + dbname="db", + sslmode="verify-full", + sslcert=TEST_SSLCERT, + sslkey=TEST_SSLKEY, + sslrootcert=TEST_SSLROOTCERT, + ) + asyncpg_connect_mock.assert_awaited_once_with( + host="localhost2", + port=6432, + user="user", + password="password", + dbname="db", + sslmode="verify-full", + sslcert=TEST_SSLCERT, + sslkey=TEST_SSLKEY, + sslrootcert=TEST_SSLROOTCERT, + ) + AsyncConnection_mock.cursor.assert_called_once_with() + AsyncCursor_mock.execute.assert_called_once_with("SELECT 1") + AsyncCursor_mock.execute.assert_awaited_once_with("SELECT 1") + AsyncCursor_mock.fetchone.assert_called_once_with() + AsyncCursor_mock.fetchone.assert_awaited_once_with() + closed.assert_called_once_with() + AsyncConnection_mock.cancel_safe.assert_called_once_with(timeout=1.5) + AsyncConnection_mock.cancel_safe.assert_awaited_once_with(timeout=1.5) + AsyncConnection_mock.close.assert_called_once_with() + AsyncConnection_mock.close.assert_awaited_once_with() + + +@pytest.mark.asyncio +async def test__call_success() -> None: + with create_temp_files([TEST_SSLCERT, TEST_SSLKEY, TEST_SSLROOTCERT]): + health_check = PostgreSQLPsycopgHealthCheck( + host="localhost", + port=5432, + user="user", + password="password", + database="db", + sslmode="verify-full", + sslcert=TEST_SSLCERT, + sslkey=TEST_SSLKEY, + sslrootcert=TEST_SSLROOTCERT, + timeout=1.5, + name="test", + ) + AsyncConnection_mock = AsyncMock(spec=AsyncConnection) # noqa: N806 + AsyncCursor_mock = AsyncMock(spec=AsyncCursor) # noqa: N806 + AsyncConnection_mock.cursor.return_value.__aenter__.return_value = AsyncCursor_mock + AsyncCursor_mock.execute.return_value = None + AsyncCursor_mock.fetchone.return_value = (1,) + + with patch("psycopg.AsyncConnection.connect", return_value=AsyncConnection_mock): + result = await health_check() + assert result.healthy is True + assert result.name == "test" + assert result.error_details is None + + +@pytest.mark.asyncio +async def test__call_failure() -> None: + with create_temp_files([TEST_SSLCERT, TEST_SSLKEY, TEST_SSLROOTCERT]): + health_check = PostgreSQLPsycopgHealthCheck( + host="localhost", + port=5432, + user="user", + password="password", + database="db", + sslmode="verify-full", + sslcert=TEST_SSLCERT, + sslkey=TEST_SSLKEY, + sslrootcert=TEST_SSLROOTCERT, + timeout=1.5, + name="test", + ) + AsyncConnection_mock = AsyncMock(spec=AsyncConnection) # noqa: N806 + AsyncCursor_mock = AsyncMock(spec=AsyncCursor) # noqa: N806 + AsyncConnection_mock.cursor.return_value.__aenter__.return_value = AsyncCursor_mock + AsyncCursor_mock.execute.side_effect = Exception("Database error") + + with patch("psycopg.AsyncConnection.connect", return_value=AsyncConnection_mock): + result = await health_check() + assert result.healthy is False + assert result.name == "test" + assert "Database error" in result.error_details diff --git a/tests/unit/checks/test__base.py b/tests/unit/checks/test__base.py new file mode 100644 index 0000000..96f295e --- /dev/null +++ b/tests/unit/checks/test__base.py @@ -0,0 +1,73 @@ +from unittest.mock import patch + +import pytest +from pydantic import AmqpDsn, KafkaDsn, MongoDsn, PostgresDsn, RedisDsn, ValidationError + +from fast_healthchecks.checks._base import HealthCheckDSN, SupportedDsns # noqa: PLC2701 +from fast_healthchecks.compat import PYDANTIC_V2 +from fast_healthchecks.models import HealthCheckResult + +pytestmark = pytest.mark.unit + + +class DummyCheck(HealthCheckDSN[HealthCheckResult]): + async def __call__(self) -> HealthCheckResult: + return HealthCheckResult(name="dummy", healthy=True) + + +def test_check_pydantinc_installed() -> None: + assert DummyCheck.check_pydantinc_installed() is None + + with ( + patch("fast_healthchecks.checks._base.PYDANTIC_INSTALLED", new=False), + pytest.raises(RuntimeError, match="Pydantic is not installed"), + ): + DummyCheck.check_pydantinc_installed() + + +@pytest.mark.parametrize( + ("dsn", "dsn_type", "expected", "pydantic_installed", "exception"), + [ + ("amqp://user:pass@host:10000/vhost", AmqpDsn, "amqp://user:pass@host:10000/vhost", True, None), + ("kafka://user:pass@host:10000/topic", KafkaDsn, "kafka://user:pass@host:10000/topic", True, None), + ("mongodb://user:pass@host:10000/db", MongoDsn, "mongodb://user:pass@host:10000/db", True, None), + ("postgresql://user:pass@host:10000/db", PostgresDsn, "postgresql://user:pass@host:10000/db", True, None), + ("redis://user:pass@host:10000/0", RedisDsn, "redis://user:pass@host:10000/0", True, None), + ("1", int, "1", True, None), + ( + "a", + int, + "validation error for int" if PYDANTIC_V2 else "value is not a valid integer", + True, + ValidationError, + ), + ("a", int, "invalid literal for int()", False, ValueError), + (1, int, "1", True, None), + ], +) +def test_validate_dsn( + dsn: str, + dsn_type: SupportedDsns, + expected: str, + pydantic_installed: bool, # noqa: FBT001 + exception: type[BaseException] | None, +) -> None: + with patch("fast_healthchecks.checks._base.PYDANTIC_INSTALLED", new=pydantic_installed): + if exception is not None: + with pytest.raises(exception, match=expected): + DummyCheck.validate_dsn(dsn, dsn_type) + else: + assert DummyCheck.validate_dsn(dsn, dsn_type) == expected + + +def test_validate_dsn_without_pydantic() -> None: + with patch("fast_healthchecks.checks._base.PYDANTIC_INSTALLED", new=False): + AmqpDsn = str # noqa: N806 + assert ( + DummyCheck.validate_dsn("amqp://user:pass@host:10000/vhost", AmqpDsn) == "amqp://user:pass@host:10000/vhost" + ) + + +def test_from_dsn_not_implemented() -> None: + with pytest.raises(NotImplementedError): + DummyCheck.from_dsn("amqp://user:pass@host:10000/vhost") diff --git a/tests/unit/checks/test_function.py b/tests/unit/checks/test_function.py new file mode 100644 index 0000000..97bf7b8 --- /dev/null +++ b/tests/unit/checks/test_function.py @@ -0,0 +1,61 @@ +import asyncio +import time + +import pytest + +from fast_healthchecks.checks.function import FunctionHealthCheck +from fast_healthchecks.models import HealthCheckResult + +pytestmark = pytest.mark.unit + + +def dummy_sync_function(arg: str, kwarg: int = 1) -> None: # noqa: ARG001 + time.sleep(0.1) + + +def dummy_sync_function_fail(arg: str, kwarg: int = 1) -> None: # noqa: ARG001 + time.sleep(0.1) + msg = "Test exception" + raise Exception(msg) from None # noqa: TRY002 + + +async def dummy_async_function(arg: str, kwarg: int = 1) -> None: # noqa: ARG001 + await asyncio.sleep(0.1) + + +async def dummy_async_function_fail(arg: str, kwarg: int = 1) -> None: # noqa: ARG001 + await asyncio.sleep(0.1) + msg = "Test exception" + raise Exception(msg) from None # noqa: TRY002 + + +@pytest.mark.asyncio +async def test_sync_function_success() -> None: + check = FunctionHealthCheck(func=dummy_sync_function, args=("arg",), kwargs={"kwarg": 2}, timeout=0.2) + result = await check() + assert result == HealthCheckResult(name="Function", healthy=True, error_details=None) + + +@pytest.mark.asyncio +async def test_sync_function_failure() -> None: + check = FunctionHealthCheck(func=dummy_sync_function_fail, args=("arg",), kwargs={"kwarg": 2}, timeout=0.2) + result = await check() + assert result.healthy is False + assert result.error_details is not None + assert "Test exception" in result.error_details + + +@pytest.mark.asyncio +async def test_async_function_success() -> None: + check = FunctionHealthCheck(func=dummy_async_function, args=("arg",), kwargs={"kwarg": 2}, timeout=0.2) + result = await check() + assert result == HealthCheckResult(name="Function", healthy=True, error_details=None) + + +@pytest.mark.asyncio +async def test_async_function_failure() -> None: + check = FunctionHealthCheck(func=dummy_async_function_fail, args=("arg",), kwargs={"kwarg": 2}, timeout=0.2) + result = await check() + assert result.healthy is False + assert result.error_details is not None + assert "Test exception" in result.error_details diff --git a/tests/unit/checks/test_kafka.py b/tests/unit/checks/test_kafka.py new file mode 100644 index 0000000..71b3b5e --- /dev/null +++ b/tests/unit/checks/test_kafka.py @@ -0,0 +1,411 @@ +import ssl +from typing import Any +from unittest.mock import patch + +import pytest +from aiokafka import AIOKafkaClient + +from fast_healthchecks.checks.kafka import KafkaHealthCheck + +pytestmark = pytest.mark.unit + +test_ssl_context = ssl.create_default_context() + + +def to_dict(obj: KafkaHealthCheck) -> dict[str, Any]: + return { + "bootstrap_servers": obj._bootstrap_servers, + "ssl_context": obj._ssl_context, + "security_protocol": obj._security_protocol, + "sasl_mechanism": obj._sasl_mechanism, + "sasl_plain_username": obj._sasl_plain_username, + "sasl_plain_password": obj._sasl_plain_password, + "timeout": obj._timeout, + "name": obj._name, + } + + +@pytest.mark.parametrize( + ("params", "expected", "exception"), + [ + ( + {}, + "missing 1 required keyword-only argument: 'bootstrap_servers'", + TypeError, + ), + ( + { + "bootstrap_servers": "localhost:9092", + }, + { + "bootstrap_servers": "localhost:9092", + "ssl_context": None, + "security_protocol": "PLAINTEXT", + "sasl_mechanism": "PLAIN", + "sasl_plain_username": None, + "sasl_plain_password": None, + "timeout": 5.0, + "name": "Kafka", + }, + None, + ), + ( + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + }, + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "PLAINTEXT", + "sasl_mechanism": "PLAIN", + "sasl_plain_username": None, + "sasl_plain_password": None, + "timeout": 5.0, + "name": "Kafka", + }, + None, + ), + ( + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "BROKEN", + }, + "Invalid security protocol: BROKEN", + ValueError, + ), + ( + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SSL", + }, + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SSL", + "sasl_mechanism": "PLAIN", + "sasl_plain_username": None, + "sasl_plain_password": None, + "timeout": 5.0, + "name": "Kafka", + }, + None, + ), + ( + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "PLAINTEXT", + }, + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "PLAINTEXT", + "sasl_mechanism": "PLAIN", + "sasl_plain_username": None, + "sasl_plain_password": None, + "timeout": 5.0, + "name": "Kafka", + }, + None, + ), + ( + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_PLAINTEXT", + }, + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_PLAINTEXT", + "sasl_mechanism": "PLAIN", + "sasl_plain_username": None, + "sasl_plain_password": None, + "timeout": 5.0, + "name": "Kafka", + }, + None, + ), + ( + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_SSL", + }, + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_SSL", + "sasl_mechanism": "PLAIN", + "sasl_plain_username": None, + "sasl_plain_password": None, + "timeout": 5.0, + "name": "Kafka", + }, + None, + ), + ( + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_SSL", + "sasl_mechanism": "BROKEN", + }, + "Invalid SASL mechanism: BROKEN", + ValueError, + ), + ( + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_SSL", + "sasl_mechanism": "PLAIN", + }, + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_SSL", + "sasl_mechanism": "PLAIN", + "sasl_plain_username": None, + "sasl_plain_password": None, + "timeout": 5.0, + "name": "Kafka", + }, + None, + ), + ( + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_SSL", + "sasl_mechanism": "GSSAPI", + }, + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_SSL", + "sasl_mechanism": "GSSAPI", + "sasl_plain_username": None, + "sasl_plain_password": None, + "timeout": 5.0, + "name": "Kafka", + }, + None, + ), + ( + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_SSL", + "sasl_mechanism": "SCRAM-SHA-256", + }, + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_SSL", + "sasl_mechanism": "SCRAM-SHA-256", + "sasl_plain_username": None, + "sasl_plain_password": None, + "timeout": 5.0, + "name": "Kafka", + }, + None, + ), + ( + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_SSL", + "sasl_mechanism": "SCRAM-SHA-512", + }, + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_SSL", + "sasl_mechanism": "SCRAM-SHA-512", + "sasl_plain_username": None, + "sasl_plain_password": None, + "timeout": 5.0, + "name": "Kafka", + }, + None, + ), + ( + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_SSL", + "sasl_mechanism": "OAUTHBEARER", + }, + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_SSL", + "sasl_mechanism": "OAUTHBEARER", + "sasl_plain_username": None, + "sasl_plain_password": None, + "timeout": 5.0, + "name": "Kafka", + }, + None, + ), + ( + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_SSL", + "sasl_mechanism": "OAUTHBEARER", + "sasl_plain_username": "user", + }, + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_SSL", + "sasl_mechanism": "OAUTHBEARER", + "sasl_plain_username": "user", + "sasl_plain_password": None, + "timeout": 5.0, + "name": "Kafka", + }, + None, + ), + ( + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_SSL", + "sasl_mechanism": "OAUTHBEARER", + "sasl_plain_username": "user", + "sasl_plain_password": "password", + }, + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_SSL", + "sasl_mechanism": "OAUTHBEARER", + "sasl_plain_username": "user", + "sasl_plain_password": "password", + "timeout": 5.0, + "name": "Kafka", + }, + None, + ), + ( + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_SSL", + "sasl_mechanism": "OAUTHBEARER", + "sasl_plain_username": "user", + "sasl_plain_password": "password", + "timeout": 1.5, + }, + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_SSL", + "sasl_mechanism": "OAUTHBEARER", + "sasl_plain_username": "user", + "sasl_plain_password": "password", + "timeout": 1.5, + "name": "Kafka", + }, + None, + ), + ( + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_SSL", + "sasl_mechanism": "OAUTHBEARER", + "sasl_plain_username": "user", + "sasl_plain_password": "password", + "timeout": 1.5, + "name": "Test", + }, + { + "bootstrap_servers": "localhost:9092", + "ssl_context": test_ssl_context, + "security_protocol": "SASL_SSL", + "sasl_mechanism": "OAUTHBEARER", + "sasl_plain_username": "user", + "sasl_plain_password": "password", + "timeout": 1.5, + "name": "Test", + }, + None, + ), + ], +) +def test__init(params: dict[str, Any], expected: dict[str, Any] | str, exception: type[BaseException] | None) -> None: + if exception is not None: + with pytest.raises(exception, match=expected): + KafkaHealthCheck(**params) + else: + obj = KafkaHealthCheck(**params) + assert to_dict(obj) == expected + + +@pytest.mark.asyncio +async def test_AIOKafkaClient_args_kwargs() -> None: # noqa: N802 + health_check = KafkaHealthCheck( + bootstrap_servers="localhost:9092", + ssl_context=test_ssl_context, + security_protocol="SASL_SSL", + sasl_mechanism="OAUTHBEARER", + sasl_plain_username="user", + sasl_plain_password="password", + timeout=1.5, + ) + with patch("fast_healthchecks.checks.kafka.AIOKafkaClient", spec=AIOKafkaClient) as mock: + await health_check() + mock.assert_called_once_with( + bootstrap_servers="localhost:9092", + client_id="fast_healthchecks", + request_timeout_ms=1.5 * 1000, + ssl_context=test_ssl_context, + security_protocol="SASL_SSL", + sasl_mechanism="OAUTHBEARER", + sasl_plain_username="user", + sasl_plain_password="password", + ) + + +@pytest.mark.asyncio +async def test__call_success() -> None: + health_check = KafkaHealthCheck(bootstrap_servers="localhost:9092") + with ( + patch.object(AIOKafkaClient, "bootstrap", return_value=None) as mock_bootstrap, + patch.object(AIOKafkaClient, "check_version", return_value=None) as mock_check_version, + patch.object(AIOKafkaClient, "close", return_value=None) as mock_close, + ): + result = await health_check() + assert result.healthy is True + assert result.name == "Kafka" + assert result.error_details is None + mock_bootstrap.assert_called_once_with() + mock_bootstrap.assert_awaited_once_with() + mock_check_version.assert_called_once_with() + mock_check_version.assert_awaited_once_with() + mock_close.assert_called_once_with() + mock_close.assert_awaited_once_with() + + +@pytest.mark.asyncio +async def test__call_failure() -> None: + health_check = KafkaHealthCheck(bootstrap_servers="localhost:9092") + with ( + patch.object(AIOKafkaClient, "bootstrap", side_effect=Exception("Connection error")) as mock_bootstrap, + patch.object(AIOKafkaClient, "close", return_value=None) as mock_close, + ): + result = await health_check() + assert result.healthy is False + assert result.name == "Kafka" + assert "Connection error" in result.error_details + mock_bootstrap.assert_called_once_with() + mock_bootstrap.assert_awaited_once_with() + mock_close.assert_called_once_with() + mock_close.assert_awaited_once_with() diff --git a/tests/unit/checks/test_mongo.py b/tests/unit/checks/test_mongo.py new file mode 100644 index 0000000..60ba7d2 --- /dev/null +++ b/tests/unit/checks/test_mongo.py @@ -0,0 +1,383 @@ +from typing import Any +from unittest.mock import AsyncMock, patch + +import pytest +from motor.motor_asyncio import AsyncIOMotorClient + +from fast_healthchecks.checks.mongo import MongoHealthCheck + +pytestmark = pytest.mark.unit + + +def to_dict(obj: MongoHealthCheck) -> dict[str, Any]: + return { + "host": obj._host, + "port": obj._port, + "user": obj._user, + "password": obj._password, + "database": obj._database, + "auth_source": obj._auth_source, + "timeout": obj._timeout, + "name": obj._name, + } + + +@pytest.mark.parametrize( + ("params", "expected", "exception"), + [ + ( + {}, + { + "host": "localhost", + "port": 27017, + "user": None, + "password": None, + "database": None, + "auth_source": "admin", + "timeout": 5.0, + "name": "MongoDB", + }, + None, + ), + ( + { + "host": "localhost2", + }, + { + "host": "localhost2", + "port": 27017, + "user": None, + "password": None, + "database": None, + "auth_source": "admin", + "timeout": 5.0, + "name": "MongoDB", + }, + None, + ), + ( + { + "host": "localhost2", + "port": 27018, + }, + { + "host": "localhost2", + "port": 27018, + "user": None, + "password": None, + "database": None, + "auth_source": "admin", + "timeout": 5.0, + "name": "MongoDB", + }, + None, + ), + ( + { + "host": "localhost2", + "port": 27018, + "user": "user", + }, + { + "host": "localhost2", + "port": 27018, + "user": "user", + "password": None, + "database": None, + "auth_source": "admin", + "timeout": 5.0, + "name": "MongoDB", + }, + None, + ), + ( + { + "host": "localhost2", + "port": 27018, + "user": "user", + "password": "pass", + }, + { + "host": "localhost2", + "port": 27018, + "user": "user", + "password": "pass", + "database": None, + "auth_source": "admin", + "timeout": 5.0, + "name": "MongoDB", + }, + None, + ), + ( + { + "host": "localhost2", + "port": 27018, + "user": "user", + "password": "pass", + "database": "test", + }, + { + "host": "localhost2", + "port": 27018, + "user": "user", + "password": "pass", + "database": "test", + "auth_source": "admin", + "timeout": 5.0, + "name": "MongoDB", + }, + None, + ), + ( + { + "host": "localhost2", + "port": 27018, + "user": "user", + "password": "pass", + "database": "test", + "auth_source": "admin2", + }, + { + "host": "localhost2", + "port": 27018, + "user": "user", + "password": "pass", + "database": "test", + "auth_source": "admin2", + "timeout": 5.0, + "name": "MongoDB", + }, + None, + ), + ( + { + "host": "localhost2", + "port": 27018, + "user": "user", + "password": "pass", + "database": "test", + "auth_source": "admin2", + "timeout": 10.0, + }, + { + "host": "localhost2", + "port": 27018, + "user": "user", + "password": "pass", + "database": "test", + "auth_source": "admin2", + "timeout": 10.0, + "name": "MongoDB", + }, + None, + ), + ( + { + "host": "localhost2", + "port": 27018, + "user": "user", + "password": "pass", + "database": "test", + "auth_source": "admin2", + "timeout": 10.0, + "name": "test", + }, + { + "host": "localhost2", + "port": 27018, + "user": "user", + "password": "pass", + "database": "test", + "auth_source": "admin2", + "timeout": 10.0, + "name": "test", + }, + None, + ), + ], +) +def test_init(params: dict[str, Any], expected: dict[str, Any], exception: type[BaseException] | None) -> None: + if exception is not None: + with pytest.raises(exception, match=str(expected)): + MongoHealthCheck(**params) + else: + obj = MongoHealthCheck(**params) + assert to_dict(obj) == expected + + +@pytest.mark.parametrize( + ("args", "kwargs", "expected", "exception"), + [ + ( + (), + {}, + "missing 1 required positional argument: 'dsn'", + TypeError, + ), + ( + ("mongodb://localhost:27017/",), + {}, + { + "host": "localhost", + "port": 27017, + "user": None, + "password": None, + "database": None, + "auth_source": "admin", + "timeout": 5.0, + "name": "MongoDB", + }, + None, + ), + ( + ("mongodb://localhost:27017/test",), + {}, + { + "host": "localhost", + "port": 27017, + "user": None, + "password": None, + "database": "test", + "auth_source": "admin", + "timeout": 5.0, + "name": "MongoDB", + }, + None, + ), + ( + ("mongodb://user:pass@localhost:27017/test",), + {}, + { + "host": "localhost", + "port": 27017, + "user": "user", + "password": "pass", + "database": "test", + "auth_source": "admin", + "timeout": 5.0, + "name": "MongoDB", + }, + None, + ), + ( + ("mongodb://user:pass@localhost:27017/test?authSource=admin2",), + {}, + { + "host": "localhost", + "port": 27017, + "user": "user", + "password": "pass", + "database": "test", + "auth_source": "admin2", + "timeout": 5.0, + "name": "MongoDB", + }, + None, + ), + ( + ("mongodb://user:pass@localhost:27017/test?authSource=admin2",), + { + "timeout": 10.0, + "name": "Test", + }, + { + "host": "localhost", + "port": 27017, + "user": "user", + "password": "pass", + "database": "test", + "auth_source": "admin2", + "timeout": 10.0, + "name": "Test", + }, + None, + ), + ], +) +def test_from_dsn( + args: tuple[Any, ...], + kwargs: dict[str, Any], + expected: dict[str, Any] | str, + exception: type[BaseException] | None, +) -> None: + if exception is not None: + with pytest.raises(exception, match=expected): + MongoHealthCheck.from_dsn(*args, **kwargs) + else: + obj = MongoHealthCheck.from_dsn(*args, **kwargs) + assert to_dict(obj) == expected + + +@pytest.mark.asyncio +async def test_AsyncIOMotorClient_args_kwargs() -> None: # noqa: N802 + health_check = MongoHealthCheck( + host="localhost2", + port=27018, + user="user", + password="password", + database="test", + auth_source="admin2", + timeout=1.5, + name="MongoDB", + ) + with patch("fast_healthchecks.checks.mongo.AsyncIOMotorClient", spec=AsyncIOMotorClient) as mock: + await health_check() + mock.assert_called_once_with( + host="localhost2", + port=27018, + username="user", + password="password", + authSource="admin2", + serverSelectionTimeoutMS=1500, + ) + + +@pytest.mark.asyncio +async def test__call_success() -> None: + health_check = MongoHealthCheck( + host="localhost", + port=27017, + user="user", + password="password", + database="test", + auth_source="admin", + timeout=1.5, + name="MongoDB", + ) + mock_client = AsyncMock(spec=AsyncIOMotorClient) + mock_client["test"].command = AsyncMock() + mock_client["test"].command.side_effect = [{"ok": 1}] + with patch("fast_healthchecks.checks.mongo.AsyncIOMotorClient", return_value=mock_client): + result = await health_check() + assert result.healthy is True + assert result.name == "MongoDB" + assert result.error_details is None + mock_client["test"].command.assert_called_once_with("ping") + mock_client["test"].command.assert_awaited_once_with("ping") + mock_client.close.assert_called_once_with() + + +@pytest.mark.asyncio +async def test__call_failure() -> None: + health_check = MongoHealthCheck( + host="localhost", + port=27017, + user="user", + password="password", + database="test", + auth_source="admin", + timeout=1.5, + name="MongoDB", + ) + mock_client = AsyncMock(spec=AsyncIOMotorClient) + mock_client["test"].command = AsyncMock() + mock_client["test"].command.side_effect = BaseException + with patch("fast_healthchecks.checks.mongo.AsyncIOMotorClient", return_value=mock_client): + result = await health_check() + assert result.healthy is False + assert result.name == "MongoDB" + assert result.error_details is not None + mock_client["test"].command.assert_called_once_with("ping") + mock_client["test"].command.assert_awaited_once_with("ping") + mock_client.close.assert_called_once_with() diff --git a/tests/unit/checks/test_rabbitmq.py b/tests/unit/checks/test_rabbitmq.py new file mode 100644 index 0000000..8546aee --- /dev/null +++ b/tests/unit/checks/test_rabbitmq.py @@ -0,0 +1,367 @@ +from typing import Any +from unittest.mock import AsyncMock, patch + +import pytest + +from fast_healthchecks.checks.rabbitmq import RabbitMQHealthCheck + +pytestmark = pytest.mark.unit + + +def to_dict(obj: RabbitMQHealthCheck) -> dict[str, Any]: + return { + "host": obj._host, + "user": obj._user, + "password": obj._password, + "port": obj._port, + "vhost": obj._vhost, + "secure": obj._secure, + "timeout": obj._timeout, + "name": obj._name, + } + + +@pytest.mark.parametrize( + ("params", "expected", "exception"), + [ + ( + {}, + "missing 3 required keyword-only arguments: 'host', 'user', and 'password'", + TypeError, + ), + ( + { + "host": "localhost", + }, + "missing 2 required keyword-only arguments: 'user' and 'password'", + TypeError, + ), + ( + { + "host": "localhost", + "user": "user", + }, + "missing 1 required keyword-only argument: 'password'", + TypeError, + ), + ( + { + "host": "localhost", + "user": "user", + "password": "password", + }, + { + "host": "localhost", + "user": "user", + "password": "password", + "port": 5672, + "vhost": "/", + "secure": False, + "timeout": 5.0, + "name": "RabbitMQ", + }, + None, + ), + ( + { + "host": "localhost2", + "user": "user", + "password": "password", + "port": 5673, + }, + { + "host": "localhost2", + "user": "user", + "password": "password", + "port": 5673, + "vhost": "/", + "secure": False, + "timeout": 5.0, + "name": "RabbitMQ", + }, + None, + ), + ( + { + "host": "localhost2", + "user": "user", + "password": "password", + "port": 5673, + "vhost": "test", + }, + { + "host": "localhost2", + "user": "user", + "password": "password", + "port": 5673, + "vhost": "test", + "secure": False, + "timeout": 5.0, + "name": "RabbitMQ", + }, + None, + ), + ( + { + "host": "localhost2", + "user": "user", + "password": "password", + "port": 5673, + "vhost": "test", + "secure": True, + }, + { + "host": "localhost2", + "user": "user", + "password": "password", + "port": 5673, + "vhost": "test", + "secure": True, + "timeout": 5.0, + "name": "RabbitMQ", + }, + None, + ), + ( + { + "host": "localhost2", + "user": "user", + "password": "password", + "port": 5673, + "vhost": "test", + "secure": True, + "timeout": 10.0, + }, + { + "host": "localhost2", + "user": "user", + "password": "password", + "port": 5673, + "vhost": "test", + "secure": True, + "timeout": 10.0, + "name": "RabbitMQ", + }, + None, + ), + ( + { + "host": "localhost2", + "user": "user", + "password": "password", + "port": 5673, + "vhost": "test", + "secure": True, + "timeout": 10.0, + "name": "test", + }, + { + "host": "localhost2", + "user": "user", + "password": "password", + "port": 5673, + "vhost": "test", + "secure": True, + "timeout": 10.0, + "name": "test", + }, + None, + ), + ], +) +def test_init(params: dict[str, Any], expected: dict[str, Any], exception: type[BaseException] | None) -> None: + if exception is not None: + with pytest.raises(exception, match=str(expected)): + RabbitMQHealthCheck(**params) + else: + obj = RabbitMQHealthCheck(**params) + assert to_dict(obj) == expected + + +@pytest.mark.parametrize( + ("args", "kwargs", "expected", "exception"), + [ + ( + (), + {}, + "missing 1 required positional argument: 'dsn'", + TypeError, + ), + ( + ("amqp://user:password@localhost/",), + {}, + { + "host": "localhost", + "user": "user", + "password": "password", + "port": 5672, + "vhost": "/", + "secure": False, + "timeout": 5.0, + "name": "RabbitMQ", + }, + None, + ), + ( + ("amqp://user:password@localhost/test",), + {}, + { + "host": "localhost", + "user": "user", + "password": "password", + "port": 5672, + "vhost": "test", + "secure": False, + "timeout": 5.0, + "name": "RabbitMQ", + }, + None, + ), + ( + ("amqp://user:password@localhost:5673/test",), + {}, + { + "host": "localhost", + "user": "user", + "password": "password", + "port": 5673, + "vhost": "test", + "secure": False, + "timeout": 5.0, + "name": "RabbitMQ", + }, + None, + ), + ( + ("amqps://user:password@localhost:5673/test",), + {}, + { + "host": "localhost", + "user": "user", + "password": "password", + "port": 5673, + "vhost": "test", + "secure": True, + "timeout": 5.0, + "name": "RabbitMQ", + }, + None, + ), + ( + ("amqps://user:password@localhost:5673/test",), + { + "timeout": 10.0, + }, + { + "host": "localhost", + "user": "user", + "password": "password", + "port": 5673, + "vhost": "test", + "secure": True, + "timeout": 10.0, + "name": "RabbitMQ", + }, + None, + ), + ( + ("amqps://user:password@localhost:5673/test",), + { + "timeout": 10.0, + "name": "test", + }, + { + "host": "localhost", + "user": "user", + "password": "password", + "port": 5673, + "vhost": "test", + "secure": True, + "timeout": 10.0, + "name": "test", + }, + None, + ), + ], +) +def test_from_dsn( + args: tuple[Any, ...], + kwargs: dict[str, Any], + expected: dict[str, Any] | str, + exception: type[BaseException] | None, +) -> None: + if exception is not None: + with pytest.raises(exception, match=expected): + RabbitMQHealthCheck.from_dsn(*args, **kwargs) + else: + obj = RabbitMQHealthCheck.from_dsn(*args, **kwargs) + assert to_dict(obj) == expected + + +@pytest.mark.asyncio +async def test_call_success() -> None: + health_check = RabbitMQHealthCheck( + host="localhost2", + user="user", + password="password", + port=5673, + vhost="test", + secure=True, + timeout=10.0, + ) + with patch("aio_pika.connect_robust", new_callable=AsyncMock) as mock_connect: + mock_connect.return_value.__aenter__.return_value = AsyncMock() + result = await health_check() + assert result.healthy is True + assert result.name == "RabbitMQ" + mock_connect.assert_called_once_with( + host="localhost2", + port=5673, + login="user", + password="password", + ssl=True, + virtualhost="test", + timeout=10.0, + ) + mock_connect.assert_awaited_once_with( + host="localhost2", + port=5673, + login="user", + password="password", + ssl=True, + virtualhost="test", + timeout=10.0, + ) + + +@pytest.mark.asyncio +async def test_call_failure() -> None: + health_check = RabbitMQHealthCheck( + host="localhost", + user="user", + password="password", + ) + with patch("aio_pika.connect_robust", new_callable=AsyncMock) as mock_connect: + mock_connect.side_effect = Exception("Connection failed") + result = await health_check() + assert result.healthy is False + assert result.name == "RabbitMQ" + assert "Connection failed" in result.error_details + mock_connect.assert_called_once_with( + host="localhost", + port=5672, + login="user", + password="password", + ssl=False, + virtualhost="/", + timeout=5.0, + ) + mock_connect.assert_awaited_once_with( + host="localhost", + port=5672, + login="user", + password="password", + ssl=False, + virtualhost="/", + timeout=5.0, + ) diff --git a/tests/unit/checks/test_redis.py b/tests/unit/checks/test_redis.py new file mode 100644 index 0000000..ea3cf29 --- /dev/null +++ b/tests/unit/checks/test_redis.py @@ -0,0 +1,356 @@ +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from redis.asyncio import Redis + +from fast_healthchecks.checks.redis import RedisHealthCheck + +pytestmark = pytest.mark.unit + + +def to_dict(obj: RedisHealthCheck) -> dict[str, Any]: + return { + "host": obj._host, + "port": obj._port, + "database": obj._database, + "user": obj._user, + "password": obj._password, + "timeout": obj._timeout, + "name": obj._name, + } + + +@pytest.mark.parametrize( + ("params", "expected", "exception"), + [ + ( + {}, + { + "host": "localhost", + "port": 6379, + "database": 0, + "user": None, + "password": None, + "timeout": 5.0, + "name": "Redis", + }, + None, + ), + ( + { + "host": "localhost2", + }, + { + "host": "localhost2", + "port": 6379, + "database": 0, + "user": None, + "password": None, + "timeout": 5.0, + "name": "Redis", + }, + None, + ), + ( + { + "host": "localhost2", + "port": 6380, + }, + { + "host": "localhost2", + "port": 6380, + "database": 0, + "user": None, + "password": None, + "timeout": 5.0, + "name": "Redis", + }, + None, + ), + ( + { + "host": "localhost2", + "port": 6380, + "database": 1, + }, + { + "host": "localhost2", + "port": 6380, + "database": 1, + "user": None, + "password": None, + "timeout": 5.0, + "name": "Redis", + }, + None, + ), + ( + { + "host": "localhost2", + "port": 6380, + "database": "test", + }, + { + "host": "localhost2", + "port": 6380, + "database": "test", + "user": None, + "password": None, + "timeout": 5.0, + "name": "Redis", + }, + None, + ), + ( + { + "host": "localhost2", + "port": 6380, + "database": "test", + "user": "user", + }, + { + "host": "localhost2", + "port": 6380, + "database": "test", + "user": "user", + "password": None, + "timeout": 5.0, + "name": "Redis", + }, + None, + ), + ( + { + "host": "localhost2", + "port": 6380, + "database": "test", + "user": "user", + "password": "pass", + }, + { + "host": "localhost2", + "port": 6380, + "database": "test", + "user": "user", + "password": "pass", + "timeout": 5.0, + "name": "Redis", + }, + None, + ), + ( + { + "host": "localhost2", + "port": 6380, + "database": "test", + "user": "user", + "password": "pass", + "timeout": 10.0, + }, + { + "host": "localhost2", + "port": 6380, + "database": "test", + "user": "user", + "password": "pass", + "timeout": 10.0, + "name": "Redis", + }, + None, + ), + ( + { + "host": "localhost2", + "port": 6380, + "database": "test", + "user": "user", + "password": "pass", + "timeout": 10.0, + "name": "test", + }, + { + "host": "localhost2", + "port": 6380, + "database": "test", + "user": "user", + "password": "pass", + "timeout": 10.0, + "name": "test", + }, + None, + ), + ], +) +def test_init(params: dict[str, Any], expected: dict[str, Any], exception: type[BaseException] | None) -> None: + if exception is not None: + with pytest.raises(exception, match=str(expected)): + RedisHealthCheck(**params) + else: + obj = RedisHealthCheck(**params) + assert to_dict(obj) == expected + + +@pytest.mark.parametrize( + ("args", "kwargs", "expected", "exception"), + [ + ( + ("redis://localhost:6379/",), + {}, + { + "host": "localhost", + "port": 6379, + "database": 0, + "user": None, + "password": None, + "timeout": 5.0, + "name": "Redis", + }, + None, + ), + ( + ("redis://localhost:6379/1",), + {}, + { + "host": "localhost", + "port": 6379, + "database": 1, + "user": None, + "password": None, + "timeout": 5.0, + "name": "Redis", + }, + None, + ), + ( + ("redis://user@localhost:6379/1",), + {}, + { + "host": "localhost", + "port": 6379, + "database": 1, + "user": "user", + "password": None, + "timeout": 5.0, + "name": "Redis", + }, + None, + ), + ( + ("redis://user:pass@localhost:6379/1",), + {}, + { + "host": "localhost", + "port": 6379, + "database": 1, + "user": "user", + "password": "pass", + "timeout": 5.0, + "name": "Redis", + }, + None, + ), + ( + ("redis://user:pass@localhost:6379/1",), + { + "timeout": 10.0, + }, + { + "host": "localhost", + "port": 6379, + "database": 1, + "user": "user", + "password": "pass", + "timeout": 10.0, + "name": "Redis", + }, + None, + ), + ( + ("redis://user:pass@localhost:6379/1",), + { + "timeout": 10.0, + "name": "test", + }, + { + "host": "localhost", + "port": 6379, + "database": 1, + "user": "user", + "password": "pass", + "timeout": 10.0, + "name": "test", + }, + None, + ), + ], +) +def test_from_dsn( + args: tuple[Any, ...], + kwargs: dict[str, Any], + expected: dict[str, Any] | str, + exception: type[BaseException] | None, +) -> None: + if exception is not None: + with pytest.raises(exception, match=expected): + RedisHealthCheck.from_dsn(*args, **kwargs) + else: + obj = RedisHealthCheck.from_dsn(*args, **kwargs) + assert to_dict(obj) == expected + + +@pytest.mark.asyncio +async def test_call_success() -> None: + health_check = RedisHealthCheck( + host="localhost2", + port=6380, + database="test", + user="user", + password="pass", + timeout=10.0, + name="Test", + ) + redis_mock = MagicMock(spec=Redis) + redis_mock.__aenter__.return_value = redis_mock + redis_mock.ping = AsyncMock(return_value=True) + with patch("fast_healthchecks.checks.redis.Redis", return_value=redis_mock) as patched_Redis: # noqa: N806 + result = await health_check() + assert result.healthy is True + assert result.name == "Test" + patched_Redis.assert_called_once_with( + host="localhost2", + port=6380, + db="test", + username="user", + password="pass", + socket_timeout=10.0, + single_connection_client=True, + ) + + +@pytest.mark.asyncio +async def test_call_exception() -> None: + health_check = RedisHealthCheck( + host="localhost", + port=6379, + database=0, + user=None, + password=None, + timeout=5.0, + name="Redis", + ) + with patch("fast_healthchecks.checks.redis.Redis") as patched_Redis: # noqa: N806 + patched_Redis.return_value.__aenter__.side_effect = Exception("Connection error") + result = await health_check() + assert result.name == "Redis" + assert result.healthy is False + assert "Connection error" in result.error_details + patched_Redis.assert_called_once_with( + host="localhost", + port=6379, + db=0, + username=None, + password=None, + socket_timeout=5.0, + single_connection_client=True, + ) diff --git a/tests/unit/checks/test_url.py b/tests/unit/checks/test_url.py new file mode 100644 index 0000000..4ce4e5f --- /dev/null +++ b/tests/unit/checks/test_url.py @@ -0,0 +1,103 @@ +from unittest.mock import MagicMock, patch + +import pytest +from httpx import AsyncClient, Response + +from fast_healthchecks.checks.url import UrlHealthCheck +from fast_healthchecks.models import HealthCheckResult + +pytestmark = pytest.mark.unit + + +@pytest.mark.vcr +@pytest.mark.asyncio +async def test_url_health_check_success() -> None: + check = UrlHealthCheck( + name="test_check", + url="https://httpbin.org/status/200", + ) + result = await check() + assert result == HealthCheckResult(name="test_check", healthy=True) + + +@pytest.mark.vcr +@pytest.mark.asyncio +async def test_url_health_check_failure() -> None: + check = UrlHealthCheck( + name="test_check", + url="https://httpbin.org/status/500", + ) + result = await check() + assert result.healthy is False + assert "500 INTERNAL SERVER ERROR" in result.error_details + + +@pytest.mark.vcr +@pytest.mark.asyncio +async def test_url_health_check_with_basic_auth_success() -> None: + check = UrlHealthCheck( + name="test_check", + url="https://httpbin.org/basic-auth/user/passwd", + username="user", + password="passwd", + ) + result = await check() + assert result == HealthCheckResult(name="test_check", healthy=True) + + +@pytest.mark.vcr +@pytest.mark.asyncio +async def test_url_health_check_with_basic_auth_failure() -> None: + check = UrlHealthCheck( + name="test_check", + url="https://httpbin.org/basic-auth/user/passwd", + username="user", + password="wrong_passwd", + ) + result = await check() + assert result.healthy is False + assert "401 UNAUTHORIZED" in result.error_details + + +@pytest.mark.vcr +@pytest.mark.asyncio +async def test_url_health_check_with_timeout() -> None: + check = UrlHealthCheck( + name="test_check", + url="https://httpbin.org/delay/5", + timeout=0.1, + ) + result = await check() + assert result.healthy is False + assert "Timeout" in result.error_details + + +@pytest.mark.asyncio +async def test_AsyncClient_args_kwargs() -> None: # noqa: N802 + health_check = UrlHealthCheck( + name="Test", + url="https://httpbin.org/status/200", + username="user", + password="passwd", + follow_redirects=False, + timeout=1.0, + ) + response = Response( + status_code=200, + content=b"", + request=MagicMock(), + history=[], + ) + AsyncClient_mock = MagicMock(spec=AsyncClient) # noqa: N806 + AsyncClient_mock.__aenter__.return_value = AsyncClient_mock + AsyncClient_mock.get.side_effect = [response] + with patch("fast_healthchecks.checks.url.AsyncClient", return_value=AsyncClient_mock) as patched_AsyncClient: # noqa: N806 + result = await health_check() + assert result == HealthCheckResult(name="Test", healthy=True) + patched_AsyncClient.assert_called_once_with( + auth=health_check._auth, + timeout=1.0, + transport=health_check._transport, + follow_redirects=False, + ) + AsyncClient_mock.get.assert_called_once_with("https://httpbin.org/status/200") diff --git a/tests/unit/integrations/__init__.py b/tests/unit/integrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/integrations/test_fastapi.py b/tests/unit/integrations/test_fastapi.py new file mode 100644 index 0000000..1880ff5 --- /dev/null +++ b/tests/unit/integrations/test_fastapi.py @@ -0,0 +1,49 @@ +import json + +import pytest +from fastapi import status +from fastapi.testclient import TestClient + +from examples.fastapi_example.main import app_custom, app_fail, app_success + +pytestmark = pytest.mark.unit + +client = TestClient(app_success) + + +def test_liveness_probe() -> None: + response = client.get("/health/liveness") + assert response.status_code == status.HTTP_204_NO_CONTENT + assert response.content == b"" + + +def test_readiness_probe() -> None: + response = client.get("/health/readiness") + assert response.status_code == status.HTTP_204_NO_CONTENT + assert response.content == b"" + + +def test_startup_probe() -> None: + response = client.get("/health/startup") + assert response.status_code == status.HTTP_204_NO_CONTENT + assert response.content == b"" + + +def test_readiness_probe_fail() -> None: + client_fail = TestClient(app_fail) + response = client_fail.get("/health/readiness") + assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE + assert response.content == b"" + + +def test_custom_handler() -> None: + client_custom = TestClient(app_custom) + response = client_custom.get("/custom_health/readiness") + assert response.status_code == status.HTTP_200_OK + assert response.content == json.dumps( + {"results": [{"name": "Async dummy", "healthy": True, "error_details": None}], "allow_partial_failure": False}, + ensure_ascii=False, + allow_nan=False, + indent=None, + separators=(",", ":"), + ).encode("utf-8") diff --git a/tests/unit/integrations/test_faststream.py b/tests/unit/integrations/test_faststream.py new file mode 100644 index 0000000..00c59a6 --- /dev/null +++ b/tests/unit/integrations/test_faststream.py @@ -0,0 +1,49 @@ +import json +from http import HTTPStatus + +import pytest +from starlette.testclient import TestClient + +from examples.faststream_example.main import app_custom, app_fail, app_success + +pytestmark = pytest.mark.unit + +client = TestClient(app_success) + + +def test_liveness_probe() -> None: + response = client.get("/health/liveness") + assert response.status_code == HTTPStatus.NO_CONTENT + assert response.content == b"" + + +def test_readiness_probe() -> None: + response = client.get("/health/readiness") + assert response.status_code == HTTPStatus.NO_CONTENT + assert response.content == b"" + + +def test_startup_probe() -> None: + response = client.get("/health/startup") + assert response.status_code == HTTPStatus.NO_CONTENT + assert response.content == b"" + + +def test_readiness_probe_fail() -> None: + client_fail = TestClient(app_fail) + response = client_fail.get("/health/readiness") + assert response.status_code == HTTPStatus.SERVICE_UNAVAILABLE + assert response.content == b"" + + +def test_custom_handler() -> None: + client_custom = TestClient(app_custom) + response = client_custom.get("/custom_health/readiness") + assert response.status_code == HTTPStatus.OK + assert response.content == json.dumps( + {"results": [{"name": "Async dummy", "healthy": True, "error_details": None}], "allow_partial_failure": False}, + ensure_ascii=False, + allow_nan=False, + indent=None, + separators=(",", ":"), + ).encode("utf-8") diff --git a/tests/unit/integrations/test_litestar.py b/tests/unit/integrations/test_litestar.py new file mode 100644 index 0000000..1a0eca0 --- /dev/null +++ b/tests/unit/integrations/test_litestar.py @@ -0,0 +1,51 @@ +import json + +import pytest +from litestar.status_codes import HTTP_200_OK, HTTP_204_NO_CONTENT, HTTP_503_SERVICE_UNAVAILABLE +from litestar.testing import TestClient + +from examples.litestar_example.main import app_custom, app_fail, app_success + +app_success.debug = True +pytestmark = pytest.mark.unit + + +def test_liveness_probe() -> None: + with TestClient(app=app_success) as client: + response = client.get("/health/liveness") + assert response.status_code == HTTP_204_NO_CONTENT + assert response.content == b"" + + +def test_readiness_probe() -> None: + with TestClient(app=app_success) as client: + response = client.get("/health/readiness") + assert response.status_code == HTTP_204_NO_CONTENT + assert response.content == b"" + + +def test_startup_probe() -> None: + with TestClient(app=app_success) as client: + response = client.get("/health/startup") + assert response.status_code == HTTP_204_NO_CONTENT + assert response.content == b"" + + +def test_readiness_probe_fail() -> None: + with TestClient(app=app_fail) as client: + response = client.get("/health/readiness") + assert response.status_code == HTTP_503_SERVICE_UNAVAILABLE + assert response.content == b"" + + +def test_custom_handler() -> None: + with TestClient(app=app_custom) as client: + response = client.get("/custom_health/readiness") + assert response.status_code == HTTP_200_OK + assert response.content == json.dumps( + {"results": [{"name": "Async dummy", "healthy": True, "error_details": None}], "allow_partial_failure": False}, + ensure_ascii=False, + allow_nan=False, + indent=None, + separators=(",", ":"), + ).encode("utf-8") diff --git a/tests/unit/test_imports.py b/tests/unit/test_imports.py new file mode 100644 index 0000000..771c4f8 --- /dev/null +++ b/tests/unit/test_imports.py @@ -0,0 +1,38 @@ +import pytest + +pytestmark = pytest.mark.imports + + +def test_import_error_PostgreSQLAsyncPGHealthCheck() -> None: # noqa: N802 + with pytest.raises(ImportError, match="asyncpg is not installed. Install it with `pip install asyncpg`."): + from fast_healthchecks.checks.postgresql.asyncpg import PostgreSQLAsyncPGHealthCheck # noqa: PLC0415, F401 + + +def test_import_error_PostgreSQLPsycopgHealthCheck() -> None: # noqa: N802 + with pytest.raises(ImportError, match="psycopg is not installed. Install it with `pip install psycopg`."): + from fast_healthchecks.checks.postgresql.psycopg import PostgreSQLPsycopgHealthCheck # noqa: PLC0415, F401 + + +def test_import_error_KafkaHealthCheck() -> None: # noqa: N802 + with pytest.raises(ImportError, match="aiokafka is not installed. Install it with `pip install aiokafka`."): + from fast_healthchecks.checks.kafka import KafkaHealthCheck # noqa: PLC0415, F401 + + +def test_import_error_MongoHealthCheck() -> None: # noqa: N802 + with pytest.raises(ImportError, match="motor is not installed. Install it with `pip install motor`."): + from fast_healthchecks.checks.mongo import MongoHealthCheck # noqa: PLC0415, F401 + + +def test_import_error_RabbitMQHealthCheck() -> None: # noqa: N802 + with pytest.raises(ImportError, match="aio-pika is not installed. Install it with `pip install aio-pika`."): + from fast_healthchecks.checks.rabbitmq import RabbitMQHealthCheck # noqa: PLC0415, F401 + + +def test_import_error_RedisHealthCheck() -> None: # noqa: N802 + with pytest.raises(ImportError, match="redis is not installed. Install it with `pip install redis`."): + from fast_healthchecks.checks.redis import RedisHealthCheck # noqa: PLC0415, F401 + + +def test_import_error_UrlHealthCheck() -> None: # noqa: N802 + with pytest.raises(ImportError, match="httpx is not installed. Install it with `pip install httpx`."): + from fast_healthchecks.checks.url import UrlHealthCheck # noqa: PLC0415, F401 diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py new file mode 100644 index 0000000..56646b5 --- /dev/null +++ b/tests/unit/test_models.py @@ -0,0 +1,53 @@ +import pytest + +from fast_healthchecks.models import HealthcheckReport, HealthCheckResult + +pytestmark = pytest.mark.unit + + +def test_healthcheck_result() -> None: + hcr1 = HealthCheckResult( + name="test", + healthy=True, + ) + assert str(hcr1) == "test: healthy" + hcr2 = HealthCheckResult( + name="test", + healthy=False, + error_details="error", + ) + assert str(hcr2) == "test: unhealthy" + + +def test_healthcheck_report() -> None: + hcr = HealthcheckReport( + results=[ + HealthCheckResult( + name="test1", + healthy=True, + ), + HealthCheckResult( + name="test2", + healthy=False, + error_details="error", + ), + ], + ) + assert str(hcr) == "test1: healthy\ntest2: unhealthy" + assert hcr.healthy is False + + hcr = HealthcheckReport( + results=[ + HealthCheckResult( + name="test1", + healthy=True, + ), + HealthCheckResult( + name="test2", + healthy=False, + error_details="error", + ), + ], + allow_partial_failure=True, + ) + assert hcr.healthy is True diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..71c3415 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,48 @@ +import shutil +import tempfile +from contextlib import contextmanager +from pathlib import Path +from urllib.parse import quote + +__all__ = ( + "SSLCERT_NAME", + "SSLKEY_NAME", + "SSLROOTCERT_NAME", + "create_temp_files", +) + + +SSLCERT_NAME = "cert.crt" +SSLKEY_NAME = "key.key" +SSLROOTCERT_NAME = "ca.crt" +TEST_CERT_LOCATION = Path(__file__).parent.parent / "certs" + +SSL_FILES_MAP = { + SSLCERT_NAME: TEST_CERT_LOCATION / SSLCERT_NAME, + SSLKEY_NAME: TEST_CERT_LOCATION / SSLKEY_NAME, + SSLROOTCERT_NAME: TEST_CERT_LOCATION / SSLROOTCERT_NAME, +} + + +temp_dir = tempfile.gettempdir() + +TEST_SSLCERT = quote(f"{temp_dir}/{SSLCERT_NAME}") +TEST_SSLKEY = quote(f"{temp_dir}/{SSLKEY_NAME}") +TEST_SSLROOTCERT = quote(f"{temp_dir}/{SSLROOTCERT_NAME}") + + +@contextmanager +def create_temp_files(temp_file_paths: list[str]) -> None: + paths = [Path(temp_file_path) for temp_file_path in temp_file_paths] + for path in paths: + if path.name in SSL_FILES_MAP: + shutil.copy(SSL_FILES_MAP[path.name], path) + else: + with path.open("w") as f: + f.write("Temporary content.") + f.flush() + + yield + + for path in paths: + path.unlink() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..315a60c --- /dev/null +++ b/uv.lock @@ -0,0 +1,2578 @@ +version = 1 +requires-python = ">=3.10.0, <4.0.0" +resolution-markers = [ + "platform_python_implementation == 'PyPy'", + "platform_python_implementation != 'PyPy'", +] + +[[package]] +name = "aio-pika" +version = "9.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiormq" }, + { name = "exceptiongroup" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/40/3e52430f2dd7ef514f2a44827491fd4cbbca22587bb82c2d998b756fbd35/aio_pika-9.5.3.tar.gz", hash = "sha256:95c19605ad2918e46ae39ead9a4991d5a8738dceedef11ff352b62eb3f935f36", size = 48304 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/83/04dbcbdcb9e812c1a7bb3b4ea788b8da6baaa9da1a8478a3dafe604df052/aio_pika-9.5.3-py3-none-any.whl", hash = "sha256:9cdbc3350a76a04947348f07fa606bb76bb2d1bcb36523cc5452210e32a5041b", size = 54135 }, +] + +[[package]] +name = "aiokafka" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout" }, + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/ca/42a962033e6a7926dcb789168bce81d0181ef4ddabce454d830b7e62370e/aiokafka-0.12.0.tar.gz", hash = "sha256:62423895b866f95b5ed8d88335295a37cc5403af64cb7cb0e234f88adc2dff94", size = 564955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/e6/8e302c5e1a4460138e56e95bf0ab201e6554d83e490f120bc45f69ef7bb2/aiokafka-0.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da8938eac2153ca767ac0144283b3df7e74bb4c0abc0c9a722f3ae63cfbf3a42", size = 375435 }, + { url = "https://files.pythonhosted.org/packages/a5/3f/41a5741335c28062721ce6f4d94c2b92931d55f06e6c28c569ecdafe4635/aiokafka-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5c827c8883cfe64bc49100de82862225714e1853432df69aba99f135969bb1b", size = 372567 }, + { url = "https://files.pythonhosted.org/packages/bb/4e/3325c3e6e9ad88f4009de67f36063f45fc719d7097a87f2547922945dbf0/aiokafka-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bea5710f7707ed12a7f8661ab38dfa80f5253a405de5ba228f457cc30404eb51", size = 1081167 }, + { url = "https://files.pythonhosted.org/packages/68/5e/65a87e1f7308ba2e23d8ca3e366f506c0bbcbbabc388b776c8a5c181bc3e/aiokafka-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d87b1a45c57bbb1c17d1900a74739eada27e4f4a0b0932ab3c5a8cbae8bbfe1e", size = 1095099 }, + { url = "https://files.pythonhosted.org/packages/c7/46/e7069d7359c77768f31629d6aa0c8d964a6942f22d74587a8944775894da/aiokafka-0.12.0-cp310-cp310-win32.whl", hash = "sha256:1158e630664d9abc74d8a7673bc70dc10737ff758e1457bebc1c05890f29ce2c", size = 349666 }, + { url = "https://files.pythonhosted.org/packages/18/83/73c54b884cf7dabe8ed5d5764e10a0ebec14c1efc19b8f243c8036963006/aiokafka-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:06f5889acf8e1a81d6e14adf035acb29afd1f5836447fa8fa23d3cbe8f7e8608", size = 368638 }, + { url = "https://files.pythonhosted.org/packages/b3/7b/8faf3ae26f43b2dcc66f45665bd243c9f736e71df04e45da5836bb7a7be4/aiokafka-0.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ddc5308c43d48af883667e2f950a0a9739ce2c9bfe69a0b55dc234f58b1b42d6", size = 375692 }, + { url = "https://files.pythonhosted.org/packages/fe/6c/c1ce38a225dfa04078f29d8734f13e483146cc2e30dbf6d13b75c2aa0724/aiokafka-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff63689cafcd6dd642a15de75b7ae121071d6162cccba16d091bcb28b3886307", size = 372335 }, + { url = "https://files.pythonhosted.org/packages/7d/bc/c5d2315e2f04768f585e31e6bd0a1fb9ed054a54c124c17087fdff507a13/aiokafka-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24633931e05a9dc80555a2f845572b6845d2dcb1af12de27837b8602b1b8bc74", size = 1141449 }, + { url = "https://files.pythonhosted.org/packages/dc/42/607caffc39b1fb2be288fa2c72e72b352872362699b6e7473189fee065b9/aiokafka-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42b2436c7c69384d210e9169fbfe339d9f49dbdcfddd8d51c79b9877de545e33", size = 1155398 }, + { url = "https://files.pythonhosted.org/packages/f9/4e/e7a4900180ff18f8468b1a1d6da821f67162409aa86eb53fdcb6bb1c5016/aiokafka-0.12.0-cp311-cp311-win32.whl", hash = "sha256:90511a2c4cf5f343fc2190575041fbc70171654ab0dae64b3bbabd012613bfa7", size = 348578 }, + { url = "https://files.pythonhosted.org/packages/12/e6/101e7b13e1a4bce745be927bcecf7d9dddd68c57bbd876e31697e60fdc8d/aiokafka-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:04c8ad27d04d6c53a1859687015a5f4e58b1eb221e8a7342d6c6b04430def53e", size = 368368 }, + { url = "https://files.pythonhosted.org/packages/53/d4/baf1b2389995c6c312834792329a1993a303ff703ac023250ff977c5923b/aiokafka-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b01947553ff1120fa1cb1a05f2c3e5aa47a5378c720bafd09e6630ba18af02aa", size = 375031 }, + { url = "https://files.pythonhosted.org/packages/54/ac/653070a4add8beea7aa8209ab396de87c7b4f9628fff15efcdbaea40e973/aiokafka-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e3c8ec1c0606fa645462c7353dc3e4119cade20c4656efa2031682ffaad361c0", size = 370619 }, + { url = "https://files.pythonhosted.org/packages/80/f2/0ddaaa11876ab78e0f3b30f272c62eea70870e1a52a5afe985c7c1d098e1/aiokafka-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:577c1c48b240e9eba57b3d2d806fb3d023a575334fc3953f063179170cc8964f", size = 1192363 }, + { url = "https://files.pythonhosted.org/packages/ae/48/541ccece0e593e24ee371dec0c33c23718bc010b04e998693e4c19091258/aiokafka-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7b815b2e5fed9912f1231be6196547a367b9eb3380b487ff5942f0c73a3fb5c", size = 1213231 }, + { url = "https://files.pythonhosted.org/packages/99/3f/75bd0faa77dfecce34dd1c0edd317b608518b096809736f9987dd61f4cec/aiokafka-0.12.0-cp312-cp312-win32.whl", hash = "sha256:5a907abcdf02430df0829ac80f25b8bb849630300fa01365c76e0ae49306f512", size = 347752 }, + { url = "https://files.pythonhosted.org/packages/ef/97/e2513a0c10585e51d4d9b42c9dd5f5ab15dfe150620a4893a2c6c20f0f4a/aiokafka-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:fdbd69ec70eea4a8dfaa5c35ff4852e90e1277fcc426b9380f0b499b77f13b16", size = 366068 }, + { url = "https://files.pythonhosted.org/packages/30/84/f1f7e603cd07e877520b5a1e48e006cbc1fe448806cabbaa98aa732f530d/aiokafka-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f9e8ab97b935ca681a5f28cf22cf2b5112be86728876b3ec07e4ed5fc6c21f2d", size = 370960 }, + { url = "https://files.pythonhosted.org/packages/d7/c7/5237b3687198c2129c0bafa4a96cf8ae3883e20cc860125bafe16af3778e/aiokafka-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed991c120fe19fd9439f564201dd746c4839700ef270dd4c3ee6d4895f64fe83", size = 366597 }, + { url = "https://files.pythonhosted.org/packages/6b/67/0154551292ec1c977e5def178ae5c947773e921aefb6877971e7fdf1942e/aiokafka-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c01abf9787b1c3f3af779ad8e76d5b74903f590593bc26f33ed48750503e7f7", size = 1152905 }, + { url = "https://files.pythonhosted.org/packages/d9/20/69f913a76916e94c4e783dc7d0d05a25c384b25faec33e121062c62411fe/aiokafka-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08c84b3894d97fd02fcc8886f394000d0f5ce771fab5c498ea2b0dd2f6b46d5b", size = 1171893 }, + { url = "https://files.pythonhosted.org/packages/16/65/41cc1b19e7dea623ef58f3bf1e2720377c5757a76d9799d53a1b5fc39255/aiokafka-0.12.0-cp313-cp313-win32.whl", hash = "sha256:63875fed922c8c7cf470d9b2a82e1b76b4a1baf2ae62e07486cf516fd09ff8f2", size = 345933 }, + { url = "https://files.pythonhosted.org/packages/bf/0d/4cb57231ff650a01123a09075bf098d8fdaf94b15a1a58465066b2251e8b/aiokafka-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:bdc0a83eb386d2384325d6571f8ef65b4cfa205f8d1c16d7863e8d10cacd995a", size = 363194 }, +] + +[[package]] +name = "aiormq" +version = "6.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pamqp" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/79/5397756a8782bf3d0dce392b48260c3ec81010f16bef8441ff03505dccb4/aiormq-6.8.1.tar.gz", hash = "sha256:a964ab09634be1da1f9298ce225b310859763d5cf83ef3a7eae1a6dc6bd1da1a", size = 30528 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/be/1a613ae1564426f86650ff58c351902895aa969f7e537e74bfd568f5c8bf/aiormq-6.8.1-py3-none-any.whl", hash = "sha256:5da896c8624193708f9409ffad0b20395010e2747f22aa4150593837f40aa017", size = 31174 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11' and python_full_version >= '3.10.0' and python_full_version < '4.0.0'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' and python_full_version >= '3.10.0' and python_full_version < '4.0.0'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/40/318e58f669b1a9e00f5c4453910682e2d9dd594334539c7b7817dabb765f/anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48", size = 177076 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/7a/4daaf3b6c08ad7ceffea4634ec206faeff697526421c20f07628c7372156/anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352", size = 93052 }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, +] + +[[package]] +name = "asyncpg" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11' and python_full_version >= '3.10.0' and python_full_version < '4.0.0'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/07/1650a8c30e3a5c625478fa8aafd89a8dd7d85999bf7169b16f54973ebf2c/asyncpg-0.30.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfb4dd5ae0699bad2b233672c8fc5ccbd9ad24b89afded02341786887e37927e", size = 673143 }, + { url = "https://files.pythonhosted.org/packages/a0/9a/568ff9b590d0954553c56806766914c149609b828c426c5118d4869111d3/asyncpg-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc1f62c792752a49f88b7e6f774c26077091b44caceb1983509edc18a2222ec0", size = 645035 }, + { url = "https://files.pythonhosted.org/packages/de/11/6f2fa6c902f341ca10403743701ea952bca896fc5b07cc1f4705d2bb0593/asyncpg-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3152fef2e265c9c24eec4ee3d22b4f4d2703d30614b0b6753e9ed4115c8a146f", size = 2912384 }, + { url = "https://files.pythonhosted.org/packages/83/83/44bd393919c504ffe4a82d0aed8ea0e55eb1571a1dea6a4922b723f0a03b/asyncpg-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7255812ac85099a0e1ffb81b10dc477b9973345793776b128a23e60148dd1af", size = 2947526 }, + { url = "https://files.pythonhosted.org/packages/08/85/e23dd3a2b55536eb0ded80c457b0693352262dc70426ef4d4a6fc994fa51/asyncpg-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:578445f09f45d1ad7abddbff2a3c7f7c291738fdae0abffbeb737d3fc3ab8b75", size = 2895390 }, + { url = "https://files.pythonhosted.org/packages/9b/26/fa96c8f4877d47dc6c1864fef5500b446522365da3d3d0ee89a5cce71a3f/asyncpg-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c42f6bb65a277ce4d93f3fba46b91a265631c8df7250592dd4f11f8b0152150f", size = 3015630 }, + { url = "https://files.pythonhosted.org/packages/34/00/814514eb9287614188a5179a8b6e588a3611ca47d41937af0f3a844b1b4b/asyncpg-0.30.0-cp310-cp310-win32.whl", hash = "sha256:aa403147d3e07a267ada2ae34dfc9324e67ccc4cdca35261c8c22792ba2b10cf", size = 568760 }, + { url = "https://files.pythonhosted.org/packages/f0/28/869a7a279400f8b06dd237266fdd7220bc5f7c975348fea5d1e6909588e9/asyncpg-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb622c94db4e13137c4c7f98834185049cc50ee01d8f657ef898b6407c7b9c50", size = 625764 }, + { url = "https://files.pythonhosted.org/packages/4c/0e/f5d708add0d0b97446c402db7e8dd4c4183c13edaabe8a8500b411e7b495/asyncpg-0.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a", size = 674506 }, + { url = "https://files.pythonhosted.org/packages/6a/a0/67ec9a75cb24a1d99f97b8437c8d56da40e6f6bd23b04e2f4ea5d5ad82ac/asyncpg-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed", size = 645922 }, + { url = "https://files.pythonhosted.org/packages/5c/d9/a7584f24174bd86ff1053b14bb841f9e714380c672f61c906eb01d8ec433/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a", size = 3079565 }, + { url = "https://files.pythonhosted.org/packages/a0/d7/a4c0f9660e333114bdb04d1a9ac70db690dd4ae003f34f691139a5cbdae3/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956", size = 3109962 }, + { url = "https://files.pythonhosted.org/packages/3c/21/199fd16b5a981b1575923cbb5d9cf916fdc936b377e0423099f209e7e73d/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056", size = 3064791 }, + { url = "https://files.pythonhosted.org/packages/77/52/0004809b3427534a0c9139c08c87b515f1c77a8376a50ae29f001e53962f/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454", size = 3188696 }, + { url = "https://files.pythonhosted.org/packages/52/cb/fbad941cd466117be58b774a3f1cc9ecc659af625f028b163b1e646a55fe/asyncpg-0.30.0-cp311-cp311-win32.whl", hash = "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d", size = 567358 }, + { url = "https://files.pythonhosted.org/packages/3c/0a/0a32307cf166d50e1ad120d9b81a33a948a1a5463ebfa5a96cc5606c0863/asyncpg-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f", size = 629375 }, + { url = "https://files.pythonhosted.org/packages/4b/64/9d3e887bb7b01535fdbc45fbd5f0a8447539833b97ee69ecdbb7a79d0cb4/asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e", size = 673162 }, + { url = "https://files.pythonhosted.org/packages/6e/eb/8b236663f06984f212a087b3e849731f917ab80f84450e943900e8ca4052/asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a", size = 637025 }, + { url = "https://files.pythonhosted.org/packages/cc/57/2dc240bb263d58786cfaa60920779af6e8d32da63ab9ffc09f8312bd7a14/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3", size = 3496243 }, + { url = "https://files.pythonhosted.org/packages/f4/40/0ae9d061d278b10713ea9021ef6b703ec44698fe32178715a501ac696c6b/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737", size = 3575059 }, + { url = "https://files.pythonhosted.org/packages/c3/75/d6b895a35a2c6506952247640178e5f768eeb28b2e20299b6a6f1d743ba0/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a", size = 3473596 }, + { url = "https://files.pythonhosted.org/packages/c8/e7/3693392d3e168ab0aebb2d361431375bd22ffc7b4a586a0fc060d519fae7/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af", size = 3641632 }, + { url = "https://files.pythonhosted.org/packages/32/ea/15670cea95745bba3f0352341db55f506a820b21c619ee66b7d12ea7867d/asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e", size = 560186 }, + { url = "https://files.pythonhosted.org/packages/7e/6b/fe1fad5cee79ca5f5c27aed7bd95baee529c1bf8a387435c8ba4fe53d5c1/asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305", size = 621064 }, + { url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373 }, + { url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745 }, + { url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103 }, + { url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471 }, + { url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253 }, + { url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720 }, + { url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404 }, + { url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623 }, +] + +[[package]] +name = "babel" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, +] + +[[package]] +name = "bracex" +version = "2.5.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/6c/57418c4404cd22fe6275b8301ca2b46a8cdaa8157938017a9ae0b3edf363/bracex-2.5.post1.tar.gz", hash = "sha256:12c50952415bfa773d2d9ccb8e79651b8cdb1f31a42f6091b804f6ba2b4a66b6", size = 26641 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/02/8db98cdc1a58e0abd6716d5e63244658e6e63513c65f469f34b6f1053fd0/bracex-2.5.post1-py3-none-any.whl", hash = "sha256:13e5732fec27828d6af308628285ad358047cec36801598368cb28bc631dbaf6", size = 11558 }, +] + +[[package]] +name = "cachetools" +version = "5.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/38/a0f315319737ecf45b4319a8cd1f3a908e29d9277b46942263292115eee7/cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a", size = 27661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/07/14f8ad37f2d12a5ce41206c21820d8cb6561b728e51fad4530dff0552a67/cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292", size = 9524 }, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "chardet" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/8b/825cc84cf13a28bfbcba7c416ec22bf85a9584971be15b21dd8300c65b7f/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", size = 196363 }, + { url = "https://files.pythonhosted.org/packages/23/81/d7eef6a99e42c77f444fdd7bc894b0ceca6c3a95c51239e74a722039521c/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", size = 125639 }, + { url = "https://files.pythonhosted.org/packages/21/67/b4564d81f48042f520c948abac7079356e94b30cb8ffb22e747532cf469d/charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", size = 120451 }, + { url = "https://files.pythonhosted.org/packages/c2/72/12a7f0943dd71fb5b4e7b55c41327ac0a1663046a868ee4d0d8e9c369b85/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", size = 140041 }, + { url = "https://files.pythonhosted.org/packages/67/56/fa28c2c3e31217c4c52158537a2cf5d98a6c1e89d31faf476c89391cd16b/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", size = 150333 }, + { url = "https://files.pythonhosted.org/packages/f9/d2/466a9be1f32d89eb1554cf84073a5ed9262047acee1ab39cbaefc19635d2/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", size = 142921 }, + { url = "https://files.pythonhosted.org/packages/f8/01/344ec40cf5d85c1da3c1f57566c59e0c9b56bcc5566c08804a95a6cc8257/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", size = 144785 }, + { url = "https://files.pythonhosted.org/packages/73/8b/2102692cb6d7e9f03b9a33a710e0164cadfce312872e3efc7cfe22ed26b4/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", size = 146631 }, + { url = "https://files.pythonhosted.org/packages/d8/96/cc2c1b5d994119ce9f088a9a0c3ebd489d360a2eb058e2c8049f27092847/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", size = 140867 }, + { url = "https://files.pythonhosted.org/packages/c9/27/cde291783715b8ec30a61c810d0120411844bc4c23b50189b81188b273db/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", size = 149273 }, + { url = "https://files.pythonhosted.org/packages/3a/a4/8633b0fc1a2d1834d5393dafecce4a1cc56727bfd82b4dc18fc92f0d3cc3/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", size = 152437 }, + { url = "https://files.pythonhosted.org/packages/64/ea/69af161062166b5975ccbb0961fd2384853190c70786f288684490913bf5/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", size = 150087 }, + { url = "https://files.pythonhosted.org/packages/3b/fd/e60a9d9fd967f4ad5a92810138192f825d77b4fa2a557990fd575a47695b/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", size = 145142 }, + { url = "https://files.pythonhosted.org/packages/6d/02/8cb0988a1e49ac9ce2eed1e07b77ff118f2923e9ebd0ede41ba85f2dcb04/charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", size = 94701 }, + { url = "https://files.pythonhosted.org/packages/d6/20/f1d4670a8a723c46be695dff449d86d6092916f9e99c53051954ee33a1bc/charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", size = 102191 }, + { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, + { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, + { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, + { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, + { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, + { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, + { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, + { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, + { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, + { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, + { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, + { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, + { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, + { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, + { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, + { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, + { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, + { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, + { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, + { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, + { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, + { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, + { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, + { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, + { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, + { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, + { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, + { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, + { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, + { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, + { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, + { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, + { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, + { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, + { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, + { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, + { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, + { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, + { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, + { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, + { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, + { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, + { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, + { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, +] + +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coverage" +version = "7.6.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/d2/c25011f4d036cf7e8acbbee07a8e09e9018390aee25ba085596c4b83d510/coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d", size = 801710 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/f3/f830fb53bf7e4f1d5542756f61d9b740352a188f43854aab9409c8cdeb18/coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb", size = 207024 }, + { url = "https://files.pythonhosted.org/packages/4e/e3/ea5632a3a6efd00ab0a791adc0f3e48512097a757ee7dcbee5505f57bafa/coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710", size = 207463 }, + { url = "https://files.pythonhosted.org/packages/e4/ae/18ff8b5580e27e62ebcc888082aa47694c2772782ea7011ddf58e377e98f/coverage-7.6.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa", size = 235902 }, + { url = "https://files.pythonhosted.org/packages/6a/52/57030a8d15ab935624d298360f0a6704885578e39f7b4f68569e59f5902d/coverage-7.6.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1", size = 233806 }, + { url = "https://files.pythonhosted.org/packages/d0/c5/4466602195ecaced298d55af1e29abceb812addabefd5bd9116a204f7bab/coverage-7.6.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec", size = 234966 }, + { url = "https://files.pythonhosted.org/packages/b0/1c/55552c3009b7bf96732e36548596ade771c87f89cf1f5a8e3975b33539b5/coverage-7.6.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3", size = 234029 }, + { url = "https://files.pythonhosted.org/packages/bb/7d/da3dca6878701182ea42c51df47a47c80eaef2a76f5aa3e891dc2a8cce3f/coverage-7.6.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5", size = 232494 }, + { url = "https://files.pythonhosted.org/packages/28/cc/39de85ac1d5652bc34ff2bee39ae251b1fdcaae53fab4b44cab75a432bc0/coverage-7.6.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073", size = 233611 }, + { url = "https://files.pythonhosted.org/packages/d1/2b/7eb011a9378911088708f121825a71134d0c15fac96972a0ae7a8f5a4049/coverage-7.6.9-cp310-cp310-win32.whl", hash = "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198", size = 209712 }, + { url = "https://files.pythonhosted.org/packages/5b/35/c3f40a2269b416db34ce1dedf682a7132c26f857e33596830fa4deebabf9/coverage-7.6.9-cp310-cp310-win_amd64.whl", hash = "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717", size = 210553 }, + { url = "https://files.pythonhosted.org/packages/b1/91/b3dc2f7f38b5cca1236ab6bbb03e84046dd887707b4ec1db2baa47493b3b/coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9", size = 207133 }, + { url = "https://files.pythonhosted.org/packages/0d/2b/53fd6cb34d443429a92b3ec737f4953627e38b3bee2a67a3c03425ba8573/coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c", size = 207577 }, + { url = "https://files.pythonhosted.org/packages/74/f2/68edb1e6826f980a124f21ea5be0d324180bf11de6fd1defcf9604f76df0/coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7", size = 239524 }, + { url = "https://files.pythonhosted.org/packages/d3/83/8fec0ee68c2c4a5ab5f0f8527277f84ed6f2bd1310ae8a19d0c5532253ab/coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9", size = 236925 }, + { url = "https://files.pythonhosted.org/packages/8b/20/8f50e7c7ad271144afbc2c1c6ec5541a8c81773f59352f8db544cad1a0ec/coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4", size = 238792 }, + { url = "https://files.pythonhosted.org/packages/6f/62/4ac2e5ad9e7a5c9ec351f38947528e11541f1f00e8a0cdce56f1ba7ae301/coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1", size = 237682 }, + { url = "https://files.pythonhosted.org/packages/58/2f/9d2203f012f3b0533c73336c74134b608742be1ce475a5c72012573cfbb4/coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b", size = 236310 }, + { url = "https://files.pythonhosted.org/packages/33/6d/31f6ab0b4f0f781636075f757eb02141ea1b34466d9d1526dbc586ed7078/coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3", size = 237096 }, + { url = "https://files.pythonhosted.org/packages/7d/fb/e14c38adebbda9ed8b5f7f8e03340ac05d68d27b24397f8d47478927a333/coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0", size = 209682 }, + { url = "https://files.pythonhosted.org/packages/a4/11/a782af39b019066af83fdc0e8825faaccbe9d7b19a803ddb753114b429cc/coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b", size = 210542 }, + { url = "https://files.pythonhosted.org/packages/60/52/b16af8989a2daf0f80a88522bd8e8eed90b5fcbdecf02a6888f3e80f6ba7/coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8", size = 207325 }, + { url = "https://files.pythonhosted.org/packages/0f/79/6b7826fca8846c1216a113227b9f114ac3e6eacf168b4adcad0cb974aaca/coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a", size = 207563 }, + { url = "https://files.pythonhosted.org/packages/a7/07/0bc73da0ccaf45d0d64ef86d33b7d7fdeef84b4c44bf6b85fb12c215c5a6/coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015", size = 240580 }, + { url = "https://files.pythonhosted.org/packages/71/8a/9761f409910961647d892454687cedbaccb99aae828f49486734a82ede6e/coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3", size = 237613 }, + { url = "https://files.pythonhosted.org/packages/8b/10/ee7d696a17ac94f32f2dbda1e17e730bf798ae9931aec1fc01c1944cd4de/coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae", size = 239684 }, + { url = "https://files.pythonhosted.org/packages/16/60/aa1066040d3c52fff051243c2d6ccda264da72dc6d199d047624d395b2b2/coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4", size = 239112 }, + { url = "https://files.pythonhosted.org/packages/4e/e5/69f35344c6f932ba9028bf168d14a79fedb0dd4849b796d43c81ce75a3c9/coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6", size = 237428 }, + { url = "https://files.pythonhosted.org/packages/32/20/adc895523c4a28f63441b8ac645abd74f9bdd499d2d175bef5b41fc7f92d/coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f", size = 239098 }, + { url = "https://files.pythonhosted.org/packages/a9/a6/e0e74230c9bb3549ec8ffc137cfd16ea5d56e993d6bffed2218bff6187e3/coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692", size = 209940 }, + { url = "https://files.pythonhosted.org/packages/3e/18/cb5b88349d4aa2f41ec78d65f92ea32572b30b3f55bc2b70e87578b8f434/coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97", size = 210726 }, + { url = "https://files.pythonhosted.org/packages/35/26/9abab6539d2191dbda2ce8c97b67d74cbfc966cc5b25abb880ffc7c459bc/coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664", size = 207356 }, + { url = "https://files.pythonhosted.org/packages/44/da/d49f19402240c93453f606e660a6676a2a1fbbaa6870cc23207790aa9697/coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c", size = 207614 }, + { url = "https://files.pythonhosted.org/packages/da/e6/93bb9bf85497816082ec8da6124c25efa2052bd4c887dd3b317b91990c9e/coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014", size = 240129 }, + { url = "https://files.pythonhosted.org/packages/df/65/6a824b9406fe066835c1274a9949e06f084d3e605eb1a602727a27ec2fe3/coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00", size = 237276 }, + { url = "https://files.pythonhosted.org/packages/9f/79/6c7a800913a9dd23ac8c8da133ebb556771a5a3d4df36b46767b1baffd35/coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d", size = 239267 }, + { url = "https://files.pythonhosted.org/packages/57/e7/834d530293fdc8a63ba8ff70033d5182022e569eceb9aec7fc716b678a39/coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a", size = 238887 }, + { url = "https://files.pythonhosted.org/packages/15/05/ec9d6080852984f7163c96984444e7cd98b338fd045b191064f943ee1c08/coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077", size = 236970 }, + { url = "https://files.pythonhosted.org/packages/0a/d8/775937670b93156aec29f694ce37f56214ed7597e1a75b4083ee4c32121c/coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb", size = 238831 }, + { url = "https://files.pythonhosted.org/packages/f4/58/88551cb7fdd5ec98cb6044e8814e38583436b14040a5ece15349c44c8f7c/coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba", size = 210000 }, + { url = "https://files.pythonhosted.org/packages/b7/12/cfbf49b95120872785ff8d56ab1c7fe3970a65e35010c311d7dd35c5fd00/coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1", size = 210753 }, + { url = "https://files.pythonhosted.org/packages/7c/68/c1cb31445599b04bde21cbbaa6d21b47c5823cdfef99eae470dfce49c35a/coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419", size = 208091 }, + { url = "https://files.pythonhosted.org/packages/11/73/84b02c6b19c4a11eb2d5b5eabe926fb26c21c080e0852f5e5a4f01165f9e/coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a", size = 208369 }, + { url = "https://files.pythonhosted.org/packages/de/e0/ae5d878b72ff26df2e994a5c5b1c1f6a7507d976b23beecb1ed4c85411ef/coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4", size = 251089 }, + { url = "https://files.pythonhosted.org/packages/ab/9c/0aaac011aef95a93ef3cb2fba3fde30bc7e68a6635199ed469b1f5ea355a/coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae", size = 246806 }, + { url = "https://files.pythonhosted.org/packages/f8/19/4d5d3ae66938a7dcb2f58cef3fa5386f838f469575b0bb568c8cc9e3a33d/coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030", size = 249164 }, + { url = "https://files.pythonhosted.org/packages/b3/0b/4ee8a7821f682af9ad440ae3c1e379da89a998883271f088102d7ca2473d/coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be", size = 248642 }, + { url = "https://files.pythonhosted.org/packages/8a/12/36ff1d52be18a16b4700f561852e7afd8df56363a5edcfb04cf26a0e19e0/coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e", size = 246516 }, + { url = "https://files.pythonhosted.org/packages/43/d0/8e258f6c3a527c1655602f4f576215e055ac704de2d101710a71a2affac2/coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9", size = 247783 }, + { url = "https://files.pythonhosted.org/packages/a9/0d/1e4a48d289429d38aae3babdfcadbf35ca36bdcf3efc8f09b550a845bdb5/coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b", size = 210646 }, + { url = "https://files.pythonhosted.org/packages/26/74/b0729f196f328ac55e42b1e22ec2f16d8bcafe4b8158a26ec9f1cdd1d93e/coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611", size = 211815 }, + { url = "https://files.pythonhosted.org/packages/15/0e/4ac9035ee2ee08d2b703fdad2d84283ec0bad3b46eb4ad6affb150174cb6/coverage-7.6.9-pp39.pp310-none-any.whl", hash = "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b", size = 199270 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11' and python_full_version >= '3.10.0' and python_full_version < '4.0.0'" }, +] + +[[package]] +name = "cryptography" +version = "44.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/4c/45dfa6829acffa344e3967d6006ee4ae8be57af746ae2eba1c431949b32c/cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02", size = 710657 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/09/8cc67f9b84730ad330b3b72cf867150744bf07ff113cda21a15a1c6d2c7c/cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123", size = 6541833 }, + { url = "https://files.pythonhosted.org/packages/7e/5b/3759e30a103144e29632e7cb72aec28cedc79e514b2ea8896bb17163c19b/cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092", size = 3922710 }, + { url = "https://files.pythonhosted.org/packages/5f/58/3b14bf39f1a0cfd679e753e8647ada56cddbf5acebffe7db90e184c76168/cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f", size = 4137546 }, + { url = "https://files.pythonhosted.org/packages/98/65/13d9e76ca19b0ba5603d71ac8424b5694415b348e719db277b5edc985ff5/cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb", size = 3915420 }, + { url = "https://files.pythonhosted.org/packages/b1/07/40fe09ce96b91fc9276a9ad272832ead0fddedcba87f1190372af8e3039c/cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b", size = 4154498 }, + { url = "https://files.pythonhosted.org/packages/75/ea/af65619c800ec0a7e4034207aec543acdf248d9bffba0533342d1bd435e1/cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543", size = 3932569 }, + { url = "https://files.pythonhosted.org/packages/4e/d5/9cc182bf24c86f542129565976c21301d4ac397e74bf5a16e48241aab8a6/cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385", size = 4164756 }, + { url = "https://files.pythonhosted.org/packages/c7/af/d1deb0c04d59612e3d5e54203159e284d3e7a6921e565bb0eeb6269bdd8a/cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e", size = 4016721 }, + { url = "https://files.pythonhosted.org/packages/bd/69/7ca326c55698d0688db867795134bdfac87136b80ef373aaa42b225d6dd5/cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e", size = 4240915 }, + { url = "https://files.pythonhosted.org/packages/ef/d4/cae11bf68c0f981e0413906c6dd03ae7fa864347ed5fac40021df1ef467c/cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053", size = 2757925 }, + { url = "https://files.pythonhosted.org/packages/64/b1/50d7739254d2002acae64eed4fc43b24ac0cc44bf0a0d388d1ca06ec5bb1/cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd", size = 3202055 }, + { url = "https://files.pythonhosted.org/packages/11/18/61e52a3d28fc1514a43b0ac291177acd1b4de00e9301aaf7ef867076ff8a/cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591", size = 6542801 }, + { url = "https://files.pythonhosted.org/packages/1a/07/5f165b6c65696ef75601b781a280fc3b33f1e0cd6aa5a92d9fb96c410e97/cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7", size = 3922613 }, + { url = "https://files.pythonhosted.org/packages/28/34/6b3ac1d80fc174812486561cf25194338151780f27e438526f9c64e16869/cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc", size = 4137925 }, + { url = "https://files.pythonhosted.org/packages/d0/c7/c656eb08fd22255d21bc3129625ed9cd5ee305f33752ef2278711b3fa98b/cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289", size = 3915417 }, + { url = "https://files.pythonhosted.org/packages/ef/82/72403624f197af0db6bac4e58153bc9ac0e6020e57234115db9596eee85d/cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7", size = 4155160 }, + { url = "https://files.pythonhosted.org/packages/a2/cd/2f3c440913d4329ade49b146d74f2e9766422e1732613f57097fea61f344/cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c", size = 3932331 }, + { url = "https://files.pythonhosted.org/packages/31/d9/90409720277f88eb3ab72f9a32bfa54acdd97e94225df699e7713e850bd4/cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba", size = 4165207 }, + { url = "https://files.pythonhosted.org/packages/7f/df/8be88797f0a1cca6e255189a57bb49237402b1880d6e8721690c5603ac23/cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64", size = 4017372 }, + { url = "https://files.pythonhosted.org/packages/af/36/5ccc376f025a834e72b8e52e18746b927f34e4520487098e283a719c205e/cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285", size = 4239657 }, + { url = "https://files.pythonhosted.org/packages/46/b0/f4f7d0d0bcfbc8dd6296c1449be326d04217c57afb8b2594f017eed95533/cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417", size = 2758672 }, + { url = "https://files.pythonhosted.org/packages/97/9b/443270b9210f13f6ef240eff73fd32e02d381e7103969dc66ce8e89ee901/cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede", size = 3202071 }, + { url = "https://files.pythonhosted.org/packages/77/d4/fea74422326388bbac0c37b7489a0fcb1681a698c3b875959430ba550daa/cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731", size = 3338857 }, + { url = "https://files.pythonhosted.org/packages/1a/aa/ba8a7467c206cb7b62f09b4168da541b5109838627f582843bbbe0235e8e/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4", size = 3850615 }, + { url = "https://files.pythonhosted.org/packages/89/fa/b160e10a64cc395d090105be14f399b94e617c879efd401188ce0fea39ee/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756", size = 4081622 }, + { url = "https://files.pythonhosted.org/packages/47/8f/20ff0656bb0cf7af26ec1d01f780c5cfbaa7666736063378c5f48558b515/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c", size = 3867546 }, + { url = "https://files.pythonhosted.org/packages/38/d9/28edf32ee2fcdca587146bcde90102a7319b2f2c690edfa627e46d586050/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa", size = 4090937 }, + { url = "https://files.pythonhosted.org/packages/cc/9d/37e5da7519de7b0b070a3fedd4230fe76d50d2a21403e0f2153d70ac4163/cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c", size = 3128774 }, +] + +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, +] + +[[package]] +name = "dnspython" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632 }, +] + +[[package]] +name = "email-validator" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "faker" +version = "33.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/9f/012fd6049fc86029951cba5112d32c7ba076c4290d7e8873b0413655b808/faker-33.1.0.tar.gz", hash = "sha256:1c925fc0e86a51fc46648b504078c88d0cd48da1da2595c4e712841cab43a1e4", size = 1850515 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/9c/2bba87fbfa42503ddd9653e3546ffc4ed18b14ecab7a07ee86491b886486/Faker-33.1.0-py3-none-any.whl", hash = "sha256:d30c5f0e2796b8970de68978365247657486eb0311c5abe88d0b895b68dff05d", size = 1889127 }, +] + +[[package]] +name = "fast-depends" +version = "2.4.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/f5/8b42b7588a67ad78991e5e7ca0e0c6a1ded535a69a725e4e48d3346a20c1/fast_depends-2.4.12.tar.gz", hash = "sha256:9393e6de827f7afa0141e54fa9553b737396aaf06bd0040e159d1f790487b16d", size = 16682 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/08/4adb160d8394053289fdf3b276e93b53271fd463e54fff8911b23c1db4ed/fast_depends-2.4.12-py3-none-any.whl", hash = "sha256:9e5d110ddc962329e46c9b35e5fe65655984247a13ee3ca5a33186db7d2d75c2", size = 17651 }, +] + +[[package]] +name = "fast-healthchecks" +version = "0.0.0" +source = { virtual = "." } + +[package.optional-dependencies] +aio-pika = [ + { name = "aio-pika" }, +] +aiokafka = [ + { name = "aiokafka" }, +] +asyncpg = [ + { name = "asyncpg" }, +] +fastapi = [ + { name = "fastapi", extra = ["standard"] }, +] +faststream = [ + { name = "faststream" }, +] +httpx = [ + { name = "httpx" }, +] +litestar = [ + { name = "litestar" }, +] +motor = [ + { name = "motor" }, +] +msgspec = [ + { name = "msgspec" }, +] +psycopg = [ + { name = "psycopg" }, +] +pydantic = [ + { name = "pydantic" }, +] +redis = [ + { name = "redis" }, +] + +[package.dev-dependencies] +dev = [ + { name = "greenlet" }, + { name = "mypy" }, + { name = "mypy-extensions" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-deadfixtures" }, + { name = "pytest-vcr" }, + { name = "python-dotenv" }, + { name = "ruff" }, + { name = "tox" }, + { name = "tox-uv" }, + { name = "types-redis" }, +] +docs = [ + { name = "mkdocs" }, + { name = "mkdocs-include-markdown-plugin" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extra = ["python"] }, + { name = "pymdown-extensions" }, +] + +[package.metadata] +requires-dist = [ + { name = "aio-pika", marker = "extra == 'aio-pika'", specifier = ">=9.5.3,<10.0.0" }, + { name = "aiokafka", marker = "extra == 'aiokafka'", specifier = ">=0.12.0,<1.0.0" }, + { name = "asyncpg", marker = "extra == 'asyncpg'", specifier = ">=0.30.0,<1.0.0" }, + { name = "fastapi", extras = ["standard"], marker = "extra == 'fastapi'", specifier = ">=0.115.6,<1.0.0" }, + { name = "faststream", marker = "extra == 'faststream'", specifier = ">=0.5.33,<1.0.0" }, + { name = "httpx", marker = "extra == 'httpx'", specifier = ">=0.28.1,<1.0.0" }, + { name = "litestar", marker = "extra == 'litestar'", specifier = ">=2.13.0,<3.0.0" }, + { name = "motor", marker = "extra == 'motor'", specifier = ">=3.6.0,<4.0.0" }, + { name = "msgspec", marker = "extra == 'msgspec'", git = "https://github.com/jcrist/msgspec.git?rev=main" }, + { name = "psycopg", marker = "extra == 'psycopg'", specifier = ">=3.2.3,<4.0.0" }, + { name = "pydantic", marker = "extra == 'pydantic'", specifier = ">=2.10.3,<3.0.0" }, + { name = "redis", marker = "extra == 'redis'", specifier = ">=5.2.1,<6.0.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "greenlet", specifier = ">=3.1.1,<4.0.0" }, + { name = "mypy", specifier = ">=1.13.0,<2.0.0" }, + { name = "mypy-extensions", specifier = ">=1.0.0,<2.0.0" }, + { name = "pre-commit", specifier = ">=4.0.1,<5.0.0" }, + { name = "pytest", specifier = ">=8.3.4,<9.0.0" }, + { name = "pytest-asyncio", specifier = ">=0.24.0,<1.0.0" }, + { name = "pytest-cov", specifier = ">=6.0.0,<7.0.0" }, + { name = "pytest-deadfixtures", specifier = ">=2.2.1,<3.0.0" }, + { name = "pytest-vcr", specifier = ">=1.0.2" }, + { name = "python-dotenv", specifier = ">=1.0.1,<2.0.0" }, + { name = "ruff", specifier = ">=0.8.2,<1.0.0" }, + { name = "tox", specifier = ">=4.23.2,<5.0.0" }, + { name = "tox-uv", specifier = ">=1.16.1,<2.0.0" }, + { name = "types-redis", specifier = ">=4.6.0.20241004,<5.0.0.0" }, +] +docs = [ + { name = "mkdocs", specifier = ">=1.6.1,<2.0.0" }, + { name = "mkdocs-include-markdown-plugin", specifier = ">=7.1.2" }, + { name = "mkdocs-material", specifier = ">=9.5.47,<10.0.0" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=0.27.0,<1.0.0" }, + { name = "pymdown-extensions", specifier = ">=10.12,<11.0" }, +] + +[[package]] +name = "fastapi" +version = "0.115.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/72/d83b98cd106541e8f5e5bfab8ef2974ab45a62e8a6c5b5e6940f26d2ed4b/fastapi-0.115.6.tar.gz", hash = "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654", size = 301336 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/b3/7e4df40e585df024fac2f80d1a2d579c854ac37109675db2b0cc22c0bb9e/fastapi-0.115.6-py3-none-any.whl", hash = "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305", size = 94843 }, +] + +[package.optional-dependencies] +standard = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"] }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich-toolkit" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/9d/9659cee212eeaecd8f54ab5f73d6d6d49a1dc4a46d2519fb9c1e66c8913e/fastapi_cli-0.0.6.tar.gz", hash = "sha256:2835a8f0c44b68e464d5cafe5ec205265f02dc1ad1d640db33a994ba3338003b", size = 16516 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/ab/0c8b6ec19594fe4c2e7fe12e8074648f59903047787ac05512c2935d5f7a/fastapi_cli-0.0.6-py3-none-any.whl", hash = "sha256:43288efee46338fae8902f9bf4559aed3aed639f9516f5d394a7ff19edcc8faf", size = 10687 }, +] + +[package.optional-dependencies] +standard = [ + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "faststream" +version = "0.5.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "fast-depends" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/51/33380ca2877a8b83e94d479b567cf15fe38bb2df0d3cbeb54d07f3b9e694/faststream-0.5.33.tar.gz", hash = "sha256:d40761c9f5db9b2cee69748832c4ead20d61805c439d3fbe3107fe4c9a066b2d", size = 286391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/db/a4aa303e425bf629523a3c706dd67035cac547a92b3c24504e194cafaf42/faststream-0.5.33-py3-none-any.whl", hash = "sha256:24b3b48dff4195c63141864fae24608c4e3e08c9172faccc74b0c46d6d88f665", size = 385102 }, +] + +[[package]] +name = "filelock" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 }, +] + +[[package]] +name = "greenlet" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/90/5234a78dc0ef6496a6eb97b67a42a8e96742a56f7dc808cb954a85390448/greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", size = 271235 }, + { url = "https://files.pythonhosted.org/packages/7c/16/cd631fa0ab7d06ef06387135b7549fdcc77d8d859ed770a0d28e47b20972/greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", size = 637168 }, + { url = "https://files.pythonhosted.org/packages/2f/b1/aed39043a6fec33c284a2c9abd63ce191f4f1a07319340ffc04d2ed3256f/greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", size = 648826 }, + { url = "https://files.pythonhosted.org/packages/76/25/40e0112f7f3ebe54e8e8ed91b2b9f970805143efef16d043dfc15e70f44b/greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", size = 644443 }, + { url = "https://files.pythonhosted.org/packages/fb/2f/3850b867a9af519794784a7eeed1dd5bc68ffbcc5b28cef703711025fd0a/greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", size = 643295 }, + { url = "https://files.pythonhosted.org/packages/cf/69/79e4d63b9387b48939096e25115b8af7cd8a90397a304f92436bcb21f5b2/greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", size = 599544 }, + { url = "https://files.pythonhosted.org/packages/46/1d/44dbcb0e6c323bd6f71b8c2f4233766a5faf4b8948873225d34a0b7efa71/greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", size = 1125456 }, + { url = "https://files.pythonhosted.org/packages/e0/1d/a305dce121838d0278cee39d5bb268c657f10a5363ae4b726848f833f1bb/greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", size = 1149111 }, + { url = "https://files.pythonhosted.org/packages/96/28/d62835fb33fb5652f2e98d34c44ad1a0feacc8b1d3f1aecab035f51f267d/greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", size = 298392 }, + { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479 }, + { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404 }, + { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813 }, + { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517 }, + { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831 }, + { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413 }, + { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619 }, + { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198 }, + { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930 }, + { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260 }, + { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064 }, + { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420 }, + { url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035 }, + { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105 }, + { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077 }, + { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975 }, + { url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955 }, + { url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655 }, + { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990 }, + { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175 }, + { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425 }, + { url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736 }, + { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347 }, + { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583 }, + { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039 }, + { url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716 }, + { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490 }, + { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731 }, + { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304 }, + { url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537 }, + { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506 }, + { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753 }, + { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731 }, + { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 }, +] + +[[package]] +name = "griffe" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/c9/8167810358ca129839156dc002526e7398b5fad4a9d7b6e88b875e802d0d/griffe-1.5.1.tar.gz", hash = "sha256:72964f93e08c553257706d6cd2c42d1c172213feb48b2be386f243380b405d4b", size = 384113 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/00/e693a155da0a2a72fd2df75b8fe338146cae59d590ad6f56800adde90cb5/griffe-1.5.1-py3-none-any.whl", hash = "sha256:ad6a7980f8c424c9102160aafa3bcdf799df0e75f7829d75af9ee5aef656f860", size = 127132 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, +] + +[[package]] +name = "httptools" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/6f/972f8eb0ea7d98a1c6be436e2142d51ad2a64ee18e02b0e7ff1f62171ab1/httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0", size = 198780 }, + { url = "https://files.pythonhosted.org/packages/6a/b0/17c672b4bc5c7ba7f201eada4e96c71d0a59fbc185e60e42580093a86f21/httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da", size = 103297 }, + { url = "https://files.pythonhosted.org/packages/92/5e/b4a826fe91971a0b68e8c2bd4e7db3e7519882f5a8ccdb1194be2b3ab98f/httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1", size = 443130 }, + { url = "https://files.pythonhosted.org/packages/b0/51/ce61e531e40289a681a463e1258fa1e05e0be54540e40d91d065a264cd8f/httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50", size = 442148 }, + { url = "https://files.pythonhosted.org/packages/ea/9e/270b7d767849b0c96f275c695d27ca76c30671f8eb8cc1bab6ced5c5e1d0/httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959", size = 415949 }, + { url = "https://files.pythonhosted.org/packages/81/86/ced96e3179c48c6f656354e106934e65c8963d48b69be78f355797f0e1b3/httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4", size = 417591 }, + { url = "https://files.pythonhosted.org/packages/75/73/187a3f620ed3175364ddb56847d7a608a6fc42d551e133197098c0143eca/httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c", size = 88344 }, + { url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029 }, + { url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492 }, + { url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891 }, + { url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", size = 459788 }, + { url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", size = 433214 }, + { url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", size = 434120 }, + { url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", size = 88565 }, + { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683 }, + { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337 }, + { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796 }, + { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837 }, + { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289 }, + { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779 }, + { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634 }, + { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214 }, + { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431 }, + { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121 }, + { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805 }, + { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858 }, + { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042 }, + { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "identify" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/5f/05f0d167be94585d502b4adf8c7af31f1dc0b1c7e14f9938a88fdbbcf4a7/identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02", size = 99179 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/f5/09644a3ad803fae9eca8efa17e1f2aef380c7f0b02f7ec4e8d446e51d64a/identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd", size = 99049 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, +] + +[[package]] +name = "litestar" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "click" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11' and python_full_version >= '3.10.0' and python_full_version < '4.0.0'" }, + { name = "httpx" }, + { name = "litestar-htmx" }, + { name = "msgspec" }, + { name = "multidict" }, + { name = "polyfactory" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "rich-click" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/c17fc38194d63c538d65cf4bdfbfe6dfc476579c39732664858b6e5dbddc/litestar-2.13.0.tar.gz", hash = "sha256:51a3ab60b7bc8de2c126f3ad907c2ba6f9d22194bdf1be9df52253e57ed80f0e", size = 725824 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/1b/31c8d75c98f69748c49d7b4196539d0e56b3c785f2b6b8e6b4686e6a934a/litestar-2.13.0-py3-none-any.whl", hash = "sha256:a40765644115639015a54e8cd7e7bdbe597a58d3f2d8f6d21afe9f343df43916", size = 555532 }, +] + +[[package]] +name = "litestar-htmx" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/0c/06ab03ee497d207dd8cb7588d1940be0b373a8ffdc7be3ec6d7e91c17ae2/litestar_htmx-0.4.1.tar.gz", hash = "sha256:ba2537008eb8cc18bfc8bee5cecb280924c7818bb1c066d79eae4b221696ca08", size = 101877 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/99/3ea64a79a2f4fea5225ccd0128201a3b8eab5e216b8fba8b778b8c462f29/litestar_htmx-0.4.1-py3-none-any.whl", hash = "sha256:ba2a8ff1e210f21980735b9cde13d239a2b7c3627cb4aeb425d66f4a314d1a59", size = 9970 }, +] + +[[package]] +name = "markdown" +version = "3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354 }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451 }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/ae/0f1154c614d6a8b8a36fff084e5b82af3a15f7d2060cf0dcdb1c53297a71/mkdocs_autorefs-1.2.0.tar.gz", hash = "sha256:a86b93abff653521bda71cf3fc5596342b7a23982093915cb74273f67522190f", size = 40262 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl", hash = "sha256:d588754ae89bd0ced0c70c06f58566a4ee43471eeeee5202427da7de9ef85a2f", size = 16522 }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 }, +] + +[[package]] +name = "mkdocs-include-markdown-plugin" +version = "7.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, + { name = "wcmatch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/c6/863c7564872aaebc0d00f3f002adf5ef85f1f5549a44f94aec4c4624c630/mkdocs_include_markdown_plugin-7.1.2.tar.gz", hash = "sha256:1b393157b1aa231b0e6c59ba80f52b723f4b7827bb7a1264b505334f8542aaf1", size = 22213 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/3c/41ffab81ef2f1c2186a0fa563680274ad03a651eddf02a1488f69a49524e/mkdocs_include_markdown_plugin-7.1.2-py3-none-any.whl", hash = "sha256:ff1175d1b4f83dea6a38e200d6f0c3db10308975bf60c197d31172671753dbc4", size = 25944 }, +] + +[[package]] +name = "mkdocs-material" +version = "9.5.47" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/0a/6b5a5761d6e500f0c6de9ae24461ac93c66b35785faac331e6d66522776f/mkdocs_material-9.5.47.tar.gz", hash = "sha256:fc3b7a8e00ad896660bd3a5cc12ca0cb28bdc2bcbe2a946b5714c23ac91b0ede", size = 3910665 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/ef/25150e53836255bc8a2cee958e251516035e85b307774fbcfc6bda0d0388/mkdocs_material-9.5.47-py3-none-any.whl", hash = "sha256:53fb9c9624e7865da6ec807d116cd7be24b3cb36ab31b1d1d1a9af58c56009a2", size = 8625827 }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 }, +] + +[[package]] +name = "mkdocstrings" +version = "0.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "platformdirs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/5a/5de70538c2cefae7ac3a15b5601e306ef3717290cb2aab11d51cbbc2d1c0/mkdocstrings-0.27.0.tar.gz", hash = "sha256:16adca6d6b0a1f9e0c07ff0b02ced8e16f228a9d65a37c063ec4c14d7b76a657", size = 94830 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/10/4c27c3063c2b3681a4b7942f8dbdeb4fa34fecb2c19b594e7345ebf4f86f/mkdocstrings-0.27.0-py3-none-any.whl", hash = "sha256:6ceaa7ea830770959b55a16203ac63da24badd71325b96af950e59fd37366332", size = 30658 }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.12.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffe" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/ec/cb6debe2db77f1ef42b25b21d93b5021474de3037cd82385e586aee72545/mkdocstrings_python-1.12.2.tar.gz", hash = "sha256:7a1760941c0b52a2cd87b960a9e21112ffe52e7df9d0b9583d04d47ed2e186f3", size = 168207 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/c1/ac524e1026d9580cbc654b5d19f5843c8b364a66d30f956372cd09fd2f92/mkdocstrings_python-1.12.2-py3-none-any.whl", hash = "sha256:7f7d40d6db3cb1f5d19dbcd80e3efe4d0ba32b073272c0c0de9de2e604eda62a", size = 111759 }, +] + +[[package]] +name = "motor" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pymongo" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/d1/06af0527fd02d49b203db70dba462e47275a3c1094f830fdaf090f0cb20c/motor-3.6.0.tar.gz", hash = "sha256:0ef7f520213e852bf0eac306adf631aabe849227d8aec900a2612512fb9c5b8d", size = 278447 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/c2/bba4dce0dc56e49d95c270c79c9330ed19e6b71a2a633aecf53e7e1f04c9/motor-3.6.0-py3-none-any.whl", hash = "sha256:9f07ed96f1754963d4386944e1b52d403a5350c687edc60da487d66f98dbf894", size = 74802 }, +] + +[[package]] +name = "msgspec" +version = "0.18.6+30.g595c33c" +source = { git = "https://github.com/jcrist/msgspec.git?rev=main#595c33c4a71c6d0c539b82233982a65819e240cf" } + +[[package]] +name = "multidict" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11' and python_full_version >= '3.10.0' and python_full_version < '4.0.0'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/68/259dee7fd14cf56a17c554125e534f6274c2860159692a414d0b402b9a6d/multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60", size = 48628 }, + { url = "https://files.pythonhosted.org/packages/50/79/53ba256069fe5386a4a9e80d4e12857ced9de295baf3e20c68cdda746e04/multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1", size = 29327 }, + { url = "https://files.pythonhosted.org/packages/ff/10/71f1379b05b196dae749b5ac062e87273e3f11634f447ebac12a571d90ae/multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53", size = 29689 }, + { url = "https://files.pythonhosted.org/packages/71/45/70bac4f87438ded36ad4793793c0095de6572d433d98575a5752629ef549/multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5", size = 126639 }, + { url = "https://files.pythonhosted.org/packages/80/cf/17f35b3b9509b4959303c05379c4bfb0d7dd05c3306039fc79cf035bbac0/multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581", size = 134315 }, + { url = "https://files.pythonhosted.org/packages/ef/1f/652d70ab5effb33c031510a3503d4d6efc5ec93153562f1ee0acdc895a57/multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56", size = 129471 }, + { url = "https://files.pythonhosted.org/packages/a6/64/2dd6c4c681688c0165dea3975a6a4eab4944ea30f35000f8b8af1df3148c/multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429", size = 124585 }, + { url = "https://files.pythonhosted.org/packages/87/56/e6ee5459894c7e554b57ba88f7257dc3c3d2d379cb15baaa1e265b8c6165/multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748", size = 116957 }, + { url = "https://files.pythonhosted.org/packages/36/9e/616ce5e8d375c24b84f14fc263c7ef1d8d5e8ef529dbc0f1df8ce71bb5b8/multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db", size = 128609 }, + { url = "https://files.pythonhosted.org/packages/8c/4f/4783e48a38495d000f2124020dc96bacc806a4340345211b1ab6175a6cb4/multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056", size = 123016 }, + { url = "https://files.pythonhosted.org/packages/3e/b3/4950551ab8fc39862ba5e9907dc821f896aa829b4524b4deefd3e12945ab/multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76", size = 133542 }, + { url = "https://files.pythonhosted.org/packages/96/4d/f0ce6ac9914168a2a71df117935bb1f1781916acdecbb43285e225b484b8/multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160", size = 130163 }, + { url = "https://files.pythonhosted.org/packages/be/72/17c9f67e7542a49dd252c5ae50248607dfb780bcc03035907dafefb067e3/multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7", size = 126832 }, + { url = "https://files.pythonhosted.org/packages/71/9f/72d719e248cbd755c8736c6d14780533a1606ffb3fbb0fbd77da9f0372da/multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0", size = 26402 }, + { url = "https://files.pythonhosted.org/packages/04/5a/d88cd5d00a184e1ddffc82aa2e6e915164a6d2641ed3606e766b5d2f275a/multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d", size = 28800 }, + { url = "https://files.pythonhosted.org/packages/93/13/df3505a46d0cd08428e4c8169a196131d1b0c4b515c3649829258843dde6/multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6", size = 48570 }, + { url = "https://files.pythonhosted.org/packages/f0/e1/a215908bfae1343cdb72f805366592bdd60487b4232d039c437fe8f5013d/multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156", size = 29316 }, + { url = "https://files.pythonhosted.org/packages/70/0f/6dc70ddf5d442702ed74f298d69977f904960b82368532c88e854b79f72b/multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb", size = 29640 }, + { url = "https://files.pythonhosted.org/packages/d8/6d/9c87b73a13d1cdea30b321ef4b3824449866bd7f7127eceed066ccb9b9ff/multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b", size = 131067 }, + { url = "https://files.pythonhosted.org/packages/cc/1e/1b34154fef373371fd6c65125b3d42ff5f56c7ccc6bfff91b9b3c60ae9e0/multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72", size = 138507 }, + { url = "https://files.pythonhosted.org/packages/fb/e0/0bc6b2bac6e461822b5f575eae85da6aae76d0e2a79b6665d6206b8e2e48/multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304", size = 133905 }, + { url = "https://files.pythonhosted.org/packages/ba/af/73d13b918071ff9b2205fcf773d316e0f8fefb4ec65354bbcf0b10908cc6/multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351", size = 129004 }, + { url = "https://files.pythonhosted.org/packages/74/21/23960627b00ed39643302d81bcda44c9444ebcdc04ee5bedd0757513f259/multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb", size = 121308 }, + { url = "https://files.pythonhosted.org/packages/8b/5c/cf282263ffce4a596ed0bb2aa1a1dddfe1996d6a62d08842a8d4b33dca13/multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3", size = 132608 }, + { url = "https://files.pythonhosted.org/packages/d7/3e/97e778c041c72063f42b290888daff008d3ab1427f5b09b714f5a8eff294/multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399", size = 127029 }, + { url = "https://files.pythonhosted.org/packages/47/ac/3efb7bfe2f3aefcf8d103e9a7162572f01936155ab2f7ebcc7c255a23212/multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423", size = 137594 }, + { url = "https://files.pythonhosted.org/packages/42/9b/6c6e9e8dc4f915fc90a9b7798c44a30773dea2995fdcb619870e705afe2b/multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3", size = 134556 }, + { url = "https://files.pythonhosted.org/packages/1d/10/8e881743b26aaf718379a14ac58572a240e8293a1c9d68e1418fb11c0f90/multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753", size = 130993 }, + { url = "https://files.pythonhosted.org/packages/45/84/3eb91b4b557442802d058a7579e864b329968c8d0ea57d907e7023c677f2/multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80", size = 26405 }, + { url = "https://files.pythonhosted.org/packages/9f/0b/ad879847ecbf6d27e90a6eabb7eff6b62c129eefe617ea45eae7c1f0aead/multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926", size = 28795 }, + { url = "https://files.pythonhosted.org/packages/fd/16/92057c74ba3b96d5e211b553895cd6dc7cc4d1e43d9ab8fafc727681ef71/multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa", size = 48713 }, + { url = "https://files.pythonhosted.org/packages/94/3d/37d1b8893ae79716179540b89fc6a0ee56b4a65fcc0d63535c6f5d96f217/multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436", size = 29516 }, + { url = "https://files.pythonhosted.org/packages/a2/12/adb6b3200c363062f805275b4c1e656be2b3681aada66c80129932ff0bae/multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761", size = 29557 }, + { url = "https://files.pythonhosted.org/packages/47/e9/604bb05e6e5bce1e6a5cf80a474e0f072e80d8ac105f1b994a53e0b28c42/multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e", size = 130170 }, + { url = "https://files.pythonhosted.org/packages/7e/13/9efa50801785eccbf7086b3c83b71a4fb501a4d43549c2f2f80b8787d69f/multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef", size = 134836 }, + { url = "https://files.pythonhosted.org/packages/bf/0f/93808b765192780d117814a6dfcc2e75de6dcc610009ad408b8814dca3ba/multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95", size = 133475 }, + { url = "https://files.pythonhosted.org/packages/d3/c8/529101d7176fe7dfe1d99604e48d69c5dfdcadb4f06561f465c8ef12b4df/multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925", size = 131049 }, + { url = "https://files.pythonhosted.org/packages/ca/0c/fc85b439014d5a58063e19c3a158a889deec399d47b5269a0f3b6a2e28bc/multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966", size = 120370 }, + { url = "https://files.pythonhosted.org/packages/db/46/d4416eb20176492d2258fbd47b4abe729ff3b6e9c829ea4236f93c865089/multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305", size = 125178 }, + { url = "https://files.pythonhosted.org/packages/5b/46/73697ad7ec521df7de5531a32780bbfd908ded0643cbe457f981a701457c/multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2", size = 119567 }, + { url = "https://files.pythonhosted.org/packages/cd/ed/51f060e2cb0e7635329fa6ff930aa5cffa17f4c7f5c6c3ddc3500708e2f2/multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2", size = 129822 }, + { url = "https://files.pythonhosted.org/packages/df/9e/ee7d1954b1331da3eddea0c4e08d9142da5f14b1321c7301f5014f49d492/multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6", size = 128656 }, + { url = "https://files.pythonhosted.org/packages/77/00/8538f11e3356b5d95fa4b024aa566cde7a38aa7a5f08f4912b32a037c5dc/multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3", size = 125360 }, + { url = "https://files.pythonhosted.org/packages/be/05/5d334c1f2462d43fec2363cd00b1c44c93a78c3925d952e9a71caf662e96/multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133", size = 26382 }, + { url = "https://files.pythonhosted.org/packages/a3/bf/f332a13486b1ed0496d624bcc7e8357bb8053823e8cd4b9a18edc1d97e73/multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1", size = 28529 }, + { url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 }, + { url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 }, + { url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 }, + { url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 }, + { url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 }, + { url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 }, + { url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 }, + { url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 }, + { url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 }, + { url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 }, + { url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 }, + { url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 }, + { url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 }, + { url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 }, + { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, +] + +[[package]] +name = "mypy" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "tomli", marker = "python_full_version < '3.11' and python_full_version >= '3.10.0' and python_full_version < '4.0.0'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/8c/206de95a27722b5b5a8c85ba3100467bd86299d92a4f71c6b9aa448bfa2f/mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a", size = 11020731 }, + { url = "https://files.pythonhosted.org/packages/ab/bb/b31695a29eea76b1569fd28b4ab141a1adc9842edde080d1e8e1776862c7/mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80", size = 10184276 }, + { url = "https://files.pythonhosted.org/packages/a5/2d/4a23849729bb27934a0e079c9c1aad912167d875c7b070382a408d459651/mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7", size = 12587706 }, + { url = "https://files.pythonhosted.org/packages/5c/c3/d318e38ada50255e22e23353a469c791379825240e71b0ad03e76ca07ae6/mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f", size = 13105586 }, + { url = "https://files.pythonhosted.org/packages/4a/25/3918bc64952370c3dbdbd8c82c363804678127815febd2925b7273d9482c/mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372", size = 9632318 }, + { url = "https://files.pythonhosted.org/packages/d0/19/de0822609e5b93d02579075248c7aa6ceaddcea92f00bf4ea8e4c22e3598/mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d", size = 10939027 }, + { url = "https://files.pythonhosted.org/packages/c8/71/6950fcc6ca84179137e4cbf7cf41e6b68b4a339a1f5d3e954f8c34e02d66/mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d", size = 10108699 }, + { url = "https://files.pythonhosted.org/packages/26/50/29d3e7dd166e74dc13d46050b23f7d6d7533acf48f5217663a3719db024e/mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b", size = 12506263 }, + { url = "https://files.pythonhosted.org/packages/3f/1d/676e76f07f7d5ddcd4227af3938a9c9640f293b7d8a44dd4ff41d4db25c1/mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73", size = 12984688 }, + { url = "https://files.pythonhosted.org/packages/9c/03/5a85a30ae5407b1d28fab51bd3e2103e52ad0918d1e68f02a7778669a307/mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", size = 9626811 }, + { url = "https://files.pythonhosted.org/packages/fb/31/c526a7bd2e5c710ae47717c7a5f53f616db6d9097caf48ad650581e81748/mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5", size = 11077900 }, + { url = "https://files.pythonhosted.org/packages/83/67/b7419c6b503679d10bd26fc67529bc6a1f7a5f220bbb9f292dc10d33352f/mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e", size = 10074818 }, + { url = "https://files.pythonhosted.org/packages/ba/07/37d67048786ae84e6612575e173d713c9a05d0ae495dde1e68d972207d98/mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", size = 12589275 }, + { url = "https://files.pythonhosted.org/packages/1f/17/b1018c6bb3e9f1ce3956722b3bf91bff86c1cefccca71cec05eae49d6d41/mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", size = 13037783 }, + { url = "https://files.pythonhosted.org/packages/cb/32/cd540755579e54a88099aee0287086d996f5a24281a673f78a0e14dba150/mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", size = 9726197 }, + { url = "https://files.pythonhosted.org/packages/11/bb/ab4cfdc562cad80418f077d8be9b4491ee4fb257440da951b85cbb0a639e/mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", size = 11069721 }, + { url = "https://files.pythonhosted.org/packages/59/3b/a393b1607cb749ea2c621def5ba8c58308ff05e30d9dbdc7c15028bca111/mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", size = 10063996 }, + { url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043 }, + { url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996 }, + { url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709 }, + { url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 }, +] + +[[package]] +name = "pamqp" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/62/35bbd3d3021e008606cd0a9532db7850c65741bbf69ac8a3a0d8cfeb7934/pamqp-3.3.0.tar.gz", hash = "sha256:40b8795bd4efcf2b0f8821c1de83d12ca16d5760f4507836267fd7a02b06763b", size = 30993 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/8d/c1e93296e109a320e508e38118cf7d1fc2a4d1c2ec64de78565b3c445eb5/pamqp-3.3.0-py2.py3-none-any.whl", hash = "sha256:c901a684794157ae39b52cbf700db8c9aae7a470f13528b9d7b4e5f7202f8eb0", size = 33848 }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "polyfactory" +version = "2.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "faker" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/0c/12b4e50ab0d165f34ae65fbf26bd93debc8d6c4e00ea62a0b086c9eb58d0/polyfactory-2.18.1.tar.gz", hash = "sha256:17c9db18afe4fb8d7dd8e5ba296e69da0fcf7d0f3b63d1840eb10d135aed5aad", size = 185001 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/80/e0bfd57b64009f476112fa81056eb64d9c95bbbbf5bb3257ad010f89907a/polyfactory-2.18.1-py3-none-any.whl", hash = "sha256:1a2b0715e08bfe9f14abc838fc013ab8772cb90e66f2e601e15e1127f0bc1b18", size = 59335 }, +] + +[[package]] +name = "pre-commit" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 }, +] + +[[package]] +name = "propcache" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/c8/2a13f78d82211490855b2fb303b6721348d0787fdd9a12ac46d99d3acde1/propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64", size = 41735 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a5/0ea64c9426959ef145a938e38c832fc551843481d356713ececa9a8a64e8/propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6", size = 79296 }, + { url = "https://files.pythonhosted.org/packages/76/5a/916db1aba735f55e5eca4733eea4d1973845cf77dfe67c2381a2ca3ce52d/propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2", size = 45622 }, + { url = "https://files.pythonhosted.org/packages/2d/62/685d3cf268b8401ec12b250b925b21d152b9d193b7bffa5fdc4815c392c2/propcache-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea", size = 45133 }, + { url = "https://files.pythonhosted.org/packages/4d/3d/31c9c29ee7192defc05aa4d01624fd85a41cf98e5922aaed206017329944/propcache-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212", size = 204809 }, + { url = "https://files.pythonhosted.org/packages/10/a1/e4050776f4797fc86140ac9a480d5dc069fbfa9d499fe5c5d2fa1ae71f07/propcache-0.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3", size = 219109 }, + { url = "https://files.pythonhosted.org/packages/c9/c0/e7ae0df76343d5e107d81e59acc085cea5fd36a48aa53ef09add7503e888/propcache-0.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d", size = 217368 }, + { url = "https://files.pythonhosted.org/packages/fc/e1/e0a2ed6394b5772508868a977d3238f4afb2eebaf9976f0b44a8d347ad63/propcache-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634", size = 205124 }, + { url = "https://files.pythonhosted.org/packages/50/c1/e388c232d15ca10f233c778bbdc1034ba53ede14c207a72008de45b2db2e/propcache-0.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2", size = 195463 }, + { url = "https://files.pythonhosted.org/packages/0a/fd/71b349b9def426cc73813dbd0f33e266de77305e337c8c12bfb0a2a82bfb/propcache-0.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958", size = 198358 }, + { url = "https://files.pythonhosted.org/packages/02/f2/d7c497cd148ebfc5b0ae32808e6c1af5922215fe38c7a06e4e722fe937c8/propcache-0.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c", size = 195560 }, + { url = "https://files.pythonhosted.org/packages/bb/57/f37041bbe5e0dfed80a3f6be2612a3a75b9cfe2652abf2c99bef3455bbad/propcache-0.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583", size = 196895 }, + { url = "https://files.pythonhosted.org/packages/83/36/ae3cc3e4f310bff2f064e3d2ed5558935cc7778d6f827dce74dcfa125304/propcache-0.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf", size = 207124 }, + { url = "https://files.pythonhosted.org/packages/8c/c4/811b9f311f10ce9d31a32ff14ce58500458443627e4df4ae9c264defba7f/propcache-0.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034", size = 210442 }, + { url = "https://files.pythonhosted.org/packages/18/dd/a1670d483a61ecac0d7fc4305d91caaac7a8fc1b200ea3965a01cf03bced/propcache-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b", size = 203219 }, + { url = "https://files.pythonhosted.org/packages/f9/2d/30ced5afde41b099b2dc0c6573b66b45d16d73090e85655f1a30c5a24e07/propcache-0.2.1-cp310-cp310-win32.whl", hash = "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4", size = 40313 }, + { url = "https://files.pythonhosted.org/packages/23/84/bd9b207ac80da237af77aa6e153b08ffa83264b1c7882495984fcbfcf85c/propcache-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba", size = 44428 }, + { url = "https://files.pythonhosted.org/packages/bc/0f/2913b6791ebefb2b25b4efd4bb2299c985e09786b9f5b19184a88e5778dd/propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16", size = 79297 }, + { url = "https://files.pythonhosted.org/packages/cf/73/af2053aeccd40b05d6e19058419ac77674daecdd32478088b79375b9ab54/propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717", size = 45611 }, + { url = "https://files.pythonhosted.org/packages/3c/09/8386115ba7775ea3b9537730e8cf718d83bbf95bffe30757ccf37ec4e5da/propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3", size = 45146 }, + { url = "https://files.pythonhosted.org/packages/03/7a/793aa12f0537b2e520bf09f4c6833706b63170a211ad042ca71cbf79d9cb/propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9", size = 232136 }, + { url = "https://files.pythonhosted.org/packages/f1/38/b921b3168d72111769f648314100558c2ea1d52eb3d1ba7ea5c4aa6f9848/propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787", size = 239706 }, + { url = "https://files.pythonhosted.org/packages/14/29/4636f500c69b5edea7786db3c34eb6166f3384b905665ce312a6e42c720c/propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465", size = 238531 }, + { url = "https://files.pythonhosted.org/packages/85/14/01fe53580a8e1734ebb704a3482b7829a0ef4ea68d356141cf0994d9659b/propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af", size = 231063 }, + { url = "https://files.pythonhosted.org/packages/33/5c/1d961299f3c3b8438301ccfbff0143b69afcc30c05fa28673cface692305/propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7", size = 220134 }, + { url = "https://files.pythonhosted.org/packages/00/d0/ed735e76db279ba67a7d3b45ba4c654e7b02bc2f8050671ec365d8665e21/propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f", size = 220009 }, + { url = "https://files.pythonhosted.org/packages/75/90/ee8fab7304ad6533872fee982cfff5a53b63d095d78140827d93de22e2d4/propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54", size = 212199 }, + { url = "https://files.pythonhosted.org/packages/eb/ec/977ffaf1664f82e90737275873461695d4c9407d52abc2f3c3e24716da13/propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505", size = 214827 }, + { url = "https://files.pythonhosted.org/packages/57/48/031fb87ab6081764054821a71b71942161619549396224cbb242922525e8/propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82", size = 228009 }, + { url = "https://files.pythonhosted.org/packages/1a/06/ef1390f2524850838f2390421b23a8b298f6ce3396a7cc6d39dedd4047b0/propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca", size = 231638 }, + { url = "https://files.pythonhosted.org/packages/38/2a/101e6386d5a93358395da1d41642b79c1ee0f3b12e31727932b069282b1d/propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e", size = 222788 }, + { url = "https://files.pythonhosted.org/packages/db/81/786f687951d0979007e05ad9346cd357e50e3d0b0f1a1d6074df334b1bbb/propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034", size = 40170 }, + { url = "https://files.pythonhosted.org/packages/cf/59/7cc7037b295d5772eceb426358bb1b86e6cab4616d971bd74275395d100d/propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3", size = 44404 }, + { url = "https://files.pythonhosted.org/packages/4c/28/1d205fe49be8b1b4df4c50024e62480a442b1a7b818e734308bb0d17e7fb/propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a", size = 79588 }, + { url = "https://files.pythonhosted.org/packages/21/ee/fc4d893f8d81cd4971affef2a6cb542b36617cd1d8ce56b406112cb80bf7/propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0", size = 45825 }, + { url = "https://files.pythonhosted.org/packages/4a/de/bbe712f94d088da1d237c35d735f675e494a816fd6f54e9db2f61ef4d03f/propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d", size = 45357 }, + { url = "https://files.pythonhosted.org/packages/7f/14/7ae06a6cf2a2f1cb382586d5a99efe66b0b3d0c6f9ac2f759e6f7af9d7cf/propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4", size = 241869 }, + { url = "https://files.pythonhosted.org/packages/cc/59/227a78be960b54a41124e639e2c39e8807ac0c751c735a900e21315f8c2b/propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d", size = 247884 }, + { url = "https://files.pythonhosted.org/packages/84/58/f62b4ffaedf88dc1b17f04d57d8536601e4e030feb26617228ef930c3279/propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5", size = 248486 }, + { url = "https://files.pythonhosted.org/packages/1c/07/ebe102777a830bca91bbb93e3479cd34c2ca5d0361b83be9dbd93104865e/propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24", size = 243649 }, + { url = "https://files.pythonhosted.org/packages/ed/bc/4f7aba7f08f520376c4bb6a20b9a981a581b7f2e385fa0ec9f789bb2d362/propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff", size = 229103 }, + { url = "https://files.pythonhosted.org/packages/fe/d5/04ac9cd4e51a57a96f78795e03c5a0ddb8f23ec098b86f92de028d7f2a6b/propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f", size = 226607 }, + { url = "https://files.pythonhosted.org/packages/e3/f0/24060d959ea41d7a7cc7fdbf68b31852331aabda914a0c63bdb0e22e96d6/propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec", size = 221153 }, + { url = "https://files.pythonhosted.org/packages/77/a7/3ac76045a077b3e4de4859a0753010765e45749bdf53bd02bc4d372da1a0/propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348", size = 222151 }, + { url = "https://files.pythonhosted.org/packages/e7/af/5e29da6f80cebab3f5a4dcd2a3240e7f56f2c4abf51cbfcc99be34e17f0b/propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6", size = 233812 }, + { url = "https://files.pythonhosted.org/packages/8c/89/ebe3ad52642cc5509eaa453e9f4b94b374d81bae3265c59d5c2d98efa1b4/propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6", size = 238829 }, + { url = "https://files.pythonhosted.org/packages/e9/2f/6b32f273fa02e978b7577159eae7471b3cfb88b48563b1c2578b2d7ca0bb/propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518", size = 230704 }, + { url = "https://files.pythonhosted.org/packages/5c/2e/f40ae6ff5624a5f77edd7b8359b208b5455ea113f68309e2b00a2e1426b6/propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246", size = 40050 }, + { url = "https://files.pythonhosted.org/packages/3b/77/a92c3ef994e47180862b9d7d11e37624fb1c00a16d61faf55115d970628b/propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1", size = 44117 }, + { url = "https://files.pythonhosted.org/packages/0f/2a/329e0547cf2def8857157f9477669043e75524cc3e6251cef332b3ff256f/propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc", size = 77002 }, + { url = "https://files.pythonhosted.org/packages/12/2d/c4df5415e2382f840dc2ecbca0eeb2293024bc28e57a80392f2012b4708c/propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9", size = 44639 }, + { url = "https://files.pythonhosted.org/packages/d0/5a/21aaa4ea2f326edaa4e240959ac8b8386ea31dedfdaa636a3544d9e7a408/propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439", size = 44049 }, + { url = "https://files.pythonhosted.org/packages/4e/3e/021b6cd86c0acc90d74784ccbb66808b0bd36067a1bf3e2deb0f3845f618/propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536", size = 224819 }, + { url = "https://files.pythonhosted.org/packages/3c/57/c2fdeed1b3b8918b1770a133ba5c43ad3d78e18285b0c06364861ef5cc38/propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629", size = 229625 }, + { url = "https://files.pythonhosted.org/packages/9d/81/70d4ff57bf2877b5780b466471bebf5892f851a7e2ca0ae7ffd728220281/propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b", size = 232934 }, + { url = "https://files.pythonhosted.org/packages/3c/b9/bb51ea95d73b3fb4100cb95adbd4e1acaf2cbb1fd1083f5468eeb4a099a8/propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052", size = 227361 }, + { url = "https://files.pythonhosted.org/packages/f1/20/3c6d696cd6fd70b29445960cc803b1851a1131e7a2e4ee261ee48e002bcd/propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce", size = 213904 }, + { url = "https://files.pythonhosted.org/packages/a1/cb/1593bfc5ac6d40c010fa823f128056d6bc25b667f5393781e37d62f12005/propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d", size = 212632 }, + { url = "https://files.pythonhosted.org/packages/6d/5c/e95617e222be14a34c709442a0ec179f3207f8a2b900273720501a70ec5e/propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce", size = 207897 }, + { url = "https://files.pythonhosted.org/packages/8e/3b/56c5ab3dc00f6375fbcdeefdede5adf9bee94f1fab04adc8db118f0f9e25/propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95", size = 208118 }, + { url = "https://files.pythonhosted.org/packages/86/25/d7ef738323fbc6ebcbce33eb2a19c5e07a89a3df2fded206065bd5e868a9/propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf", size = 217851 }, + { url = "https://files.pythonhosted.org/packages/b3/77/763e6cef1852cf1ba740590364ec50309b89d1c818e3256d3929eb92fabf/propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f", size = 222630 }, + { url = "https://files.pythonhosted.org/packages/4f/e9/0f86be33602089c701696fbed8d8c4c07b6ee9605c5b7536fd27ed540c5b/propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30", size = 216269 }, + { url = "https://files.pythonhosted.org/packages/cc/02/5ac83217d522394b6a2e81a2e888167e7ca629ef6569a3f09852d6dcb01a/propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6", size = 39472 }, + { url = "https://files.pythonhosted.org/packages/f4/33/d6f5420252a36034bc8a3a01171bc55b4bff5df50d1c63d9caa50693662f/propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1", size = 43363 }, + { url = "https://files.pythonhosted.org/packages/41/b6/c5319caea262f4821995dca2107483b94a3345d4607ad797c76cb9c36bcc/propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54", size = 11818 }, +] + +[[package]] +name = "psycopg" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13' and python_full_version >= '3.10.0' and python_full_version < '4.0.0'" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/ad/7ce016ae63e231575df0498d2395d15f005f05e32d3a2d439038e1bd0851/psycopg-3.2.3.tar.gz", hash = "sha256:a5764f67c27bec8bfac85764d23c534af2c27b893550377e37ce59c12aac47a2", size = 155550 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/21/534b8f5bd9734b7a2fcd3a16b1ee82ef6cad81a4796e95ebf4e0c6a24119/psycopg-3.2.3-py3-none-any.whl", hash = "sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907", size = 197934 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pydantic" +version = "2.10.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/0f/27908242621b14e649a84e62b133de45f84c255eecb350ab02979844a788/pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9", size = 786486 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/51/72c18c55cf2f46ff4f91ebcc8f75aa30f7305f3d726be3f4ebffb4ae972b/pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d", size = 456997 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/9f/7de1f19b6aea45aeb441838782d68352e71bfa98ee6fa048d5041991b33e/pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", size = 412785 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/ce/60fd96895c09738648c83f3f00f595c807cb6735c70d3306b548cc96dd49/pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a", size = 1897984 }, + { url = "https://files.pythonhosted.org/packages/fd/b9/84623d6b6be98cc209b06687d9bca5a7b966ffed008d15225dd0d20cce2e/pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b", size = 1807491 }, + { url = "https://files.pythonhosted.org/packages/01/72/59a70165eabbc93b1111d42df9ca016a4aa109409db04304829377947028/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278", size = 1831953 }, + { url = "https://files.pythonhosted.org/packages/7c/0c/24841136476adafd26f94b45bb718a78cb0500bd7b4f8d667b67c29d7b0d/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05", size = 1856071 }, + { url = "https://files.pythonhosted.org/packages/53/5e/c32957a09cceb2af10d7642df45d1e3dbd8596061f700eac93b801de53c0/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4", size = 2038439 }, + { url = "https://files.pythonhosted.org/packages/e4/8f/979ab3eccd118b638cd6d8f980fea8794f45018255a36044dea40fe579d4/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f", size = 2787416 }, + { url = "https://files.pythonhosted.org/packages/02/1d/00f2e4626565b3b6d3690dab4d4fe1a26edd6a20e53749eb21ca892ef2df/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08", size = 2134548 }, + { url = "https://files.pythonhosted.org/packages/9d/46/3112621204128b90898adc2e721a3cd6cf5626504178d6f32c33b5a43b79/pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6", size = 1989882 }, + { url = "https://files.pythonhosted.org/packages/49/ec/557dd4ff5287ffffdf16a31d08d723de6762bb1b691879dc4423392309bc/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807", size = 1995829 }, + { url = "https://files.pythonhosted.org/packages/6e/b2/610dbeb74d8d43921a7234555e4c091cb050a2bdb8cfea86d07791ce01c5/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c", size = 2091257 }, + { url = "https://files.pythonhosted.org/packages/8c/7f/4bf8e9d26a9118521c80b229291fa9558a07cdd9a968ec2d5c1026f14fbc/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206", size = 2143894 }, + { url = "https://files.pythonhosted.org/packages/1f/1c/875ac7139c958f4390f23656fe696d1acc8edf45fb81e4831960f12cd6e4/pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c", size = 1816081 }, + { url = "https://files.pythonhosted.org/packages/d7/41/55a117acaeda25ceae51030b518032934f251b1dac3704a53781383e3491/pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17", size = 1981109 }, + { url = "https://files.pythonhosted.org/packages/27/39/46fe47f2ad4746b478ba89c561cafe4428e02b3573df882334bd2964f9cb/pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8", size = 1895553 }, + { url = "https://files.pythonhosted.org/packages/1c/00/0804e84a78b7fdb394fff4c4f429815a10e5e0993e6ae0e0b27dd20379ee/pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330", size = 1807220 }, + { url = "https://files.pythonhosted.org/packages/01/de/df51b3bac9820d38371f5a261020f505025df732ce566c2a2e7970b84c8c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52", size = 1829727 }, + { url = "https://files.pythonhosted.org/packages/5f/d9/c01d19da8f9e9fbdb2bf99f8358d145a312590374d0dc9dd8dbe484a9cde/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4", size = 1854282 }, + { url = "https://files.pythonhosted.org/packages/5f/84/7db66eb12a0dc88c006abd6f3cbbf4232d26adfd827a28638c540d8f871d/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c", size = 2037437 }, + { url = "https://files.pythonhosted.org/packages/34/ac/a2537958db8299fbabed81167d58cc1506049dba4163433524e06a7d9f4c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de", size = 2780899 }, + { url = "https://files.pythonhosted.org/packages/4a/c1/3e38cd777ef832c4fdce11d204592e135ddeedb6c6f525478a53d1c7d3e5/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025", size = 2135022 }, + { url = "https://files.pythonhosted.org/packages/7a/69/b9952829f80fd555fe04340539d90e000a146f2a003d3fcd1e7077c06c71/pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e", size = 1987969 }, + { url = "https://files.pythonhosted.org/packages/05/72/257b5824d7988af43460c4e22b63932ed651fe98804cc2793068de7ec554/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919", size = 1994625 }, + { url = "https://files.pythonhosted.org/packages/73/c3/78ed6b7f3278a36589bcdd01243189ade7fc9b26852844938b4d7693895b/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c", size = 2090089 }, + { url = "https://files.pythonhosted.org/packages/8d/c8/b4139b2f78579960353c4cd987e035108c93a78371bb19ba0dc1ac3b3220/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc", size = 2142496 }, + { url = "https://files.pythonhosted.org/packages/3e/f8/171a03e97eb36c0b51981efe0f78460554a1d8311773d3d30e20c005164e/pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9", size = 1811758 }, + { url = "https://files.pythonhosted.org/packages/6a/fe/4e0e63c418c1c76e33974a05266e5633e879d4061f9533b1706a86f77d5b/pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5", size = 1980864 }, + { url = "https://files.pythonhosted.org/packages/50/fc/93f7238a514c155a8ec02fc7ac6376177d449848115e4519b853820436c5/pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89", size = 1864327 }, + { url = "https://files.pythonhosted.org/packages/be/51/2e9b3788feb2aebff2aa9dfbf060ec739b38c05c46847601134cc1fed2ea/pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f", size = 1895239 }, + { url = "https://files.pythonhosted.org/packages/7b/9e/f8063952e4a7d0127f5d1181addef9377505dcce3be224263b25c4f0bfd9/pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02", size = 1805070 }, + { url = "https://files.pythonhosted.org/packages/2c/9d/e1d6c4561d262b52e41b17a7ef8301e2ba80b61e32e94520271029feb5d8/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c", size = 1828096 }, + { url = "https://files.pythonhosted.org/packages/be/65/80ff46de4266560baa4332ae3181fffc4488ea7d37282da1a62d10ab89a4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac", size = 1857708 }, + { url = "https://files.pythonhosted.org/packages/d5/ca/3370074ad758b04d9562b12ecdb088597f4d9d13893a48a583fb47682cdf/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb", size = 2037751 }, + { url = "https://files.pythonhosted.org/packages/b1/e2/4ab72d93367194317b99d051947c071aef6e3eb95f7553eaa4208ecf9ba4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529", size = 2733863 }, + { url = "https://files.pythonhosted.org/packages/8a/c6/8ae0831bf77f356bb73127ce5a95fe115b10f820ea480abbd72d3cc7ccf3/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35", size = 2161161 }, + { url = "https://files.pythonhosted.org/packages/f1/f4/b2fe73241da2429400fc27ddeaa43e35562f96cf5b67499b2de52b528cad/pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089", size = 1993294 }, + { url = "https://files.pythonhosted.org/packages/77/29/4bb008823a7f4cc05828198153f9753b3bd4c104d93b8e0b1bfe4e187540/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381", size = 2001468 }, + { url = "https://files.pythonhosted.org/packages/f2/a9/0eaceeba41b9fad851a4107e0cf999a34ae8f0d0d1f829e2574f3d8897b0/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb", size = 2091413 }, + { url = "https://files.pythonhosted.org/packages/d8/36/eb8697729725bc610fd73940f0d860d791dc2ad557faaefcbb3edbd2b349/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae", size = 2154735 }, + { url = "https://files.pythonhosted.org/packages/52/e5/4f0fbd5c5995cc70d3afed1b5c754055bb67908f55b5cb8000f7112749bf/pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", size = 1833633 }, + { url = "https://files.pythonhosted.org/packages/ee/f2/c61486eee27cae5ac781305658779b4a6b45f9cc9d02c90cb21b940e82cc/pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", size = 1986973 }, + { url = "https://files.pythonhosted.org/packages/df/a6/e3f12ff25f250b02f7c51be89a294689d175ac76e1096c32bf278f29ca1e/pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", size = 1883215 }, + { url = "https://files.pythonhosted.org/packages/0f/d6/91cb99a3c59d7b072bded9959fbeab0a9613d5a4935773c0801f1764c156/pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", size = 1895033 }, + { url = "https://files.pythonhosted.org/packages/07/42/d35033f81a28b27dedcade9e967e8a40981a765795c9ebae2045bcef05d3/pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", size = 1807542 }, + { url = "https://files.pythonhosted.org/packages/41/c2/491b59e222ec7e72236e512108ecad532c7f4391a14e971c963f624f7569/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", size = 1827854 }, + { url = "https://files.pythonhosted.org/packages/e3/f3/363652651779113189cefdbbb619b7b07b7a67ebb6840325117cc8cc3460/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", size = 1857389 }, + { url = "https://files.pythonhosted.org/packages/5f/97/be804aed6b479af5a945daec7538d8bf358d668bdadde4c7888a2506bdfb/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", size = 2037934 }, + { url = "https://files.pythonhosted.org/packages/42/01/295f0bd4abf58902917e342ddfe5f76cf66ffabfc57c2e23c7681a1a1197/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", size = 2735176 }, + { url = "https://files.pythonhosted.org/packages/9d/a0/cd8e9c940ead89cc37812a1a9f310fef59ba2f0b22b4e417d84ab09fa970/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", size = 2160720 }, + { url = "https://files.pythonhosted.org/packages/73/ae/9d0980e286627e0aeca4c352a60bd760331622c12d576e5ea4441ac7e15e/pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", size = 1992972 }, + { url = "https://files.pythonhosted.org/packages/bf/ba/ae4480bc0292d54b85cfb954e9d6bd226982949f8316338677d56541b85f/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", size = 2001477 }, + { url = "https://files.pythonhosted.org/packages/55/b7/e26adf48c2f943092ce54ae14c3c08d0d221ad34ce80b18a50de8ed2cba8/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", size = 2091186 }, + { url = "https://files.pythonhosted.org/packages/ba/cc/8491fff5b608b3862eb36e7d29d36a1af1c945463ca4c5040bf46cc73f40/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", size = 2154429 }, + { url = "https://files.pythonhosted.org/packages/78/d8/c080592d80edd3441ab7f88f865f51dae94a157fc64283c680e9f32cf6da/pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", size = 1833713 }, + { url = "https://files.pythonhosted.org/packages/83/84/5ab82a9ee2538ac95a66e51f6838d6aba6e0a03a42aa185ad2fe404a4e8f/pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", size = 1987897 }, + { url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 }, + { url = "https://files.pythonhosted.org/packages/7c/60/e5eb2d462595ba1f622edbe7b1d19531e510c05c405f0b87c80c1e89d5b1/pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6", size = 1894016 }, + { url = "https://files.pythonhosted.org/packages/61/20/da7059855225038c1c4326a840908cc7ca72c7198cb6addb8b92ec81c1d6/pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676", size = 1771648 }, + { url = "https://files.pythonhosted.org/packages/8f/fc/5485cf0b0bb38da31d1d292160a4d123b5977841ddc1122c671a30b76cfd/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d", size = 1826929 }, + { url = "https://files.pythonhosted.org/packages/a1/ff/fb1284a210e13a5f34c639efc54d51da136074ffbe25ec0c279cf9fbb1c4/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c", size = 1980591 }, + { url = "https://files.pythonhosted.org/packages/f1/14/77c1887a182d05af74f6aeac7b740da3a74155d3093ccc7ee10b900cc6b5/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27", size = 1981326 }, + { url = "https://files.pythonhosted.org/packages/06/aa/6f1b2747f811a9c66b5ef39d7f02fbb200479784c75e98290d70004b1253/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f", size = 1989205 }, + { url = "https://files.pythonhosted.org/packages/7a/d2/8ce2b074d6835f3c88d85f6d8a399790043e9fdb3d0e43455e72d19df8cc/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed", size = 2079616 }, + { url = "https://files.pythonhosted.org/packages/65/71/af01033d4e58484c3db1e5d13e751ba5e3d6b87cc3368533df4c50932c8b/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f", size = 2133265 }, + { url = "https://files.pythonhosted.org/packages/33/72/f881b5e18fbb67cf2fb4ab253660de3c6899dbb2dba409d0b757e3559e3d/pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c", size = 2001864 }, +] + +[[package]] +name = "pygments" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/0b/32f05854cfd432e9286bb41a870e0d1a926b72df5f5cdb6dec962b2e369e/pymdown_extensions-10.12.tar.gz", hash = "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7", size = 840790 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/32/95a164ddf533bd676cbbe878e36e89b4ade3efde8dd61d0148c90cbbe57e/pymdown_extensions-10.12-py3-none-any.whl", hash = "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77", size = 263448 }, +] + +[[package]] +name = "pymongo" +version = "4.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/43/d5e8993bd43e6f9cbe985e8ae1398eb73309e88694ac2ea618eacbc9cea2/pymongo-4.9.2.tar.gz", hash = "sha256:3e63535946f5df7848307b9031aa921f82bb0cbe45f9b0c3296f2173f9283eb0", size = 1889366 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/af/1ce26b971e520de621239842f2be302749eb752a5cb29dd253f4c210eb0a/pymongo-4.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ab8d54529feb6e29035ba8f0570c99ad36424bc26486c238ad7ce28597bc43c8", size = 833709 }, + { url = "https://files.pythonhosted.org/packages/a6/bd/7bc8224ae96fd9ffe8b2a193469200b9c75787178c5b1955bd20e5d024c7/pymongo-4.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f928bdc152a995cbd0b563fab201b2df873846d11f7a41d1f8cc8a01b35591ab", size = 833974 }, + { url = "https://files.pythonhosted.org/packages/87/2e/3cc96aec7a1d6151677bb108af606ea220205a47255ed53255bfe1d8f31f/pymongo-4.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6e7251d59fa3dcbb1399a71a3aec63768cebc6b22180b671601c2195fe1f90a", size = 1405440 }, + { url = "https://files.pythonhosted.org/packages/e8/9c/2d5db2fcabc873daead275729c17ddeb2b437010858fe101e8d59a276209/pymongo-4.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e759ed0459e7264a11b6896016f616341a8e4c6ab7f71ae651bd21ffc7e9524", size = 1454720 }, + { url = "https://files.pythonhosted.org/packages/6f/84/b382e7f817fd39dcd02ae69e21afd538251acf5de1904606a9908d8895fe/pymongo-4.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f3fc60f242191840ccf02b898bc615b5141fbb70064f38f7e60fcaa35d3b5efd", size = 1431625 }, + { url = "https://files.pythonhosted.org/packages/87/f5/653f9af6a7625353138bded4548a5a48729352b963fc2a059e07241b37c2/pymongo-4.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c798351666ac97a0ddaa823689061c3af949c2d6acf7fb2d9ab0a7f465ced79", size = 1409027 }, + { url = "https://files.pythonhosted.org/packages/36/26/f4159209cf6229ce0a5ac37f093dab49495c51daad8ca835279f0058b060/pymongo-4.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aac78b5fdd49ed8cae49adf76befacb02293a23b412676775c4715148e166d85", size = 1378524 }, + { url = "https://files.pythonhosted.org/packages/57/3c/78c60e721a975b836922467410dd4b9616ac84f096eec00f7bde9e889b2b/pymongo-4.9.2-cp310-cp310-win32.whl", hash = "sha256:bf77bf175c315e299a91332c2bbebc097c4d4fcc8713e513a9861684aa39023a", size = 810564 }, + { url = "https://files.pythonhosted.org/packages/71/cf/790c8da7fdd55e5e824b08eaf63355732bbf278ebcb98615e723feb05702/pymongo-4.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:c42b5aad8971256365bfd0a545fb1c7a199c93db80decd298ea2f987419e2a6d", size = 825019 }, + { url = "https://files.pythonhosted.org/packages/a8/b4/7af80304a0798526fac959e3de651b0747472c049c8b89a6c15fed2026f6/pymongo-4.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:99e40f44877b32bf4b3c46ceed2228f08c222cf7dec8a4366dd192a1429143fa", size = 887499 }, + { url = "https://files.pythonhosted.org/packages/33/ee/5389229774f842bd92a123fd3ea4f2d72b474bde9315ff00e889fe104a0d/pymongo-4.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6f6834d575ed87edc7dfcab4501d961b6a423b3839edd29ecb1382eee7736777", size = 887755 }, + { url = "https://files.pythonhosted.org/packages/d4/fd/3f0ae0fd3a7049ec67ab8f952020bc9fad841791d52d8c51405bd91b3c9b/pymongo-4.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3010018f5672e5b7e8d096dea9f1ea6545b05345ff0eb1754f6ee63785550773", size = 1647336 }, + { url = "https://files.pythonhosted.org/packages/00/b7/0472d51778e9e22b2ffd5ae9a401888525c4872cb2073f1bff8d5ae9659b/pymongo-4.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69394ee9f0ce38ff71266bad01b7e045cd75e58500ebad5d72187cbabf2e652a", size = 1713193 }, + { url = "https://files.pythonhosted.org/packages/8c/ac/aa41cb291107bb16bae286d7b9f2c868e393765830bc173609ae4dc9a3ae/pymongo-4.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87b18094100f21615d9db99c255dcd9e93e476f10fb03c1d3632cf4b82d201d2", size = 1681720 }, + { url = "https://files.pythonhosted.org/packages/dc/70/ac12eb58bd46a7254daaa4d39e7c4109983ee2227dac44df6587954fe345/pymongo-4.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3039e093d28376d6a54bdaa963ca12230c8a53d7b19c8e6368e19bcfbd004176", size = 1652109 }, + { url = "https://files.pythonhosted.org/packages/d3/20/38f71e0f1c7878b287305b2965cebe327fc5626ecca83ea52a272968cbe2/pymongo-4.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ab42d9ee93fe6b90020c42cba5bfb43a2b4660951225d137835efc21940da48", size = 1611503 }, + { url = "https://files.pythonhosted.org/packages/9b/4c/d3b26e1040c9538b9c8aed005ec18af7515c6dd3091aabfbf6c30a3b3b1a/pymongo-4.9.2-cp311-cp311-win32.whl", hash = "sha256:a663ca60e187a248d370c58961e40f5463077d2b43831eb92120ea28a79ecf96", size = 855570 }, + { url = "https://files.pythonhosted.org/packages/40/3d/7de1a4cf51bf2b10bb9f43ffa208acad0d64c18994ca8d83f490edef6834/pymongo-4.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:24e7b6887bbfefd05afed26a99a2c69459e2daa351a43a410de0d6c0ee3cce4e", size = 874715 }, + { url = "https://files.pythonhosted.org/packages/a1/08/7d95aab0463dc5a2c460a0b4e50a45a743afbe20986f47f87a9a88f43c0c/pymongo-4.9.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8083bbe8cb10bb33dca4d93f8223dd8d848215250bb73867374650bac5fe69e1", size = 941617 }, + { url = "https://files.pythonhosted.org/packages/bb/28/40613d8d97fc33bf2b9187446a6746925623aa04a9a27c9b058e97076f7a/pymongo-4.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1b8c636bf557c7166e3799bbf1120806ca39e3f06615b141c88d9c9ceae4d8c", size = 941394 }, + { url = "https://files.pythonhosted.org/packages/df/b2/7f1a0d75f538c0dcaa004ea69e28706fa3ca72d848e0a5a7dafd30939fff/pymongo-4.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8aac5dce28454f47576063fbad31ea9789bba67cab86c95788f97aafd810e65b", size = 1907396 }, + { url = "https://files.pythonhosted.org/packages/ba/70/9304bae47a361a4b12adb5be714bad41478c0e5bc3d6cf403b328d6398a0/pymongo-4.9.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1d5e7123af1fddf15b2b53e58f20bf5242884e671bcc3860f5e954fe13aeddd", size = 1986029 }, + { url = "https://files.pythonhosted.org/packages/ae/51/ac0378d001995c4a705da64a4a2b8e1732f95de5080b752d69f452930cc7/pymongo-4.9.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe97c847b56d61e533a7af0334193d6b28375b9189effce93129c7e4733794a9", size = 1949088 }, + { url = "https://files.pythonhosted.org/packages/1a/30/e93dc808039dc29fc47acee64f128aa650aacae3e4b57b68e01ff1001cda/pymongo-4.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ad54433a996e2d1985a9cd8fc82538ca8747c95caae2daf453600cc8c317f9", size = 1910516 }, + { url = "https://files.pythonhosted.org/packages/2b/34/895b9cad3bd5342d5ab51a853ed3a814840ce281d55c6928968e9f3f49f5/pymongo-4.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:98b9cade40f5b13e04492a42ae215c3721099be1014ddfe0fbd23f27e4f62c0c", size = 1860499 }, + { url = "https://files.pythonhosted.org/packages/24/7e/167818f324bf2122d45551680671a3c6406a345d3fcace4e737f57bda4e4/pymongo-4.9.2-cp312-cp312-win32.whl", hash = "sha256:dde6068ae7c62ea8ee2c5701f78c6a75618cada7e11f03893687df87709558de", size = 901282 }, + { url = "https://files.pythonhosted.org/packages/12/6b/b7ffa7114177fc1c60ae529512b82629ff7e25d19be88e97f2d0ddd16717/pymongo-4.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:e1ab6cd7cd2d38ffc7ccdc79fdc166c7a91a63f844a96e3e6b2079c054391c68", size = 924925 }, + { url = "https://files.pythonhosted.org/packages/5b/d6/b57ef5f376e2e171218a98b8c30dfd001aa5cac6338aa7f3ca76e6315667/pymongo-4.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ad79d6a74f439a068caf9a1e2daeabc20bf895263435484bbd49e90fbea7809", size = 995233 }, + { url = "https://files.pythonhosted.org/packages/32/80/4ec79e36e99f86a063d297a334883fb5115ad70e9af46142b8dc33f636fa/pymongo-4.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:877699e21703717507cbbea23e75b419f81a513b50b65531e1698df08b2d7094", size = 995025 }, + { url = "https://files.pythonhosted.org/packages/c4/fd/8f5464321fdf165700f10aec93b07a75c3537be593291ac2f8c8f5f69bd0/pymongo-4.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc9322ce7cf116458a637ac10517b0c5926a8211202be6dbdc51dab4d4a9afc8", size = 2167429 }, + { url = "https://files.pythonhosted.org/packages/da/42/0f749d805d17f5b17f48f2ee1aaf2a74e67939607b87b245e5ec9b4c1452/pymongo-4.9.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cca029f46acf475504eedb33c7839f030c4bc4f946dcba12d9a954cc48850b79", size = 2258834 }, + { url = "https://files.pythonhosted.org/packages/b8/52/b0c1b8e9cbeae234dd1108a906f30b680755533b7229f9f645d7e7adad25/pymongo-4.9.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c8c861e77527eec5a4b7363c16030dd0374670b620b08a5300f97594bbf5a40", size = 2216412 }, + { url = "https://files.pythonhosted.org/packages/4d/20/53395473a1023bb6a670b68fbfa937664c75b354c2444463075ff43523e2/pymongo-4.9.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fc70326ae71b3c7b8d6af82f46bb71dafdba3c8f335b29382ae9cf263ef3a5c", size = 2168891 }, + { url = "https://files.pythonhosted.org/packages/01/b7/fa4030279d8a4a9c0a969a719b6b89da8a59795b5cdf129ef553fce6d1f2/pymongo-4.9.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba9d2f6df977fee24437f82f7412460b0628cd6b961c4235c9cff71577a5b61f", size = 2109380 }, + { url = "https://files.pythonhosted.org/packages/f3/55/f252972a039fc6bfca748625c5080d6f88801eb61f118fe79cde47342d6a/pymongo-4.9.2-cp313-cp313-win32.whl", hash = "sha256:b3254769e708bc4aa634745c262081d13c841a80038eff3afd15631540a1d227", size = 946962 }, + { url = "https://files.pythonhosted.org/packages/7b/36/88d8438699ba09b714dece00a4a7462330c1d316f5eaa28db450572236f6/pymongo-4.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:169b85728cc17800344ba17d736375f400ef47c9fbb4c42910c4b3e7c0247382", size = 975113 }, +] + +[[package]] +name = "pyproject-api" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "tomli", marker = "python_full_version < '3.11' and python_full_version >= '3.10.0' and python_full_version < '4.0.0'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/19/441e0624a8afedd15bbcce96df1b80479dd0ff0d965f5ce8fde4f2f6ffad/pyproject_api-1.8.0.tar.gz", hash = "sha256:77b8049f2feb5d33eefcc21b57f1e279636277a8ac8ad6b5871037b243778496", size = 22340 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/f4/3c4ddfcc0c19c217c6de513842d286de8021af2f2ab79bbb86c00342d778/pyproject_api-1.8.0-py3-none-any.whl", hash = "sha256:3d7d347a047afe796fd5d1885b1e391ba29be7169bd2f102fcd378f04273d228", size = 13100 }, +] + +[[package]] +name = "pytest" +version = "8.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11' and python_full_version >= '3.10.0' and python_full_version < '4.0.0'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11' and python_full_version >= '3.10.0' and python_full_version < '4.0.0'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, +] + +[[package]] +name = "pytest-asyncio" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/c6cf50ce320cf8611df7a1254d86233b3df7cc07f9b5f5cbcb82e08aa534/pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276", size = 49855 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/31/6607dab48616902f76885dfcf62c08d929796fc3b2d2318faf9fd54dbed9/pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", size = 18024 }, +] + +[[package]] +name = "pytest-cov" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, +] + +[[package]] +name = "pytest-deadfixtures" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/d5/0472e7dd4c5794cd720f8add3ba59b35e45c1bb7482e04b6d6e60ff4eed0/pytest-deadfixtures-2.2.1.tar.gz", hash = "sha256:ca15938a4e8330993ccec9c6c847383d88b3cd574729530647dc6b492daa9c1e", size = 6616 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/c7/d8c3fa6d2de6814c92161fcce984a7d38a63186590a66238e7b749f7fe37/pytest_deadfixtures-2.2.1-py2.py3-none-any.whl", hash = "sha256:db71533f2d9456227084e00a1231e732973e299ccb7c37ab92e95032ab6c083e", size = 5059 }, +] + +[[package]] +name = "pytest-vcr" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "vcrpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/60/104c619483c1a42775d3f8b27293f1ecfc0728014874d065e68cb9702d49/pytest-vcr-1.0.2.tar.gz", hash = "sha256:23ee51b75abbcc43d926272773aae4f39f93aceb75ed56852d0bf618f92e1896", size = 3810 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/d3/ff520d11e6ee400602711d1ece8168dcfc5b6d8146fb7db4244a6ad6a9c3/pytest_vcr-1.0.2-py2.py3-none-any.whl", hash = "sha256:2f316e0539399bea0296e8b8401145c62b6f85e9066af7e57b6151481b0d6d9c", size = 4137 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, +] + +[[package]] +name = "python-multipart" +version = "0.0.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/19/93bfb43a3c41b1dd0fa1fa66a08286f6467d36d30297a7aaab8c0b176a26/python_multipart-0.0.19.tar.gz", hash = "sha256:905502ef39050557b7a6af411f454bc19526529ca46ae6831508438890ce12cc", size = 36886 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/f4/ddd0fcdc454cf3870153ae16a818256523d31c3c8136e216bc6836ed4cd1/python_multipart-0.0.19-py3-none-any.whl", hash = "sha256:f8d5b0b9c618575bf9df01c684ded1d94a338839bdd8223838afacfb4bb2082d", size = 24448 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911 }, +] + +[[package]] +name = "redis" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3' and python_full_version >= '3.10.0' and python_full_version < '4.0.0'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/da/d283a37303a995cd36f8b92db85135153dc4f7a8e4441aa827721b442cfb/redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f", size = 4608355 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502 }, +] + +[[package]] +name = "regex" +version = "2024.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/3c/4651f6b130c6842a8f3df82461a8950f923925db8b6961063e82744bddcc/regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91", size = 482674 }, + { url = "https://files.pythonhosted.org/packages/15/51/9f35d12da8434b489c7b7bffc205c474a0a9432a889457026e9bc06a297a/regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/bd/18/b731f5510d1b8fb63c6b6d3484bfa9a59b84cc578ac8b5172970e05ae07c/regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/78/a2/6dd36e16341ab95e4c6073426561b9bfdeb1a9c9b63ab1b579c2e96cb105/regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", size = 782511 }, + { url = "https://files.pythonhosted.org/packages/1b/2b/323e72d5d2fd8de0d9baa443e1ed70363ed7e7b2fb526f5950c5cb99c364/regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", size = 821149 }, + { url = "https://files.pythonhosted.org/packages/90/30/63373b9ea468fbef8a907fd273e5c329b8c9535fee36fc8dba5fecac475d/regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", size = 809707 }, + { url = "https://files.pythonhosted.org/packages/f2/98/26d3830875b53071f1f0ae6d547f1d98e964dd29ad35cbf94439120bb67a/regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", size = 781702 }, + { url = "https://files.pythonhosted.org/packages/87/55/eb2a068334274db86208ab9d5599ffa63631b9f0f67ed70ea7c82a69bbc8/regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", size = 771976 }, + { url = "https://files.pythonhosted.org/packages/74/c0/be707bcfe98254d8f9d2cff55d216e946f4ea48ad2fd8cf1428f8c5332ba/regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", size = 697397 }, + { url = "https://files.pythonhosted.org/packages/49/dc/bb45572ceb49e0f6509f7596e4ba7031f6819ecb26bc7610979af5a77f45/regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", size = 768726 }, + { url = "https://files.pythonhosted.org/packages/5a/db/f43fd75dc4c0c2d96d0881967897926942e935d700863666f3c844a72ce6/regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", size = 775098 }, + { url = "https://files.pythonhosted.org/packages/99/d7/f94154db29ab5a89d69ff893159b19ada89e76b915c1293e98603d39838c/regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", size = 839325 }, + { url = "https://files.pythonhosted.org/packages/f7/17/3cbfab1f23356fbbf07708220ab438a7efa1e0f34195bf857433f79f1788/regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", size = 843277 }, + { url = "https://files.pythonhosted.org/packages/7e/f2/48b393b51900456155de3ad001900f94298965e1cad1c772b87f9cfea011/regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", size = 773197 }, + { url = "https://files.pythonhosted.org/packages/45/3f/ef9589aba93e084cd3f8471fded352826dcae8489b650d0b9b27bc5bba8a/regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", size = 261714 }, + { url = "https://files.pythonhosted.org/packages/42/7e/5f1b92c8468290c465fd50c5318da64319133231415a8aa6ea5ab995a815/regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", size = 274042 }, + { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, + { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, + { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, + { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, + { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, + { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, + { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, + { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, + { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, + { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, + { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, + { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, + { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, + { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, + { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, + { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, + { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, + { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, + { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, + { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, + { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, + { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, + { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, + { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, + { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, + { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, + { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, + { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, + { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, + { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, + { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, + { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, + { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, + { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, + { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, + { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, + { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, + { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, + { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, + { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, + { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, + { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, + { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation == 'PyPy'" }, + { name = "urllib3", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' and python_full_version >= '3.10.0' and python_full_version < '4.0.0'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, +] + +[[package]] +name = "rich-click" +version = "1.8.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/31/103501e85e885e3e202c087fa612cfe450693210372766552ce1ab5b57b9/rich_click-1.8.5.tar.gz", hash = "sha256:a3eebe81da1c9da3c32f3810017c79bd687ff1b3fa35bfc9d8a3338797f1d1a1", size = 38229 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/0b/e2de98c538c0ee9336211d260f88b7e69affab44969750aaca0b48a697c8/rich_click-1.8.5-py3-none-any.whl", hash = "sha256:0fab7bb5b66c15da17c210b4104277cd45f3653a7322e0098820a169880baee0", size = 35081 }, +] + +[[package]] +name = "rich-toolkit" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/88/58c193e2e353b0ef8b4b9a91031bbcf8a9a3b431f5ebb4f55c3f3b1992e8/rich_toolkit-0.12.0.tar.gz", hash = "sha256:facb0b40418010309f77abd44e2583b4936656f6ee5c8625da807564806a6c40", size = 71673 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/3c/3b66696fc8a6c980674851108d7d57fbcbfedbefb3d8b61a64166dc9b18e/rich_toolkit-0.12.0-py3-none-any.whl", hash = "sha256:a2da4416384410ae871e890db7edf8623e1f5e983341dbbc8cc03603ce24f0ab", size = 13012 }, +] + +[[package]] +name = "ruff" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/2b/01245f4f3a727d60bebeacd7ee6d22586c7f62380a2597ddb22c2f45d018/ruff-0.8.2.tar.gz", hash = "sha256:b84f4f414dda8ac7f75075c1fa0b905ac0ff25361f42e6d5da681a465e0f78e5", size = 3349020 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/29/366be70216dba1731a00a41f2f030822b0c96c7c4f3b2c0cdce15cbace74/ruff-0.8.2-py3-none-linux_armv6l.whl", hash = "sha256:c49ab4da37e7c457105aadfd2725e24305ff9bc908487a9bf8d548c6dad8bb3d", size = 10530649 }, + { url = "https://files.pythonhosted.org/packages/63/82/a733956540bb388f00df5a3e6a02467b16c0e529132625fe44ce4c5fb9c7/ruff-0.8.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec016beb69ac16be416c435828be702ee694c0d722505f9c1f35e1b9c0cc1bf5", size = 10274069 }, + { url = "https://files.pythonhosted.org/packages/3d/12/0b3aa14d1d71546c988a28e1b412981c1b80c8a1072e977a2f30c595cc4a/ruff-0.8.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f05cdf8d050b30e2ba55c9b09330b51f9f97d36d4673213679b965d25a785f3c", size = 9909400 }, + { url = "https://files.pythonhosted.org/packages/23/08/f9f08cefb7921784c891c4151cce6ed357ff49e84b84978440cffbc87408/ruff-0.8.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60f578c11feb1d3d257b2fb043ddb47501ab4816e7e221fbb0077f0d5d4e7b6f", size = 10766782 }, + { url = "https://files.pythonhosted.org/packages/e4/71/bf50c321ec179aa420c8ec40adac5ae9cc408d4d37283a485b19a2331ceb/ruff-0.8.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbd5cf9b0ae8f30eebc7b360171bd50f59ab29d39f06a670b3e4501a36ba5897", size = 10286316 }, + { url = "https://files.pythonhosted.org/packages/f2/83/c82688a2a6117539aea0ce63fdf6c08e60fe0202779361223bcd7f40bd74/ruff-0.8.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b402ddee3d777683de60ff76da801fa7e5e8a71038f57ee53e903afbcefdaa58", size = 11338270 }, + { url = "https://files.pythonhosted.org/packages/7f/d7/bc6a45e5a22e627640388e703160afb1d77c572b1d0fda8b4349f334fc66/ruff-0.8.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:705832cd7d85605cb7858d8a13d75993c8f3ef1397b0831289109e953d833d29", size = 12058579 }, + { url = "https://files.pythonhosted.org/packages/da/3b/64150c93946ec851e6f1707ff586bb460ca671581380c919698d6a9267dc/ruff-0.8.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32096b41aaf7a5cc095fa45b4167b890e4c8d3fd217603f3634c92a541de7248", size = 11615172 }, + { url = "https://files.pythonhosted.org/packages/e4/9e/cf12b697ea83cfe92ec4509ae414dc4c9b38179cc681a497031f0d0d9a8e/ruff-0.8.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e769083da9439508833cfc7c23e351e1809e67f47c50248250ce1ac52c21fb93", size = 12882398 }, + { url = "https://files.pythonhosted.org/packages/a9/27/96d10863accf76a9c97baceac30b0a52d917eb985a8ac058bd4636aeede0/ruff-0.8.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fe716592ae8a376c2673fdfc1f5c0c193a6d0411f90a496863c99cd9e2ae25d", size = 11176094 }, + { url = "https://files.pythonhosted.org/packages/eb/10/cd2fd77d4a4e7f03c29351be0f53278a393186b540b99df68beb5304fddd/ruff-0.8.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:81c148825277e737493242b44c5388a300584d73d5774defa9245aaef55448b0", size = 10771884 }, + { url = "https://files.pythonhosted.org/packages/71/5d/beabb2ff18870fc4add05fa3a69a4cb1b1d2d6f83f3cf3ae5ab0d52f455d/ruff-0.8.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d261d7850c8367704874847d95febc698a950bf061c9475d4a8b7689adc4f7fa", size = 10382535 }, + { url = "https://files.pythonhosted.org/packages/ae/29/6b3fdf3ad3e35b28d87c25a9ff4c8222ad72485ab783936b2b267250d7a7/ruff-0.8.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1ca4e3a87496dc07d2427b7dd7ffa88a1e597c28dad65ae6433ecb9f2e4f022f", size = 10886995 }, + { url = "https://files.pythonhosted.org/packages/e9/dc/859d889b4d9356a1a2cdbc1e4a0dda94052bc5b5300098647e51a58c430b/ruff-0.8.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:729850feed82ef2440aa27946ab39c18cb4a8889c1128a6d589ffa028ddcfc22", size = 11220750 }, + { url = "https://files.pythonhosted.org/packages/0b/08/e8f519f61f1d624264bfd6b8829e4c5f31c3c61193bc3cff1f19dbe7626a/ruff-0.8.2-py3-none-win32.whl", hash = "sha256:ac42caaa0411d6a7d9594363294416e0e48fc1279e1b0e948391695db2b3d5b1", size = 8729396 }, + { url = "https://files.pythonhosted.org/packages/f8/d4/ba1c7ab72aba37a2b71fe48ab95b80546dbad7a7f35ea28cf66fc5cea5f6/ruff-0.8.2-py3-none-win_amd64.whl", hash = "sha256:2aae99ec70abf43372612a838d97bfe77d45146254568d94926e8ed5bbb409ea", size = 9594729 }, + { url = "https://files.pythonhosted.org/packages/23/34/db20e12d3db11b8a2a8874258f0f6d96a9a4d631659d54575840557164c8/ruff-0.8.2-py3-none-win_arm64.whl", hash = "sha256:fb88e2a506b70cfbc2de6fae6681c4f944f7dd5f2fe87233a7233d888bad73e8", size = 9035131 }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "starlette" +version = "0.41.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/4c/9b5764bd22eec91c4039ef4c55334e9187085da2d8a2df7bd570869aae18/starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835", size = 2574159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "tox" +version = "4.23.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "chardet" }, + { name = "colorama" }, + { name = "filelock" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "pluggy" }, + { name = "pyproject-api" }, + { name = "tomli", marker = "python_full_version < '3.11' and python_full_version >= '3.10.0' and python_full_version < '4.0.0'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' and python_full_version >= '3.10.0' and python_full_version < '4.0.0'" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/86/32b10f91b4b975a37ac402b0f9fa016775088e0565c93602ba0b3c729ce8/tox-4.23.2.tar.gz", hash = "sha256:86075e00e555df6e82e74cfc333917f91ecb47ffbc868dcafbd2672e332f4a2c", size = 189998 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/c0/124b73d01c120e917383bc6c53ebc34efdf7243faa9fca64d105c94cf2ab/tox-4.23.2-py3-none-any.whl", hash = "sha256:452bc32bb031f2282881a2118923176445bac783ab97c874b8770ab4c3b76c38", size = 166758 }, +] + +[[package]] +name = "tox-uv" +version = "1.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "tox" }, + { name = "uv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/5a/5335fe0348d626a01e7a600e85cbea04e0f5f221e9c2f3a2b3664d774f69/tox_uv-1.16.1.tar.gz", hash = "sha256:63b0a872f1f97263f89fe41e7195b401a18a67a9a14e3803372091a364b43cd6", size = 16940 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/1d/c44b1e246f95fadf9fc83b34f70fb297466bc580b71d756e85097ef1dd6a/tox_uv-1.16.1-py3-none-any.whl", hash = "sha256:ab5dae5df02ca3b6c00f95c86810afe6c0e2b8ff8e6d76053ef677dcce38975b", size = 13681 }, +] + +[[package]] +name = "typer" +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/dca7b219718afd37a0068f4f2530a727c2b74a8b6e8e0c0080a4c0de4fcd/typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a", size = 99789 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 }, +] + +[[package]] +name = "types-cffi" +version = "1.16.0.20240331" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/c8/81e5699160b91f0f91eea852d84035c412bfb4b3a29389701044400ab379/types-cffi-1.16.0.20240331.tar.gz", hash = "sha256:b8b20d23a2b89cfed5f8c5bc53b0cb8677c3aac6d970dbc771e28b9c698f5dee", size = 11318 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/7a/98f5d2493a652cec05d3b09be59202d202004a41fca9c70d224782611365/types_cffi-1.16.0.20240331-py3-none-any.whl", hash = "sha256:a363e5ea54a4eb6a4a105d800685fde596bc318089b025b27dee09849fe41ff0", size = 14550 }, +] + +[[package]] +name = "types-pyopenssl" +version = "24.1.0.20240722" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "types-cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/29/47a346550fd2020dac9a7a6d033ea03fccb92fa47c726056618cc889745e/types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39", size = 8458 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/05/c868a850b6fbb79c26f5f299b768ee0adc1f9816d3461dcf4287916f655b/types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54", size = 7499 }, +] + +[[package]] +name = "types-redis" +version = "4.6.0.20241004" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "types-pyopenssl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/95/c054d3ac940e8bac4ca216470c80c26688a0e79e09f520a942bb27da3386/types-redis-4.6.0.20241004.tar.gz", hash = "sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e", size = 49679 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/82/7d25dce10aad92d2226b269bce2f85cfd843b4477cd50245d7d40ecf8f89/types_redis-4.6.0.20241004-py3-none-any.whl", hash = "sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed", size = 58737 }, +] + +[[package]] +name = "types-setuptools" +version = "75.6.0.20241126" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/d2/15ede73bc3faf647af2c7bfefa90dde563a4b6bb580b1199f6255463c272/types_setuptools-75.6.0.20241126.tar.gz", hash = "sha256:7bf25ad4be39740e469f9268b6beddda6e088891fa5a27e985c6ce68bf62ace0", size = 48569 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/a0/898a1363592d372d4103b76b7c723d84fcbde5fa4ed0c3a29102805ed7db/types_setuptools-75.6.0.20241126-py3-none-any.whl", hash = "sha256:aaae310a0e27033c1da8457d4d26ac673b0c8a0de7272d6d4708e263f2ea3b9b", size = 72732 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "tzdata" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 }, +] + +[[package]] +name = "urllib3" +version = "1.26.20" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "platform_python_implementation == 'PyPy'", +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/e8/6ff5e6bc22095cfc59b6ea711b687e2b7ed4bdb373f7eeec370a97d7392f/urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32", size = 307380 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225 }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "platform_python_implementation != 'PyPy'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, +] + +[[package]] +name = "uv" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/1c/8c40ec75c26656bec9ada97833a437b49fd443b5d6dfd61d6dda8ad90cbe/uv-0.5.7.tar.gz", hash = "sha256:4d22a5046a6246af85c92257d110ed8fbcd98b16824e4efa9d825d001222b2cb", size = 2356161 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/15/4d05061146ef1ff909458f75812633944a144ebadf73ccd38bef127adc6a/uv-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:fb4a3ccbe13072b98919413ac8378dd3e2b5480352f75c349a4f71f423801485", size = 14208956 }, + { url = "https://files.pythonhosted.org/packages/ba/8f/dc99e8f026da8b3c74661ca60d424472b8fc73854be8dd0375c9a487474b/uv-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a4fc62749bda8e7ae62212b1d85cdf6c7bad41918b3c8ac5a6d730dd093d793d", size = 14205195 }, + { url = "https://files.pythonhosted.org/packages/fe/67/fba55047c34ceae31cf92f6286a8517749d8c86a2151620fccb4dfb01cba/uv-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:78c3c040e52c09a410b9788656d6e760d557f223058537081cb03a3e25ce89de", size = 13178700 }, + { url = "https://files.pythonhosted.org/packages/5c/af/476c4d3486690e3cd6a9d1e040e350aefcd374b6adf919228594c9e0d9d2/uv-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:76b514c79136e779cccf90cce5d60f317a0d42074e9f4c059f198ef435f2f6ab", size = 13438725 }, + { url = "https://files.pythonhosted.org/packages/a0/18/ab89b12e695e069f6a181f66fd22dfa66b3bb5b7508938a4d4a3bff6d214/uv-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a45648db157d2aaff859fe71ec738efea09b972b8864feb2fd61ef856a15b24f", size = 13987146 }, + { url = "https://files.pythonhosted.org/packages/60/72/0eedd9b4d25657124ee5715ec08a0b278716905dd4c2a79b2af5e742c421/uv-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1e7b5bcc8b380e333e948c01f6f4c6203067b5de60a05f8ed786332af7a9132", size = 14513180 }, + { url = "https://files.pythonhosted.org/packages/9c/b3/feef463577bb31f692b2e52fdce76865d297fe1a4ae48d2bad855b255a67/uv-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:737a06b15c4e6b8ab7dd0a577ba766380bda4c18ba4ecfcfff37d336f1b03a00", size = 15216614 }, + { url = "https://files.pythonhosted.org/packages/99/dd/90e3360402610e1f687fc52c1c0b12906530986c7fe87d63414e0b8ac045/uv-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba25eb99891b95b5200d5e369b788d443fae370b097e7268a71e9ba753f2af3f", size = 15005351 }, + { url = "https://files.pythonhosted.org/packages/f2/c5/1fd7eafa61d2659ab4b27314e01eaa2cd62acb0f3a8bceb6420d38f3137f/uv-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:747c011da9f631354a1c89b62b19b8572e040d3fe01c6fb8d650facc7a09fdbb", size = 19537320 }, + { url = "https://files.pythonhosted.org/packages/12/77/36eb833476111af75ecc624d103662aba650b2b3c47abf4df5917697a5b1/uv-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a141b40444c4184efba9fdc10abb3c1cff32154c7f8b0ad46ddc180d65a82d90", size = 14678070 }, + { url = "https://files.pythonhosted.org/packages/a9/c6/7a70672f383ec639d178e0b1481048f181c05bbe372f23a66853a02e0346/uv-0.5.7-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:46b03a9a78438219fb3060c096773284e2f22417a9c1f8fdd602f0650b3355c2", size = 13637987 }, + { url = "https://files.pythonhosted.org/packages/98/d1/a7c80c0a582344cf63ad17c8c344c9194a2f4475f6b522adbdb3b8cb6ac6/uv-0.5.7-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:13961a8116515eb288c4f91849fba11ebda0dfeec44cc356e388b3b03b2dbbe1", size = 13974519 }, + { url = "https://files.pythonhosted.org/packages/84/23/55ef8f1fdd750aa1a123dac92bac249cbf8268bd9ab5b63b33580cd4dc23/uv-0.5.7-py3-none-musllinux_1_1_i686.whl", hash = "sha256:071b57c934bdee8d7502a70e9ea0739a10e9b2d1d0c67e923a09e7a23d9a181b", size = 14241488 }, + { url = "https://files.pythonhosted.org/packages/e8/42/0cb96aa85849e55f3dcf4080fec1c13e75eb6179cbff630e4ded22b455f6/uv-0.5.7-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:1c5b89c64fb627f52f1e9c9bbc4dcc7bae29c4c5ab8eff46da3c966bbd4caed2", size = 16082215 }, + { url = "https://files.pythonhosted.org/packages/c5/d0/51e588ef932160f113a379781b7edf781d2a7e4667ff4a26b1f3146df359/uv-0.5.7-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:b79e32438390add793bebc41b0729054e375be30bc53f124ee212d9c97affc39", size = 14809685 }, + { url = "https://files.pythonhosted.org/packages/cc/2b/5cc8622473e61b252211811ee6cb0471ac060dc4a36391747217a717a19a/uv-0.5.7-py3-none-win32.whl", hash = "sha256:d0600d2b2fbd9a9446bfbb7f03d88bc3d0293b949ce40e326429dd4fe246c926", size = 14074020 }, + { url = "https://files.pythonhosted.org/packages/e1/e0/2ce3eb10fab05d900b3434dce09f59f5ac0689e52ca4979e3bfd32e71b61/uv-0.5.7-py3-none-win_amd64.whl", hash = "sha256:27c630780e1856a70fbeb267e1ed6835268a1b50963ab9a984fafa4184389def", size = 15842701 }, +] + +[[package]] +name = "uvicorn" +version = "0.32.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' and python_full_version >= '3.10.0' and python_full_version < '4.0.0'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/3c/21dba3e7d76138725ef307e3d7ddd29b763119b3aa459d02cc05fefcff75/uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175", size = 77630 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/c1/2d27b0a15826c2b71dcf6e2f5402181ef85acf439617bb2f1453125ce1f3/uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e", size = 63828 }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/76/44a55515e8c9505aa1420aebacf4dd82552e5e15691654894e90d0bd051a/uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f", size = 1442019 }, + { url = "https://files.pythonhosted.org/packages/35/5a/62d5800358a78cc25c8a6c72ef8b10851bdb8cca22e14d9c74167b7f86da/uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d", size = 801898 }, + { url = "https://files.pythonhosted.org/packages/f3/96/63695e0ebd7da6c741ccd4489b5947394435e198a1382349c17b1146bb97/uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26", size = 3827735 }, + { url = "https://files.pythonhosted.org/packages/61/e0/f0f8ec84979068ffae132c58c79af1de9cceeb664076beea86d941af1a30/uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb", size = 3825126 }, + { url = "https://files.pythonhosted.org/packages/bf/fe/5e94a977d058a54a19df95f12f7161ab6e323ad49f4dabc28822eb2df7ea/uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f", size = 3705789 }, + { url = "https://files.pythonhosted.org/packages/26/dd/c7179618e46092a77e036650c1f056041a028a35c4d76945089fcfc38af8/uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c", size = 3800523 }, + { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410 }, + { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476 }, + { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855 }, + { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185 }, + { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256 }, + { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323 }, + { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284 }, + { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349 }, + { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089 }, + { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 }, + { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 }, + { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 }, + { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 }, + { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 }, + { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 }, + { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 }, + { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 }, + { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 }, +] + +[[package]] +name = "vcrpy" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation == 'PyPy'" }, + { name = "urllib3", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation != 'PyPy' and python_full_version >= '3.10.0' and python_full_version < '4.0.0'" }, + { name = "wrapt" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/4e/fff59599826793f9e3460c22c0af0377abb27dc9781a7d5daca8cb03da25/vcrpy-6.0.2.tar.gz", hash = "sha256:88e13d9111846745898411dbc74a75ce85870af96dd320d75f1ee33158addc09", size = 85472 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/ed/25d19705791d3fccc84423d564695421a75b4e08e8ab15a004a49068742d/vcrpy-6.0.2-py2.py3-none-any.whl", hash = "sha256:40370223861181bc76a5e5d4b743a95058bb1ad516c3c08570316ab592f56cad", size = 42431 }, +] + +[[package]] +name = "virtualenv" +version = "20.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/75/53316a5a8050069228a2f6d11f32046cfa94fbb6cc3f08703f59b873de2e/virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa", size = 7650368 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/f9/0919cf6f1432a8c4baa62511f8f8da8225432d22e83e3476f5be1a1edc6e/virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0", size = 4276702 }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390 }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389 }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020 }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393 }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392 }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019 }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471 }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449 }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054 }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902 }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380 }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, +] + +[[package]] +name = "watchfiles" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/5e/5a9dfb8594b075d7c225d5fb628d498001c5dfae62298e9eb85b8754668f/watchfiles-1.0.0.tar.gz", hash = "sha256:37566c844c9ce3b5deb964fe1a23378e575e74b114618d211fbda8f59d7b5dab", size = 38187 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/3b/c453e0f87b34ad4ef72cb193b2fd6d40c682cb217b06d51c17400fbe8650/watchfiles-1.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1d19df28f99d6a81730658fbeb3ade8565ff687f95acb59665f11502b441be5f", size = 394140 }, + { url = "https://files.pythonhosted.org/packages/6d/0c/f795dce52ca55472aa75fabd12f963ab5bbb860de5d24a5b2eeabeb44613/watchfiles-1.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:28babb38cf2da8e170b706c4b84aa7e4528a6fa4f3ee55d7a0866456a1662041", size = 382832 }, + { url = "https://files.pythonhosted.org/packages/71/ab/e1452f6e4cd0d829ae27ea8af6d3674d734332566fa5ceb870151b49b4f4/watchfiles-1.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12ab123135b2f42517f04e720526d41448667ae8249e651385afb5cda31fedc0", size = 441232 }, + { url = "https://files.pythonhosted.org/packages/37/76/548e9aee70bbe00b728bd33076e764f0c9d9beb8247c63073d9d10295571/watchfiles-1.0.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13a4f9ee0cd25682679eea5c14fc629e2eaa79aab74d963bc4e21f43b8ea1877", size = 447570 }, + { url = "https://files.pythonhosted.org/packages/0d/2c/e8d627f29353e8a10054243801e7bc305bd34789cf2101eadb42a0fdba51/watchfiles-1.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e1d9284cc84de7855fcf83472e51d32daf6f6cecd094160192628bc3fee1b78", size = 472440 }, + { url = "https://files.pythonhosted.org/packages/8b/86/ce94bba556dee4643d4b19f62bace982b08c4d86c7aa345fe9129519772b/watchfiles-1.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ee5edc939f53466b329bbf2e58333a5461e6c7b50c980fa6117439e2c18b42d", size = 492706 }, + { url = "https://files.pythonhosted.org/packages/67/5c/7db33af6d0d7d46618b67dda4f5448cbbc0366d0a5eb115020ab42faa129/watchfiles-1.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dccfc70480087567720e4e36ec381bba1ed68d7e5f368fe40c93b3b1eba0105", size = 489295 }, + { url = "https://files.pythonhosted.org/packages/14/a2/8237e16017c0bab92163381ac8e780b9fe85b4efa71893faf1a18c8a9e35/watchfiles-1.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c83a6d33a9eda0af6a7470240d1af487807adc269704fe76a4972dd982d16236", size = 442559 }, + { url = "https://files.pythonhosted.org/packages/ce/71/8c6ff2f5f985c1e44395936e8f95495d5c42fdd649fbaa6f1ebaa5233d13/watchfiles-1.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:905f69aad276639eff3893759a07d44ea99560e67a1cf46ff389cd62f88872a2", size = 614528 }, + { url = "https://files.pythonhosted.org/packages/f8/be/6b2c73b8de25162e5b665607c847bb1ec98d60b5cccc638d915a870b4621/watchfiles-1.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:09551237645d6bff3972592f2aa5424df9290e7a2e15d63c5f47c48cde585935", size = 612848 }, + { url = "https://files.pythonhosted.org/packages/e7/0e/79ad259865fa4be453f18cb006dd14234c293d91d3ff41e3cc7e406bff0a/watchfiles-1.0.0-cp310-none-win32.whl", hash = "sha256:d2b39aa8edd9e5f56f99a2a2740a251dc58515398e9ed5a4b3e5ff2827060755", size = 272040 }, + { url = "https://files.pythonhosted.org/packages/e9/3e/1b8e86a0970a7292f3fdef94acb4468212b81cd7c8ad4f724c5d56cdc02e/watchfiles-1.0.0-cp310-none-win_amd64.whl", hash = "sha256:2de52b499e1ab037f1a87cb8ebcb04a819bf087b1015a4cf6dcf8af3c2a2613e", size = 285356 }, + { url = "https://files.pythonhosted.org/packages/a7/10/10759faea3f011b86867a534a47c9aedca667a4b3806ffeac7d8a4c8adee/watchfiles-1.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:fbd0ab7a9943bbddb87cbc2bf2f09317e74c77dc55b1f5657f81d04666c25269", size = 394139 }, + { url = "https://files.pythonhosted.org/packages/b9/71/b76be784f3e48bb1929e2c1376f227608be9bda4f7ba0c06832f0d190bed/watchfiles-1.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:774ef36b16b7198669ce655d4f75b4c3d370e7f1cbdfb997fb10ee98717e2058", size = 382832 }, + { url = "https://files.pythonhosted.org/packages/d6/88/393b33c6da4963933e810eb0b8d6b44c7ba52ed2aaf6bb7709db377289f8/watchfiles-1.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b4fb98100267e6a5ebaff6aaa5d20aea20240584647470be39fe4823012ac96", size = 441232 }, + { url = "https://files.pythonhosted.org/packages/35/2c/2d2c131866f7c49ec68c504565d2336f40a595bcd857cd464a68ea0fdb42/watchfiles-1.0.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0fc3bf0effa2d8075b70badfdd7fb839d7aa9cea650d17886982840d71fdeabf", size = 447569 }, + { url = "https://files.pythonhosted.org/packages/ab/08/373713cc4859958cdf0a38ad85740010dbbf5617441edc3480d37387024c/watchfiles-1.0.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:648e2b6db53eca6ef31245805cd528a16f56fa4cc15aeec97795eaf713c11435", size = 472439 }, + { url = "https://files.pythonhosted.org/packages/2b/df/8e209910e260f58f005974a60423bb6fc243d26e8793103462870502c744/watchfiles-1.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa13d604fcb9417ae5f2e3de676e66aa97427d888e83662ad205bed35a313176", size = 492707 }, + { url = "https://files.pythonhosted.org/packages/83/4d/d0673571c223a784849f45c4da6de2af960602ba5061a2f033f96606a118/watchfiles-1.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:936f362e7ff28311b16f0b97ec51e8f2cc451763a3264640c6ed40fb252d1ee4", size = 489294 }, + { url = "https://files.pythonhosted.org/packages/32/ed/0c96c714408c8edab862e816b45be51dbe4e77dc7518c29b0dccc02961a8/watchfiles-1.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:245fab124b9faf58430da547512d91734858df13f2ddd48ecfa5e493455ffccb", size = 442559 }, + { url = "https://files.pythonhosted.org/packages/3d/2b/665bf9aefd0f22a265f7b93e69aa4dc068d8ac5ad9ecbd974305eaeff2c0/watchfiles-1.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4ff9c7e84e8b644a8f985c42bcc81457240316f900fc72769aaedec9d088055a", size = 614531 }, + { url = "https://files.pythonhosted.org/packages/9f/41/fd125e824a195219adb204b54f3affce5615f5f1b3889acd441f28d2fbd2/watchfiles-1.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c9a8d8fd97defe935ef8dd53d562e68942ad65067cd1c54d6ed8a088b1d931d", size = 612852 }, + { url = "https://files.pythonhosted.org/packages/dc/ac/750bf3625f4d3172ee7acfd952552070a88fd697935cfead79a68eb8d69d/watchfiles-1.0.0-cp311-none-win32.whl", hash = "sha256:a0abf173975eb9dd17bb14c191ee79999e650997cc644562f91df06060610e62", size = 272294 }, + { url = "https://files.pythonhosted.org/packages/bd/04/8c18986b79d106a88f54629f8f901cd725d76227c9a9191ada8ce8c962e8/watchfiles-1.0.0-cp311-none-win_amd64.whl", hash = "sha256:2a825ba4b32c214e3855b536eb1a1f7b006511d8e64b8215aac06eb680642d84", size = 285435 }, + { url = "https://files.pythonhosted.org/packages/b4/38/7e64929e8ca2b2a94cb9d8ddf6be9c06be8be870b6014d0f06e76b72f9cf/watchfiles-1.0.0-cp311-none-win_arm64.whl", hash = "sha256:a5a7a06cfc65e34fd0a765a7623c5ba14707a0870703888e51d3d67107589817", size = 276512 }, + { url = "https://files.pythonhosted.org/packages/37/0a/75491ba001f1495d2a12d7f6b90738f20badac78291ca5d56bf7990c859a/watchfiles-1.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:28fb64b5843d94e2c2483f7b024a1280662a44409bedee8f2f51439767e2d107", size = 394139 }, + { url = "https://files.pythonhosted.org/packages/5a/ee/935095538ff08ab68555de2bbc18acaf91f4cce8518bf32196f1ff9b8326/watchfiles-1.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e3750434c83b61abb3163b49c64b04180b85b4dabb29a294513faec57f2ffdb7", size = 382832 }, + { url = "https://files.pythonhosted.org/packages/74/40/86787dca3ea251aabb3abfbe4beeffe9c7ae6e69de56a25d572aecde580e/watchfiles-1.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bedf84835069f51c7b026b3ca04e2e747ea8ed0a77c72006172c72d28c9f69fc", size = 441232 }, + { url = "https://files.pythonhosted.org/packages/59/e2/08db1ba48a30462ec7e382c2b1de5400b09a2a7c95fe3f16d3e7da844f0c/watchfiles-1.0.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90004553be36427c3d06ec75b804233f8f816374165d5225b93abd94ba6e7234", size = 447569 }, + { url = "https://files.pythonhosted.org/packages/73/54/10adf42f203d876076cf0684726c102b3dba82b1c7eea2d82e5991875f62/watchfiles-1.0.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b46e15c34d4e401e976d6949ad3a74d244600d5c4b88c827a3fdf18691a46359", size = 472439 }, + { url = "https://files.pythonhosted.org/packages/29/77/d0d3b5ec6224800cd77f5d058473d0a844d753a3dad9f53f369bc98946bc/watchfiles-1.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:487d15927f1b0bd24e7df921913399bb1ab94424c386bea8b267754d698f8f0e", size = 492707 }, + { url = "https://files.pythonhosted.org/packages/c8/74/616bd8edfa7b0aaee96e4b3ad7edd0ccf0f4213a06050e965d68e0cdbaef/watchfiles-1.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ff236d7a3f4b0a42f699a22fc374ba526bc55048a70cbb299661158e1bb5e1f", size = 489293 }, + { url = "https://files.pythonhosted.org/packages/9c/1e/5335eaf5fb9a9516722c7f63f477ca1e361d8159fe46e03d96539cb80f5b/watchfiles-1.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c01446626574561756067f00b37e6b09c8622b0fc1e9fdbc7cbcea328d4e514", size = 442559 }, + { url = "https://files.pythonhosted.org/packages/c7/1c/df716e9acf7931b52f48bd9b2eec9a26ff55c73b43bfdbc03ea985543d01/watchfiles-1.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b551c465a59596f3d08170bd7e1c532c7260dd90ed8135778038e13c5d48aa81", size = 614531 }, + { url = "https://files.pythonhosted.org/packages/8d/38/c97d572e147234dd5f107179854efbf9ac6470db11db96f690cdb80e9b1b/watchfiles-1.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1ed613ee107269f66c2df631ec0fc8efddacface85314d392a4131abe299f00", size = 612853 }, + { url = "https://files.pythonhosted.org/packages/2d/1d/161eb1caa7e63b60428b2439efb0a87f0db4d5f4b91dd8712b6eca689954/watchfiles-1.0.0-cp312-none-win32.whl", hash = "sha256:5f75cd42e7e2254117cf37ff0e68c5b3f36c14543756b2da621408349bd9ca7c", size = 272337 }, + { url = "https://files.pythonhosted.org/packages/fc/1d/62acefeb546d24971e8f77cf5c475307054da4c21e9c49ec1917b293368b/watchfiles-1.0.0-cp312-none-win_amd64.whl", hash = "sha256:cf517701a4a872417f4e02a136e929537743461f9ec6cdb8184d9a04f4843545", size = 285572 }, + { url = "https://files.pythonhosted.org/packages/41/08/e20f3dbd2db59067596acc9b81345ac68a9c762352d38e789b2516719876/watchfiles-1.0.0-cp312-none-win_arm64.whl", hash = "sha256:8a2127cd68950787ee36753e6d401c8ea368f73beaeb8e54df5516a06d1ecd82", size = 276513 }, + { url = "https://files.pythonhosted.org/packages/c6/14/e14eb2ad369b306be70423fbf6da47bc39333d2beeafb14f23d2f37fdd79/watchfiles-1.0.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:95de85c254f7fe8cbdf104731f7f87f7f73ae229493bebca3722583160e6b152", size = 394141 }, + { url = "https://files.pythonhosted.org/packages/81/c3/738aeb2a01cbdf5fa823f702694ac72879a97fa5873d15d4607a877c7082/watchfiles-1.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:533a7cbfe700e09780bb31c06189e39c65f06c7f447326fee707fd02f9a6e945", size = 382833 }, + { url = "https://files.pythonhosted.org/packages/ed/aa/1cc14d11be667eb7189a2daa0adf307b93d6624fee5b80b8e84c23fb2486/watchfiles-1.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2218e78e2c6c07b1634a550095ac2a429026b2d5cbcd49a594f893f2bb8c936", size = 441231 }, + { url = "https://files.pythonhosted.org/packages/c5/38/96f4c3485094a164ced67ae444f3e890bdaad17d1b62c894aa8439443d81/watchfiles-1.0.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9122b8fdadc5b341315d255ab51d04893f417df4e6c1743b0aac8bf34e96e025", size = 447570 }, + { url = "https://files.pythonhosted.org/packages/9e/ce/0e35e0191517fa1d876ce0b4e23c818cf3a50d825305dcb7471da8774da7/watchfiles-1.0.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9272fdbc0e9870dac3b505bce1466d386b4d8d6d2bacf405e603108d50446940", size = 472440 }, + { url = "https://files.pythonhosted.org/packages/2c/b5/eb9c799c6e14f25f26573ac08734225035a8821f7dd9161c69df882fc119/watchfiles-1.0.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a3b33c3aefe9067ebd87846806cd5fc0b017ab70d628aaff077ab9abf4d06b3", size = 492706 }, + { url = "https://files.pythonhosted.org/packages/84/fa/985d4cbfe99a56d7277c0e522fd138fe5fc4d8ea6351ee3302e93ed67e63/watchfiles-1.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc338ce9f8846543d428260fa0f9a716626963148edc937d71055d01d81e1525", size = 489295 }, + { url = "https://files.pythonhosted.org/packages/94/1a/8bc18a170eb621a30fb01f4902d60ce362c88b1f65f3b15d45f53b467200/watchfiles-1.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ac778a460ea22d63c7e6fb0bc0f5b16780ff0b128f7f06e57aaec63bd339285", size = 442560 }, + { url = "https://files.pythonhosted.org/packages/e9/e0/07ce46f1770ca1d229635efb5393ff593c41762f389532ae9c7b2ced79b0/watchfiles-1.0.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:53ae447f06f8f29f5ab40140f19abdab822387a7c426a369eb42184b021e97eb", size = 614532 }, + { url = "https://files.pythonhosted.org/packages/7b/56/cdd2847d24249e879a001e6aed9ddeeaa24a80aabfdcb9c19389d0837dfe/watchfiles-1.0.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1f73c2147a453315d672c1ad907abe6d40324e34a185b51e15624bc793f93cc6", size = 612852 }, + { url = "https://files.pythonhosted.org/packages/72/c9/89a3df27c97eeef5890591a95f7afd266a32dfe55bce1f3bea3390fa56f5/watchfiles-1.0.0-cp313-none-win32.whl", hash = "sha256:eba98901a2eab909dbd79681190b9049acc650f6111fde1845484a4450761e98", size = 271721 }, + { url = "https://files.pythonhosted.org/packages/ef/e9/6e1bd83a08d254b0394500a2bb691b7940f09fcd849f400d01491932f641/watchfiles-1.0.0-cp313-none-win_amd64.whl", hash = "sha256:d562a6114ddafb09c33246c6ace7effa71ca4b6a2324a47f4b09b6445ea78941", size = 284809 }, + { url = "https://files.pythonhosted.org/packages/c7/6a/2abb1c062def34f9521bac3ca68a5a3f82fe10ec99b2c3cfff1d80fe9c4b/watchfiles-1.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f159ac795785cde4899e0afa539f4c723fb5dd336ce5605bc909d34edd00b79b", size = 395369 }, + { url = "https://files.pythonhosted.org/packages/5d/19/ee2fcaa691f59d30537aedb5ae206add0faf869c91843e2b86dc4d4bb783/watchfiles-1.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c3d258d78341d5d54c0c804a5b7faa66cd30ba50b2756a7161db07ce15363b8d", size = 384725 }, + { url = "https://files.pythonhosted.org/packages/68/93/583e52c1143b8e72564ae92d2b51c384245287b4782e039affa75e49487b/watchfiles-1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bbd0311588c2de7f9ea5cf3922ccacfd0ec0c1922870a2be503cc7df1ca8be7", size = 442645 }, + { url = "https://files.pythonhosted.org/packages/96/3e/1ff270fc153f051a8a2e5840917a48d72028bff83905f6b6a7d431fa0e3d/watchfiles-1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a13ac46b545a7d0d50f7641eefe47d1597e7d1783a5d89e09d080e6dff44b0", size = 442565 }, +] + +[[package]] +name = "wcmatch" +version = "10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/ab/b3a52228538ccb983653c446c1656eddf1d5303b9cb8b9aef6a91299f862/wcmatch-10.0.tar.gz", hash = "sha256:e72f0de09bba6a04e0de70937b0cf06e55f36f37b3deb422dfaf854b867b840a", size = 115578 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/df/4ee467ab39cc1de4b852c212c1ed3becfec2e486a51ac1ce0091f85f38d7/wcmatch-10.0-py3-none-any.whl", hash = "sha256:0dd927072d03c0a6527a20d2e6ad5ba8d0380e60870c383bc533b71744df7b7a", size = 39347 }, +] + +[[package]] +name = "websockets" +version = "14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/1b/380b883ce05bb5f45a905b61790319a28958a9ab1e4b6b95ff5464b60ca1/websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8", size = 162840 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/91/b1b375dbd856fd5fff3f117de0e520542343ecaf4e8fc60f1ac1e9f5822c/websockets-14.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a0adf84bc2e7c86e8a202537b4fd50e6f7f0e4a6b6bf64d7ccb96c4cd3330b29", size = 161950 }, + { url = "https://files.pythonhosted.org/packages/61/8f/4d52f272d3ebcd35e1325c646e98936099a348374d4a6b83b524bded8116/websockets-14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90b5d9dfbb6d07a84ed3e696012610b6da074d97453bd01e0e30744b472c8179", size = 159601 }, + { url = "https://files.pythonhosted.org/packages/c4/b1/29e87b53eb1937992cdee094a0988aadc94f25cf0b37e90c75eed7123d75/websockets-14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2177ee3901075167f01c5e335a6685e71b162a54a89a56001f1c3e9e3d2ad250", size = 159854 }, + { url = "https://files.pythonhosted.org/packages/3f/e6/752a2f5e8321ae2a613062676c08ff2fccfb37dc837a2ee919178a372e8a/websockets-14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f14a96a0034a27f9d47fd9788913924c89612225878f8078bb9d55f859272b0", size = 168835 }, + { url = "https://files.pythonhosted.org/packages/60/27/ca62de7877596926321b99071639275e94bb2401397130b7cf33dbf2106a/websockets-14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f874ba705deea77bcf64a9da42c1f5fc2466d8f14daf410bc7d4ceae0a9fcb0", size = 167844 }, + { url = "https://files.pythonhosted.org/packages/7e/db/f556a1d06635c680ef376be626c632e3f2bbdb1a0189d1d1bffb061c3b70/websockets-14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9607b9a442392e690a57909c362811184ea429585a71061cd5d3c2b98065c199", size = 168157 }, + { url = "https://files.pythonhosted.org/packages/b3/bc/99e5f511838c365ac6ecae19674eb5e94201aa4235bd1af3e6fa92c12905/websockets-14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bea45f19b7ca000380fbd4e02552be86343080120d074b87f25593ce1700ad58", size = 168561 }, + { url = "https://files.pythonhosted.org/packages/c6/e7/251491585bad61c79e525ac60927d96e4e17b18447cc9c3cfab47b2eb1b8/websockets-14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:219c8187b3ceeadbf2afcf0f25a4918d02da7b944d703b97d12fb01510869078", size = 167979 }, + { url = "https://files.pythonhosted.org/packages/ac/98/7ac2e4eeada19bdbc7a3a66a58e3ebdf33648b9e1c5b3f08c3224df168cf/websockets-14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad2ab2547761d79926effe63de21479dfaf29834c50f98c4bf5b5480b5838434", size = 167925 }, + { url = "https://files.pythonhosted.org/packages/ab/3d/09e65c47ee2396b7482968068f6e9b516221e1032b12dcf843b9412a5dfb/websockets-14.1-cp310-cp310-win32.whl", hash = "sha256:1288369a6a84e81b90da5dbed48610cd7e5d60af62df9851ed1d1d23a9069f10", size = 162831 }, + { url = "https://files.pythonhosted.org/packages/8a/67/59828a3d09740e6a485acccfbb66600632f2178b6ed1b61388ee96f17d5a/websockets-14.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0744623852f1497d825a49a99bfbec9bea4f3f946df6eb9d8a2f0c37a2fec2e", size = 163266 }, + { url = "https://files.pythonhosted.org/packages/97/ed/c0d03cb607b7fe1f7ff45e2cd4bb5cd0f9e3299ced79c2c303a6fff44524/websockets-14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:449d77d636f8d9c17952628cc7e3b8faf6e92a17ec581ec0c0256300717e1512", size = 161949 }, + { url = "https://files.pythonhosted.org/packages/06/91/bf0a44e238660d37a2dda1b4896235d20c29a2d0450f3a46cd688f43b239/websockets-14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a35f704be14768cea9790d921c2c1cc4fc52700410b1c10948511039be824aac", size = 159606 }, + { url = "https://files.pythonhosted.org/packages/ff/b8/7185212adad274c2b42b6a24e1ee6b916b7809ed611cbebc33b227e5c215/websockets-14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b1f3628a0510bd58968c0f60447e7a692933589b791a6b572fcef374053ca280", size = 159854 }, + { url = "https://files.pythonhosted.org/packages/5a/8a/0849968d83474be89c183d8ae8dcb7f7ada1a3c24f4d2a0d7333c231a2c3/websockets-14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c3deac3748ec73ef24fc7be0b68220d14d47d6647d2f85b2771cb35ea847aa1", size = 169402 }, + { url = "https://files.pythonhosted.org/packages/bd/4f/ef886e37245ff6b4a736a09b8468dae05d5d5c99de1357f840d54c6f297d/websockets-14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7048eb4415d46368ef29d32133134c513f507fff7d953c18c91104738a68c3b3", size = 168406 }, + { url = "https://files.pythonhosted.org/packages/11/43/e2dbd4401a63e409cebddedc1b63b9834de42f51b3c84db885469e9bdcef/websockets-14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cf0ad281c979306a6a34242b371e90e891bce504509fb6bb5246bbbf31e7b6", size = 168776 }, + { url = "https://files.pythonhosted.org/packages/6d/d6/7063e3f5c1b612e9f70faae20ebaeb2e684ffa36cb959eb0862ee2809b32/websockets-14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc1fc87428c1d18b643479caa7b15db7d544652e5bf610513d4a3478dbe823d0", size = 169083 }, + { url = "https://files.pythonhosted.org/packages/49/69/e6f3d953f2fa0f8a723cf18cd011d52733bd7f6e045122b24e0e7f49f9b0/websockets-14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f95ba34d71e2fa0c5d225bde3b3bdb152e957150100e75c86bc7f3964c450d89", size = 168529 }, + { url = "https://files.pythonhosted.org/packages/70/ff/f31fa14561fc1d7b8663b0ed719996cf1f581abee32c8fb2f295a472f268/websockets-14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9481a6de29105d73cf4515f2bef8eb71e17ac184c19d0b9918a3701c6c9c4f23", size = 168475 }, + { url = "https://files.pythonhosted.org/packages/f1/15/b72be0e4bf32ff373aa5baef46a4c7521b8ea93ad8b49ca8c6e8e764c083/websockets-14.1-cp311-cp311-win32.whl", hash = "sha256:368a05465f49c5949e27afd6fbe0a77ce53082185bbb2ac096a3a8afaf4de52e", size = 162833 }, + { url = "https://files.pythonhosted.org/packages/bc/ef/2d81679acbe7057ffe2308d422f744497b52009ea8bab34b6d74a2657d1d/websockets-14.1-cp311-cp311-win_amd64.whl", hash = "sha256:6d24fc337fc055c9e83414c94e1ee0dee902a486d19d2a7f0929e49d7d604b09", size = 163263 }, + { url = "https://files.pythonhosted.org/packages/55/64/55698544ce29e877c9188f1aee9093712411a8fc9732cca14985e49a8e9c/websockets-14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed", size = 161957 }, + { url = "https://files.pythonhosted.org/packages/a2/b1/b088f67c2b365f2c86c7b48edb8848ac27e508caf910a9d9d831b2f343cb/websockets-14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d", size = 159620 }, + { url = "https://files.pythonhosted.org/packages/c1/89/2a09db1bbb40ba967a1b8225b07b7df89fea44f06de9365f17f684d0f7e6/websockets-14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707", size = 159852 }, + { url = "https://files.pythonhosted.org/packages/ca/c1/f983138cd56e7d3079f1966e81f77ce6643f230cd309f73aa156bb181749/websockets-14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a", size = 169675 }, + { url = "https://files.pythonhosted.org/packages/c1/c8/84191455d8660e2a0bdb33878d4ee5dfa4a2cedbcdc88bbd097303b65bfa/websockets-14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45", size = 168619 }, + { url = "https://files.pythonhosted.org/packages/8d/a7/62e551fdcd7d44ea74a006dc193aba370505278ad76efd938664531ce9d6/websockets-14.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58", size = 169042 }, + { url = "https://files.pythonhosted.org/packages/ad/ed/1532786f55922c1e9c4d329608e36a15fdab186def3ca9eb10d7465bc1cc/websockets-14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058", size = 169345 }, + { url = "https://files.pythonhosted.org/packages/ea/fb/160f66960d495df3de63d9bcff78e1b42545b2a123cc611950ffe6468016/websockets-14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4", size = 168725 }, + { url = "https://files.pythonhosted.org/packages/cf/53/1bf0c06618b5ac35f1d7906444b9958f8485682ab0ea40dee7b17a32da1e/websockets-14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05", size = 168712 }, + { url = "https://files.pythonhosted.org/packages/e5/22/5ec2f39fff75f44aa626f86fa7f20594524a447d9c3be94d8482cd5572ef/websockets-14.1-cp312-cp312-win32.whl", hash = "sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0", size = 162838 }, + { url = "https://files.pythonhosted.org/packages/74/27/28f07df09f2983178db7bf6c9cccc847205d2b92ced986cd79565d68af4f/websockets-14.1-cp312-cp312-win_amd64.whl", hash = "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f", size = 163277 }, + { url = "https://files.pythonhosted.org/packages/34/77/812b3ba5110ed8726eddf9257ab55ce9e85d97d4aa016805fdbecc5e5d48/websockets-14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9", size = 161966 }, + { url = "https://files.pythonhosted.org/packages/8d/24/4fcb7aa6986ae7d9f6d083d9d53d580af1483c5ec24bdec0978307a0f6ac/websockets-14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b", size = 159625 }, + { url = "https://files.pythonhosted.org/packages/f8/47/2a0a3a2fc4965ff5b9ce9324d63220156bd8bedf7f90824ab92a822e65fd/websockets-14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3", size = 159857 }, + { url = "https://files.pythonhosted.org/packages/dd/c8/d7b425011a15e35e17757e4df75b25e1d0df64c0c315a44550454eaf88fc/websockets-14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59", size = 169635 }, + { url = "https://files.pythonhosted.org/packages/93/39/6e3b5cffa11036c40bd2f13aba2e8e691ab2e01595532c46437b56575678/websockets-14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2", size = 168578 }, + { url = "https://files.pythonhosted.org/packages/cf/03/8faa5c9576299b2adf34dcccf278fc6bbbcda8a3efcc4d817369026be421/websockets-14.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da", size = 169018 }, + { url = "https://files.pythonhosted.org/packages/8c/05/ea1fec05cc3a60defcdf0bb9f760c3c6bd2dd2710eff7ac7f891864a22ba/websockets-14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9", size = 169383 }, + { url = "https://files.pythonhosted.org/packages/21/1d/eac1d9ed787f80754e51228e78855f879ede1172c8b6185aca8cef494911/websockets-14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7", size = 168773 }, + { url = "https://files.pythonhosted.org/packages/0e/1b/e808685530185915299740d82b3a4af3f2b44e56ccf4389397c7a5d95d39/websockets-14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a", size = 168757 }, + { url = "https://files.pythonhosted.org/packages/b6/19/6ab716d02a3b068fbbeb6face8a7423156e12c446975312f1c7c0f4badab/websockets-14.1-cp313-cp313-win32.whl", hash = "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6", size = 162834 }, + { url = "https://files.pythonhosted.org/packages/6c/fd/ab6b7676ba712f2fc89d1347a4b5bdc6aa130de10404071f2b2606450209/websockets-14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0", size = 163277 }, + { url = "https://files.pythonhosted.org/packages/fb/cd/382a05a1ba2a93bd9fb807716a660751295df72e77204fb130a102fcdd36/websockets-14.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e5dc25a9dbd1a7f61eca4b7cb04e74ae4b963d658f9e4f9aad9cd00b688692c8", size = 159633 }, + { url = "https://files.pythonhosted.org/packages/b7/a0/fa7c62e2952ef028b422fbf420f9353d9dd4dfaa425de3deae36e98c0784/websockets-14.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:04a97aca96ca2acedf0d1f332c861c5a4486fdcba7bcef35873820f940c4231e", size = 159867 }, + { url = "https://files.pythonhosted.org/packages/c1/94/954b4924f868db31d5f0935893c7a8446515ee4b36bb8ad75a929469e453/websockets-14.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df174ece723b228d3e8734a6f2a6febbd413ddec39b3dc592f5a4aa0aff28098", size = 161121 }, + { url = "https://files.pythonhosted.org/packages/7a/2e/f12bbb41a8f2abb76428ba4fdcd9e67b5b364a3e7fa97c88f4d6950aa2d4/websockets-14.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:034feb9f4286476f273b9a245fb15f02c34d9586a5bc936aff108c3ba1b21beb", size = 160731 }, + { url = "https://files.pythonhosted.org/packages/13/97/b76979401f2373af1fe3e08f960b265cecab112e7dac803446fb98351a52/websockets-14.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c308dabd2b380807ab64b62985eaccf923a78ebc572bd485375b9ca2b7dc7", size = 160681 }, + { url = "https://files.pythonhosted.org/packages/39/9c/16916d9a436c109a1d7ba78817e8fee357b78968be3f6e6f517f43afa43d/websockets-14.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5a42d3ecbb2db5080fc578314439b1d79eef71d323dc661aa616fb492436af5d", size = 163316 }, + { url = "https://files.pythonhosted.org/packages/b0/0b/c7e5d11020242984d9d37990310520ed663b942333b83a033c2f20191113/websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e", size = 156277 }, +] + +[[package]] +name = "wrapt" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/a1/fc03dca9b0432725c2e8cdbf91a349d2194cf03d8523c124faebe581de09/wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801", size = 55542 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/f9/85220321e9bb1a5f72ccce6604395ae75fcb463d87dad0014dc1010bd1f1/wrapt-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8", size = 38766 }, + { url = "https://files.pythonhosted.org/packages/ff/71/ff624ff3bde91ceb65db6952cdf8947bc0111d91bd2359343bc2fa7c57fd/wrapt-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d", size = 83262 }, + { url = "https://files.pythonhosted.org/packages/9f/0a/814d4a121a643af99cfe55a43e9e6dd08f4a47cdac8e8f0912c018794715/wrapt-1.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e185ec6060e301a7e5f8461c86fb3640a7beb1a0f0208ffde7a65ec4074931df", size = 74990 }, + { url = "https://files.pythonhosted.org/packages/cd/c7/b8c89bf5ca5c4e6a2d0565d149d549cdb4cffb8916d1d1b546b62fb79281/wrapt-1.17.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb90765dd91aed05b53cd7a87bd7f5c188fcd95960914bae0d32c5e7f899719d", size = 82712 }, + { url = "https://files.pythonhosted.org/packages/19/7c/5977aefa8460906c1ff914fd42b11cf6c09ded5388e46e1cc6cea4ab15e9/wrapt-1.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:879591c2b5ab0a7184258274c42a126b74a2c3d5a329df16d69f9cee07bba6ea", size = 81705 }, + { url = "https://files.pythonhosted.org/packages/ae/e7/233402d7bd805096bb4a8ec471f5a141421a01de3c8c957cce569772c056/wrapt-1.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fce6fee67c318fdfb7f285c29a82d84782ae2579c0e1b385b7f36c6e8074fffb", size = 74636 }, + { url = "https://files.pythonhosted.org/packages/93/81/b6c32d8387d9cfbc0134f01585dee7583315c3b46dfd3ae64d47693cd078/wrapt-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0698d3a86f68abc894d537887b9bbf84d29bcfbc759e23f4644be27acf6da301", size = 81299 }, + { url = "https://files.pythonhosted.org/packages/d1/c3/1fae15d453468c98f09519076f8d401b476d18d8d94379e839eed14c4c8b/wrapt-1.17.0-cp310-cp310-win32.whl", hash = "sha256:69d093792dc34a9c4c8a70e4973a3361c7a7578e9cd86961b2bbf38ca71e4e22", size = 36425 }, + { url = "https://files.pythonhosted.org/packages/c6/f4/77e0886c95556f2b4caa8908ea8eb85f713fc68296a2113f8c63d50fe0fb/wrapt-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:f28b29dc158ca5d6ac396c8e0a2ef45c4e97bb7e65522bfc04c989e6fe814575", size = 38748 }, + { url = "https://files.pythonhosted.org/packages/0e/40/def56538acddc2f764c157d565b9f989072a1d2f2a8e384324e2e104fc7d/wrapt-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a", size = 38766 }, + { url = "https://files.pythonhosted.org/packages/89/e2/8c299f384ae4364193724e2adad99f9504599d02a73ec9199bf3f406549d/wrapt-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed", size = 83730 }, + { url = "https://files.pythonhosted.org/packages/29/ef/fcdb776b12df5ea7180d065b28fa6bb27ac785dddcd7202a0b6962bbdb47/wrapt-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489", size = 75470 }, + { url = "https://files.pythonhosted.org/packages/55/b5/698bd0bf9fbb3ddb3a2feefbb7ad0dea1205f5d7d05b9cbab54f5db731aa/wrapt-1.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9", size = 83168 }, + { url = "https://files.pythonhosted.org/packages/ce/07/701a5cee28cb4d5df030d4b2649319e36f3d9fdd8000ef1d84eb06b9860d/wrapt-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339", size = 82307 }, + { url = "https://files.pythonhosted.org/packages/42/92/c48ba92cda6f74cb914dc3c5bba9650dc80b790e121c4b987f3a46b028f5/wrapt-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d", size = 75101 }, + { url = "https://files.pythonhosted.org/packages/8a/0a/9276d3269334138b88a2947efaaf6335f61d547698e50dff672ade24f2c6/wrapt-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b", size = 81835 }, + { url = "https://files.pythonhosted.org/packages/b9/4c/39595e692753ef656ea94b51382cc9aea662fef59d7910128f5906486f0e/wrapt-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346", size = 36412 }, + { url = "https://files.pythonhosted.org/packages/63/bb/c293a67fb765a2ada48f48cd0f2bb957da8161439da4c03ea123b9894c02/wrapt-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a", size = 38744 }, + { url = "https://files.pythonhosted.org/packages/85/82/518605474beafff11f1a34759f6410ab429abff9f7881858a447e0d20712/wrapt-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569", size = 38904 }, + { url = "https://files.pythonhosted.org/packages/80/6c/17c3b2fed28edfd96d8417c865ef0b4c955dc52c4e375d86f459f14340f1/wrapt-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504", size = 88622 }, + { url = "https://files.pythonhosted.org/packages/4a/11/60ecdf3b0fd3dca18978d89acb5d095a05f23299216e925fcd2717c81d93/wrapt-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451", size = 80920 }, + { url = "https://files.pythonhosted.org/packages/d2/50/dbef1a651578a3520d4534c1e434989e3620380c1ad97e309576b47f0ada/wrapt-1.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1", size = 89170 }, + { url = "https://files.pythonhosted.org/packages/44/a2/78c5956bf39955288c9e0dd62e807b308c3aa15a0f611fbff52aa8d6b5ea/wrapt-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106", size = 86748 }, + { url = "https://files.pythonhosted.org/packages/99/49/2ee413c78fc0bdfebe5bee590bf3becdc1fab0096a7a9c3b5c9666b2415f/wrapt-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada", size = 79734 }, + { url = "https://files.pythonhosted.org/packages/c0/8c/4221b7b270e36be90f0930fe15a4755a6ea24093f90b510166e9ed7861ea/wrapt-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4", size = 87552 }, + { url = "https://files.pythonhosted.org/packages/4c/6b/1aaccf3efe58eb95e10ce8e77c8909b7a6b0da93449a92c4e6d6d10b3a3d/wrapt-1.17.0-cp312-cp312-win32.whl", hash = "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635", size = 36647 }, + { url = "https://files.pythonhosted.org/packages/b3/4f/243f88ac49df005b9129194c6511b3642818b3e6271ddea47a15e2ee4934/wrapt-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7", size = 38830 }, + { url = "https://files.pythonhosted.org/packages/67/9c/38294e1bb92b055222d1b8b6591604ca4468b77b1250f59c15256437644f/wrapt-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181", size = 38904 }, + { url = "https://files.pythonhosted.org/packages/78/b6/76597fb362cbf8913a481d41b14b049a8813cd402a5d2f84e57957c813ae/wrapt-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393", size = 88608 }, + { url = "https://files.pythonhosted.org/packages/bc/69/b500884e45b3881926b5f69188dc542fb5880019d15c8a0df1ab1dfda1f7/wrapt-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4", size = 80879 }, + { url = "https://files.pythonhosted.org/packages/52/31/f4cc58afe29eab8a50ac5969963010c8b60987e719c478a5024bce39bc42/wrapt-1.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b", size = 89119 }, + { url = "https://files.pythonhosted.org/packages/aa/9c/05ab6bf75dbae7a9d34975fb6ee577e086c1c26cde3b6cf6051726d33c7c/wrapt-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721", size = 86778 }, + { url = "https://files.pythonhosted.org/packages/0e/6c/4b8d42e3db355603d35fe5c9db79c28f2472a6fd1ccf4dc25ae46739672a/wrapt-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90", size = 79793 }, + { url = "https://files.pythonhosted.org/packages/69/23/90e3a2ee210c0843b2c2a49b3b97ffcf9cad1387cb18cbeef9218631ed5a/wrapt-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a", size = 87606 }, + { url = "https://files.pythonhosted.org/packages/5f/06/3683126491ca787d8d71d8d340e775d40767c5efedb35039d987203393b7/wrapt-1.17.0-cp313-cp313-win32.whl", hash = "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045", size = 36651 }, + { url = "https://files.pythonhosted.org/packages/f1/bc/3bf6d2ca0d2c030d324ef9272bea0a8fdaff68f3d1fa7be7a61da88e51f7/wrapt-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838", size = 38835 }, + { url = "https://files.pythonhosted.org/packages/ce/b5/251165c232d87197a81cd362eeb5104d661a2dd3aa1f0b33e4bf61dda8b8/wrapt-1.17.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b", size = 40146 }, + { url = "https://files.pythonhosted.org/packages/89/33/1e1bdd3e866eeb73d8c4755db1ceb8a80d5bd51ee4648b3f2247adec4e67/wrapt-1.17.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379", size = 113444 }, + { url = "https://files.pythonhosted.org/packages/9f/7c/94f53b065a43f5dc1fbdd8b80fd8f41284315b543805c956619c0b8d92f0/wrapt-1.17.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d", size = 101246 }, + { url = "https://files.pythonhosted.org/packages/62/5d/640360baac6ea6018ed5e34e6e80e33cfbae2aefde24f117587cd5efd4b7/wrapt-1.17.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f", size = 109320 }, + { url = "https://files.pythonhosted.org/packages/e3/cf/6c7a00ae86a2e9482c91170aefe93f4ccda06c1ac86c4de637c69133da59/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c", size = 110193 }, + { url = "https://files.pythonhosted.org/packages/cd/cc/aa718df0d20287e8f953ce0e2f70c0af0fba1d3c367db7ee8bdc46ea7003/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b", size = 100460 }, + { url = "https://files.pythonhosted.org/packages/f7/16/9f3ac99fe1f6caaa789d67b4e3c562898b532c250769f5255fa8b8b93983/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab", size = 106347 }, + { url = "https://files.pythonhosted.org/packages/64/85/c77a331b2c06af49a687f8b926fc2d111047a51e6f0b0a4baa01ff3a673a/wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf", size = 37971 }, + { url = "https://files.pythonhosted.org/packages/05/9b/b2469f8be9efed24283fd7b9eeb8e913e9bc0715cf919ea8645e428ab7af/wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a", size = 40755 }, + { url = "https://files.pythonhosted.org/packages/4b/d9/a8ba5e9507a9af1917285d118388c5eb7a81834873f45df213a6fe923774/wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371", size = 23592 }, +] + +[[package]] +name = "yarl" +version = "1.18.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/98/e005bc608765a8a5569f58e650961314873c8469c333616eb40bff19ae97/yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34", size = 141458 }, + { url = "https://files.pythonhosted.org/packages/df/5d/f8106b263b8ae8a866b46d9be869ac01f9b3fb7f2325f3ecb3df8003f796/yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7", size = 94365 }, + { url = "https://files.pythonhosted.org/packages/56/3e/d8637ddb9ba69bf851f765a3ee288676f7cf64fb3be13760c18cbc9d10bd/yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed", size = 92181 }, + { url = "https://files.pythonhosted.org/packages/76/f9/d616a5c2daae281171de10fba41e1c0e2d8207166fc3547252f7d469b4e1/yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde", size = 315349 }, + { url = "https://files.pythonhosted.org/packages/bb/b4/3ea5e7b6f08f698b3769a06054783e434f6d59857181b5c4e145de83f59b/yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b", size = 330494 }, + { url = "https://files.pythonhosted.org/packages/55/f1/e0fc810554877b1b67420568afff51b967baed5b53bcc983ab164eebf9c9/yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5", size = 326927 }, + { url = "https://files.pythonhosted.org/packages/a9/42/b1753949b327b36f210899f2dd0a0947c0c74e42a32de3f8eb5c7d93edca/yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc", size = 319703 }, + { url = "https://files.pythonhosted.org/packages/f0/6d/e87c62dc9635daefb064b56f5c97df55a2e9cc947a2b3afd4fd2f3b841c7/yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd", size = 310246 }, + { url = "https://files.pythonhosted.org/packages/e3/ef/e2e8d1785cdcbd986f7622d7f0098205f3644546da7919c24b95790ec65a/yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990", size = 319730 }, + { url = "https://files.pythonhosted.org/packages/fc/15/8723e22345bc160dfde68c4b3ae8b236e868f9963c74015f1bc8a614101c/yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db", size = 321681 }, + { url = "https://files.pythonhosted.org/packages/86/09/bf764e974f1516efa0ae2801494a5951e959f1610dd41edbfc07e5e0f978/yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62", size = 324812 }, + { url = "https://files.pythonhosted.org/packages/f6/4c/20a0187e3b903c97d857cf0272d687c1b08b03438968ae8ffc50fe78b0d6/yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760", size = 337011 }, + { url = "https://files.pythonhosted.org/packages/c9/71/6244599a6e1cc4c9f73254a627234e0dad3883ece40cc33dce6265977461/yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b", size = 338132 }, + { url = "https://files.pythonhosted.org/packages/af/f5/e0c3efaf74566c4b4a41cb76d27097df424052a064216beccae8d303c90f/yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690", size = 331849 }, + { url = "https://files.pythonhosted.org/packages/8a/b8/3d16209c2014c2f98a8f658850a57b716efb97930aebf1ca0d9325933731/yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6", size = 84309 }, + { url = "https://files.pythonhosted.org/packages/fd/b7/2e9a5b18eb0fe24c3a0e8bae994e812ed9852ab4fd067c0107fadde0d5f0/yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8", size = 90484 }, + { url = "https://files.pythonhosted.org/packages/40/93/282b5f4898d8e8efaf0790ba6d10e2245d2c9f30e199d1a85cae9356098c/yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069", size = 141555 }, + { url = "https://files.pythonhosted.org/packages/6d/9c/0a49af78df099c283ca3444560f10718fadb8a18dc8b3edf8c7bd9fd7d89/yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193", size = 94351 }, + { url = "https://files.pythonhosted.org/packages/5a/a1/205ab51e148fdcedad189ca8dd587794c6f119882437d04c33c01a75dece/yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889", size = 92286 }, + { url = "https://files.pythonhosted.org/packages/ed/fe/88b690b30f3f59275fb674f5f93ddd4a3ae796c2b62e5bb9ece8a4914b83/yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8", size = 340649 }, + { url = "https://files.pythonhosted.org/packages/07/eb/3b65499b568e01f36e847cebdc8d7ccb51fff716dbda1ae83c3cbb8ca1c9/yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca", size = 356623 }, + { url = "https://files.pythonhosted.org/packages/33/46/f559dc184280b745fc76ec6b1954de2c55595f0ec0a7614238b9ebf69618/yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8", size = 354007 }, + { url = "https://files.pythonhosted.org/packages/af/ba/1865d85212351ad160f19fb99808acf23aab9a0f8ff31c8c9f1b4d671fc9/yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae", size = 344145 }, + { url = "https://files.pythonhosted.org/packages/94/cb/5c3e975d77755d7b3d5193e92056b19d83752ea2da7ab394e22260a7b824/yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3", size = 336133 }, + { url = "https://files.pythonhosted.org/packages/19/89/b77d3fd249ab52a5c40859815765d35c91425b6bb82e7427ab2f78f5ff55/yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb", size = 347967 }, + { url = "https://files.pythonhosted.org/packages/35/bd/f6b7630ba2cc06c319c3235634c582a6ab014d52311e7d7c22f9518189b5/yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e", size = 346397 }, + { url = "https://files.pythonhosted.org/packages/18/1a/0b4e367d5a72d1f095318344848e93ea70da728118221f84f1bf6c1e39e7/yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59", size = 350206 }, + { url = "https://files.pythonhosted.org/packages/b5/cf/320fff4367341fb77809a2d8d7fe75b5d323a8e1b35710aafe41fdbf327b/yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d", size = 362089 }, + { url = "https://files.pythonhosted.org/packages/57/cf/aadba261d8b920253204085268bad5e8cdd86b50162fcb1b10c10834885a/yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e", size = 366267 }, + { url = "https://files.pythonhosted.org/packages/54/58/fb4cadd81acdee6dafe14abeb258f876e4dd410518099ae9a35c88d8097c/yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a", size = 359141 }, + { url = "https://files.pythonhosted.org/packages/9a/7a/4c571597589da4cd5c14ed2a0b17ac56ec9ee7ee615013f74653169e702d/yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1", size = 84402 }, + { url = "https://files.pythonhosted.org/packages/ae/7b/8600250b3d89b625f1121d897062f629883c2f45339623b69b1747ec65fa/yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5", size = 91030 }, + { url = "https://files.pythonhosted.org/packages/33/85/bd2e2729752ff4c77338e0102914897512e92496375e079ce0150a6dc306/yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50", size = 142644 }, + { url = "https://files.pythonhosted.org/packages/ff/74/1178322cc0f10288d7eefa6e4a85d8d2e28187ccab13d5b844e8b5d7c88d/yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576", size = 94962 }, + { url = "https://files.pythonhosted.org/packages/be/75/79c6acc0261e2c2ae8a1c41cf12265e91628c8c58ae91f5ff59e29c0787f/yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640", size = 92795 }, + { url = "https://files.pythonhosted.org/packages/6b/32/927b2d67a412c31199e83fefdce6e645247b4fb164aa1ecb35a0f9eb2058/yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2", size = 332368 }, + { url = "https://files.pythonhosted.org/packages/19/e5/859fca07169d6eceeaa4fde1997c91d8abde4e9a7c018e371640c2da2b71/yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75", size = 342314 }, + { url = "https://files.pythonhosted.org/packages/08/75/76b63ccd91c9e03ab213ef27ae6add2e3400e77e5cdddf8ed2dbc36e3f21/yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512", size = 341987 }, + { url = "https://files.pythonhosted.org/packages/1a/e1/a097d5755d3ea8479a42856f51d97eeff7a3a7160593332d98f2709b3580/yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba", size = 336914 }, + { url = "https://files.pythonhosted.org/packages/0b/42/e1b4d0e396b7987feceebe565286c27bc085bf07d61a59508cdaf2d45e63/yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb", size = 325765 }, + { url = "https://files.pythonhosted.org/packages/7e/18/03a5834ccc9177f97ca1bbb245b93c13e58e8225276f01eedc4cc98ab820/yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272", size = 344444 }, + { url = "https://files.pythonhosted.org/packages/c8/03/a713633bdde0640b0472aa197b5b86e90fbc4c5bc05b727b714cd8a40e6d/yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6", size = 340760 }, + { url = "https://files.pythonhosted.org/packages/eb/99/f6567e3f3bbad8fd101886ea0276c68ecb86a2b58be0f64077396cd4b95e/yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e", size = 346484 }, + { url = "https://files.pythonhosted.org/packages/8e/a9/84717c896b2fc6cb15bd4eecd64e34a2f0a9fd6669e69170c73a8b46795a/yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb", size = 359864 }, + { url = "https://files.pythonhosted.org/packages/1e/2e/d0f5f1bef7ee93ed17e739ec8dbcb47794af891f7d165fa6014517b48169/yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393", size = 364537 }, + { url = "https://files.pythonhosted.org/packages/97/8a/568d07c5d4964da5b02621a517532adb8ec5ba181ad1687191fffeda0ab6/yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285", size = 357861 }, + { url = "https://files.pythonhosted.org/packages/7d/e3/924c3f64b6b3077889df9a1ece1ed8947e7b61b0a933f2ec93041990a677/yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2", size = 84097 }, + { url = "https://files.pythonhosted.org/packages/34/45/0e055320daaabfc169b21ff6174567b2c910c45617b0d79c68d7ab349b02/yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477", size = 90399 }, + { url = "https://files.pythonhosted.org/packages/30/c7/c790513d5328a8390be8f47be5d52e141f78b66c6c48f48d241ca6bd5265/yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb", size = 140789 }, + { url = "https://files.pythonhosted.org/packages/30/aa/a2f84e93554a578463e2edaaf2300faa61c8701f0898725842c704ba5444/yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa", size = 94144 }, + { url = "https://files.pythonhosted.org/packages/c6/fc/d68d8f83714b221a85ce7866832cba36d7c04a68fa6a960b908c2c84f325/yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782", size = 91974 }, + { url = "https://files.pythonhosted.org/packages/56/4e/d2563d8323a7e9a414b5b25341b3942af5902a2263d36d20fb17c40411e2/yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0", size = 333587 }, + { url = "https://files.pythonhosted.org/packages/25/c9/cfec0bc0cac8d054be223e9f2c7909d3e8442a856af9dbce7e3442a8ec8d/yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482", size = 344386 }, + { url = "https://files.pythonhosted.org/packages/ab/5d/4c532190113b25f1364d25f4c319322e86232d69175b91f27e3ebc2caf9a/yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186", size = 345421 }, + { url = "https://files.pythonhosted.org/packages/23/d1/6cdd1632da013aa6ba18cee4d750d953104a5e7aac44e249d9410a972bf5/yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58", size = 339384 }, + { url = "https://files.pythonhosted.org/packages/9a/c4/6b3c39bec352e441bd30f432cda6ba51681ab19bb8abe023f0d19777aad1/yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53", size = 326689 }, + { url = "https://files.pythonhosted.org/packages/23/30/07fb088f2eefdc0aa4fc1af4e3ca4eb1a3aadd1ce7d866d74c0f124e6a85/yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2", size = 345453 }, + { url = "https://files.pythonhosted.org/packages/63/09/d54befb48f9cd8eec43797f624ec37783a0266855f4930a91e3d5c7717f8/yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8", size = 341872 }, + { url = "https://files.pythonhosted.org/packages/91/26/fd0ef9bf29dd906a84b59f0cd1281e65b0c3e08c6aa94b57f7d11f593518/yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1", size = 347497 }, + { url = "https://files.pythonhosted.org/packages/d9/b5/14ac7a256d0511b2ac168d50d4b7d744aea1c1aa20c79f620d1059aab8b2/yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a", size = 359981 }, + { url = "https://files.pythonhosted.org/packages/ca/b3/d493221ad5cbd18bc07e642894030437e405e1413c4236dd5db6e46bcec9/yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10", size = 366229 }, + { url = "https://files.pythonhosted.org/packages/04/56/6a3e2a5d9152c56c346df9b8fb8edd2c8888b1e03f96324d457e5cf06d34/yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8", size = 360383 }, + { url = "https://files.pythonhosted.org/packages/fd/b7/4b3c7c7913a278d445cc6284e59b2e62fa25e72758f888b7a7a39eb8423f/yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d", size = 310152 }, + { url = "https://files.pythonhosted.org/packages/f5/d5/688db678e987c3e0fb17867970700b92603cadf36c56e5fb08f23e822a0c/yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c", size = 315723 }, + { url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109 }, +]