diff --git a/.all-contributorsrc b/.all-contributorsrc index 0696300986..8121a36273 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1072,7 +1072,9 @@ "avatar_url": "https://avatars.githubusercontent.com/u/57007485?v=4", "profile": "https://github.com/floxay", "contributions": [ - "doc" + "doc", + "code", + "test" ] }, { @@ -1621,7 +1623,9 @@ "avatar_url": "https://avatars.githubusercontent.com/u/75439739?v=4", "profile": "https://github.com/aranvir", "contributions": [ - "doc" + "doc", + "code", + "test" ] }, { @@ -1838,6 +1842,100 @@ "test", "doc" ] + }, + { + "login": "trim21", + "name": "Trim21", + "avatar_url": "https://avatars.githubusercontent.com/u/13553903?v=4", + "profile": "https://blog.trim21.me/", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "aarcex3", + "name": "Agustin Arce", + "avatar_url": "https://avatars.githubusercontent.com/u/59893355?v=4", + "profile": "http://aarcex3.github.io", + "contributions": [ + "doc" + ] + }, + { + "login": "FarhanAliRaza", + "name": "Farhan Ali Raza", + "avatar_url": "https://avatars.githubusercontent.com/u/62690310?v=4", + "profile": "https://github.com/FarhanAliRaza", + "contributions": [ + "doc" + ] + }, + { + "login": "pogopaule", + "name": "Fabian", + "avatar_url": "https://avatars.githubusercontent.com/u/576949?v=4", + "profile": "https://github.com/pogopaule", + "contributions": [ + "code" + ] + }, + { + "login": "mohammedbabelly20", + "name": "Mohammed Babelly", + "avatar_url": "https://avatars.githubusercontent.com/u/104768048?v=4", + "profile": "https://github.com/mohammedbabelly20", + "contributions": [ + "code" + ] + }, + { + "login": "charles-dyfis-net", + "name": "Charles Duffy", + "avatar_url": "https://avatars.githubusercontent.com/u/22370?v=4", + "profile": "https://keybase.io/charlesdyfisnet", + "contributions": [ + "code" + ] + }, + { + "login": "RenameMe1", + "name": "Evgeny Demchenko", + "avatar_url": "https://avatars.githubusercontent.com/u/165988121?v=4", + "profile": "https://github.com/RenameMe1", + "contributions": [ + "doc" + ] + }, + { + "login": "olzhasar", + "name": "Olzhas Arystanov", + "avatar_url": "https://avatars.githubusercontent.com/u/12471703?v=4", + "profile": "https://olzhasar.com", + "contributions": [ + "bug", + "doc" + ] + }, + { + "login": "vikigenius", + "name": "Vikash", + "avatar_url": "https://avatars.githubusercontent.com/u/12724810?v=4", + "profile": "https://github.com/vikigenius", + "contributions": [ + "code" + ] + }, + { + "login": "ftsartek", + "name": "Jordan Russell", + "avatar_url": "https://avatars.githubusercontent.com/u/20253317?v=4", + "profile": "https://github.com/ftsartek", + "contributions": [ + "doc", + "test", + "code" + ] } ], "contributorsPerLine": 7, diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 6eaa23c944..2513f353b5 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,8 +1,13 @@ -# [Choice] Python version (use -bookworm or -bullseye variants on local arm64/Apple Silicon): 3, 3.11, 3.10, 3.9, 3.8, 3-bookworm, 3.11-bookworm, 3.10-bookworm, 3.9-bookworm, 3.8-bookworm, 3-bullseye, 3.11-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3-buster, 3.11-buster, 3.10-buster, 3.9-buster, 3.8-buster -ARG VARIANT=3-bookworm -FROM python:${VARIANT} +# [Choice] Python version (use -bookworm or -bullseye variants on local arm64/Apple Silicon): 3, 3.13, 3.12, 3.11, 3.10, 3.9, 3.8, 3-bookworm, 3.13-bookworm, 3.12-bookworm, 3.11-bookworm, 3.10-bookworm, 3.9-bookworm, 3.8-bookworm, 3-bullseye, 3.11-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3-buster, 3.11-buster, 3.10-buster, 3.9-buster, 3.8-buster +ARG VERSION=3.12 +ARG VARIANT=-bookworm +FROM python:${VERSION}${VARIANT} + +ARG VERSION +ENV UV_LOCKED=1 UV_PYTHON=${VERSION} +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ && apt-get purge -y fish -RUN python3 -m pip install --upgrade setuptools cython pip pdm +RUN python3 -m pip install --upgrade setuptools cython pip diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7164363c93..cc7e84c193 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -59,8 +59,8 @@ }, "forwardPorts": [8000], "postCreateCommand": [ - "pdm", - "install" + "uv", + "sync" ], "remoteUser": "vscode" } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8505f5972e..2e71d4d67b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,9 @@ on: - main - v1.51 +env: + UV_LOCKED: 1 + jobs: validate: runs-on: ubuntu-latest @@ -41,20 +44,14 @@ jobs: python-version: "3.8" allow-prereleases: true - - uses: pdm-project/setup-pdm@v4 - name: Set up PDM + - name: Install uv + uses: astral-sh/setup-uv@v5 with: - python-version: "3.8" - allow-python-prereleases: false - cache: true - cache-dependency-path: | - ./pdm.lock - - - name: Install dependencies - run: pdm install -G:all + version: "0.5.4" + enable-cache: true - name: Run mypy - run: pdm run mypy + run: uv run mypy pyright: runs-on: ubuntu-latest @@ -66,20 +63,14 @@ jobs: python-version: "3.8" allow-prereleases: true - - uses: pdm-project/setup-pdm@v4 - name: Set up PDM + - name: Install uv + uses: astral-sh/setup-uv@v5 with: - python-version: "3.8" - allow-python-prereleases: false - cache: true - cache-dependency-path: | - ./pdm.lock - - - name: Install dependencies - run: pdm install -G:all + version: "0.5.4" + enable-cache: true - name: Run pyright - run: pdm run pyright + run: uv run pyright slotscheck: runs-on: ubuntu-latest @@ -91,20 +82,14 @@ jobs: python-version: "3.8" allow-prereleases: false - - uses: pdm-project/setup-pdm@v4 - name: Set up PDM + - name: Install uv + uses: astral-sh/setup-uv@v5 with: - python-version: "3.8" - allow-python-prereleases: false - cache: true - cache-dependency-path: | - ./pdm.lock - - - name: Install dependencies - run: pdm install -G:all + version: "0.5.4" + enable-cache: true - name: Run slotscheck - run: pdm run slotscheck litestar + run: uv run slotscheck litestar test: name: "test (${{ matrix.python-version }})" @@ -132,25 +117,22 @@ jobs: with: python-version: 3.11 - - uses: pdm-project/setup-pdm@v4 - name: Set up PDM + - name: Install uv + uses: astral-sh/setup-uv@v5 with: - python-version: 3.11 - allow-python-prereleases: false - cache: true - cache-dependency-path: | - ./pdm.lock + version: "0.5.4" + enable-cache: true - name: Install dependencies run: | - pdm install -G:all - pip install -U "${{ matrix.uvicorn-version }}" + uv sync + uv pip install -U "${{ matrix.uvicorn-version }}" - name: Set PYTHONPATH run: echo "PYTHONPATH=$PWD" >> $GITHUB_ENV - name: Test - run: pdm run pytest tests -m server_integration + run: uv run --no-sync pytest tests -m server_integration test-platform-compat: if: github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'test platform compat') @@ -214,25 +196,19 @@ jobs: python-version: "3.12" allow-prereleases: true - - uses: pdm-project/setup-pdm@v4 - name: Set up PDM + - name: Install uv + uses: astral-sh/setup-uv@v5 with: - python-version: "3.12" - allow-python-prereleases: false - cache: true - cache-dependency-path: | - ./pdm.lock - - - name: Install dependencies - run: pdm install -G:all + version: "0.5.4" + enable-cache: true - name: Build docs - run: pdm run make docs + run: uv run make docs - name: Check docs links env: LITESTAR_DOCS_IGNORE_MISSING_EXAMPLE_OUTPUT: 1 - run: pdm run make docs-linkcheck + run: uv run make docs-linkcheck - name: Save PR number run: | @@ -245,6 +221,7 @@ jobs: path: | docs/_build/html .pr_number + include-hidden-files: true test_minimal_app: name: Test Minimal Application with Base Dependencies @@ -260,23 +237,19 @@ jobs: with: python-version: "3.12" - - uses: pdm-project/setup-pdm@v4 - name: Set up PDM + - name: Install uv + uses: astral-sh/setup-uv@v5 with: - python-version: "3.12" - allow-python-prereleases: false - cache: true - cache-dependency-path: | - ./pdm.lock - - - name: Install dependencies - run: pdm install + version: "0.5.4" + enable-cache: true - name: Set pythonpath run: echo "PYTHONPATH=$PWD" >> $GITHUB_ENV - name: Test - run: mv tests/examples/test_hello_world.py test_hello_world.py && pdm run pytest test_hello_world.py + run: | + mv tests/examples/test_hello_world.py test_hello_world.py + uv run pytest test_hello_world.py test_pydantic_1_app: name: Test Minimal Pydantic 1 application @@ -292,26 +265,22 @@ jobs: with: python-version: "3.12" - - uses: pdm-project/setup-pdm@v4 - name: Set up PDM + - name: Install uv + uses: astral-sh/setup-uv@v5 with: - python-version: "3.12" - allow-python-prereleases: false - cache: true - cache-dependency-path: | - ./pdm.lock + version: "0.5.4" + enable-cache: true - name: Install dependencies run: | - pdm install - pdm run python -m ensurepip - pdm run python -m pip install "pydantic==1.*" + uv sync + uv pip install "pydantic==1.*" - name: Set pythonpath run: echo "PYTHONPATH=$PWD" >> $GITHUB_ENV - name: Test - run: pdm run coverage run --branch -m unittest test_apps/pydantic_1_app.py + run: uv run --no-sync coverage run --branch -m unittest test_apps/pydantic_1_app.py - name: Rename coverage file run: mv .coverage* .coverage.pydantic_v1 @@ -320,6 +289,7 @@ jobs: with: name: coverage-data path: .coverage.pydantic_v1 + include-hidden-files: true upload-test-coverage: runs-on: ubuntu-latest @@ -347,7 +317,7 @@ jobs: run: sed -i "s/home\/runner\/work\/litestar\/litestar/github\/workspace/g" coverage.xml - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: files: coverage.xml token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/docs-preview.yml b/.github/workflows/docs-preview.yml index 7ccb6c6fb2..2870371fb0 100644 --- a/.github/workflows/docs-preview.yml +++ b/.github/workflows/docs-preview.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v4 - name: Download artifact - uses: dawidd6/action-download-artifact@v6 + uses: dawidd6/action-download-artifact@v7 with: workflow_conclusion: success run_id: ${{ github.event.workflow_run.id }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c96b5e05f5..1785170f51 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,9 +6,11 @@ on: push: branches: - main - - develop - v3.0 +env: + UV_LOCKED: 1 + jobs: docs: permissions: @@ -21,35 +23,28 @@ jobs: with: python-version: "3.12" - - uses: pdm-project/setup-pdm@v4 - name: Set up PDM + - name: Install uv + uses: astral-sh/setup-uv@v5 with: - python-version: "3.12" - allow-python-prereleases: false - cache: true - cache-dependency-path: | - ./pdm.lock + version: "0.5.4" + enable-cache: true - name: Install dependencies - run: pdm sync -G:all + run: uv sync - name: Fetch gh pages run: git fetch origin gh-pages --depth=1 - name: Build release docs - run: pdm run python tools/build_docs.py docs-build + run: uv run python tools/build_docs.py docs-build if: github.event_name == 'release' - name: Build docs (main branch) - run: pdm run python tools/build_docs.py docs-build --version main + run: uv run python tools/build_docs.py docs-build --version main if: github.event_name == 'push' && github.ref == 'refs/heads/main' - - name: Build docs (develop branch) - run: pdm run python tools/build_docs.py docs-build --version develop - if: github.event_name == 'push' && github.ref == 'refs/heads/develop' - - name: Build docs (v3.0 branch) - run: pdm run python tools/build_docs.py docs-build --version 3-dev + run: uv run python tools/build_docs.py docs-build --version 3-dev if: github.event_name == 'push' && github.ref == 'refs/heads/v3.0' - name: Deploy diff --git a/.github/workflows/pr-merged.yml b/.github/workflows/pr-merged.yml index f83fea1649..c13039ea8d 100644 --- a/.github/workflows/pr-merged.yml +++ b/.github/workflows/pr-merged.yml @@ -5,8 +5,8 @@ on: types: - closed branches: - - develop - main + - v3.0 jobs: close_and_notify: @@ -19,8 +19,8 @@ jobs: script: | const prNumber = context.payload.number const branch = context.baseRef - const isDevelop = branch === "develop" - const commentBody = `\nThis issue has been closed in #${prNumber}. The change will be included in the upcoming ${isDevelop ? "minor" : "patch"} release.` + // TODO: use semantic commits to specify the exact version, when it will be released + const commentBody = `\nThis issue has been closed in #${prNumber}. The change will be included in upcoming releases.` const query = `query($number: Int!, $owner: String!, $name: String!) { repository(owner: $owner, name: $name) { pullRequest(number: $number) { diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e713debae3..3f8493b3da 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,14 +21,14 @@ jobs: with: python-version: "3.12" - - uses: pdm-project/setup-pdm@v4 - name: Set up PDM + - name: Install uv + uses: astral-sh/setup-uv@v5 with: - python-version: "3.12" - cache: true + version: "0.5.4" + enable-cache: true - name: Build package - run: pdm build + run: uv build - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d770f6a553..d0be5130d2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,6 +19,9 @@ on: type: number default: 10 +env: + UV_LOCKED: 1 + jobs: test: runs-on: ${{ inputs.os }} @@ -35,28 +38,25 @@ jobs: with: python-version: ${{ inputs.python-version }} - - uses: pdm-project/setup-pdm@v4 - name: Set up PDM + - name: Install uv + uses: astral-sh/setup-uv@v5 with: - python-version: ${{ inputs.python-version }} - allow-python-prereleases: false - cache: true - cache-dependency-path: | - ./pdm.lock + version: "0.5.4" + enable-cache: true - name: Install dependencies - run: pdm install -G:all + run: uv sync - name: Set PYTHONPATH run: echo "PYTHONPATH=$PWD" >> $GITHUB_ENV - name: Test if: ${{ !inputs.coverage }} - run: pdm run pytest docs/examples tests -n auto + run: uv run pytest docs/examples tests -n auto - name: Test with coverage if: inputs.coverage - run: pdm run pytest docs/examples tests -n auto --cov + run: uv run pytest docs/examples tests -n auto --cov - name: Rename coverage file if: inputs.coverage @@ -67,3 +67,4 @@ jobs: with: name: coverage-data path: .coverage.${{ inputs.python-version }} + include-hidden-files: true diff --git a/.gitignore b/.gitignore index 72e2ed9cfb..8e4c4ae0bc 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ __pypackages__/ # test certificates certs/ pdm.toml +.zed diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 92da9843cf..8dcfdb3442 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,13 @@ default_language_version: - python: "3.8" + python: "3" repos: - repo: https://github.com/compilerla/conventional-pre-commit - rev: v3.4.0 + rev: v3.6.0 hooks: - id: conventional-pre-commit stages: [commit-msg] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-ast - id: check-case-conflict @@ -23,8 +23,8 @@ repos: hooks: - id: unasyncd additional_dependencies: ["ruff"] - - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: "v0.6.2" + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.8.1" hooks: - id: ruff args: ["--fix"] @@ -43,7 +43,7 @@ repos: exclude: "test*|examples*|tools" args: ["--use-tuple"] - repo: https://github.com/sphinx-contrib/sphinx-lint - rev: "v0.9.1" + rev: "v1.0.0" hooks: - id: sphinx-lint - repo: local diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 751658f4d7..7d40c259a3 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -37,94 +37,25 @@ Setting up the environment If you are utilizing `GitHub Codespaces `_, the environment will bootstrap itself automatically. The steps below are for local development. -#. Install `PDM `_: - - .. tab-set:: - - .. tab-item:: Makefile - :sync: makefile - - .. code-block:: bash - :caption: Using our Make target to install PDM - - make install-pdm - - .. tab-item:: pipx - :sync: pipx - - .. code-block:: bash - :caption: Using |pipx| - - pipx install pdm - - .. tab-item:: Homebrew - :sync: homebrew - - .. code-block:: bash - :caption: Using |homebrew| - - brew install pdm +#. Install `uv `_: #. Run ``make install`` to create a `virtual environment `_ - and install the required development dependencies or run the PDM installation command manually: - - .. code-block:: shell - :caption: Installing the documentation dependencies - - pdm install - -#. If you're working on the documentation and need to build it locally, install the extra dependencies with - ``make docs-install`` or: + and install the required development dependencies or run the uv sync command manually: .. code-block:: shell :caption: Installing the documentation dependencies - pdm install -G:docs - -#. Install `pre-commit `_: - - .. tab-set:: + uv install - .. tab-item:: pip - :sync: pip - .. code-block:: bash - :caption: Using pip - - python3 -m pip install pre-commit - - .. tab-item:: pipx - :sync: pipx - - .. code-block:: bash - :caption: Using |pipx| - - pipx install pre-commit - - .. tab-item:: Homebrew - :sync: homebrew - - .. code-block:: bash - :caption: Using |homebrew| - - brew install pre-commit - -#. Install our pre-commit hooks. by running ``make install`` or: - - .. code-block:: shell - :caption: Installing pre-commit hooks - - pre-commit install --install-hooks - -.. tip:: Many modern IDEs like PyCharm or VS Code will enable the PDM-managed virtualenv that is created in step 2 +.. tip:: Many modern IDEs like PyCharm or VS Code will enable the uv-managed virtualenv that is created in step 2 for you automatically. If your IDE / editor does not offer this functionality, then you will need to manually activate the virtualenv yourself. Otherwise you may encounter errors or unexpected behaviour when trying to run the commands referenced within this document. - To activate the virtualenv manually, please consult PDM's documentation on - `working with virtual environments `_. - A simpler alternative is using the PDM plugin `pdm-shell `_. + To activate the virtualenv manually, please consult uv's documentation on + `working with virtual environments `_. The rest of this document will assume this environment is active wherever commands are referenced. @@ -230,19 +161,20 @@ as a first step. Running the docs locally ++++++++++++++++++++++++ -To run or build the docs locally, you need to first install the required dependencies: +You can serve the documentation locally with .. code-block:: shell - :caption: Installing the documentation dependencies + :caption: Serving the documentation locally - pdm install -G:docs + make docs-serve -Then you can serve the documentation with our helpful Makefile targets: +or build it with .. code-block:: shell :caption: Serving the documentation locally - make docs-serve + make docs + Writing and editing docs ++++++++++++++++++++++++ @@ -276,6 +208,7 @@ Please follow the next guidelines when adding a new example: :caption: An example of how to use literal includes of external files .. literalinclude:: /examples/test_thing.py + :language: python :caption: All includes should have a descriptive caption Automatically execute examples diff --git a/Makefile b/Makefile index b43b1ae0c1..5042ead299 100644 --- a/Makefile +++ b/Makefile @@ -5,11 +5,8 @@ SHELL := /bin/bash .DEFAULT_GOAL:=help .ONESHELL: -USING_PDM = $(shell grep "tool.pdm" pyproject.toml && echo "yes") ENV_PREFIX = .venv/bin/ VENV_EXISTS = $(shell python3 -c "if __import__('pathlib').Path('.venv/bin/activate').exists(): print('yes')") -PDM_OPTS ?= -PDM ?= pdm $(PDM_OPTS) .EXPORT_ALL_VARIABLES: @@ -21,37 +18,25 @@ help: ## Display this help text for Makefile .PHONY: upgrade upgrade: ## Upgrade all dependencies to the latest stable versions @echo "=> Updating all dependencies" - @if [ "$(USING_PDM)" ]; then $(PDM) update; fi + @uv lock --upgrade @echo "=> Dependencies Updated" - @$(PDM) run pre-commit autoupdate + @uv run pre-commit autoupdate @echo "=> Updated Pre-commit" # ============================================================================= # Developer Utils # ============================================================================= -.PHONY: install-pdm -install-pdm: ## Install latest version of PDM - @curl -sSLO https://pdm.fming.dev/install-pdm.py && \ - curl -sSL https://pdm.fming.dev/install-pdm.py.sha256 | shasum -a 256 -c - && \ - python3 install-pdm.py && \ - rm install-pdm.py .PHONY: install -install: clean ## Install the project, dependencies, and pre-commit for local development - @if ! $(PDM) --version > /dev/null; then echo '=> Installing PDM'; $(MAKE) install-pdm; fi - @if [ "$(VENV_EXISTS)" ]; then echo "=> Removing existing virtual environment"; fi - if [ "$(VENV_EXISTS)" ]; then $(MAKE) destroy; fi - if [ "$(VENV_EXISTS)" ]; then $(MAKE) clean; fi - @if [ "$(USING_PDM)" ]; then $(PDM) config --local venv.in_project true && python3 -m venv --copies .venv && . $(ENV_PREFIX)/activate && $(ENV_PREFIX)/pip install --quiet -U wheel setuptools cython mypy pip; fi - @if [ "$(USING_PDM)" ]; then $(PDM) install -dG:all; fi - @echo "=> Install complete! Note: If you want to re-install re-run 'make install'" +install: + @uv sync .PHONY: clean clean: ## Cleanup temporary build artifacts @echo "=> Cleaning working directory" @rm -rf .pytest_cache .ruff_cache .hypothesis build/ -rf dist/ .eggs/ @find . -name '*.egg-info' -exec rm -rf {} + - @find . -name '*.egg' -exec rm -f {} + + @find . -type f -name '*.egg' -exec rm -f {} + @find . -name '*.pyc' -exec rm -f {} + @find . -name '*.pyo' -exec rm -f {} + @find . -name '*~' -exec rm -f {} + @@ -64,13 +49,10 @@ clean: ## Cleanup temporary build artifacts destroy: ## Destroy the virtual environment @rm -rf .venv -.PHONY: refresh-lockfiles -refresh-lockfiles: ## Sync lockfiles with requirements files. - pdm update --update-reuse --group :all .PHONY: lock lock: ## Rebuild lockfiles from scratch, updating all dependencies - pdm update --update-eager --group :all + @uv lock # ============================================================================= # Tests, Linting, Coverage @@ -78,19 +60,19 @@ lock: ## Rebuild lockfiles from scra .PHONY: mypy mypy: ## Run mypy @echo "=> Running mypy" - @$(PDM) run dmypy run + @uv run dmypy run @echo "=> mypy complete" .PHONY: mypy-nocache mypy-nocache: ## Run Mypy without cache @echo "=> Running mypy without a cache" - @$(PDM) run dmypy run -- --cache-dir=/dev/null + @uv run dmypy run -- --cache-dir=/dev/null @echo "=> mypy complete" .PHONY: pyright pyright: ## Run pyright @echo "=> Running pyright" - @$(PDM) run pyright + @uv run pyright @echo "=> pyright complete" .PHONY: type-check @@ -99,13 +81,13 @@ type-check: mypy pyright ## Run all type checking .PHONY: pre-commit pre-commit: ## Runs pre-commit hooks; includes ruff formatting and linting, codespell @echo "=> Running pre-commit process" - @$(PDM) run pre-commit run --all-files + @uv run pre-commit run --all-files @echo "=> Pre-commit complete" .PHONY: slots-check slots-check: ## Check for slots usage in classes @echo "=> Checking for slots usage in classes" - @$(PDM) run slotscheck litestar + @uv run slotscheck litestar @echo "=> Slots check complete" .PHONY: lint @@ -114,20 +96,20 @@ lint: pre-commit type-check slots-check ## Run all linting .PHONY: coverage coverage: ## Run the tests and generate coverage report @echo "=> Running tests with coverage" - @$(PDM) run pytest tests --cov -n auto - @$(PDM) run coverage html - @$(PDM) run coverage xml + @uv run pytest tests --cov -n auto + @uv run coverage html + @uv run coverage xml @echo "=> Coverage report generated" .PHONY: test test: ## Run the tests @echo "=> Running test cases" - @$(PDM) run pytest tests + @uv run pytest tests @echo "=> Tests complete" .PHONY: test-examples test-examples: ## Run the examples tests - @$(PDM) run pytest docs/examples + @uv run pytest docs/examples .PHONY: test-all test-all: test test-examples ## Run all tests @@ -142,7 +124,7 @@ check-all: lint test-all coverage ## Run all linting, tests, a .PHONY: docs-install docs-install: ## Install docs dependencies @echo "=> Installing documentation dependencies" - @$(PDM) install --group docs + @uv install --group docs @echo "=> Installed documentation dependencies" docs-clean: ## Dump the existing built docs @@ -152,16 +134,16 @@ docs-clean: ## Dump the existing built docs docs-serve: docs-clean ## Serve the docs locally @echo "=> Serving documentation" - $(PDM) run sphinx-autobuild docs docs/_build/ -j auto --watch litestar --watch docs --watch tests --watch CONTRIBUTING.rst --port 8002 + uv run sphinx-autobuild docs docs/_build/ -j auto --watch litestar --watch docs --watch tests --watch CONTRIBUTING.rst --port 8002 docs: docs-clean ## Dump the existing built docs and rebuild them @echo "=> Building documentation" - @$(PDM) run sphinx-build -M html docs docs/_build/ -E -a -j auto -W --keep-going + @uv run sphinx-build -M html docs docs/_build/ -E -a -j auto -W --keep-going .PHONY: docs-linkcheck docs-linkcheck: ## Run the link check on the docs - @$(PDM) run sphinx-build -b linkcheck ./docs ./docs/_build -D linkcheck_ignore='http://.*','https://.*' + @uv run sphinx-build -b linkcheck ./docs ./docs/_build -D linkcheck_ignore='http://.*','https://.*' .PHONY: docs-linkcheck-full docs-linkcheck-full: ## Run the full link check on the docs - @$(PDM) run sphinx-build -b linkcheck ./docs ./docs/_build -D linkcheck_anchors=0 + @uv run sphinx-build -b linkcheck ./docs ./docs/_build -D linkcheck_anchors=0 diff --git a/README.md b/README.md index 906054d173..3edaa6a46f 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ app = Litestar(route_handlers=[hello_world]) - Support for `dataclasses`, `TypedDict`, [pydantic version 1 and version 2](https://docs.pydantic.dev/latest/), [msgspec](https://github.com/jcrist/msgspec) and [attrs](https://www.attrs.org/en/stable/) - Layered parameter declaration +- Support for [RFC 9457](https://datatracker.ietf.org/doc/html/rfc9457) standardized "Problem Detail" error responses - [Automatic API documentation with](#redoc-swagger-ui-and-stoplight-elements-api-documentation): - [Scalar](https://github.com/scalar/scalar/) - [RapiDoc](https://github.com/rapi-doc/RapiDoc) @@ -105,13 +106,11 @@ app = Litestar(route_handlers=[hello_world])
Pre-built Example Apps -- [litestar-pg-redis-docker](https://github.com/litestar-org/litestar-pg-redis-docker): In addition to Litestar, this - demonstrates a pattern of application modularity, SQLAlchemy 2.0 ORM, Redis cache connectivity, and more. Like all - Litestar projects, this application is open to contributions, big and small. -- [litestar-fullstack](https://github.com/litestar-org/litestar-fullstack): A reference application that contains most of the boilerplate required for a web application. - It features a Litestar app configured with best practices, SQLAlchemy 2.0 and SAQ, a frontend integrated with Vitejs and Jinja2 templates, Docker, and more. - [litestar-hello-world](https://github.com/litestar-org/litestar-hello-world): A bare-minimum application setup. Great for testing and POC work. +- [litestar-fullstack](https://github.com/litestar-org/litestar-fullstack): A reference application that contains most of the boilerplate required for a web application. + It features a Litestar app configured with best practices, SQLAlchemy 2.0 and SAQ, a frontend integrated with Vitejs and Jinja2 templates, Docker, and more. Like all + Litestar projects, this application is open to contributions, big and small.
## Sponsors @@ -125,7 +124,6 @@ A **huge** thanks to our sponsors: Scalar.com Telemetry Sports -Stok Check out our sponsors in the docs @@ -468,7 +466,7 @@ see [the contribution guide](CONTRIBUTING.rst). Ryan Seeley
Ryan Seeley

💻 Felix
Felix

📖 🐛 George Sakkis
George Sakkis

💻 - Huba Tuba
Huba Tuba

📖 + Huba Tuba
Huba Tuba

📖 💻 ⚠️ Stefane Fermigier
Stefane Fermigier

📖 @@ -544,7 +542,7 @@ see [the contribution guide](CONTRIBUTING.rst). Leo Alekseyev
Leo Alekseyev

💻 - aranvir
aranvir

📖 + aranvir
aranvir

📖 💻 ⚠️ bunny-therapist
bunny-therapist

💻 Ben Luo
Ben Luo

📖 Hugo van Kemenade
Hugo van Kemenade

📖 @@ -574,6 +572,18 @@ see [the contribution guide](CONTRIBUTING.rst). Anuranjan Srivastava
Anuranjan Srivastava

💻 Simon Joseph
Simon Joseph

📖 Abel Kidanemariam
Abel Kidanemariam

💻 ⚠️ 📖 + Trim21
Trim21

💻 ⚠️ + Agustin Arce
Agustin Arce

📖 + Farhan Ali Raza
Farhan Ali Raza

📖 + Fabian
Fabian

💻 + + + Mohammed Babelly
Mohammed Babelly

💻 + Charles Duffy
Charles Duffy

💻 + Evgeny Demchenko
Evgeny Demchenko

📖 + Olzhas Arystanov
Olzhas Arystanov

🐛 📖 + Vikash
Vikash

💻 + Jordan Russell
Jordan Russell

📖 ⚠️ 💻 diff --git a/docs/PYPI_README.md b/docs/PYPI_README.md index fcfb5cd2b7..9dd9f3f4fe 100644 --- a/docs/PYPI_README.md +++ b/docs/PYPI_README.md @@ -86,6 +86,7 @@ app = Litestar(route_handlers=[hello_world]) - Support for `dataclasses`, `TypedDict`, [pydantic version 1 and version 2](https://docs.pydantic.dev/latest/), [msgspec](https://github.com/jcrist/msgspec) and [attrs](https://www.attrs.org/en/stable/) - Layered parameter declaration +- Support for [RFC 9457](https://datatracker.ietf.org/doc/html/rfc9457) standardized "Problem Detail" error responses - [Automatic API documentation with](#redoc-swagger-ui-and-stoplight-elements-api-documentation): - [Scalar](https://github.com/scalar/scalar/) - [RapiDoc](https://github.com/rapi-doc/RapiDoc) @@ -102,13 +103,11 @@ app = Litestar(route_handlers=[hello_world])
Pre-built Example Apps -- [litestar-pg-redis-docker](https://github.com/litestar-org/litestar-pg-redis-docker): In addition to Litestar, this - demonstrates a pattern of application modularity, SQLAlchemy 2.0 ORM, Redis cache connectivity, and more. Like all - Litestar projects, this application is open to contributions, big and small. -- [litestar-fullstack](https://github.com/litestar-org/litestar-fullstack): A reference application that contains most of the boilerplate required for a web application. - It features a Litestar app configured with best practices, SQLAlchemy 2.0 and SAQ, a frontend integrated with Vitejs and Jinja2 templates, Docker, and more. - [litestar-hello-world](https://github.com/litestar-org/litestar-hello-world): A bare-minimum application setup. Great for testing and POC work. +- [litestar-fullstack](https://github.com/litestar-org/litestar-fullstack): A reference application that contains most of the boilerplate required for a web application. + It features a Litestar app configured with best practices, SQLAlchemy 2.0 and SAQ, a frontend integrated with Vitejs and Jinja2 templates, Docker, and more. Like all + Litestar projects, this application is open to contributions, big and small.
## Sponsors @@ -122,7 +121,6 @@ A **huge** thanks to our sponsors: Scalar.com Telemetry Sports -Stok Check out our sponsors in the docs diff --git a/docs/_static/versions.json b/docs/_static/versions.json index b98d2d9024..3e4a37a00d 100644 --- a/docs/_static/versions.json +++ b/docs/_static/versions.json @@ -1 +1 @@ -{ "versions": ["1", "2", "main", "develop", "3-dev"], "latest": "2" } +{ "versions": ["1", "2", "main", "3-dev"], "latest": "2" } diff --git a/docs/admonitions/sync-to-thread-info.rst b/docs/admonitions/sync-to-thread-info.rst index 6b13a0b063..fe9329aa5a 100644 --- a/docs/admonitions/sync-to-thread-info.rst +++ b/docs/admonitions/sync-to-thread-info.rst @@ -7,8 +7,11 @@ running the event loop, and in turn block the whole application. To mitigate this, the ``sync_to_thread`` parameter can be set to ``True``, which - will result in the function being run in a thread pool. Should the function be - non-blocking, ``sync_to_thread`` should be set to ``False`` instead. + will result in the function being run in a thread pool. + + If a synchronous function is non-blocking, setting ``sync_to_thread`` to ``False`` + will tell Litestar that the user is sure about its behavior + and the function can be treated as non-blocking. If a synchronous function is passed, without setting an explicit ``sync_to_thread`` value, a warning will be raised. diff --git a/docs/conf.py b/docs/conf.py index bcdee8ef27..cdfa343929 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,15 +23,15 @@ PY_FUNC = "py:func" project = "Litestar" -copyright = "2023, Litestar-Org" +copyright = "2024, Litestar-Org" author = "Litestar-Org" release = os.getenv("_LITESTAR_DOCS_BUILD_VERSION", importlib.metadata.version("litestar").rsplit(".")[0]) extensions = [ "sphinx.ext.intersphinx", - "sphinx.ext.autosectionlabel", "sphinx.ext.autodoc", "sphinx.ext.napoleon", + "sphinx.ext.autosectionlabel", "sphinx_design", "auto_pytabs.sphinx_ext", "tools.sphinx_ext", @@ -48,6 +48,7 @@ "msgspec": ("https://jcristharif.com/msgspec/", None), "anyio": ("https://anyio.readthedocs.io/en/stable/", None), "multidict": ("https://multidict.aio-libs.org/en/stable/", None), + "cryptography": ("https://cryptography.io/en/latest/", None), "sqlalchemy": ("https://docs.sqlalchemy.org/en/20/", None), "alembic": ("https://alembic.sqlalchemy.org/en/latest/", None), "click": ("https://click.palletsprojects.com/en/8.1.x/", None), @@ -60,6 +61,9 @@ "advanced-alchemy": ("https://docs.advanced-alchemy.litestar.dev/latest/", None), "jinja2": ("https://jinja.palletsprojects.com/en/latest/", None), "trio": ("https://trio.readthedocs.io/en/stable/", None), + "pydantic": ("https://docs.pydantic.dev/latest/", None), + "typing_extensions": ("https://typing-extensions.readthedocs.io/en/stable/", None), + "valkey": ("https://valkey-py.readthedocs.io/en/latest/", None), } napoleon_google_docstring = True @@ -74,6 +78,7 @@ autodoc_default_options = {"special-members": "__init__", "show-inheritance": True, "members": True} autodoc_member_order = "bysource" autodoc_typehints_format = "short" +autodoc_mock_imports = [] nitpicky = True nitpick_ignore = [ @@ -98,6 +103,7 @@ (PY_CLASS, "sqlalchemy.dialects.postgresql.named_types.ENUM"), (PY_CLASS, "sqlalchemy.orm.decl_api.DeclarativeMeta"), (PY_CLASS, "sqlalchemy.sql.sqltypes.TupleType"), + (PY_CLASS, "valkey.asyncio.Valkey"), (PY_METH, "_types.TypeDecorator.process_bind_param"), (PY_METH, "_types.TypeDecorator.process_result_value"), (PY_METH, "litestar.typing.ParsedType.is_subclass_of"), @@ -153,31 +159,18 @@ (PY_CLASS, "litestar.response.RedirectResponse"), (PY_CLASS, "litestar.response_containers.Redirect"), (PY_CLASS, "litestar.response_containers.Template"), + (PY_CLASS, "litestar.contrib.sqlalchemy.plugins.SQLAlchemyPlugin"), + (PY_CLASS, "litestar.contrib.sqlalchemy.plugins.SQLAlchemySerializationPlugin"), + (PY_CLASS, "litestar.contrib.sqlalchemy.plugins.SQLAlchemyInitPlugin"), + (PY_CLASS, "litestar.contrib.sqlalchemy.dto.SQLAlchemyDTO"), (PY_CLASS, "litestar.contrib.sqlalchemy.types.BigIntIdentity"), (PY_CLASS, "litestar.contrib.sqlalchemy.types.JsonB"), + (PY_CLASS, "litestar.contrib.htmx.request.HTMXRequest"), (PY_CLASS, "litestar.typing.ParsedType"), (PY_METH, "litestar.dto.factory.DTOData.create_instance"), (PY_METH, "litestar.dto.interface.DTOInterface.data_to_encodable_type"), (PY_CLASS, "MetaData"), - (PY_CLASS, "advanced_alchemy.repository.typing.ModelT"), - (PY_OBJ, "advanced_alchemy.config.common.SessionMakerT"), - (PY_OBJ, "advanced_alchemy.config.common.ConnectionT"), - (PY_CLASS, "advanced_alchemy.extensions.litestar.plugins._slots_base.SlotsBase"), - (PY_CLASS, "advanced_alchemy.config.EngineConfig"), - (PY_CLASS, "advanced_alchemy.config.common.GenericAlembicConfig"), - (PY_CLASS, "advanced_alchemy.extensions.litestar.plugins.SQLAlchemyPlugin"), - (PY_CLASS, "advanced_alchemy.extensions.litestar.plugins.SQLAlchemySerializationPlugin"), - (PY_CLASS, "advanced_alchemy.extensions.litestar.plugins.SQLAlchemyInitPlugin"), - (PY_CLASS, "advanced_alchemy.extensions.litestar.config.SQLAlchemySyncConfig"), - (PY_CLASS, "advanced_alchemy.extensions.litestar.config.SQLAlchemyAsyncConfig"), - (PY_METH, "advanced_alchemy.extensions.litestar.plugins.SQLAlchemySerializationPlugin.create_dto_for_type"), - (PY_CLASS, "advanced_alchemy.base.BasicAttributes"), - (PY_CLASS, "advanced_alchemy.config.AsyncSessionConfig"), - (PY_CLASS, "advanced_alchemy.config.SyncSessionConfig"), - (PY_CLASS, "advanced_alchemy.types.JsonB"), - (PY_CLASS, "advanced_alchemy.types.BigIntIdentity"), (PY_FUNC, "sqlalchemy.get_engine"), - (PY_ATTR, "advanced_alchemy.repository.AbstractAsyncRepository.id_attribute"), (PY_OBJ, "litestar.template.base.T_co"), ("py:exc", "RepositoryError"), ("py:exc", "InternalServerError"), @@ -185,6 +178,21 @@ (PY_CLASS, "litestar.template.Template"), (PY_CLASS, "litestar.middleware.compression.gzip_facade.GzipCompression"), (PY_CLASS, "litestar.handlers.http_handlers.decorators._subclass_warning"), + (PY_CLASS, "litestar.background_tasks.P"), + (PY_CLASS, "P.args"), + (PY_CLASS, "P.kwargs"), + (PY_CLASS, "litestar.contrib.jinja.P"), + (PY_CLASS, "litestar.contrib.mako.P"), + (PY_CLASS, "JWTDecodeOptions"), + (PY_CLASS, "litestar.template.base.P"), + (PY_CLASS, "litestar.contrib.pydantic.PydanticDTO"), + (PY_CLASS, "litestar.contrib.pydantic.PydanticPlugin"), + (PY_CLASS, "typing.Self"), + (PY_CLASS, "attr.AttrsInstance"), + (PY_CLASS, "typing_extensions.TypeGuard"), + (PY_CLASS, "advanced_alchemy.types.BigIntIdentity"), + (PY_CLASS, "advanced_alchemy.types.JsonB"), + (PY_CLASS, "advanced_alchemy.repository.SQLAlchemyAsyncRepository"), ] nitpick_ignore_regex = [ @@ -214,6 +222,8 @@ (PY_RE, r"advanced_alchemy\.config.common\.SessionT"), (PY_RE, r".*R"), (PY_OBJ, r"litestar.security.jwt.auth.TokenT"), + (PY_CLASS, "ExceptionToProblemDetailMapType"), + (PY_CLASS, "litestar.security.jwt.token.JWTDecodeOptions"), ] # Warnings about missing references to those targets in the specified location will be ignored. @@ -226,7 +236,6 @@ "litestar.template": {"litestar.template.base.T_co"}, "litestar.openapi.OpenAPIController.security": {"SecurityRequirement"}, "litestar.response.file.async_file_iterator": {"FileSystemAdapter"}, - "advanced_alchemy._listeners.touch_updated_timestamp": {"Session"}, re.compile("litestar.response.redirect.*"): {"RedirectStatusType"}, re.compile(r"litestar\.plugins.*"): re.compile(".*ModelT"), re.compile(r"litestar\.(contrib|repository)\.*"): re.compile(".*T"), @@ -238,6 +247,7 @@ "litestar.concurrency.set_asyncio_executor": {"ThreadPoolExecutor"}, "litestar.concurrency.get_asyncio_executor": {"ThreadPoolExecutor"}, re.compile(r"litestar\.channels\.backends\.asyncpg.*"): {"asyncpg.connection.Connection", "asyncpg.Connection"}, + re.compile(r"litestar\.handlers\.websocket_handlers\.stream.*"): {"WebSocketMode"}, } # Do not warn about broken links to the following: @@ -347,11 +357,11 @@ def delayed_setup(app: Sphinx) -> None: return app.setup_extension("pydata_sphinx_theme") - app.connect("html-page-context", update_html_context) + app.connect("html-page-context", update_html_context) # type: ignore def setup(app: Sphinx) -> dict[str, bool]: - app.connect("builder-inited", delayed_setup, priority=0) + app.connect("builder-inited", delayed_setup, priority=0) # type: ignore app.setup_extension("litestar_sphinx_theme") diff --git a/docs/examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_plugin.py b/docs/examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_plugin.py index fd55548c1f..aec569996d 100644 --- a/docs/examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_plugin.py +++ b/docs/examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_plugin.py @@ -1,6 +1,5 @@ from typing import AsyncGenerator, List, Optional -from advanced_alchemy.extensions.litestar.plugins.init.config.asyncio import autocommit_before_send_handler from sqlalchemy import select from sqlalchemy.exc import IntegrityError, NoResultFound from sqlalchemy.ext.asyncio import AsyncSession @@ -74,7 +73,7 @@ async def update_item(item_title: str, data: TodoItem, transaction: AsyncSession connection_string="sqlite+aiosqlite:///todo.sqlite", metadata=Base.metadata, create_all=True, - before_send_handler=autocommit_before_send_handler, + before_send_handler="autocommit", ) app = Litestar( diff --git a/docs/examples/contrib/sqlalchemy/sqlalchemy_async_repository.py b/docs/examples/contrib/sqlalchemy/sqlalchemy_async_repository.py index 69cc19242d..615b5aa8e6 100644 --- a/docs/examples/contrib/sqlalchemy/sqlalchemy_async_repository.py +++ b/docs/examples/contrib/sqlalchemy/sqlalchemy_async_repository.py @@ -10,15 +10,19 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship, selectinload from litestar import Litestar, get -from litestar.contrib.sqlalchemy.base import UUIDAuditBase, UUIDBase -from litestar.contrib.sqlalchemy.plugins import AsyncSessionConfig, SQLAlchemyAsyncConfig, SQLAlchemyInitPlugin -from litestar.contrib.sqlalchemy.repository import SQLAlchemyAsyncRepository from litestar.controller import Controller from litestar.di import Provide from litestar.handlers.http_handlers.decorators import delete, patch, post from litestar.pagination import OffsetPagination from litestar.params import Parameter -from litestar.repository.filters import LimitOffset +from litestar.plugins.sqlalchemy import ( + AsyncSessionConfig, + SQLAlchemyAsyncConfig, + SQLAlchemyInitPlugin, + base, + filters, + repository, +) if TYPE_CHECKING: from sqlalchemy.ext.asyncio import AsyncSession @@ -30,9 +34,9 @@ class BaseModel(_BaseModel): model_config = {"from_attributes": True} -# the SQLAlchemy base includes a declarative model for you to use in your models. -# The `Base` class includes a `UUID` based primary key (`id`) -class AuthorModel(UUIDBase): +# The SQLAlchemy base includes a declarative model for you to use in your models. +# The `UUIDBase` class includes a `UUID` based primary key (`id`) +class AuthorModel(base.UUIDBase): # we can optionally provide the table name instead of auto-generating it __tablename__ = "author" # type: ignore[assignment] name: Mapped[str] @@ -40,10 +44,10 @@ class AuthorModel(UUIDBase): books: Mapped[list[BookModel]] = relationship(back_populates="author", lazy="noload") -# The `AuditBase` class includes the same UUID` based primary key (`id`) and 2 -# additional columns: `created` and `updated`. `created` is a timestamp of when the -# record created, and `updated` is the last time the record was modified. -class BookModel(UUIDAuditBase): +# The `UUIDAuditBase` class includes the same UUID` based primary key (`id`) and 2 +# additional columns: `created_at` and `updated_at`. `created_at` is a timestamp of when the +# record created, and `updated_at` is the last time the record was modified. +class BookModel(base.UUIDAuditBase): __tablename__ = "book" # type: ignore[assignment] title: Mapped[str] author_id: Mapped[UUID] = mapped_column(ForeignKey("author.id")) @@ -69,7 +73,7 @@ class AuthorUpdate(BaseModel): dob: date | None = None -class AuthorRepository(SQLAlchemyAsyncRepository[AuthorModel]): +class AuthorRepository(repository.SQLAlchemyAsyncRepository[AuthorModel]): """Author repository.""" model_type = AuthorModel @@ -98,7 +102,7 @@ def provide_limit_offset_pagination( default=10, required=False, ), -) -> LimitOffset: +) -> filters.LimitOffset: """Add offset/limit pagination. Return type consumed by `Repository.apply_limit_offset_pagination()`. @@ -110,7 +114,7 @@ def provide_limit_offset_pagination( page_size : int OFFSET to apply to select. """ - return LimitOffset(page_size, page_size * (current_page - 1)) + return filters.LimitOffset(page_size, page_size * (current_page - 1)) class AuthorController(Controller): @@ -122,7 +126,7 @@ class AuthorController(Controller): async def list_authors( self, authors_repo: AuthorRepository, - limit_offset: LimitOffset, + limit_offset: filters.LimitOffset, ) -> OffsetPagination[Author]: """List authors.""" results, total = await authors_repo.list_and_count(limit_offset) @@ -205,7 +209,7 @@ async def delete_author( async def on_startup() -> None: """Initializes the database.""" async with sqlalchemy_config.get_engine().begin() as conn: - await conn.run_sync(UUIDBase.metadata.create_all) + await conn.run_sync(base.UUIDBase.metadata.create_all) app = Litestar( diff --git a/docs/examples/contrib/sqlalchemy/sqlalchemy_declarative_models.py b/docs/examples/contrib/sqlalchemy/sqlalchemy_declarative_models.py index cae8ff5ab1..251f98b63f 100644 --- a/docs/examples/contrib/sqlalchemy/sqlalchemy_declarative_models.py +++ b/docs/examples/contrib/sqlalchemy/sqlalchemy_declarative_models.py @@ -10,22 +10,23 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship from litestar import Litestar, get -from litestar.contrib.sqlalchemy.base import UUIDAuditBase, UUIDBase -from litestar.contrib.sqlalchemy.plugins import AsyncSessionConfig, SQLAlchemyAsyncConfig, SQLAlchemyPlugin +from litestar.plugins.sqlalchemy import AsyncSessionConfig, SQLAlchemyAsyncConfig, SQLAlchemyPlugin, base -# the SQLAlchemy base includes a declarative model for you to use in your models. -# The `Base` class includes a `UUID` based primary key (`id`) -class Author(UUIDBase): +# The SQLAlchemy base includes a declarative model for you to use in your models. +# The `UUIDBase` class includes a `UUID` based primary key (`id`) +class Author(base.UUIDBase): + __tablename__ = "author" name: Mapped[str] dob: Mapped[date] books: Mapped[List[Book]] = relationship(back_populates="author", lazy="selectin") -# The `AuditBase` class includes the same UUID` based primary key (`id`) and 2 +# The `UUIDAuditBase` class includes the same UUID` based primary key (`id`) and 2 # additional columns: `created_at` and `updated_at`. `created_at` is a timestamp of when the # record created, and `updated_at` is the last time the record was modified. -class Book(UUIDAuditBase): +class Book(base.UUIDAuditBase): + __tablename__ = "book" title: Mapped[str] author_id: Mapped[UUID] = mapped_column(ForeignKey("author.id")) author: Mapped[Author] = relationship(lazy="joined", innerjoin=True, viewonly=True) @@ -37,7 +38,7 @@ class Book(UUIDAuditBase): ) # Create 'async_session' dependency. -async def on_startup() -> None: +async def on_startup(app: Litestar) -> None: """Adds some dummy data if no data is present.""" async with sqlalchemy_config.get_session() as session: statement = select(func.count()).select_from(Author) diff --git a/docs/examples/contrib/sqlalchemy/sqlalchemy_repository_extension.py b/docs/examples/contrib/sqlalchemy/sqlalchemy_repository_extension.py index f864bbcdd7..258fae4377 100644 --- a/docs/examples/contrib/sqlalchemy/sqlalchemy_repository_extension.py +++ b/docs/examples/contrib/sqlalchemy/sqlalchemy_repository_extension.py @@ -96,9 +96,9 @@ async def _is_slug_unique( return await self.get_one_or_none(slug=slug) is None -# The `AuditBase` class includes the same UUID` based primary key (`id`) and 2 -# additional columns: `created` and `updated`. `created` is a timestamp of when the -# record created, and `updated` is the last time the record was modified. +# The `UUIDAuditBase` class includes the same UUID` based primary key (`id`) and 2 +# additional columns: `created_at` and `updated_at`. `created_at` is a timestamp of when the +# record created, and `updated_at` is the last time the record was modified. class BlogPost(UUIDAuditBase, SlugKey): title: Mapped[str] content: Mapped[str] diff --git a/docs/examples/contrib/sqlalchemy/sqlalchemy_sync_repository.py b/docs/examples/contrib/sqlalchemy/sqlalchemy_sync_repository.py index 1e8cb107c9..fc22e451eb 100644 --- a/docs/examples/contrib/sqlalchemy/sqlalchemy_sync_repository.py +++ b/docs/examples/contrib/sqlalchemy/sqlalchemy_sync_repository.py @@ -10,8 +10,8 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship, selectinload from litestar import Litestar, get +from litestar.contrib.sqlalchemy import SQLAlchemyInitPlugin, SQLAlchemySyncConfig from litestar.contrib.sqlalchemy.base import UUIDAuditBase, UUIDBase -from litestar.contrib.sqlalchemy.plugins.init import SQLAlchemyInitPlugin, SQLAlchemySyncConfig from litestar.contrib.sqlalchemy.repository import SQLAlchemySyncRepository from litestar.controller import Controller from litestar.di import Provide @@ -30,8 +30,8 @@ class BaseModel(_BaseModel): model_config = {"from_attributes": True} -# the SQLAlchemy base includes a declarative model for you to use in your models. -# The `Base` class includes a `UUID` based primary key (`id`) +# The SQLAlchemy base includes a declarative model for you to use in your models. +# The `UUIDBase` class includes a `UUID` based primary key (`id`) class AuthorModel(UUIDBase): # we can optionally provide the table name instead of auto-generating it __tablename__ = "author" # type: ignore[assignment] @@ -40,9 +40,9 @@ class AuthorModel(UUIDBase): books: Mapped[list[BookModel]] = relationship(back_populates="author", lazy="noload") -# The `AuditBase` class includes the same UUID` based primary key (`id`) and 2 -# additional columns: `created` and `updated`. `created` is a timestamp of when the -# record created, and `updated` is the last time the record was modified. +# The `UUIDAuditBase` class includes the same UUID` based primary key (`id`) and 2 +# additional columns: `created_at` and `updated_at`. `created_at` is a timestamp of when the +# record created, and `updated_at` is the last time the record was modified. class BookModel(UUIDAuditBase): __tablename__ = "book" # type: ignore[assignment] title: Mapped[str] diff --git a/docs/examples/data_transfer_objects/factory/enveloping_return_data.py b/docs/examples/data_transfer_objects/factory/enveloping_return_data.py index f25908028f..d0b8393672 100644 --- a/docs/examples/data_transfer_objects/factory/enveloping_return_data.py +++ b/docs/examples/data_transfer_objects/factory/enveloping_return_data.py @@ -5,8 +5,8 @@ from sqlalchemy.orm import Mapped from litestar import Litestar, get -from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO from litestar.dto import DTOConfig +from litestar.plugins.sqlalchemy import SQLAlchemyDTO from .my_lib import Base diff --git a/docs/examples/data_transfer_objects/factory/excluding_fields.py b/docs/examples/data_transfer_objects/factory/excluding_fields.py index 3199dc160b..3e77533bc3 100644 --- a/docs/examples/data_transfer_objects/factory/excluding_fields.py +++ b/docs/examples/data_transfer_objects/factory/excluding_fields.py @@ -7,8 +7,8 @@ from typing_extensions import Annotated from litestar import Litestar, post -from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO from litestar.dto import DTOConfig, dto_field +from litestar.plugins.sqlalchemy import SQLAlchemyDTO from .my_lib import Base diff --git a/docs/examples/data_transfer_objects/factory/included_fields.py b/docs/examples/data_transfer_objects/factory/included_fields.py index e9be67832b..6e583e5228 100644 --- a/docs/examples/data_transfer_objects/factory/included_fields.py +++ b/docs/examples/data_transfer_objects/factory/included_fields.py @@ -7,8 +7,8 @@ from typing_extensions import Annotated from litestar import Litestar, post -from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO from litestar.dto import DTOConfig, dto_field +from litestar.plugins.sqlalchemy import SQLAlchemyDTO from .my_lib import Base diff --git a/docs/examples/data_transfer_objects/factory/marking_fields.py b/docs/examples/data_transfer_objects/factory/marking_fields.py index 14366cee6f..0574273c3f 100644 --- a/docs/examples/data_transfer_objects/factory/marking_fields.py +++ b/docs/examples/data_transfer_objects/factory/marking_fields.py @@ -3,13 +3,15 @@ from sqlalchemy.orm import Mapped, mapped_column from litestar import Litestar, post -from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO from litestar.dto import dto_field +from litestar.plugins.sqlalchemy import SQLAlchemyDTO from .my_lib import Base class User(Base): + # `Base` defines `id` field as: + # id: Mapped[UUID] = mapped_column(default=uuid4, primary_key=True) name: Mapped[str] password: Mapped[str] = mapped_column(info=dto_field("private")) created_at: Mapped[datetime] = mapped_column(info=dto_field("read-only")) @@ -20,6 +22,9 @@ class User(Base): @post("/users", dto=UserDTO, sync_to_thread=False) def create_user(data: User) -> User: + # even though the client did not send the id field, + # since it is a primary key it is autogenerated + assert "id" in vars(data) # even though the client sent the password and created_at field, it is not in the data object assert "password" not in vars(data) assert "created_at" not in vars(data) diff --git a/docs/examples/data_transfer_objects/factory/paginated_return_data.py b/docs/examples/data_transfer_objects/factory/paginated_return_data.py index de76472692..4206343265 100644 --- a/docs/examples/data_transfer_objects/factory/paginated_return_data.py +++ b/docs/examples/data_transfer_objects/factory/paginated_return_data.py @@ -3,13 +3,12 @@ from sqlalchemy.orm import Mapped from litestar import Litestar, get -from litestar.contrib.sqlalchemy.base import UUIDBase -from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO from litestar.dto import DTOConfig from litestar.pagination import ClassicPagination +from litestar.plugins.sqlalchemy import SQLAlchemyDTO, base -class User(UUIDBase): +class User(base.UUIDBase): name: Mapped[str] password: Mapped[str] created_at: Mapped[datetime] diff --git a/docs/examples/data_transfer_objects/factory/patch_requests.py b/docs/examples/data_transfer_objects/factory/patch_requests.py index c8991c11ae..e153c489fe 100644 --- a/docs/examples/data_transfer_objects/factory/patch_requests.py +++ b/docs/examples/data_transfer_objects/factory/patch_requests.py @@ -20,17 +20,14 @@ class PatchDTO(DataclassDTO[Person]): config = DTOConfig(exclude={"id"}, partial=True) -database = { - UUID("f32ff2ce-e32f-4537-9dc0-26e7599f1380"): Person( - id=UUID("f32ff2ce-e32f-4537-9dc0-26e7599f1380"), name="Peter", age=40 - ) -} +peter_uuid = UUID("f32ff2ce-e32f-4537-9dc0-26e7599f1380") +database = {peter_uuid: Person(id=peter_uuid, name="Peter", age=40)} @patch("/person/{person_id:uuid}", dto=PatchDTO, return_dto=None, sync_to_thread=False) def update_person(person_id: UUID, data: DTOData[Person]) -> Person: - """Create a person.""" - return data.update_instance(database.get(person_id)) + """Partially update a person.""" + return data.update_instance(database[person_id]) app = Litestar(route_handlers=[update_person]) diff --git a/docs/examples/data_transfer_objects/factory/related_items.py b/docs/examples/data_transfer_objects/factory/related_items.py index 0831c196b2..9cccdaa5ab 100644 --- a/docs/examples/data_transfer_objects/factory/related_items.py +++ b/docs/examples/data_transfer_objects/factory/related_items.py @@ -7,8 +7,8 @@ from typing_extensions import Annotated from litestar import Litestar, put -from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO from litestar.dto import DTOConfig +from litestar.plugins.sqlalchemy import SQLAlchemyDTO from .my_lib import Base diff --git a/docs/examples/data_transfer_objects/factory/renaming_all_fields.py b/docs/examples/data_transfer_objects/factory/renaming_all_fields.py index eb2bca28e1..d001db36a8 100644 --- a/docs/examples/data_transfer_objects/factory/renaming_all_fields.py +++ b/docs/examples/data_transfer_objects/factory/renaming_all_fields.py @@ -4,8 +4,8 @@ from typing_extensions import Annotated from litestar import Litestar, post -from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO from litestar.dto import DTOConfig, dto_field +from litestar.plugins.sqlalchemy import SQLAlchemyDTO from .my_lib import Base diff --git a/docs/examples/data_transfer_objects/factory/renaming_fields.py b/docs/examples/data_transfer_objects/factory/renaming_fields.py index 632617fa1c..693e6e8576 100644 --- a/docs/examples/data_transfer_objects/factory/renaming_fields.py +++ b/docs/examples/data_transfer_objects/factory/renaming_fields.py @@ -4,8 +4,8 @@ from typing_extensions import Annotated from litestar import Litestar, post -from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO from litestar.dto import DTOConfig, dto_field +from litestar.plugins.sqlalchemy import SQLAlchemyDTO from .my_lib import Base diff --git a/docs/examples/data_transfer_objects/factory/response_return_data.py b/docs/examples/data_transfer_objects/factory/response_return_data.py index d83069ec75..15f2ce224d 100644 --- a/docs/examples/data_transfer_objects/factory/response_return_data.py +++ b/docs/examples/data_transfer_objects/factory/response_return_data.py @@ -3,8 +3,8 @@ from sqlalchemy.orm import Mapped from litestar import Litestar, Response, get -from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO from litestar.dto import DTOConfig +from litestar.plugins.sqlalchemy import SQLAlchemyDTO from .my_lib import Base diff --git a/docs/examples/data_transfer_objects/factory/simple_dto_factory_example.py b/docs/examples/data_transfer_objects/factory/simple_dto_factory_example.py index b293690f73..88f78beb16 100644 --- a/docs/examples/data_transfer_objects/factory/simple_dto_factory_example.py +++ b/docs/examples/data_transfer_objects/factory/simple_dto_factory_example.py @@ -3,12 +3,14 @@ from sqlalchemy.orm import Mapped from litestar import Litestar, post -from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO +from litestar.plugins.sqlalchemy import SQLAlchemyDTO from .my_lib import Base class User(Base): + # `Base` defines `id` field as: + # id: Mapped[UUID] = mapped_column(default=uuid4, primary_key=True) name: Mapped[str] password: Mapped[str] created_at: Mapped[datetime] diff --git a/docs/examples/data_transfer_objects/factory/type_checking.py b/docs/examples/data_transfer_objects/factory/type_checking.py index 670b91703f..4a7ec357ad 100644 --- a/docs/examples/data_transfer_objects/factory/type_checking.py +++ b/docs/examples/data_transfer_objects/factory/type_checking.py @@ -3,8 +3,8 @@ from sqlalchemy.orm import Mapped, mapped_column from litestar import Litestar, post -from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO from litestar.dto import dto_field +from litestar.plugins.sqlalchemy import SQLAlchemyDTO from .my_lib import Base diff --git a/docs/examples/middleware/base.py b/docs/examples/middleware/base.py index adc8bcf4f0..564c02a875 100644 --- a/docs/examples/middleware/base.py +++ b/docs/examples/middleware/base.py @@ -1,46 +1,38 @@ -from time import time -from typing import TYPE_CHECKING, Dict +import time +from typing import Dict -from litestar import Litestar, get, websocket +from litestar import Litestar, WebSocket, get, websocket from litestar.datastructures import MutableScopeHeaders from litestar.enums import ScopeType from litestar.middleware import AbstractMiddleware - -if TYPE_CHECKING: - from litestar import WebSocket - from litestar.types import Message, Receive, Scope, Send +from litestar.types import Message, Receive, Scope, Send class MyMiddleware(AbstractMiddleware): scopes = {ScopeType.HTTP} exclude = ["first_path", "second_path"] - exclude_opt_key = "exclude_from_middleware" + exclude_opt_key = "exclude_from_my_middleware" - async def __call__( - self, - scope: "Scope", - receive: "Receive", - send: "Send", - ) -> None: - start_time = time() + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + start_time = time.monotonic() async def send_wrapper(message: "Message") -> None: if message["type"] == "http.response.start": - process_time = time() - start_time + process_time = time.monotonic() - start_time headers = MutableScopeHeaders.from_message(message=message) headers["X-Process-Time"] = str(process_time) - await send(message) + await send(message) await self.app(scope, receive, send_wrapper) @websocket("/my-websocket") -async def websocket_handler(socket: "WebSocket") -> None: +async def websocket_handler(socket: WebSocket) -> None: """ Websocket handler - is excluded because the middleware scopes includes 'ScopeType.HTTP' """ await socket.accept() - await socket.send_json({"hello websocket"}) + await socket.send_json({"hello": "websocket"}) await socket.close() @@ -56,10 +48,10 @@ def second_handler() -> Dict[str, str]: return {"hello": "second"} -@get("/third_path", exclude_from_middleware=True, sync_to_thread=False) +@get("/third_path", exclude_from_my_middleware=True, sync_to_thread=False) def third_handler() -> Dict[str, str]: - """Handler is excluded due to the opt key 'exclude_from_middleware' matching the middleware 'exclude_opt_key'.""" - return {"hello": "second"} + """Handler is excluded due to the opt key 'exclude_from_my_middleware' matching the middleware 'exclude_opt_key'.""" + return {"hello": "third"} @get("/greet", sync_to_thread=False) diff --git a/docs/examples/openapi/plugins/swagger_ui_config.py b/docs/examples/openapi/plugins/swagger_ui_config.py index 94abdf049a..7280f24e0a 100644 --- a/docs/examples/openapi/plugins/swagger_ui_config.py +++ b/docs/examples/openapi/plugins/swagger_ui_config.py @@ -1,3 +1,3 @@ from litestar.openapi.plugins import SwaggerRenderPlugin -swagger_plugin = SwaggerRenderPlugin(version="5.1.3", path="/swagger") +swagger_plugin = SwaggerRenderPlugin(version="5.18.2", path="/swagger") diff --git a/docs/examples/plugins/problem_details/basic_usage.py b/docs/examples/plugins/problem_details/basic_usage.py new file mode 100644 index 0000000000..7403d66af0 --- /dev/null +++ b/docs/examples/plugins/problem_details/basic_usage.py @@ -0,0 +1,30 @@ +from dataclasses import dataclass + +from litestar import Litestar, post +from litestar.plugins.problem_details import ProblemDetailsConfig, ProblemDetailsException, ProblemDetailsPlugin + + +@dataclass +class PurchaseItem: + item_id: int + quantity: int + + +@post("/purchase") +async def purchase(data: PurchaseItem) -> None: + # Logic to check if the user has enough credit to buy the item. + # We assume the user does not have enough credit. + + raise ProblemDetailsException( + type_="https://example.com/probs/out-of-credit", + title="You do not have enough credit.", + detail="Your current balance is 30, but that costs 50.", + instance="/account/12345/msgs/abc", + extra={"balance": 30}, + ) + + +problem_details_plugin = ProblemDetailsPlugin(ProblemDetailsConfig()) +app = Litestar(route_handlers=[purchase], plugins=[problem_details_plugin]) + +# run: /purchase --header "Content-Type: application/json" --request POST --data '{"item_id": 1234, "quantity": 2}' diff --git a/docs/examples/plugins/problem_details/convert_exceptions.py b/docs/examples/plugins/problem_details/convert_exceptions.py new file mode 100644 index 0000000000..ab977faba1 --- /dev/null +++ b/docs/examples/plugins/problem_details/convert_exceptions.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from litestar import Litestar, post +from litestar.plugins.problem_details import ProblemDetailsConfig, ProblemDetailsException, ProblemDetailsPlugin + + +@dataclass +class PurchaseItem: + item_id: int + quantity: int + + +class PurchaseNotAllowedError(Exception): + def __init__(self, account_id: int, balance: int, detail: str) -> None: + self.account_id = account_id + self.balance = balance + self.detail = detail + + +@post("/purchase") +async def purchase(data: PurchaseItem) -> None: + raise PurchaseNotAllowedError( + account_id=12345, + balance=30, + detail="Your current balance is 30, but that costs 50.", + ) + + +def convert_purchase_not_allowed_to_problem_details(exc: PurchaseNotAllowedError) -> ProblemDetailsException: + return ProblemDetailsException( + type_="https://example.com/probs/out-of-credit", + title="You do not have enough credit.", + detail=exc.detail, + instance=f"/account/{exc.account_id}/msgs/abc", + extra={"balance": exc.balance}, + ) + + +problem_details_plugin = ProblemDetailsPlugin( + ProblemDetailsConfig( + enable_for_all_http_exceptions=True, + exception_to_problem_detail_map={PurchaseNotAllowedError: convert_purchase_not_allowed_to_problem_details}, + ) +) +app = Litestar(route_handlers=[purchase], plugins=[problem_details_plugin]) + +# run: /purchase --header "Content-Type: application/json" --request POST --data '{"item_id": 1234, "quantity": 2}' diff --git a/docs/examples/plugins/problem_details/convert_http_exceptions.py b/docs/examples/plugins/problem_details/convert_http_exceptions.py new file mode 100644 index 0000000000..e8cf669f06 --- /dev/null +++ b/docs/examples/plugins/problem_details/convert_http_exceptions.py @@ -0,0 +1,25 @@ +from dataclasses import dataclass + +from litestar import Litestar, post +from litestar.exceptions.http_exceptions import NotFoundException +from litestar.plugins.problem_details import ProblemDetailsConfig, ProblemDetailsPlugin + + +@dataclass +class PurchaseItem: + item_id: int + quantity: int + + +@post("/purchase") +async def purchase(data: PurchaseItem) -> None: + # Logic to check if the user has enough credit to buy the item. + # We assume the user does not have enough credit. + + raise NotFoundException(detail="No item with the given ID was found", extra={"item_id": data.item_id}) + + +problem_details_plugin = ProblemDetailsPlugin(ProblemDetailsConfig(enable_for_all_http_exceptions=True)) +app = Litestar(route_handlers=[purchase], plugins=[problem_details_plugin]) + +# run: /purchase --header "Content-Type: application/json" --request POST --data '{"item_id": 1234, "quantity": 2}' diff --git a/docs/examples/contrib/prometheus/__init__.py b/docs/examples/plugins/prometheus/__init__.py similarity index 100% rename from docs/examples/contrib/prometheus/__init__.py rename to docs/examples/plugins/prometheus/__init__.py diff --git a/docs/examples/contrib/prometheus/using_prometheus_exporter.py b/docs/examples/plugins/prometheus/using_prometheus_exporter.py similarity index 90% rename from docs/examples/contrib/prometheus/using_prometheus_exporter.py rename to docs/examples/plugins/prometheus/using_prometheus_exporter.py index 99ed912be4..28738e9d5a 100644 --- a/docs/examples/contrib/prometheus/using_prometheus_exporter.py +++ b/docs/examples/plugins/prometheus/using_prometheus_exporter.py @@ -1,5 +1,5 @@ from litestar import Litestar -from litestar.contrib.prometheus import PrometheusConfig, PrometheusController +from litestar.plugins.prometheus import PrometheusConfig, PrometheusController def create_app(group_path: bool = False): diff --git a/docs/examples/contrib/prometheus/using_prometheus_exporter_with_extra_configs.py b/docs/examples/plugins/prometheus/using_prometheus_exporter_with_extra_configs.py similarity index 92% rename from docs/examples/contrib/prometheus/using_prometheus_exporter_with_extra_configs.py rename to docs/examples/plugins/prometheus/using_prometheus_exporter_with_extra_configs.py index d14f4e92a1..913d93748e 100644 --- a/docs/examples/contrib/prometheus/using_prometheus_exporter_with_extra_configs.py +++ b/docs/examples/plugins/prometheus/using_prometheus_exporter_with_extra_configs.py @@ -1,7 +1,7 @@ from typing import Any, Dict from litestar import Litestar, Request -from litestar.contrib.prometheus import PrometheusConfig, PrometheusController +from litestar.plugins.prometheus import PrometheusConfig, PrometheusController # We can modify the path of our custom handler and override the metrics format by subclassing the PrometheusController. @@ -38,7 +38,7 @@ def custom_exemplar(request: Request[Any, Any, Any]) -> Dict[str, str]: app_name="litestar-example", prefix="litestar", labels=extra_labels, - buckets=buckets, + buckets=buckets, # pyright: ignore[reportArgumentType] exemplars=custom_exemplar, excluded_http_methods=["POST"], ) diff --git a/docs/examples/responses/json_suffix_responses.py b/docs/examples/responses/json_suffix_responses.py index 6de3b3e048..3c73794406 100644 --- a/docs/examples/responses/json_suffix_responses.py +++ b/docs/examples/responses/json_suffix_responses.py @@ -4,7 +4,11 @@ from litestar import Litestar, get -@get("/resources", status_code=litestar.status_codes.HTTP_418_IM_A_TEAPOT, media_type="application/problem+json") +@get( + "/resources", + status_code=litestar.status_codes.HTTP_418_IM_A_TEAPOT, + media_type="application/vnd.example.resource+json", +) async def retrieve_resource() -> Dict[str, Any]: return { "title": "Server thinks it is a teapot", diff --git a/docs/examples/security/jwt/custom_decode_payload.py b/docs/examples/security/jwt/custom_decode_payload.py new file mode 100644 index 0000000000..6ae2df58d7 --- /dev/null +++ b/docs/examples/security/jwt/custom_decode_payload.py @@ -0,0 +1,28 @@ +import dataclasses +from typing import Any, List, Optional, Sequence, Union + +from litestar.security.jwt.token import JWTDecodeOptions, Token + + +@dataclasses.dataclass +class CustomToken(Token): + @classmethod + def decode_payload( + cls, + encoded_token: str, + secret: str, + algorithms: List[str], + issuer: Optional[List[str]] = None, + audience: Union[str, Sequence[str], None] = None, + options: Optional[JWTDecodeOptions] = None, + ) -> Any: + payload = super().decode_payload( + encoded_token=encoded_token, + secret=secret, + algorithms=algorithms, + issuer=issuer, + audience=audience, + options=options, + ) + payload["sub"] = payload["sub"].split("@", maxsplit=1)[1] + return payload diff --git a/docs/examples/security/jwt/verify_issuer_audience.py b/docs/examples/security/jwt/verify_issuer_audience.py new file mode 100644 index 0000000000..2b340fc0e9 --- /dev/null +++ b/docs/examples/security/jwt/verify_issuer_audience.py @@ -0,0 +1,32 @@ +import dataclasses +import secrets +from typing import Any, Dict + +from litestar import Litestar, Request, get +from litestar.connection import ASGIConnection +from litestar.security.jwt import JWTAuth, Token + + +@dataclasses.dataclass +class User: + id: str + + +async def retrieve_user_handler(token: Token, connection: ASGIConnection) -> User: + return User(id=token.sub) + + +jwt_auth = JWTAuth[User]( + token_secret=secrets.token_hex(), + retrieve_user_handler=retrieve_user_handler, + accepted_audiences=["https://api.testserver.local"], + accepted_issuers=["https://auth.testserver.local"], +) + + +@get("/") +def handler(request: Request[User, Token, Any]) -> Dict[str, Any]: + return {"id": request.user.id} + + +app = Litestar([handler], middleware=[jwt_auth.middleware]) diff --git a/docs/examples/testing/subprocess_sse_app.py b/docs/examples/testing/subprocess_sse_app.py new file mode 100644 index 0000000000..13a1f96c3a --- /dev/null +++ b/docs/examples/testing/subprocess_sse_app.py @@ -0,0 +1,24 @@ +""" +Assemble components into an app that shall be tested +""" + +from typing import AsyncGenerator + +from litestar import Litestar, get +from litestar.response import ServerSentEvent +from litestar.types import SSEData + + +async def generator(topic: str) -> AsyncGenerator[SSEData, None]: + count = 0 + while count < 2: + yield topic + count += 1 + + +@get("/notify/{topic:str}") +async def get_notified(topic: str) -> ServerSentEvent: + return ServerSentEvent(generator(topic), event_type="Notifier") + + +app = Litestar(route_handlers=[get_notified]) diff --git a/docs/examples/testing/test_get_session_data.py b/docs/examples/testing/test_get_session_data.py index 80c9819a93..f0428b082c 100644 --- a/docs/examples/testing/test_get_session_data.py +++ b/docs/examples/testing/test_get_session_data.py @@ -10,7 +10,7 @@ def set_session_data(request: Request) -> None: request.session["foo"] = "bar" -app = Litestar(route_handlers=[set_session_data], middleware=[session_config.middleware]) +app = Litestar(route_handlers=[set_session_data], middleware=[session_config.middleware], debug=True) with TestClient(app=app, session_config=session_config) as client: client.post("/test").json() diff --git a/docs/examples/testing/test_get_session_data_async.py b/docs/examples/testing/test_get_session_data_async.py index a35756790a..35ebd83352 100644 --- a/docs/examples/testing/test_get_session_data_async.py +++ b/docs/examples/testing/test_get_session_data_async.py @@ -10,7 +10,7 @@ def set_session_data(request: Request) -> None: request.session["foo"] = "bar" -app = Litestar(route_handlers=[set_session_data], middleware=[session_config.middleware]) +app = Litestar(route_handlers=[set_session_data], middleware=[session_config.middleware], debug=True) async def test_set_session_data() -> None: diff --git a/docs/examples/testing/test_health_check_async.py b/docs/examples/testing/test_health_check_async.py index 3806c22b61..0dc5cc0a18 100644 --- a/docs/examples/testing/test_health_check_async.py +++ b/docs/examples/testing/test_health_check_async.py @@ -12,14 +12,7 @@ def health_check() -> str: return "healthy" -app = Litestar(route_handlers=[health_check]) - - -async def test_health_check() -> None: - async with AsyncTestClient(app=app) as client: - response = await client.get("/health-check") - assert response.status_code == HTTP_200_OK - assert response.text == "healthy" +app = Litestar(route_handlers=[health_check], debug=True) @pytest.fixture(scope="function") diff --git a/docs/examples/testing/test_health_check_sync.py b/docs/examples/testing/test_health_check_sync.py index e7886ff938..88826165fa 100644 --- a/docs/examples/testing/test_health_check_sync.py +++ b/docs/examples/testing/test_health_check_sync.py @@ -12,14 +12,7 @@ def health_check() -> str: return "healthy" -app = Litestar(route_handlers=[health_check]) - - -def test_health_check() -> None: - with TestClient(app=app) as client: - response = client.get("/health-check") - assert response.status_code == HTTP_200_OK - assert response.text == "healthy" +app = Litestar(route_handlers=[health_check], debug=True) @pytest.fixture(scope="function") diff --git a/docs/examples/testing/test_set_session_data.py b/docs/examples/testing/test_set_session_data.py index 913c690aa8..919e6a67f6 100644 --- a/docs/examples/testing/test_set_session_data.py +++ b/docs/examples/testing/test_set_session_data.py @@ -12,7 +12,7 @@ def get_session_data(request: Request) -> Dict[str, Any]: return request.session -app = Litestar(route_handlers=[get_session_data], middleware=[session_config.middleware]) +app = Litestar(route_handlers=[get_session_data], middleware=[session_config.middleware], debug=True) def test_get_session_data() -> None: diff --git a/docs/examples/testing/test_set_session_data_async.py b/docs/examples/testing/test_set_session_data_async.py index f89d7c80cd..167f85095c 100644 --- a/docs/examples/testing/test_set_session_data_async.py +++ b/docs/examples/testing/test_set_session_data_async.py @@ -12,7 +12,7 @@ def get_session_data(request: Request) -> Dict[str, Any]: return request.session -app = Litestar(route_handlers=[get_session_data], middleware=[session_config.middleware]) +app = Litestar(route_handlers=[get_session_data], middleware=[session_config.middleware], debug=True) async def test_get_session_data() -> None: diff --git a/docs/examples/testing/test_subprocess_sse.py b/docs/examples/testing/test_subprocess_sse.py new file mode 100644 index 0000000000..e1772069ef --- /dev/null +++ b/docs/examples/testing/test_subprocess_sse.py @@ -0,0 +1,45 @@ +""" +Test the app running in a subprocess +""" + +import asyncio +import pathlib +import sys +from typing import AsyncIterator + +import httpx +import httpx_sse +import pytest + +from litestar.testing import subprocess_async_client + +if sys.platform == "win32": + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + +pytestmark = pytest.mark.anyio + + +@pytest.fixture(scope="session") +def anyio_backend() -> str: + return "asyncio" + + +ROOT = pathlib.Path(__file__).parent + + +@pytest.fixture(name="async_client", scope="session") +async def fx_async_client() -> AsyncIterator[httpx.AsyncClient]: + async with subprocess_async_client(workdir=ROOT, app="subprocess_sse_app:app") as client: + yield client + + +async def test_subprocess_async_client(async_client: httpx.AsyncClient) -> None: + """Demonstrates functionality of the async client with an infinite SSE source that cannot be tested with the + regular async test client. + """ + topic = "demo" + + async with httpx_sse.aconnect_sse(async_client, "GET", f"/notify/{topic}") as event_source: + async for event in event_source.aiter_sse(): + assert event.data == topic + break diff --git a/docs/examples/websockets/stream_and_receive_listener.py b/docs/examples/websockets/stream_and_receive_listener.py new file mode 100644 index 0000000000..d01158e12d --- /dev/null +++ b/docs/examples/websockets/stream_and_receive_listener.py @@ -0,0 +1,26 @@ +import asyncio +import time +from typing import Any, AsyncGenerator + +from litestar import Litestar, WebSocket, websocket_listener +from litestar.handlers import send_websocket_stream + + +async def listener_lifespan(socket: WebSocket) -> None: + async def handle_stream() -> AsyncGenerator[dict[str, float], None]: + while True: + yield {"time": time.time()} + await asyncio.sleep(0.5) + + task = asyncio.create_task(send_websocket_stream(socket=socket, stream=handle_stream())) + yield + task.cancel() + await task + + +@websocket_listener("/", connection_lifespan=listener_lifespan) +def handler(socket: WebSocket, data: Any) -> None: + print(f"{socket.client}: {data}") + + +app = Litestar([handler]) diff --git a/docs/examples/websockets/stream_and_receive_raw.py b/docs/examples/websockets/stream_and_receive_raw.py new file mode 100644 index 0000000000..6e6966b0e7 --- /dev/null +++ b/docs/examples/websockets/stream_and_receive_raw.py @@ -0,0 +1,27 @@ +import asyncio +import time +from typing import AsyncGenerator + +from litestar import Litestar, WebSocket, websocket +from litestar.handlers import send_websocket_stream + + +@websocket("/") +async def handler(socket: WebSocket) -> None: + await socket.accept() + + async def handle_stream() -> AsyncGenerator[dict[str, float], None]: + while True: + yield {"time": time.time()} + await asyncio.sleep(0.5) + + async def handle_receive() -> None: + async for event in socket.iter_json(): + print(f"{socket.client}: {event}") + + async with asyncio.TaskGroup() as tg: + tg.create_task(send_websocket_stream(socket=socket, stream=handle_stream())) + tg.create_task(handle_receive()) + + +app = Litestar([handler]) diff --git a/docs/examples/websockets/stream_basic.py b/docs/examples/websockets/stream_basic.py new file mode 100644 index 0000000000..7ed6517331 --- /dev/null +++ b/docs/examples/websockets/stream_basic.py @@ -0,0 +1,15 @@ +import asyncio +import time +from typing import AsyncGenerator + +from litestar import Litestar, websocket_stream + + +@websocket_stream("/") +async def ping() -> AsyncGenerator[float, None]: + while True: + yield time.time() + await asyncio.sleep(0.5) + + +app = Litestar([ping]) diff --git a/docs/examples/websockets/stream_di_hog.py b/docs/examples/websockets/stream_di_hog.py new file mode 100644 index 0000000000..dca552bbc7 --- /dev/null +++ b/docs/examples/websockets/stream_di_hog.py @@ -0,0 +1,23 @@ +import asyncio +from typing import AsyncGenerator + +from app.lib import ping_external_resource +from litestar import Litestar, websocket_stream + +RESOURCE_LOCK = asyncio.Lock() + + +async def acquire_lock() -> AsyncGenerator[None, None]: + async with RESOURCE_LOCK: + yield + + +@websocket_stream("/") +async def ping(lock: asyncio.Lock) -> AsyncGenerator[float, None]: + while True: + alive = await ping_external_resource() + yield alive + await asyncio.sleep(1) + + +app = Litestar([ping], dependencies={"lock": acquire_lock}) diff --git a/docs/examples/websockets/stream_di_hog_fix.py b/docs/examples/websockets/stream_di_hog_fix.py new file mode 100644 index 0000000000..38716f3ef9 --- /dev/null +++ b/docs/examples/websockets/stream_di_hog_fix.py @@ -0,0 +1,19 @@ +import asyncio +from typing import AsyncGenerator + +from app.lib import ping_external_resource +from litestar import Litestar, websocket_stream + +RESOURCE_LOCK = asyncio.Lock() + + +@websocket_stream("/") +async def ping() -> AsyncGenerator[float, None]: + while True: + async with RESOURCE_LOCK: + alive = await ping_external_resource() + yield alive + await asyncio.sleep(1) + + +app = Litestar([ping]) diff --git a/docs/examples/websockets/stream_socket_access.py b/docs/examples/websockets/stream_socket_access.py new file mode 100644 index 0000000000..63d2c99d06 --- /dev/null +++ b/docs/examples/websockets/stream_socket_access.py @@ -0,0 +1,15 @@ +import asyncio +import time +from typing import Any, AsyncGenerator + +from litestar import Litestar, WebSocket, websocket_stream + + +@websocket_stream("/") +async def ping(socket: WebSocket) -> AsyncGenerator[dict[str, Any], None]: + while True: + yield {"time": time.time(), "client": socket.client} + await asyncio.sleep(0.5) + + +app = Litestar([ping]) diff --git a/docs/examples/websockets/with_dto.py b/docs/examples/websockets/with_dto.py index d3f66dba17..6cb6eccf72 100644 --- a/docs/examples/websockets/with_dto.py +++ b/docs/examples/websockets/with_dto.py @@ -1,11 +1,10 @@ from sqlalchemy.orm import Mapped from litestar import Litestar, websocket_listener -from litestar.contrib.sqlalchemy.base import UUIDBase -from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO +from litestar.plugins.sqlalchemy import SQLAlchemyDTO, base -class User(UUIDBase): +class User(base.UUIDBase): name: Mapped[str] diff --git a/docs/index.rst b/docs/index.rst index c73c26b5e1..5bea2e7dd7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -155,12 +155,6 @@ A huge thank you to our current sponsors:

Telemetry Sports

-
- - Stok - -

Stok

-
We invite organizations and individuals to join our sponsorship program. @@ -325,13 +319,13 @@ Example Applications -------------------- -* `litestar-pg-redis-docker `_ : In addition to Litestar, this - demonstrates a pattern of application modularity, SQLAlchemy 2.0 ORM, Redis cache connectivity, and more. Like all - Litestar projects, this application is open to contributions, big and small. * `litestar-fullstack `_ : A fully-capable, production-ready fullstack Litestar web application configured with best practices. It includes SQLAlchemy 2.0, VueJS, `Vite `_, `SAQ job queue `_, ``Jinja`` templates and more. - `Read more `_. + `Read more `_. Like all + Litestar projects, this application is open to contributions, big and small. +* `litestar-fullstack-inertia `_ : Similar to + `Litestar Fullstack `_ but uses `Inertia.js `_. * `litestar-hello-world `_: A bare-minimum application setup. Great for testing and POC work. diff --git a/docs/reference/plugins/attrs.rst b/docs/reference/plugins/attrs.rst new file mode 100644 index 0000000000..e3bcbb96cd --- /dev/null +++ b/docs/reference/plugins/attrs.rst @@ -0,0 +1,5 @@ +attrs +===== + +.. automodule:: litestar.plugins.attrs + :members: diff --git a/docs/reference/plugins/htmx.rst b/docs/reference/plugins/htmx.rst new file mode 100644 index 0000000000..d13083ec7e --- /dev/null +++ b/docs/reference/plugins/htmx.rst @@ -0,0 +1,7 @@ +==== +htmx +==== + + +.. automodule:: litestar.plugins.htmx + :members: diff --git a/docs/reference/plugins/index.rst b/docs/reference/plugins/index.rst index 128fdf0302..69b0063948 100644 --- a/docs/reference/plugins/index.rst +++ b/docs/reference/plugins/index.rst @@ -9,6 +9,11 @@ plugins :maxdepth: 1 :hidden: + attrs flash_messages + htmx + problem_details + prometheus + pydantic structlog sqlalchemy diff --git a/docs/reference/plugins/problem_details.rst b/docs/reference/plugins/problem_details.rst new file mode 100644 index 0000000000..c825c84f77 --- /dev/null +++ b/docs/reference/plugins/problem_details.rst @@ -0,0 +1,7 @@ +=============== +problem details +=============== + + +.. automodule:: litestar.plugins.problem_details + :members: diff --git a/docs/reference/plugins/prometheus.rst b/docs/reference/plugins/prometheus.rst new file mode 100644 index 0000000000..c45052a9f8 --- /dev/null +++ b/docs/reference/plugins/prometheus.rst @@ -0,0 +1,5 @@ +prometheus +========== + +.. automodule:: litestar.plugins.prometheus + :members: diff --git a/docs/reference/plugins/pydantic.rst b/docs/reference/plugins/pydantic.rst new file mode 100644 index 0000000000..104e9ace7a --- /dev/null +++ b/docs/reference/plugins/pydantic.rst @@ -0,0 +1,5 @@ +pydantic +======== + +.. automodule:: litestar.plugins.pydantic + :members: diff --git a/docs/reference/stores/index.rst b/docs/reference/stores/index.rst index 8123ba1da4..23095869c3 100644 --- a/docs/reference/stores/index.rst +++ b/docs/reference/stores/index.rst @@ -8,3 +8,4 @@ stores memory redis registry + valkey diff --git a/docs/reference/stores/valkey.rst b/docs/reference/stores/valkey.rst new file mode 100644 index 0000000000..288118c2e3 --- /dev/null +++ b/docs/reference/stores/valkey.rst @@ -0,0 +1,5 @@ +valkey +====== + +.. automodule:: litestar.stores.valkey + :members: diff --git a/docs/reference/testing.rst b/docs/reference/testing.rst index 85767df5bf..ebc9bc1044 100644 --- a/docs/reference/testing.rst +++ b/docs/reference/testing.rst @@ -3,7 +3,7 @@ testing .. automodule:: litestar.testing - :members: RequestFactory, BaseTestClient, TestClient, AsyncTestClient, create_async_test_client, create_test_client + :members: RequestFactory, BaseTestClient, TestClient, AsyncTestClient, create_async_test_client, create_test_client, subprocess_sync_client, subprocess_async_client :undoc-members: WebSocketTestSession diff --git a/docs/reference/types.rst b/docs/reference/types.rst index 52767a5d01..2cd000f640 100644 --- a/docs/reference/types.rst +++ b/docs/reference/types.rst @@ -1,7 +1,7 @@ types ===== -.. py:currentmodule:: litestar.types +.. module:: litestar.types @@ -58,15 +58,15 @@ ASGI Application Parameters ASGI Scopes ~~~~~~~~~~~~ -.. autodata:: litestar.types.ASGIVersion +.. autoclass:: litestar.types.ASGIVersion -.. autodata:: litestar.types.BaseScope +.. autoclass:: litestar.types.BaseScope -.. autodata:: litestar.types.HTTPScope +.. autoclass:: litestar.types.HTTPScope -.. autodata:: litestar.types.LifeSpanScope +.. autoclass:: litestar.types.LifeSpanScope -.. autodata:: litestar.types.WebSocketScope +.. autoclass:: litestar.types.WebSocketScope ASGI Events diff --git a/docs/release-notes/changelog.rst b/docs/release-notes/changelog.rst index 03fefbd852..5257fd1f37 100644 --- a/docs/release-notes/changelog.rst +++ b/docs/release-notes/changelog.rst @@ -3,6 +3,576 @@ 2.x Changelog ============= + +.. changelog:: 2.13.0 + :date: 2024-11-20 + + .. change:: Add ``request_max_body_size`` layered parameter + :type: feature + + Add a new ``request_max_body_size`` layered parameter, which limits the + maximum size of a request body before returning a ``413 - Request Entity Too Large``. + + .. seealso:: + :ref:`usage/requests:limits` + + + .. change:: Send CSRF request header in OpenAPI plugins + :type: feature + :pr: 3754 + + Supported OpenAPI UI clients will extract the CSRF cookie value and attach it to + the request headers if CSRF is enabled on the application. + + .. change:: deprecate `litestar.contrib.sqlalchemy` + :type: feature + :pr: 3755 + + Deprecate the ``litestar.contrib.sqlalchemy`` module in favor of ``litestar.plugins.sqlalchemy`` + + + .. change:: implement `HTMX` plugin using `litestar-htmx` + :type: feature + :pr: 3837 + + This plugin migrates the HTMX integration to ``litestar.plugins.htmx``. + + This logic has been moved to it's own repository named ``litestar-htmx`` + + .. change:: Pydantic: honor ``hide_input_in_errors`` in throwing validation exceptions + :type: feature + :pr: 3843 + + Pydantic's ``BaseModel`` supports configuration to hide data values when + throwing exceptions, via setting ``hide_input_in_errors`` -- see + https://docs.pydantic.dev/2.0/api/config/#pydantic.config.ConfigDict.hide_input_in_errors + and https://docs.pydantic.dev/latest/usage/model_config/#hide-input-in-errors + + Litestar will now honour this setting + + .. change:: deprecate``litestar.contrib.pydantic`` + :type: feature + :pr: 3852 + :issue: 3787 + + ## Description + + Deprecate ``litestar.contrib.pydantic`` in favor of ``litestar.plugins.pydantic`` + + + .. change:: Fix sign bug in rate limit middelware + :type: bugfix + :pr: 3776 + + Fix a bug in the rate limit middleware, that would cause the response header + fields ``RateLimit-Remaining`` and ``RateLimit-Reset`` to have negative values. + + + .. change:: OpenAPI: map JSONSchema spec naming convention to snake_case when names from ``schema_extra`` are not found + :type: bugfix + :pr: 3767 + :issue: 3766 + + Address rejection of ``schema_extra`` values using JSONSchema spec-compliant + key names by mapping between the relevant naming conventions. + + .. change:: Use correct path template for routes without path parameters + :type: bugfix + :pr: 3784 + + Fix a but where, when using ``PrometheusConfig.group_path=True``, the metrics + exporter response content would ignore all paths with no path parameters. + + .. change:: Fix a dangling anyio stream in ``TestClient`` + :type: bugfix + :pr: 3836 + :issue: 3834 + + Fix a dangling anyio stream in ``TestClient`` that would cause a resource warning + + Closes #3834. + + .. change:: Fix bug in handling of missing ``more_body`` key in ASGI response + :type: bugfix + :pr: 3845 + + Some frameworks do not include the ``more_body`` key in the "http.response.body" ASGI event. + According to the ASGI specification, this key should be set to ``False`` when + there is no additional body content. Litestar expects ``more_body`` to be + explicitly defined, but others might not. + + This leads to failures when an ASGI framework mounted on Litestar throws error + if this key is missing. + + + .. change:: Fix duplicate ``RateLimit-*`` headers with caching + :type: bugfix + :pr: 3855 + :issue: 3625 + + Fix a bug where ``RateLimitMiddleware`` duplicate all ``RateLimit-*`` headers + when handler cache is enabled. + + +.. changelog:: 2.12.1 + :date: 2024-09-21 + + .. change:: Fix base package requiring ``annotated_types`` dependency + :type: bugfix + :pr: 3750 + :issue: 3749 + + Fix a bug introduced in #3721 that was released with ``2.12.0`` caused an + :exc:`ImportError` when the ``annotated_types`` package was not installed. + + +.. changelog:: 2.12.0 + :date: 2024-09-21 + + .. change:: Fix overzealous warning for greedy middleware ``exclude`` pattern + :type: bugfix + :pr: 3712 + + Fix a bug introduced in ``2.11.0`` (https://github.com/litestar-org/litestar/pull/3700), + where the added warning for a greedy pattern use for the middleware ``exclude`` + parameter was itself greedy, and would warn for non-greedy patterns, e.g. + ``^/$``. + + .. change:: Fix dangling coroutines in request extraction handling cleanup + :type: bugfix + :pr: 3735 + :issue: 3734 + + Fix a bug where, when a required header parameter was defined for a request that + also expects a request body, failing to provide the header resulted in a + :exc:`RuntimeWarning`. + + .. code-block:: python + + @post() + async def handler(data: str, secret: Annotated[str, Parameter(header="x-secret")]) -> None: + return None + + If the ``x-secret`` header was not provided, warning like this would be seen: + + .. code-block:: + + RuntimeWarning: coroutine 'json_extractor' was never awaited + + + .. change:: OpenAPI: Correctly handle ``type`` keyword + :type: bugfix + :pr: 3715 + :issue: 3714 + + Fix a bug where a type alias created with the ``type`` keyword would create an + empty OpenAPI schema entry for that parameter + + .. change:: OpenAPI: Ensure valid schema keys + :type: bugfix + :pr: 3635 + :issue: 3630 + + Ensure that generated schema component keys are always valid according to + `§ 4.8.7.1 `_ of the + OpenAPI specification. + + + .. change:: OpenAPI: Correctly handle ``msgspec.Struct`` tagged unions + :type: bugfix + :pr: 3742 + :issue: 3659 + + Fix a bug where the OpenAPI schema would not include the struct fields + implicitly generated by msgspec for its + `tagged union `_ + support. + + The tag field of the struct will now be added as a ``const`` of the appropriate + type to the schema. + + + .. change:: OpenAPI: Fix Pydantic 1 constrained string with default factory + :type: bugfix + :pr: 3721 + :issue: 3710 + + Fix a bug where using a Pydantic model with a ``default_factory`` set for a + constrained string field would raise a :exc:`SerializationException`. + + .. code-block:: python + + class Model(BaseModel): + field: str = Field(default_factory=str, max_length=600) + + + .. change:: OpenAPI/DTO: Fix missing Pydantic 2 computed fields + :type: bugfix + :pr: 3721 + :issue: 3656 + + Fix a bug that would lead to Pydantic computed fields to be ignored during + schema generation when the model was using a + :class:`~litestar.contrib.pydantic.PydanticDTO`. + + .. code-block:: python + :caption: Only the ``foo`` field would be included in the schema + + class MyModel(BaseModel): + foo: int + + @computed_field + def bar(self) -> int: + return 123 + + @get(path="/", return_dto=PydanticDTO[MyModel]) + async def test() -> MyModel: + return MyModel.model_validate({"foo": 1}) + + .. change:: OpenAPI: Fix Pydantic ``json_schema_extra`` overrides only being merged partially + :type: bugfix + :pr: 3721 + :issue: 3656 + + Fix a bug where ``json_schema_extra`` were not reliably extracted from Pydantic + models and included in the OpenAPI schema. + + .. code-block:: python + :caption: Only the title set directly on the field would be used for the schema + + class Model(pydantic.BaseModel): + with_title: str = pydantic.Field(title="new_title") + with_extra_title: str = pydantic.Field(json_schema_extra={"title": "more_new_title"}) + + + @get("/example") + async def example_route() -> Model: + return Model(with_title="1", with_extra_title="2") + + + .. change:: Support strings in ``media_type`` for ``ResponseSpec`` + :type: feature + :pr: 3729 + :issue: 3728 + + Accept strings for the ``media_type`` parameter of :class:`~litestar.openapi.datastructures.ResponseSpec`, + making it behave the same way as :paramref:`~litestar.response.Response.media_type`. + + + .. change:: OpenAPI: Allow customizing schema component keys + :type: feature + :pr: 3738 + + Allow customizing the schema key used for a component in the OpenAPI schema. + The supplied keys are enforced to be unique, and it is checked that they won't + be reused across different types. + + The keys can be set with the newly introduced ``schema_component_key`` parameter, + which is available on :class:`~litestar.params.KwargDefinition`, + :func:`~litestar.params.Body` and :func:`~litestar.params.Parameter`. + + .. code-block:: python + :caption: Two components will be generated: ``Data`` and ``not_data`` + + @dataclass + class Data: + pass + + @post("/") + def handler( + data: Annotated[Data, Parameter(schema_component_key="not_data")], + ) -> Data: + return Data() + + @get("/") + def handler_2() -> Annotated[Data, Parameter(schema_component_key="not_data")]: + return Data() + + .. change:: Raise exception when body parameter is annotated with non-bytes type + :type: feature + :pr: 3740 + + Add an informative error message to help avoid the common mistake of attempting + to use the ``body`` parameter to receive validated / structured data by + annotating it with a type such as ``list[str]``, instead of ``bytes``. + + + .. change:: OpenAPI: Default to ``latest`` scalar version + :type: feature + :pr: 3747 + + Change the default version of the scalar OpenAPI renderer to ``latest`` + + +.. changelog:: 2.11.0 + :date: 2024-08-27 + + .. change:: Use PyJWT instead of python-jose + :type: feature + :pr: 3684 + + The functionality in :mod:`litestar.security.jwt` is now backed by + `PyJWT `_ instead of + `python-jose `_, due to the unclear + maintenance status of the latter. + + .. change:: DTO: Introduce ``forbid_unknown_fields`` config + :type: feature + :pr: 3690 + + Add a new config option to :class:`~litestar.dto.config.DTOConfig`: + :attr:`~litestar.dto.config.DTOConfig.forbid_unknown_fields` + When set to ``True``, a validation error response will be returned if the source + data contains fields not defined on the model. + + .. change:: DTO: Support ``extra="forbid"`` model config for ``PydanticDTO`` + :type: feature + :pr: 3691 + + For Pydantic models with `extra="forbid" `_ + in their configuration: + + .. tab-set:: + + .. tab-item:: Pydantic 2 + + .. code-block:: python + + class User(BaseModel): + model_config = ConfigDict(extra='ignore') + name: str + + .. tab-item:: Pydantic 1 + + .. code-block:: python + + class User(BaseModel): + class Config: + extra = "ignore" + name: str + + :attr:`~litestar.dto.config.DTOConfig.forbid_unknown_fields` will be set to ``True`` by default. + + .. note:: + It's still possible to override this configuration at the DTO level + + + To facilitate this feature, :meth:`~litestar.dto.base_dto.AbstractDTO.get_config_for_model_type` + has been added to :class:`~litestar.dto.base_dto.AbstractDTO`, allowing the + customization of the base config defined on the DTO factory for a specific model + type. It will be called on DTO factory initialization, and receives the concrete + DTO model type along side the :class:`~litestar.dto.config.DTOConfig` defined + on the base DTO, which it can alter and return a new version to be used within + the DTO instance. + + .. change:: Custom JWT payload classes + :type: feature + :pr: 3692 + + Support extending the default :class:`~litestar.security.jwt.Token` class used + by the JWT backends decode the payload into. + + - Add new ``token_cls`` field on the JWT auth config classes + - Add new ``token_cls`` parameter to JWT auth middlewares + - Switch to using msgspec to convert the JWT payload into instances of the token + class + + .. code-block:: python + + import dataclasses + import secrets + from typing import Any, Dict + + from litestar import Litestar, Request, get + from litestar.connection import ASGIConnection + from litestar.security.jwt import JWTAuth, Token + + @dataclasses.dataclass + class CustomToken(Token): + token_flag: bool = False + + @dataclasses.dataclass + class User: + id: str + + async def retrieve_user_handler(token: CustomToken, connection: ASGIConnection) -> User: + return User(id=token.sub) + + TOKEN_SECRET = secrets.token_hex() + + jwt_auth = JWTAuth[User]( + token_secret=TOKEN_SECRET, + retrieve_user_handler=retrieve_user_handler, + token_cls=CustomToken, + ) + + @get("/") + def handler(request: Request[User, CustomToken, Any]) -> Dict[str, Any]: + return {"id": request.user.id, "token_flag": request.auth.token_flag} + + + .. change:: Extended JWT configuration options + :type: feature + :pr: 3695 + + **New JWT backend fields** + + - :attr:`~litestar.security.jwt.JWTAuth.accepted_audiences` + - :attr:`~litestar.security.jwt.JWTAuth.accepted_issuers` + - :attr:`~litestar.security.jwt.JWTAuth.require_claims` + - :attr:`~litestar.security.jwt.JWTAuth.verify_expiry` + - :attr:`~litestar.security.jwt.JWTAuth.verify_not_before` + - :attr:`~litestar.security.jwt.JWTAuth.strict_audience` + + **New JWT middleware parameters** + + - :paramref:`~litestar.security.jwt.JWTAuthenticationMiddleware.token_audience` + - :paramref:`~litestar.security.jwt.JWTAuthenticationMiddleware.token_issuer` + - :paramref:`~litestar.security.jwt.JWTAuthenticationMiddleware.require_claims` + - :paramref:`~litestar.security.jwt.JWTAuthenticationMiddleware.verify_expiry` + - :paramref:`~litestar.security.jwt.JWTAuthenticationMiddleware.verify_not_before` + - :paramref:`~litestar.security.jwt.JWTAuthenticationMiddleware.strict_audience` + + **New ``Token.decode`` parameters** + + - :paramref:`~litestar.security.jwt.Token.decode.audience` + - :paramref:`~litestar.security.jwt.Token.decode.issuer` + - :paramref:`~litestar.security.jwt.Token.decode.require_claims` + - :paramref:`~litestar.security.jwt.Token.decode.verify_exp` + - :paramref:`~litestar.security.jwt.Token.decode.verify_nbf` + - :paramref:`~litestar.security.jwt.Token.decode.strict_audience` + + **Other changes** + + :meth`Token.decode_payload <~litestar.security.jwt.Token.decode_payload>` has + been added to make customization of payload decoding / verification easier + without having to re-implement the functionality of the base class method. + + .. seealso:: + :doc:`/usage/security/jwt` + + .. change:: Warn about greedy exclude patterns in middlewares + :type: feature + :pr: 3700 + + Raise a warning when a middlewares ``exclude`` pattern greedily matches all + paths. + + .. code-block:: python + + from litestar.middlewares + + class MyMiddleware(AbstractMiddleware): + exclude = ["/", "/home"] + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + await self.app(scope, receive, send) + + Middleware like this would silently be disabled for every route, since the + exclude pattern ``/`` matches all paths. If a configuration like this is + detected, a warning will now be raised at application startup. + + .. change:: RFC 9457 *Problem Details* plugin + :type: feature + :pr: 3323 + :issue: 3199 + + Add a plugin to support `RFC 9457 `_ + *Problem Details* responses for error response. + + :class:`~litestar.plugins.problem_details.ProblemDetailsPlugin` enables to + selectively or collectively turn responses with an error status code into + *Problem Detail* responses. + + .. seealso:: + :doc:`/usage/plugins/problem_details` + + .. change:: Fix creation of ``FormMultiDict`` in ``Request.form`` to properly handle multi-keys + :type: bugfix + :pr: 3639 + :issue: 3627 + + Fix https://github.com/litestar-org/litestar/issues/3627 by properly handling + the creation of :class:`~litestar.datastructures.FormMultiDict` where multiple + values are given for a single key, to make + :meth:`~litestar.connection.Request.form` match the behaviour of receiving form + data via the ``data`` kwarg inside a route handler. + + **Before** + + .. code-block:: python + + @post("/") + async def handler(request: Request) -> Any: + return (await request.form()).getall("foo") + + with create_test_client(handler) as client: + print(client.post("/", data={"foo": ["1", "2"]}).json()) # [["1", "2"]] + + **After** + + .. code-block:: python + + @post("/") + async def handler(request: Request) -> Any: + return (await request.form()).getall("foo") + + with create_test_client(handler) as client: + print(client.post("/", data={"foo": ["1", "2"]}).json()) # ["1", "2"] + + .. change:: DTO: Fix inconsistent use of strict decoding mode + :type: bugfix + :pr: 3685 + + Fix inconsistent usage of msgspec's ``strict`` mode in the base DTO backend. + + ``strict=False`` was being used when transferring from builtins, while + ``strict=True`` was used transferring from raw data, causing an unwanted + discrepancy in behaviour. + + .. change:: Use path template for prometheus metrics + :type: bugfix + :pr: 3687 + + Changed previous 1-by-1 replacement logic for + ``PrometheusMiddleware.group_path=true`` with a more robust and slightly faster + solution. + + .. change:: Ensure OpenTelemetry captures exceptions in the outermost application layers + :type: bugfix + :pr: 3689 + :issue: 3663 + + A bug was fixed that resulted in exception occurring in the outermost + application layer not being captured under the current request span, which led + to incomplete traces. + + .. change:: Fix CSRFMiddleware sometimes setting cookies for excluded paths + :type: bugfix + :pr: 3698 + :issue: 3688 + + Fix a bug that would cause :class:`~litestar.middleware.csrf.CSRFMiddleware` to + set a cookie (which would not be used subsequently) on routes it had been + excluded from via a path pattern. + + .. change:: Make override behaviour consistent between ``signature_namespace`` and ``signature_types`` + :type: bugfix + :pr: 3696 + :issue: 3681 + + Ensure that adding signature types to ``signature_namespace`` and + ``signature_types`` behaves the same way when a name was already present in the + namespace. + + Both will now issue a warning if a name is being overwritten with a different + type. If a name is registered again for the same type, no warning will be given. + + .. note:: + + You can disable this warning globally by setting + ``LITESTAR_WARN_SIGNATURE_NAMESPACE_OVERRIDE=0`` in your environment + .. changelog:: 2.10.0 :date: 2024-07-26 @@ -1586,8 +2156,7 @@ - ``--schema``, to include the routes serving OpenAPI schema and docs - ``--exclude`` to exclude routes matching a specified pattern - .. seealso:: - :ref:`usage/cli:routes` + .. seealso:: Read more in the CLI :doc:`/reference/cli` section. .. change:: Improve performance of threaded synchronous execution :type: misc diff --git a/docs/topics/sync-vs-async.rst b/docs/topics/sync-vs-async.rst index 9df304dc0a..e7550f051b 100644 --- a/docs/topics/sync-vs-async.rst +++ b/docs/topics/sync-vs-async.rst @@ -101,7 +101,7 @@ When to use a synchronous function ---------------------------------- As an inverse of the previous paragraph, it follows that synchronous functions should -be used for non-blocking, non-computationally intensive tasks. The synchronous execution +be used for non-io intensive tasks. The synchronous execution model allows for the smallest amount of overhead and should therefore be preferred in such situations where no asynchronous functionality is made use of. diff --git a/docs/tutorials/repository-tutorial/01-modeling-and-features.rst b/docs/tutorials/repository-tutorial/01-modeling-and-features.rst index ce66ea2979..849d7cc4f9 100644 --- a/docs/tutorials/repository-tutorial/01-modeling-and-features.rst +++ b/docs/tutorials/repository-tutorial/01-modeling-and-features.rst @@ -74,6 +74,7 @@ Additional features provided by the built-in base models include: reverts to an ``Integer`` for unsupported variants. - A custom :class:`JsonB ` type that uses native ``JSONB`` where possible and ``Binary`` or ``Blob`` as an alternative. +- A custom :class:`EncryptedString ` encrypted string that supports multiple cryptography backends. Let's build on this as we look at the repository classes. diff --git a/docs/tutorials/sqlalchemy/3-init-plugin.rst b/docs/tutorials/sqlalchemy/3-init-plugin.rst index 0990c1cf96..94b3eb75ef 100644 --- a/docs/tutorials/sqlalchemy/3-init-plugin.rst +++ b/docs/tutorials/sqlalchemy/3-init-plugin.rst @@ -19,7 +19,7 @@ Here's the updated code: .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_init_plugin.py :language: python :linenos: - :emphasize-lines: 12,30,78-79,87 + :emphasize-lines: 11,30,78-79,87 The most notable difference is that we no longer need the ``db_connection()`` lifespan context manager - the plugin handles this for us. It also handles the creation of the tables in our database if we supply our metadata and diff --git a/docs/tutorials/sqlalchemy/4-final-touches-and-recap.rst b/docs/tutorials/sqlalchemy/4-final-touches-and-recap.rst index 9b70b8f776..a619dff114 100644 --- a/docs/tutorials/sqlalchemy/4-final-touches-and-recap.rst +++ b/docs/tutorials/sqlalchemy/4-final-touches-and-recap.rst @@ -59,7 +59,7 @@ engine and session lifecycle, and register our ``transaction`` dependency. .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_plugin.py :language: python :linenos: - :lines: 80-84 + :lines: 80-83 .. seealso:: diff --git a/docs/usage/applications.rst b/docs/usage/applications.rst index 293d2be120..63cd97ed94 100644 --- a/docs/usage/applications.rst +++ b/docs/usage/applications.rst @@ -15,6 +15,7 @@ of :class:`Controllers <.controller.Controller>`, :class:`Routers <.router.Route or :class:`Route handlers <.handlers.BaseRouteHandler>`: .. literalinclude:: /examples/hello_world.py + :language: python :caption: A simple Hello World Litestar app The app instance is the root level of the app - it has the base path of ``/`` and all root level @@ -49,6 +50,7 @@ For example, let us create a database connection using the async engine from establish the connection, and another to close it, and then pass them to the :class:`~litestar.app.Litestar` constructor: .. literalinclude:: /examples/startup_and_shutdown.py + :language: python :caption: Startup and Shutdown .. _lifespan-context-managers: @@ -61,6 +63,7 @@ In addition to the lifespan hooks, Litestar also supports managing the lifespan keep a certain context object, such as a connection, around. .. literalinclude:: /examples/application_hooks/lifespan_manager.py + :language: python :caption: Handling a database connection Order of execution @@ -116,6 +119,7 @@ Therefore, :paramref:`~.app.Litestar.state` offers an easy way to share contextu of the application, as seen below: .. literalinclude:: /examples/application_state/using_application_state.py + :language: python :caption: Using Application State .. _Initializing Application State: @@ -127,6 +131,7 @@ To seed application state, you can pass a :class:`~.datastructures.state.State` :paramref:`~.app.Litestar.state` parameter of the Litestar constructor: .. literalinclude:: /examples/application_state/passing_initial_state.py + :language: python :caption: Using Application State .. note:: :class:`~.datastructures.state.State` can be initialized with a :class:`dictionary `, an instance of @@ -166,6 +171,7 @@ To discourage its use, Litestar also offers a builtin :class:`~.datastructures.s You can use this class to type state and ensure that no mutation of state is allowed: .. literalinclude:: /examples/application_state/using_immutable_state.py + :language: python :caption: Using Custom State to ensure immutability Application Hooks @@ -187,6 +193,7 @@ The :paramref:`~litestar.app.Litestar.after_exception` hook takes a the ``exception`` that occurred and the ASGI ``scope`` of the request or websocket connection. .. literalinclude:: /examples/application_hooks/after_exception_hook.py + :language: python :caption: After Exception Hook .. attention:: This hook is not meant to handle exceptions - it just receives them to allow for side effects. @@ -200,6 +207,7 @@ The :paramref:`~litestar.app.Litestar.before_send` hook takes a sent. The hook receives the message instance and the ASGI ``scope``. .. literalinclude:: /examples/application_hooks/before_send_hook.py + :language: python :caption: Before Send Hook Initialization @@ -218,6 +226,7 @@ develop third-party application configuration systems. called within :paramref:`~litestar.app.Litestar.__init__`, outside of an async context. .. literalinclude:: /examples/application_hooks/on_app_init.py + :language: python :caption: Example usage of the ``on_app_init`` hook to modify the application configuration. .. _layered-architecture: diff --git a/docs/usage/caching.rst b/docs/usage/caching.rst index 3eaf68cb8e..46f9254470 100644 --- a/docs/usage/caching.rst +++ b/docs/usage/caching.rst @@ -35,7 +35,7 @@ sentinel instead: :language: python :caption: Caching the response indefinitely by setting the :paramref:`~litestar.handlers.HTTPRouteHandler.cache` parameter to :class:`~litestar.config.response_cache.CACHE_FOREVER`. - :lines: 1, 3, 14-18 + :lines: 1-3, 14-18 :emphasize-lines: 5 Configuration diff --git a/docs/usage/channels.rst b/docs/usage/channels.rst index 5d9fd8daf4..e139adb60a 100644 --- a/docs/usage/channels.rst +++ b/docs/usage/channels.rst @@ -219,8 +219,10 @@ subscriptions need to be managed dynamically. .. code-block:: python subscriber = await channels.subscribe(["foo", "bar"]) - ... # do some stuff here - await channels.unsubscribe(subscriber, ["foo"]) + try: + ... # do some stuff here + finally: + await channels.unsubscribe(subscriber, ["foo"]) Or, using the context manager diff --git a/docs/usage/cli.rst b/docs/usage/cli.rst index c1de49b3fe..52a2bfb39e 100644 --- a/docs/usage/cli.rst +++ b/docs/usage/cli.rst @@ -1,6 +1,9 @@ CLI === +.. |uvicorn| replace:: uvicorn +.. _uvicorn: https://www.uvicorn.org/ + Litestar provides a convenient command line interface (CLI) for running and managing Litestar applications. The CLI is powered by `click `_, `rich `_, and `rich-click `_. @@ -10,14 +13,16 @@ Enabling all CLI features The CLI and its hard dependencies are included by default. However, if you want to run your application (using ``litestar run`` ) or beautify the Typescript generated by the ``litestar schema typescript`` -command, you'll need ``uvicorn`` and ``jsbeautifier`` . They can be installed independently, but we -recommend installing the ``standard`` group which conveniently bundles commonly used optional dependencies. +command, you will need |uvicorn|_ and `jsbeautifier `_. +They can be installed independently, but we recommend installing the ``standard`` extra which conveniently bundles +commonly used optional dependencies. .. code-block:: shell + :caption: Install the standard group - pip install litestar[standard] + pip install litestar[standard] -Once you have installed ``standard``, you'll have access to the ``litestar run`` command. +Once you have installed ``standard``, you will have access to the ``litestar run`` command. Autodiscovery ------------- @@ -38,300 +43,18 @@ The autodiscovery follows these lookup locations in order: Within these locations, Litestar CLI looks for: -1. An object named ``app`` that is an instance of :class:`Litestar <.app.Litestar>` -2. An object named ``application`` that is an instance of :class:`Litestar <.app.Litestar>` -3. Any object that is an instance of :class:`Litestar <.app.Litestar>` -4. A callable named ``create_app`` -5. A callable annotated to return an instance of :class:`Litestar <.app.Litestar>` - -Commands --------- - -litestar -^^^^^^^^ - -The main entrypoint to the Litestar CLI is the ``litestar`` command. - -If you don't pass the ``--app`` flag, the application will be automatically discovered, as explained in -`Autodiscovery`_. - -Options -~~~~~~~ - -+---------------+---------------------------+-----------------------------------------------------------------+ -| Flag | Environment variable | Description | -+===============+===========================+=================================================================+ -| ``--app`` | ``LITESTAR_APP`` | ``.:`` | -+---------------+---------------------------+-----------------------------------------------------------------+ -| ``--app-dir`` | N/A | Look for the app in the specified directory by adding it to the | -| | | PYTHONPATH. Defaults to the current working directory. | -+---------------+---------------------------+-----------------------------------------------------------------+ - -version -^^^^^^^ - -Prints the currently installed version of Litestar. - -Options -~~~~~~~ - -+-------------------------+------------------------------------+ -| Name | Description | -+=========================+====================================+ -| ``-s``\ , ``--short`` | Include only ``MAJOR.MINOR.PATCH`` | -+-------------------------+------------------------------------+ - - -run -^^^ - -The ``run`` command executes a Litestar application using `uvicorn `_. - -.. code-block:: shell - - litestar run - -.. caution:: - - This feature is intended for development purposes only and should not be used to deploy production applications. - -.. versionchanged:: 2.8.0 - CLI options take precedence over environment variables! - -.. _cli-run-options: - -Options -~~~~~~~ - -+-------------------------------------------+----------------------------------------------+--------------------------------------------------------------------------------------------+ -| Flag | Environment variable | Description | -+===========================================+==============================================+============================================================================================+ -| ``-r``\ , ``--reload`` | ``LITESTAR_RELOAD`` | Reload the application when files in its directory are changed | -+-------------------------------------------+----------------------------------------------+--------------------------------------------------------------------------------------------+ -| ``-R``\ , ``--reload-dir`` | ``LITESTAR_RELOAD_DIRS`` | Specify directories to watch for reload. | -+-------------------------------------------+----------------------------------------------+--------------------------------------------------------------------------------------------+ -| ``-I``\ , ``--reload-include`` | ``LITESTAR_RELOAD_INCLUDES`` | Specify glob patterns for files to include when watching for reload. | -+-------------------------------------------+----------------------------------------------+--------------------------------------------------------------------------------------------+ -| ``-E``\ , ``--reload-exclude`` | ``LITESTAR_RELOAD_EXCLUDES`` | Specify glob patterns for files to exclude when watching for reload. | -+-------------------------------------------+----------------------------------------------+--------------------------------------------------------------------------------------------+ -| ``-p``\ , ``--port`` | ``LITESTAR_PORT`` | Bind the server to this port [default: 8000] | -+-------------------------------------------+----------------------------------------------+--------------------------------------------------------------------------------------------+ -| ``--wc``\ , ``--web-concurrency`` | ``LITESTAR_WEB_CONCURRENCY`` | .. versionchanged:: 2.8 | -| | ``WEB_CONCURRENCY`` | ``LITESTAR_WEB_CONCURRENCY`` is supported and takes precedence over ``WEB_CONCURRENCY`` | -| | | | -| | | The number of concurrent web workers to start [default: 1] | -+-------------------------------------------+----------------------------------------------+--------------------------------------------------------------------------------------------+ -| ``-H``\ , ``--host`` | ``LITESTAR_HOST`` | Bind the server to this host [default: 127.0.0.1] | -+-------------------------------------------+----------------------------------------------+--------------------------------------------------------------------------------------------+ -| ``--fd``\ , ``--file-descriptor`` | ``LITESTAR_FILE_DESCRIPTOR`` | Bind to a socket from this file descriptor. | -+-------------------------------------------+----------------------------------------------+--------------------------------------------------------------------------------------------+ -| ``--uds``\ , ``--unix-domain-socket`` | ``LITESTAR_UNIX_DOMAIN_SOCKET`` | Bind to a UNIX domain socket. | -+-------------------------------------------+----------------------------------------------+--------------------------------------------------------------------------------------------+ -| ``-d``\ , ``--debug`` | ``LITESTAR_DEBUG`` | Run the application in debug mode | -+-------------------------------------------+----------------------------------------------+--------------------------------------------------------------------------------------------+ -| ``--pdb``\ , ``--use_pdb`` | ``LITESTAR_PDB`` | Drop into the Python debugger when an exception occurs | -+-------------------------------------------+----------------------------------------------+--------------------------------------------------------------------------------------------+ -| ``--ssl-certfile`` | ``LITESTAR_SSL_CERT_PATH`` | Path to a SSL certificate file | -+-------------------------------------------+----------------------------------------------+--------------------------------------------------------------------------------------------+ -| ``--ssl-keyfile`` | ``LITESTAR_SSL_KEY_PATH`` | Path to the private key to the SSL certificate | -+-------------------------------------------+----------------------------------------------+--------------------------------------------------------------------------------------------+ -| ``--create-self-signed-cert`` | ``LITESTAR_CREATE_SELF_SIGNED_CERT`` | If the SSL certificate and key are not found, generate a self-signed certificate | -+-------------------------------------------+----------------------------------------------+--------------------------------------------------------------------------------------------+ - ---reload-dir -++++++++++++ - -The ``--reload-dir`` flag allows you to specify directories to watch for changes. If you specify this flag, the ``--reload`` flag is implied. You can specify multiple directories by passing the flag multiple times: - -.. code-block:: shell - - litestar run --reload-dir=. --reload-dir=../other-library/src - -To set multiple directories via an environment variable, use a comma-separated list: - -.. code-block:: shell - - LITESTAR_RELOAD_DIRS=.,../other-library/src - ---reload-include -++++++++++++++++ - -The ``--reload-include`` flag allows you to specify glob patterns to include when watching for file changes. If you specify this flag, the ``--reload`` flag is implied. Furthermore, ``.py`` files are included implicitly by default. - -You can specify multiple glob patterns by passing the flag multiple times: - -.. code-block:: shell - - litestar run --reload-include="*.rst" --reload-include="*.yml" - -To set multiple directories via an environment variable, use a comma-separated list: - -.. code-block:: shell - - LITESTAR_RELOAD_INCLUDES=*.rst,*.yml - ---reload-exclude -++++++++++++++++ - -The ``--reload-exclude`` flag allows you to specify glob patterns to exclude when watching for file changes. If you specify this flag, the ``--reload`` flag is implied. - -You can specify multiple glob patterns by passing the flag multiple times: - -.. code-block:: shell - - litestar run --reload-exclude="*.py" --reload-exclude="*.yml" - -To set multiple directories via an environment variable, use a comma-separated list: - -.. code-block:: shell - - LITESTAR_RELOAD_EXCLUDES=*.py,*.yml - -SSL -+++ - -You can pass paths to an SSL certificate and it's private key to run the server using the HTTPS protocol: - -.. code-block:: shell - - litestar run --ssl-certfile=certs/cert.pem --ssl-keyfile=certs/key.pem - -Both flags must be provided and both files must exist. These are then passed to ``uvicorn``. -You can also use the ``--create-self-signed-cert`` flag: - -.. code-block:: shell - - litestar run --ssl-certfile=certs/cert.pem --ssl-keyfile=certs/key.pem --create-self-signed-cert - -This way, if the given files don't exist, a self-signed certificate and a passwordless key will be generated. -If the files are found, they will be reused. - -info -^^^^ - -The ``info`` command displays useful information about the selected application and its configuration. - -.. code-block:: shell - - litestar info - - -.. image:: /images/cli/litestar_info.png - :alt: litestar info - - -routes -^^^^^^ - -The ``routes`` command displays a tree view of the routing table. - -.. code-block:: shell - - litestar routes - -Options -~~~~~~~ - -+-----------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+ -| Flag | Description | -+=================+===========================================================================================================================================================+ -| ``--schema`` | Include default auto generated openAPI schema routes | -+-----------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``--exclude`` | Exclude endpoints from query with given regex patterns. Multiple excludes allowed. e.g., ``litestar routes --schema --exclude=routes/.* --exclude=[]`` | -+-----------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+ - - - - -.. image:: /images/cli/litestar_routes.png - :alt: litestar info - - -sessions -^^^^^^^^ - -This command and its subcommands provide management utilities for server-side session backends. - -delete -~~~~~~ - -The ``delete`` subcommand deletes a specific session from the backend. - -.. code-block:: shell - - litestar sessions delete cc3debc7-1ab6-4dc8-a220-91934a473717 - -clear -~~~~~ - -The `clear` subcommand is used to remove all sessions from the backend. - -.. code-block:: shell - - litestar sessions clear - -openapi -^^^^^^^ - -This command provides utilities to generate OpenAPI schemas and TypeScript types. - -schema -~~~~~~ - -The `schema` subcommand generates OpenAPI specifications from the Litestar application and serializes them as either -JSON or YAML. The serialization format depends on the filename, which is by default `openapi_schema.json`. You can -specify a different filename using the `--output` flag. For example: - -.. code-block:: shell - - litestar schema openapi --output my-specs.yml - -typescript -~~~~~~~~~~ - -The `typescript` subcommand generates TypeScript definitions from the Litestar application's OpenAPI specifications. -For example: - -.. code-block:: shell - - litestar schema typescript - -By default, this command outputs a file called `api-specs.ts`. You can change this using the `--output` option: - -.. code-block:: shell - - litestar schema typescript --output my-types.ts - -You can also specify the top-level TypeScript namespace that will be created, which is `API` by default: - -.. code-block:: typescript - - export namespace API { - // ... - } - -To do this, use the `--namespace` option: - -.. code-block:: shell - - litestar schema typescript --namespace MyNamespace - -This will result in: - -.. code-block:: typescript - - export namespace MyNamespace { - // ... - } +1. An :term:`object` named ``app`` that is an instance of :class:`~.app.Litestar` +2. An object named ``application`` that is an instance of :class:`~.app.Litestar` +3. Any object that is an instance of :class:`~.app.Litestar` +4. A :term:`callable` named ``create_app`` +5. A callable annotated to return an instance of :class:`~.app.Litestar` Extending the CLI ----------------- -Litestar's CLI is built with `click `_ and can be -extended by making use of +Litestar's CLI is built with `click `_ and can be extended by making use of `entry points `_, -or by creating a plugin that conforms to the -:class:`~litestar.plugins.CLIPluginProtocol`. +or by creating a plugin that conforms to the :class:`~.plugins.CLIPluginProtocol`. Using entry points ^^^^^^^^^^^^^^^^^^ @@ -344,16 +67,17 @@ entries should point to a :class:`click.Command` or :class:`click.Group`: .. tab-item:: setup.py .. code-block:: python + :caption: Using `setuptools `_ - from setuptools import setup + from setuptools import setup - setup( + setup( name="my-litestar-plugin", ..., entry_points={ "litestar.commands": ["my_command=my_litestar_plugin.cli:main"], }, - ) + ) .. tab-item:: pdm @@ -373,20 +97,18 @@ entries should point to a :class:`click.Command` or :class:`click.Group`: .. code-block:: toml :caption: Using `Poetry `_ - [tool.poetry.plugins."litestar.commands"] my_command = "my_litestar_plugin.cli:main" Using a plugin ^^^^^^^^^^^^^^ -A plugin extending the CLI can be created using the -:class:`~litestar.plugins.CLIPluginProtocol`. Its -:meth:`~litestar.plugins.CLIPluginProtocol.on_cli_init` will be called during the -initialization of the CLI, and receive the root :class:`click.Group` as its first -argument, which can then be used to add or override commands: +A plugin extending the CLI can be created using the :class:`~.plugins.CLIPluginProtocol`. +Its :meth:`~.plugins.CLIPluginProtocol.on_cli_init` will be called during the initialization of the CLI, +and receive the root :class:`click.Group` as its first argument, which can then be used to add or override commands: .. code-block:: python + :caption: Creating a CLI plugin from litestar import Litestar from litestar.plugins import CLIPluginProtocol @@ -402,7 +124,6 @@ argument, which can then be used to add or override commands: app = Litestar(plugins=[CLIPlugin()]) - Accessing the app instance ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -411,15 +132,24 @@ You can achieve this by adding the special ``app`` parameter to your CLI functio ``Litestar`` instance to be injected into the function whenever it is called from a click-context. .. code-block:: python + :caption: Accessing the app instance programmatically - import click - from litestar import Litestar + import click + from litestar import Litestar - @click.command() - def my_command(app: Litestar) -> None: ... + @click.command() + def my_command(app: Litestar) -> None: ... CLI Reference ------------- -For more information, visit the :doc:`Litestar CLI Click API Reference `. +The most up-to-date reference for the Litestar CLI can be found by running: + +.. code-block:: shell + :caption: Display the CLI help + + litestar --help + +You can also visit the :doc:`Litestar CLI Click API Reference ` for that same +information. diff --git a/docs/usage/custom-types.rst b/docs/usage/custom-types.rst index 4dad94e86a..05a1335bd5 100644 --- a/docs/usage/custom-types.rst +++ b/docs/usage/custom-types.rst @@ -23,6 +23,7 @@ Litestar supports a mechanism where you provide encoding and decoding hook funct Here is an example: .. literalinclude:: /examples/encoding_decoding/custom_type_encoding_decoding.py + :language: python :caption: Tell Litestar how to encode and decode a custom type Custom Pydantic types @@ -31,4 +32,5 @@ Custom Pydantic types If you use a custom Pydantic type you can use it directly: .. literalinclude:: /examples/encoding_decoding/custom_type_pydantic.py + :language: python :caption: Tell Litestar how to encode and decode a custom Pydantic type diff --git a/docs/usage/databases/sqlalchemy/models_and_repository.rst b/docs/usage/databases/sqlalchemy/models_and_repository.rst index d5dc0f1b77..c74604b1ef 100644 --- a/docs/usage/databases/sqlalchemy/models_and_repository.rst +++ b/docs/usage/databases/sqlalchemy/models_and_repository.rst @@ -36,7 +36,7 @@ implementations: * :class:`UUIDAuditBase ` Both include a ``UUID`` based primary key -and ``UUIDAuditBase`` includes an ``updated_at`` and ``created_at`` timestamp column. +and ``UUIDAuditBase`` includes ``updated_at`` and ``created_at`` timestamp columns. The ``UUID`` will be a native ``UUID``/``GUID`` type on databases that support it such as Postgres. For other engines without a native UUID data type, the UUID is stored as a 16-byte ``BYTES`` or ``RAW`` field. @@ -45,7 +45,7 @@ a native UUID data type, the UUID is stored as a 16-byte ``BYTES`` or ``RAW`` fi * :class:`BigIntAuditBase ` Both include a ``BigInteger`` based primary key -and ``BigIntAuditBase`` includes an ``updated_at`` and ``created_at`` timestamp column. +and ``BigIntAuditBase`` includes ``updated_at`` and ``created_at`` timestamp columns. Models using these bases also include the following enhancements: diff --git a/docs/usage/databases/sqlalchemy/plugins/sqlalchemy_init_plugin.rst b/docs/usage/databases/sqlalchemy/plugins/sqlalchemy_init_plugin.rst index 2ebd069f6a..581e39fe53 100644 --- a/docs/usage/databases/sqlalchemy/plugins/sqlalchemy_init_plugin.rst +++ b/docs/usage/databases/sqlalchemy/plugins/sqlalchemy_init_plugin.rst @@ -1,7 +1,7 @@ SQLAlchemy Init Plugin ---------------------- -The :class:`SQLAlchemyInitPlugin ` adds functionality to the +The :class:`SQLAlchemyInitPlugin ` adds functionality to the application that supports using Litestar with `SQLAlchemy `_. The plugin: @@ -39,8 +39,8 @@ Renaming the dependencies ######################### You can change the name that the engine and session are bound to by setting the -:attr:`engine_dependency_key ` -and :attr:`session_dependency_key ` +:attr:`engine_dependency_key ` +and :attr:`session_dependency_key ` attributes on the plugin configuration. Configuring the before send handler @@ -50,7 +50,7 @@ The plugin configures a ``before_send`` handler that is called before sending a session and removes it from the connection scope. You can change the handler by setting the -:attr:`before_send_handler ` +:attr:`before_send_handler ` attribute on the configuration object. For example, an alternate handler is available that will also commit the session on success and rollback upon failure. @@ -73,21 +73,21 @@ on success and rollback upon failure. Configuring the plugins ####################### -Both the :class:`SQLAlchemyAsyncConfig ` and the -:class:`SQLAlchemySyncConfig ` have an ``engine_config`` +Both the :class:`SQLAlchemyAsyncConfig ` and the +:class:`SQLAlchemySyncConfig ` have an ``engine_config`` attribute that is used to configure the engine. The ``engine_config`` attribute is an instance of -:class:`EngineConfig ` and exposes all of the configuration options +:class:`EngineConfig ` and exposes all of the configuration options available to the SQLAlchemy engine. -The :class:`SQLAlchemyAsyncConfig ` class and the -:class:`SQLAlchemySyncConfig ` class also have a +The :class:`SQLAlchemyAsyncConfig ` class and the +:class:`SQLAlchemySyncConfig ` class also have a ``session_config`` attribute that is used to configure the session. This is either an instance of -:class:`AsyncSessionConfig ` or -:class:`SyncSessionConfig ` depending on the type of config +:class:`AsyncSessionConfig ` or +:class:`SyncSessionConfig ` depending on the type of config object. These classes expose all of the configuration options available to the SQLAlchemy session. -Finally, the :class:`SQLAlchemyAsyncConfig ` class and the -:class:`SQLAlchemySyncConfig ` class expose configuration +Finally, the :class:`SQLAlchemyAsyncConfig ` class and the +:class:`SQLAlchemySyncConfig ` class expose configuration options to control their behavior. Consult the reference documentation for more information. @@ -98,7 +98,7 @@ Example The below example is a complete demonstration of use of the init plugin. Readers who are familiar with the prior section may note the additional complexity involved in managing the conversion to and from SQLAlchemy objects within the handlers. Read on to see how this increased complexity is efficiently handled by the -:class:`SQLAlchemySerializationPlugin `. +:class:`SQLAlchemySerializationPlugin `. .. tab-set:: diff --git a/docs/usage/databases/sqlalchemy/plugins/sqlalchemy_plugin.rst b/docs/usage/databases/sqlalchemy/plugins/sqlalchemy_plugin.rst index 8b0a702a85..c28b5e654b 100644 --- a/docs/usage/databases/sqlalchemy/plugins/sqlalchemy_plugin.rst +++ b/docs/usage/databases/sqlalchemy/plugins/sqlalchemy_plugin.rst @@ -1,18 +1,18 @@ SQLAlchemy Plugin ----------------- -The :class:`SQLAlchemyPlugin ` provides complete support for +The :class:`SQLAlchemyPlugin ` provides complete support for working with `SQLAlchemy `_ in Litestar applications. .. note:: This plugin is only compatible with SQLAlchemy 2.0+. -The :class:`SQLAlchemyPlugin ` combines the functionality of -:class:`SQLAlchemyInitPlugin ` and -:class:`SQLAlchemySerializationPlugin `, each of +The :class:`SQLAlchemyPlugin ` combines the functionality of +:class:`SQLAlchemyInitPlugin ` and +:class:`SQLAlchemySerializationPlugin `, each of which are examined in detail in the following sections. As such, this section describes a complete example of using the -:class:`SQLAlchemyPlugin ` with a Litestar application and a +:class:`SQLAlchemyPlugin ` with a Litestar application and a SQLite database. Or, skip ahead to :doc:`/usage/databases/sqlalchemy/plugins/sqlalchemy_init_plugin` or diff --git a/docs/usage/dto/0-basic-use.rst b/docs/usage/dto/0-basic-use.rst index 46693489e6..621e655df2 100644 --- a/docs/usage/dto/0-basic-use.rst +++ b/docs/usage/dto/0-basic-use.rst @@ -76,15 +76,14 @@ DTOs can similarly be defined on :class:`Routers ` and Improving performance with the codegen backend -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. note:: - This feature was introduced in ``2.2.0`` and hidden behind the ``DTO_CODEGEN`` - feature flag. As of ``2.8.0`` it is considered stable and enabled by default. It can - still be disabled selectively by using the - ``DTOConfig(experimental_codegen_backend=True)`` override. - + This feature was introduced in ``2.2.0`` and was hidden behind the ``DTO_CODEGEN`` + feature flag. As of ``2.8.0`` it is considered stable and is enabled by default. + It can still be disabled selectively by using the + ``DTOConfig(experimental_codegen_backend=False)`` override. The DTO backend is the part that does the heavy lifting for all the DTO features. It is responsible for the transforming, validation and parsing. Because of this, @@ -93,21 +92,11 @@ introduced by the DTOs, the DTO codegen backend was introduced; A DTO backend th increases efficiency by generating optimized Python code at runtime to perform all the necessary operations. -Enabling the backend --------------------- - -You can enable this backend globally for all DTOs by passing the appropriate feature -flag to your Litestar application: - -.. code-block:: python - - from litestar import Litestar - from litestar.config.app import ExperimentalFeatures - - app = Litestar(experimental_features=[ExperimentalFeatures.DTO_CODEGEN]) - +Disabling the backend +--------------------- -or selectively for individual DTOs: +You can use ``experimental_codegen_backend=False`` +to disable the codegen backend selectively: .. code-block:: python @@ -121,23 +110,61 @@ or selectively for individual DTOs: class FooDTO(DataclassDTO[Foo]): - config = DTOConfig(experimental_codegen_backend=True) + config = DTOConfig(experimental_codegen_backend=False) -The same flag can be used to disable the backend selectively: +Enabling the backend +-------------------- -.. code-block:: python +.. note:: This is a historical document meant for Litestar versions prior to 2.8.0 + This backend was enabled by default since 2.8.0 - from dataclasses import dataclass - from litestar.dto import DTOConfig, DataclassDTO +.. warning:: ``ExperimentalFeatures.DTO_CODEGEN`` is deprecated and will be removed in 3.0.0 +.. dropdown:: Enabling DTO codegen backend + :icon: git-pull-request-closed - @dataclass - class Foo: - name: str + You can enable this backend globally for all DTOs by passing the appropriate feature + flag to your Litestar application: + .. code-block:: python - class FooDTO(DataclassDTO[Foo]): - config = DTOConfig(experimental_codegen_backend=False) + from litestar import Litestar + from litestar.config.app import ExperimentalFeatures + + app = Litestar(experimental_features=[ExperimentalFeatures.DTO_CODEGEN]) + + + or selectively for individual DTOs: + + .. code-block:: python + + from dataclasses import dataclass + from litestar.dto import DTOConfig, DataclassDTO + + + @dataclass + class Foo: + name: str + + + class FooDTO(DataclassDTO[Foo]): + config = DTOConfig(experimental_codegen_backend=True) + + The same flag can be used to disable the backend selectively: + + .. code-block:: python + + from dataclasses import dataclass + from litestar.dto import DTOConfig, DataclassDTO + + + @dataclass + class Foo: + name: str + + + class FooDTO(DataclassDTO[Foo]): + config = DTOConfig(experimental_codegen_backend=False) Performance improvements diff --git a/docs/usage/dto/1-abstract-dto.rst b/docs/usage/dto/1-abstract-dto.rst index 72f99577a8..f7aed1cd6d 100644 --- a/docs/usage/dto/1-abstract-dto.rst +++ b/docs/usage/dto/1-abstract-dto.rst @@ -10,7 +10,7 @@ The following factories are currently available: - :class:`DataclassDTO ` - :class:`MsgspecDTO ` -- :class:`PydanticDTO ` +- :class:`PydanticDTO ` - :class:`SQLAlchemyDTO ` Using DTO Factories @@ -50,6 +50,8 @@ Fields marked as ``"private"`` or ``"read-only"`` will not be parsed from client :emphasize-lines: 6,14,15 :linenos: +Note that ``id`` field is the primary key and is handled specially by the defined SQLAlchemy base. + .. note: The procedure for "marking" a model field will vary depending on the library. For example, @@ -118,7 +120,7 @@ Fields can also be renamed using a renaming strategy that will be applied to all Fields that are directly renamed using `rename_fields` mapping will be excluded from `rename_strategy`. -The rename strategy either accepts one of the pre-defined strategies: "camel", "pascal", "upper", "lower", or it can be provided a callback that accepts the field name as an argument and should return a string. +The rename strategy either accepts one of the pre-defined strategies: "camel", "pascal", "upper", "lower", "kebab", or it can be provided a callback that accepts the field name as a string argument and should return a string. Type checking ------------- @@ -182,21 +184,23 @@ DTO Data Sometimes we need to be able to access the data that has been parsed and validated by the DTO, but not converted into an instance of our data model. -In the following example, we create a ``Person`` model, that is a :func:`dataclass ` with 3 -required fields, ``id``, ``name``, and ``age``. +In the following example, we create a ``User`` model, that is a :func:`dataclass ` with 3 +required fields: ``id``, ``name``, and ``age``. -We also create a DTO that doesn't allow clients to set the ``id`` field on the ``Person`` model and set it on the +We also create a DTO that doesn't allow clients to set the ``id`` field on the ``User`` model and set it on the handler. .. literalinclude:: /examples/data_transfer_objects/factory/dto_data_problem_statement.py :language: python - :emphasize-lines: 19,20,21,22,28 + :emphasize-lines: 18-21,27 :linenos: -Notice that we get a ``500`` response from the handler - this is because the DTO has attempted to convert the request -data into a ``Person`` object and failed because it has no value for the required ``id`` field. +Notice that our `User` model has a model-level ``default_factory=uuid4`` +for ``id`` field. That's why we can decode the client data into this model. + +However, in some cases there's no clear way to provide a default this way. -One way to handle this is to create different models, e.g., we might create a ``CreatePerson`` model that has no ``id`` +One way to handle this is to create different models, e.g., we might create a ``UserCreate`` model that has no ``id`` field, and decode the client data into that. However, this method can become quite cumbersome when we have a lot of variability in the data that we accept from clients, for example, `PATCH `_ requests. @@ -206,11 +210,11 @@ type of the data that it will contain, and provides useful methods for interacti .. literalinclude:: /examples/data_transfer_objects/factory/dto_data_usage.py :language: python - :emphasize-lines: 7,25,27 + :emphasize-lines: 5,23,25 :linenos: In the above example, we've injected an instance of :class:`DTOData ` into our handler, -and have used that to create our ``Person`` instance, after augmenting the client data with a server generated ``id`` +and have used that to create our ``User`` instance, after augmenting the client data with a server generated ``id`` value. Consult the :class:`Reference Docs ` for more information on the methods available. @@ -228,7 +232,7 @@ nested model with excluded fields. .. literalinclude:: /examples/data_transfer_objects/factory/providing_values_for_nested_data.py :language: python - :emphasize-lines: 10,11,12,13,21,29,35 + :emphasize-lines: 9-12,20,28,34 :linenos: The double-underscore syntax ``address__id`` passed as a keyword argument to the @@ -237,7 +241,7 @@ nested attribute. In this case, it's used to provide a value for the ``id`` attr within the ``Person`` instance. This is a common convention in Python for dealing with nested structures. The double underscore can be interpreted as -"traverse through", so ``address__id`` means "traverse through address to get to id". +"traverse through", so ``address__id`` means "traverse through address to get to its id". In the context of this script, ``create_instance(id=1, address__id=2)`` is saying "create a new ``Person`` instance from the client data given an id of ``1``, and supplement the client address data with an id of ``2``". @@ -251,17 +255,17 @@ attributes in the client payload, which requires some special handling internall .. literalinclude:: /examples/data_transfer_objects/factory/patch_requests.py :language: python - :emphasize-lines: 7,21,32,34 + :emphasize-lines: 7,20,27,28,30 :linenos: -The ``PatchDTO`` class is defined for the Person class. The ``config`` attribute of ``PatchDTO`` is set to exclude the -id field, preventing clients from setting it when updating a person, and the ``partial`` attribute is set to ``True``, +The ``PatchDTO`` class is defined for the ``Person`` class. The ``config`` attribute of ``PatchDTO`` is set to exclude the +``id`` field, preventing clients from setting it when updating a person, and the ``partial`` attribute is set to ``True``, which allows the DTO to accept a subset of the model attributes. Inside the handler, the :meth:`DTOData.update_instance ` method is called to update the instance of ``Person`` before returning it. -In our request, we set only the ``name`` property of the ``Person``, from ``"Peter"`` to ``"Peter Pan"`` and received +In our request, we update only the ``name`` property of the ``Person``, from ``"Peter"`` to ``"Peter Pan"`` and receive the full object - with the modified name - back in the response. Implicit Private Fields diff --git a/docs/usage/exceptions.rst b/docs/usage/exceptions.rst index 956a29620c..2f3cc6a83c 100644 --- a/docs/usage/exceptions.rst +++ b/docs/usage/exceptions.rst @@ -2,20 +2,20 @@ Exceptions and exception handling ================================= Litestar define a base exception called :class:`LitestarException ` which serves -as a basis to all other exceptions. +as a base class for all other exceptions, see :mod:`API Reference `. -In general, Litestar will raise two types of exceptions: +In general, Litestar has two scenarios for exception handling: -- Exceptions that arise during application init, which fall -- Exceptions that are raised as part of the normal application flow, i.e. - exceptions in route handlers, dependencies, and middleware, that should be serialized in some fashion. +- Exceptions that are raised during application configuration, startup, and initialization, which are handled like regular Python exceptions +- Exceptions that are raised as part of the request handling, i.e. + exceptions in route handlers, dependencies, and middleware, that should be returned as a response to the end user Configuration Exceptions ------------------------ For missing extra dependencies, Litestar will raise either :class:`MissingDependencyException `. For example, if you try to use the -:doc:`SQLAlchemyPlugin ` without having SQLAlchemy installed, this will be raised when you +:ref:`SQLAlchemyPlugin ` without having SQLAlchemy installed, this will be raised when you start the application. For other configuration issues, Litestar will raise @@ -25,8 +25,8 @@ issue. Application Exceptions ---------------------- -For application exceptions, Litestar uses the class :class:`HTTPException <.exceptions.http_exceptions.HTTPException>`, -which inherits from :class:`LitestarException <.exceptions.LitestarException>`. This exception will be serialized +For application exceptions, Litestar uses the class :class:`~litestar.exceptions.http_exceptions.HTTPException`, +which inherits from :class:`~litestar.exceptions.LitestarException`. This exception will be serialized into a JSON response of the following schema: .. code-block:: json @@ -37,10 +37,10 @@ into a JSON response of the following schema: "extra": {} } -Litestar also offers several pre-configured exception subclasses with pre-set error codes that you can use, such as: +Litestar also offers several pre-configured ``HTTPException`` subclasses with pre-set error codes that you can use, such as: -.. py:currentmodule:: litestar.exceptions.http_exceptions +.. :currentmodule:: litestar.exceptions.http_exceptions +----------------------------------------+-------------+------------------------------------------+ | Exception | Status code | Description | @@ -49,26 +49,30 @@ Litestar also offers several pre-configured exception subclasses with pre-set er +----------------------------------------+-------------+------------------------------------------+ | :class:`ValidationException` | 400 | Raised when validation or parsing failed | +----------------------------------------+-------------+------------------------------------------+ -| :class:`NotFoundException` | 404 | HTTP status code 404 | -+----------------------------------------+-------------+------------------------------------------+ | :class:`NotAuthorizedException` | 401 | HTTP status code 401 | +----------------------------------------+-------------+------------------------------------------+ | :class:`PermissionDeniedException` | 403 | HTTP status code 403 | +----------------------------------------+-------------+------------------------------------------+ +| :class:`NotFoundException` | 404 | HTTP status code 404 | ++----------------------------------------+-------------+------------------------------------------+ | :class:`InternalServerException` | 500 | HTTP status code 500 | +----------------------------------------+-------------+------------------------------------------+ | :class:`ServiceUnavailableException` | 503 | HTTP status code 503 | +----------------------------------------+-------------+------------------------------------------+ -When a value fails ``pydantic`` validation, the result will be a :class:`ValidationException` with the ``extra`` key set to the -pydantic validation errors. Thus, this data will be made available for the API consumers by default. +.. :currentmodule:: None + +When a value fails validation, the result will be a :class:`~litestar.exceptions.http_exceptions.ValidationException` with the ``extra`` key set to the validation error message. + +.. warning:: All validation error messages will be made available for the API consumers by default. + If this is not your intent, adjust the exception contents. Exception handling ------------------ Litestar handles all errors by default by transforming them into **JSON responses**. If the errors are **instances of** -:class:`HTTPException`, the responses will include the appropriate ``status_code``. +:class:`~litestar.exceptions.http_exceptions.HTTPException`, the responses will include the appropriate ``status_code``. Otherwise, the responses will default to ``500 - "Internal Server Error"``. You can customize exception handling by passing a dictionary, mapping either status codes @@ -89,14 +93,6 @@ exceptions that inherit from ``HTTPException``. You could of course be more gran The choice whether to use a single function that has switching logic inside it, or multiple functions depends on your specific needs. -While it does not make much sense to have different functions with a top-level exception handling, -Litestar supports defining exception handlers on all layers of the app, with the lower layers overriding layer above -them. In the following example, the exception handler for the route handler function will only handle -the ``ValidationException`` occurring within that route handler: - -.. literalinclude:: /examples/exceptions/layered_handlers.py - :language: python - Exception handling layers ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -116,3 +112,10 @@ As a result of the above structure, the exceptions raised by the ASGI Router its and ``405 Method Not Allowed`` are handled only by exception handlers defined on the app layer. Thus, if you want to affect these exceptions, you will need to pass the exception handlers for them to the Litestar constructor and cannot use other layers for this purpose. + +Litestar supports defining exception handlers on all layers of the app, with the lower layers overriding layer above +them. In the following example, the exception handler for the route handler function will only handle +the ``ValidationException`` occurring within that route handler: + +.. literalinclude:: /examples/exceptions/layered_handlers.py + :language: python diff --git a/docs/usage/htmx.rst b/docs/usage/htmx.rst index 966b166d64..7dd2b28374 100644 --- a/docs/usage/htmx.rst +++ b/docs/usage/htmx.rst @@ -1,18 +1,50 @@ HTMX ==== -Litestar HTMX integration. +Litestar `HTMX `_ integration. + +HTMX is a JavaScript library that gives you access to AJAX, CSS Transitions, WebSockets and Server Sent Events directly in HTML, using attributes, so you can build modern user interfaces with the simplicity and power of hypertext. + +This section assumes that you have prior knowledge of HTMX. +If you want to learn HTMX, we recommend consulting their `official tutorial `_. + +HTMXPlugin +------------ + +a Litestar plugin ``HTMXPlugin`` is available to easily configure the default request class for all Litestar routes. + +.. code-block:: python + + from litestar.plugins.htmx import HTMXPlugin + from litestar import Litestar + + from litestar.contrib.jinja import JinjaTemplateEngine + from litestar.template.config import TemplateConfig + + from pathlib import Path + + app = Litestar( + route_handlers=[get_form], + debug=True, + plugins=[HTMXPlugin()], + template_config=TemplateConfig( + directory=Path("litestar_htmx/templates"), + engine=JinjaTemplateEngine, + ), + ) + +See :class:`~litestar.plugins.htmx.HTMXDetails` for a full list of +available properties. HTMXRequest ------------ A special :class:`~litestar.connection.Request` class, providing interaction with the -HTMX client. +HTMX client. You can configure this globally by using the ``HTMXPlugin`` or by setting the `request_class` setting on any route, controller, router, or application. .. code-block:: python - from litestar.contrib.htmx.request import HTMXRequest - from litestar.contrib.htmx.response import HTMXTemplate + from litestar.plugins.htmx import HTMXRequest, HTMXTemplate from litestar import get, Litestar from litestar.response import Template @@ -24,11 +56,8 @@ HTMX client. @get(path="/form") def get_form(request: HTMXRequest) -> Template: - htmx = request.htmx # if true will return HTMXDetails class object - if htmx: - print(htmx.current_url) - # OR - if request.htmx: + if request.htmx: # if request has "HX-Request" header, then + print(request.htmx) # HTMXDetails instance print(request.htmx.current_url) return HTMXTemplate(template_name="partial.html", context=context, push_url="/form") @@ -43,7 +72,7 @@ HTMX client. ), ) -See :class:`HTMXDetails ` for a full list of +See :class:`~litestar.plugins.htmx.HTMXDetails` for a full list of available properties. @@ -54,12 +83,12 @@ HTMX Response Classes HTMXTemplate Response Classes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The most common use-case for `htmx` to render an html page or html snippet. Litestar makes this easy by providing -an :class:`HTMXTemplate ` response: +The most common use-case for HTMX to render an html page or html snippet. Litestar makes this easy by providing +an :class:`~litestar.plugins.htmx.HTMXTemplate` response: .. code-block:: python - from litestar.contrib.htmx.response import HTMXTemplate + from litestar.plugins.htmx import HTMXTemplate from litestar.response import Template @@ -89,10 +118,10 @@ an :class:`HTMXTemplate ` response: HTMX provides two types of responses - one that doesn't allow changes to the DOM and one that does. Litestar supports both of these: -1 - Responses that don't make any changes to DOM. -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +1 - Responses that don't make any changes to DOM +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Use :class:`HXStopPolling ` to stop polling for a response. +Use :class:`~litestar.plugins.htmx.HXStopPolling` to stop polling for a response. .. code-block:: python @@ -101,7 +130,7 @@ Use :class:`HXStopPolling ` to sto ... return HXStopPolling() -Use :class:`ClientRedirect ` to redirect with a page reload. +Use :class:`~litestar.plugins.htmx.ClientRedirect` to redirect with a page reload. .. code-block:: python @@ -110,7 +139,7 @@ Use :class:`ClientRedirect ` to ... return ClientRedirect(redirect_to="/contact-us") -Use :class:`ClientRefresh ` to force a full page refresh. +Use :class:`~litestar.plugins.htmx.ClientRefresh` to force a full page refresh. .. code-block:: python @@ -119,12 +148,12 @@ Use :class:`ClientRefresh ` to fo ... return ClientRefresh() -2 - Responses that may change DOM. -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +2 - Responses that may change DOM +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Use :class:`HXLocation ` to redirect to a new location without page reload. +Use :class:`~litestar.plugins.htmx.HXLocation` to redirect to a new location without page reload. -- Note: this class provides the ability to change ``target``, ``swapping`` method, the sent ``values``, and the ``headers``.) +.. note:: This class provides the ability to change ``target``, ``swapping`` method, the sent ``values``, and the ``headers``. .. code-block:: python @@ -138,13 +167,13 @@ Use :class:`HXLocation ` to redirect event, # an event that "triggered" the request. target="#target", # element id to target to. swap="outerHTML", # swapping method to use. - hx_headers={"attr": "val"}, # headers to pass to htmx. + hx_headers={"attr": "val"}, # headers to pass to HTMX. values={"val": "one"}, ) # values to submit with response. -Use :class:`PushUrl ` to carry a response and push a url to the browser, optionally updating the `history` stack. +Use :class:`~litestar.plugins.htmx.PushUrl` to carry a response and push a url to the browser, optionally updating the ``history`` stack. -- Note: If the value for ``push_url`` is set to ``False`` it will prevent updating browser history. +.. note:: If the value for ``push_url`` is set to ``False`` it will prevent updating browser history. .. code-block:: python @@ -153,8 +182,9 @@ Use :class:`PushUrl ` to carry a respons ... return PushUrl(content="Success!", push_url="/about") -Use :class:`ReplaceUrl ` to carry a response and replace the url in the browser's ``location`` bar. -- Note: If the value to ``replace_url`` is set to ``False`` it will prevent it updating the browser location bar. +Use :class:`~litestar.plugins.htmx.ReplaceUrl` to carry a response and replace the url in the browser's ``location`` bar. + +.. note:: If the value to ``replace_url`` is set to ``False`` it will prevent updating the browser's location. .. code-block:: python @@ -163,7 +193,7 @@ Use :class:`ReplaceUrl ` to carry a r ... return ReplaceUrl(content="Success!", replace_url="/contact-us") -Use :class:`Reswap ` to carry a response perhaps a swap +Use :class:`~litestar.plugins.htmx.Reswap` to carry a response with a possible swap. .. code-block:: python @@ -172,7 +202,7 @@ Use :class:`Reswap ` to carry a response ... return Reswap(content="Success!", method="beforebegin") -Use :class:`Retarget ` to carry a response and change the target element. +Use :class:`~litestar.plugins.htmx.Retarget` to carry a response and change the target element. .. code-block:: python @@ -181,7 +211,7 @@ Use :class:`Retarget ` to carry a respo ... return Retarget(content="Success!", target="#new-target") -Use :class:`TriggerEvent ` to carry a response and trigger an event. +Use :class:`~litestar.plugins.htmx.TriggerEvent` to carry a response and trigger an event. .. code-block:: python diff --git a/docs/usage/lifecycle-hooks.rst b/docs/usage/lifecycle-hooks.rst index 0e2e0fdb25..cd02833f81 100644 --- a/docs/usage/lifecycle-hooks.rst +++ b/docs/usage/lifecycle-hooks.rst @@ -20,7 +20,7 @@ Before Request -------------- The ``before_request`` hook runs immediately before calling the route handler function. It -can be any callable accepting a :class:`Request <.connection.Request>` as its first parameter +can be any callable accepting a :class:`~litestar.connection.Request` as its first parameter and returns either ``None`` or a value that can be used in a response. If a value is returned, the router handler for this request will be bypassed. @@ -34,7 +34,7 @@ After Request ------------- The ``after_request`` hook runs after the route handler returned and the response object -has been resolved. It can be any callable which takes a :class:`Response <.response.Response>` +has been resolved. It can be any callable which takes a :class:`~litestar.response.Response` instance as its first parameter, and returns a ``Response`` instance. The ``Response`` instance returned does not necessarily have to be the one that was received. @@ -48,7 +48,7 @@ After Response -------------- The ``after_response`` hook runs after the response has been returned by the server. -It can be any callable accepting a :class:`Request <.connection.Request>` as its first parameter +It can be any callable accepting a :class:`~litestar.connection.Request` as its first parameter and does not return any value. This hook is meant for data post-processing, transmission of data to third party @@ -60,8 +60,8 @@ services, gathering of metrics, etc. .. note:: - Since the request has already been returned by the time the `after_response` is called, - the updated state of `COUNTER` is not reflected in the response. + Since the request has already been returned by the time the ``after_response`` is called, + the updated state of ``COUNTER`` is not reflected in the response. Layered hooks diff --git a/docs/usage/logging.rst b/docs/usage/logging.rst index c0d2ad46c3..c1b42bbc70 100644 --- a/docs/usage/logging.rst +++ b/docs/usage/logging.rst @@ -1,3 +1,5 @@ +.. _logging-usage: + Logging ======= diff --git a/docs/usage/metrics/prometheus.rst b/docs/usage/metrics/prometheus.rst index 766de0a2fb..49db6555a0 100644 --- a/docs/usage/metrics/prometheus.rst +++ b/docs/usage/metrics/prometheus.rst @@ -1,7 +1,7 @@ Prometheus ========== -Litestar includes optional Prometheus exporter that is exported from ``litestar.contrib.prometheus``. To use +Litestar includes optional Prometheus exporter that is exported from ``litestar.plugins.prometheus``. To use this package, you should first install the required dependencies: .. code-block:: bash @@ -17,12 +17,12 @@ this package, you should first install the required dependencies: Once these requirements are satisfied, you can instrument your Litestar application: -.. literalinclude:: /examples/contrib/prometheus/using_prometheus_exporter.py +.. literalinclude:: /examples/plugins/prometheus/using_prometheus_exporter.py :language: python :caption: Using the Prometheus Exporter You can also customize the configuration: -.. literalinclude:: /examples/contrib/prometheus/using_prometheus_exporter_with_extra_configs.py +.. literalinclude:: /examples/plugins/prometheus/using_prometheus_exporter_with_extra_configs.py :language: python :caption: Configuring the Prometheus Exporter diff --git a/docs/usage/middleware/builtin-middleware.rst b/docs/usage/middleware/builtin-middleware.rst index e95102efcf..03ca6413a5 100644 --- a/docs/usage/middleware/builtin-middleware.rst +++ b/docs/usage/middleware/builtin-middleware.rst @@ -6,7 +6,7 @@ CORS `CORS (Cross-Origin Resource Sharing) `_ is a common security mechanism that is often implemented using middleware. To enable CORS in a litestar application simply pass an instance -of :class:`CORSConfig <.config.cors.CORSConfig>` to :class:`Litestar <.app.Litestar>`: +of :class:`~litestar.config.cors.CORSConfig` to :class:`~litestar.app.Litestar`: .. code-block:: python @@ -21,7 +21,7 @@ of :class:`CORSConfig <.config.cors.CORSConfig>` to :class:`Litestar <.app.Lites CSRF ---- -CSRF (Cross-site request forgery) is a type of attack where unauthorized commands are submitted from a user that the web +`CSRF (Cross-site request forgery) `_ is a type of attack where unauthorized commands are submitted from a user that the web application trusts. This attack often uses social engineering that tricks the victim into clicking a URL that contains a maliciously crafted, unauthorized request for a particular Web application. The user’s browser then sends this maliciously crafted request to the targeted Web application. If the user is in an active session with the Web application, @@ -45,7 +45,7 @@ This middleware prevents CSRF attacks by doing the following: form field or an additional header that has this token (more on this below) To enable CSRF protection in a Litestar application simply pass an instance of -:class:`CSRFConfig <.config.csrf.CSRFConfig>` to the Litestar constructor: +:class:`~litestar.config.csrf.CSRFConfig` to the Litestar constructor: .. code-block:: python @@ -68,7 +68,7 @@ To enable CSRF protection in a Litestar application simply pass an instance of app = Litestar([get_resource, create_resource], csrf_config=csrf_config) -The following snippet demonstrates how to change the cookie name to "some-cookie-name" and header name to "some-header-name". +The following snippet demonstrates how to change the cookie name to ``"some-cookie-name"`` and header name to ``"some-header-name"``. .. code-block:: python @@ -80,11 +80,11 @@ A CSRF protected route can be accessed by any client that can make a request wit .. note:: - The form-data key can not be currently configured. It should only be passed via the key "_csrf_token" + The form-data key can not be currently configured. It should only be passed via the key ``"_csrf_token"`` In Python, any client such as `requests `_ or `httpx `_ can be used. The usage of clients or sessions is recommended due to the cookie persistence it offers across requests. -The following is an example using ``httpx.Client``. +The following is an example using `httpx.Client `_. .. code-block:: python @@ -98,7 +98,7 @@ The following is an example using ``httpx.Client``. csrf = get_response.cookies["csrftoken"] # "x-csrftoken" is the default header name - post_response_using_header = client.post("http://localhost:8000/", headers={"x-csrftoken": csrf}) + post_response_using_header = client.post("http://localhost:8000/1", headers={"x-csrftoken": csrf}) assert post_response_using_header.status_code == 201 # "_csrf_token" is the default *non* configurable form-data key @@ -121,7 +121,7 @@ Routes can be marked as being exempt from the protection offered by this middlew If you need to exempt many routes at once you might want to consider using the -:attr:`exclude <.config.csrf.CSRFConfig.exclude>` kwarg which accepts list of path +:attr:`~litestar.config.csrf.CSRFConfig.exclude` kwarg which accepts list of path patterns to skip in the middleware. .. seealso:: @@ -134,12 +134,12 @@ patterns to skip in the middleware. Allowed Hosts ------------- -Another common security mechanism is to require that each incoming request has a "Host" or "X-Forwarded-Host" header, +Another common security mechanism is to require that each incoming request has a ``"Host"`` or ``"X-Forwarded-Host"`` header, and then to restrict hosts to a specific set of domains - what's called "allowed hosts". -Litestar includes an :class:`AllowedHostsMiddleware <.middleware.allowed_hosts.AllowedHostsMiddleware>` class that can be -easily enabled by either passing an instance of :class:`AllowedHostsConfig <.config.allowed_hosts.AllowedHostsConfig>` or a -list of domains to :class:`Litestar `: +Litestar includes an :class:`~litestar.middleware.allowed_hosts.AllowedHostsMiddleware` class that can be +easily enabled by either passing an instance of :class:`~litestar.config.allowed_hosts.AllowedHostsConfig` or a +list of domains to :class:`~litestar.app.Litestar`: .. code-block:: python @@ -169,13 +169,13 @@ HTML responses can optionally be compressed. Litestar has built in support for g through the built-in Starlette classes, and brotli support can be added by installing the ``brotli`` extras. You can enable either backend by passing an instance of -:class:`CompressionConfig <.config.compression.CompressionConfig>` to ``compression_config`` of -:class:`Litestar `. +:class:`~litestar.config.compression.CompressionConfig` to ``compression_config`` of +:class:`~litestar.app.Litestar`. GZIP ^^^^ -You can enable gzip compression of responses by passing an instance of :class:`CompressionConfig <.config.compression.CompressionConfig>` with +You can enable gzip compression of responses by passing an instance of :class:`~litestar.config.compression.CompressionConfig` with the ``backend`` parameter set to ``"gzip"``. You can configure the following additional gzip-specific values: @@ -199,25 +199,25 @@ You can configure the following additional gzip-specific values: Brotli ^^^^^^ -The Brotli package is required to run this middleware. It is available as an extras to litestar with the ``brotli`` +The `Brotli `_ package is required to run this middleware. It is available as an extras to litestar with the ``brotli`` extra (``pip install litestar[brotli]``). You can enable brotli compression of responses by passing an instance of -:class:`CompressionConfig <.config.compression.CompressionConfig>` with the ``backend`` parameter set to ``"brotli"``. +:class:`~litestar.config.compression.CompressionConfig` with the ``backend`` parameter set to ``"brotli"``. You can configure the following additional brotli-specific values: * ``minimum_size``: the minimum threshold for response size to enable compression. Smaller responses will not be - compressed. Defaults is ``500``, i.e. half a kilobyte. + compressed. Default is 500, i.e. half a kilobyte * ``brotli_quality``: Range [0-11], Controls the compression-speed vs compression-density tradeoff. The higher the - quality, the slower the compression. -* ``brotli_mode``: The compression mode can be MODE_GENERIC (default), MODE_TEXT (for UTF-8 format text input), or - MODE_FONT (for WOFF 2.0). -* ``brotli_lgwin``: Base 2 logarithm of size. Range is 10 to 24. Defaults to 22. -* ``brotli_lgblock``: Base 2 logarithm of the maximum input block size. Range is 16 to 24. If set to 0, the value will - be set based on the quality. Defaults to 0. -* ``brotli_gzip_fallback``: a boolean to indicate if gzip should be used if brotli is not supported. + quality, the slower the compression. Defaults to 5 +* ``brotli_mode``: The compression mode can be ``"generic"`` (for mixed content), ``"text"`` (for UTF-8 format text input), or + ``"font"`` (for WOFF 2.0). Defaults to ``"text"`` +* ``brotli_lgwin``: Base 2 logarithm of size. Range [10-24]. Defaults to 22. +* ``brotli_lgblock``: Base 2 logarithm of the maximum input block size. Range [16-24]. If set to 0, the value will + be set based on the quality. Defaults to 0 +* ``brotli_gzip_fallback``: a boolean to indicate if gzip should be used if brotli is not supported .. code-block:: python @@ -232,31 +232,30 @@ You can configure the following additional brotli-specific values: Rate-Limit Middleware --------------------- -Litestar includes an optional :class:`RateLimitMiddleware ` that follows +Litestar includes an optional :class:`~litestar.middleware.rate_limit.RateLimitMiddleware` that follows the `IETF RateLimit draft specification `_. -To use the rate limit middleware, use the :class:`RateLimitConfig `: +To use the rate limit middleware, use the :class:`~litestar.middleware.rate_limit.RateLimitConfig`: .. literalinclude:: /examples/middleware/rate_limit.py :language: python -The only required configuration kwarg is ``rate_limit``, which expects a tuple containing a time-unit (``second``, -``minute``, ``hour``, ``day``\ ) and a value for the request quota (integer). +The only required configuration kwarg is ``rate_limit``, which expects a tuple containing a time-unit (``"second"``, +``"minute"``, ``"hour"``, ``"day"``\ ) and a value for the request quota (integer). Logging Middleware ------------------ Litestar ships with a robust logging middleware that allows logging HTTP request and responses while building on -the :doc:`logging configuration `: +the Litestar's :ref:`logging configuration `: .. literalinclude:: /examples/middleware/logging_middleware.py :language: python -The logging middleware uses the logger configuration defined on the application level, which allows for using both stdlib -logging or `structlog `_ , depending on the configuration used -(see :doc:`logging configuration ` for more details). +The logging middleware uses the logger configuration defined on the application level, which allows for using any supported logging tool, depending on the configuration used +(see :ref:`logging configuration ` for more details). Obfuscating Logging Output ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -281,18 +280,18 @@ The middleware will obfuscate the headers ``Authorization`` and ``X-API-KEY`` , Compression and Logging of Response Body ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -If both :class:`CompressionConfig ` and -:class:`LoggingMiddleware ` have been defined for the application, the response +If both :class:`~litestar.config.compression.CompressionConfig` and +:class:`~litestar.middleware.logging.LoggingMiddleware` have been defined for the application, the response body will be omitted from response logging if it has been compressed, even if ``"body"`` has been included in -:class:`response_log_fields `. To force the body of +:class:`~litestar.middleware.logging.LoggingMiddlewareConfig.response_log_fields`. To force the body of compressed responses to be logged, set -:attr:`include_compressed_body ` to ``True`` , in +:attr:`~litestar.middleware.logging.LoggingMiddlewareConfig.include_compressed_body` to ``True`` , in addition to including ``"body"`` in ``response_log_fields``. Session Middleware ------------------ -Litestar includes a :class:`SessionMiddleware <.middleware.session.base.SessionMiddleware>`, +Litestar includes a :class:`~litestar.middleware.session.base.SessionMiddleware`, offering client- and server-side sessions. Server-side sessions are backed by Litestar's :doc:`stores `, which offer support for: @@ -316,12 +315,12 @@ add its middleware to your application's middleware stack: Since both client- and server-side sessions rely on cookies (one for storing the actual session data, the other for storing the session ID), they share most of the cookie configuration. - A complete reference of the cookie configuration can be found at :class:`BaseBackendConfig `. + A complete reference of the cookie configuration can be found at :class:`~litestar.middleware.session.base.BaseBackendConfig`. Client-side sessions ^^^^^^^^^^^^^^^^^^^^ -Client side sessions are available through the :class:`ClientSideSessionBackend `, +Client side sessions are available through the :class:`~litestar.middleware.session.client_side.ClientSideSessionBackend`, which offers strong AES-CGM encryption security best practices while support cookie splitting. .. important:: @@ -336,7 +335,7 @@ which offers strong AES-CGM encryption security best practices while support coo .. seealso:: - * :class:`CookieBackendConfig ` + * :class:`~litestar.middleware.session.client_side.CookieBackendConfig` Server-side sessions @@ -352,4 +351,4 @@ and load the appropriate data from the store .. seealso:: * :doc:`/usage/stores` - * :class:`ServerSideSessionConfig ` + * :class:`~litestar.middleware.session.server_side.ServerSideSessionConfig` diff --git a/docs/usage/middleware/creating-middleware.rst b/docs/usage/middleware/creating-middleware.rst index f9824b27c6..bc111194c0 100644 --- a/docs/usage/middleware/creating-middleware.rst +++ b/docs/usage/middleware/creating-middleware.rst @@ -2,9 +2,9 @@ Creating Middleware =================== -As mentioned in :doc:`using middleware `, a middleware in Litestar +As mentioned in :ref:`using middleware `, a middleware in Litestar is **any callable** that takes a kwarg called ``app``, which is the next ASGI handler, i.e. an -:class:`ASGIApp `, and returns an ``ASGIApp``. +:class:`~litestar.types.ASGIApp`, and returns an ``ASGIApp``. The example previously given was using a factory function, i.e.: @@ -22,14 +22,14 @@ The example previously given was using a factory function, i.e.: return my_middleware While using functions is a perfectly viable approach, you can also use classes to do the same. See the next sections on -two base classes you can use for this purpose - the :class:`MiddlewareProtocol <.middleware.base.MiddlewareProtocol>` , -which gives a bare-bones type, or the :class:`AbstractMiddleware <.middleware.base.AbstractMiddleware>` that offers a +two base classes you can use for this purpose - the :class:`~litestar.middleware.base.MiddlewareProtocol` , +which gives a bare-bones type, or the :class:`~litestar.middleware.base.AbstractMiddleware` that offers a base class with some built in functionality. Using MiddlewareProtocol ------------------------ -The :class:`MiddlewareProtocol ` class is a +The :class:`~litestar.middleware.base.MiddlewareProtocol` class is a `PEP 544 Protocol `_ that specifies the minimal implementation of a middleware as follows: @@ -50,7 +50,7 @@ this case, but rather the next middleware in the stack, which is also an ASGI ap The ``__call__`` method makes this class into a ``callable``, i.e. once instantiated this class acts like a function, that has the signature of an ASGI app: The three parameters, ``scope, receive, send`` are specified by `the ASGI specification `_, and their values originate with the ASGI -server (e.g. *uvicorn*\ ) used to run Litestar. +server (e.g. ``uvicorn``\ ) used to run Litestar. To use this protocol as a basis, simply subclass it - as you would any other class, and implement the two methods it specifies: @@ -67,20 +67,19 @@ specifies: class MyRequestLoggingMiddleware(MiddlewareProtocol): - def __init__(self, app: ASGIApp) -> None: - super().__init__(app) + def __init__(self, app: ASGIApp) -> None: # can have other parameters as well self.app = app async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] == "http": request = Request(scope) - logger.info("%s - %s" % request.method, request.url) + logger.info("Got request: %s - %s", request.method, request.url) await self.app(scope, receive, send) .. important:: Although ``scope`` is used to create an instance of request by passing it to the - :class:`Request <.connection.Request>` constructor, which makes it simpler to access because it does some parsing + :class:`~litestar.connection.Request` constructor, which makes it simpler to access because it does some parsing for you already, the actual source of truth remains ``scope`` - not the request. If you need to modify the data of the request you must modify the scope object, not any ephemeral request objects created as in the above. @@ -103,7 +102,6 @@ explore another example - redirecting the request to a different url from a midd class RedirectMiddleware(MiddlewareProtocol): def __init__(self, app: ASGIApp) -> None: - super().__init__(app) self.app = app async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: @@ -113,24 +111,24 @@ explore another example - redirecting the request to a different url from a midd else: await self.app(scope, receive, send) -As you can see in the above, given some condition (``request.session`` being None) we create a -:class:`ASGIRedirectResponse ` and then await it. Otherwise, we await ``self.app`` +As you can see in the above, given some condition (``request.session`` being ``None``) we create a +:class:`~litestar.response.redirect.ASGIRedirectResponse` and then await it. Otherwise, we await ``self.app`` Modifying ASGI Requests and Responses using the MiddlewareProtocol ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. important:: - If you'd like to modify a :class:`Response <.response.Response>` object after it was created for a route + If you'd like to modify a :class:`~litestar.response.Response` object after it was created for a route handler function but before the actual response message is transmitted, the correct place to do this is using the special life-cycle hook called :ref:`after_request `. The instructions in this section are for how to modify the ASGI response message itself, which is a step further in the response process. -Using the :class:`MiddlewareProtocol <.middleware.base.MiddlewareProtocol>` you can intercept and modifying both the +Using the :class:`~litestar.middleware.base.MiddlewareProtocol` you can intercept and modifying both the incoming and outgoing data in a request / response cycle by "wrapping" that respective ``receive`` and ``send`` ASGI functions. -To demonstrate this, lets say we want to append a header with a timestamp to all outgoing responses. We could achieve +To demonstrate this, let's say we want to append a header with a timestamp to all outgoing responses. We could achieve this by doing the following: .. code-block:: python @@ -150,11 +148,11 @@ this by doing the following: async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] == "http": - start_time = time.time() + start_time = time.monotonic() async def send_wrapper(message: Message) -> None: if message["type"] == "http.response.start": - process_time = time.time() - start_time + process_time = time.monotonic() - start_time headers = MutableScopeHeaders.from_message(message=message) headers["X-Process-Time"] = str(process_time) await send(message) @@ -166,21 +164,17 @@ this by doing the following: Inheriting AbstractMiddleware ----------------------------- -Litestar offers an :class:`AbstractMiddleware <.middleware.base.AbstractMiddleware>` class that can be extended to +Litestar offers an :class:`~litestar.middleware.base.AbstractMiddleware` class that can be extended to create middleware: .. code-block:: python - from typing import TYPE_CHECKING - from time import time + import time from litestar.enums import ScopeType from litestar.middleware import AbstractMiddleware from litestar.datastructures import MutableScopeHeaders - - - if TYPE_CHECKING: - from litestar.types import Message, Receive, Scope, Send + from litestar.types import Message, Receive, Scope, Send class MyMiddleware(AbstractMiddleware): @@ -188,15 +182,15 @@ create middleware: exclude = ["first_path", "second_path"] exclude_opt_key = "exclude_from_middleware" - async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: - start_time = time() + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + start_time = time.monotonic() async def send_wrapper(message: "Message") -> None: if message["type"] == "http.response.start": - process_time = time() - start_time + process_time = time.monotonic() - start_time headers = MutableScopeHeaders.from_message(message=message) headers["X-Process-Time"] = str(process_time) - await send(message) + await send(message) await self.app(scope, receive, send_wrapper) @@ -204,22 +198,26 @@ The three class variables defined in the above example ``scopes``, ``exclude``, fine-tune for which routes and request types the middleware is called: -- The scopes variable is a set that can include either or both ``ScopeType.HTTP`` and ``ScopeType.WEBSOCKET`` , with the default being both. +- The scopes variable is a set that can include either or both : ``ScopeType.HTTP`` and ``ScopeType.WEBSOCKET`` , with the default being both. - ``exclude`` accepts either a single string or list of strings that are compiled into a regex against which the request's ``path`` is checked. -- ``exclude_opt_key`` is the key to use for in a route handler's ``opt`` dict for a boolean, whether to omit from the middleware. +- ``exclude_opt_key`` is the key to use for in a route handler's :class:`Router.opt ` dict for a boolean, whether to omit from the middleware. -Thus, in the following example, the middleware will only run against the route handler called ``not_excluded_handler``: +Thus, in the following example, the middleware will only run against the handler called ``not_excluded_handler`` for ``/greet`` route: .. literalinclude:: /examples/middleware/base.py :language: python +.. danger:: + + Using ``/`` as an exclude pattern, will disable this middleware for all routes, + since, as a regex, it matches *every* path Using DefineMiddleware to pass arguments ---------------------------------------- -Litestar offers a simple way to pass positional arguments (``*args``) and key-word arguments (``**kwargs``) to middleware -using the :class:`DefineMiddleware ` class. Let's extend +Litestar offers a simple way to pass positional arguments (``*args``) and keyword arguments (``**kwargs``) to middleware +using the :class:`~litestar.middleware.base.DefineMiddleware` class. Let's extend the factory function used in the examples above to take some args and kwargs and then use ``DefineMiddleware`` to pass these values to our middleware: diff --git a/docs/usage/middleware/using-middleware.rst b/docs/usage/middleware/using-middleware.rst index 2190676fc5..47456bfc27 100644 --- a/docs/usage/middleware/using-middleware.rst +++ b/docs/usage/middleware/using-middleware.rst @@ -1,3 +1,5 @@ +.. _using-middleware: + Using Middleware ================ diff --git a/docs/usage/openapi/index.rst b/docs/usage/openapi/index.rst index 0d5a5d3c14..19ffb4c2cb 100644 --- a/docs/usage/openapi/index.rst +++ b/docs/usage/openapi/index.rst @@ -6,15 +6,19 @@ Litestar has first class OpenAPI support offering the following features: - Automatic `OpenAPI 3.1.0 Schema `_ generation, which is available as both YAML and JSON. - Builtin support for static documentation site generation using several different libraries. -- Simple configuration using pydantic based classes. +- Full configuration using pre-defined type-safe dataclasses. Litestar includes a complete implementation of the `latest version of the OpenAPI specification `_ -using Python dataclasses. This implementation is used as a basis for generating OpenAPI specs, supporting builtins including -``dataclasses`` and ``TypedDict``, as well as Pydantic models and any 3rd party entities for which a plugin is implemented. +using Python dataclasses. This implementation is used as a basis for generating OpenAPI specs, +supporting :func:`~dataclasses.dataclass`, :class:`~typing.TypedDict`, +as well as Pydantic and msgspec models, and any 3rd party entities +for which a :ref:`plugin ` is implemented. This is also highly configurable - and users can customize the OpenAPI spec in a variety of ways - ranging from passing -configuration globally, to settings specific kwargs on route handler decorators. +configuration globally to setting +:ref:`specific kwargs on route ` +handler decorators. .. toctree:: diff --git a/docs/usage/plugins/index.rst b/docs/usage/plugins/index.rst index ff0cdc1652..5eb27c9d32 100644 --- a/docs/usage/plugins/index.rst +++ b/docs/usage/plugins/index.rst @@ -1,3 +1,5 @@ +.. _plugins: + ======= Plugins ======= @@ -89,11 +91,11 @@ The following example shows the actual implementation of the ``SerializationPlug :language: python :caption: ``SerializationPluginProtocol`` implementation example -:meth:`supports_type(self, field_definition: FieldDefinition) -> bool: ` +:meth:`supports_type(self, field_definition: FieldDefinition) -> bool: ` returns a :class:`bool` indicating whether the plugin supports serialization for the given type. Specifically, we return ``True`` if the parsed type is either a collection of SQLAlchemy models or a single SQLAlchemy model. -:meth:`create_dto_for_type(self, field_definition: FieldDefinition) -> type[AbstractDTO]: ` +:meth:`create_dto_for_type(self, field_definition: FieldDefinition) -> type[AbstractDTO]: ` takes a :class:`FieldDefinition ` instance as an argument and returns a :class:`SQLAlchemyDTO ` subclass and includes some logic that may be interesting to potential serialization plugin authors. @@ -129,3 +131,4 @@ signature (their :func:`__init__` method). :titlesonly: flash_messages + problem_details diff --git a/docs/usage/plugins/problem_details.rst b/docs/usage/plugins/problem_details.rst new file mode 100644 index 0000000000..9ede5ec9f6 --- /dev/null +++ b/docs/usage/plugins/problem_details.rst @@ -0,0 +1,40 @@ +=============== +Problem Details +=============== + +.. versionadded:: 2.9.0 + +Problem details are a standardized way of providing machine-readable details of errors in HTTP +responses as specified in `RFC 9457`_, the latest RFC at the time of writing. + +.. _RFC 9457: https://datatracker.ietf.org/doc/html/rfc9457 + +Usage +----- + +To send a problem details response, the ``ProblemDetailsPlugin`` should be registered and then +a ``ProblemDetailsException`` can be raised anywhere which will automatically be converted +into a problem details response. + +.. literalinclude:: /examples/plugins/problem_details/basic_usage.py + :language: python + :caption: Basic usage of the problem details plugin. + +You can convert all ``HTTPExceptions`` into problem details response by enabling the flag in the ``ProblemDetailsConfig.`` + +.. literalinclude:: /examples/plugins/problem_details/convert_http_exceptions.py + :language: python + :caption: Converting ``HTTPException`` into problem details response. + + +You can also convert any exception that is not a ``HTTPException`` into a problem details response +by providing a mapping of the exception type to a callable that converts the exception into a +``ProblemDetailsException.`` + +.. tip:: This can used to override how the ``HTTPException`` is converted into a problem details response as well. + +.. literalinclude:: /examples/plugins/problem_details/convert_exceptions.py + :language: python + :caption: Converting custom exceptions into problem details response. + +.. warning:: If the ``extra`` field is a ``Mapping``, then it's merged into the problem details response, otherwise it's included in the response with the key ``extra.`` diff --git a/docs/usage/requests.rst b/docs/usage/requests.rst index 485748ac8c..3dfef97435 100644 --- a/docs/usage/requests.rst +++ b/docs/usage/requests.rst @@ -10,14 +10,14 @@ The body of HTTP requests can be accessed using the special ``data`` parameter i :language: python -The type of ``data`` an be any supported type, including +The type of ``data`` can be any supported type, including * :func:`dataclasses ` * :class:`TypedDicts ` * Pydantic models * Arbitrary stdlib types -* Typed supported via :doc:`plugins ` +* Types supported via :doc:`plugins ` .. literalinclude:: /examples/request_data/request_data_2.py :language: python @@ -160,3 +160,52 @@ The example below illustrates how to implement custom request class for the whol class on multiple layers, the layer closest to the route handler will take precedence. You can read more about this in the :ref:`usage/applications:layered architecture` section + + +Limits +------- + +Body size +^^^^^^^^^^ + +A limit for the allowed request body size can be set on all layers via the +``request_max_body_size`` parameter and defaults to 10MB. If a request body exceeds this +limit, a ``413 - Request Entity Too Large`` +response will be returned. This limit applies to all methods of consuming the request +body, including requesting it via the ``body`` parameter in a route handler and +consuming it through a manually constructed :class:`~litestar.connection.Request` +instance, e.g. in a middleware. + +To disable this limit for a specific handler / router / controller, it can be set to +:obj:`None`. + +.. danger:: + Setting ``request_max_body_size=None`` is strongly discouraged as it exposes the + application to a denial of service (DoS) attack by sending arbitrarily large + request bodies to the affected endpoint. Because Litestar has to read the whole body + to perform certain actions, such as parsing JSON, it will fill up all the available + memory / swap until the application / server crashes, should no outside limits be + imposed. + + This is generally only recommended in environments where the application is running + behind a reverse proxy such as NGINX, where a size limit is already set. + + +.. danger:: + Since ``request_max_body_size`` is handled on a per-request basis, it won't affect + middlewares or ASGI handlers when they try to access the request body via the raw + ASGI events. To avoid this, middlewares and ASGI handlers should construct a + :class:`~litestar.connection.Request` instance and use the regular + :meth:`~litestar.connection.Request.stream` / + :meth:`~litestar.connection.Request.body` or content-appropriate method to consume + the request body in a safe manner. + + +.. tip:: + For requests that define a ``Content-Length`` header, Litestar will not attempt to + read the request body should the header value exceed the ``request_max_body_size``. + + If the header value is within the allowed bounds, Litestar will verify during the + streaming of the request body that it does not exceed the size specified in the + header. Should the request exceed this size, it will abort the request with a + ``400 - Bad Request``. diff --git a/docs/usage/responses.rst b/docs/usage/responses.rst index e48b28305d..a7c2e49f05 100644 --- a/docs/usage/responses.rst +++ b/docs/usage/responses.rst @@ -80,8 +80,8 @@ this :ref:`custom responses `. You can also set an application media type string with the ``+json`` suffix defined in `RFC 6839 `_ as the ``media_type`` and it will be recognized and serialized as json. -For example, you can use ``application/problem+json`` -(see `RFC 7807 `_) + +For example, you can use ``application/vnd.example.resource+json`` and it will work just like json but have the appropriate content-type header and show up in the generated OpenAPI schema. diff --git a/docs/usage/routing/handlers.rst b/docs/usage/routing/handlers.rst index 78fc9f6e7d..d6207ed954 100644 --- a/docs/usage/routing/handlers.rst +++ b/docs/usage/routing/handlers.rst @@ -199,7 +199,7 @@ These are used exactly like :func:`@route() <.handlers.route>` with the sole exc from litestar import delete, get, patch, post, put, head from litestar.dto import DTOConfig, DTOData - from litestar.contrib.pydantic import PydanticDTO + from litestar.plugins.pydantic import PydanticDTO from pydantic import BaseModel @@ -576,18 +576,21 @@ However, this approach can get tedious; as an alternative, Litestar accepts a `` every :ref:`layer ` of the application, as demonstrated in the following example: .. literalinclude:: /examples/signature_namespace/domain.py + :language: python :caption: This module defines our domain type in some central place. This module defines our controller, note that we do not import ``Model`` into the runtime :term:`namespace`, nor do we require any directives to control behavior of linters. .. literalinclude:: /examples/signature_namespace/controller.py + :language: python :caption: This module defines our controller without importing ``Model`` into the runtime namespace. Finally, we ensure that our application knows that when it encounters the name "Model" when parsing signatures, that it should reference our domain ``Model`` type. .. literalinclude:: /examples/signature_namespace/app.py + :language: python :caption: Ensuring the application knows how to resolve the ``Model`` type when parsing signatures. .. tip:: If you want to map your type to a name that is different from its ``__name__`` attribute, diff --git a/docs/usage/routing/overview.rst b/docs/usage/routing/overview.rst index e462ded15d..d67a73bee1 100644 --- a/docs/usage/routing/overview.rst +++ b/docs/usage/routing/overview.rst @@ -137,7 +137,7 @@ Their purpose is to allow users to utilize Python OOP for better code organizati .. code-block:: python :caption: Registering a :class:`~.controller.Controller` - from litestar.contrib.pydantic import PydanticDTO + from litestar.plugins.pydantic import PydanticDTO from litestar.controller import Controller from litestar.dto import DTOConfig, DTOData from litestar.handlers import get, post, patch, delete @@ -251,6 +251,7 @@ requests addressed to a given path. .. dropdown:: Click to see an example of mounting an ASGI app .. literalinclude:: /examples/routing/mount_custom_app.py + :language: python :caption: Mounting an ASGI App The handler function will receive all requests with an url that begins with ``/some/sub-path``, e.g, ``/some/sub-path``, @@ -270,6 +271,7 @@ party libraries. The following example is identical in principle to the one abov .. dropdown:: Click to see an example of mounting a Starlette app .. literalinclude:: /examples/routing/mounting_starlette_app.py + :language: python :caption: Mounting a Starlette App .. admonition:: Why Litestar uses radix based routing diff --git a/docs/usage/routing/parameters.rst b/docs/usage/routing/parameters.rst index 4355309fce..59b026fd65 100644 --- a/docs/usage/routing/parameters.rst +++ b/docs/usage/routing/parameters.rst @@ -8,6 +8,7 @@ Path :term:`parameters ` are parameters declared as part of the ``pat the URL. They are declared using a simple syntax ``{param_name:param_type}`` : .. literalinclude:: /examples/parameters/path_parameters_1.py + :language: python :caption: Defining a path parameter in a route handler In the above there are two components: @@ -32,8 +33,8 @@ Currently, the following types are supported: * :class:`int`: Accepts ints and floats. * :class:`path`: Accepts valid POSIX paths. * :class:`str`: Accepts all string values. -* ``time``: Accepts time strings with optional timezone compatible with pydantic formats. -* ``timedelta``: Accepts duration strings compatible with the pydantic formats. +* ``time``: Accepts time strings with optional timezone compatible with standard (Pydantic/Msgspec) datetime formats. +* ``timedelta``: Accepts duration strings compatible with the standard (Pydantic/Msgspec) timedelta formats. * ``uuid``: Accepts all uuid values. The types declared in the path :term:`parameter` and the function do not need to match 1:1 - as long as @@ -41,14 +42,14 @@ parameter inside the function declaration is typed with a "higher" type to which this is fine. For example, consider this: .. literalinclude:: /examples/parameters/path_parameters_2.py + :language: python :caption: Coercing path parameters into different types The :term:`parameter` defined inside the ``path`` :term:`kwarg ` is typed as :class:`int` , because the value passed as part of the request will be a timestamp in milliseconds without any decimals. The parameter in the function declaration though is typed as :class:`datetime.datetime`. -This works because the int value will be passed to a pydantic model representing the function signature, which will -coerce the :class:`int` into a :class:`~datetime.datetime`. +This works because the int value will be automatically coerced from an :class:`int` into a :class:`~datetime.datetime`. Thus, when the function is called it will be called with a :class:`~datetime.datetime`-typed parameter. @@ -69,6 +70,7 @@ If you want to add validation or enhance the OpenAPI documentation generated for you can do so using the `the parameter function`_: .. literalinclude:: /examples/parameters/path_parameters_3.py + :language: python :caption: Adding extra validation and documentation to a path parameter In the above example, :func:`~.params.Parameter` is used to restrict the value of :paramref:`~.params.Parameter.version` @@ -84,16 +86,17 @@ Every :term:`keyword argument ` that is not otherwise specified (for e :ref:`path parameter `) will be interpreted as a query parameter. .. literalinclude:: /examples/parameters/query_params.py + :language: python :caption: Defining query parameters in a route handler .. admonition:: Technical details :class: info - These :term:`parameters ` will be parsed from the function signature and used to generate a Pydantic model. + These :term:`parameters ` will be parsed from the function signature and used to generate an internal data model. This model in turn will be used to validate the parameters and generate the OpenAPI schema. - This means that you can also use any pydantic type in the signature, and it will - follow the same kind of validation and parsing as you would get from pydantic. + This ability allows you to use any number of schema/modelling libraries, including Pydantic, Msgspec, Attrs, and Dataclasses, and it will + follow the same kind of validation and parsing as you would get from these libraries. Query :term:`parameters ` come in three basic types: @@ -111,6 +114,7 @@ In this example, ``param`` will have the value ``"hello"`` if it is not specifie If it is passed as a query :term:`parameter` however, it will be overwritten: .. literalinclude:: /examples/parameters/query_params_default.py + :language: python :caption: Defining a default value for a query parameter Optional :term:`parameters ` @@ -125,6 +129,7 @@ If it is given, it has to be a :class:`string `. If it is not given, it will have a default value of ``None`` .. literalinclude:: /examples/parameters/query_params_optional.py + :language: python :caption: Defining an optional query parameter Type coercion @@ -133,9 +138,8 @@ Type coercion It is possible to coerce query :term:`parameters ` into different types. A query starts out as a :class:`string `, but its values can be parsed into all kinds of types. -Since this is done by Pydantic, everything that works there will work for query parameters as well. - .. literalinclude:: /examples/parameters/query_params_types.py + :language: python :caption: Coercing query parameters into different types Alternative names and constraints @@ -145,6 +149,7 @@ Sometimes you might want to "remap" query :term:`parameters ` to allo than what is being used in the handler function. This can be done by making use of :func:`~.params.Parameter`. .. literalinclude:: /examples/parameters/query_params_remap.py + :language: python :caption: Remapping query parameters to different names Here, we remap from ``snake_case`` in the handler function to ``camelCase`` in the URL. @@ -154,6 +159,7 @@ will be used for the value of the ``snake_case`` parameter. :func:`~.params.Parameter` also allows us to define additional constraints: .. literalinclude:: /examples/parameters/query_params_constraints.py + :language: python :caption: Constraints on query parameters In this case, ``param`` is validated to be an *integer larger than 5*. @@ -165,6 +171,7 @@ Unlike *Query* :term:`parameters `, *Header* and *Cookie* parameters declared using `the parameter function`_ , for example: .. literalinclude:: /examples/parameters/header_and_cookie_parameters.py + :language: python :caption: Defining header and cookie parameters As you can see in the above, header parameters are declared using the ``header`` @@ -179,6 +186,7 @@ As part of Litestar's :ref:`layered architecture ` on the :class:`Litestar app <.app.Litestar>`, diff --git a/docs/usage/security/abstract-authentication-middleware.rst b/docs/usage/security/abstract-authentication-middleware.rst index 9db3ac6c77..615c698346 100644 --- a/docs/usage/security/abstract-authentication-middleware.rst +++ b/docs/usage/security/abstract-authentication-middleware.rst @@ -65,7 +65,7 @@ example here let us say it is a `SQLAlchemy `_ mod # ... other fields follow, but we only require id for this example We will also need some utility methods to encode and decode tokens. To this end we will use -the `python-jose `_ library. We will also create a Pydantic model representing a +the `pyjwt `_ library. We will also create a Pydantic model representing a JWT Token: .. dropdown:: Click to see the JWT utility methods and Token model diff --git a/docs/usage/security/excluding-and-including-endpoints.rst b/docs/usage/security/excluding-and-including-endpoints.rst index c63142bbd1..4e6ce6ac8f 100644 --- a/docs/usage/security/excluding-and-including-endpoints.rst +++ b/docs/usage/security/excluding-and-including-endpoints.rst @@ -1,16 +1,26 @@ Excluding and including endpoints ================================= -Please make sure you read the :doc:`security backends documentation ` first for learning how to set up a security backend. This section focuses on configuring the ``exclude`` rule for those backends. +Please make sure you read the :doc:`security backends documentation ` first for +learning how to set up a security backend. This section focuses on configuring the ``exclude`` rule for those backends. -There are multiple ways for including or excluding endpoints in the authentication flow. The default rules are configured in the ``Auth`` object used (subclass of :class:`~.security.base.AbstractSecurityConfig`). The examples below use :class:`~.security.session_auth.auth.SessionAuth` but it is the same for :class:`~.security.jwt.auth.JWTAuth` and :class:`~.security.jwt.auth.JWTCookieAuth`. +There are multiple ways for including or excluding endpoints in the authentication flow. The default rules are +configured in the ``Auth`` object used (subclass of :class:`~.security.base.AbstractSecurityConfig`). The examples +below use :class:`~.security.session_auth.auth.SessionAuth` but it is the same for :class:`~.security.jwt.auth.JWTAuth` +and :class:`~.security.jwt.auth.JWTCookieAuth`. Excluding routes -------------------- -The ``exclude`` argument takes a :class:`string ` or :class:`list` of :class:`strings ` that are interpreted as regex patterns. For example, the configuration below would apply authentication to all endpoints except those where the route starts with ``/login``, ``/signup``, or ``/schema``. Thus, one does not have to exclude ``/schema/swagger`` as well - it is included in the ``/schema`` pattern. +The ``exclude`` argument takes a :class:`string ` or :class:`list` of :class:`strings ` that are interpreted +as regex patterns. For example, the configuration below would apply authentication to all endpoints except those where +the route starts with ``/login``, ``/signup``, or ``/schema``. Thus, one does not have to exclude ``/schema/swagger`` +as well - it is included in the ``/schema`` pattern. -This also means that passing ``/`` will disable authentication for all routes. +.. danger:: + + Passing ``/`` will disable authentication for all routes, since, as a regex, it + matches *every* path. .. code-block:: python @@ -28,7 +38,9 @@ This also means that passing ``/`` will disable authentication for all routes. Including routes ---------------- -Since the exclusion rules are evaluated as regex, it is possible to pass a rule that inverts exclusion - meaning, no path but the one specified in the pattern will be protected by authentication. In the example below, only endpoints under the ``/secured`` route will require authentication - all other routes do not. +Since the exclusion rules are evaluated as regex, it is possible to pass a rule that inverts exclusion - meaning, no +path but the one specified in the pattern will be protected by authentication. In the example below, only endpoints +under the ``/secured`` route will require authentication - all other routes do not. .. code-block:: python @@ -46,7 +58,8 @@ Since the exclusion rules are evaluated as regex, it is possible to pass a rule Exclude from auth -------------------- -Sometimes, you might want to apply authentication to all endpoints under a route but a few selected. In this case, you can pass ``exclude_from_auth=True`` to the route handler as shown below. +Sometimes, you might want to apply authentication to all endpoints under a route but a few selected. In this case, you +can pass ``exclude_from_auth=True`` to the route handler as shown below. .. code-block:: python @@ -60,7 +73,8 @@ Sometimes, you might want to apply authentication to all endpoints under a route ... ... -You can set an alternative option key in the security configuration, e.g., you can use ``no_auth`` instead of ``exclude_from_auth``. +You can set an alternative option key in the security configuration, e.g., you can use ``no_auth`` instead of +``exclude_from_auth``. .. code-block:: python diff --git a/docs/usage/security/guards.rst b/docs/usage/security/guards.rst index 2a3b879b8b..b9987d1106 100644 --- a/docs/usage/security/guards.rst +++ b/docs/usage/security/guards.rst @@ -1,8 +1,7 @@ Guards ====== -Guards are :term:`callables ` that receive two arguments - ``connection``, which is the -:class:`~.connection.ASGIConnection` instance, and ``route_handler``, which is a copy of the +Guards are :term:`callables ` that receive two arguments - ``connection``, which is the :class:`Request <.connection.Request>` or :class:`WebSocket <.connection.WebSocket>` instance (both sub-classes of :class:`~.connection.ASGIConnection`), and ``route_handler``, which is a copy of the :class:`~.handlers.BaseRouteHandler`. Their role is to *authorize* the request by verifying that the connection is allowed to reach the endpoint handler in question. If verification fails, the guard should raise an :exc:`HTTPException`, usually a :class:`~.exceptions.NotAuthorizedException` with a diff --git a/docs/usage/security/jwt.rst b/docs/usage/security/jwt.rst index c1a96610df..a25f45be06 100644 --- a/docs/usage/security/jwt.rst +++ b/docs/usage/security/jwt.rst @@ -2,7 +2,7 @@ JWT Security Backends ===================== Litestar offers optional JWT based security backends. To use these make sure to install the -`python-jose `_ and `cryptography `_ +`pyjwt `_ and `cryptography `_ packages, or simply install Litestar with the ``jwt`` `extra `_: @@ -20,6 +20,7 @@ It sends the JWT token using a header - and it expects requests to send the JWT .. dropdown:: Click to see the code .. literalinclude:: /examples/security/jwt/using_jwt_auth.py + :language: python :caption: Using JWT Auth :class:`JWT Cookie Auth <.security.jwt.JWTCookieAuth>` Backend @@ -31,6 +32,7 @@ that instead of using a header for the JWT Token, it uses a cookie. .. dropdown:: Click to see the code .. literalinclude:: /examples/security/jwt/using_jwt_cookie_auth.py + :language: python :caption: Using JWT Cookie Auth :class:`OAuth2 Bearer <.security.jwt.auth.OAuth2PasswordBearerAuth>` Password Flow @@ -43,6 +45,7 @@ OAuth 2.0 Bearer password flows. .. dropdown:: Click to see the code .. literalinclude:: /examples/security/jwt/using_oauth2_password_bearer.py + :language: python :caption: Using OAUTH2 Bearer Password @@ -53,6 +56,7 @@ The token class used can be customized with arbitrary fields, by creating a subc :class:`~.security.jwt.Token`, and specifying it on the backend: .. literalinclude:: /examples/security/jwt/custom_token_cls.py + :language: python :caption: Using a custom token @@ -65,3 +69,34 @@ conversions. converting the token. To support more complex conversions, the :meth:`~.security.jwt.Token.encode` and :meth:`~.security.jwt.Token.decode` methods must be overwritten in the subclass. + + +Verifying issuer and audience +----------------------------- + +To verify the JWT ``iss`` (*issuer*) and ``aud`` (*audience*) claim, a list of accepted +issuers or audiences can bet set on the authentication backend. When a JWT is decoded, +the issuer or audience on the token is compared to the list of accepted issuers / +audiences. If the value in the token does not match any value in the respective list, +a :exc:`NotAuthorizedException` will be raised, returning a response with a +``401 Unauthorized`` status. + + +.. literalinclude:: /examples/security/jwt/verify_issuer_audience.py + :language: python + :caption: Verifying issuer and audience + + +Customizing token validation +---------------------------- + +Token decoding / validation can be further customized by overriding the +:meth:`~.security.jwt.Token.decode_payload` method. It will be called by +:meth:`~.security.jwt.Token.decode` with the encoded token string, and must return a +dictionary representing the decoded payload, which will then used by +:meth:`~.security.jwt.Token.decode` to construct an instance of the token class. + + +.. literalinclude:: /examples/security/jwt/custom_decode_payload.py + :language: python + :caption: Customizing payload decoding diff --git a/docs/usage/security/secret-datastructures.rst b/docs/usage/security/secret-datastructures.rst index 4b00095568..8e0821e786 100644 --- a/docs/usage/security/secret-datastructures.rst +++ b/docs/usage/security/secret-datastructures.rst @@ -14,6 +14,7 @@ Secret Parameters The following example demonstrates how to use :class:`~datastructures.SecretString` to accept a secret value as a parameter in a GET request: .. literalinclude:: /examples/datastructures/secrets/secret_header.py + :language: python :caption: Example of using SecretString for a Header Parameter .. note:: @@ -35,6 +36,7 @@ This example demonstrates use of a data structure with a :class:`~datastructures within the HTTP body of a request: .. literalinclude:: /examples/datastructures/secrets/secret_body.py + :language: python :caption: Example of using SecretString for a Request Body Security Considerations diff --git a/docs/usage/security/security-backends.rst b/docs/usage/security/security-backends.rst index a1335044b4..eed2891e3e 100644 --- a/docs/usage/security/security-backends.rst +++ b/docs/usage/security/security-backends.rst @@ -20,6 +20,7 @@ middleware. .. dropdown:: Click to see an example of using the session auth backend .. literalinclude:: /examples/security/using_session_auth.py + :language: python :caption: Using Session Auth JWT Auth diff --git a/docs/usage/static-files.rst b/docs/usage/static-files.rst index e1d474cdc8..d180942156 100644 --- a/docs/usage/static-files.rst +++ b/docs/usage/static-files.rst @@ -6,6 +6,7 @@ To serve static files (i.e., serve arbitrary files from a given directory), the :class:`Router ` to handle this task. .. literalinclude:: /examples/static_files/full_example.py + :language: python :caption: Serving static files using :func:`create_static_files_router ` In this example, files from the directory ``assets`` will be served on the path @@ -24,6 +25,7 @@ Setting :paramref:`~litestar.static_files.create_static_files_router.params.send them with a ``Content-Disposition: attachment`` instead: .. literalinclude:: /examples/static_files/send_as_attachment.py + :language: python :caption: Sending files as attachments using the the :paramref:`~litestar.static_files.create_static_files_router.params.send_as_attachment` parameter of :func:`create_static_files_router` @@ -40,6 +42,7 @@ This will: - Attempt to serve ``/404.html`` when a requested file is not found .. literalinclude:: /examples/static_files/html_mode.py + :language: python :caption: Serving HTML files using the :paramref:`~litestar.static_files.create_static_files_router.params.html_mode` parameter of :func:`create_static_files_router` @@ -50,6 +53,7 @@ Options available on :class:`~litestar.router.Router` can be passed to directly :func:`~litestar.static_files.create_static_files_router`: .. literalinclude:: /examples/static_files/passing_options.py + :language: python :caption: Passing options to the router generated by :func:`create_static_files_router` @@ -60,6 +64,7 @@ The router class used can be customized with the :paramref:`~.static_files.create_static_files_router.params.router_class` parameter: .. literalinclude:: /examples/static_files/custom_router.py + :language: python :caption: Using a custom router class with :func:`create_static_files_router` @@ -71,6 +76,7 @@ Retrieving paths to static files under which a specific file will be available: .. literalinclude:: /examples/static_files/route_reverse.py + :language: python :caption: Retrieving paths to static files using :meth:`~.app.Litestar.route_reverse` .. tip:: The ``name`` parameter has to match the ``name`` parameter passed to @@ -92,6 +98,7 @@ with support for popular cloud providers available via 3rd party implementations - Azure Blob Storage via `adlfs `_ .. literalinclude:: /examples/static_files/file_system.py + :language: python :caption: Using a custom file system with :func:`create_static_files_router` @@ -106,8 +113,10 @@ Existing code can be upgraded to :func:`create_static_files_router` by replacing ``route_handlers`` instead of ``static_files_config``: .. literalinclude:: /examples/static_files/upgrade_from_static_1.py + :language: python :caption: Using the deprecated :class:`~.static_files.config.StaticFilesConfig` .. literalinclude:: /examples/static_files/upgrade_from_static_2.py + :language: python :caption: Upgrading from :class:`~.static_files.config.StaticFilesConfig` to :func:`create_static_files_router` diff --git a/docs/usage/stores.rst b/docs/usage/stores.rst index fb580f75f7..253cc9deee 100644 --- a/docs/usage/stores.rst +++ b/docs/usage/stores.rst @@ -23,7 +23,7 @@ Built-in stores :class:`MemoryStore ` A simple in-memory store, using a dictionary to hold data. This store offers no persistence and is not thread or multiprocess safe, but it is suitable for basic applications such as caching and has generally the lowest overhead. This is the default store used by - Litestar internally. If you plan to enable :ref:`multiple web workers` and you need inter-process communication + Litestar internally. If you plan to enable :doc:`multiple web workers ` and you need inter-process communication across multiple worker processes, you should use one of the other non-memory stores instead. :class:`FileStore ` @@ -35,6 +35,12 @@ Built-in stores A store backend by `redis `_. It offers all the guarantees and features of Redis, making it suitable for almost all applications. Offers `namespacing`_. +:class:`ValKeyStore ` + A store backed by `valkey `_, a fork of Redis created as the result of Redis' license changes. + Similarly to the RedisStore, it is suitable for almost all applications and supports `namespacing`_. + At the time of writing, :class:`Valkey ` is equivalent to :class:`redis.asyncio.Redis`, + and all notes pertaining to Redis also apply to Valkey. + .. admonition:: Why not memcached? :class: info diff --git a/docs/usage/testing.rst b/docs/usage/testing.rst index 90102f640d..a2dc47d6cd 100644 --- a/docs/usage/testing.rst +++ b/docs/usage/testing.rst @@ -42,6 +42,8 @@ We would then test it using the test client like so: from my_app.main import app + app.debug = True + def test_health_check(): with TestClient(app=app) as client: @@ -60,6 +62,8 @@ We would then test it using the test client like so: from my_app.main import app + app.debug = True + async def test_health_check(): async with AsyncTestClient(app=app) as client: @@ -90,6 +94,8 @@ Since we would probably need to use the client in multiple places, it's better t if TYPE_CHECKING: from litestar import Litestar + app.debug = True + @pytest.fixture(scope="function") def test_client() -> Iterator[TestClient[Litestar]]: @@ -114,6 +120,8 @@ Since we would probably need to use the client in multiple places, it's better t if TYPE_CHECKING: from litestar import Litestar + app.debug = True + @pytest.fixture(scope="function") async def test_client() -> AsyncIterator[AsyncTestClient[Litestar]]: @@ -279,6 +287,31 @@ But also this: assert response.text == "healthy" +Running a live server +--------------------- + +The test clients make use of HTTPX's ability to directly call into an ASGI app, without +having to run an actual server. In most cases this is sufficient but there are some +exceptions where this won't work, due to the limitations of the emulated client-server +communication. + +For example, when using server-sent events with an infinite generator, it will lock up +the test client, since HTTPX tries to consume the full response before returning a +request. + +Litestar offers two helper functions, +:func:`litestar.testing.subprocess_sync_client` and +:func:`litestar.testing.subprocess_async_client` that will +launch a Litestar instance with in a subprocess and set up an httpx client for running +tests. You can either load your actual app file or create subsets from it as you would +with the regular test client setup: + +.. literalinclude:: /examples/testing/subprocess_sse_app.py + :language: python + +.. literalinclude:: /examples/testing/test_subprocess_sse.py + :language: python + RequestFactory -------------- diff --git a/docs/usage/websockets.rst b/docs/usage/websockets.rst index dad724587b..dc9dc7eeb5 100644 --- a/docs/usage/websockets.rst +++ b/docs/usage/websockets.rst @@ -1,20 +1,31 @@ WebSockets ========== +There are three ways to handle WebSockets in Litestar: -Handling WebSockets in an application often involves dealing with low level constructs -such as the socket itself, setting up a loop and listening for incoming data, handling -exceptions, and parsing incoming and serializing outgoing data. In addition to the -low-level :class:`WebSocket route handler <.handlers.websocket>`, Litestar offers two -high level interfaces: +1. The low-level :class:`~litestar.handlers.websocket` route handler, providing basic + abstractions over the ASGI WebSocket interface +2. :class:`~litestar.handlers.websocket_listener` and :class:`~litestar.handlers.WebsocketListener`\ : + Reactive, event-driven WebSockets with full serialization and DTO support and support + for a synchronous interface +3. :class:`~litestar.handlers.websocket_stream` and :func:`~litestar.handlers.send_websocket_stream`\ : + Proactive, stream oriented WebSockets with full serialization and DTO support -- :class:`websocket_listener <.handlers.websocket_listener>` -- :class:`WebSocketListener <.handlers.WebsocketListener>` +The main difference between the low and high level interfaces is that, dealing with low +level interface requires, setting up a loop and listening for incoming data, handling +exceptions, client disconnects, and parsing incoming and serializing outgoing data. -These treat a WebSocket handler like any other route handler: as a callable that takes -in incoming data in an already pre-processed form and returns data to be serialized and -sent over the connection. The low level details will be handled behind the curtains. + + +WebSocket Listeners +-------------------- + +WebSocket Listeners can be used to interact with a WebSocket in an event-driven manner, +using a callback style interface. They treat a WebSocket handler like any other route +handler: A callable that takes in incoming data in an already pre-processed form and +returns data to be serialized and sent over the connection. The low level details will +be handled behind the curtains. .. code-block:: python @@ -44,7 +55,7 @@ type of data which should be received, and it will be converted accordingly. Receiving data --------------- +++++++++++++++ Data can be received in the listener via the ``data`` parameter. The data passed to this will be converted / parsed according to the given type annotation and supports @@ -62,11 +73,13 @@ form of JSON. .. tab-item:: Text .. literalinclude:: /examples/websockets/receive_str.py + :language: python .. tab-item:: Bytes .. literalinclude:: /examples/websockets/receive_bytes.py + :language: python .. important:: @@ -76,7 +89,7 @@ form of JSON. Sending data ------------- ++++++++++++++ Sending data is done by simply returning the value to be sent from the handler function. Similar to receiving data, type annotations configure how the data is being handled. @@ -84,7 +97,8 @@ Values that are not :class:`str` or :class:`bytes` are assumed to be JSON encoda will be serialized accordingly before being sent. This serialization is available for all data types currently supported by Litestar ( :doc:`dataclasses `\ , :class:`TypedDict `, -:class:`NamedTuple `, :class:`msgspec.Struct`, etc.). +:class:`NamedTuple `, :class:`msgspec.Struct`, etc.), including +DTOs. .. tab-set:: @@ -111,25 +125,12 @@ all data types currently supported by Litestar ( :language: python -Transport modes ---------------- - -WebSockets have two transport modes: Text and binary. These can be specified -individually for receiving and sending data. - -.. note:: - It may seem intuitive that ``text`` and ``binary`` should map to :class:`str` and - :class:`bytes` respectively, but this is not the case. Listeners can receive and - send data in any format, independently of the mode. The mode only affects how - data is encoded during transport (i.e. on the protocol level). In most cases the - default mode - ``text`` - is all that's needed. Binary transport is usually employed - when sending binary blobs that don't have a meaningful string representation, such - as images. - +Setting transport modes ++++++++++++++++++++++++ -Setting the receive mode -++++++++++++++++++++++++ +Receive mode +~~~~~~~~~~~~ .. tab-set:: @@ -154,8 +155,8 @@ Setting the receive mode it will not respond to WebSocket events sending data in the text channel. -Setting the send mode -++++++++++++++++++++++ +Send mode +~~~~~~~~~ .. tab-set:: @@ -177,10 +178,10 @@ Setting the send mode Dependency injection --------------------- +++++++++++++++++++++ -:doc:`dependency-injection` is available as well and generally works the same as with -regular route handlers: +:doc:`dependency-injection` is available and generally works the same as in regular +route handlers: .. literalinclude:: /examples/websockets/dependency_injection_simple.py :language: python @@ -201,7 +202,7 @@ the ``yield`` will only be executed after the connection has been closed. Interacting with the WebSocket directly ---------------------------------------- ++++++++++++++++++++++++++++++++++++++++ Sometimes access to the socket instance is needed, in which case the :class:`WebSocket <.connection.WebSocket>` instance can be injected into the handler @@ -218,7 +219,7 @@ function via the ``socket`` argument: Customising connection acceptance ---------------------------------- ++++++++++++++++++++++++++++++++++ By default, Litestar will accept all incoming connections by awaiting ``WebSocket.accept()`` without arguments. This behavior can be customized by passing a custom ``connection_accept_handler`` function. Litestar will await this @@ -229,7 +230,7 @@ function to accept the connection. Class based WebSocket handling ------------------------------- +++++++++++++++++++++++++++++++ In addition to using a simple function as in the examples above, a class based approach is made possible by extending the @@ -252,7 +253,7 @@ encapsulate more complex logic. Custom WebSocket ----------------- +++++++++++++++++ .. versionadded:: 2.7.0 @@ -271,3 +272,118 @@ The example below illustrates how to implement a custom WebSocket class for the class on multiple layers, the layer closest to the route handler will take precedence. You can read more about this in the :ref:`usage/applications:layered architecture` section + + +WebSocket Streams +----------------- + +WebSocket streams can be used to proactively push data to a client, using an +asynchronous generator function. Data will be sent via the socket every time the +generator ``yield``\ s, until it is either exhausted or the client disconnects. + +.. literalinclude:: /examples/websockets/stream_basic.py + :language: python + :caption: Streaming the current time in 0.5 second intervals + + +Serialization ++++++++++++++ + +Just like with route handlers, type annotations configure how the data is being handled. +:class:`str` or :class:`bytes` will be sent as-is, while everything else will be encoded +as JSON before being sent. This serialization is available for all data types currently +supported by Litestar (:doc:`dataclasses `, +:class:`TypedDict `, :class:`NamedTuple `, +:class:`msgspec.Struct`, etc.), including DTOs. + + +Dependency Injection +++++++++++++++++++++ + +Dependency injection is available and works analogous to regular route handlers. + +.. important:: + One thing to keep in mind, especially for long-lived streams, is that dependencies + are scoped to the lifetime of the handler. This means that if for example a + database connection is acquired in a dependency, it will be held until the generator + stops. This may not be desirable in all cases, and acquiring resources ad-hoc inside + the generator itself preferable + + .. literalinclude:: /examples/websockets/stream_di_hog.py + :language: python + :caption: Bad: The lock will be held until the client disconnects + + + .. literalinclude:: /examples/websockets/stream_di_hog_fix.py + :language: python + :caption: Good: The lock will only be acquired when it's needed + + +Interacting with the WebSocket directly ++++++++++++++++++++++++++++++++++++++++ + +To interact with the :class:`WebSocket <.connection.WebSocket>` directly, it can be +injected into the generator function via the ``socket`` argument: + +.. literalinclude:: /examples/websockets/stream_socket_access.py + :language: python + + +Receiving data while streaming +++++++++++++++++++++++++++++++ + +By default, a stream will listen for a client disconnect in the background, and stop +the generator once received. Since this requires receiving data from the socket, it can +lead to data loss if the application is attempting to read from the same socket +simultaneously. + +.. tip:: + To prevent data loss, by default, ``websocket_stream`` will raise an + exception if it receives any data while listening for client disconnects. If + incoming data should be ignored, ``allow_data_discard`` should be set to ``True`` + +If receiving data while streaming is desired, +:func:`~litestar.handlers.send_websocket_stream` can be configured to not listen for +disconnects by setting ``listen_for_disconnect=False``. + +.. important:: + When using ``listen_for_disconnect=False``, the application needs to ensure the + disconnect event is received elsewhere, otherwise the stream will only terminate + when the generator is exhausted + + +Combining streaming and receiving data +--------------------------------------- + +To stream and receive data concurrently, the stream can be set up manually using +:func:`~litestar.handlers.send_websocket_stream` in combination with either a regular +:class:`~litestar.handlers.websocket` handler or a WebSocket listener. + +.. tab-set:: + + .. tab-item:: websocket_listener + + .. literalinclude:: /examples/websockets/stream_and_receive_listener.py + :language: python + + .. tab-item:: websocket handler + + .. literalinclude:: /examples/websockets/stream_and_receive_raw.py + :language: python + + +Transport modes +--------------- + +WebSockets have two transport modes: ``text`` and ``binary``. They dictate how bytes are +transferred over the wire and can be set independently from another, i.e. a socket can +send ``binary`` and receive ``text`` + + +It may seem intuitive that ``text`` and ``binary`` should map to :class:`str` and +:class:`bytes` respectively, but this is not the case. WebSockets can receive and +send data in any format, independently of the mode. The mode only affects how the +bytes are handled during transport (i.e. on the protocol level). In most cases the +default mode - ``text`` - is all that's needed. Binary transport is usually employed +when sending binary blobs that don't have a meaningful string representation, such +as images. diff --git a/litestar/__init__.py b/litestar/__init__.py index 3235113fef..1303d932aa 100644 --- a/litestar/__init__.py +++ b/litestar/__init__.py @@ -2,7 +2,19 @@ from litestar.connection import Request, WebSocket from litestar.controller import Controller from litestar.enums import HttpMethod, MediaType -from litestar.handlers import asgi, delete, get, head, patch, post, put, route, websocket, websocket_listener +from litestar.handlers import ( + asgi, + delete, + get, + head, + patch, + post, + put, + route, + websocket, + websocket_listener, + websocket_stream, +) from litestar.response import Response from litestar.router import Router from litestar.utils.version import get_version @@ -30,4 +42,5 @@ "route", "websocket", "websocket_listener", + "websocket_stream", ) diff --git a/litestar/_asgi/routing_trie/mapping.py b/litestar/_asgi/routing_trie/mapping.py index 1a977d478d..c96db46cff 100644 --- a/litestar/_asgi/routing_trie/mapping.py +++ b/litestar/_asgi/routing_trie/mapping.py @@ -111,7 +111,7 @@ def add_route_to_trie( next_node_key = component if next_node_key not in current_node.children: - current_node.children[next_node_key] = create_node(path_template=route.path_format) + current_node.children[next_node_key] = create_node() current_node.child_keys = set(current_node.children.keys()) current_node = current_node.children[next_node_key] @@ -140,6 +140,7 @@ def configure_node( """ from litestar.routes import HTTPRoute, WebSocketRoute + node.path_template = route.path_format if not node.path_parameters: node.path_parameters = {} diff --git a/litestar/_asgi/routing_trie/traversal.py b/litestar/_asgi/routing_trie/traversal.py index 499e0e72a1..77d1b4b3d3 100644 --- a/litestar/_asgi/routing_trie/traversal.py +++ b/litestar/_asgi/routing_trie/traversal.py @@ -134,7 +134,7 @@ def parse_path_to_route( try: if path in plain_routes: asgi_app, handler = parse_node_handlers(node=root_node.children[path], method=method) - return asgi_app, handler, path, {}, root_node.path_template + return asgi_app, handler, path, {}, path if mount_paths_regex and (match := mount_paths_regex.match(path)): mount_path = path[: match.end()] diff --git a/litestar/_asgi/routing_trie/types.py b/litestar/_asgi/routing_trie/types.py index da86482dfa..a07e3d3567 100644 --- a/litestar/_asgi/routing_trie/types.py +++ b/litestar/_asgi/routing_trie/types.py @@ -34,9 +34,9 @@ class RouteTrieNode: "children", "is_asgi", "is_mount", - "is_static", "is_path_param_node", "is_path_type", + "is_static", "path_parameters", "path_template", ) @@ -68,7 +68,7 @@ class RouteTrieNode: """The path template string used to lower prometheus cardinality when group_path enabled""" -def create_node(path_template: str = "") -> RouteTrieNode: +def create_node() -> RouteTrieNode: """Create a RouteMapNode instance. Returns: @@ -85,5 +85,5 @@ def create_node(path_template: str = "") -> RouteTrieNode: is_static=False, is_path_type=False, path_parameters={}, - path_template=path_template, + path_template="", ) diff --git a/litestar/_kwargs/cleanup.py b/litestar/_kwargs/cleanup.py index 8839d360df..12e5f31af4 100644 --- a/litestar/_kwargs/cleanup.py +++ b/litestar/_kwargs/cleanup.py @@ -24,7 +24,7 @@ class DependencyCleanupGroup: exceptions caught in this manner will be re-raised after they have been thrown in the generators. """ - __slots__ = ("_generators", "_closed") + __slots__ = ("_closed", "_generators") def __init__(self, generators: list[AnyGenerator] | None = None) -> None: """Initialize ``DependencyCleanupGroup``. diff --git a/litestar/_kwargs/dependencies.py b/litestar/_kwargs/dependencies.py index bd3eb1b33c..8f5accede8 100644 --- a/litestar/_kwargs/dependencies.py +++ b/litestar/_kwargs/dependencies.py @@ -16,7 +16,7 @@ class Dependency: """Dependency graph of a given combination of ``Route`` + ``RouteHandler``""" - __slots__ = ("key", "provide", "dependencies") + __slots__ = ("dependencies", "key", "provide") def __init__(self, key: str, provide: Provide, dependencies: list[Dependency]) -> None: """Initialize a dependency. diff --git a/litestar/_kwargs/extractors.py b/litestar/_kwargs/extractors.py index 20f6a46cee..e5a7737125 100644 --- a/litestar/_kwargs/extractors.py +++ b/litestar/_kwargs/extractors.py @@ -23,6 +23,7 @@ if TYPE_CHECKING: from litestar._kwargs import KwargsModel from litestar._kwargs.parameter_definition import ParameterDefinition + from litestar._kwargs.types import Extractor from litestar.connection import ASGIConnection, Request from litestar.dto import AbstractDTO from litestar.typing import FieldDefinition @@ -83,7 +84,7 @@ def create_connection_value_extractor( connection_key: str, expected_params: set[ParameterDefinition], parser: Callable[[ASGIConnection, KwargsModel], Mapping[str, Any]] | None = None, -) -> Callable[[dict[str, Any], ASGIConnection], None]: +) -> Extractor: """Create a kwargs extractor function. Args: @@ -98,7 +99,7 @@ def create_connection_value_extractor( alias_and_key_tuples, alias_defaults, alias_to_params = _create_param_mappings(expected_params) - def extractor(values: dict[str, Any], connection: ASGIConnection) -> None: + async def extractor(values: dict[str, Any], connection: ASGIConnection) -> None: data = parser(connection, kwargs_model) if parser else getattr(connection, connection_key, {}) try: @@ -178,7 +179,7 @@ def parse_connection_headers(connection: ASGIConnection, _: KwargsModel) -> Head return Headers.from_scope(connection.scope) -def state_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: +async def state_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: """Extract the app state from the connection and insert it to the kwargs injected to the handler. Args: @@ -191,7 +192,7 @@ def state_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: values["state"] = connection.app.state._state -def headers_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: +async def headers_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: """Extract the headers from the connection and insert them to the kwargs injected to the handler. Args: @@ -206,7 +207,7 @@ def headers_extractor(values: dict[str, Any], connection: ASGIConnection) -> Non values["headers"] = dict(connection.headers.items()) -def cookies_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: +async def cookies_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: """Extract the cookies from the connection and insert them to the kwargs injected to the handler. Args: @@ -219,7 +220,7 @@ def cookies_extractor(values: dict[str, Any], connection: ASGIConnection) -> Non values["cookies"] = connection.cookies -def query_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: +async def query_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: """Extract the query params from the connection and insert them to the kwargs injected to the handler. Args: @@ -232,7 +233,7 @@ def query_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: values["query"] = connection.query_params -def scope_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: +async def scope_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: """Extract the scope from the connection and insert it into the kwargs injected to the handler. Args: @@ -245,7 +246,7 @@ def scope_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: values["scope"] = connection.scope -def request_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: +async def request_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: """Set the connection instance as the 'request' value in the kwargs injected to the handler. Args: @@ -258,7 +259,7 @@ def request_extractor(values: dict[str, Any], connection: ASGIConnection) -> Non values["request"] = connection -def socket_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: +async def socket_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: """Set the connection instance as the 'socket' value in the kwargs injected to the handler. Args: @@ -271,7 +272,7 @@ def socket_extractor(values: dict[str, Any], connection: ASGIConnection) -> None values["socket"] = connection -def body_extractor( +async def body_extractor( values: dict[str, Any], connection: Request[Any, Any, Any], ) -> None: @@ -287,7 +288,7 @@ def body_extractor( Returns: The Body value. """ - values["body"] = connection.body() + values["body"] = await connection.body() async def json_extractor(connection: Request[Any, Any, Any]) -> Any: @@ -336,16 +337,16 @@ async def _extract_multipart( if body_kwarg_multipart_form_part_limit is not None else connection.app.multipart_form_part_limit ) - connection.scope["_form"] = form_values = ( # type: ignore[typeddict-unknown-key] - connection.scope["_form"] # type: ignore[typeddict-item] - if "_form" in connection.scope - else parse_multipart_form( - body=await connection.body(), + scope_state = ScopeState.from_scope(connection.scope) + if scope_state.form is Empty: + scope_state.form = form_values = await parse_multipart_form( + stream=connection.stream(), boundary=connection.content_type[-1].get("boundary", "").encode(), multipart_form_part_limit=multipart_form_part_limit, type_decoders=connection.route_handler.resolve_type_decoders(), ) - ) + else: + form_values = scope_state.form if field_definition.is_non_string_sequence: values = list(form_values.values()) @@ -376,7 +377,7 @@ async def _extract_multipart( or (is_optional_union(tp) and is_non_string_sequence(make_non_optional_union(tp))) ) ): - form_values[name] = [value] + form_values[name] = [value] # pyright: ignore return form_values @@ -425,11 +426,13 @@ def create_url_encoded_data_extractor( async def extract_url_encoded_extractor( connection: Request[Any, Any, Any], ) -> Any: - connection.scope["_form"] = form_values = ( # type: ignore[typeddict-unknown-key] - connection.scope["_form"] # type: ignore[typeddict-item] - if "_form" in connection.scope - else parse_url_encoded_form_data(await connection.body()) - ) + scope_state = ScopeState.from_scope(connection.scope) + if scope_state.form is Empty: + scope_state.form = form_values = ( # type: ignore[assignment] + parse_url_encoded_form_data(await connection.body()) + ) + else: + form_values = scope_state.form # type: ignore[assignment] if not form_values and is_data_optional: return None @@ -441,7 +444,7 @@ async def extract_url_encoded_extractor( ) -def create_data_extractor(kwargs_model: KwargsModel) -> Callable[[dict[str, Any], ASGIConnection], None]: +def create_data_extractor(kwargs_model: KwargsModel) -> Extractor: """Create an extractor for a request's body. Args: @@ -476,11 +479,11 @@ def create_data_extractor(kwargs_model: KwargsModel) -> Callable[[dict[str, Any] "Callable[[ASGIConnection[Any, Any, Any, Any]], Coroutine[Any, Any, Any]]", json_extractor ) - def extractor( + async def extractor( values: dict[str, Any], connection: ASGIConnection[Any, Any, Any, Any], ) -> None: - values["data"] = data_extractor(connection) + values["data"] = await data_extractor(connection) return extractor diff --git a/litestar/_kwargs/kwargs_model.py b/litestar/_kwargs/kwargs_model.py index 01ed2e5aef..9d757e92f7 100644 --- a/litestar/_kwargs/kwargs_model.py +++ b/litestar/_kwargs/kwargs_model.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any from anyio import create_task_group @@ -40,6 +40,7 @@ if TYPE_CHECKING: + from litestar._kwargs.types import Extractor from litestar._signature import SignatureModel from litestar.connection import ASGIConnection from litestar.di import Provide @@ -124,11 +125,11 @@ def __init__( ) self.is_data_optional = is_data_optional - self.extractors = self._create_extractors() + self.extractors: list[Extractor] = self._create_extractors() self.dependency_batches = create_dependency_batches(expected_dependencies) - def _create_extractors(self) -> list[Callable[[dict[str, Any], ASGIConnection], None]]: - reserved_kwargs_extractors: dict[str, Callable[[dict[str, Any], ASGIConnection], None]] = { + def _create_extractors(self) -> list[Extractor]: + reserved_kwargs_extractors: dict[str, Extractor] = { "data": create_data_extractor(self), "state": state_extractor, "scope": scope_extractor, @@ -140,7 +141,7 @@ def _create_extractors(self) -> list[Callable[[dict[str, Any], ASGIConnection], "body": body_extractor, # type: ignore[dict-item] } - extractors: list[Callable[[dict[str, Any], ASGIConnection], None]] = [ + extractors: list[Extractor] = [ reserved_kwargs_extractors[reserved_kwarg] for reserved_kwarg in self.expected_reserved_kwargs ] @@ -362,7 +363,7 @@ def create_for_signature_model( sequence_query_parameter_names=sequence_query_parameter_names, ) - def to_kwargs(self, connection: ASGIConnection) -> dict[str, Any]: + async def to_kwargs(self, connection: ASGIConnection) -> dict[str, Any]: """Return a dictionary of kwargs. Async values, i.e. CoRoutines, are not resolved to ensure this function is sync. @@ -376,7 +377,7 @@ def to_kwargs(self, connection: ASGIConnection) -> dict[str, Any]: output: dict[str, Any] = {} for extractor in self.extractors: - extractor(output, connection) + await extractor(output, connection) return output diff --git a/litestar/_kwargs/types.py b/litestar/_kwargs/types.py new file mode 100644 index 0000000000..5d9f343aa9 --- /dev/null +++ b/litestar/_kwargs/types.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from typing import Any, Awaitable, Callable, Dict + +from typing_extensions import TypeAlias + +from litestar.connection import ASGIConnection + +Extractor: TypeAlias = Callable[[Dict[str, Any], ASGIConnection], Awaitable[None]] diff --git a/litestar/_multipart.py b/litestar/_multipart.py index 55b36208aa..abc1fe0b59 100644 --- a/litestar/_multipart.py +++ b/litestar/_multipart.py @@ -1,41 +1,22 @@ -"""The contents of this file were adapted from sanic. - -MIT License - -Copyright (c) 2016-present Sanic Community - -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. -""" - from __future__ import annotations import re from collections import defaultdict -from email.utils import decode_rfc2231 -from typing import TYPE_CHECKING, Any -from urllib.parse import unquote +from typing import TYPE_CHECKING, Any, AsyncGenerator + +from multipart import ( # type: ignore[import-untyped] + MultipartSegment, + ParserError, + ParserLimitReached, + PushMultipartParser, +) from litestar.datastructures.upload_file import UploadFile -from litestar.exceptions import ValidationException +from litestar.exceptions import ClientException -__all__ = ("parse_body", "parse_content_header", "parse_multipart_form") +__all__ = ("parse_content_header", "parse_multipart_form") +from litestar.utils.compat import async_next if TYPE_CHECKING: from litestar.types import TypeDecodersSequence @@ -67,34 +48,15 @@ def parse_content_header(value: str) -> tuple[str, dict[str, str]]: return value.strip().lower(), options -def parse_body(body: bytes, boundary: bytes, multipart_form_part_limit: int) -> list[bytes]: - """Split the body using the boundary - and validate the number of form parts is within the allowed limit. - - Args: - body: The form body. - boundary: The boundary used to separate form components. - multipart_form_part_limit: The limit of allowed form components - - Returns: - A list of form components. - """ - if not (body and boundary): - return [] - - form_parts = body.split(boundary, multipart_form_part_limit + 3)[1:-1] - - if len(form_parts) > multipart_form_part_limit: - raise ValidationException( - f"number of multipart components exceeds the allowed limit of {multipart_form_part_limit}, " - f"this potentially indicates a DoS attack" - ) - - return form_parts +async def _close_upload_files(fields: dict[str, list[Any]]) -> None: + for values in fields.values(): + for value in values: + if isinstance(value, UploadFile): + await value.close() -def parse_multipart_form( - body: bytes, +async def parse_multipart_form( # noqa: C901 + stream: AsyncGenerator[bytes, None], boundary: bytes, multipart_form_part_limit: int = 1000, type_decoders: TypeDecodersSequence | None = None, @@ -102,7 +64,7 @@ def parse_multipart_form( """Parse multipart form data. Args: - body: Body of the request. + stream: Body of the request. boundary: Boundary of the multipart message. multipart_form_part_limit: Limit of the number of parts allowed. type_decoders: A sequence of type decoders to use. @@ -113,51 +75,65 @@ def parse_multipart_form( fields: defaultdict[str, list[Any]] = defaultdict(list) - for form_part in parse_body(body=body, boundary=boundary, multipart_form_part_limit=multipart_form_part_limit): - file_name = None - content_type = "text/plain" - content_charset = "utf-8" - field_name = None - line_index = 2 - line_end_index = 0 - headers: list[tuple[str, str]] = [] - - while line_end_index != -1: - line_end_index = form_part.find(b"\r\n", line_index) - form_line = form_part[line_index:line_end_index].decode("utf-8") - - if not form_line: - break - - line_index = line_end_index + 2 - colon_index = form_line.index(":") - current_idx = colon_index + 2 - form_header_field = form_line[:colon_index].lower() - form_header_value, form_parameters = parse_content_header(form_line[current_idx:]) - - if form_header_field == "content-disposition": - field_name = form_parameters.get("name") - file_name = form_parameters.get("filename") - - if file_name is None and (filename_with_asterisk := form_parameters.get("filename*")): - encoding, _, value = decode_rfc2231(filename_with_asterisk) - file_name = unquote(value, encoding=encoding or content_charset) - - elif form_header_field == "content-type": - content_type = form_header_value - content_charset = form_parameters.get("charset", "utf-8") - headers.append((form_header_field, form_header_value)) - - if field_name: - post_data = form_part[line_index:-4].lstrip(b"\r\n") - if file_name: - form_file = UploadFile( - content_type=content_type, filename=file_name, file_data=post_data, headers=dict(headers) - ) - fields[field_name].append(form_file) - elif post_data: - fields[field_name].append(post_data.decode(content_charset)) - else: - fields[field_name].append(None) + chunk = await async_next(stream, b"") + if not chunk: + return fields + + data: UploadFile | bytearray = bytearray() + + try: + with PushMultipartParser(boundary, max_segment_count=multipart_form_part_limit) as parser: + segment: MultipartSegment | None = None + while not parser.closed: + for form_part in parser.parse(chunk): + if isinstance(form_part, MultipartSegment): + segment = form_part + if segment.filename: + data = UploadFile( + content_type=segment.content_type or "text/plain", + filename=segment.filename, + headers=dict(segment.headerlist), + ) + elif form_part: + if isinstance(data, UploadFile): + await data.write(form_part) + else: + data.extend(form_part) + else: + # end of part + if segment is None: + # we have reached the end of a segment before we have + # received a complete header segment + raise ClientException("Unexpected eof in multipart/form-data") + + if isinstance(data, UploadFile): + await data.seek(0) + fields[segment.name].append(data) + elif data: + fields[segment.name].append(data.decode(segment.charset or "utf-8")) + else: + fields[segment.name].append(None) + + # reset for next part + data = bytearray() + segment = None + + chunk = await async_next(stream, b"") + + except ParserError as exc: + # if an exception is raised, make sure that all 'UploadFile's are closed + if isinstance(data, UploadFile): + await data.close() + await _close_upload_files(fields) + + raise ClientException("Invalid multipart/form-data") from exc + except ParserLimitReached: + if isinstance(data, UploadFile): + await data.close() + await _close_upload_files(fields) + + # FIXME (3.0): This should raise a '413 - Request Entity Too Large', but for + # backwards compatibility, we keep it as a 400 for now + raise ClientException("Request Entity Too Large") from None return {k: v if len(v) > 1 else v[0] for k, v in fields.items()} diff --git a/litestar/_openapi/datastructures.py b/litestar/_openapi/datastructures.py index d97c8db405..91761e7eff 100644 --- a/litestar/_openapi/datastructures.py +++ b/litestar/_openapi/datastructures.py @@ -1,14 +1,71 @@ from __future__ import annotations +import re from collections import defaultdict -from typing import TYPE_CHECKING, Iterator, Sequence +from typing import TYPE_CHECKING, Iterator, Sequence, _GenericAlias # type: ignore[attr-defined] from litestar.exceptions import ImproperlyConfiguredException from litestar.openapi.spec import Reference, Schema +from litestar.params import KwargDefinition if TYPE_CHECKING: from litestar.openapi import OpenAPIConfig from litestar.plugins import OpenAPISchemaPluginProtocol + from litestar.typing import FieldDefinition + + +INVALID_KEY_CHARACTER_PATTERN = re.compile(r"[^a-zA-Z0-9._-]+") + + +def _longest_common_prefix(tuples_: list[tuple[str, ...]]) -> tuple[str, ...]: + """Find the longest common prefix of a list of tuples. + + Args: + tuples_: A list of tuples to find the longest common prefix of. + + Returns: + The longest common prefix of the tuples. + """ + prefix_ = tuples_[0] + for t in tuples_: + # Compare the current prefix with each tuple and shorten it + prefix_ = prefix_[: min(len(prefix_), len(t))] + for i in range(len(prefix_)): + if prefix_[i] != t[i]: + prefix_ = prefix_[:i] + break + return prefix_ + + +def _get_component_key_override(field: FieldDefinition) -> str | None: + if ( + (kwarg_definition := field.kwarg_definition) + and isinstance(kwarg_definition, KwargDefinition) + and (schema_key := kwarg_definition.schema_component_key) + ): + return schema_key + return None + + +def _get_normalized_schema_key(field_definition: FieldDefinition) -> tuple[str, ...]: + """Create a key for a type annotation. + + The key should be a tuple such as ``("path", "to", "type", "TypeName")``. + + Args: + field_definition: Field definition + + Returns: + A tuple of strings. + """ + if override := _get_component_key_override(field_definition): + return (override,) + + annotation = field_definition.annotation + module = getattr(annotation, "__module__", "") + name = str(annotation)[len(module) + 1 :] if isinstance(annotation, _GenericAlias) else annotation.__qualname__ + name = name.replace("..", ".") + return *module.split("."), re.sub(INVALID_KEY_CHARACTER_PATTERN, "_", name) class RegisteredSchema: @@ -43,32 +100,63 @@ def __init__(self) -> None: self._schema_key_map: dict[tuple[str, ...], RegisteredSchema] = {} self._schema_reference_map: dict[int, RegisteredSchema] = {} self._model_name_groups: defaultdict[str, list[RegisteredSchema]] = defaultdict(list) + self._component_type_map: dict[tuple[str, ...], FieldDefinition] = {} - def get_schema_for_key(self, key: tuple[str, ...]) -> Schema: + def get_schema_for_field_definition(self, field: FieldDefinition) -> Schema: """Get a registered schema by its key. Args: - key: The key to the schema to get. + field: The field definition to get the schema for Returns: A RegisteredSchema object. """ + key = _get_normalized_schema_key(field) if key not in self._schema_key_map: self._schema_key_map[key] = registered_schema = RegisteredSchema(key, Schema(), []) self._model_name_groups[key[-1]].append(registered_schema) + self._component_type_map[key] = field + else: + if (existing_type := self._component_type_map[key]) != field: + raise ImproperlyConfiguredException( + f"Schema component keys must be unique. Cannot override existing key {'_'.join(key)!r} for type " + f"{existing_type.raw!r} with new type {field.raw!r}" + ) return self._schema_key_map[key].schema - def get_reference_for_key(self, key: tuple[str, ...]) -> Reference | None: + def get_reference_for_field_definition(self, field: FieldDefinition) -> Reference | None: """Get a reference to a registered schema by its key. Args: - key: The key to the schema to get. + field: The field definition to get the reference for Returns: A Reference object. """ + key = _get_normalized_schema_key(field) if key not in self._schema_key_map: return None + + if (existing_type := self._component_type_map[key]) != field: + # TODO: This should check for strict equality, e.g. changes in type metadata + # However, this is currently not possible to do without breaking things, as + # we allow to define metadata on a type annotation in one place to be used + # for the same type in a different place, where that same type is *not* + # annotated with this metadata. The proper fix for this would be to e.g. + # inline DTO definitions when they are created at the handler level, as + # they won't be reused (they already generate a unique key), and create a + # more strict lookup policy for component schemas + msg = ( + f"Schema component keys must be unique. While obtaining a reference for the type '{field.raw!r}', the " + f"generated key {'_'.join(key)!r} was already associated with a different type '{existing_type.raw!r}'. " + ) + if key_override := _get_component_key_override(field): # pragma: no branch + # Currently, this can never not be true, however, in the future we might + # decide to do a stricter equality check as lined out above, in which + # case there can be other cases than overrides that cause this error + msg += f"Hint: Both types are defining a 'schema_component_key' with the value of {key_override!r}" + raise ImproperlyConfiguredException(msg) + registered_schema = self._schema_key_map[key] reference = Reference(f"#/components/schemas/{'_'.join(key)}") registered_schema.references.append(reference) @@ -107,26 +195,7 @@ def remove_common_prefix(tuples: list[tuple[str, ...]]) -> list[tuple[str, ...]] A list of tuples with the common prefix removed. """ - def longest_common_prefix(tuples_: list[tuple[str, ...]]) -> tuple[str, ...]: - """Find the longest common prefix of a list of tuples. - - Args: - tuples_: A list of tuples to find the longest common prefix of. - - Returns: - The longest common prefix of the tuples. - """ - prefix_ = tuples_[0] - for t in tuples_: - # Compare the current prefix with each tuple and shorten it - prefix_ = prefix_[: min(len(prefix_), len(t))] - for i in range(len(prefix_)): - if prefix_[i] != t[i]: - prefix_ = prefix_[:i] - break - return prefix_ - - prefix = longest_common_prefix(tuples) + prefix = _longest_common_prefix(tuples) prefix_length = len(prefix) return [t[prefix_length:] for t in tuples] diff --git a/litestar/_openapi/plugin.py b/litestar/_openapi/plugin.py index 96eb80afa7..78349b7e3f 100644 --- a/litestar/_openapi/plugin.py +++ b/litestar/_openapi/plugin.py @@ -53,11 +53,11 @@ def handle_schema_path_not_found(path: str = "/") -> Response: class OpenAPIPlugin(InitPluginProtocol, ReceiveRoutePlugin): __slots__ = ( - "app", - "included_routes", - "_openapi_config", "_openapi", + "_openapi_config", "_openapi_schema", + "app", + "included_routes", ) def __init__(self, app: Litestar) -> None: diff --git a/litestar/_openapi/responses.py b/litestar/_openapi/responses.py index 6b0f312d3c..1be71838f2 100644 --- a/litestar/_openapi/responses.py +++ b/litestar/_openapi/responses.py @@ -252,7 +252,12 @@ def create_additional_responses(self) -> Iterator[tuple[str, OpenAPIResponse]]: content: dict[str, OpenAPIMediaType] | None if additional_response.data_container is not None: schema = schema_creator.for_field_definition(field_def) - content = {additional_response.media_type: OpenAPIMediaType(schema=schema, examples=examples)} + media_type = additional_response.media_type + content = { + get_enum_string_value(media_type) + if not isinstance(media_type, str) + else media_type: OpenAPIMediaType(schema=schema, examples=examples) + } else: content = None diff --git a/litestar/_openapi/schema_generation/examples.py b/litestar/_openapi/schema_generation/examples.py index 49edf72868..8841bc70cb 100644 --- a/litestar/_openapi/schema_generation/examples.py +++ b/litestar/_openapi/schema_generation/examples.py @@ -14,8 +14,8 @@ from polyfactory.utils.predicates import is_union from typing_extensions import get_args -from litestar.contrib.pydantic.utils import is_pydantic_model_instance from litestar.openapi.spec import Example +from litestar.plugins.pydantic.utils import is_pydantic_model_instance from litestar.types import Empty if TYPE_CHECKING: @@ -47,7 +47,7 @@ def _normalize_example_value(value: Any) -> Any: if isinstance(value, Enum): value = value.value if is_pydantic_model_instance(value): - from litestar.contrib.pydantic import _model_dump + from litestar.plugins.pydantic import _model_dump value = _model_dump(value) if isinstance(value, (list, set)): diff --git a/litestar/_openapi/schema_generation/plugins/struct.py b/litestar/_openapi/schema_generation/plugins/struct.py index da6d8d8c6b..e8fa8e273e 100644 --- a/litestar/_openapi/schema_generation/plugins/struct.py +++ b/litestar/_openapi/schema_generation/plugins/struct.py @@ -1,19 +1,17 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal import msgspec from msgspec import Struct -from msgspec.structs import fields from litestar.plugins import OpenAPISchemaPlugin +from litestar.plugins.core._msgspec import kwarg_definition_from_field from litestar.types.empty import Empty from litestar.typing import FieldDefinition from litestar.utils.predicates import is_optional_union if TYPE_CHECKING: - from msgspec.structs import FieldInfo - from litestar._openapi.schema_generation import SchemaCreator from litestar.openapi.spec import Schema @@ -22,28 +20,46 @@ class StructSchemaPlugin(OpenAPISchemaPlugin): def is_plugin_supported_field(self, field_definition: FieldDefinition) -> bool: return not field_definition.is_union and field_definition.is_subclass_of(Struct) - def to_openapi_schema(self, field_definition: FieldDefinition, schema_creator: SchemaCreator) -> Schema: - def is_field_required(field: FieldInfo) -> bool: - return field.required or field.default_factory is Empty + @staticmethod + def _is_field_required(field: msgspec.inspect.Field) -> bool: + return field.required or field.default_factory is Empty + def to_openapi_schema(self, field_definition: FieldDefinition, schema_creator: SchemaCreator) -> Schema: type_hints = field_definition.get_type_hints(include_extras=True, resolve_generics=True) - struct_fields = fields(field_definition.type_) + struct_info: msgspec.inspect.StructType = msgspec.inspect.type_info(field_definition.type_) # type: ignore[assignment] + struct_fields = struct_info.fields + + property_fields = {} + for field in struct_fields: + field_definition_kwargs = {} + if kwarg_definition := kwarg_definition_from_field(field)[0]: + field_definition_kwargs["kwarg_definition"] = kwarg_definition + + property_fields[field.encode_name] = FieldDefinition.from_annotation( + annotation=type_hints[field.name], + name=field.encode_name, + default=field.default if field.default not in {msgspec.NODEFAULT, msgspec.UNSET} else Empty, + **field_definition_kwargs, + ) + + required = [ + field.encode_name + for field in struct_fields + if self._is_field_required(field=field) and not is_optional_union(type_hints[field.name]) + ] + + # Support tagged unions: https://jcristharif.com/msgspec/structs.html#tagged-unions + # These structs contain a tag_field and a tag. Since these fields are added + # dynamically, they are not present within the regular struct fields and don't + # have any type annotation associated with them, so we create a FieldDefinition + # manually + if struct_info.tag_field: + # using a Literal here will set these as a const in the schema + property_fields[struct_info.tag_field] = FieldDefinition.from_annotation(Literal[struct_info.tag]) # pyright: ignore + required.append(struct_info.tag_field) return schema_creator.create_component_schema( field_definition, - required=sorted( - [ - field.encode_name - for field in struct_fields - if is_field_required(field=field) and not is_optional_union(type_hints[field.name]) - ] - ), - property_fields={ - field.encode_name: FieldDefinition.from_kwarg( - type_hints[field.name], - field.encode_name, - default=field.default if field.default not in {msgspec.NODEFAULT, msgspec.UNSET} else Empty, - ) - for field in struct_fields - }, + required=sorted(required), + property_fields=property_fields, ) diff --git a/litestar/_openapi/schema_generation/schema.py b/litestar/_openapi/schema_generation/schema.py index 4ee341c94b..9464599108 100644 --- a/litestar/_openapi/schema_generation/schema.py +++ b/litestar/_openapi/schema_generation/schema.py @@ -4,7 +4,7 @@ from copy import copy from datetime import date, datetime, time, timedelta from decimal import Decimal -from enum import Enum, EnumMeta +from enum import Enum from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network from pathlib import Path from typing import ( @@ -40,10 +40,7 @@ create_string_constrained_field_schema, ) from litestar._openapi.schema_generation.utils import ( - _get_normalized_schema_key, - _should_create_enum_schema, _should_create_literal_schema, - _type_or_first_not_none_inner_type, get_json_schema_formatted_examples, ) from litestar.datastructures import SecretBytes, SecretString, UploadFile @@ -182,22 +179,6 @@ def _get_type_schema_name(field_definition: FieldDefinition) -> str: return name -def create_enum_schema(annotation: EnumMeta, include_null: bool = False) -> Schema: - """Create a schema instance for an enum. - - Args: - annotation: An enum. - include_null: Whether to include null as a possible value. - - Returns: - A schema instance. - """ - enum_values: list[str | int | None] = [v.value for v in annotation] # type: ignore[var-annotated] - if include_null and None not in enum_values: - enum_values.append(None) - return Schema(type=_types_in_list(enum_values), enum=enum_values) - - def _iter_flat_literal_args(annotation: Any) -> Iterable[Any]: """Iterate over the flattened arguments of a Literal. @@ -328,28 +309,34 @@ def for_field_definition(self, field_definition: FieldDefinition) -> Schema | Re if field_definition.is_new_type: result = self.for_new_type(field_definition) + elif field_definition.is_type_alias_type: + result = self.for_type_alias_type(field_definition) elif plugin_for_annotation := self.get_plugin_for(field_definition): result = self.for_plugin(field_definition, plugin_for_annotation) - elif _should_create_enum_schema(field_definition): - annotation = _type_or_first_not_none_inner_type(field_definition) - result = create_enum_schema(annotation, include_null=field_definition.is_optional) elif _should_create_literal_schema(field_definition): annotation = ( make_non_optional_union(field_definition.annotation) if field_definition.is_optional else field_definition.annotation ) - result = create_literal_schema(annotation, include_null=field_definition.is_optional) + result = create_literal_schema( + annotation, + include_null=field_definition.is_optional, + ) elif field_definition.is_optional: result = self.for_optional_field(field_definition) + elif field_definition.is_enum: + result = self.for_enum_field(field_definition) elif field_definition.is_union: result = self.for_union_field(field_definition) elif field_definition.is_type_var: result = self.for_typevar() - elif field_definition.inner_types and not field_definition.is_generic: - result = self.for_object_type(field_definition) elif self.is_constrained_field(field_definition): result = self.for_constrained_field(field_definition) + elif field_definition.inner_types and not field_definition.is_generic: + # this case does not recurse for all base cases, so it needs to happen + # after all non-concrete cases + result = self.for_object_type(field_definition) elif field_definition.is_subclass_of(UploadFile): result = self.for_upload_file(field_definition) else: @@ -366,6 +353,16 @@ def for_new_type(self, field_definition: FieldDefinition) -> Schema | Reference: ) ) + def for_type_alias_type(self, field_definition: FieldDefinition) -> Schema | Reference: + return self.for_field_definition( + FieldDefinition.from_kwarg( + annotation=field_definition.annotation.__value__, + name=field_definition.name, + default=field_definition.default, + kwarg_definition=field_definition.kwarg_definition, + ) + ) + @staticmethod def for_upload_file(field_definition: FieldDefinition) -> Schema: """Create schema for UploadFile. @@ -432,7 +429,7 @@ def for_optional_field(self, field_definition: FieldDefinition) -> Schema: else: result = [schema_or_reference] - return Schema(one_of=[Schema(type=OpenAPIType.NULL), *result]) + return Schema(one_of=[*result, Schema(type=OpenAPIType.NULL)]) def for_union_field(self, field_definition: FieldDefinition) -> Schema: """Create a Schema for a union FieldDefinition. @@ -472,7 +469,7 @@ def for_object_type(self, field_definition: FieldDefinition) -> Schema: if field_definition.is_non_string_sequence or field_definition.is_non_string_iterable: # filters out ellipsis from tuple[int, ...] type annotations inner_types = (f for f in field_definition.inner_types if f.annotation is not Ellipsis) - items = list(map(self.for_field_definition, inner_types or ())) + items = list(map(self.for_field_definition, inner_types)) return Schema( type=OpenAPIType.ARRAY, @@ -496,8 +493,7 @@ def for_plugin(self, field_definition: FieldDefinition, plugin: OpenAPISchemaPlu Returns: A schema instance. """ - key = _get_normalized_schema_key(field_definition.annotation) - if (ref := self.schema_registry.get_reference_for_key(key)) is not None: + if (ref := self.schema_registry.get_reference_for_field_definition(field_definition)) is not None: return ref schema = plugin.to_openapi_schema(field_definition=field_definition, schema_creator=self) @@ -554,14 +550,41 @@ def for_collection_constrained_field(self, field_definition: FieldDefinition) -> if field_definition.inner_types: items = list(map(item_creator.for_field_definition, field_definition.inner_types)) schema.items = Schema(one_of=items) if len(items) > 1 else items[0] - else: - schema.items = item_creator.for_field_definition( - FieldDefinition.from_kwarg( - field_definition.annotation.item_type, f"{field_definition.annotation.__name__}Field" - ) - ) + # INFO: Removed because it was only for pydantic constrained collections return schema + def for_enum_field( + self, + field_definition: FieldDefinition, + ) -> Schema | Reference: + """Create a schema instance for an enum. + + Args: + field_definition: A signature field instance. + + Returns: + A schema or reference instance. + """ + enum_type: None | OpenAPIType | list[OpenAPIType] = None + if issubclass(field_definition.annotation, Enum): # pragma: no branch + # This method is only called for enums, so this branch is always executed + if issubclass(field_definition.annotation, str): # StrEnum + enum_type = OpenAPIType.STRING + elif issubclass(field_definition.annotation, int): # IntEnum + enum_type = OpenAPIType.INTEGER + + enum_values: list[Any] = [v.value for v in field_definition.annotation] + if enum_type is None: + enum_type = _types_in_list(enum_values) + + schema = self.schema_registry.get_schema_for_field_definition(field_definition) + schema.type = enum_type + schema.enum = enum_values + schema.title = get_name(field_definition.annotation) + schema.description = field_definition.annotation.__doc__ + + return self.schema_registry.get_reference_for_field_definition(field_definition) or schema + def process_schema_result(self, field: FieldDefinition, schema: Schema) -> Schema | Reference: if field.kwarg_definition and field.is_const and field.has_default and schema.const is None: schema.const = field.default @@ -584,7 +607,9 @@ def process_schema_result(self, field: FieldDefinition, schema: Schema) -> Schem setattr(schema, schema_key, value) if isinstance(field.kwarg_definition, KwargDefinition) and (extra := field.kwarg_definition.schema_extra): + field_aliases = schema.field_aliases() for schema_key, value in extra.items(): + schema_key = field_aliases.get(schema_key, schema_key) if not hasattr(schema, schema_key): raise ValueError( f"`schema_extra` declares key `{schema_key}` which does not exist in `Schema` object" @@ -600,8 +625,7 @@ def process_schema_result(self, field: FieldDefinition, schema: Schema) -> Schem schema.examples = get_json_schema_formatted_examples(create_examples_for_field(field)) if schema.title and schema.type == OpenAPIType.OBJECT: - key = _get_normalized_schema_key(field.annotation) - return self.schema_registry.get_reference_for_key(key) or schema + return self.schema_registry.get_reference_for_field_definition(field) or schema return schema def create_component_schema( @@ -632,7 +656,7 @@ def create_component_schema( Returns: A schema instance. """ - schema = self.schema_registry.get_schema_for_key(_get_normalized_schema_key(type_.annotation)) + schema = self.schema_registry.get_schema_for_field_definition(type_) schema.title = title or _get_type_schema_name(type_) schema.required = required schema.type = openapi_type diff --git a/litestar/_openapi/schema_generation/utils.py b/litestar/_openapi/schema_generation/utils.py index 7ce27ca945..175a519aea 100644 --- a/litestar/_openapi/schema_generation/utils.py +++ b/litestar/_openapi/schema_generation/utils.py @@ -1,7 +1,6 @@ from __future__ import annotations -from enum import Enum -from typing import TYPE_CHECKING, Any, Mapping, _GenericAlias # type: ignore[attr-defined] +from typing import TYPE_CHECKING, Any, Mapping from litestar.utils.helpers import get_name @@ -11,54 +10,7 @@ from litestar.openapi.spec import Example from litestar.typing import FieldDefinition -__all__ = ( - "_type_or_first_not_none_inner_type", - "_should_create_enum_schema", - "_should_create_literal_schema", - "_get_normalized_schema_key", -) - - -def _type_or_first_not_none_inner_type(field_definition: FieldDefinition) -> Any: - """Get the first inner type that is not None. - - This is a narrow focussed utility to be used when we know that a field definition either represents - a single type, or a single type in a union with `None`, and we want the single type. - - Args: - field_definition: A field definition instance. - - Returns: - A field definition instance. - """ - if not field_definition.is_optional: - return field_definition.annotation - inner = next((t for t in field_definition.inner_types if not t.is_none_type), None) - if inner is None: - raise ValueError("Field definition has no inner type that is not None") - return inner.annotation - - -def _should_create_enum_schema(field_definition: FieldDefinition) -> bool: - """Predicate to determine if we should create an enum schema for the field def, or not. - - This returns true if the field definition is an enum, or if the field definition is a union - of an enum and ``None``. - - When an annotation is ``SomeEnum | None`` we should create a schema for the enum that includes ``null`` - in the enum values. - - Args: - field_definition: A field definition instance. - - Returns: - A boolean - """ - return field_definition.is_subclass_of(Enum) or ( - field_definition.is_optional - and len(field_definition.args) == 2 - and field_definition.has_inner_subclass_of(Enum) - ) +__all__ = ("_should_create_literal_schema",) def _should_create_literal_schema(field_definition: FieldDefinition) -> bool: @@ -76,30 +28,12 @@ def _should_create_literal_schema(field_definition: FieldDefinition) -> bool: Returns: A boolean """ - return ( - field_definition.is_literal - or field_definition.is_optional + return field_definition.is_literal or ( + field_definition.is_optional and all(inner.is_literal for inner in field_definition.inner_types if not inner.is_none_type) ) -def _get_normalized_schema_key(annotation: Any) -> tuple[str, ...]: - """Create a key for a type annotation. - - The key should be a tuple such as ``("path", "to", "type", "TypeName")``. - - Args: - annotation: a type annotation - - Returns: - A tuple of strings. - """ - module = getattr(annotation, "__module__", "") - name = str(annotation)[len(module) + 1 :] if isinstance(annotation, _GenericAlias) else annotation.__qualname__ - name = name.replace("..", ".") - return *module.split("."), name - - def get_formatted_examples(field_definition: FieldDefinition, examples: Sequence[Example]) -> Mapping[str, Example]: """Format the examples into the OpenAPI schema format.""" diff --git a/litestar/_openapi/typescript_converter/converter.py b/litestar/_openapi/typescript_converter/converter.py index 4782dbe2e8..b2ec87c049 100644 --- a/litestar/_openapi/typescript_converter/converter.py +++ b/litestar/_openapi/typescript_converter/converter.py @@ -264,7 +264,7 @@ def convert_openapi_to_typescript(openapi_schema: OpenAPI, namespace: str = "API """ if not openapi_schema.paths: # pragma: no cover raise ValueError("OpenAPI schema has no paths") - if not openapi_schema.components: # pragma: no cover + if not openapi_schema.components: # type: ignore[truthy-bool] # pragma: no cover raise ValueError("OpenAPI schema has no components") operations: list[TypeScriptNamespace] = [] diff --git a/litestar/_openapi/utils.py b/litestar/_openapi/utils.py index b1950fa0be..aadc67b3bc 100644 --- a/litestar/_openapi/utils.py +++ b/litestar/_openapi/utils.py @@ -10,7 +10,7 @@ from litestar.types import Method -__all__ = ("default_operation_id_creator", "SEPARATORS_CLEANUP_PATTERN") +__all__ = ("SEPARATORS_CLEANUP_PATTERN", "default_operation_id_creator") SEPARATORS_CLEANUP_PATTERN = re.compile(r"[!#$%&'*+\-.^_`|~:]+") diff --git a/litestar/_signature/model.py b/litestar/_signature/model.py index 3faec5de59..b9a8255c09 100644 --- a/litestar/_signature/model.py +++ b/litestar/_signature/model.py @@ -156,6 +156,8 @@ def _build_error_message(cls, keys: Sequence[str], exc_msg: str, connection: ASG message["source"] = "body" elif key in connection.query_params: message["source"] = ParamType.QUERY + elif key in connection.path_params: + message["source"] = ParamType.PATH elif key in cls._fields and isinstance(cls._fields[key].kwarg_definition, ParameterKwarg): if cast(ParameterKwarg, cls._fields[key].kwarg_definition).cookie: diff --git a/litestar/_signature/utils.py b/litestar/_signature/utils.py index 8c0d15fe2b..c0156450df 100644 --- a/litestar/_signature/utils.py +++ b/litestar/_signature/utils.py @@ -12,7 +12,7 @@ from litestar.utils.signature import ParsedSignature -__all__ = ("_validate_signature_dependencies", "_normalize_annotation", "_get_decoder_for_type") +__all__ = ("_get_decoder_for_type", "_normalize_annotation", "_validate_signature_dependencies") def _validate_signature_dependencies( diff --git a/litestar/app.py b/litestar/app.py index b17bff272a..399c01dc03 100644 --- a/litestar/app.py +++ b/litestar/app.py @@ -103,7 +103,7 @@ from litestar.types.callable_types import LifespanHook -__all__ = ("HandlerIndex", "Litestar", "DEFAULT_OPENAPI_CONFIG") +__all__ = ("DEFAULT_OPENAPI_CONFIG", "HandlerIndex", "Litestar") DEFAULT_OPENAPI_CONFIG = OpenAPIConfig(title="Litestar API", version="1.0.0") """The default OpenAPI config used if not configuration is explicitly passed to the @@ -136,12 +136,11 @@ class Litestar(Router): """ __slots__ = ( - "_lifespan_managers", - "_server_lifespan_managers", "_debug", + "_lifespan_managers", "_openapi_schema", + "_server_lifespan_managers", "_static_files_config", - "plugins", "after_exception", "allowed_hosts", "asgi_handler", @@ -151,6 +150,7 @@ class Litestar(Router): "cors_config", "csrf_config", "event_emitter", + "experimental_features", "get_logger", "logger", "logging_config", @@ -158,13 +158,13 @@ class Litestar(Router): "on_shutdown", "on_startup", "openapi_config", + "pdb_on_exception", + "plugins", "response_cache_config", "route_map", "state", "stores", "template_engine", - "pdb_on_exception", - "experimental_features", ) def __init__( @@ -202,6 +202,7 @@ def __init__( path: str | None = None, plugins: Sequence[PluginProtocol] | None = None, request_class: type[Request] | None = None, + request_max_body_size: int | None = 10_000_000, response_cache_config: ResponseCacheConfig | None = None, response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, @@ -286,6 +287,8 @@ def __init__( pdb_on_exception: Drop into the PDB when an exception occurs. plugins: Sequence of plugins. request_class: An optional subclass of :class:`Request <.connection.Request>` to use for http connections. + request_max_body_size: Maximum allowed size of the request body in bytes. If this size is exceeded, a + '413 - Request Entity Too Large' error response is returned. response_class: A custom subclass of :class:`Response <.response.Response>` to be used as the app's default response. response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>`. @@ -361,6 +364,7 @@ def __init__( pdb_on_exception=pdb_on_exception, plugins=self._get_default_plugins(list(plugins or [])), request_class=request_class, + request_max_body_size=request_max_body_size, response_cache_config=response_cache_config or ResponseCacheConfig(), response_class=response_class, response_cookies=response_cookies or [], @@ -464,6 +468,7 @@ def __init__( parameters=config.parameters, path=config.path, request_class=self.request_class, + request_max_body_size=request_max_body_size, response_class=config.response_class, response_cookies=config.response_cookies, response_headers=config.response_headers, @@ -538,7 +543,7 @@ def _get_default_plugins(plugins: list[PluginProtocol]) -> list[PluginProtocol]: plugins.append(MsgspecDIPlugin()) with suppress(MissingDependencyException): - from litestar.contrib.pydantic import ( + from litestar.plugins.pydantic import ( PydanticDIPlugin, PydanticInitPlugin, PydanticPlugin, diff --git a/litestar/background_tasks.py b/litestar/background_tasks.py index a47583603b..4e5182e567 100644 --- a/litestar/background_tasks.py +++ b/litestar/background_tasks.py @@ -17,7 +17,7 @@ class BackgroundTask: Background tasks are called once a Response finishes. """ - __slots__ = ("fn", "args", "kwargs") + __slots__ = ("args", "fn", "kwargs") def __init__(self, fn: Callable[P, Any], *args: P.args, **kwargs: P.kwargs) -> None: """Initialize ``BackgroundTask``. @@ -46,7 +46,7 @@ class BackgroundTasks: Background tasks are called once a Response finishes. """ - __slots__ = ("tasks", "run_in_task_group") + __slots__ = ("run_in_task_group", "tasks") def __init__(self, tasks: Iterable[BackgroundTask], run_in_task_group: bool = False) -> None: """Initialize ``BackgroundTasks``. diff --git a/litestar/channels/__init__.py b/litestar/channels/__init__.py index 0167223ddb..d6fefec6c9 100644 --- a/litestar/channels/__init__.py +++ b/litestar/channels/__init__.py @@ -2,4 +2,4 @@ from .plugin import ChannelsPlugin from .subscriber import Subscriber -__all__ = ("ChannelsPlugin", "ChannelsBackend", "Subscriber") +__all__ = ("ChannelsBackend", "ChannelsPlugin", "Subscriber") diff --git a/litestar/channels/backends/psycopg.py b/litestar/channels/backends/psycopg.py index 14b53bcd1a..599d68fa8e 100644 --- a/litestar/channels/backends/psycopg.py +++ b/litestar/channels/backends/psycopg.py @@ -1,19 +1,16 @@ from __future__ import annotations from contextlib import AsyncExitStack -from typing import AsyncGenerator, Iterable +from typing import Any, AsyncGenerator, Iterable -import psycopg +from psycopg import AsyncConnection +from psycopg.sql import SQL, Identifier -from .base import ChannelsBackend - - -def _safe_quote(ident: str) -> str: - return '"{}"'.format(ident.replace('"', '""')) # sourcery skip +from litestar.channels.backends.base import ChannelsBackend class PsycoPgChannelsBackend(ChannelsBackend): - _listener_conn: psycopg.AsyncConnection + _listener_conn: AsyncConnection[Any] def __init__(self, pg_dsn: str) -> None: self._pg_dsn = pg_dsn @@ -21,7 +18,7 @@ def __init__(self, pg_dsn: str) -> None: self._exit_stack = AsyncExitStack() async def on_startup(self) -> None: - self._listener_conn = await psycopg.AsyncConnection.connect(self._pg_dsn, autocommit=True) + self._listener_conn = await AsyncConnection[Any].connect(self._pg_dsn, autocommit=True) await self._exit_stack.enter_async_context(self._listener_conn) async def on_shutdown(self) -> None: @@ -29,21 +26,20 @@ async def on_shutdown(self) -> None: async def publish(self, data: bytes, channels: Iterable[str]) -> None: dec_data = data.decode("utf-8") - async with await psycopg.AsyncConnection.connect(self._pg_dsn) as conn: + async with await AsyncConnection[Any].connect(self._pg_dsn, autocommit=True) as conn: for channel in channels: - await conn.execute("SELECT pg_notify(%s, %s);", (channel, dec_data)) + await conn.execute(SQL("NOTIFY {channel}, {data}").format(channel=Identifier(channel), data=dec_data)) async def subscribe(self, channels: Iterable[str]) -> None: for channel in set(channels) - self._subscribed_channels: - # can't use placeholders in LISTEN - await self._listener_conn.execute(f"LISTEN {_safe_quote(channel)};") # pyright: ignore - + await self._listener_conn.execute(SQL("LISTEN {channel}").format(channel=Identifier(channel))) self._subscribed_channels.add(channel) + await self._listener_conn.commit() async def unsubscribe(self, channels: Iterable[str]) -> None: for channel in channels: - # can't use placeholders in UNLISTEN - await self._listener_conn.execute(f"UNLISTEN {_safe_quote(channel)};") # pyright: ignore + await self._listener_conn.execute(SQL("UNLISTEN {channel}").format(channel=Identifier(channel))) + await self._listener_conn.commit() self._subscribed_channels = self._subscribed_channels - set(channels) async def stream_events(self) -> AsyncGenerator[tuple[str, bytes], None]: diff --git a/litestar/cli/_utils.py b/litestar/cli/_utils.py index 41494bc5d1..c6542c1b32 100644 --- a/litestar/cli/_utils.py +++ b/litestar/cli/_utils.py @@ -47,13 +47,13 @@ __all__ = ( - "UVICORN_INSTALLED", "JSBEAUTIFIER_INSTALLED", - "LoadedApp", + "UVICORN_INSTALLED", "LitestarCLIException", "LitestarEnv", "LitestarExtensionGroup", "LitestarGroup", + "LoadedApp", "show_app_info", ) diff --git a/litestar/cli/commands/core.py b/litestar/cli/commands/core.py index a50463e5a4..59ffc17efb 100644 --- a/litestar/cli/commands/core.py +++ b/litestar/cli/commands/core.py @@ -221,11 +221,11 @@ def run_command( ) -> None: """Run a Litestar app; requires ``uvicorn``. - The app can be either passed as a module path in the form of .:, - set as an environment variable LITESTAR_APP with the same format or automatically discovered from one of these - canonical paths: app.py, asgi.py, application.py or app/__init__.py. When auto-discovering application factories, - functions with the name ``create_app`` are considered, or functions that are annotated as returning a ``Litestar`` - instance. + The app can be either passed as a module path in the form of ``.:`` + set as an environment variable ``LITESTAR_APP`` with the same format or automatically discovered from one of these + canonical paths: ``app.py``, ``asgi.py``, ``application.py`` or ``app/__init__.py``. + When auto-discovering application factories, functions with the name ``create_app`` are considered, + or functions that are annotated as returning a ``Litestar`` instance. """ if debug: diff --git a/litestar/concurrency.py b/litestar/concurrency.py index 90eadbf724..f9e2051dfb 100644 --- a/litestar/concurrency.py +++ b/litestar/concurrency.py @@ -19,11 +19,11 @@ __all__ = ( - "sync_to_thread", - "set_asyncio_executor", "get_asyncio_executor", - "set_trio_capacity_limiter", "get_trio_capacity_limiter", + "set_asyncio_executor", + "set_trio_capacity_limiter", + "sync_to_thread", ) diff --git a/litestar/config/app.py b/litestar/config/app.py index aef812ecca..170598a422 100644 --- a/litestar/config/app.py +++ b/litestar/config/app.py @@ -163,6 +163,9 @@ class AppConfig: """List of :class:`SerializationPluginProtocol <.plugins.SerializationPluginProtocol>`.""" request_class: type[Request] | None = field(default=None) """An optional subclass of :class:`Request <.connection.Request>` to use for http connections.""" + request_max_body_size: int | None | EmptyType = Empty + """Maximum allowed size of the request body in bytes. If this size is exceeded, a '413 - Request Entity Too Large' + error response is returned.""" response_class: type[Response] | None = field(default=None) """A custom subclass of :class:`Response <.response.Response>` to be used as the app's default response.""" response_cookies: ResponseCookies = field(default_factory=list) @@ -227,3 +230,6 @@ def __post_init__(self) -> None: class ExperimentalFeatures(str, enum.Enum): DTO_CODEGEN = "DTO_CODEGEN" + """Enable DTO codegen.""" + FUTURE = "FUTURE" + """Enable future features that may be considered breaking or changing.""" diff --git a/litestar/config/response_cache.py b/litestar/config/response_cache.py index 4f1dfe9698..8392f5b2d9 100644 --- a/litestar/config/response_cache.py +++ b/litestar/config/response_cache.py @@ -17,7 +17,7 @@ from litestar.stores.base import Store from litestar.types import CacheKeyBuilder, HTTPScope -__all__ = ("ResponseCacheConfig", "default_cache_key_builder", "CACHE_FOREVER") +__all__ = ("CACHE_FOREVER", "ResponseCacheConfig", "default_cache_key_builder") @final diff --git a/litestar/connection/base.py b/litestar/connection/base.py index d14c6620e5..6c80e96522 100644 --- a/litestar/connection/base.py +++ b/litestar/connection/base.py @@ -57,15 +57,15 @@ class ASGIConnection(Generic[HandlerT, UserT, AuthT, StateT]): """The base ASGI connection container.""" __slots__ = ( - "scope", - "receive", - "send", "_base_url", - "_url", - "_parsed_query", + "_connection_state", "_cookies", + "_parsed_query", "_server_extensions", - "_connection_state", + "_url", + "receive", + "scope", + "send", ) scope: Scope diff --git a/litestar/connection/request.py b/litestar/connection/request.py index 23c60f0b3c..065312d7ed 100644 --- a/litestar/connection/request.py +++ b/litestar/connection/request.py @@ -1,7 +1,8 @@ from __future__ import annotations +import math import warnings -from typing import TYPE_CHECKING, Any, AsyncGenerator, Generic +from typing import TYPE_CHECKING, Any, AsyncGenerator, Generic, cast from litestar._multipart import parse_content_header, parse_multipart_form from litestar._parsers import parse_url_encoded_form_data @@ -17,12 +18,14 @@ from litestar.datastructures.multi_dicts import FormMultiDict from litestar.enums import ASGIExtension, RequestEncodingType from litestar.exceptions import ( + ClientException, InternalServerException, LitestarException, LitestarWarning, ) +from litestar.exceptions.http_exceptions import RequestEntityTooLarge from litestar.serialization import decode_json, decode_msgpack -from litestar.types import Empty +from litestar.types import Empty, HTTPReceiveMessage __all__ = ("Request",) @@ -46,12 +49,13 @@ class Request(Generic[UserT, AuthT, StateT], ASGIConnection["HTTPRouteHandler", """The Litestar Request class.""" __slots__ = ( - "_json", - "_form", + "_accept", "_body", - "_msgpack", + "_content_length", "_content_type", - "_accept", + "_form", + "_json", + "_msgpack", "is_connected", "supports_push_promise", ) @@ -73,12 +77,13 @@ def __init__(self, scope: Scope, receive: Receive = empty_receive, send: Send = """ super().__init__(scope, receive, send) self.is_connected: bool = True - self._body: bytes | EmptyType = Empty + self._body: bytes | EmptyType = self._connection_state.body self._form: FormMultiDict | EmptyType = Empty self._json: Any = Empty self._msgpack: Any = Empty self._content_type: tuple[str, dict[str, str]] | EmptyType = Empty self._accept: Accept | EmptyType = Empty + self._content_length: int | None | EmptyType = Empty self.supports_push_promise = ASGIExtension.SERVER_PUSH in self._server_extensions @property @@ -152,6 +157,21 @@ async def msgpack(self) -> Any: ) return self._msgpack + @property + def content_length(self) -> int | None: + cached_content_length = self._content_length + if cached_content_length is not Empty: + return cached_content_length + + content_length_header = self.headers.get("content-length") + try: + content_length = self._content_length = ( + int(content_length_header) if content_length_header is not None else None + ) + except ValueError: + raise ClientException(f"Invalid content-length: {content_length_header!r}") from None + return content_length + async def stream(self) -> AsyncGenerator[bytes, None]: """Return an async generator that streams chunks of bytes. @@ -164,10 +184,46 @@ async def stream(self) -> AsyncGenerator[bytes, None]: if self._body is Empty: if not self.is_connected: raise InternalServerException("stream consumed") - while event := await self.receive(): + + announced_content_length = self.content_length + # setting this to 'math.inf' as a micro-optimisation; Comparing against a + # float is slightly faster than checking if a value is 'None' and then + # comparing it to an int. since we expect a limit to be set most of the + # time, this is a bit more efficient + max_content_length = self.route_handler.resolve_request_max_body_size() or math.inf + + # if the 'content-length' header is set, and exceeds the limit, we can bail + # out early before reading anything + if announced_content_length is not None and announced_content_length > max_content_length: + raise RequestEntityTooLarge + + total_bytes_streamed: int = 0 + while event := cast("HTTPReceiveMessage", await self.receive()): if event["type"] == "http.request": - if event["body"]: - yield event["body"] + body = event["body"] + if body: + total_bytes_streamed += len(body) + + # if a 'content-length' header was set, check if we have + # received more bytes than specified. in most cases this should + # be caught before it hits the application layer and an ASGI + # server (e.g. uvicorn) will not allow this, but since it's not + # forbidden according to the HTTP or ASGI spec, we err on the + # side of caution and still perform this check. + # + # uvicorn documented behaviour for this case: + # https://github.com/encode/uvicorn/blob/fe3910083e3990695bc19c2ef671dd447262ae18/docs/server-behavior.md?plain=1#L11 + if announced_content_length: + if total_bytes_streamed > announced_content_length: + raise ClientException("Malformed request") + + # we don't have a 'content-length' header, likely a chunked + # transfer. we don't really care and simply check if we have + # received more bytes than allowed + elif total_bytes_streamed > max_content_length: + raise RequestEntityTooLarge + + yield body if not event.get("more_body", False): break @@ -208,31 +264,21 @@ async def form(self) -> FormMultiDict: if (form_data := self._connection_state.form) is Empty: content_type, options = self.content_type if content_type == RequestEncodingType.MULTI_PART: - form_data = parse_multipart_form( - body=await self.body(), + form_data = await parse_multipart_form( + stream=self.stream(), boundary=options.get("boundary", "").encode(), multipart_form_part_limit=self.app.multipart_form_part_limit, ) elif content_type == RequestEncodingType.URL_ENCODED: - form_data = parse_url_encoded_form_data( + form_data = parse_url_encoded_form_data( # type: ignore[assignment] await self.body(), ) else: form_data = {} - self._connection_state.form = form_data + self._connection_state.form = form_data # pyright: ignore - # form_data is a dict[str, list[str] | str | UploadFile]. Convert it to a - # list[tuple[str, str | UploadFile]] before passing it to FormMultiDict so - # multi-keys can be accessed properly - items = [] - for k, v in form_data.items(): - if isinstance(v, list): - for sv in v: - items.append((k, sv)) - else: - items.append((k, v)) - self._form = FormMultiDict(items) + self._form = FormMultiDict.from_form_data(cast("dict[str, Any]", form_data)) return self._form diff --git a/litestar/contrib/attrs/__init__.py b/litestar/contrib/attrs/__init__.py index ddd2a3f9b8..0045680697 100644 --- a/litestar/contrib/attrs/__init__.py +++ b/litestar/contrib/attrs/__init__.py @@ -1,3 +1,30 @@ -from .attrs_schema_plugin import AttrsSchemaPlugin +# ruff: noqa: TC004, F401 +from __future__ import annotations -__all__ = ("AttrsSchemaPlugin",) +from typing import TYPE_CHECKING, Any + +from litestar.utils import warn_deprecation + +__all__ = ("AttrsSchemaPlugin", "is_attrs_class") + + +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + from litestar.plugins.attrs import AttrsSchemaPlugin, is_attrs_class + + warn_deprecation( + deprecated_name=f"litestar.contrib.attrs.{attr_name}", + version="2.13.0", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.attrs' is deprecated, please " + f"import it from 'litestar.plugins.attrs' instead", + ) + value = globals()[attr_name] = locals()[attr_name] + return value + + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover + + +if TYPE_CHECKING: + from litestar.plugins.attrs import AttrsSchemaPlugin, is_attrs_class diff --git a/litestar/contrib/attrs/attrs_schema_plugin.py b/litestar/contrib/attrs/attrs_schema_plugin.py index 76f7491982..5b1bb194bd 100644 --- a/litestar/contrib/attrs/attrs_schema_plugin.py +++ b/litestar/contrib/attrs/attrs_schema_plugin.py @@ -1,64 +1,30 @@ +# ruff: noqa: TC004, F401 from __future__ import annotations from typing import TYPE_CHECKING, Any -from typing_extensions import TypeGuard +from litestar.utils import warn_deprecation -from litestar.exceptions import MissingDependencyException -from litestar.plugins import OpenAPISchemaPluginProtocol -from litestar.types import Empty -from litestar.typing import FieldDefinition -from litestar.utils import is_optional_union +__all__ = ("AttrsSchemaPlugin", "is_attrs_class") -try: - import attr - import attrs -except ImportError as e: - raise MissingDependencyException("attrs") from e -if TYPE_CHECKING: - from litestar._openapi.schema_generation import SchemaCreator - from litestar.openapi.spec import Schema - - -class AttrsSchemaPlugin(OpenAPISchemaPluginProtocol): - @staticmethod - def is_plugin_supported_type(value: Any) -> bool: - return is_attrs_class(value) or is_attrs_class(type(value)) - - def to_openapi_schema(self, field_definition: FieldDefinition, schema_creator: SchemaCreator) -> Schema: - """Given a type annotation, transform it into an OpenAPI schema class. +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + from litestar.plugins.attrs import AttrsSchemaPlugin, is_attrs_class - Args: - field_definition: FieldDefinition instance. - schema_creator: An instance of the schema creator class - - Returns: - An :class:`OpenAPI ` instance. - """ - - type_hints = field_definition.get_type_hints(include_extras=True, resolve_generics=True) - attr_fields = attr.fields_dict(field_definition.type_) - return schema_creator.create_component_schema( - field_definition, - required=sorted( - field_name - for field_name, attribute in attr_fields.items() - if attribute.default is attrs.NOTHING and not is_optional_union(type_hints[field_name]) - ), - property_fields={ - field_name: FieldDefinition.from_kwarg(type_hints[field_name], field_name) for field_name in attr_fields - }, + warn_deprecation( + deprecated_name=f"litestar.contrib.attrs.attrs_schema_plugin.{attr_name}", + version="2.13.0", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.attrs.attrs_schema_plugin' is deprecated, please " + f"import it from 'litestar.plugins.attrs' instead", ) + value = globals()[attr_name] = locals()[attr_name] + return value + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover -def is_attrs_class(annotation: Any) -> TypeGuard[type[attrs.AttrsInstance]]: # pyright: ignore - """Given a type annotation determine if the annotation is a class that includes an attrs attribute. - Args: - annotation: A type. - - Returns: - A typeguard determining whether the type is an attrs class. - """ - return attrs.has(annotation) if attrs is not Empty else False # type: ignore[comparison-overlap] +if TYPE_CHECKING: + from litestar.plugins.attrs import AttrsSchemaPlugin, is_attrs_class diff --git a/litestar/contrib/htmx/_utils.py b/litestar/contrib/htmx/_utils.py index 894fd25bf2..a313c24ce6 100644 --- a/litestar/contrib/htmx/_utils.py +++ b/litestar/contrib/htmx/_utils.py @@ -1,12 +1,22 @@ from __future__ import annotations -from enum import Enum -from typing import TYPE_CHECKING, Any, Callable, cast -from urllib.parse import quote +from typing import TYPE_CHECKING -from litestar.exceptions import ImproperlyConfiguredException -from litestar.serialization import encode_json +from litestar.utils import warn_deprecation +if TYPE_CHECKING: + from litestar_htmx._utils import ( # noqa: TC004 + HTMXHeaders, + get_headers, + get_location_headers, + get_push_url_header, + get_redirect_header, + get_refresh_header, + get_replace_url_header, + get_reswap_header, + get_retarget_header, + get_trigger_event_headers, + ) __all__ = ( "HTMXHeaders", "get_headers", @@ -21,128 +31,21 @@ ) -if TYPE_CHECKING: - from litestar.contrib.htmx.types import ( - EventAfterType, - HtmxHeaderType, - LocationType, - PushUrlType, - ReSwapMethod, - TriggerEventType, - ) - -HTMX_STOP_POLLING = 286 - - -class HTMXHeaders(str, Enum): - """Enum for HTMX Headers""" - - REDIRECT = "HX-Redirect" - REFRESH = "HX-Refresh" - PUSH_URL = "HX-Push-Url" - REPLACE_URL = "HX-Replace-Url" - RE_SWAP = "HX-Reswap" - RE_TARGET = "HX-Retarget" - LOCATION = "HX-Location" - - TRIGGER_EVENT = "HX-Trigger" - TRIGGER_AFTER_SETTLE = "HX-Trigger-After-Settle" - TRIGGER_AFTER_SWAP = "HX-Trigger-After-Swap" - - REQUEST = "HX-Request" - BOOSTED = "HX-Boosted" - CURRENT_URL = "HX-Current-URL" - HISTORY_RESTORE_REQUEST = "HX-History-Restore-Request" - PROMPT = "HX-Prompt" - TARGET = "HX-Target" - TRIGGER_ID = "HX-Trigger" # noqa: PIE796 - TRIGGER_NAME = "HX-Trigger-Name" - TRIGGERING_EVENT = "Triggering-Event" - - -def get_trigger_event_headers(trigger_event: TriggerEventType) -> dict[str, Any]: - """Return headers for trigger event response.""" - after_params: dict[EventAfterType, str] = { - "receive": HTMXHeaders.TRIGGER_EVENT.value, - "settle": HTMXHeaders.TRIGGER_AFTER_SETTLE.value, - "swap": HTMXHeaders.TRIGGER_AFTER_SWAP.value, - } - - if trigger_header := after_params.get(trigger_event["after"]): - return {trigger_header: encode_json({trigger_event["name"]: trigger_event["params"] or {}}).decode()} - - raise ImproperlyConfiguredException( - "invalid value for 'after' param- allowed values are 'receive', 'settle' or 'swap'." - ) - - -def get_redirect_header(url: str) -> dict[str, Any]: - """Return headers for redirect response.""" - return {HTMXHeaders.REDIRECT.value: quote(url, safe="/#%[]=:;$&()+,!?*@'~"), "Location": ""} - - -def get_push_url_header(url: PushUrlType) -> dict[str, Any]: - """Return headers for push url to browser history response.""" - if isinstance(url, str): - url = url if url != "False" else "false" - elif isinstance(url, bool): - url = "false" - - return {HTMXHeaders.PUSH_URL.value: url} - - -def get_replace_url_header(url: PushUrlType) -> dict[str, Any]: - """Return headers for replace url in browser tab response.""" - url = (url if url != "False" else "false") if isinstance(url, str) else "false" - return {HTMXHeaders.REPLACE_URL: url} - - -def get_refresh_header(refresh: bool) -> dict[str, Any]: - """Return headers for client refresh response.""" - return {HTMXHeaders.REFRESH.value: "true" if refresh else ""} - - -def get_reswap_header(method: ReSwapMethod) -> dict[str, Any]: - """Return headers for change swap method response.""" - return {HTMXHeaders.RE_SWAP.value: method} - - -def get_retarget_header(target: str) -> dict[str, Any]: - """Return headers for change target element response.""" - return {HTMXHeaders.RE_TARGET.value: target} - - -def get_location_headers(location: LocationType) -> dict[str, Any]: - """Return headers for redirect without page-reload response.""" - if spec := {key: value for key, value in location.items() if value}: - return {HTMXHeaders.LOCATION.value: encode_json(spec).decode()} - raise ValueError("redirect_to is required parameter.") +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + from litestar_htmx import _utils as utils + module = "litestar.plugins.htmx._utils" + value = globals()[attr_name] = getattr(utils, attr_name) -def get_headers(hx_headers: HtmxHeaderType) -> dict[str, Any]: - """Return headers for HTMX responses.""" - if not hx_headers: - raise ValueError("Value for hx_headers cannot be None.") - htmx_headers_dict: dict[str, Callable] = { - "redirect": get_redirect_header, - "refresh": get_refresh_header, - "push_url": get_push_url_header, - "replace_url": get_replace_url_header, - "re_swap": get_reswap_header, - "re_target": get_retarget_header, - "trigger_event": get_trigger_event_headers, - "location": get_location_headers, - } + warn_deprecation( + deprecated_name=f"litestar.contrib.htmx._utils.{attr_name}", + version="2.13", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.htmx._utils' is deprecated, please import it from '{module}' instead", + ) - header: dict[str, Any] = {} - response: dict[str, Any] - key: str - value: Any + return value - for key, value in hx_headers.items(): - if key in ["redirect", "refresh", "location", "replace_url"]: - return cast("dict[str, Any]", htmx_headers_dict[key](value)) - if value is not None: - response = htmx_headers_dict[key](value) - header.update(response) - return header + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") diff --git a/litestar/contrib/htmx/request.py b/litestar/contrib/htmx/request.py index b4fad18a9b..54c7fb7fa5 100644 --- a/litestar/contrib/htmx/request.py +++ b/litestar/contrib/htmx/request.py @@ -1,113 +1,33 @@ from __future__ import annotations -from contextlib import suppress -from functools import cached_property -from typing import TYPE_CHECKING, Any -from urllib.parse import unquote, urlsplit, urlunsplit - -from litestar import Request -from litestar.connection.base import empty_receive, empty_send -from litestar.contrib.htmx._utils import HTMXHeaders -from litestar.exceptions import SerializationException -from litestar.serialization import decode_json - -__all__ = ("HTMXDetails", "HTMXRequest") +from typing import TYPE_CHECKING +from litestar.utils import warn_deprecation if TYPE_CHECKING: - from litestar.types import Receive, Scope, Send - - -class HTMXDetails: - """HTMXDetails holds all the values sent by HTMX client in headers and provide convenient properties.""" - - def __init__(self, request: Request) -> None: - """Initialize :class:`HTMXDetails`""" - self.request = request - - def _get_header_value(self, name: HTMXHeaders) -> str | None: - """Parse request header - - Check for uri encoded header and unquotes it in readable format. - """ - - if value := self.request.headers.get(name.value.lower()): - is_uri_encoded = self.request.headers.get(f"{name.value.lower()}-uri-autoencoded") == "true" - return unquote(value) if is_uri_encoded else value - return None - - def __bool__(self) -> bool: - """Check if request is sent by an HTMX client.""" - return self._get_header_value(HTMXHeaders.REQUEST) == "true" + from litestar_htmx import ( # noqa: TC004 + HTMXDetails, + HTMXRequest, + ) - @cached_property - def boosted(self) -> bool: - """Check if request is boosted.""" - return self._get_header_value(HTMXHeaders.BOOSTED) == "true" - - @cached_property - def current_url(self) -> str | None: - """Current url value sent by HTMX client.""" - return self._get_header_value(HTMXHeaders.CURRENT_URL) - - @cached_property - def current_url_abs_path(self) -> str | None: - """Current url abs path value, to get query and path parameter sent by HTMX client.""" - if self.current_url: - split = urlsplit(self.current_url) - if split.scheme == self.request.scope["scheme"] and split.netloc == self.request.headers.get("host"): - return str(urlunsplit(split._replace(scheme="", netloc=""))) - return None - return self.current_url - - @cached_property - def history_restore_request(self) -> bool: - """If True then, request is for history restoration after a miss in the local history cache.""" - return self._get_header_value(HTMXHeaders.HISTORY_RESTORE_REQUEST) == "true" - - @cached_property - def prompt(self) -> str | None: - """User Response to prompt. - - .. code-block:: html - - - """ - return self._get_header_value(HTMXHeaders.PROMPT) - - @cached_property - def target(self) -> str | None: - """ID of the target element if provided on the element.""" - return self._get_header_value(HTMXHeaders.TARGET) - - @cached_property - def trigger(self) -> str | None: - """ID of the triggered element if provided on the element.""" - return self._get_header_value(HTMXHeaders.TRIGGER_ID) - - @cached_property - def trigger_name(self) -> str | None: - """Name of the triggered element if provided on the element.""" - return self._get_header_value(HTMXHeaders.TRIGGER_NAME) +__all__ = ("HTMXDetails", "HTMXRequest") - @cached_property - def triggering_event(self) -> Any: - """Name of the triggered event. - This value is added by ``event-header`` extension of HTMX to the ``Triggering-Event`` header to requests. - """ - if value := self._get_header_value(HTMXHeaders.TRIGGERING_EVENT): - with suppress(SerializationException): - return decode_json(value=value, type_decoders=self.request.route_handler.resolve_type_decoders()) - return None +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + import litestar_htmx + module = "litestar.plugins.htmx" + value = globals()[attr_name] = getattr(litestar_htmx, attr_name) -class HTMXRequest(Request): - """HTMX Request class to work with HTMX client.""" + warn_deprecation( + deprecated_name=f"litestar.contrib.htmx.request.{attr_name}", + version="2.13", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.htmx.request' is deprecated, please import it from '{module}' instead", + ) - __slots__ = ("htmx",) + return value - def __init__(self, scope: Scope, receive: Receive = empty_receive, send: Send = empty_send) -> None: - """Initialize :class:`HTMXRequest`""" - super().__init__(scope=scope, receive=receive, send=send) - self.htmx = HTMXDetails(self) + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") diff --git a/litestar/contrib/htmx/response.py b/litestar/contrib/htmx/response.py index 0a56e1f7ec..aa1abd5fc1 100644 --- a/litestar/contrib/htmx/response.py +++ b/litestar/contrib/htmx/response.py @@ -1,20 +1,22 @@ from __future__ import annotations -from typing import Any, Generic, TypeVar -from urllib.parse import quote - -from litestar import Response -from litestar.contrib.htmx._utils import HTMX_STOP_POLLING, get_headers -from litestar.contrib.htmx.types import ( - EventAfterType, - HtmxHeaderType, - LocationType, - PushUrlType, - ReSwapMethod, - TriggerEventType, -) -from litestar.response import Template -from litestar.status_codes import HTTP_200_OK +from typing import TYPE_CHECKING + +from litestar.utils import warn_deprecation + +if TYPE_CHECKING: + from litestar_htmx import ( # noqa: TC004 + ClientRedirect, + ClientRefresh, + HTMXTemplate, + HXLocation, + HXStopPolling, + PushUrl, + ReplaceUrl, + Reswap, + Retarget, + TriggerEvent, + ) __all__ = ( "ClientRedirect", @@ -30,171 +32,21 @@ ) -# HTMX defined HTTP status code. -# Response carrying this status code will ask client to stop Polling. -T = TypeVar("T") - - -class HXStopPolling(Response): - """Stop HTMX client from Polling.""" - - def __init__(self) -> None: - """Initialize""" - super().__init__(content=None) - self.status_code = HTMX_STOP_POLLING - - -class ClientRedirect(Response): - """HTMX Response class to support client side redirect.""" - - def __init__(self, redirect_to: str) -> None: - """Set status code to 200 (required by HTMX), and pass redirect url.""" - super().__init__(content=None, headers=get_headers(hx_headers=HtmxHeaderType(redirect=redirect_to))) - del self.headers["Location"] - +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + import litestar_htmx -class ClientRefresh(Response): - """Response to support HTMX client page refresh""" + module = "litestar.plugins.htmx" + value = globals()[attr_name] = getattr(litestar_htmx, attr_name) - def __init__(self) -> None: - """Set Status code to 200 and set headers.""" - super().__init__(content=None, headers=get_headers(hx_headers=HtmxHeaderType(refresh=True))) - - -class PushUrl(Generic[T], Response[T]): - """Response to push new url into the history stack.""" - - def __init__(self, content: T, push_url: PushUrlType, **kwargs: Any) -> None: - """Initialize PushUrl.""" - super().__init__( - content=content, - status_code=HTTP_200_OK, - headers=get_headers(hx_headers=HtmxHeaderType(push_url=push_url)), - **kwargs, + warn_deprecation( + deprecated_name=f"litestar.contrib.htmx.response.{attr_name}", + version="2.13", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.htmx.response' is deprecated, please import it from '{module}' instead", ) + return value -class ReplaceUrl(Generic[T], Response[T]): - """Response to replace url in the Browser Location bar.""" - - def __init__(self, content: T, replace_url: PushUrlType, **kwargs: Any) -> None: - """Initialize ReplaceUrl.""" - super().__init__( - content=content, - status_code=HTTP_200_OK, - headers=get_headers(hx_headers=HtmxHeaderType(replace_url=replace_url)), - **kwargs, - ) - - -class Reswap(Generic[T], Response[T]): - """Response to specify how the response will be swapped.""" - - def __init__( - self, - content: T, - method: ReSwapMethod, - **kwargs: Any, - ) -> None: - """Initialize Reswap.""" - super().__init__(content=content, headers=get_headers(hx_headers=HtmxHeaderType(re_swap=method)), **kwargs) - - -class Retarget(Generic[T], Response[T]): - """Response to target different element on the page.""" - - def __init__(self, content: T, target: str, **kwargs: Any) -> None: - """Initialize Retarget.""" - super().__init__(content=content, headers=get_headers(hx_headers=HtmxHeaderType(re_target=target)), **kwargs) - - -class TriggerEvent(Generic[T], Response[T]): - """Trigger Client side event.""" - - def __init__( - self, - content: T, - name: str, - after: EventAfterType, - params: dict[str, Any] | None = None, - **kwargs: Any, - ) -> None: - """Initialize TriggerEvent.""" - event = TriggerEventType(name=name, params=params, after=after) - headers = get_headers(hx_headers=HtmxHeaderType(trigger_event=event)) - super().__init__(content=content, headers=headers, **kwargs) - - -class HXLocation(Response): - """Client side redirect without full page reload.""" - - def __init__( - self, - redirect_to: str, - source: str | None = None, - event: str | None = None, - target: str | None = None, - swap: ReSwapMethod | None = None, - hx_headers: dict[str, Any] | None = None, - values: dict[str, str] | None = None, - **kwargs: Any, - ) -> None: - """Initialize HXLocation, Set status code to 200 (required by HTMX), - and pass redirect url. - """ - super().__init__( - content=None, - headers={"Location": quote(redirect_to, safe="/#%[]=:;$&()+,!?*@'~")}, - **kwargs, - ) - spec: dict[str, Any] = get_headers( - hx_headers=HtmxHeaderType( - location=LocationType( - path=str(self.headers.get("Location")), - source=source, - event=event, - target=target, - swap=swap, - values=values, - hx_headers=hx_headers, - ) - ) - ) - del self.headers["Location"] - self.headers.update(spec) - - -class HTMXTemplate(Template): - """HTMX template wrapper""" - - def __init__( - self, - push_url: PushUrlType | None = None, - re_swap: ReSwapMethod | None = None, - re_target: str | None = None, - trigger_event: str | None = None, - params: dict[str, Any] | None = None, - after: EventAfterType | None = None, - **kwargs: Any, - ) -> None: - """Create HTMXTemplate response. - - Args: - push_url: Either a string value specifying a URL to push to browser history or ``False`` to prevent HTMX client from - pushing a url to browser history. - re_swap: Method value to instruct HTMX which swapping method to use. - re_target: Value for 'id of target element' to apply changes to. - trigger_event: Event name to trigger. - params: Dictionary of parameters if any required with trigger event parameter. - after: Changes to apply after ``receive``, ``settle`` or ``swap`` event. - **kwargs: Additional arguments to pass to ``Template``. - """ - super().__init__(**kwargs) - - event: TriggerEventType | None = None - if trigger_event: - event = TriggerEventType(name=str(trigger_event), params=params, after=after) - - self.headers.update( - get_headers(HtmxHeaderType(push_url=push_url, re_swap=re_swap, re_target=re_target, trigger_event=event)) - ) + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") diff --git a/litestar/contrib/htmx/types.py b/litestar/contrib/htmx/types.py index aa8f9cdb31..a83806ed69 100644 --- a/litestar/contrib/htmx/types.py +++ b/litestar/contrib/htmx/types.py @@ -1,54 +1,33 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Literal, TypedDict, Union +from typing import TYPE_CHECKING +from litestar.utils import warn_deprecation + +if TYPE_CHECKING: + from litestar_htmx import HtmxHeaderType, LocationType, TriggerEventType # noqa: TC004 __all__ = ( "HtmxHeaderType", "LocationType", "TriggerEventType", ) -if TYPE_CHECKING: - from typing_extensions import Required - - -EventAfterType = Literal["receive", "settle", "swap", None] - -PushUrlType = Union[str, bool] - -ReSwapMethod = Literal[ - "innerHTML", "outerHTML", "beforebegin", "afterbegin", "beforeend", "afterend", "delete", "none", None -] - - -class LocationType(TypedDict): - """Type for HX-Location header.""" - - path: Required[str] - source: str | None - event: str | None - target: str | None - swap: ReSwapMethod | None - values: dict[str, str] | None - hx_headers: dict[str, Any] | None - -class TriggerEventType(TypedDict): - """Type for HX-Trigger header.""" +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + import litestar_htmx - name: Required[str] - params: dict[str, Any] | None - after: EventAfterType | None + module = "litestar.plugins.htmx" + value = globals()[attr_name] = getattr(litestar_htmx, attr_name) + warn_deprecation( + deprecated_name=f"litestar.contrib.htmx.types.{attr_name}", + version="2.13", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.htmx.types' is deprecated, please import it from '{module}' instead", + ) -class HtmxHeaderType(TypedDict, total=False): - """Type for hx_headers parameter in get_headers().""" + return value - location: LocationType | None - redirect: str | None - refresh: bool - push_url: PushUrlType | None - replace_url: PushUrlType | None - re_swap: ReSwapMethod | None - re_target: str | None - trigger_event: TriggerEventType | None + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") diff --git a/litestar/contrib/opentelemetry/plugin.py b/litestar/contrib/opentelemetry/plugin.py index b8f60d6d5b..6d5f34677c 100644 --- a/litestar/contrib/opentelemetry/plugin.py +++ b/litestar/contrib/opentelemetry/plugin.py @@ -15,7 +15,7 @@ class OpenTelemetryPlugin(InitPluginProtocol): """OpenTelemetry Plugin.""" - __slots__ = ("config", "_middleware") + __slots__ = ("_middleware", "config") def __init__(self, config: OpenTelemetryConfig | None = None) -> None: self.config = config or OpenTelemetryConfig() @@ -42,7 +42,8 @@ def _pop_otel_middleware(middlewares: list[Middleware]) -> tuple[list[Middleware for middleware in middlewares: if ( isinstance(middleware, DefineMiddleware) - and middleware.middleware is OpenTelemetryInstrumentationMiddleware + and isinstance(middleware.middleware, type) + and issubclass(middleware.middleware, OpenTelemetryInstrumentationMiddleware) ): otel_middleware = middleware else: diff --git a/litestar/contrib/prometheus/__init__.py b/litestar/contrib/prometheus/__init__.py index 1ccb494695..7959799731 100644 --- a/litestar/contrib/prometheus/__init__.py +++ b/litestar/contrib/prometheus/__init__.py @@ -1,5 +1,38 @@ -from .config import PrometheusConfig -from .controller import PrometheusController -from .middleware import PrometheusMiddleware +# ruff: noqa: TC004, F401 +from __future__ import annotations -__all__ = ("PrometheusMiddleware", "PrometheusConfig", "PrometheusController") +from typing import TYPE_CHECKING + +from litestar.utils import warn_deprecation + +__all__ = ("PrometheusConfig", "PrometheusController", "PrometheusMiddleware") + + +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + from litestar.plugins.prometheus import ( + PrometheusConfig, + PrometheusController, + PrometheusMiddleware, + ) + + warn_deprecation( + deprecated_name=f"litestar.contrib.prometheus.{attr_name}", + version="2.13.0", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.prometheus' is deprecated, please " + f"import it from 'litestar.plugins.prometheus' instead", + ) + value = globals()[attr_name] = locals()[attr_name] + return value + + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover + + +if TYPE_CHECKING: + from litestar.plugins.prometheus import ( + PrometheusConfig, + PrometheusController, + PrometheusMiddleware, + ) diff --git a/litestar/contrib/prometheus/config.py b/litestar/contrib/prometheus/config.py index 6b0ceb6409..5eb0c4e5ef 100644 --- a/litestar/contrib/prometheus/config.py +++ b/litestar/contrib/prometheus/config.py @@ -1,67 +1,30 @@ +# ruff: noqa: TC004, F401 from __future__ import annotations -from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Callable, Mapping, Sequence +from typing import TYPE_CHECKING -from litestar.contrib.prometheus.middleware import ( - PrometheusMiddleware, -) -from litestar.exceptions import MissingDependencyException -from litestar.middleware.base import DefineMiddleware +from litestar.utils import warn_deprecation __all__ = ("PrometheusConfig",) -try: - import prometheus_client # noqa: F401 -except ImportError as e: - raise MissingDependencyException("prometheus_client", "prometheus-client", "prometheus") from e +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + from litestar.plugins.prometheus import PrometheusConfig + warn_deprecation( + deprecated_name=f"litestar.contrib.prometheus.config.{attr_name}", + version="2.13.0", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.prometheus.config' is deprecated, please " + f"import it from 'litestar.plugins.prometheus' instead", + ) + value = globals()[attr_name] = locals()[attr_name] + return value -if TYPE_CHECKING: - from litestar.connection.request import Request - from litestar.types import Method, Scopes - - -@dataclass -class PrometheusConfig: - """Configuration class for the PrometheusConfig middleware.""" + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover - app_name: str = field(default="litestar") - """The name of the application to use in the metrics.""" - prefix: str = "litestar" - """The prefix to use for the metrics.""" - labels: Mapping[str, str | Callable] | None = field(default=None) - """A mapping of labels to add to the metrics. The values can be either a string or a callable that returns a string.""" - exemplars: Callable[[Request], dict] | None = field(default=None) - """A callable that returns a list of exemplars to add to the metrics. Only supported in opementrics-text exposition format.""" - buckets: list[str | float] | None = field(default=None) - """A list of buckets to use for the histogram.""" - excluded_http_methods: Method | Sequence[Method] | None = field(default=None) - """A list of http methods to exclude from the metrics.""" - exclude_unhandled_paths: bool = field(default=False) - """Whether to ignore requests for unhandled paths from the metrics.""" - exclude: str | list[str] | None = field(default=None) - """A pattern or list of patterns for routes to exclude from the metrics.""" - exclude_opt_key: str | None = field(default=None) - """A key or list of keys in ``opt`` with which a route handler can "opt-out" of the middleware.""" - scopes: Scopes | None = field(default=None) - """ASGI scopes processed by the middleware, if None both ``http`` and ``websocket`` will be processed.""" - middleware_class: type[PrometheusMiddleware] = field(default=PrometheusMiddleware) - """The middleware class to use. - """ - group_path: bool = field(default=False) - """Whether to group paths in the metrics to avoid cardinality explosion. - """ - @property - def middleware(self) -> DefineMiddleware: - """Create an instance of :class:`DefineMiddleware ` that wraps with. - - [PrometheusMiddleware][litestar.contrib.prometheus.PrometheusMiddleware]. or a subclass - of this middleware. - - Returns: - An instance of ``DefineMiddleware``. - """ - return DefineMiddleware(self.middleware_class, config=self) +if TYPE_CHECKING: + from litestar.plugins.prometheus import PrometheusConfig diff --git a/litestar/contrib/prometheus/controller.py b/litestar/contrib/prometheus/controller.py index 15f5bf1d52..a2ae199a1e 100644 --- a/litestar/contrib/prometheus/controller.py +++ b/litestar/contrib/prometheus/controller.py @@ -1,53 +1,30 @@ +# ruff: noqa: TC004, F401 from __future__ import annotations -import os - -from litestar import Controller, get -from litestar.exceptions import MissingDependencyException -from litestar.response import Response - -try: - import prometheus_client # noqa: F401 -except ImportError as e: - raise MissingDependencyException("prometheus_client", "prometheus-client", "prometheus") from e - -from prometheus_client import ( - CONTENT_TYPE_LATEST, - REGISTRY, - CollectorRegistry, - generate_latest, - multiprocess, -) -from prometheus_client.openmetrics.exposition import ( - CONTENT_TYPE_LATEST as OPENMETRICS_CONTENT_TYPE_LATEST, -) -from prometheus_client.openmetrics.exposition import ( - generate_latest as openmetrics_generate_latest, -) - -__all__ = [ - "PrometheusController", -] - - -class PrometheusController(Controller): - """Controller for Prometheus endpoints.""" - - path: str = "/metrics" - """The path to expose the metrics on.""" - openmetrics_format: bool = False - """Whether to expose the metrics in OpenMetrics format.""" - - @get() - async def get(self) -> Response: - registry = REGISTRY - if "prometheus_multiproc_dir" in os.environ or "PROMETHEUS_MULTIPROC_DIR" in os.environ: - registry = CollectorRegistry() - multiprocess.MultiProcessCollector(registry) # type: ignore[no-untyped-call] - - if self.openmetrics_format: - headers = {"Content-Type": OPENMETRICS_CONTENT_TYPE_LATEST} - return Response(openmetrics_generate_latest(registry), status_code=200, headers=headers) # type: ignore[no-untyped-call] - - headers = {"Content-Type": CONTENT_TYPE_LATEST} - return Response(generate_latest(registry), status_code=200, headers=headers) +from typing import TYPE_CHECKING + +from litestar.utils import warn_deprecation + +__all__ = ("PrometheusController",) + + +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + from litestar.plugins.prometheus import PrometheusController + + warn_deprecation( + deprecated_name=f"litestar.contrib.prometheus.controller.{attr_name}", + version="2.13.0", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.prometheus.controller' is deprecated, please " + f"import it from 'litestar.plugins.prometheus' instead", + ) + value = globals()[attr_name] = locals()[attr_name] + return value + + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover + + +if TYPE_CHECKING: + from litestar.plugins.prometheus import PrometheusController diff --git a/litestar/contrib/prometheus/middleware.py b/litestar/contrib/prometheus/middleware.py index 150cf59311..e12099c6f5 100644 --- a/litestar/contrib/prometheus/middleware.py +++ b/litestar/contrib/prometheus/middleware.py @@ -1,184 +1,30 @@ +# ruff: noqa: TC004, F401 from __future__ import annotations -import time -from functools import wraps -from typing import TYPE_CHECKING, Any, Callable, ClassVar, cast +from typing import TYPE_CHECKING -from litestar.connection.request import Request -from litestar.enums import ScopeType -from litestar.exceptions import MissingDependencyException -from litestar.middleware.base import AbstractMiddleware +from litestar.utils import warn_deprecation __all__ = ("PrometheusMiddleware",) -from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR -try: - import prometheus_client # noqa: F401 -except ImportError as e: - raise MissingDependencyException("prometheus_client", "prometheus-client", "prometheus") from e +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + from litestar.plugins.prometheus import PrometheusMiddleware -from prometheus_client import Counter, Gauge, Histogram + warn_deprecation( + deprecated_name=f"litestar.contrib.prometheus.middleware.{attr_name}", + version="2.13.0", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.prometheus.middleware' is deprecated, please " + f"import it from 'litestar.plugins.prometheus' instead", + ) + value = globals()[attr_name] = locals()[attr_name] + return value -if TYPE_CHECKING: - from prometheus_client.metrics import MetricWrapperBase - - from litestar.contrib.prometheus import PrometheusConfig - from litestar.types import ASGIApp, Message, Receive, Scope, Send - - -class PrometheusMiddleware(AbstractMiddleware): - """Prometheus Middleware.""" - - _metrics: ClassVar[dict[str, MetricWrapperBase]] = {} - - def __init__(self, app: ASGIApp, config: PrometheusConfig) -> None: - """Middleware that adds Prometheus instrumentation to the application. - - Args: - app: The ``next`` ASGI app to call. - config: An instance of :class:`PrometheusConfig <.contrib.prometheus.PrometheusConfig>` - """ - super().__init__(app=app, scopes=config.scopes, exclude=config.exclude, exclude_opt_key=config.exclude_opt_key) - self._config = config - self._kwargs: dict[str, Any] = {} - - if self._config.buckets is not None: - self._kwargs["buckets"] = self._config.buckets - - def request_count(self, labels: dict[str, str | int | float]) -> Counter: - metric_name = f"{self._config.prefix}_requests_total" - - if metric_name not in PrometheusMiddleware._metrics: - PrometheusMiddleware._metrics[metric_name] = Counter( - name=metric_name, - documentation="Total requests", - labelnames=[*labels.keys()], - ) - - return cast("Counter", PrometheusMiddleware._metrics[metric_name]) - - def request_time(self, labels: dict[str, str | int | float]) -> Histogram: - metric_name = f"{self._config.prefix}_request_duration_seconds" - - if metric_name not in PrometheusMiddleware._metrics: - PrometheusMiddleware._metrics[metric_name] = Histogram( - name=metric_name, - documentation="Request duration, in seconds", - labelnames=[*labels.keys()], - **self._kwargs, - ) - return cast("Histogram", PrometheusMiddleware._metrics[metric_name]) - - def requests_in_progress(self, labels: dict[str, str | int | float]) -> Gauge: - metric_name = f"{self._config.prefix}_requests_in_progress" - - if metric_name not in PrometheusMiddleware._metrics: - PrometheusMiddleware._metrics[metric_name] = Gauge( - name=metric_name, - documentation="Total requests currently in progress", - labelnames=[*labels.keys()], - multiprocess_mode="livesum", - ) - return cast("Gauge", PrometheusMiddleware._metrics[metric_name]) - - def requests_error_count(self, labels: dict[str, str | int | float]) -> Counter: - metric_name = f"{self._config.prefix}_requests_error_total" - - if metric_name not in PrometheusMiddleware._metrics: - PrometheusMiddleware._metrics[metric_name] = Counter( - name=metric_name, - documentation="Total errors in requests", - labelnames=[*labels.keys()], - ) - return cast("Counter", PrometheusMiddleware._metrics[metric_name]) - - def _get_extra_labels(self, request: Request[Any, Any, Any]) -> dict[str, str]: - """Get extra labels provided by the config and if they are callable, parse them. - - Args: - request: The request object. - - Returns: - A dictionary of extra labels. - """ - - return {k: str(v(request) if callable(v) else v) for k, v in (self._config.labels or {}).items()} + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover - def _get_default_labels(self, request: Request[Any, Any, Any]) -> dict[str, str | int | float]: - """Get default label values from the request. - Args: - request: The request object. - - Returns: - A dictionary of default labels. - """ - - path = request.url.path - if self._config.group_path: - path = request.scope["path_template"] - return { - "method": request.method if request.scope["type"] == ScopeType.HTTP else request.scope["type"], - "path": path, - "status_code": 200, - "app_name": self._config.app_name, - } - - async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: - """ASGI callable. - - Args: - scope: The ASGI connection scope. - receive: The ASGI receive function. - send: The ASGI send function. - - Returns: - None - """ - - request = Request[Any, Any, Any](scope, receive) - - if self._config.excluded_http_methods and request.method in self._config.excluded_http_methods: - await self.app(scope, receive, send) - return - - labels = {**self._get_default_labels(request), **self._get_extra_labels(request)} - - request_span = {"start_time": time.perf_counter(), "end_time": 0, "duration": 0, "status_code": 200} - - wrapped_send = self._get_wrapped_send(send, request_span) - - self.requests_in_progress(labels).labels(*labels.values()).inc() - - try: - await self.app(scope, receive, wrapped_send) - finally: - extra: dict[str, Any] = {} - if self._config.exemplars: - extra["exemplar"] = self._config.exemplars(request) - - self.requests_in_progress(labels).labels(*labels.values()).dec() - - labels["status_code"] = request_span["status_code"] - label_values = [*labels.values()] - - if request_span["status_code"] >= HTTP_500_INTERNAL_SERVER_ERROR: - self.requests_error_count(labels).labels(*label_values).inc(**extra) - - self.request_count(labels).labels(*label_values).inc(**extra) - self.request_time(labels).labels(*label_values).observe(request_span["duration"], **extra) - - def _get_wrapped_send(self, send: Send, request_span: dict[str, float]) -> Callable: - @wraps(send) - async def wrapped_send(message: Message) -> None: - if message["type"] == "http.response.start": - request_span["status_code"] = message["status"] - - if message["type"] == "http.response.body": - end = time.perf_counter() - request_span["duration"] = end - request_span["start_time"] - request_span["end_time"] = end - await send(message) - - return wrapped_send +if TYPE_CHECKING: + from litestar.plugins.prometheus import PrometheusMiddleware diff --git a/litestar/contrib/pydantic/__init__.py b/litestar/contrib/pydantic/__init__.py index 094560fe94..4e141deb96 100644 --- a/litestar/contrib/pydantic/__init__.py +++ b/litestar/contrib/pydantic/__init__.py @@ -1,106 +1,48 @@ +# ruff: noqa: TC004, F401 from __future__ import annotations from typing import TYPE_CHECKING, Any -from litestar.plugins import InitPluginProtocol - -from .pydantic_di_plugin import PydanticDIPlugin -from .pydantic_dto_factory import PydanticDTO -from .pydantic_init_plugin import PydanticInitPlugin -from .pydantic_schema_plugin import PydanticSchemaPlugin - -if TYPE_CHECKING: - from pydantic import BaseModel - from pydantic.v1 import BaseModel as BaseModelV1 - - from litestar.config.app import AppConfig - from litestar.types.serialization import PydanticV1FieldsListType, PydanticV2FieldsListType +from litestar.utils import warn_deprecation __all__ = ( + "PydanticDIPlugin", "PydanticDTO", "PydanticInitPlugin", - "PydanticSchemaPlugin", "PydanticPlugin", - "PydanticDIPlugin", + "PydanticSchemaPlugin", ) -def _model_dump(model: BaseModel | BaseModelV1, *, by_alias: bool = False) -> dict[str, Any]: - return ( - model.model_dump(mode="json", by_alias=by_alias) # pyright: ignore - if hasattr(model, "model_dump") - else {k: v.decode() if isinstance(v, bytes) else v for k, v in model.dict(by_alias=by_alias).items()} - ) - +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + from litestar.plugins.pydantic import ( + PydanticDIPlugin, + PydanticDTO, + PydanticInitPlugin, + PydanticPlugin, + PydanticSchemaPlugin, + ) -def _model_dump_json(model: BaseModel | BaseModelV1, by_alias: bool = False) -> str: - return ( - model.model_dump_json(by_alias=by_alias) # pyright: ignore - if hasattr(model, "model_dump_json") - else model.json(by_alias=by_alias) # pyright: ignore - ) + warn_deprecation( + deprecated_name=f"litestar.contrib.pydantic.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.pydantic' is deprecated, please " + f"import it from 'litestar.plugins.pydantic' instead", + ) + value = globals()[attr_name] = locals()[attr_name] + return value + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover -class PydanticPlugin(InitPluginProtocol): - """A plugin that provides Pydantic integration.""" - __slots__ = ( - "exclude", - "exclude_defaults", - "exclude_none", - "exclude_unset", - "include", - "prefer_alias", - "validate_strict", +if TYPE_CHECKING: + from litestar.plugins.pydantic import ( + PydanticDIPlugin, + PydanticDTO, + PydanticInitPlugin, + PydanticPlugin, + PydanticSchemaPlugin, ) - - def __init__( - self, - exclude: PydanticV1FieldsListType | PydanticV2FieldsListType | None = None, - exclude_defaults: bool = False, - exclude_none: bool = False, - exclude_unset: bool = False, - include: PydanticV1FieldsListType | PydanticV2FieldsListType | None = None, - prefer_alias: bool = False, - validate_strict: bool = False, - ) -> None: - """Pydantic Plugin to support serialization / validation of Pydantic types / models - - :param exclude: Fields to exclude during serialization - :param exclude_defaults: Fields to exclude during serialization when they are set to their default value - :param exclude_none: Fields to exclude during serialization when they are set to ``None`` - :param exclude_unset: Fields to exclude during serialization when they arenot set - :param include: Fields to exclude during serialization - :param prefer_alias: Use the ``by_alias=True`` flag when dumping models - :param validate_strict: Use ``strict=True`` when calling ``.model_validate`` on Pydantic 2.x models - """ - self.exclude = exclude - self.exclude_defaults = exclude_defaults - self.exclude_none = exclude_none - self.exclude_unset = exclude_unset - self.include = include - self.prefer_alias = prefer_alias - self.validate_strict = validate_strict - - def on_app_init(self, app_config: AppConfig) -> AppConfig: - """Configure application for use with Pydantic. - - Args: - app_config: The :class:`AppConfig <.config.app.AppConfig>` instance. - """ - app_config.plugins.extend( - [ - PydanticInitPlugin( - exclude=self.exclude, - exclude_defaults=self.exclude_defaults, - exclude_none=self.exclude_none, - exclude_unset=self.exclude_unset, - include=self.include, - prefer_alias=self.prefer_alias, - validate_strict=self.validate_strict, - ), - PydanticSchemaPlugin(prefer_alias=self.prefer_alias), - PydanticDIPlugin(), - ] - ) - return app_config diff --git a/litestar/contrib/pydantic/pydantic_di_plugin.py b/litestar/contrib/pydantic/pydantic_di_plugin.py index 2096fd4ab6..026076e09e 100644 --- a/litestar/contrib/pydantic/pydantic_di_plugin.py +++ b/litestar/contrib/pydantic/pydantic_di_plugin.py @@ -1,26 +1,30 @@ +# ruff: noqa: TC004, F401 from __future__ import annotations -import inspect -from inspect import Signature -from typing import Any +from typing import TYPE_CHECKING # pragma: no cover -from litestar.contrib.pydantic.utils import is_pydantic_model_class -from litestar.plugins import DIPlugin +from litestar.utils import warn_deprecation # pragma: no cover +__all__ = ("PydanticDIPlugin",) # pragma: no cover -class PydanticDIPlugin(DIPlugin): - def has_typed_init(self, type_: Any) -> bool: - return is_pydantic_model_class(type_) - def get_typed_init(self, type_: Any) -> tuple[Signature, dict[str, Any]]: - try: - model_fields = dict(type_.model_fields) - except AttributeError: - model_fields = {k: model_field.field_info for k, model_field in type_.__fields__.items()} +def __getattr__(attr_name: str) -> object: # pragma: no cover + if attr_name in __all__: + from litestar.plugins.pydantic.plugins.di import PydanticDIPlugin - parameters = [ - inspect.Parameter(name=field_name, kind=inspect.Parameter.KEYWORD_ONLY, annotation=Any) - for field_name in model_fields - ] - type_hints = {field_name: Any for field_name in model_fields} - return Signature(parameters), type_hints + warn_deprecation( + deprecated_name=f"litestar.contrib.pydantic.pydantic_di_plugin.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.pydantic.pydantic_di_plugin' is deprecated, please " + f"import it from 'litestar.plugins.pydantic' instead", + ) + value = globals()[attr_name] = locals()[attr_name] + return value + + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") + + +if TYPE_CHECKING: + from litestar.plugins.pydantic.plugins.di import PydanticDIPlugin diff --git a/litestar/contrib/pydantic/pydantic_dto_factory.py b/litestar/contrib/pydantic/pydantic_dto_factory.py index 4322aa4c32..a9af073a1a 100644 --- a/litestar/contrib/pydantic/pydantic_dto_factory.py +++ b/litestar/contrib/pydantic/pydantic_dto_factory.py @@ -1,175 +1,30 @@ +# ruff: noqa: TC004, F401 from __future__ import annotations -import dataclasses -from dataclasses import replace -from typing import TYPE_CHECKING, Any, Collection, Generic, TypeVar -from warnings import warn +from typing import TYPE_CHECKING # pragma: no cover -from typing_extensions import Annotated, TypeAlias, override +from litestar.utils import warn_deprecation # pragma: no cover -from litestar.contrib.pydantic.utils import is_pydantic_2_model, is_pydantic_undefined, is_pydantic_v2 -from litestar.dto.base_dto import AbstractDTO -from litestar.dto.data_structures import DTOFieldDefinition -from litestar.dto.field import DTO_FIELD_META_KEY, extract_dto_field -from litestar.exceptions import MissingDependencyException, ValidationException -from litestar.types.empty import Empty -from litestar.typing import FieldDefinition +__all__ = ("PydanticDTO",) # pragma: no cover -if TYPE_CHECKING: - from typing import Generator - - from litestar.dto import DTOConfig - -try: - import pydantic as _ # noqa: F401 -except ImportError as e: - raise MissingDependencyException("pydantic") from e - - -try: - import pydantic as pydantic_v2 - - if not is_pydantic_v2(pydantic_v2): - raise ImportError - - from pydantic import ValidationError as ValidationErrorV2 - from pydantic import v1 as pydantic_v1 - from pydantic.v1 import ValidationError as ValidationErrorV1 - - ModelType: TypeAlias = "pydantic_v1.BaseModel | pydantic_v2.BaseModel" - -except ImportError: - import pydantic as pydantic_v1 # type: ignore[no-redef] - - pydantic_v2 = Empty # type: ignore[assignment] - from pydantic import ValidationError as ValidationErrorV1 # type: ignore[assignment] - - ValidationErrorV2 = ValidationErrorV1 # type: ignore[assignment, misc] - ModelType = "pydantic_v1.BaseModel" # type: ignore[misc] - - -T = TypeVar("T", bound="ModelType | Collection[ModelType]") - - -__all__ = ("PydanticDTO",) -_down_types: dict[Any, Any] = { - pydantic_v1.EmailStr: str, - pydantic_v1.IPvAnyAddress: str, - pydantic_v1.IPvAnyInterface: str, - pydantic_v1.IPvAnyNetwork: str, -} +def __getattr__(attr_name: str) -> object: # pragma: no cover + if attr_name in __all__: + from litestar.plugins.pydantic.dto import PydanticDTO -if pydantic_v2 is not Empty: # type: ignore[comparison-overlap] # pragma: no cover - _down_types.update( - { - pydantic_v2.JsonValue: Any, - pydantic_v2.EmailStr: str, - pydantic_v2.IPvAnyAddress: str, - pydantic_v2.IPvAnyInterface: str, - pydantic_v2.IPvAnyNetwork: str, - } - ) - - -def convert_validation_error(validation_error: ValidationErrorV1 | ValidationErrorV2) -> list[dict[str, Any]]: - error_list = validation_error.errors() - for error in error_list: - if isinstance(exception := error.get("ctx", {}).get("error"), Exception): - error["ctx"]["error"] = type(exception).__name__ - return error_list # type: ignore[return-value] - - -def downtype_for_data_transfer(field_definition: FieldDefinition) -> FieldDefinition: - if sub := _down_types.get(field_definition.annotation): - return FieldDefinition.from_kwarg( - annotation=Annotated[sub, field_definition.metadata], name=field_definition.name + warn_deprecation( + deprecated_name=f"litestar.contrib.pydantic.pydantic_dto_factory.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.pydantic.pydantic_dto_factory' is deprecated, please " + f"import it from 'litestar.plugins.pydantic' instead", ) - return field_definition - - -class PydanticDTO(AbstractDTO[T], Generic[T]): - """Support for domain modelling with Pydantic.""" - - @override - def decode_builtins(self, value: dict[str, Any]) -> Any: - try: - return super().decode_builtins(value) - except (ValidationErrorV2, ValidationErrorV1) as ex: - raise ValidationException(extra=convert_validation_error(ex)) from ex - - @override - def decode_bytes(self, value: bytes) -> Any: - try: - return super().decode_bytes(value) - except (ValidationErrorV2, ValidationErrorV1) as ex: - raise ValidationException(extra=convert_validation_error(ex)) from ex + value = globals()[attr_name] = locals()[attr_name] + return value - @classmethod - def generate_field_definitions( - cls, model_type: type[pydantic_v1.BaseModel | pydantic_v2.BaseModel] - ) -> Generator[DTOFieldDefinition, None, None]: - model_field_definitions = cls.get_model_type_hints(model_type) + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") - model_fields: dict[str, pydantic_v1.fields.FieldInfo | pydantic_v2.fields.FieldInfo] - try: - model_fields = dict(model_type.model_fields) # type: ignore[union-attr] - except AttributeError: - model_fields = { - k: model_field.field_info - for k, model_field in model_type.__fields__.items() # type: ignore[union-attr] - } - for field_name, field_info in model_fields.items(): - field_definition = downtype_for_data_transfer(model_field_definitions[field_name]) - dto_field = extract_dto_field(field_definition, field_definition.extra) - - try: - extra = field_info.extra # type: ignore[union-attr] - except AttributeError: - extra = field_info.json_schema_extra # type: ignore[union-attr] - - if extra is not None and extra.pop(DTO_FIELD_META_KEY, None): - warn( - message="Declaring 'DTOField' via Pydantic's 'Field.extra' is deprecated. " - "Use 'Annotated', e.g., 'Annotated[str, DTOField(mark='read-only')]' instead. " - "Support for 'DTOField' in 'Field.extra' will be removed in v3.", - category=DeprecationWarning, - stacklevel=2, - ) - - if not is_pydantic_undefined(field_info.default): - default = field_info.default - elif field_definition.is_optional: - default = None - else: - default = Empty - - yield replace( - DTOFieldDefinition.from_field_definition( - field_definition=field_definition, - dto_field=dto_field, - model_name=model_type.__name__, - default_factory=field_info.default_factory - if field_info.default_factory and not is_pydantic_undefined(field_info.default_factory) - else None, - ), - default=default, - name=field_name, - ) - - @classmethod - def detect_nested_field(cls, field_definition: FieldDefinition) -> bool: - if pydantic_v2 is not Empty: # type: ignore[comparison-overlap] - return field_definition.is_subclass_of((pydantic_v1.BaseModel, pydantic_v2.BaseModel)) - return field_definition.is_subclass_of(pydantic_v1.BaseModel) # type: ignore[unreachable] - - @classmethod - def get_config_for_model_type(cls, config: DTOConfig, model_type: type[Any]) -> DTOConfig: - if is_pydantic_2_model(model_type) and (model_config := getattr(model_type, "model_config", None)): - if model_config.get("extra") == "forbid": - config = dataclasses.replace(config, forbid_unknown_fields=True) - elif issubclass(model_type, pydantic_v1.BaseModel) and (model_config := getattr(model_type, "Config", None)): # noqa: SIM102 - if getattr(model_config, "extra", None) == "forbid": - config = dataclasses.replace(config, forbid_unknown_fields=True) - return config +if TYPE_CHECKING: + from litestar.plugins.pydantic.dto import PydanticDTO diff --git a/litestar/contrib/pydantic/pydantic_init_plugin.py b/litestar/contrib/pydantic/pydantic_init_plugin.py index 1d425f3420..57414fe9cd 100644 --- a/litestar/contrib/pydantic/pydantic_init_plugin.py +++ b/litestar/contrib/pydantic/pydantic_init_plugin.py @@ -1,296 +1,30 @@ +# ruff: noqa: TC004, F401 from __future__ import annotations -from contextlib import suppress -from functools import partial -from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast -from uuid import UUID +from typing import TYPE_CHECKING # pragma: no cover -from msgspec import ValidationError -from typing_extensions import Buffer, TypeGuard +from litestar.utils import warn_deprecation # pragma: no cover -from litestar._signature.types import ExtendedMsgSpecValidationError -from litestar.contrib.pydantic.utils import is_pydantic_constrained_field, is_pydantic_v2 -from litestar.exceptions import MissingDependencyException -from litestar.plugins import InitPluginProtocol -from litestar.typing import _KWARG_META_EXTRACTORS -from litestar.utils import is_class_and_subclass +__all__ = ("PydanticInitPlugin",) # pragma: no cover -try: - import pydantic as _ # noqa: F401 -except ImportError as e: - raise MissingDependencyException("pydantic") from e -try: - import pydantic as pydantic_v2 +def __getattr__(attr_name: str) -> object: # pragma: no cover + if attr_name in __all__: + from litestar.plugins.pydantic.plugins.init import PydanticInitPlugin - if not is_pydantic_v2(pydantic_v2): - raise ImportError + warn_deprecation( + deprecated_name=f"litestar.contrib.pydantic.pydantic_init_plugin.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.pydantic.pydantic_init_plugin' is deprecated, please " + f"import it from 'litestar.plugins.pydantic' instead", + ) + value = globals()[attr_name] = locals()[attr_name] + return value - from pydantic import v1 as pydantic_v1 -except ImportError: - import pydantic as pydantic_v1 # type: ignore[no-redef] - - pydantic_v2 = None # type: ignore[assignment] + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") if TYPE_CHECKING: - from litestar.config.app import AppConfig - from litestar.types.serialization import PydanticV1FieldsListType, PydanticV2FieldsListType - - -T = TypeVar("T") - - -def _dec_pydantic_v1(model_type: type[pydantic_v1.BaseModel], value: Any) -> pydantic_v1.BaseModel: - try: - return model_type.parse_obj(value) - except pydantic_v1.ValidationError as e: - raise ExtendedMsgSpecValidationError(errors=cast("list[dict[str, Any]]", e.errors())) from e - - -def _dec_pydantic_v2(model_type: type[pydantic_v2.BaseModel], value: Any, strict: bool) -> pydantic_v2.BaseModel: - try: - return model_type.model_validate(value, strict=strict) - except pydantic_v2.ValidationError as e: - raise ExtendedMsgSpecValidationError(errors=cast("list[dict[str, Any]]", e.errors())) from e - - -def _dec_pydantic_uuid( - uuid_type: type[pydantic_v1.UUID1] | type[pydantic_v1.UUID3] | type[pydantic_v1.UUID4] | type[pydantic_v1.UUID5], - value: Any, -) -> ( - type[pydantic_v1.UUID1] | type[pydantic_v1.UUID3] | type[pydantic_v1.UUID4] | type[pydantic_v1.UUID5] -): # pragma: no cover - if isinstance(value, str): - value = uuid_type(value) - - elif isinstance(value, Buffer): - value = bytes(value) - try: - value = uuid_type(value.decode()) - except ValueError: - # 16 bytes in big-endian order as the bytes argument fail - # the above check - value = uuid_type(bytes=value) - elif isinstance(value, UUID): - value = uuid_type(str(value)) - - if not isinstance(value, uuid_type): - raise ValidationError(f"Invalid UUID: {value!r}") - - if value._required_version != value.version: - raise ValidationError(f"Invalid UUID version: {value!r}") - - return cast( - "type[pydantic_v1.UUID1] | type[pydantic_v1.UUID3] | type[pydantic_v1.UUID4] | type[pydantic_v1.UUID5]", value - ) - - -def _is_pydantic_v1_uuid(value: Any) -> bool: # pragma: no cover - return is_class_and_subclass(value, (pydantic_v1.UUID1, pydantic_v1.UUID3, pydantic_v1.UUID4, pydantic_v1.UUID5)) - - -_base_encoders: dict[Any, Callable[[Any], Any]] = { - pydantic_v1.EmailStr: str, - pydantic_v1.NameEmail: str, - pydantic_v1.ByteSize: lambda val: val.real, -} - -if pydantic_v2 is not None: # pragma: no cover - _base_encoders.update( - { - pydantic_v2.EmailStr: str, - pydantic_v2.NameEmail: str, - pydantic_v2.ByteSize: lambda val: val.real, - } - ) - - -def is_pydantic_v1_model_class(annotation: Any) -> TypeGuard[type[pydantic_v1.BaseModel]]: - return is_class_and_subclass(annotation, pydantic_v1.BaseModel) - - -def is_pydantic_v2_model_class(annotation: Any) -> TypeGuard[type[pydantic_v2.BaseModel]]: - return is_class_and_subclass(annotation, pydantic_v2.BaseModel) - - -class ConstrainedFieldMetaExtractor: - @staticmethod - def matches(annotation: Any, name: str | None, default: Any) -> bool: - return is_pydantic_constrained_field(annotation) - - @staticmethod - def extract(annotation: Any, default: Any) -> Any: - return [annotation] - - -class PydanticInitPlugin(InitPluginProtocol): - __slots__ = ( - "exclude", - "exclude_defaults", - "exclude_none", - "exclude_unset", - "include", - "prefer_alias", - "validate_strict", - ) - - def __init__( - self, - exclude: PydanticV1FieldsListType | PydanticV2FieldsListType | None = None, - exclude_defaults: bool = False, - exclude_none: bool = False, - exclude_unset: bool = False, - include: PydanticV1FieldsListType | PydanticV2FieldsListType | None = None, - prefer_alias: bool = False, - validate_strict: bool = False, - ) -> None: - """Pydantic Plugin to support serialization / validation of Pydantic types / models - - :param exclude: Fields to exclude during serialization - :param exclude_defaults: Fields to exclude during serialization when they are set to their default value - :param exclude_none: Fields to exclude during serialization when they are set to ``None`` - :param exclude_unset: Fields to exclude during serialization when they arenot set - :param include: Fields to exclude during serialization - :param prefer_alias: Use the ``by_alias=True`` flag when dumping models - :param validate_strict: Use ``strict=True`` when calling ``.model_validate`` on Pydantic 2.x models - """ - self.exclude = exclude - self.exclude_defaults = exclude_defaults - self.exclude_none = exclude_none - self.exclude_unset = exclude_unset - self.include = include - self.prefer_alias = prefer_alias - self.validate_strict = validate_strict - - @classmethod - def encoders( - cls, - exclude: PydanticV1FieldsListType | PydanticV2FieldsListType | None = None, - exclude_defaults: bool = False, - exclude_none: bool = False, - exclude_unset: bool = False, - include: PydanticV1FieldsListType | PydanticV2FieldsListType | None = None, - prefer_alias: bool = False, - ) -> dict[Any, Callable[[Any], Any]]: - encoders = { - **_base_encoders, - **cls._create_pydantic_v1_encoders( - prefer_alias=prefer_alias, - exclude=exclude, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - exclude_unset=exclude_unset, - include=include, - ), - } - if pydantic_v2 is not None: # pragma: no cover - encoders.update( - cls._create_pydantic_v2_encoders( - prefer_alias=prefer_alias, - exclude=exclude, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - exclude_unset=exclude_unset, - include=include, - ) - ) - return encoders - - @classmethod - def decoders(cls, validate_strict: bool = False) -> list[tuple[Callable[[Any], bool], Callable[[Any, Any], Any]]]: - decoders: list[tuple[Callable[[Any], bool], Callable[[Any, Any], Any]]] = [ - (is_pydantic_v1_model_class, _dec_pydantic_v1) - ] - - if pydantic_v2 is not None: # pragma: no cover - decoders.append( - ( - is_pydantic_v2_model_class, - partial(_dec_pydantic_v2, strict=validate_strict), - ) - ) - - decoders.append((_is_pydantic_v1_uuid, _dec_pydantic_uuid)) - - return decoders - - @staticmethod - def _create_pydantic_v1_encoders( - exclude: PydanticV1FieldsListType | None = None, - exclude_defaults: bool = False, - exclude_none: bool = False, - exclude_unset: bool = False, - include: PydanticV1FieldsListType | None = None, - prefer_alias: bool = False, - ) -> dict[Any, Callable[[Any], Any]]: # pragma: no cover - return { - pydantic_v1.BaseModel: lambda model: { - k: v.decode() if isinstance(v, bytes) else v - for k, v in model.dict( - by_alias=prefer_alias, - exclude=exclude, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - exclude_unset=exclude_unset, - include=include, - ).items() - }, - pydantic_v1.SecretField: str, - pydantic_v1.StrictBool: int, - pydantic_v1.color.Color: str, - pydantic_v1.ConstrainedBytes: lambda val: val.decode("utf-8"), - pydantic_v1.ConstrainedDate: lambda val: val.isoformat(), - pydantic_v1.AnyUrl: str, - } - - @staticmethod - def _create_pydantic_v2_encoders( - exclude: PydanticV2FieldsListType | None = None, - exclude_defaults: bool = False, - exclude_none: bool = False, - exclude_unset: bool = False, - include: PydanticV2FieldsListType | None = None, - prefer_alias: bool = False, - ) -> dict[Any, Callable[[Any], Any]]: - encoders: dict[Any, Callable[[Any], Any]] = { - pydantic_v2.BaseModel: lambda model: model.model_dump( - by_alias=prefer_alias, - exclude=exclude, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - exclude_unset=exclude_unset, - include=include, - mode="json", - ), - pydantic_v2.types.SecretStr: lambda val: "**********" if val else "", - pydantic_v2.types.SecretBytes: lambda val: "**********" if val else "", - pydantic_v2.AnyUrl: str, - } - - with suppress(ImportError): - from pydantic_extra_types import color - - encoders[color.Color] = str - - return encoders - - def on_app_init(self, app_config: AppConfig) -> AppConfig: - app_config.type_encoders = { - **self.encoders( - prefer_alias=self.prefer_alias, - exclude=self.exclude, - exclude_defaults=self.exclude_defaults, - exclude_none=self.exclude_none, - exclude_unset=self.exclude_unset, - include=self.include, - ), - **(app_config.type_encoders or {}), - } - app_config.type_decoders = [ - *self.decoders(validate_strict=self.validate_strict), - *(app_config.type_decoders or []), - ] - - _KWARG_META_EXTRACTORS.add(ConstrainedFieldMetaExtractor) - return app_config + from litestar.plugins.pydantic.plugins.init import PydanticInitPlugin diff --git a/litestar/contrib/pydantic/pydantic_schema_plugin.py b/litestar/contrib/pydantic/pydantic_schema_plugin.py index 2eda65f2fc..b15a5dd217 100644 --- a/litestar/contrib/pydantic/pydantic_schema_plugin.py +++ b/litestar/contrib/pydantic/pydantic_schema_plugin.py @@ -1,323 +1,30 @@ +# ruff: noqa: TC004, F401 from __future__ import annotations -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING # pragma: no cover -from typing_extensions import Annotated +from litestar.utils import warn_deprecation # pragma: no cover -from litestar.contrib.pydantic.utils import ( - create_field_definitions_for_computed_fields, - is_pydantic_2_model, - is_pydantic_constrained_field, - is_pydantic_model_class, - is_pydantic_undefined, - is_pydantic_v2, - pydantic_get_type_hints_with_generics_resolved, - pydantic_unwrap_and_get_origin, -) -from litestar.exceptions import MissingDependencyException -from litestar.openapi.spec import OpenAPIFormat, OpenAPIType, Schema -from litestar.plugins import OpenAPISchemaPlugin -from litestar.types import Empty -from litestar.typing import FieldDefinition -from litestar.utils import is_class_and_subclass, is_generic +__all__ = ("PydanticSchemaPlugin",) # pragma: no cover -try: - import pydantic as _ # noqa: F401 -except ImportError as e: - raise MissingDependencyException("pydantic") from e -try: - import pydantic as pydantic_v2 +def __getattr__(attr_name: str) -> object: # pragma: no cover + if attr_name in __all__: + from litestar.plugins.pydantic.plugins.schema import PydanticSchemaPlugin - if not is_pydantic_v2(pydantic_v2): - raise ImportError + warn_deprecation( + deprecated_name=f"litestar.contrib.pydantic.pydantic_schema_plugin.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.pydantic.pydantic_schema_plugin' is deprecated, please " + f"import it from 'litestar.plugins.pydantic' instead", + ) + value = globals()[attr_name] = locals()[attr_name] + return value - from pydantic import v1 as pydantic_v1 -except ImportError: - import pydantic as pydantic_v1 # type: ignore[no-redef] + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") - pydantic_v2 = None # type: ignore[assignment] if TYPE_CHECKING: - from litestar._openapi.schema_generation.schema import SchemaCreator - -PYDANTIC_TYPE_MAP: dict[type[Any] | None | Any, Schema] = { - pydantic_v1.ByteSize: Schema(type=OpenAPIType.INTEGER), - pydantic_v1.EmailStr: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.EMAIL), - pydantic_v1.IPvAnyAddress: Schema( - one_of=[ - Schema( - type=OpenAPIType.STRING, - format=OpenAPIFormat.IPV4, - description="IPv4 address", - ), - Schema( - type=OpenAPIType.STRING, - format=OpenAPIFormat.IPV6, - description="IPv6 address", - ), - ] - ), - pydantic_v1.IPvAnyInterface: Schema( - one_of=[ - Schema( - type=OpenAPIType.STRING, - format=OpenAPIFormat.IPV4, - description="IPv4 interface", - ), - Schema( - type=OpenAPIType.STRING, - format=OpenAPIFormat.IPV6, - description="IPv6 interface", - ), - ] - ), - pydantic_v1.IPvAnyNetwork: Schema( - one_of=[ - Schema( - type=OpenAPIType.STRING, - format=OpenAPIFormat.IPV4, - description="IPv4 network", - ), - Schema( - type=OpenAPIType.STRING, - format=OpenAPIFormat.IPV6, - description="IPv6 network", - ), - ] - ), - pydantic_v1.Json: Schema(type=OpenAPIType.OBJECT, format=OpenAPIFormat.JSON_POINTER), - pydantic_v1.NameEmail: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.EMAIL, description="Name and email"), - # removed in v2 - pydantic_v1.PyObject: Schema( - type=OpenAPIType.STRING, - description="dot separated path identifying a python object, e.g. 'decimal.Decimal'", - ), - # annotated in v2 - pydantic_v1.UUID1: Schema( - type=OpenAPIType.STRING, - format=OpenAPIFormat.UUID, - description="UUID1 string", - ), - pydantic_v1.UUID3: Schema( - type=OpenAPIType.STRING, - format=OpenAPIFormat.UUID, - description="UUID3 string", - ), - pydantic_v1.UUID4: Schema( - type=OpenAPIType.STRING, - format=OpenAPIFormat.UUID, - description="UUID4 string", - ), - pydantic_v1.UUID5: Schema( - type=OpenAPIType.STRING, - format=OpenAPIFormat.UUID, - description="UUID5 string", - ), - pydantic_v1.DirectoryPath: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URI_REFERENCE), - pydantic_v1.AnyUrl: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URL), - pydantic_v1.AnyHttpUrl: Schema( - type=OpenAPIType.STRING, format=OpenAPIFormat.URL, description="must be a valid HTTP based URL" - ), - pydantic_v1.FilePath: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URI_REFERENCE), - pydantic_v1.HttpUrl: Schema( - type=OpenAPIType.STRING, - format=OpenAPIFormat.URL, - description="must be a valid HTTP based URL", - max_length=2083, - ), - pydantic_v1.RedisDsn: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URI, description="redis DSN"), - pydantic_v1.PostgresDsn: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URI, description="postgres DSN"), - pydantic_v1.SecretBytes: Schema(type=OpenAPIType.STRING), - pydantic_v1.SecretStr: Schema(type=OpenAPIType.STRING), - pydantic_v1.StrictBool: Schema(type=OpenAPIType.BOOLEAN), - pydantic_v1.StrictBytes: Schema(type=OpenAPIType.STRING), - pydantic_v1.StrictFloat: Schema(type=OpenAPIType.NUMBER), - pydantic_v1.StrictInt: Schema(type=OpenAPIType.INTEGER), - pydantic_v1.StrictStr: Schema(type=OpenAPIType.STRING), - pydantic_v1.NegativeFloat: Schema(type=OpenAPIType.NUMBER, exclusive_maximum=0.0), - pydantic_v1.NegativeInt: Schema(type=OpenAPIType.INTEGER, exclusive_maximum=0), - pydantic_v1.NonNegativeInt: Schema(type=OpenAPIType.INTEGER, minimum=0), - pydantic_v1.NonPositiveFloat: Schema(type=OpenAPIType.NUMBER, maximum=0.0), - pydantic_v1.PaymentCardNumber: Schema(type=OpenAPIType.STRING, min_length=12, max_length=19), - pydantic_v1.PositiveFloat: Schema(type=OpenAPIType.NUMBER, exclusive_minimum=0.0), - pydantic_v1.PositiveInt: Schema(type=OpenAPIType.INTEGER, exclusive_minimum=0), -} - -if pydantic_v2 is not None: # pragma: no cover - PYDANTIC_TYPE_MAP.update( - { - pydantic_v2.SecretStr: Schema(type=OpenAPIType.STRING), - pydantic_v2.SecretBytes: Schema(type=OpenAPIType.STRING), - pydantic_v2.ByteSize: Schema(type=OpenAPIType.INTEGER), - pydantic_v2.EmailStr: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.EMAIL), - pydantic_v2.IPvAnyAddress: Schema( - one_of=[ - Schema( - type=OpenAPIType.STRING, - format=OpenAPIFormat.IPV4, - description="IPv4 address", - ), - Schema( - type=OpenAPIType.STRING, - format=OpenAPIFormat.IPV6, - description="IPv6 address", - ), - ] - ), - pydantic_v2.IPvAnyInterface: Schema( - one_of=[ - Schema( - type=OpenAPIType.STRING, - format=OpenAPIFormat.IPV4, - description="IPv4 interface", - ), - Schema( - type=OpenAPIType.STRING, - format=OpenAPIFormat.IPV6, - description="IPv6 interface", - ), - ] - ), - pydantic_v2.IPvAnyNetwork: Schema( - one_of=[ - Schema( - type=OpenAPIType.STRING, - format=OpenAPIFormat.IPV4, - description="IPv4 network", - ), - Schema( - type=OpenAPIType.STRING, - format=OpenAPIFormat.IPV6, - description="IPv6 network", - ), - ] - ), - pydantic_v2.Json: Schema(type=OpenAPIType.OBJECT, format=OpenAPIFormat.JSON_POINTER), - pydantic_v2.NameEmail: Schema( - type=OpenAPIType.STRING, format=OpenAPIFormat.EMAIL, description="Name and email" - ), - pydantic_v2.AnyUrl: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URL), - } - ) - - -_supported_types = (pydantic_v1.BaseModel, *PYDANTIC_TYPE_MAP.keys()) -if pydantic_v2 is not None: # pragma: no cover - _supported_types = (pydantic_v2.BaseModel, *_supported_types) - - -class PydanticSchemaPlugin(OpenAPISchemaPlugin): - __slots__ = ("prefer_alias",) - - def __init__(self, prefer_alias: bool = False) -> None: - self.prefer_alias = prefer_alias - - @staticmethod - def is_plugin_supported_type(value: Any) -> bool: - return isinstance(value, _supported_types) or is_class_and_subclass(value, _supported_types) # type: ignore[arg-type] - - @staticmethod - def is_undefined_sentinel(value: Any) -> bool: - return is_pydantic_undefined(value) - - @staticmethod - def is_constrained_field(field_definition: FieldDefinition) -> bool: - return is_pydantic_constrained_field(field_definition.annotation) - - def to_openapi_schema(self, field_definition: FieldDefinition, schema_creator: SchemaCreator) -> Schema: - """Given a type annotation, transform it into an OpenAPI schema class. - - Args: - field_definition: FieldDefinition instance. - schema_creator: An instance of the schema creator class - - Returns: - An :class:`OpenAPI ` instance. - """ - if schema_creator.prefer_alias != self.prefer_alias: - schema_creator.prefer_alias = True - if is_pydantic_model_class(field_definition.annotation): - return self.for_pydantic_model(field_definition=field_definition, schema_creator=schema_creator) - return PYDANTIC_TYPE_MAP[field_definition.annotation] # pragma: no cover - - @classmethod - def for_pydantic_model(cls, field_definition: FieldDefinition, schema_creator: SchemaCreator) -> Schema: # pyright: ignore - """Create a schema object for a given pydantic model class. - - Args: - field_definition: FieldDefinition instance. - schema_creator: An instance of the schema creator class - - Returns: - A schema instance. - """ - - annotation = field_definition.annotation - if is_generic(annotation): - is_generic_model = True - model = pydantic_unwrap_and_get_origin(annotation) or annotation - else: - is_generic_model = False - model = annotation - - if is_pydantic_2_model(model): - model_config = model.model_config - model_field_info = model.model_fields - title = model_config.get("title") - example = model_config.get("example") - is_v2_model = True - else: - model_config = annotation.__config__ - model_field_info = model.__fields__ - title = getattr(model_config, "title", None) - example = getattr(model_config, "example", None) - is_v2_model = False - - model_fields: dict[str, pydantic_v1.fields.FieldInfo | pydantic_v2.fields.FieldInfo] = { # pyright: ignore - k: getattr(f, "field_info", f) for k, f in model_field_info.items() - } - - if is_v2_model: - # extract the annotations from the FieldInfo. This allows us to skip fields - # which have been marked as private - model_annotations = {k: field_info.annotation for k, field_info in model_fields.items()} # type: ignore[union-attr] - - else: - # pydantic v1 requires some workarounds here - model_annotations = { - k: f.outer_type_ if f.required or f.default else Optional[f.outer_type_] - for k, f in model.__fields__.items() - } - - if is_generic_model: - # if the model is generic, resolve the type variables. We pass in the - # already extracted annotations, to keep the logic of respecting private - # fields consistent with the above - model_annotations = pydantic_get_type_hints_with_generics_resolved( - annotation, model_annotations=model_annotations, include_extras=True - ) - - property_fields = { - field_info.alias if field_info.alias and schema_creator.prefer_alias else k: FieldDefinition.from_kwarg( - annotation=Annotated[model_annotations[k], field_info, field_info.metadata] # type: ignore[union-attr] - if is_v2_model - else Annotated[model_annotations[k], field_info], # pyright: ignore - name=field_info.alias if field_info.alias and schema_creator.prefer_alias else k, - default=Empty if schema_creator.is_undefined(field_info.default) else field_info.default, - ) - for k, field_info in model_fields.items() - } - - computed_field_definitions = create_field_definitions_for_computed_fields( - annotation, schema_creator.prefer_alias - ) - property_fields.update(computed_field_definitions) - - return schema_creator.create_component_schema( - field_definition, - required=sorted(f.name for f in property_fields.values() if f.is_required), - property_fields=property_fields, - title=title, - examples=None if example is None else [example], - ) + from litestar.plugins.pydantic.plugins.schema import PydanticSchemaPlugin diff --git a/litestar/contrib/pydantic/utils.py b/litestar/contrib/pydantic/utils.py index 45362c1506..ace5dbf8e8 100644 --- a/litestar/contrib/pydantic/utils.py +++ b/litestar/contrib/pydantic/utils.py @@ -1,230 +1,48 @@ -# mypy: strict-equality=False +# ruff: noqa: TC004, F401 from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING -from typing_extensions import Annotated, get_type_hints +from litestar.utils import warn_deprecation -from litestar.params import KwargDefinition -from litestar.types import Empty -from litestar.typing import FieldDefinition -from litestar.utils import deprecated, is_class_and_subclass -from litestar.utils.predicates import is_generic -from litestar.utils.typing import ( - _substitute_typevars, - get_origin_or_inner_type, - get_type_hints_with_generics_resolved, - normalize_type_annotation, +__all__ = ( + "get_model_info", + "is_pydantic_constrained_field", + "is_pydantic_model_class", + "is_pydantic_undefined", + "is_pydantic_v2", ) -# isort: off -try: - from pydantic import v1 as pydantic_v1 - import pydantic as pydantic_v2 - from pydantic.fields import PydanticUndefined as Pydantic2Undefined # type: ignore[attr-defined] - from pydantic.v1.fields import Undefined as Pydantic1Undefined - PYDANTIC_UNDEFINED_SENTINELS = {Pydantic1Undefined, Pydantic2Undefined} -except ImportError: - try: - import pydantic as pydantic_v1 # type: ignore[no-redef] - from pydantic.fields import Undefined as Pydantic1Undefined # type: ignore[attr-defined, no-redef] - - pydantic_v2 = Empty # type: ignore[assignment] - PYDANTIC_UNDEFINED_SENTINELS = {Pydantic1Undefined} - - except ImportError: # pyright: ignore - pydantic_v1 = Empty # type: ignore[assignment] - pydantic_v2 = Empty # type: ignore[assignment] - PYDANTIC_UNDEFINED_SENTINELS = set() -# isort: on - - -if TYPE_CHECKING: - from types import ModuleType - - from typing_extensions import TypeGuard - - -def is_pydantic_model_class( - annotation: Any, -) -> TypeGuard[type[pydantic_v1.BaseModel | pydantic_v2.BaseModel]]: # pyright: ignore - """Given a type annotation determine if the annotation is a subclass of pydantic's BaseModel. - - Args: - annotation: A type. - - Returns: - A typeguard determining whether the type is :data:`BaseModel pydantic.BaseModel>`. - """ - tests: list[bool] = [] - - if pydantic_v1 is not Empty: # pragma: no cover - tests.append(is_class_and_subclass(annotation, pydantic_v1.BaseModel)) - - if pydantic_v2 is not Empty: # pragma: no cover - tests.append(is_class_and_subclass(annotation, pydantic_v2.BaseModel)) - - return any(tests) - - -def is_pydantic_model_instance( - annotation: Any, -) -> TypeGuard[pydantic_v1.BaseModel | pydantic_v2.BaseModel]: # pyright: ignore - """Given a type annotation determine if the annotation is an instance of pydantic's BaseModel. - - Args: - annotation: A type. - - Returns: - A typeguard determining whether the type is :data:`BaseModel pydantic.BaseModel>`. - """ - tests: list[bool] = [] - - if pydantic_v1 is not Empty: # pragma: no cover - tests.append(isinstance(annotation, pydantic_v1.BaseModel)) - - if pydantic_v2 is not Empty: # pragma: no cover - tests.append(isinstance(annotation, pydantic_v2.BaseModel)) - - return any(tests) - - -def is_pydantic_constrained_field(annotation: Any) -> bool: - """Check if the given annotation is a constrained pydantic type. - - Args: - annotation: A type annotation - - Returns: - True if pydantic is installed and the type is a constrained type, otherwise False. - """ - if pydantic_v1 is Empty: # pragma: no cover - return False # type: ignore[unreachable] - - return any( - is_class_and_subclass(annotation, constrained_type) # pyright: ignore - for constrained_type in ( - pydantic_v1.ConstrainedBytes, - pydantic_v1.ConstrainedDate, - pydantic_v1.ConstrainedDecimal, - pydantic_v1.ConstrainedFloat, - pydantic_v1.ConstrainedFrozenSet, - pydantic_v1.ConstrainedInt, - pydantic_v1.ConstrainedList, - pydantic_v1.ConstrainedSet, - pydantic_v1.ConstrainedStr, +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + from litestar.plugins.pydantic.utils import ( + get_model_info, + is_pydantic_constrained_field, + is_pydantic_model_class, + is_pydantic_undefined, + is_pydantic_v2, ) - ) - -def pydantic_unwrap_and_get_origin(annotation: Any) -> Any | None: - if pydantic_v2 is Empty or (pydantic_v1 is not Empty and is_class_and_subclass(annotation, pydantic_v1.BaseModel)): - return get_origin_or_inner_type(annotation) - - origin = annotation.__pydantic_generic_metadata__["origin"] - return normalize_type_annotation(origin) - - -def pydantic_get_type_hints_with_generics_resolved( - annotation: Any, - globalns: dict[str, Any] | None = None, - localns: dict[str, Any] | None = None, - include_extras: bool = False, - model_annotations: dict[str, Any] | None = None, -) -> dict[str, Any]: - if pydantic_v2 is Empty or (pydantic_v1 is not Empty and is_class_and_subclass(annotation, pydantic_v1.BaseModel)): - return get_type_hints_with_generics_resolved(annotation, type_hints=model_annotations) - - origin = pydantic_unwrap_and_get_origin(annotation) - if origin is None: - if model_annotations is None: # pragma: no cover - model_annotations = get_type_hints( - annotation, globalns=globalns, localns=localns, include_extras=include_extras - ) - typevar_map = {p: p for p in annotation.__pydantic_generic_metadata__["parameters"]} - else: - if model_annotations is None: - model_annotations = get_type_hints( - origin, globalns=globalns, localns=localns, include_extras=include_extras - ) - args = annotation.__pydantic_generic_metadata__["args"] - parameters = origin.__pydantic_generic_metadata__["parameters"] - typevar_map = dict(zip(parameters, args)) - - return {n: _substitute_typevars(type_, typevar_map) for n, type_ in model_annotations.items()} - - -@deprecated(version="2.6.2") -def pydantic_get_unwrapped_annotation_and_type_hints(annotation: Any) -> tuple[Any, dict[str, Any]]: # pragma: pver - """Get the unwrapped annotation and the type hints after resolving generics. - - Args: - annotation: A type annotation. - - Returns: - A tuple containing the unwrapped annotation and the type hints. - """ - - if is_generic(annotation): - origin = pydantic_unwrap_and_get_origin(annotation) - return origin or annotation, pydantic_get_type_hints_with_generics_resolved(annotation, include_extras=True) - return annotation, get_type_hints(annotation, include_extras=True) - - -def is_pydantic_2_model( - obj: type[pydantic_v1.BaseModel | pydantic_v2.BaseModel], # pyright: ignore -) -> TypeGuard[pydantic_v2.BaseModel]: # pyright: ignore - return pydantic_v2 is not Empty and issubclass(obj, pydantic_v2.BaseModel) - - -def is_pydantic_undefined(value: Any) -> bool: - return any(v is value for v in PYDANTIC_UNDEFINED_SENTINELS) - - -def create_field_definitions_for_computed_fields( - model: type[pydantic_v1.BaseModel | pydantic_v2.BaseModel], # pyright: ignore - prefer_alias: bool, -) -> dict[str, FieldDefinition]: - """Create field definitions for computed fields. - - Args: - model: A pydantic model. - prefer_alias: Whether to prefer the alias or the name of the field. - - Returns: - A dictionary containing the field definitions for the computed fields. - """ - pydantic_decorators = getattr(model, "__pydantic_decorators__", None) - if pydantic_decorators is None: - return {} - - def get_name(k: str, dec: Any) -> str: - if not dec.info.alias: - return k - return dec.info.alias if prefer_alias else k # type: ignore[no-any-return] - - return { - (name := get_name(k, dec)): FieldDefinition.from_annotation( - Annotated[ - dec.info.return_type, - KwargDefinition(title=dec.info.title, description=dec.info.description, read_only=True), - ], - name=name, + warn_deprecation( + deprecated_name=f"litestar.contrib.pydantic.utils.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.pydantic.utils' is deprecated, please " + f"import it from 'litestar.plugins.pydantic.utils' instead", ) - for k, dec in pydantic_decorators.computed_fields.items() - } - + value = globals()[attr_name] = locals()[attr_name] + return value -def is_pydantic_v2(module: ModuleType) -> bool: - """Determine if the given module is pydantic v2. + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") - Given a module we expect to be a pydantic version, determine if it is pydantic v2. - Args: - module: A module. - - Returns: - True if the module is pydantic v2, otherwise False. - """ - return bool(module.__version__.startswith("2.")) +if TYPE_CHECKING: + from litestar.plugins.pydantic.utils import ( + get_model_info, + is_pydantic_constrained_field, + is_pydantic_model_class, + is_pydantic_undefined, + is_pydantic_v2, + ) diff --git a/litestar/contrib/sqlalchemy/__init__.py b/litestar/contrib/sqlalchemy/__init__.py index e69de29bb2..1448e26757 100644 --- a/litestar/contrib/sqlalchemy/__init__.py +++ b/litestar/contrib/sqlalchemy/__init__.py @@ -0,0 +1,58 @@ +# ruff: noqa: TC004, F401 +from __future__ import annotations + +from typing import TYPE_CHECKING + +from litestar.utils import warn_deprecation + +__all__ = ( + "ModelT", + "SQLAlchemyAsyncRepository", + "SQLAlchemySyncRepository", + "wrap_sqlalchemy_exception", +) + + +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + if attr_name in ("SQLAlchemyAsyncRepository", "SQLAlchemySyncRepository", "ModelT"): + module = "litestar.plugins.sqlalchemy.repository" + from advanced_alchemy.extensions.litestar import ( + repository, # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] + ) + + value = globals()[attr_name] = getattr(repository, attr_name) + elif attr_name == "wrap_sqlalchemy_exception": + module = "litestar.plugins.sqlalchemy.exceptions" + from advanced_alchemy.extensions.litestar import ( + exceptions, # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] + ) + + value = globals()[attr_name] = getattr(exceptions, attr_name) + + else: # pragma: no cover + raise RuntimeError(f"Unhandled module attribute: {attr_name!r}") + + warn_deprecation( + deprecated_name=f"litestar.contrib.sqlalchemy.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy' is deprecated, please " + f"import it from '{module}' instead", + ) + + return value + + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover + + +if TYPE_CHECKING: + from advanced_alchemy.exceptions import ( # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] + wrap_sqlalchemy_exception, + ) + from advanced_alchemy.repository import ( # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] + ModelT, + SQLAlchemyAsyncRepository, + SQLAlchemySyncRepository, + ) diff --git a/litestar/contrib/sqlalchemy/base.py b/litestar/contrib/sqlalchemy/base.py index 9ce9608f7e..1a1e147eb1 100644 --- a/litestar/contrib/sqlalchemy/base.py +++ b/litestar/contrib/sqlalchemy/base.py @@ -1,26 +1,12 @@ +# ruff: noqa: TC004, F401 +# pyright: reportUnusedImport=false """Application ORM configuration.""" from __future__ import annotations -try: - # v0.6.0+ - from advanced_alchemy._listeners import touch_updated_timestamp # pyright: ignore -except ImportError: - from advanced_alchemy.base import touch_updated_timestamp # type: ignore[no-redef,attr-defined] - -from advanced_alchemy.base import ( - AuditColumns, - BigIntAuditBase, - BigIntBase, - BigIntPrimaryKey, - CommonTableAttributes, - ModelProtocol, - UUIDAuditBase, - UUIDBase, - UUIDPrimaryKey, - create_registry, - orm_registry, -) +from typing import TYPE_CHECKING + +from litestar.utils import warn_deprecation __all__ = ( "AuditColumns", @@ -28,11 +14,83 @@ "BigIntBase", "BigIntPrimaryKey", "CommonTableAttributes", - "create_registry", "ModelProtocol", - "touch_updated_timestamp", "UUIDAuditBase", "UUIDBase", "UUIDPrimaryKey", + "create_registry", "orm_registry", + "touch_updated_timestamp", ) + + +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + if attr_name == "touch_updated_timestamp": + try: + # v0.6.0+ + from advanced_alchemy._listeners import touch_updated_timestamp # pyright: ignore + except ImportError: + from advanced_alchemy.base import touch_updated_timestamp # type: ignore[no-redef,attr-defined] + warn_deprecation( + deprecated_name=f"litestar.contrib.sqlalchemy.base.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.base' is deprecated, please" + f"see the 'Advanced Alchemy' documentation for more details on how to use '{attr_name}' instead", + ) + value = globals()[attr_name] = locals()[attr_name] # pyright: ignore[reportUnknownVariableType] + return value # pyright: ignore[reportUnknownVariableType] + from advanced_alchemy.base import ( # pyright: ignore[reportMissingImports] + BigIntAuditBase, + BigIntBase, + CommonTableAttributes, + ModelProtocol, + UUIDAuditBase, + UUIDBase, + create_registry, + orm_registry, + ) + from advanced_alchemy.mixins import ( # pyright: ignore[reportMissingImports] + AuditColumns, + BigIntPrimaryKey, + UUIDPrimaryKey, + ) + + warn_deprecation( + deprecated_name=f"litestar.contrib.sqlalchemy.base.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.base' is deprecated, please" + f"import it from 'litestar.plugins.sqlalchemy.base.{attr_name}' instead", + ) + value = globals()[attr_name] = locals()[attr_name] # pyright: ignore[reportUnknownVariableType] + return value + + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover + + +if TYPE_CHECKING: + try: + # v0.6.0+ + from advanced_alchemy._listeners import touch_updated_timestamp # pyright: ignore + except ImportError: + from advanced_alchemy.base import touch_updated_timestamp # type: ignore[no-redef,attr-defined] + + from advanced_alchemy.base import ( # pyright: ignore[reportMissingImports] + BigIntAuditBase, + BigIntBase, + CommonTableAttributes, + ModelProtocol, + UUIDAuditBase, + UUIDBase, + create_registry, + orm_registry, + ) + from advanced_alchemy.mixins import ( # pyright: ignore[reportMissingImports] + AuditColumns, + BigIntPrimaryKey, + UUIDPrimaryKey, + ) diff --git a/litestar/contrib/sqlalchemy/dto.py b/litestar/contrib/sqlalchemy/dto.py index beea75d262..69c170a4ac 100644 --- a/litestar/contrib/sqlalchemy/dto.py +++ b/litestar/contrib/sqlalchemy/dto.py @@ -1,5 +1,39 @@ +# ruff: noqa: TC004, F401 +# pyright: reportUnusedImport=false +"""SQLAlchemy DTO configuration.""" + from __future__ import annotations -from advanced_alchemy.extensions.litestar.dto import SQLAlchemyDTO, SQLAlchemyDTOConfig +from typing import TYPE_CHECKING + +from litestar.utils import warn_deprecation __all__ = ("SQLAlchemyDTO", "SQLAlchemyDTOConfig") + + +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + from advanced_alchemy.extensions.litestar.dto import ( + SQLAlchemyDTO, # pyright: ignore[reportMissingImports] + SQLAlchemyDTOConfig, # pyright: ignore[reportMissingImports] + ) + + warn_deprecation( + deprecated_name=f"litestar.contrib.sqlalchemy.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.dto' is deprecated, please " + f"import it from 'litestar.plugins.sqlalchemy.dto' instead", + ) + value = globals()[attr_name] = locals()[attr_name] + return value + + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover + + +if TYPE_CHECKING: + from advanced_alchemy.extensions.litestar.dto import ( + SQLAlchemyDTO, # pyright: ignore[reportMissingImports] + SQLAlchemyDTOConfig, # pyright: ignore[reportMissingImports] + ) diff --git a/litestar/contrib/sqlalchemy/plugins/__init__.py b/litestar/contrib/sqlalchemy/plugins/__init__.py index 5bc913c9ab..91bce102da 100644 --- a/litestar/contrib/sqlalchemy/plugins/__init__.py +++ b/litestar/contrib/sqlalchemy/plugins/__init__.py @@ -1,18 +1,10 @@ +# ruff: noqa: TC004, F401 +# pyright: reportUnusedImport=false from __future__ import annotations -from advanced_alchemy.extensions.litestar.plugins import SQLAlchemyPlugin - -from .init import ( - AsyncSessionConfig, - EngineConfig, - GenericSessionConfig, - GenericSQLAlchemyConfig, - SQLAlchemyAsyncConfig, - SQLAlchemyInitPlugin, - SQLAlchemySyncConfig, - SyncSessionConfig, -) -from .serialization import SQLAlchemySerializationPlugin +from typing import TYPE_CHECKING + +from litestar.utils import warn_deprecation __all__ = ( "AsyncSessionConfig", @@ -26,3 +18,54 @@ "SQLAlchemySyncConfig", "SyncSessionConfig", ) + + +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + if attr_name in ("GenericSQLAlchemyConfig", "GenericSessionConfig"): + module = "litestar.plugins.sqlalchemy.config" + from advanced_alchemy.config import ( # pyright: ignore[reportMissingImports] + GenericSessionConfig, + GenericSQLAlchemyConfig, + ) + + value = globals()[attr_name] = locals()[attr_name] + else: + module = "litestar.plugins.sqlalchemy" + from advanced_alchemy.extensions.litestar import ( # pyright: ignore[reportMissingImports] + AsyncSessionConfig, + EngineConfig, + SQLAlchemyAsyncConfig, + SQLAlchemyInitPlugin, + SQLAlchemyPlugin, + SQLAlchemySerializationPlugin, + SQLAlchemySyncConfig, + SyncSessionConfig, + ) + + value = globals()[attr_name] = locals()[attr_name] + warn_deprecation( + deprecated_name=f"litestar.contrib.sqlalchemy.plugins.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.plugins' is deprecated, please " + f"import it from '{module}' instead", + ) + return value + + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover + + +if TYPE_CHECKING: + from advanced_alchemy.config import GenericSessionConfig, GenericSQLAlchemyConfig + from advanced_alchemy.extensions.litestar import ( + AsyncSessionConfig, + EngineConfig, + SQLAlchemyAsyncConfig, + SQLAlchemyInitPlugin, + SQLAlchemyPlugin, + SQLAlchemySerializationPlugin, + SQLAlchemySyncConfig, + SyncSessionConfig, + ) diff --git a/litestar/contrib/sqlalchemy/plugins/init/__init__.py b/litestar/contrib/sqlalchemy/plugins/init/__init__.py index 2e507c1066..f151a71f08 100644 --- a/litestar/contrib/sqlalchemy/plugins/init/__init__.py +++ b/litestar/contrib/sqlalchemy/plugins/init/__init__.py @@ -1,15 +1,10 @@ +# ruff: noqa: TC004, F401 +# pyright: reportUnusedImport=false from __future__ import annotations -from .config import ( - AsyncSessionConfig, - EngineConfig, - GenericSessionConfig, - GenericSQLAlchemyConfig, - SQLAlchemyAsyncConfig, - SQLAlchemySyncConfig, - SyncSessionConfig, -) -from .plugin import SQLAlchemyInitPlugin +from typing import TYPE_CHECKING + +from litestar.utils import warn_deprecation __all__ = ( "AsyncSessionConfig", @@ -21,3 +16,53 @@ "SQLAlchemySyncConfig", "SyncSessionConfig", ) + + +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + if attr_name in ("GenericSQLAlchemyConfig", "GenericSessionConfig"): + module = "advanced_alchemy.config" + from advanced_alchemy.config import ( # pyright: ignore[reportMissingImports] + GenericSessionConfig, + GenericSQLAlchemyConfig, + ) + + value = globals()[attr_name] = locals()[attr_name] + else: + module = "litestar.plugins.sqlalchemy" + from advanced_alchemy.extensions.litestar import ( + AsyncSessionConfig, + EngineConfig, + SQLAlchemyAsyncConfig, + SQLAlchemyInitPlugin, + SQLAlchemySyncConfig, + SyncSessionConfig, + ) + + value = globals()[attr_name] = locals()[attr_name] + warn_deprecation( + deprecated_name=f"litestar.contrib.sqlalchemy.plugins.init.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.plugins.init' is deprecated, please " + f"import it from '{module}' instead", + ) + return value + + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover + + +if TYPE_CHECKING: + from advanced_alchemy.config import ( # pyright: ignore[reportMissingImports] + GenericSessionConfig, + GenericSQLAlchemyConfig, + ) + from advanced_alchemy.extensions.litestar import ( # pyright: ignore[reportMissingImports] + AsyncSessionConfig, + EngineConfig, + SQLAlchemyAsyncConfig, + SQLAlchemyInitPlugin, + SQLAlchemySyncConfig, + SyncSessionConfig, + ) diff --git a/litestar/contrib/sqlalchemy/plugins/init/config/__init__.py b/litestar/contrib/sqlalchemy/plugins/init/config/__init__.py index f2e39da99e..c641d62a60 100644 --- a/litestar/contrib/sqlalchemy/plugins/init/config/__init__.py +++ b/litestar/contrib/sqlalchemy/plugins/init/config/__init__.py @@ -1,9 +1,9 @@ +# ruff: noqa: TC004,F401 from __future__ import annotations -from .asyncio import AsyncSessionConfig, SQLAlchemyAsyncConfig -from .common import GenericSessionConfig, GenericSQLAlchemyConfig -from .engine import EngineConfig -from .sync import SQLAlchemySyncConfig, SyncSessionConfig +from typing import TYPE_CHECKING + +from litestar.utils import warn_deprecation __all__ = ( "AsyncSessionConfig", @@ -14,3 +14,53 @@ "SQLAlchemySyncConfig", "SyncSessionConfig", ) + + +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + if attr_name in ("GenericSQLAlchemyConfig", "GenericSessionConfig"): + module = "litestar.plugins.sqlalchemy.config" + from advanced_alchemy.config import ( # pyright: ignore[reportMissingImports] + GenericSessionConfig, # pyright: ignore[reportUnusedImport] + GenericSQLAlchemyConfig, # pyright: ignore[reportUnusedImport] + ) + + value = globals()[attr_name] = locals()[attr_name] + else: + module = "litestar.plugins.sqlalchemy" + from advanced_alchemy.extensions.litestar import ( # pyright: ignore[reportMissingImports] + AsyncSessionConfig, # pyright: ignore[reportUnusedImport] + EngineConfig, # pyright: ignore[reportUnusedImport] + SQLAlchemyAsyncConfig, # pyright: ignore[reportUnusedImport] + SQLAlchemySyncConfig, # pyright: ignore[reportUnusedImport] + SyncSessionConfig, # pyright: ignore[reportUnusedImport] + ) + + value = globals()[attr_name] = locals()[attr_name] + + warn_deprecation( + deprecated_name=f"litestar.contrib.sqlalchemy.plugins.init.config.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.plugins.init.config' is deprecated, please " + f"import it from '{module}' instead", + ) + value = globals()[attr_name] = locals()[attr_name] + return value + + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover + + +if TYPE_CHECKING: + from advanced_alchemy.config import ( # pyright: ignore[reportMissingImports] + GenericSessionConfig, + GenericSQLAlchemyConfig, + ) + from advanced_alchemy.extensions.litestar import ( # pyright: ignore[reportMissingImports] + AsyncSessionConfig, + EngineConfig, + SQLAlchemyAsyncConfig, + SQLAlchemySyncConfig, + SyncSessionConfig, + ) diff --git a/litestar/contrib/sqlalchemy/plugins/init/config/asyncio.py b/litestar/contrib/sqlalchemy/plugins/init/config/asyncio.py index 434c7611ae..1fe69cab40 100644 --- a/litestar/contrib/sqlalchemy/plugins/init/config/asyncio.py +++ b/litestar/contrib/sqlalchemy/plugins/init/config/asyncio.py @@ -1,24 +1,74 @@ +# ruff: noqa: TC004, F401 +# pyright: reportUnusedImport=false from __future__ import annotations -from advanced_alchemy.config.asyncio import AlembicAsyncConfig, AsyncSessionConfig -from advanced_alchemy.extensions.litestar.plugins.init.config.asyncio import ( - SQLAlchemyAsyncConfig as _SQLAlchemyAsyncConfig, -) -from advanced_alchemy.extensions.litestar.plugins.init.config.asyncio import ( - autocommit_before_send_handler, - default_before_send_handler, -) -from sqlalchemy.ext.asyncio import AsyncEngine +from typing import TYPE_CHECKING -from litestar.contrib.sqlalchemy.plugins.init.config.compat import _CreateEngineMixin +from litestar.utils import warn_deprecation __all__ = ( - "SQLAlchemyAsyncConfig", "AlembicAsyncConfig", "AsyncSessionConfig", - "default_before_send_handler", + "SQLAlchemyAsyncConfig", "autocommit_before_send_handler", + "default_before_send_handler", ) -class SQLAlchemyAsyncConfig(_SQLAlchemyAsyncConfig, _CreateEngineMixin[AsyncEngine]): ... +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + if attr_name == "SQLAlchemyAsyncConfig": + from advanced_alchemy.extensions.litestar.plugins.init.config.asyncio import ( + SQLAlchemyAsyncConfig as _SQLAlchemyAsyncConfig, + ) + from sqlalchemy.ext.asyncio import AsyncEngine + + from litestar.contrib.sqlalchemy.plugins.init.config.compat import ( + _CreateEngineMixin, # pyright: ignore[reportPrivateUsage] + ) + + class SQLAlchemyAsyncConfig(_SQLAlchemyAsyncConfig, _CreateEngineMixin[AsyncEngine]): ... + + module = "litestar.plugins.sqlalchemy" + value = globals()[attr_name] = SQLAlchemyAsyncConfig + elif attr_name in {"default_before_send_handler", "autocommit_before_send_handler"}: + module = "litestar.plugins.sqlalchemy.plugins.init.config.asyncio" + from advanced_alchemy.extensions.litestar.plugins.init.config.asyncio import ( + autocommit_before_send_handler, + default_before_send_handler, + ) + + value = globals()[attr_name] = locals()[attr_name] + else: + module = "litestar.plugins.sqlalchemy" + from advanced_alchemy.extensions.litestar import ( + AlembicAsyncConfig, + AsyncSessionConfig, + ) + + value = globals()[attr_name] = locals()[attr_name] + + warn_deprecation( + deprecated_name=f"litestar.contrib.sqlalchemy.plugins.init.config.asyncio.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.plugins.init.config.asyncio' is deprecated, please " + f"import it from '{module}' instead", + ) + value = globals()[attr_name] = locals()[attr_name] + return value + + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover + + +if TYPE_CHECKING: + from advanced_alchemy.extensions.litestar import ( + AlembicAsyncConfig, + AsyncSessionConfig, + SQLAlchemyAsyncConfig, + ) + from advanced_alchemy.extensions.litestar.plugins.init.config.asyncio import ( + autocommit_before_send_handler, + default_before_send_handler, + ) diff --git a/litestar/contrib/sqlalchemy/plugins/init/config/common.py b/litestar/contrib/sqlalchemy/plugins/init/config/common.py index 9afc48c428..64ad05d267 100644 --- a/litestar/contrib/sqlalchemy/plugins/init/config/common.py +++ b/litestar/contrib/sqlalchemy/plugins/init/config/common.py @@ -1,15 +1,53 @@ +# ruff: noqa: TC004, F401 from __future__ import annotations -from advanced_alchemy.config.common import GenericAlembicConfig, GenericSessionConfig, GenericSQLAlchemyConfig -from advanced_alchemy.extensions.litestar.plugins.init.config.common import ( - SESSION_SCOPE_KEY, - SESSION_TERMINUS_ASGI_EVENTS, -) +from typing import TYPE_CHECKING + +from litestar.utils import warn_deprecation __all__ = ( "SESSION_SCOPE_KEY", "SESSION_TERMINUS_ASGI_EVENTS", + "GenericAlembicConfig", "GenericSQLAlchemyConfig", "GenericSessionConfig", - "GenericAlembicConfig", ) + + +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + if attr_name in ("GenericSQLAlchemyConfig", "GenericSessionConfig", "GenericAlembicConfig"): + module = "litestar.plugins.sqlalchemy.config" + from advanced_alchemy.config.common import ( # pyright: ignore[reportMissingImports] + GenericAlembicConfig, # pyright: ignore[reportUnusedImport] + GenericSessionConfig, # pyright: ignore[reportUnusedImport] + GenericSQLAlchemyConfig, # pyright: ignore[reportUnusedImport] + ) + else: + from advanced_alchemy.extensions.litestar.plugins.init.config.common import ( # pyright: ignore[reportMissingImports] + SESSION_SCOPE_KEY, # pyright: ignore[reportUnusedImport] + SESSION_TERMINUS_ASGI_EVENTS, # pyright: ignore[reportUnusedImport] + ) + + module = "litestar.plugins.sqlalchemy.plugins.init.config.common" + + warn_deprecation( + deprecated_name=f"litestar.contrib.sqlalchemy.plugins.init.config.common.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.plugins.init.config.common' is deprecated, please " + f"import it from '{module}' instead", + ) + value = globals()[attr_name] = locals()[attr_name] + return value + + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover + + +if TYPE_CHECKING: + from advanced_alchemy.config.common import GenericAlembicConfig, GenericSessionConfig, GenericSQLAlchemyConfig + from advanced_alchemy.extensions.litestar.plugins.init.config.common import ( + SESSION_SCOPE_KEY, + SESSION_TERMINUS_ASGI_EVENTS, + ) diff --git a/litestar/contrib/sqlalchemy/plugins/init/config/compat.py b/litestar/contrib/sqlalchemy/plugins/init/config/compat.py index d76dea700d..73969f8944 100644 --- a/litestar/contrib/sqlalchemy/plugins/init/config/compat.py +++ b/litestar/contrib/sqlalchemy/plugins/init/config/compat.py @@ -16,7 +16,7 @@ class HasGetEngine(Protocol[EngineT_co]): def get_engine(self) -> EngineT_co: ... -class _CreateEngineMixin(Generic[EngineT_co]): +class _CreateEngineMixin(Generic[EngineT_co]): # pyright: ignore[reportUnusedClass] @deprecated(version="2.1.1", removal_in="3.0.0", alternative="get_engine()") def create_engine(self: HasGetEngine[EngineT_co]) -> EngineT_co: return self.get_engine() diff --git a/litestar/contrib/sqlalchemy/plugins/init/config/engine.py b/litestar/contrib/sqlalchemy/plugins/init/config/engine.py index 31c3f5e2a3..6cc5e385e7 100644 --- a/litestar/contrib/sqlalchemy/plugins/init/config/engine.py +++ b/litestar/contrib/sqlalchemy/plugins/init/config/engine.py @@ -1,5 +1,32 @@ +# ruff: noqa: TC004, F401 from __future__ import annotations -from advanced_alchemy.config.engine import EngineConfig +from typing import TYPE_CHECKING + +from litestar.utils import warn_deprecation __all__ = ("EngineConfig",) + + +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + from advanced_alchemy.extensions.litestar import EngineConfig + + module = "litestar.plugins.sqlalchemy" + + warn_deprecation( + deprecated_name=f"litestar.contrib.sqlalchemy.plugins.init.config.engine.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.plugins.init.config.engine' is deprecated, please " + f"import it from '{module}' instead", + ) + value = globals()[attr_name] = EngineConfig + return value + + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover + + +if TYPE_CHECKING: + from advanced_alchemy.extensions.litestar import EngineConfig diff --git a/litestar/contrib/sqlalchemy/plugins/init/config/sync.py b/litestar/contrib/sqlalchemy/plugins/init/config/sync.py index 48a029b3db..4974fa6aba 100644 --- a/litestar/contrib/sqlalchemy/plugins/init/config/sync.py +++ b/litestar/contrib/sqlalchemy/plugins/init/config/sync.py @@ -1,24 +1,73 @@ +# ruff: noqa: TC004, F401 + from __future__ import annotations -from advanced_alchemy.config.sync import AlembicSyncConfig, SyncSessionConfig -from advanced_alchemy.extensions.litestar.plugins.init.config.sync import ( - SQLAlchemySyncConfig as _SQLAlchemySyncConfig, -) -from advanced_alchemy.extensions.litestar.plugins.init.config.sync import ( - autocommit_before_send_handler, - default_before_send_handler, -) -from sqlalchemy import Engine +from typing import TYPE_CHECKING -from litestar.contrib.sqlalchemy.plugins.init.config.compat import _CreateEngineMixin +from litestar.utils import warn_deprecation __all__ = ( - "SQLAlchemySyncConfig", "AlembicSyncConfig", + "SQLAlchemySyncConfig", "SyncSessionConfig", - "default_before_send_handler", "autocommit_before_send_handler", + "default_before_send_handler", ) -class SQLAlchemySyncConfig(_SQLAlchemySyncConfig, _CreateEngineMixin[Engine]): ... +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + if attr_name == "SQLAlchemySyncConfig": + from advanced_alchemy.extensions.litestar.plugins.init.config.sync import ( + SQLAlchemySyncConfig as _SQLAlchemySyncConfig, + ) + from sqlalchemy import Engine + + from litestar.contrib.sqlalchemy.plugins.init.config.compat import ( + _CreateEngineMixin, # pyright: ignore[reportPrivateUsage] + ) + + class SQLAlchemySyncConfig(_SQLAlchemySyncConfig, _CreateEngineMixin[Engine]): ... + + module = "litestar.plugins.sqlalchemy" + value = globals()[attr_name] = SQLAlchemySyncConfig + elif attr_name in {"default_before_send_handler", "autocommit_before_send_handler"}: + module = "litestar.plugins.sqlalchemy.plugins.init.config.sync" + from advanced_alchemy.extensions.litestar.plugins.init.config.sync import ( + autocommit_before_send_handler, # pyright: ignore[reportUnusedImport] + default_before_send_handler, # pyright: ignore[reportUnusedImport] + ) + + value = globals()[attr_name] = locals()[attr_name] + else: + module = "litestar.plugins.sqlalchemy" + from advanced_alchemy.extensions.litestar import ( + AlembicSyncConfig, # pyright: ignore[reportUnusedImport] + SyncSessionConfig, # pyright: ignore[reportUnusedImport] + ) + + value = globals()[attr_name] = locals()[attr_name] + + warn_deprecation( + deprecated_name=f"litestar.contrib.sqlalchemy.plugins.init.config.sync.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.plugins.init.config.sync' is deprecated, please " + f"import it from '{module}' instead", + ) + return value + + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover + + +if TYPE_CHECKING: + from advanced_alchemy.extensions.litestar import ( + AlembicSyncConfig, + SQLAlchemySyncConfig, + SyncSessionConfig, + ) + from advanced_alchemy.extensions.litestar.plugins.init.config.sync import ( + autocommit_before_send_handler, + default_before_send_handler, + ) diff --git a/litestar/contrib/sqlalchemy/plugins/init/plugin.py b/litestar/contrib/sqlalchemy/plugins/init/plugin.py index dbf814bfef..2b08acc697 100644 --- a/litestar/contrib/sqlalchemy/plugins/init/plugin.py +++ b/litestar/contrib/sqlalchemy/plugins/init/plugin.py @@ -1,5 +1,31 @@ +# ruff: noqa: TC004, F401 +# pyright: reportUnusedImport=false from __future__ import annotations -from advanced_alchemy.extensions.litestar.plugins import SQLAlchemyInitPlugin +from typing import TYPE_CHECKING + +from litestar.utils import warn_deprecation __all__ = ("SQLAlchemyInitPlugin",) + + +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + warn_deprecation( + deprecated_name=f"litestar.contrib.sqlalchemy.plugins.init.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.plugins.init' is deprecated, please " + f"import it from 'litestar.plugins.sqlalchemy' instead", + ) + from advanced_alchemy.extensions.litestar import SQLAlchemyInitPlugin + + value = globals()[attr_name] = locals()[attr_name] + return value + + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover + + +if TYPE_CHECKING: + from advanced_alchemy.extensions.litestar import SQLAlchemyInitPlugin diff --git a/litestar/contrib/sqlalchemy/plugins/serialization.py b/litestar/contrib/sqlalchemy/plugins/serialization.py index 539b194cc4..a8257507a3 100644 --- a/litestar/contrib/sqlalchemy/plugins/serialization.py +++ b/litestar/contrib/sqlalchemy/plugins/serialization.py @@ -1,5 +1,30 @@ +# ruff: noqa: TC004 from __future__ import annotations -from advanced_alchemy.extensions.litestar.plugins import SQLAlchemySerializationPlugin +from typing import TYPE_CHECKING + +from litestar.utils import warn_deprecation __all__ = ("SQLAlchemySerializationPlugin",) + + +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + warn_deprecation( + deprecated_name=f"litestar.contrib.sqlalchemy.plugins.serialization.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.plugins.serialization' is deprecated, please " + "import it from 'litestar.plugins.sqlalchemy' instead", + ) + from advanced_alchemy.extensions.litestar import SQLAlchemySerializationPlugin + + value = globals()[attr_name] = SQLAlchemySerializationPlugin + return value + + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover + + +if TYPE_CHECKING: + from advanced_alchemy.extensions.litestar import SQLAlchemySerializationPlugin diff --git a/litestar/contrib/sqlalchemy/repository/__init__.py b/litestar/contrib/sqlalchemy/repository/__init__.py index 64a8359169..1e9b060d04 100644 --- a/litestar/contrib/sqlalchemy/repository/__init__.py +++ b/litestar/contrib/sqlalchemy/repository/__init__.py @@ -1,11 +1,58 @@ -from ._async import SQLAlchemyAsyncRepository -from ._sync import SQLAlchemySyncRepository -from ._util import wrap_sqlalchemy_exception -from .types import ModelT +# ruff: noqa: TC004, F401 +# pyright: reportUnusedImport=false +from __future__ import annotations + +from typing import TYPE_CHECKING + +from litestar.utils import warn_deprecation __all__ = ( + "ModelT", "SQLAlchemyAsyncRepository", "SQLAlchemySyncRepository", - "ModelT", "wrap_sqlalchemy_exception", ) + + +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + if attr_name in ("SQLAlchemyAsyncRepository", "SQLAlchemySyncRepository", "ModelT"): + module = "litestar.plugins.sqlalchemy.repository" + from advanced_alchemy.repository import ( # type: ignore[import-not-found] # pyright: ignore[reportMissingImport] + ModelT, + SQLAlchemyAsyncRepository, + SQLAlchemySyncRepository, + ) + + elif attr_name == "wrap_sqlalchemy_exception": + module = "litestar.plugins.sqlalchemy.exceptions" + from advanced_alchemy.exceptions import ( # type: ignore[import-not-found] # pyright: ignore[reportMissingImport] + wrap_sqlalchemy_exception, # type: ignore[import-not-found] # pyright: ignore[reportMissingImport] + ) + + else: # pragma: no cover + raise RuntimeError(f"Unhandled module attribute: {attr_name!r}") + + value = globals()[attr_name] = locals()[attr_name] + warn_deprecation( + deprecated_name=f"litestar.contrib.sqlalchemy.repository.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.repository' is deprecated, please " + f"import it from '{module}' instead", + ) + return value + + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover + + +if TYPE_CHECKING: + from advanced_alchemy.exceptions import ( # type: ignore[import-not-found] # pyright: ignore[reportMissingImport] + wrap_sqlalchemy_exception, + ) + from advanced_alchemy.repository import ( # type: ignore[import-not-found] # pyright: ignore[reportMissingImport] + ModelT, + SQLAlchemyAsyncRepository, + SQLAlchemySyncRepository, + ) diff --git a/litestar/contrib/sqlalchemy/repository/_async.py b/litestar/contrib/sqlalchemy/repository/_async.py index 417ec35e05..3f9451c22a 100644 --- a/litestar/contrib/sqlalchemy/repository/_async.py +++ b/litestar/contrib/sqlalchemy/repository/_async.py @@ -1,5 +1,34 @@ +# ruff: noqa: TC004, F401 from __future__ import annotations -from advanced_alchemy.repository import SQLAlchemyAsyncRepository +from typing import TYPE_CHECKING + +from litestar.utils import warn_deprecation __all__ = ("SQLAlchemyAsyncRepository",) + + +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + from advanced_alchemy.repository import ( + SQLAlchemyAsyncRepository, # type: ignore[import-not-found] # pyright: ignore[reportMissingImports,reportUnusedImport] + ) + + warn_deprecation( + deprecated_name=f"litestar.contrib.sqlalchemy.repository.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.repository._async' is deprecated, please " + f"import it from 'litestar.plugins.sqlalchemy.repository' instead", + ) + value = globals()[attr_name] = locals()[attr_name] + return value + + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover + + +if TYPE_CHECKING: + from advanced_alchemy.repository import ( # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] + SQLAlchemyAsyncRepository, + ) diff --git a/litestar/contrib/sqlalchemy/repository/_sync.py b/litestar/contrib/sqlalchemy/repository/_sync.py index 58ccbb8109..5d4ddf7274 100644 --- a/litestar/contrib/sqlalchemy/repository/_sync.py +++ b/litestar/contrib/sqlalchemy/repository/_sync.py @@ -1,7 +1,36 @@ +# ruff: noqa: TC004, F401 # Do not edit this file directly. It has been autogenerated from # litestar/contrib/sqlalchemy/repository/_async.py from __future__ import annotations -from advanced_alchemy.repository import SQLAlchemySyncRepository +from typing import TYPE_CHECKING + +from litestar.utils import warn_deprecation __all__ = ("SQLAlchemySyncRepository",) + + +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + from advanced_alchemy.repository import ( + SQLAlchemySyncRepository, # type: ignore[import-not-found] # pyright: ignore[reportMissingImports,reportUnusedImport] + ) + + warn_deprecation( + deprecated_name=f"litestar.contrib.sqlalchemy.repository.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.repository._sync' is deprecated, please " + f"import it from 'litestar.plugins.sqlalchemy.repository' instead", + ) + value = globals()[attr_name] = locals()[attr_name] + return value + + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover + + +if TYPE_CHECKING: + from advanced_alchemy.repository import ( + SQLAlchemySyncRepository, # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] + ) diff --git a/litestar/contrib/sqlalchemy/repository/_util.py b/litestar/contrib/sqlalchemy/repository/_util.py index c0ce7476f4..261f67a790 100644 --- a/litestar/contrib/sqlalchemy/repository/_util.py +++ b/litestar/contrib/sqlalchemy/repository/_util.py @@ -1,8 +1,43 @@ +# ruff: noqa: TC004, F401 from __future__ import annotations -from advanced_alchemy.repository._util import get_instrumented_attr, wrap_sqlalchemy_exception +from typing import TYPE_CHECKING + +from litestar.utils import warn_deprecation __all__ = ( - "wrap_sqlalchemy_exception", "get_instrumented_attr", + "wrap_sqlalchemy_exception", ) + + +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + from advanced_alchemy.exceptions import ( + wrap_sqlalchemy_exception, # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] + ) + from advanced_alchemy.repository import ( + get_instrumented_attr, # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] + ) + + warn_deprecation( + deprecated_name=f"litestar.contrib.sqlalchemy.repository._util.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.repository._util' is deprecated, please " + f"import it from 'litestar.plugins.sqlalchemy.repository' instead", + ) + value = globals()[attr_name] = locals()[attr_name] + return value + + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover + + +if TYPE_CHECKING: + from advanced_alchemy.exceptions import ( + wrap_sqlalchemy_exception, # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] + ) + from advanced_alchemy.repository import ( + get_instrumented_attr, # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] + ) diff --git a/litestar/contrib/sqlalchemy/repository/types.py b/litestar/contrib/sqlalchemy/repository/types.py index 2a4204cb62..7733175987 100644 --- a/litestar/contrib/sqlalchemy/repository/types.py +++ b/litestar/contrib/sqlalchemy/repository/types.py @@ -1,15 +1,49 @@ -from advanced_alchemy.repository.typing import ( - ModelT, - RowT, - SelectT, - SQLAlchemyAsyncRepositoryT, - SQLAlchemySyncRepositoryT, -) +# ruff: noqa: TC004, F401 +# pyright: reportUnusedImport=false +from __future__ import annotations + +from typing import TYPE_CHECKING + +from litestar.utils import warn_deprecation __all__ = ( "ModelT", - "SelectT", "RowT", - "SQLAlchemySyncRepositoryT", "SQLAlchemyAsyncRepositoryT", + "SQLAlchemySyncRepositoryT", + "SelectT", ) + + +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + from advanced_alchemy.repository.typing import ( # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] + ModelT, + RowT, + SelectT, + SQLAlchemyAsyncRepositoryT, + SQLAlchemySyncRepositoryT, + ) + + warn_deprecation( + deprecated_name=f"litestar.contrib.sqlalchemy.repository.types.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.repository.types' is deprecated, please " + f"import it from 'litestar.plugins.sqlalchemy.repository.typing' instead", + ) + value = globals()[attr_name] = locals()[attr_name] + return value + + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover + + +if TYPE_CHECKING: + from advanced_alchemy.repository.typing import ( # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] + ModelT, + RowT, + SelectT, + SQLAlchemyAsyncRepositoryT, + SQLAlchemySyncRepositoryT, + ) diff --git a/litestar/contrib/sqlalchemy/types.py b/litestar/contrib/sqlalchemy/types.py index 61fb75a0c8..0ad8fed7df 100644 --- a/litestar/contrib/sqlalchemy/types.py +++ b/litestar/contrib/sqlalchemy/types.py @@ -1,11 +1,49 @@ +# ruff: noqa: TC004, F401 +# pyright: reportUnusedImport=false from __future__ import annotations -from advanced_alchemy.types import GUID, ORA_JSONB, BigIntIdentity, DateTimeUTC, JsonB +from typing import TYPE_CHECKING + +from litestar.utils import warn_deprecation __all__ = ( "GUID", "ORA_JSONB", - "DateTimeUTC", "BigIntIdentity", + "DateTimeUTC", "JsonB", ) + + +def __getattr__(attr_name: str) -> object: + if attr_name in __all__: + from advanced_alchemy.types import ( + GUID, + ORA_JSONB, + BigIntIdentity, + DateTimeUTC, + JsonB, + ) + + warn_deprecation( + deprecated_name=f"litestar.contrib.sqlalchemy.{attr_name}", + version="2.12", + kind="import", + removal_in="3.0", + info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy' is deprecated, please " + f"import it from 'advanced_alchemy.extensions.litestar.types' instead", + ) + value = globals()[attr_name] = locals()[attr_name] + return value + + raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover + + +if TYPE_CHECKING: + from advanced_alchemy.types import ( + GUID, + ORA_JSONB, + BigIntIdentity, + DateTimeUTC, + JsonB, + ) diff --git a/litestar/controller.py b/litestar/controller.py index 3893acdf98..a49a297c5e 100644 --- a/litestar/controller.py +++ b/litestar/controller.py @@ -64,6 +64,7 @@ class Controller: "parameters", "path", "request_class", + "request_max_body_size", "response_class", "response_cookies", "response_headers", @@ -72,8 +73,8 @@ class Controller: "signature_namespace", "signature_types", "tags", - "type_encoders", "type_decoders", + "type_encoders", "websocket_class", ) @@ -136,6 +137,11 @@ class Controller: """A custom subclass of :class:`Request <.connection.Request>` to be used as the default request for all route handlers under the controller. """ + request_max_body_size: int | None | EmptyType + """ + Maximum allowed size of the request body in bytes. If this size is exceeded, a '413 - Request Entity Too Large' + error response is returned.""" + response_class: type[Response] | None """A custom subclass of :class:`Response <.response.Response>` to be used as the default response for all route handlers under the controller. @@ -191,6 +197,9 @@ def __init__(self, owner: Router) -> None: if not hasattr(self, "include_in_schema"): self.include_in_schema = Empty + if not hasattr(self, "request_max_body_size"): + self.request_max_body_size = Empty + self.signature_namespace = add_types_to_signature_namespace( getattr(self, "signature_types", []), getattr(self, "signature_namespace", {}) ) @@ -235,6 +244,7 @@ def as_router(self) -> Router: type_encoders=self.type_encoders, type_decoders=self.type_decoders, websocket_class=self.websocket_class, + request_max_body_size=self.request_max_body_size, ) router.owner = self.owner return router diff --git a/litestar/data_extractors.py b/litestar/data_extractors.py index 61993b4552..73e1a1c03c 100644 --- a/litestar/data_extractors.py +++ b/litestar/data_extractors.py @@ -12,8 +12,8 @@ "ConnectionDataExtractor", "ExtractedRequestData", "ExtractedResponseData", - "ResponseDataExtractor", "RequestExtractorField", + "ResponseDataExtractor", "ResponseExtractorField", ) @@ -66,11 +66,11 @@ class ConnectionDataExtractor: __slots__ = ( "connection_extractors", - "request_extractors", + "obfuscate_cookies", + "obfuscate_headers", "parse_body", "parse_query", - "obfuscate_headers", - "obfuscate_cookies", + "request_extractors", "skip_parse_malformed_body", ) @@ -324,7 +324,7 @@ class ExtractedResponseData(TypedDict, total=False): class ResponseDataExtractor: """Utility class to extract data from a ``Message``""" - __slots__ = ("extractors", "parse_headers", "obfuscate_headers", "obfuscate_cookies") + __slots__ = ("extractors", "obfuscate_cookies", "obfuscate_headers", "parse_headers") def __init__( self, diff --git a/litestar/datastructures/__init__.py b/litestar/datastructures/__init__.py index cddf0ae3e0..64bd7101b6 100644 --- a/litestar/datastructures/__init__.py +++ b/litestar/datastructures/__init__.py @@ -20,6 +20,7 @@ from litestar.datastructures.url import URL, Address __all__ = ( + "URL", "Accept", "Address", "CacheControlHeader", @@ -38,5 +39,4 @@ "SecretString", "State", "UploadFile", - "URL", ) diff --git a/litestar/datastructures/headers.py b/litestar/datastructures/headers.py index 17c9fc92fc..ad9e3df62f 100644 --- a/litestar/datastructures/headers.py +++ b/litestar/datastructures/headers.py @@ -385,7 +385,7 @@ def __post_init__(self) -> None: class MediaTypeHeader: """A helper class for ``Accept`` header parsing.""" - __slots__ = ("maintype", "subtype", "params", "_params_str") + __slots__ = ("_params_str", "maintype", "params", "subtype") def __init__(self, type_str: str) -> None: # preserve the original parameters, because the order might be diff --git a/litestar/datastructures/multi_dicts.py b/litestar/datastructures/multi_dicts.py index 7702e1a8d5..733be6be0b 100644 --- a/litestar/datastructures/multi_dicts.py +++ b/litestar/datastructures/multi_dicts.py @@ -95,6 +95,27 @@ def copy(self) -> Self: # type: ignore[override] class FormMultiDict(ImmutableMultiDict[Any]): """MultiDict for form data.""" + @classmethod + def from_form_data(cls, form_data: dict[str, list[str] | str | UploadFile]) -> FormMultiDict: + """Create a FormMultiDict from form data. + + Args: + form_data: Form data to create the FormMultiDict from. + + Returns: + A FormMultiDict instance + """ + # Convert form_data to a list[tuple[str, str | UploadFile]] before passing it + # to FormMultiDict so multi-keys can be accessed properly + items = [] + for k, v in form_data.items(): + if not isinstance(v, list): + items.append((k, v)) + else: + for sv in v: + items.append((k, sv)) + return cls(items) + async def close(self) -> None: """Close all files in the multi-dict. diff --git a/litestar/datastructures/response_header.py b/litestar/datastructures/response_header.py index f781d0c37e..807eec6bd5 100644 --- a/litestar/datastructures/response_header.py +++ b/litestar/datastructures/response_header.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Any from litestar.exceptions import ImproperlyConfiguredException +from litestar.utils import warn_deprecation if TYPE_CHECKING: from litestar.openapi.spec import Example @@ -46,7 +47,7 @@ class ResponseHeader: Default value is `false`. """ - allow_empty_value: bool = False + allow_empty_value: bool = None # type: ignore[assignment] """Sets the ability to pass empty-valued parameters. This is valid only for `query` parameters and allows sending a parameter with an empty value. Default value is `false`. If. @@ -80,7 +81,7 @@ class ResponseHeader: For all other styles, the default value is `false`. """ - allow_reserved: bool = False + allow_reserved: bool = None # type: ignore[assignment] """Determines whether the parameter value SHOULD allow reserved characters, as defined by. @@ -121,5 +122,27 @@ def __post_init__(self) -> None: if not self.documentation_only and self.value is None: raise ImproperlyConfiguredException("value must be set if documentation_only is false") + if self.allow_reserved is None: + self.allow_reserved = False # type: ignore[unreachable] + else: + warn_deprecation( + "2.13.1", + "allow_reserved", + kind="parameter", + removal_in="4", + info="This property is invalid for headers and will be ignored", + ) + + if self.allow_empty_value is None: + self.allow_empty_value = False # type: ignore[unreachable] + else: + warn_deprecation( + "2.13.1", + "allow_empty_value", + kind="parameter", + removal_in="4", + info="This property is invalid for headers and will be ignored", + ) + def __hash__(self) -> int: return hash(self.name) diff --git a/litestar/datastructures/state.py b/litestar/datastructures/state.py index a11ed37791..1d5b8371b5 100644 --- a/litestar/datastructures/state.py +++ b/litestar/datastructures/state.py @@ -19,8 +19,8 @@ class ImmutableState(Mapping[str, Any]): """ __slots__ = ( - "_state", "_deep_copy", + "_state", ) _state: dict[str, Any] diff --git a/litestar/datastructures/upload_file.py b/litestar/datastructures/upload_file.py index 09ad2d32ab..3a83bfcb81 100644 --- a/litestar/datastructures/upload_file.py +++ b/litestar/datastructures/upload_file.py @@ -1,3 +1,4 @@ +# ruff: noqa: SIM115 from __future__ import annotations from tempfile import SpooledTemporaryFile @@ -11,7 +12,7 @@ class UploadFile: """Representation of a file upload""" - __slots__ = ("filename", "file", "content_type", "headers") + __slots__ = ("content_type", "file", "filename", "headers") def __init__( self, @@ -48,7 +49,7 @@ def rolled_to_disk(self) -> bool: """ return getattr(self.file, "_rolled", False) - async def write(self, data: bytes) -> int: + async def write(self, data: bytes | bytearray) -> int: """Proxy for data writing. Args: @@ -93,6 +94,8 @@ async def close(self) -> None: Returns: None. """ + if self.file.closed: + return None if self.rolled_to_disk: return await sync_to_thread(self.file.close) return self.file.close() diff --git a/litestar/datastructures/url.py b/litestar/datastructures/url.py index f3441d06ef..6f78415ac8 100644 --- a/litestar/datastructures/url.py +++ b/litestar/datastructures/url.py @@ -13,7 +13,7 @@ from litestar.types import EmptyType, Scope -__all__ = ("Address", "URL") +__all__ = ("URL", "Address") _DEFAULT_SCHEME_PORTS = {"http": 80, "https": 443, "ftp": 21, "ws": 80, "wss": 443} @@ -47,8 +47,8 @@ class URL: """Representation and modification utilities of a URL.""" __slots__ = ( - "_query_params", "_parsed_url", + "_query_params", "fragment", "hostname", "netloc", diff --git a/litestar/di.py b/litestar/di.py index 066a128454..b47f13478f 100644 --- a/litestar/di.py +++ b/litestar/di.py @@ -26,9 +26,9 @@ class Provide: __slots__ = ( "dependency", + "has_async_generator_dependency", "has_sync_callable", "has_sync_generator_dependency", - "has_async_generator_dependency", "parsed_fn_signature", "signature_model", "sync_to_thread", @@ -54,7 +54,7 @@ def __init__( sync_to_thread: Run sync code in an async thread. Defaults to False. """ if not callable(dependency): - raise ImproperlyConfiguredException("Provider dependency must a callable value") + raise ImproperlyConfiguredException("Provider dependency must be a callable value") is_class_dependency = isclass(dependency) self.has_sync_generator_dependency = isgeneratorfunction( diff --git a/litestar/dto/_backend.py b/litestar/dto/_backend.py index 167a2a9740..4d9651a19e 100644 --- a/litestar/dto/_backend.py +++ b/litestar/dto/_backend.py @@ -805,8 +805,11 @@ def _create_struct_for_field_definitions( if field_definition.is_partial: field_type = Union[field_type, UnsetType] - if (field_meta := _create_struct_field_meta_for_field_definition(field_definition)) is not None: - field_type = Annotated[field_type, field_meta] + if field_definition.passthrough_constraints: + if (field_meta := _create_struct_field_meta_for_field_definition(field_definition)) is not None: + field_type = Annotated[field_type, field_meta] + elif field_definition.kwarg_definition: + field_type = Annotated[field_type, field_definition.kwarg_definition] struct_fields.append( ( diff --git a/litestar/dto/_codegen_backend.py b/litestar/dto/_codegen_backend.py index a3fb25f615..de29ba07cd 100644 --- a/litestar/dto/_codegen_backend.py +++ b/litestar/dto/_codegen_backend.py @@ -43,11 +43,11 @@ class DTOCodegenBackend(DTOBackend): __slots__ = ( - "_transfer_to_dict", - "_transfer_to_model_type", + "_encode_data", "_transfer_data_from_builtins", "_transfer_data_from_builtins_with_overrides", - "_encode_data", + "_transfer_to_dict", + "_transfer_to_model_type", ) def __init__( diff --git a/litestar/dto/_types.py b/litestar/dto/_types.py index b0863b2593..0ded5a85a8 100644 --- a/litestar/dto/_types.py +++ b/litestar/dto/_types.py @@ -17,7 +17,7 @@ class NestedFieldInfo: """Type for representing fields and model type of nested model type.""" - __slots__ = ("model", "field_definitions") + __slots__ = ("field_definitions", "model") model: type[Any] field_definitions: tuple[TransferDTOFieldDefinition, ...] @@ -142,4 +142,5 @@ def from_dto_field_definition( transfer_type=transfer_type, type_wrappers=field_definition.type_wrappers, model_name=field_definition.model_name, + passthrough_constraints=field_definition.passthrough_constraints, ) diff --git a/litestar/dto/data_structures.py b/litestar/dto/data_structures.py index a5c3386f1c..b660cddd72 100644 --- a/litestar/dto/data_structures.py +++ b/litestar/dto/data_structures.py @@ -68,6 +68,7 @@ class DTOFieldDefinition(FieldDefinition): "default_factory", "dto_field", "model_name", + "passthrough_constraints", ) model_name: str @@ -76,6 +77,8 @@ class DTOFieldDefinition(FieldDefinition): """Default factory of the field.""" dto_field: DTOField """DTO field configuration.""" + passthrough_constraints: bool + """Pass constraints of the source annotation to be validated by the DTO backend""" @classmethod def from_field_definition( @@ -84,6 +87,7 @@ def from_field_definition( model_name: str, default_factory: Callable[[], Any] | None, dto_field: DTOField, + passthrough_constraints: bool = True, ) -> DTOFieldDefinition: """Create a :class:`FieldDefinition` from a :class:`FieldDefinition`. @@ -92,6 +96,7 @@ def from_field_definition( model_name: The name of the model. default_factory: Default factory function, if any. dto_field: DTOField instance. + passthrough_constraints: Pass constraints of the source annotation to be validated by the DTO backend Returns: A :class:`FieldDefinition` instance. @@ -113,4 +118,5 @@ def from_field_definition( raw=field_definition.raw, safe_generic_origin=field_definition.safe_generic_origin, type_wrappers=field_definition.type_wrappers, + passthrough_constraints=passthrough_constraints, ) diff --git a/litestar/dto/msgspec_dto.py b/litestar/dto/msgspec_dto.py index 9996319747..c0a2d4b633 100644 --- a/litestar/dto/msgspec_dto.py +++ b/litestar/dto/msgspec_dto.py @@ -1,13 +1,16 @@ from __future__ import annotations +import dataclasses from dataclasses import replace from typing import TYPE_CHECKING, Generic, TypeVar +import msgspec.inspect from msgspec import NODEFAULT, Struct, structs from litestar.dto.base_dto import AbstractDTO from litestar.dto.data_structures import DTOFieldDefinition from litestar.dto.field import DTO_FIELD_META_KEY, extract_dto_field +from litestar.plugins.core._msgspec import kwarg_definition_from_field from litestar.types.empty import Empty if TYPE_CHECKING: @@ -28,16 +31,25 @@ class MsgspecDTO(AbstractDTO[T], Generic[T]): def generate_field_definitions(cls, model_type: type[Struct]) -> Generator[DTOFieldDefinition, None, None]: msgspec_fields = {f.name: f for f in structs.fields(model_type)} + # TODO: Move out of here def default_or_empty(value: Any) -> Any: return Empty if value is NODEFAULT else value def default_or_none(value: Any) -> Any: return None if value is NODEFAULT else value + inspect_fields: dict[str, msgspec.inspect.Field] = { + field.name: field + for field in msgspec.inspect.type_info(model_type).fields # type: ignore[attr-defined] + } + for key, field_definition in cls.get_model_type_hints(model_type).items(): - msgspec_field = msgspec_fields[key] + kwarg_definition, extra = kwarg_definition_from_field(inspect_fields[key]) + field_definition = dataclasses.replace(field_definition, kwarg_definition=kwarg_definition) + field_definition.extra.update(extra) dto_field = extract_dto_field(field_definition, field_definition.extra) field_definition.extra.pop(DTO_FIELD_META_KEY, None) + msgspec_field = msgspec_fields[key] yield replace( DTOFieldDefinition.from_field_definition( diff --git a/litestar/events/__init__.py b/litestar/events/__init__.py index a291141af3..e3bd6e7117 100644 --- a/litestar/events/__init__.py +++ b/litestar/events/__init__.py @@ -1,4 +1,4 @@ from .emitter import BaseEventEmitterBackend, SimpleEventEmitter from .listener import EventListener, listener -__all__ = ("EventListener", "SimpleEventEmitter", "BaseEventEmitterBackend", "listener") +__all__ = ("BaseEventEmitterBackend", "EventListener", "SimpleEventEmitter", "listener") diff --git a/litestar/events/emitter.py b/litestar/events/emitter.py index 7c33c9e73f..ec6634f107 100644 --- a/litestar/events/emitter.py +++ b/litestar/events/emitter.py @@ -63,7 +63,7 @@ def emit(self, event_id: str, *args: Any, **kwargs: Any) -> None: class SimpleEventEmitter(BaseEventEmitterBackend): """Event emitter the works only in the current process""" - __slots__ = ("_queue", "_exit_stack", "_receive_stream", "_send_stream") + __slots__ = ("_exit_stack", "_queue", "_receive_stream", "_send_stream") def __init__(self, listeners: Sequence[EventListener]) -> None: """Create an event emitter instance. diff --git a/litestar/exceptions/base_exceptions.py b/litestar/exceptions/base_exceptions.py index bbd4040f5b..d23d3b5957 100644 --- a/litestar/exceptions/base_exceptions.py +++ b/litestar/exceptions/base_exceptions.py @@ -2,7 +2,7 @@ from typing import Any -__all__ = ("MissingDependencyException", "SerializationException", "LitestarException", "LitestarWarning") +__all__ = ("LitestarException", "LitestarWarning", "MissingDependencyException", "SerializationException") class LitestarException(Exception): diff --git a/litestar/exceptions/http_exceptions.py b/litestar/exceptions/http_exceptions.py index bd384c363b..f3a34174eb 100644 --- a/litestar/exceptions/http_exceptions.py +++ b/litestar/exceptions/http_exceptions.py @@ -10,6 +10,7 @@ HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND, HTTP_405_METHOD_NOT_ALLOWED, + HTTP_413_REQUEST_ENTITY_TOO_LARGE, HTTP_429_TOO_MANY_REQUESTS, HTTP_500_INTERNAL_SERVER_ERROR, HTTP_503_SERVICE_UNAVAILABLE, @@ -119,6 +120,11 @@ class MethodNotAllowedException(ClientException): status_code = HTTP_405_METHOD_NOT_ALLOWED +class RequestEntityTooLarge(ClientException): + status_code = HTTP_413_REQUEST_ENTITY_TOO_LARGE + detail = "Request Entity Too Large" + + class TooManyRequestsException(ClientException): """Request limits have been exceeded.""" diff --git a/litestar/exceptions/responses/__init__.py b/litestar/exceptions/responses/__init__.py index 9ed1df1c52..9db3c41f83 100644 --- a/litestar/exceptions/responses/__init__.py +++ b/litestar/exceptions/responses/__init__.py @@ -11,8 +11,8 @@ __all__ = ( "ExceptionResponseContent", - "create_exception_response", "create_debug_response", + "create_exception_response", ) diff --git a/litestar/handlers/__init__.py b/litestar/handlers/__init__.py index 822fe7e812..8cb1731f57 100644 --- a/litestar/handlers/__init__.py +++ b/litestar/handlers/__init__.py @@ -5,8 +5,10 @@ WebsocketListener, WebsocketListenerRouteHandler, WebsocketRouteHandler, + send_websocket_stream, websocket, websocket_listener, + websocket_stream, ) __all__ = ( @@ -14,8 +16,8 @@ "BaseRouteHandler", "HTTPRouteHandler", "WebsocketListener", - "WebsocketRouteHandler", "WebsocketListenerRouteHandler", + "WebsocketRouteHandler", "asgi", "delete", "get", @@ -24,6 +26,8 @@ "post", "put", "route", + "send_websocket_stream", "websocket", "websocket_listener", + "websocket_stream", ) diff --git a/litestar/handlers/base.py b/litestar/handlers/base.py index b6d58b398d..6f07d393a5 100644 --- a/litestar/handlers/base.py +++ b/litestar/handlers/base.py @@ -22,7 +22,7 @@ from litestar.typing import FieldDefinition from litestar.utils import ensure_async_callable, get_name, normalize_path from litestar.utils.helpers import unwrap_partial -from litestar.utils.signature import ParsedSignature, add_types_to_signature_namespace +from litestar.utils.signature import ParsedSignature, add_types_to_signature_namespace, merge_signature_namespaces if TYPE_CHECKING: from typing_extensions import Self @@ -147,7 +147,6 @@ def __init__( ) self.type_decoders = type_decoders self.type_encoders = type_encoders - self.paths = ( {normalize_path(p) for p in path} if path and isinstance(path, list) else {normalize_path(path or "/")} # type: ignore[arg-type] ) @@ -437,8 +436,9 @@ def resolve_signature_namespace(self) -> dict[str, Any]: if self._resolved_layered_parameters is Empty: ns: dict[str, Any] = {} for layer in self.ownership_layers: - ns.update(layer.signature_namespace) - + merge_signature_namespaces( + signature_namespace=ns, additional_signature_namespace=layer.signature_namespace + ) self._resolved_signature_namespace = ns return cast("dict[str, Any]", self._resolved_signature_namespace) diff --git a/litestar/handlers/http_handlers/_utils.py b/litestar/handlers/http_handlers/_utils.py index ec95145ab8..d8104b1491 100644 --- a/litestar/handlers/http_handlers/_utils.py +++ b/litestar/handlers/http_handlers/_utils.py @@ -211,10 +211,8 @@ def is_empty_response_annotation(return_annotation: FieldDefinition) -> bool: Returns: Whether the return annotation is an empty response. """ - return ( - return_annotation.is_subclass_of(NoneType) - or return_annotation.is_subclass_of(Response) - and return_annotation.has_inner_subclass_of(NoneType) + return return_annotation.is_subclass_of(NoneType) or ( + return_annotation.is_subclass_of(Response) and return_annotation.has_inner_subclass_of(NoneType) ) diff --git a/litestar/handlers/http_handlers/base.py b/litestar/handlers/http_handlers/base.py index 6ec2a05109..461c4c9bf1 100644 --- a/litestar/handlers/http_handlers/base.py +++ b/litestar/handlers/http_handlers/base.py @@ -76,12 +76,13 @@ class HTTPRouteHandler(BaseRouteHandler): __slots__ = ( "_resolved_after_response", "_resolved_before_request", - "_response_handler_mapping", "_resolved_include_in_schema", - "_resolved_response_class", "_resolved_request_class", - "_resolved_tags", + "_resolved_request_max_body_size", + "_resolved_response_class", "_resolved_security", + "_resolved_tags", + "_response_handler_mapping", "after_request", "after_response", "background", @@ -102,6 +103,7 @@ class HTTPRouteHandler(BaseRouteHandler): "operation_id", "raises", "request_class", + "request_max_body_size", "response_class", "response_cookies", "response_description", @@ -139,6 +141,7 @@ def __init__( name: str | None = None, opt: Mapping[str, Any] | None = None, request_class: type[Request] | None = None, + request_max_body_size: int | None | EmptyType = Empty, response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, @@ -204,6 +207,8 @@ def __init__( :class:`ASGI Scope <.types.Scope>`. request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's default request. + request_max_body_size: Maximum allowed size of the request body in bytes. If this size is exceeded, + a '413 - Request Entity Too Large' error response is returned. response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's default response. response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. @@ -214,8 +219,9 @@ def __init__( return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing outbound response data. signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. - status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and - ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. + status_code: An http status code for the response. Defaults to ``200`` for ``GET``, ``PUT`` and ``PATCH``, + ``201`` for ``POST`` and ``204`` for ``DELETE``. For mixed method requests it will check for ``POST`` and ``DELETE`` first + then defaults to ``200``. sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the main event loop. This has an effect only for sync handler functions. See using sync handler functions. content_encoding: A string describing the encoding of the content, e.g. ``"base64"``. @@ -271,6 +277,7 @@ def __init__( self.response_class = response_class self.response_cookies: Sequence[Cookie] | None = narrow_response_cookies(response_cookies) self.response_headers: Sequence[ResponseHeader] | None = narrow_response_headers(response_headers) + self.request_max_body_size = request_max_body_size self.sync_to_thread = sync_to_thread # OpenAPI related attributes @@ -296,6 +303,7 @@ def __init__( self._resolved_request_class: type[Request] | EmptyType = Empty self._resolved_security: list[SecurityRequirement] | EmptyType = Empty self._resolved_tags: list[str] | EmptyType = Empty + self._resolved_request_max_body_size: int | EmptyType | None = Empty def __call__(self, fn: AnyCallable) -> HTTPRouteHandler: """Replace a function with itself.""" @@ -472,6 +480,25 @@ def resolve_tags(self) -> list[str]: return self._resolved_tags + def resolve_request_max_body_size(self) -> int | None: + if (resolved_limits := self._resolved_request_max_body_size) is not Empty: + return resolved_limits + + max_body_size = self._resolved_request_max_body_size = next( # pyright: ignore + ( + max_body_size + for layer in reversed(self.ownership_layers) + if (max_body_size := layer.request_max_body_size) is not Empty + ), + Empty, + ) + if max_body_size is Empty: + raise ImproperlyConfiguredException( + "'request_max_body_size' set to 'Empty' on all layers. To omit a limit, " + "set 'request_max_body_size=None'" + ) + return max_body_size + def get_response_handler(self, is_response_type_data: bool = False) -> Callable[[Any], Awaitable[ASGIApp]]: """Resolve the response_handler function for the route handler. @@ -598,5 +625,12 @@ def _validate_handler_function(self) -> None: if "data" in self.parsed_fn_signature.parameters and "GET" in self.http_methods: raise ImproperlyConfiguredException("'data' kwarg is unsupported for 'GET' request handlers") + if (body_param := self.parsed_fn_signature.parameters.get("body")) and not body_param.is_subclass_of(bytes): + raise ImproperlyConfiguredException( + f"Invalid type annotation for 'body' parameter in route handler {self}. 'body' will always receive the " + f"raw request body as bytes but was annotated with '{body_param.raw!r}'. If you want to receive " + "processed request data, use the 'data' parameter." + ) + route = HTTPRouteHandler diff --git a/litestar/handlers/http_handlers/decorators.py b/litestar/handlers/http_handlers/decorators.py index 593a1a7d19..69df2c95a9 100644 --- a/litestar/handlers/http_handlers/decorators.py +++ b/litestar/handlers/http_handlers/decorators.py @@ -41,7 +41,7 @@ from litestar.types.callable_types import OperationIDCreator -__all__ = ("get", "head", "post", "put", "patch", "delete") +__all__ = ("delete", "get", "head", "patch", "post", "put") MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP = "semantic route handlers cannot define http_method" @@ -628,6 +628,7 @@ def __init__( name: str | None = None, opt: Mapping[str, Any] | None = None, request_class: type[Request] | None = None, + request_max_body_size: int | None | EmptyType = Empty, response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, @@ -692,6 +693,8 @@ def __init__( wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's default request. + request_max_body_size: Maximum allowed size of the request body in bytes. If this size is exceeded, + a '413 - Request Entity Too Large' error response is returned. response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's default response. response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. @@ -755,6 +758,7 @@ def __init__( path=path, raises=raises, request_class=request_class, + request_max_body_size=request_max_body_size, response_class=response_class, response_cookies=response_cookies, response_description=response_description, @@ -803,6 +807,7 @@ def __init__( name: str | None = None, opt: Mapping[str, Any] | None = None, request_class: type[Request] | None = None, + request_max_body_size: int | None | EmptyType = Empty, response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, @@ -867,6 +872,8 @@ def __init__( wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's default request. + request_max_body_size: Maximum allowed size of the request body in bytes. If this size is exceeded, + a '413 - Request Entity Too Large' error response is returned. response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's default response. response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. @@ -930,6 +937,7 @@ def __init__( path=path, raises=raises, request_class=request_class, + request_max_body_size=request_max_body_size, response_class=response_class, response_cookies=response_cookies, response_description=response_description, @@ -978,6 +986,7 @@ def __init__( name: str | None = None, opt: Mapping[str, Any] | None = None, request_class: type[Request] | None = None, + request_max_body_size: int | None | EmptyType = Empty, response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, @@ -1042,6 +1051,8 @@ def __init__( wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's default request. + request_max_body_size: Maximum allowed size of the request body in bytes. If this size is exceeded, + a '413 - Request Entity Too Large' error response is returned. response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's default response. response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. @@ -1105,6 +1116,7 @@ def __init__( path=path, raises=raises, request_class=request_class, + request_max_body_size=request_max_body_size, response_class=response_class, response_cookies=response_cookies, response_description=response_description, diff --git a/litestar/handlers/websocket_handlers/__init__.py b/litestar/handlers/websocket_handlers/__init__.py index 5b24734948..a156c6c455 100644 --- a/litestar/handlers/websocket_handlers/__init__.py +++ b/litestar/handlers/websocket_handlers/__init__.py @@ -6,11 +6,14 @@ websocket_listener, ) from litestar.handlers.websocket_handlers.route_handler import WebsocketRouteHandler, websocket +from litestar.handlers.websocket_handlers.stream import send_websocket_stream, websocket_stream __all__ = ( "WebsocketListener", "WebsocketListenerRouteHandler", "WebsocketRouteHandler", + "send_websocket_stream", "websocket", "websocket_listener", + "websocket_stream", ) diff --git a/litestar/handlers/websocket_handlers/listener.py b/litestar/handlers/websocket_handlers/listener.py index 8e702ea1aa..cfe35eb7ed 100644 --- a/litestar/handlers/websocket_handlers/listener.py +++ b/litestar/handlers/websocket_handlers/listener.py @@ -58,7 +58,7 @@ class WebsocketListenerRouteHandler(WebsocketRouteHandler): returned """ - __slots__ = { + __slots__ = { # noqa: RUF023 "connection_accept_handler": "Callback to accept a WebSocket connection. By default, calls WebSocket.accept", "on_accept": "Callback invoked after a WebSocket connection has been accepted", "on_disconnect": "Callback invoked after a WebSocket connection has been closed", @@ -335,10 +335,6 @@ class WebsocketListener(ABC): """A sequence of :class:`Guard <.types.Guard>` callables.""" middleware: list[Middleware] | None = None """A sequence of :class:`Middleware <.types.Middleware>`.""" - on_accept: AnyCallable | None = None - """Called after a :class:`WebSocket <.connection.WebSocket>` connection has been accepted. Can receive any dependencies""" - on_disconnect: AnyCallable | None = None - """Called after a :class:`WebSocket <.connection.WebSocket>` connection has been disconnected. Can receive any dependencies""" receive_mode: WebSocketMode = "text" """:class:`WebSocket <.connection.WebSocket>` mode to receive data in, either ``text`` or ``binary``.""" send_mode: WebSocketMode = "text" @@ -380,6 +376,9 @@ def __init__(self, owner: Router) -> None: self._owner = owner def to_handler(self) -> WebsocketListenerRouteHandler: + on_accept = self.on_accept if self.on_accept != WebsocketListener.on_accept else None + on_disconnect = self.on_disconnect if self.on_disconnect != WebsocketListener.on_disconnect else None + handler = WebsocketListenerRouteHandler( dependencies=self.dependencies, dto=self.dto, @@ -389,8 +388,8 @@ def to_handler(self) -> WebsocketListenerRouteHandler: send_mode=self.send_mode, receive_mode=self.receive_mode, name=self.name, - on_accept=self.on_accept, - on_disconnect=self.on_disconnect, + on_accept=on_accept, + on_disconnect=on_disconnect, opt=self.opt, path=self.path, return_dto=self.return_dto, @@ -402,6 +401,16 @@ def to_handler(self) -> WebsocketListenerRouteHandler: handler.owner = self._owner return handler + def on_accept(self, *args: Any, **kwargs: Any) -> Any: + """Called after a :class:`WebSocket <.connection.WebSocket>` connection + has been accepted. Can receive any dependencies + """ + + def on_disconnect(self, *args: Any, **kwargs: Any) -> Any: + """Called after a :class:`WebSocket <.connection.WebSocket>` connection + has been disconnected. Can receive any dependencies + """ + @abstractmethod def on_receive(self, *args: Any, **kwargs: Any) -> Any: """Called after data has been received from the WebSocket. diff --git a/litestar/handlers/websocket_handlers/route_handler.py b/litestar/handlers/websocket_handlers/route_handler.py index 3b3b8f03bf..4356d618fc 100644 --- a/litestar/handlers/websocket_handlers/route_handler.py +++ b/litestar/handlers/websocket_handlers/route_handler.py @@ -49,9 +49,9 @@ def __init__( :class:`ASGI Scope <.types.Scope>`. signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. type_encoders: A mapping of types to callables that transform them into types supported for serialization. - **kwargs: Any additional kwarg - will be set in the opt dictionary. websocket_class: A custom subclass of :class:`WebSocket <.connection.WebSocket>` to be used as route handler's default websocket class. + **kwargs: Any additional kwarg - will be set in the opt dictionary. """ self.websocket_class = websocket_class diff --git a/litestar/handlers/websocket_handlers/stream.py b/litestar/handlers/websocket_handlers/stream.py new file mode 100644 index 0000000000..b52a3eb8d2 --- /dev/null +++ b/litestar/handlers/websocket_handlers/stream.py @@ -0,0 +1,312 @@ +from __future__ import annotations + +import dataclasses +import functools +import warnings +from typing import TYPE_CHECKING, Any, AsyncGenerator, Awaitable, Callable, Mapping, cast + +import anyio +from msgspec.json import Encoder as JsonEncoder +from typing_extensions import Self + +from litestar.exceptions import ImproperlyConfiguredException, LitestarWarning, WebSocketDisconnect +from litestar.handlers.websocket_handlers.route_handler import WebsocketRouteHandler +from litestar.types import Empty +from litestar.types.builtin_types import NoneType +from litestar.typing import FieldDefinition +from litestar.utils.signature import ParsedSignature + +if TYPE_CHECKING: + from litestar import Litestar, WebSocket + from litestar.dto import AbstractDTO + from litestar.types import Dependencies, EmptyType, ExceptionHandler, Guard, Middleware, TypeEncodersMap + from litestar.types.asgi_types import WebSocketMode + + +async def send_websocket_stream( + socket: WebSocket, + stream: AsyncGenerator[Any, Any], + *, + close: bool = True, + mode: WebSocketMode = "text", + send_handler: Callable[[WebSocket, Any], Awaitable[Any]] | None = None, + listen_for_disconnect: bool = False, + warn_on_data_discard: bool = True, +) -> None: + """Stream data to the ``socket`` from an asynchronous generator. + + Example: + Sending the current time to the connected client every 0.5 seconds: + + .. code-block:: python + + async def stream_current_time() -> AsyncGenerator[str, None]: + while True: + yield str(time.time()) + await asyncio.sleep(0.5) + + + @websocket("/time") + async def time_handler(socket: WebSocket) -> None: + await socket.accept() + await send_websocket_stream( + socket, + stream_current_time(), + listen_for_disconnect=True, + ) + + + Args: + socket: The :class:`~litestar.connection.WebSocket` to send to + stream: An asynchronous generator yielding data to send + close: If ``True``, close the socket after the generator is exhausted + mode: WebSocket mode to use for sending when no ``send_handler`` is specified + send_handler: Callable to handle the send process. If ``None``, defaults to ``type(socket).send_data`` + listen_for_disconnect: If ``True``, listen for client disconnects in the background. If a client disconnects, + stop the generator and cancel sending data. Should always be ``True`` unless disconnects are handled + elsewhere, for example by reading data from the socket concurrently. Should never be set to ``True`` when + reading data from socket concurrently, as it can lead to data loss + warn_on_data_discard: If ``True`` and ``listen_for_disconnect=True``, warn if during listening for client + disconnects, data is received from the socket + """ + if send_handler is None: + send_handler = functools.partial(type(socket).send_data, mode=mode) + + async def send_stream() -> None: + try: + # client might have disconnected elsewhere, so we stop sending + while socket.connection_state != "disconnect": + await send_handler(socket, await stream.__anext__()) + except StopAsyncIteration: + pass + + if listen_for_disconnect: + # wrap 'send_stream' and disconnect listener, so they'll cancel the other once + # one of the finishes + async def wrapped_stream() -> None: + await send_stream() + # stream exhausted, we can stop listening for a disconnect + tg.cancel_scope.cancel() + + async def disconnect_listener() -> None: + try: + # run this in a loop - we might receive other data than disconnects. + # listen_for_disconnect is explicitly not safe when consuming WS data + # in other places, so discarding that data here is fine + while True: + await socket.receive_data("text") + if warn_on_data_discard: + warnings.warn( + "received data from websocket while listening for client " + "disconnect in a websocket_stream. listen_for_disconnect " + "is not safe to use when attempting to receive data from " + "the same socket concurrently with a websocket_stream. set " + "listen_for_disconnect=False if you're attempting to " + "receive data from this socket or set " + "warn_on_data_discard=False to disable this warning", + stacklevel=2, + category=LitestarWarning, + ) + + except WebSocketDisconnect: + # client disconnected, we can stop streaming + tg.cancel_scope.cancel() + + async with anyio.create_task_group() as tg: + tg.start_soon(wrapped_stream) + tg.start_soon(disconnect_listener) + + else: + await send_stream() + + if close and socket.connection_state != "disconnect": + await socket.close() + + +def websocket_stream( + path: str | list[str] | None = None, + *, + dependencies: Dependencies | None = None, + exception_handlers: dict[int | type[Exception], ExceptionHandler] | None = None, + guards: list[Guard] | None = None, + middleware: list[Middleware] | None = None, + name: str | None = None, + opt: dict[str, Any] | None = None, + signature_namespace: Mapping[str, Any] | None = None, + websocket_class: type[WebSocket] | None = None, + mode: WebSocketMode = "text", + return_dto: type[AbstractDTO] | None | EmptyType = Empty, + type_encoders: TypeEncodersMap | None = None, + listen_for_disconnect: bool = True, + warn_on_data_discard: bool = True, + **kwargs: Any, +) -> Callable[[Callable[..., AsyncGenerator[Any, Any]]], WebsocketRouteHandler]: + """Create a WebSocket handler that accepts a connection and sends data to it from an + async generator. + + Example: + Sending the current time to the connected client every 0.5 seconds: + + .. code-block:: python + + @websocket_stream("/time") + async def send_time() -> AsyncGenerator[str, None]: + while True: + yield str(time.time()) + await asyncio.sleep(0.5) + + Args: + path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults + to ``/`` + dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. + exception_handlers: A mapping of status codes and/or exception types to handler functions. + guards: A sequence of :class:`Guard <.types.Guard>` callables. + middleware: A sequence of :class:`Middleware <.types.Middleware>`. + name: A string identifying the route handler. + opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or + wherever you have access to :class:`Request <.connection.Request>` or + :class:`ASGI Scope <.types.Scope>`. + signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. + websocket_class: A custom subclass of :class:`WebSocket <.connection.WebSocket>` to be used as route handler's + default websocket class. + mode: WebSocket mode used for sending + return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing outbound response data. + type_encoders: A mapping of types to callables that transform them into types supported for serialization. + listen_for_disconnect: If ``True``, listen for client disconnects in the background. If a client disconnects, + stop the generator and cancel sending data. Should always be ``True`` unless disconnects are handled + elsewhere, for example by reading data from the socket concurrently. Should never be set to ``True`` when + reading data from socket concurrently, as it can lead to data loss + warn_on_data_discard: If ``True`` and ``listen_for_disconnect=True``, warn if during listening for client + disconnects, data is received from the socket + **kwargs: Any additional kwarg - will be set in the opt dictionary. + """ + + def decorator(fn: Callable[..., AsyncGenerator[Any, Any]]) -> WebsocketRouteHandler: + return WebSocketStreamHandler( + path=path, + dependencies=dependencies, + exception_handlers=exception_handlers, + guard=guards, + middleware=middleware, + name=name, + opt=opt, + signature_namespace=signature_namespace, + websocket_class=websocket_class, + return_dto=return_dto, + type_encoders=type_encoders, + **kwargs, + )( + _WebSocketStreamOptions( + generator_fn=fn, + send_mode=mode, + listen_for_disconnect=listen_for_disconnect, + warn_on_data_discard=warn_on_data_discard, + ) + ) + + return decorator + + +class WebSocketStreamHandler(WebsocketRouteHandler): + __slots__ = ("_ws_stream_options",) + _ws_stream_options: _WebSocketStreamOptions + + def __call__(self, fn: _WebSocketStreamOptions) -> Self: # type: ignore[override] + self._ws_stream_options = fn + self._fn = self._ws_stream_options.generator_fn # type: ignore[assignment] + return self + + def on_registration(self, app: Litestar) -> None: + parsed_handler_signature = parsed_stream_fn_signature = ParsedSignature.from_fn( + self.fn, self.resolve_signature_namespace() + ) + + if not parsed_stream_fn_signature.return_type.is_subclass_of(AsyncGenerator): + raise ImproperlyConfiguredException( + f"Route handler {self}: 'websocket_stream' handlers must return an " + f"'AsyncGenerator', not {type(parsed_stream_fn_signature.return_type.raw)!r}" + ) + + # important not to use 'self._ws_stream_options.generator_fn' here; This would + # break in cases the decorator has been used inside a controller, as it would + # be a reference to the unbound method. The bound method is patched in later + # after the controller has been initialized. This is a workaround that should + # go away with v3.0's static handlers + stream_fn = cast(Callable[..., AsyncGenerator[Any, Any]], self.fn) + + # construct a fake signature for the kwargs modelling, using the generator + # function passed to the handler as a base, to include all the dependencies, + # params, injection kwargs, etc. + 'socket', so DI works properly, but the + # signature looks to kwargs/signature modelling like a plain '@websocket' + # handler that returns 'None' + parsed_handler_signature = dataclasses.replace( + parsed_handler_signature, return_type=FieldDefinition.from_annotation(NoneType) + ) + receives_socket_parameter = "socket" in parsed_stream_fn_signature.parameters + + if not receives_socket_parameter: + parsed_handler_signature = dataclasses.replace( + parsed_handler_signature, + parameters={ + **parsed_handler_signature.parameters, + "socket": FieldDefinition.from_annotation("WebSocket", name="socket"), + }, + ) + + self._parsed_fn_signature = parsed_handler_signature + self._parsed_return_field = parsed_stream_fn_signature.return_type.inner_types[0] + + json_encoder = JsonEncoder(enc_hook=self.default_serializer) + return_dto = self.resolve_return_dto() + + # make sure the closure doesn't capture self._ws_stream / self + send_mode: WebSocketMode = self._ws_stream_options.send_mode # pyright: ignore + listen_for_disconnect = self._ws_stream_options.listen_for_disconnect + warn_on_data_discard = self._ws_stream_options.warn_on_data_discard + + async def send_handler(socket: WebSocket, data: Any) -> None: + if isinstance(data, (str, bytes)): + await socket.send_data(data=data, mode=send_mode) + return + + if return_dto: + encoded_data = return_dto(socket).data_to_encodable_type(data) + data = json_encoder.encode(encoded_data) + await socket.send_data(data=data, mode=send_mode) + return + + data = json_encoder.encode(data) + await socket.send_data(data=data, mode=send_mode) + + @functools.wraps(stream_fn) + async def handler_fn(*args: Any, socket: WebSocket, **kw: Any) -> None: + if receives_socket_parameter: + kw["socket"] = socket + + await send_websocket_stream( + socket=socket, + stream=stream_fn(*args, **kw), + mode=send_mode, + close=True, + listen_for_disconnect=listen_for_disconnect, + warn_on_data_discard=warn_on_data_discard, + send_handler=send_handler, + ) + + self._fn = handler_fn + + super().on_registration(app) + + +class _WebSocketStreamOptions: + def __init__( + self, + generator_fn: Callable[..., AsyncGenerator[Any, Any]], + listen_for_disconnect: bool, + warn_on_data_discard: bool, + send_mode: WebSocketMode, + ) -> None: + self.generator_fn = generator_fn + self.listen_for_disconnect = listen_for_disconnect + self.warn_on_data_discard = warn_on_data_discard + self.send_mode = send_mode diff --git a/litestar/logging/__init__.py b/litestar/logging/__init__.py index b05ceba18c..8c4ba81f56 100644 --- a/litestar/logging/__init__.py +++ b/litestar/logging/__init__.py @@ -1,3 +1,3 @@ from .config import BaseLoggingConfig, LoggingConfig, StructLoggingConfig -__all__ = ("BaseLoggingConfig", "StructLoggingConfig", "LoggingConfig") +__all__ = ("BaseLoggingConfig", "LoggingConfig", "StructLoggingConfig") diff --git a/litestar/logging/config.py b/litestar/logging/config.py index d82acac5c4..1edf357188 100644 --- a/litestar/logging/config.py +++ b/litestar/logging/config.py @@ -155,7 +155,7 @@ def _default_exception_logging_handler(logger: Logger, scope: Scope, tb: list[st class BaseLoggingConfig(ABC): """Abstract class that should be extended by logging configs.""" - __slots__ = ("log_exceptions", "traceback_line_limit", "exception_logging_handler") + __slots__ = ("exception_logging_handler", "log_exceptions", "traceback_line_limit") log_exceptions: Literal["always", "debug", "never"] """Should exceptions be logged, defaults to log exceptions when ``app.debug == True``'""" @@ -292,13 +292,16 @@ def configure(self) -> GetLogger: if self.logging_module == "picologging": try: - from picologging import config, getLogger + from picologging import ( # pyright: ignore[reportMissingImports,reportGeneralTypeIssues] + config, # pyright: ignore[reportMissingImports,reportGeneralTypeIssues] + getLogger, # pyright: ignore[reportMissingImports,reportGeneralTypeIssues] + ) except ImportError as e: raise MissingDependencyException("picologging") from e excluded_fields.add("incremental") else: - from logging import config, getLogger # type: ignore[no-redef, assignment] + from logging import config, getLogger # type: ignore[no-redef,assignment,unused-ignore] values = { _field.name: getattr(self, _field.name) diff --git a/litestar/logging/picologging.py b/litestar/logging/picologging.py index 2cd599f463..e07c074283 100644 --- a/litestar/logging/picologging.py +++ b/litestar/logging/picologging.py @@ -11,15 +11,15 @@ try: - import picologging # noqa: F401 + import picologging # noqa: F401 # pyright: ignore[reportMissingImports] except ImportError as e: raise MissingDependencyException("picologging") from e -from picologging import StreamHandler -from picologging.handlers import QueueHandler, QueueListener +from picologging import StreamHandler # pyright: ignore[reportMissingImports] +from picologging.handlers import QueueHandler, QueueListener # pyright: ignore[reportMissingImports] -class QueueListenerHandler(QueueHandler): +class QueueListenerHandler(QueueHandler): # type: ignore[misc,unused-ignore] """Configure queue listener and handler to support non-blocking logging configuration.""" def __init__(self, handlers: list[Any] | None = None) -> None: @@ -32,8 +32,8 @@ def __init__(self, handlers: list[Any] | None = None) -> None: - Requires ``picologging`` to be installed. """ super().__init__(Queue(-1)) - handlers = resolve_handlers(handlers) if handlers else [StreamHandler()] - self.listener = QueueListener(self.queue, *handlers) + handlers = resolve_handlers(handlers) if handlers else [StreamHandler()] # pyright: ignore[reportGeneralTypeIssues] + self.listener = QueueListener(self.queue, *handlers) # pyright: ignore[reportGeneralTypeIssues] self.listener.start() atexit.register(self.listener.stop) diff --git a/litestar/middleware/_internal/exceptions/middleware.py b/litestar/middleware/_internal/exceptions/middleware.py index c11c28543e..3801d995cf 100644 --- a/litestar/middleware/_internal/exceptions/middleware.py +++ b/litestar/middleware/_internal/exceptions/middleware.py @@ -81,7 +81,7 @@ def _starlette_exception_handler(request: Request[Any, Any, Any], exc: Starlette exc=HTTPException( detail=exc.detail, status_code=exc.status_code, - headers=exc.headers, + headers=exc.headers, # type: ignore[arg-type] ), ) diff --git a/litestar/middleware/_utils.py b/litestar/middleware/_utils.py index 778a5083af..565a8d886d 100644 --- a/litestar/middleware/_utils.py +++ b/litestar/middleware/_utils.py @@ -7,16 +7,23 @@ __all__ = ("build_exclude_path_pattern", "should_bypass_middleware") +from litestar.utils.warnings import warn_middleware_excluded_on_all_routes if TYPE_CHECKING: from litestar.types import Method, Scope, Scopes -def build_exclude_path_pattern(*, exclude: str | list[str] | None = None) -> Pattern | None: +def build_exclude_path_pattern( + *, + exclude: str | list[str] | None = None, + middleware_cls: type | None = None, +) -> Pattern | None: """Build single path pattern from list of patterns to opt-out from middleware processing. Args: exclude: A pattern or a list of patterns. + middleware_cls: Middleware class this is being called from - used for creating + more informative warnings Returns: An optional pattern to match against scope["path"] to opt-out from middleware processing. @@ -25,7 +32,12 @@ def build_exclude_path_pattern(*, exclude: str | list[str] | None = None) -> Pat return None try: - return re.compile("|".join(exclude)) if isinstance(exclude, list) else re.compile(exclude) + pattern = re.compile("|".join(exclude)) if isinstance(exclude, list) else re.compile(exclude) + if pattern.match("/") and pattern.match("/982c7064-6ac7-44b7-9be5-07a2ff6d8a92"): + # match a UUID to ensure that it matches paths greedily and not just a literal / + warn_middleware_excluded_on_all_routes(pattern, middleware_cls=middleware_cls) + return pattern + except re.error as e: # pragma: no cover raise ImproperlyConfiguredException( "Unable to compile exclude patterns for middleware. Please make sure you passed a valid regular expression." diff --git a/litestar/middleware/authentication.py b/litestar/middleware/authentication.py index 4d1acdbc3e..bc6192c272 100644 --- a/litestar/middleware/authentication.py +++ b/litestar/middleware/authentication.py @@ -22,7 +22,7 @@ class AuthenticationResult: """Dataclass for authentication result.""" - __slots__ = ("user", "auth") + __slots__ = ("auth", "user") user: Any """The user model, this can be any value corresponding to a user of the API.""" @@ -61,7 +61,7 @@ def __init__( scopes: ASGI scopes processed by the authentication middleware. """ self.app = app - self.exclude = build_exclude_path_pattern(exclude=exclude) + self.exclude = build_exclude_path_pattern(exclude=exclude, middleware_cls=type(self)) self.exclude_http_methods = (HttpMethod.OPTIONS,) if exclude_http_methods is None else exclude_http_methods self.exclude_opt_key = exclude_from_auth_key self.scopes = scopes or {ScopeType.HTTP, ScopeType.WEBSOCKET} diff --git a/litestar/middleware/base.py b/litestar/middleware/base.py index 43106c9805..c3c5a5f873 100644 --- a/litestar/middleware/base.py +++ b/litestar/middleware/base.py @@ -46,7 +46,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: class DefineMiddleware: """Container enabling passing ``*args`` and ``**kwargs`` to Middleware class constructors and factory functions.""" - __slots__ = ("middleware", "args", "kwargs") + __slots__ = ("args", "kwargs", "middleware") def __init__(self, middleware: Callable[..., ASGIApp], *args: Any, **kwargs: Any) -> None: """Initialize ``DefineMiddleware``. @@ -109,7 +109,7 @@ def __init__( self.app = app self.scopes = scopes or self.scopes self.exclude_opt_key = exclude_opt_key or self.exclude_opt_key - self.exclude_pattern = build_exclude_path_pattern(exclude=(exclude or self.exclude)) + self.exclude_pattern = build_exclude_path_pattern(exclude=(exclude or self.exclude), middleware_cls=type(self)) @classmethod def __init_subclass__(cls, **kwargs: Any) -> None: diff --git a/litestar/middleware/compression/__init__.py b/litestar/middleware/compression/__init__.py index 0885932dd0..20f4939ddf 100644 --- a/litestar/middleware/compression/__init__.py +++ b/litestar/middleware/compression/__init__.py @@ -1,4 +1,4 @@ from litestar.middleware.compression.facade import CompressionFacade from litestar.middleware.compression.middleware import CompressionMiddleware -__all__ = ("CompressionMiddleware", "CompressionFacade") +__all__ = ("CompressionFacade", "CompressionMiddleware") diff --git a/litestar/middleware/compression/brotli_facade.py b/litestar/middleware/compression/brotli_facade.py index 3d01950a45..7673134436 100644 --- a/litestar/middleware/compression/brotli_facade.py +++ b/litestar/middleware/compression/brotli_facade.py @@ -19,7 +19,7 @@ class BrotliCompression(CompressionFacade): - __slots__ = ("compressor", "buffer", "compression_encoding") + __slots__ = ("buffer", "compression_encoding", "compressor") encoding = CompressionEncoding.BROTLI diff --git a/litestar/middleware/compression/gzip_facade.py b/litestar/middleware/compression/gzip_facade.py index b10ef73991..e57afbb80c 100644 --- a/litestar/middleware/compression/gzip_facade.py +++ b/litestar/middleware/compression/gzip_facade.py @@ -13,7 +13,7 @@ class GzipCompression(CompressionFacade): - __slots__ = ("compressor", "buffer", "compression_encoding") + __slots__ = ("buffer", "compression_encoding", "compressor") encoding = CompressionEncoding.GZIP diff --git a/litestar/middleware/csrf.py b/litestar/middleware/csrf.py index 94dd422d57..0fef4fbe42 100644 --- a/litestar/middleware/csrf.py +++ b/litestar/middleware/csrf.py @@ -32,7 +32,6 @@ __all__ = ("CSRFMiddleware",) - CSRF_SECRET_BYTES = 32 CSRF_SECRET_LENGTH = CSRF_SECRET_BYTES * 2 @@ -81,7 +80,7 @@ def __init__(self, app: ASGIApp, config: CSRFConfig) -> None: """ self.app = app self.config = config - self.exclude = build_exclude_path_pattern(exclude=config.exclude) + self.exclude = build_exclude_path_pattern(exclude=config.exclude, middleware_cls=type(self)) async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: """ASGI callable. @@ -98,6 +97,15 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: await self.app(scope, receive, send) return + if should_bypass_middleware( + scope=scope, + scopes=self.scopes, + exclude_opt_key=self.config.exclude_from_csrf_key, + exclude_path_pattern=self.exclude, + ): + await self.app(scope, receive, send) + return + request: Request[Any, Any, Any] = scope["app"].request_class(scope=scope, receive=receive) content_type, _ = request.content_type csrf_cookie = request.cookies.get(self.config.cookie_name) @@ -111,12 +119,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: existing_csrf_token = form.get("_csrf_token", None) connection_state = ScopeState.from_scope(scope) - if request.method in self.config.safe_methods or should_bypass_middleware( - scope=scope, - scopes=self.scopes, - exclude_opt_key=self.config.exclude_from_csrf_key, - exclude_path_pattern=self.exclude, - ): + if request.method in self.config.safe_methods: token = connection_state.csrf_token = csrf_cookie or generate_csrf_token(secret=self.config.secret) await self.app(scope, receive, self.create_send_wrapper(send=send, csrf_cookie=csrf_cookie, token=token)) elif ( diff --git a/litestar/middleware/logging.py b/litestar/middleware/logging.py index 1f73c7079c..c909dfed23 100644 --- a/litestar/middleware/logging.py +++ b/litestar/middleware/logging.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, Iterable +from typing import TYPE_CHECKING, Any, Collection, Iterable from litestar.constants import ( HTTP_RESPONSE_BODY, @@ -227,7 +227,7 @@ async def send_wrapper(message: Message) -> None: connection_state.log_context[HTTP_RESPONSE_BODY] = message self.log_response(scope=scope) - if not message["more_body"]: + if not message.get("more_body"): connection_state.log_context.clear() await send(message) @@ -273,7 +273,7 @@ class LoggingMiddlewareConfig: """Log message to prepend when logging a request.""" response_log_message: str = field(default="HTTP Response") """Log message to prepend when logging a response.""" - request_log_fields: Iterable[RequestExtractorField] = field( + request_log_fields: Collection[RequestExtractorField] = field( default=( "path", "method", @@ -292,7 +292,7 @@ class LoggingMiddlewareConfig: Thus, re-arranging the log-message is as simple as changing the iterable. - To turn off logging of requests, use and empty iterable. """ - response_log_fields: Iterable[ResponseExtractorField] = field( + response_log_fields: Collection[ResponseExtractorField] = field( default=( "status_code", "cookies", diff --git a/litestar/middleware/rate_limit.py b/litestar/middleware/rate_limit.py index 0c3de7f6e5..11a6653924 100644 --- a/litestar/middleware/rate_limit.py +++ b/litestar/middleware/rate_limit.py @@ -109,7 +109,7 @@ async def send_wrapper(message: Message) -> None: message.setdefault("headers", []) headers = MutableScopeHeaders(message) for key, value in self.create_response_headers(cache_object=cache_object).items(): - headers.add(key, value) + headers[key] = value await send(message) return send_wrapper @@ -198,14 +198,14 @@ def create_response_headers(self, cache_object: CacheObject) -> dict[str, str]: A dict of http headers. """ remaining_requests = str( - len(cache_object.history) - self.max_requests if len(cache_object.history) <= self.max_requests else 0 + self.max_requests - len(cache_object.history) if len(cache_object.history) <= self.max_requests else 0 ) return { self.config.rate_limit_policy_header_key: f"{self.max_requests}; w={DURATION_VALUES[self.unit]}", self.config.rate_limit_limit_header_key: str(self.max_requests), self.config.rate_limit_remaining_header_key: remaining_requests, - self.config.rate_limit_reset_header_key: str(int(time()) - cache_object.reset), + self.config.rate_limit_reset_header_key: str(cache_object.reset - int(time())), } diff --git a/litestar/middleware/response_cache.py b/litestar/middleware/response_cache.py index 62dcde6e23..a2c4bf7e71 100644 --- a/litestar/middleware/response_cache.py +++ b/litestar/middleware/response_cache.py @@ -49,7 +49,7 @@ async def wrapped_send(message: Message) -> None: elif value_or_default(connection_state.do_cache, False): messages.append(message) - if messages and message["type"] == HTTP_RESPONSE_BODY and not message["more_body"]: + if messages and message["type"] == HTTP_RESPONSE_BODY and not message.get("more_body"): key = (route_handler.cache_key_builder or self.config.key_builder)(Request(scope)) store = self.config.get_store_from_app(scope["app"]) await store.set(key, encode_msgpack(messages), expires_in=expires_in) diff --git a/litestar/openapi/__init__.py b/litestar/openapi/__init__.py index 8cc83d3697..50019b7863 100644 --- a/litestar/openapi/__init__.py +++ b/litestar/openapi/__init__.py @@ -2,4 +2,4 @@ from .controller import OpenAPIController from .datastructures import ResponseSpec -__all__ = ("OpenAPIController", "OpenAPIConfig", "ResponseSpec") +__all__ = ("OpenAPIConfig", "OpenAPIController", "ResponseSpec") diff --git a/litestar/openapi/controller.py b/litestar/openapi/controller.py index 61f1148b1d..ca5c7e56ed 100644 --- a/litestar/openapi/controller.py +++ b/litestar/openapi/controller.py @@ -36,7 +36,7 @@ class OpenAPIController(Controller): """Base styling of the html body.""" redoc_version: str = "next" """Redoc version to download from the CDN.""" - swagger_ui_version: str = "5.1.3" + swagger_ui_version: str = "5.18.2" """SwaggerUI version to download from the CDN.""" stoplight_elements_version: str = "7.7.18" """StopLight Elements version to download from the CDN.""" diff --git a/litestar/openapi/datastructures.py b/litestar/openapi/datastructures.py index 5796a48d4c..cc0981c6c3 100644 --- a/litestar/openapi/datastructures.py +++ b/litestar/openapi/datastructures.py @@ -23,7 +23,7 @@ class ResponseSpec: """Generate examples for the response content.""" description: str = field(default="Additional response") """A description of the response.""" - media_type: MediaType = field(default=MediaType.JSON) + media_type: MediaType | str = field(default=MediaType.JSON) """Response media type.""" examples: list[Example] | None = field(default=None) """A list of Example models.""" diff --git a/litestar/openapi/plugins.py b/litestar/openapi/plugins.py index a1d6c910d0..a006381737 100644 --- a/litestar/openapi/plugins.py +++ b/litestar/openapi/plugins.py @@ -12,6 +12,7 @@ from litestar.serialization import encode_json, get_serializer if TYPE_CHECKING: + from litestar.config.csrf import CSRFConfig from litestar.connection import Request from litestar.router import Router @@ -30,6 +31,11 @@ _default_style = "" +def _get_cookie_value_or_undefined(cookie_name: str) -> str: + """Javascript code as a string to get the value of a cookie by name or undefined.""" + return f"document.cookie.split('; ').find((row) => row.startsWith('{cookie_name}='))?.split('=')[1];" + + class OpenAPIRenderPlugin(ABC): """Base class for OpenAPI UI render plugins.""" @@ -221,6 +227,25 @@ def render(self, request: Request, openapi_schema: dict[str, Any]) -> bytes: A rendered html string. """ + def create_request_interceptor(csrf_config: CSRFConfig) -> str: + if csrf_config.cookie_httponly: + return "" + + return f""" + """ + head = f""" {openapi_schema["info"]["title"]} @@ -235,6 +260,7 @@ def render(self, request: Request, openapi_schema: dict[str, Any]) -> bytes: body = f""" + {create_request_interceptor(request.app.csrf_config) if request.app.csrf_config else ""} """ @@ -333,7 +359,7 @@ class ScalarRenderPlugin(OpenAPIRenderPlugin): def __init__( self, *, - version: str = "1.19.5", + version: str = "latest", js_url: str | None = None, css_url: str | None = None, path: str | Sequence[str] = "/scalar", @@ -473,7 +499,7 @@ class SwaggerRenderPlugin(OpenAPIRenderPlugin): def __init__( self, - version: str = "5.1.3", + version: str = "5.18.2", js_url: str | None = None, css_url: str | None = None, standalone_preset_js_url: str | None = None, @@ -520,6 +546,21 @@ def render(self, request: Request, openapi_schema: dict[str, Any]) -> bytes: A rendered html string. """ + def create_request_interceptor(csrf_config: CSRFConfig) -> bytes: + if csrf_config.cookie_httponly: + return b"" + + return f""" + requestInterceptor: (request) => {{ + const csrf_token = {_get_cookie_value_or_undefined(csrf_config.cookie_name)}; + + if (csrf_token !== undefined) {{ + request.headers['{csrf_config.header_name}'] = csrf_token; + }} + + return request; + }},""".encode() + head = f""" {openapi_schema["info"]["title"]} @@ -550,7 +591,9 @@ def render(self, request: Request, openapi_schema: dict[str, Any]) -> bytes: presets: [ SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset - ], + ],""", + create_request_interceptor(request.app.csrf_config) if request.app.csrf_config else b"", + b""" }) ui.initOAuth(""", encode_json(self.init_oauth), diff --git a/litestar/openapi/spec/__init__.py b/litestar/openapi/spec/__init__.py index 438c3514ba..1d7c9ed9a5 100644 --- a/litestar/openapi/spec/__init__.py +++ b/litestar/openapi/spec/__init__.py @@ -32,6 +32,7 @@ from .xml import XML __all__ = ( + "XML", "BaseSchemaObject", "Callback", "Components", @@ -64,5 +65,4 @@ "Server", "ServerVariable", "Tag", - "XML", ) diff --git a/litestar/openapi/spec/base.py b/litestar/openapi/spec/base.py index 873a26adbc..f5ae453036 100644 --- a/litestar/openapi/spec/base.py +++ b/litestar/openapi/spec/base.py @@ -42,6 +42,10 @@ def _normalize_value(value: Any) -> Any: class BaseSchemaObject: """Base class for schema spec objects""" + @property + def _exclude_fields(self) -> set[str]: + return set() + def _iter_fields(self) -> Iterator[Field[Any]]: yield from fields(self) @@ -50,8 +54,11 @@ def to_schema(self) -> dict[str, Any]: recursively. """ result: dict[str, Any] = {} + exclude = self._exclude_fields for field in self._iter_fields(): + if field.name in exclude: + continue value = _normalize_value(getattr(self, field.name, None)) if value is not None: diff --git a/litestar/openapi/spec/header.py b/litestar/openapi/spec/header.py index 32f4ad95bc..4425f52bf7 100644 --- a/litestar/openapi/spec/header.py +++ b/litestar/openapi/spec/header.py @@ -1,11 +1,10 @@ from __future__ import annotations -from dataclasses import Field, dataclass -from typing import TYPE_CHECKING, Any, Iterator, Literal - -from typing_extensions import override +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Literal from litestar.openapi.spec.base import BaseSchemaObject +from litestar.utils import warn_deprecation if TYPE_CHECKING: from litestar.openapi.spec.example import Example @@ -55,7 +54,7 @@ class OpenAPIHeader(BaseSchemaObject): deprecated: bool = False """Specifies that a parameter is deprecated and SHOULD be transitioned out of usage. Default value is ``False``.""" - allow_empty_value: bool = False + allow_empty_value: bool = None # type: ignore[assignment] """Sets the ability to pass empty-valued parameters. This is valid only for ``query`` parameters and allows sending a parameter with an empty value. Default value is ``False``. If `style `__ is used, and if behavior is ``n/a`` (cannot be @@ -87,7 +86,7 @@ class OpenAPIHeader(BaseSchemaObject): other styles, the default value is ``False``. """ - allow_reserved: bool = False + allow_reserved: bool = None # type: ignore[assignment] """Determines whether the parameter value SHOULD allow reserved characters, as defined by. :rfc:`3986` (``:/?#[]@!$&'()*+,;=``) to be included without percent-encoding. @@ -122,6 +121,29 @@ class OpenAPIHeader(BaseSchemaObject): The key is the media type and the value describes it. The map MUST only contain one entry. """ - @override - def _iter_fields(self) -> Iterator[Field[Any]]: - yield from (f for f in super()._iter_fields() if f.name not in {"name", "param_in"}) + @property + def _exclude_fields(self) -> set[str]: + return {"name", "param_in", "allow_reserved", "allow_empty_value"} + + def __post_init__(self) -> None: + if self.allow_reserved is None: + self.allow_reserved = False # type: ignore[unreachable] + else: + warn_deprecation( + "2.13.1", + "allow_reserved", + kind="parameter", + removal_in="4", + info="This property is invalid for headers and will be ignored", + ) + + if self.allow_empty_value is None: + self.allow_empty_value = False # type: ignore[unreachable] + else: + warn_deprecation( + "2.13.1", + "allow_empty_value", + kind="parameter", + removal_in="4", + info="This property is invalid for headers and will be ignored", + ) diff --git a/litestar/openapi/spec/parameter.py b/litestar/openapi/spec/parameter.py index 74a100fe4c..81305d134a 100644 --- a/litestar/openapi/spec/parameter.py +++ b/litestar/openapi/spec/parameter.py @@ -134,3 +134,12 @@ class Parameter(BaseSchemaObject): The key is the media type and the value describes it. The map MUST only contain one entry. """ + + @property + def _exclude_fields(self) -> set[str]: + exclude = set() + if self.param_in != "query": + # these are only allowed in query params + exclude.update({"allow_empty_value", "allow_reserved"}) + + return exclude diff --git a/litestar/openapi/spec/schema.py b/litestar/openapi/spec/schema.py index 4be2b7cfa0..cb998ead8e 100644 --- a/litestar/openapi/spec/schema.py +++ b/litestar/openapi/spec/schema.py @@ -1,7 +1,7 @@ from __future__ import annotations -from dataclasses import dataclass, fields, is_dataclass -from typing import TYPE_CHECKING, Any, Hashable, Mapping, Sequence +from dataclasses import dataclass, field, fields, is_dataclass +from typing import TYPE_CHECKING, Any, Hashable, Mapping, Sequence, cast from litestar.openapi.spec.base import BaseSchemaObject from litestar.utils.predicates import is_non_string_sequence @@ -55,14 +55,14 @@ class Schema(BaseSchemaObject): `JSON Schema Core `_ and follow the same specifications. """ - all_of: Sequence[Reference | Schema] | None = None + all_of: Sequence[Reference | Schema] | None = field(default=None, metadata={"alias": "allOf"}) """This keyword's value MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema. An instance validates successfully against this keyword if it validates successfully against all schemas defined by this keyword's value. """ - any_of: Sequence[Reference | Schema] | None = None + any_of: Sequence[Reference | Schema] | None = field(default=None, metadata={"alias": "anyOf"}) """This keyword's value MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema. An instance validates successfully against this keyword if it validates successfully against at least one schema @@ -70,21 +70,21 @@ class Schema(BaseSchemaObject): that annotations are collected from each subschema that validates successfully. """ - one_of: Sequence[Reference | Schema] | None = None + one_of: Sequence[Reference | Schema] | None = field(default=None, metadata={"alias": "oneOf"}) """This keyword's value MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema. An instance validates successfully against this keyword if it validates successfully against exactly one schema defined by this keyword's value. """ - schema_not: Reference | Schema | None = None + schema_not: Reference | Schema | None = field(default=None, metadata={"alias": "not"}) """This keyword's value MUST be a valid JSON Schema. An instance is valid against this keyword if it fails to validate successfully against the schema defined by this keyword. """ - schema_if: Reference | Schema | None = None + schema_if: Reference | Schema | None = field(default=None, metadata={"alias": "if"}) """This keyword's value MUST be a valid JSON Schema. This validation outcome of this keyword's subschema has no direct effect on the overall validation result. Rather, @@ -111,7 +111,7 @@ class Schema(BaseSchemaObject): purposes, in such cases. """ - schema_else: Reference | Schema | None = None + schema_else: Reference | Schema | None = field(default=None, metadata={"alias": "else"}) """This keyword's value MUST be a valid JSON Schema. When "if" is present, and the instance fails to validate against its subschema, then validation succeeds against @@ -122,7 +122,9 @@ class Schema(BaseSchemaObject): purposes, in such cases. """ - dependent_schemas: dict[str, Reference | Schema] | None = None + dependent_schemas: dict[str, Reference | Schema] | None = field( + default=None, metadata={"alias": "dependentSchemas"} + ) """This keyword specifies subschemas that are evaluated if the instance is an object and contains a certain property. @@ -134,7 +136,7 @@ class Schema(BaseSchemaObject): Omitting this keyword has the same behavior as an empty object. """ - prefix_items: Sequence[Reference | Schema] | None = None + prefix_items: Sequence[Reference | Schema] | None = field(default=None, metadata={"alias": "prefixItems"}) """The value of "prefixItems" MUST be a non-empty array of valid JSON Schemas. Validation succeeds if each element of the instance validates against the schema at the same position, if any. @@ -194,7 +196,9 @@ class Schema(BaseSchemaObject): Omitting this keyword has the same assertion behavior as an empty object. """ - pattern_properties: dict[str, Reference | Schema] | None = None + pattern_properties: dict[str, Reference | Schema] | None = field( + default=None, metadata={"alias": "patternProperties"} + ) """The value of "patternProperties" MUST be an object. Each property name of this object SHOULD be a valid regular expression, according to the ECMA-262 regular expression dialect. Each property value of this object MUST be a valid JSON Schema. @@ -208,7 +212,9 @@ class Schema(BaseSchemaObject): Omitting this keyword has the same assertion behavior as an empty object. """ - additional_properties: Reference | Schema | bool | None = None + additional_properties: Reference | Schema | bool | None = field( + default=None, metadata={"alias": "additionalProperties"} + ) """The value of "additionalProperties" MUST be a valid JSON Schema. The behavior of this keyword depends on the presence and annotation results of "properties" and "patternProperties" @@ -227,7 +233,7 @@ class Schema(BaseSchemaObject): property set. Implementations that do not support annotation collection MUST do so. """ - property_names: Reference | Schema | None = None + property_names: Reference | Schema | None = field(default=None, metadata={"alias": "propertyNames"}) """The value of "propertyNames" MUST be a valid JSON Schema. If the instance is an object, this keyword validates if every property name in the instance validates against the @@ -236,7 +242,7 @@ class Schema(BaseSchemaObject): Omitting this keyword has the same behavior as an empty schema. """ - unevaluated_items: Reference | Schema | None = None + unevaluated_items: Reference | Schema | None = field(default=None, metadata={"alias": "unevaluatedItems"}) """The value of "unevaluatedItems" MUST be a valid JSON Schema. The behavior of this keyword depends on the annotation results of adjacent keywords that apply to the instance @@ -261,7 +267,7 @@ class Schema(BaseSchemaObject): Omitting this keyword has the same assertion behavior as an empty schema. """ - unevaluated_properties: Reference | Schema | None = None + unevaluated_properties: Reference | Schema | None = field(default=None, metadata={"alias": "unevaluatedProperties"}) """The value of "unevaluatedProperties" MUST be a valid JSON Schema. The behavior of this keyword depends on the annotation results of adjacent keywords that apply to the instance @@ -319,7 +325,7 @@ class Schema(BaseSchemaObject): An instance validates successfully against this keyword if its value is equal to the value of the keyword. """ - multiple_of: float | None = None + multiple_of: float | None = field(default=None, metadata={"alias": "multipleOf"}) """The value of "multipleOf" MUST be a number, strictly greater than 0. A numeric instance is only valid if division by this keyword's value results in an integer. @@ -346,14 +352,14 @@ class Schema(BaseSchemaObject): "minimum". """ - exclusive_minimum: float | None = None + exclusive_minimum: float | None = field(default=None, metadata={"alias": "exclusiveMinimum"}) """The value of "exclusiveMinimum" MUST be a number, representing an exclusive lower limit for a numeric instance. If the instance is a number, then the instance is valid only if it has a value strictly greater than (not equal to) "exclusiveMinimum". """ - max_length: int | None = None + max_length: int | None = field(default=None, metadata={"alias": "maxLength"}) """The value of this keyword MUST be a non-negative integer. A string instance is valid against this keyword if its length is less than, or equal to, the value of this keyword. @@ -361,7 +367,7 @@ class Schema(BaseSchemaObject): The length of a string instance is defined as the number of its characters as defined by :rfc:`8259`. """ - min_length: int | None = None + min_length: int | None = field(default=None, metadata={"alias": "minLength"}) """The value of this keyword MUST be a non-negative integer. A string instance is valid against this keyword if its length is greater than, or equal to, the value of this @@ -380,13 +386,13 @@ class Schema(BaseSchemaObject): expressions are not implicitly anchored. """ - max_items: int | None = None + max_items: int | None = field(default=None, metadata={"alias": "maxItems"}) """The value of this keyword MUST be a non-negative integer. An array instance is valid against "maxItems" if its size is less than, or equal to, the value of this keyword. """ - min_items: int | None = None + min_items: int | None = field(default=None, metadata={"alias": "minItems"}) """The value of this keyword MUST be a non-negative integer. An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. @@ -394,7 +400,7 @@ class Schema(BaseSchemaObject): Omitting this keyword has the same behavior as a value of 0. """ - unique_items: bool | None = None + unique_items: bool | None = field(default=None, metadata={"alias": "uniqueItems"}) """The value of this keyword MUST be a boolean. If this keyword has boolean value false, the instance validates successfully. If it has boolean value true, the @@ -403,7 +409,7 @@ class Schema(BaseSchemaObject): Omitting this keyword has the same behavior as a value of false. """ - max_contains: int | None = None + max_contains: int | None = field(default=None, metadata={"alias": "maxContains"}) """The value of this keyword MUST be a non-negative integer. If "contains" is not present within the same schema object, then this keyword has no effect. @@ -414,7 +420,7 @@ class Schema(BaseSchemaObject): boolean "true" and the instance array length is less than r equal to the "maxContains" value. """ - min_contains: int | None = None + min_contains: int | None = field(default=None, metadata={"alias": "minContains"}) """The value of this keyword MUST be a non-negative integer. If "contains" is not present within the same schema object, then this keyword has no effect. @@ -430,14 +436,14 @@ class Schema(BaseSchemaObject): Omitting this keyword has the same behavior as a value of 1. """ - max_properties: int | None = None + max_properties: int | None = field(default=None, metadata={"alias": "maxProperties"}) """The value of this keyword MUST be a non-negative integer. An object instance is valid against "maxProperties" if its number of properties is less than, or equal to, the value of this keyword. """ - min_properties: int | None = None + min_properties: int | None = field(default=None, metadata={"alias": "minProperties"}) """The value of this keyword MUST be a non-negative integer. An object instance is valid against "minProperties" if its number of properties is greater than, or equal to, the @@ -454,7 +460,7 @@ class Schema(BaseSchemaObject): Omitting this keyword has the same behavior as an empty array. """ - dependent_required: dict[str, Sequence[str]] | None = None + dependent_required: dict[str, Sequence[str]] | None = field(default=None, metadata={"alias": "dependentRequired"}) """The value of this keyword MUST be an object. Properties in this object, f any, MUST be arrays. Elements in each array, if any, MUST be strings, and MUST be unique. @@ -490,7 +496,7 @@ class Schema(BaseSchemaObject): only applying to integers. ]] """ - content_encoding: str | None = None + content_encoding: str | None = field(default=None, metadata={"alias": "contentEncoding"}) """If the instance value is a string, this property defines that the string SHOULD be interpreted as binary data and decoded using the encoding named by this property. @@ -504,14 +510,14 @@ class Schema(BaseSchemaObject): encoding, meaning that no transformation was needed in order to represent the content in a UTF-8 string. """ - content_media_type: str | None = None + content_media_type: str | None = field(default=None, metadata={"alias": "contentMediaType"}) """If the instance is a string, this property indicates the media type of the contents of the string. If "contentEncoding" is present, this property describes the decoded string. The value of this property MUST be a string, which MUST be a media type, as defined by :rfc:`2046` """ - content_schema: Reference | Schema | None = None + content_schema: Reference | Schema | None = field(default=None, metadata={"alias": "contentSchema"}) """If the instance is a string, and if "contentMediaType" is present, this property contains a schema which describes the structure of the string. @@ -565,7 +571,7 @@ class Schema(BaseSchemaObject): Omitting this keyword has the same behavior as a value of false. """ - read_only: bool | None = None + read_only: bool | None = field(default=None, metadata={"alias": "readOnly"}) """The value of "readOnly" MUST be a boolean. When multiple occurrences of this keyword are applicable to a single sub-instance, the resulting behavior SHOULD be as for a true value if any occurrence specifies a true value, and SHOULD be as for a false value otherwise. @@ -586,7 +592,7 @@ class Schema(BaseSchemaObject): Omitting these keywords has the same behavior as values of false. """ - write_only: bool | None = None + write_only: bool | None = field(default=None, metadata={"alias": "writeOnly"}) """The value of "writeOnly" MUST be a boolean. When multiple occurrences of this keyword are applicable to a single sub-instance, the resulting behavior SHOULD be as for a true value if any occurrence specifies a true value, and SHOULD be as for a false value otherwise. @@ -626,7 +632,7 @@ class Schema(BaseSchemaObject): It has no effect on root schemas. Adds additional metadata to describe the XML representation of this property. """ - external_docs: ExternalDocumentation | None = None + external_docs: ExternalDocumentation | None = field(default=None, metadata={"alias": "externalDocs"}) """Additional external documentation for this schema.""" example: Any | None = None @@ -641,6 +647,17 @@ class Schema(BaseSchemaObject): def __hash__(self) -> int: return _recursive_hash(self) + @classmethod + def field_aliases(cls) -> dict[str, str]: + if hasattr(cls, "_field_aliases"): + return cast("dict[str, str]", cls._field_aliases) + retval = {} + for field_def in fields(cls): + if field_def.metadata is not None and (field_alias := field_def.metadata.get("alias")): + retval[field_alias] = field_def.name + cls._field_aliases = retval # type: ignore[attr-defined] + return retval + @dataclass class SchemaDataContainer(Schema): diff --git a/litestar/pagination.py b/litestar/pagination.py index 6e81371958..9bfe5ee5ca 100644 --- a/litestar/pagination.py +++ b/litestar/pagination.py @@ -27,7 +27,7 @@ class ClassicPagination(Generic[T]): """Container for data returned using limit/offset pagination.""" - __slots__ = ("items", "page_size", "current_page", "total_pages") + __slots__ = ("current_page", "items", "page_size", "total_pages") items: List[T] """List of data being sent as part of the response.""" @@ -70,7 +70,7 @@ class OffsetPagination(Generic[T]): # type: ignore[no-redef] class CursorPagination(Generic[C, T]): """Container for data returned using cursor pagination.""" - __slots__ = ("items", "results_per_page", "cursor", "next_cursor") + __slots__ = ("cursor", "items", "next_cursor", "results_per_page") items: List[T] """List of data being sent as part of the response.""" diff --git a/litestar/params.py b/litestar/params.py index c52389e0f4..b1ef2361ce 100644 --- a/litestar/params.py +++ b/litestar/params.py @@ -119,6 +119,11 @@ class KwargDefinition: .. versionadded:: 2.8.0 """ + schema_component_key: str | None = None + """ + Use as the key for the reference when creating a component for this type + .. versionadded:: 2.12.0 + """ @property def is_constrained(self) -> bool: @@ -195,6 +200,7 @@ def Parameter( required: bool | None = None, title: str | None = None, schema_extra: dict[str, Any] | None = None, + schema_component_key: str | None = None, ) -> Any: """Create an extended parameter kwarg definition. @@ -239,6 +245,8 @@ def Parameter( schema. .. versionadded:: 2.8.0 + schema_component_key: Use this as the key for the reference when creating a component for this type + .. versionadded:: 2.12.0 """ return ParameterKwarg( annotation=annotation, @@ -264,6 +272,7 @@ def Parameter( max_length=max_length, pattern=pattern, schema_extra=schema_extra, + schema_component_key=schema_component_key, ) @@ -308,6 +317,7 @@ def Body( pattern: str | None = None, title: str | None = None, schema_extra: dict[str, Any] | None = None, + schema_component_key: str | None = None, ) -> Any: """Create an extended request body kwarg definition. @@ -349,6 +359,8 @@ def Body( schema. .. versionadded:: 2.8.0 + schema_component_key: Use this as the key for the reference when creating a component for this type + .. versionadded:: 2.12.0 """ return BodyKwarg( media_type=media_type, @@ -371,6 +383,7 @@ def Body( pattern=pattern, multipart_form_part_limit=multipart_form_part_limit, schema_extra=schema_extra, + schema_component_key=schema_component_key, ) diff --git a/litestar/plugins/__init__.py b/litestar/plugins/__init__.py index f09310436d..39421e7d30 100644 --- a/litestar/plugins/__init__.py +++ b/litestar/plugins/__init__.py @@ -11,13 +11,13 @@ ) __all__ = ( - "SerializationPluginProtocol", - "DIPlugin", "CLIPlugin", + "CLIPluginProtocol", + "DIPlugin", "InitPluginProtocol", - "OpenAPISchemaPluginProtocol", "OpenAPISchemaPlugin", + "OpenAPISchemaPluginProtocol", "PluginProtocol", - "CLIPluginProtocol", "PluginRegistry", + "SerializationPluginProtocol", ) diff --git a/litestar/plugins/attrs.py b/litestar/plugins/attrs.py new file mode 100644 index 0000000000..831984f895 --- /dev/null +++ b/litestar/plugins/attrs.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from typing_extensions import TypeGuard + +from litestar.exceptions import MissingDependencyException +from litestar.plugins import OpenAPISchemaPluginProtocol +from litestar.types import Empty +from litestar.typing import FieldDefinition +from litestar.utils import is_optional_union + +try: + import attr + import attrs +except ImportError as e: + raise MissingDependencyException("attrs") from e + +if TYPE_CHECKING: + from litestar._openapi.schema_generation import SchemaCreator + from litestar.openapi.spec import Schema + +__all__ = ("AttrsSchemaPlugin", "is_attrs_class") + + +class AttrsSchemaPlugin(OpenAPISchemaPluginProtocol): + @staticmethod + def is_plugin_supported_type(value: Any) -> bool: + return is_attrs_class(value) or is_attrs_class(type(value)) + + def to_openapi_schema(self, field_definition: FieldDefinition, schema_creator: SchemaCreator) -> Schema: + """Given a type annotation, transform it into an OpenAPI schema class. + + Args: + field_definition: FieldDefinition instance. + schema_creator: An instance of the schema creator class + + Returns: + An :class:`OpenAPI ` instance. + """ + + type_hints = field_definition.get_type_hints(include_extras=True, resolve_generics=True) + attr_fields = attr.fields_dict(field_definition.type_) + return schema_creator.create_component_schema( + field_definition, + required=sorted( + field_name + for field_name, attribute in attr_fields.items() + if attribute.default is attrs.NOTHING and not is_optional_union(type_hints[field_name]) + ), + property_fields={ + field_name: FieldDefinition.from_kwarg(type_hints[field_name], field_name) for field_name in attr_fields + }, + ) + + +def is_attrs_class(annotation: Any) -> TypeGuard[type[attrs.AttrsInstance]]: # pyright: ignore + """Given a type annotation determine if the annotation is a class that includes an attrs attribute. + + Args: + annotation: A type. + + Returns: + A typeguard determining whether the type is an attrs class. + """ + return attrs.has(annotation) if attrs is not Empty else False # type: ignore[comparison-overlap] diff --git a/litestar/plugins/base.py b/litestar/plugins/base.py index 41b0c34bb5..c043fbd7cf 100644 --- a/litestar/plugins/base.py +++ b/litestar/plugins/base.py @@ -18,15 +18,15 @@ from litestar.typing import FieldDefinition __all__ = ( - "SerializationPluginProtocol", + "CLIPlugin", + "CLIPluginProtocol", + "DIPlugin", "InitPluginProtocol", - "OpenAPISchemaPluginProtocol", "OpenAPISchemaPlugin", + "OpenAPISchemaPluginProtocol", "PluginProtocol", - "CLIPlugin", - "CLIPluginProtocol", "PluginRegistry", - "DIPlugin", + "SerializationPluginProtocol", ) @@ -271,7 +271,7 @@ def is_constrained_field(field_definition: FieldDefinition) -> bool: class PluginRegistry: - __slots__ = { + __slots__ = { # noqa: RUF023 "init": "Plugins that implement the InitPluginProtocol", "openapi": "Plugins that implement the OpenAPISchemaPluginProtocol", "receive_route": "ReceiveRoutePlugin instances", diff --git a/litestar/plugins/core.py b/litestar/plugins/core.py deleted file mode 100644 index 010250e103..0000000000 --- a/litestar/plugins/core.py +++ /dev/null @@ -1,31 +0,0 @@ -from __future__ import annotations - -import inspect -from inspect import Signature -from typing import Any - -import msgspec - -from litestar.plugins import DIPlugin - -__all__ = ("MsgspecDIPlugin",) - - -class MsgspecDIPlugin(DIPlugin): - def has_typed_init(self, type_: Any) -> bool: - return type(type_) is type(msgspec.Struct) - - def get_typed_init(self, type_: Any) -> tuple[Signature, dict[str, Any]]: - parameters = [] - type_hints = {} - for field_info in msgspec.structs.fields(type_): - type_hints[field_info.name] = field_info.type - parameters.append( - inspect.Parameter( - name=field_info.name, - kind=inspect.Parameter.KEYWORD_ONLY, - annotation=field_info.type, - default=field_info.default, - ) - ) - return inspect.Signature(parameters), type_hints diff --git a/litestar/plugins/core/__init__.py b/litestar/plugins/core/__init__.py new file mode 100644 index 0000000000..802bcf3259 --- /dev/null +++ b/litestar/plugins/core/__init__.py @@ -0,0 +1,3 @@ +from ._msgspec import MsgspecDIPlugin + +__all__ = ("MsgspecDIPlugin",) diff --git a/litestar/plugins/core/_msgspec.py b/litestar/plugins/core/_msgspec.py new file mode 100644 index 0000000000..2aa06c7247 --- /dev/null +++ b/litestar/plugins/core/_msgspec.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import dataclasses +import inspect +from inspect import Signature +from typing import Any + +import msgspec + +from litestar.openapi.spec import Example +from litestar.params import ParameterKwarg +from litestar.plugins import DIPlugin + +__all__ = ("MsgspecDIPlugin", "kwarg_definition_from_field") + + +class MsgspecDIPlugin(DIPlugin): + def has_typed_init(self, type_: Any) -> bool: + return type(type_) is type(msgspec.Struct) + + def get_typed_init(self, type_: Any) -> tuple[Signature, dict[str, Any]]: + parameters = [] + type_hints = {} + for field_info in msgspec.structs.fields(type_): + type_hints[field_info.name] = field_info.type + parameters.append( + inspect.Parameter( + name=field_info.name, + kind=inspect.Parameter.KEYWORD_ONLY, + annotation=field_info.type, + default=field_info.default, + ) + ) + return inspect.Signature(parameters), type_hints + + +def kwarg_definition_from_field(field: msgspec.inspect.Field) -> tuple[ParameterKwarg | None, dict[str, Any]]: + extra: dict[str, Any] = {} + kwargs: dict[str, Any] = {} + if isinstance(field.type, msgspec.inspect.Metadata): + meta = field.type + field_type = meta.type + if extra_json_schema := meta.extra_json_schema: + kwargs["title"] = extra_json_schema.get("title") + kwargs["description"] = extra_json_schema.get("description") + if examples := extra_json_schema.get("examples"): + kwargs["examples"] = [Example(value=e) for e in examples] + kwargs["schema_extra"] = extra_json_schema.get("extra") + extra = meta.extra or {} + else: + field_type = field.type + + if isinstance( + field_type, + ( + msgspec.inspect.IntType, + msgspec.inspect.FloatType, + ), + ): + kwargs["gt"] = field_type.gt + kwargs["ge"] = field_type.ge + kwargs["lt"] = field_type.lt + kwargs["le"] = field_type.le + kwargs["multiple_of"] = field_type.multiple_of + elif isinstance( + field_type, + ( + msgspec.inspect.StrType, + msgspec.inspect.BytesType, + msgspec.inspect.ByteArrayType, + msgspec.inspect.MemoryViewType, + ), + ): + kwargs["min_length"] = field_type.min_length + kwargs["max_length"] = field_type.max_length + if isinstance(field_type, msgspec.inspect.StrType): + kwargs["pattern"] = field_type.pattern + + parameter_defaults = { + f.name: default for f in dataclasses.fields(ParameterKwarg) if (default := f.default) is not dataclasses.MISSING + } + kwargs_without_defaults = {k: v for k, v in kwargs.items() if v != parameter_defaults[k]} + + if kwargs_without_defaults: + return ParameterKwarg(**kwargs_without_defaults), extra + return None, extra diff --git a/litestar/plugins/htmx.py b/litestar/plugins/htmx.py new file mode 100644 index 0000000000..d85df61a12 --- /dev/null +++ b/litestar/plugins/htmx.py @@ -0,0 +1,53 @@ +# ruff: noqa: TC004, F401 +# pyright: reportUnusedImport=false +from __future__ import annotations + +from litestar_htmx import ( + ClientRedirect, + ClientRefresh, + EventAfterType, + HTMXConfig, + HTMXDetails, + HTMXHeaders, + HtmxHeaderType, + HTMXPlugin, + HTMXRequest, + HTMXTemplate, + HXLocation, + HXStopPolling, + LocationType, + PushUrl, + PushUrlType, + ReplaceUrl, + Reswap, + ReSwapMethod, + Retarget, + TriggerEvent, + TriggerEventType, + _utils, +) + +__all__ = ( + "ClientRedirect", + "ClientRefresh", + "EventAfterType", + "HTMXConfig", + "HTMXDetails", + "HTMXHeaders", + "HTMXPlugin", + "HTMXRequest", + "HTMXTemplate", + "HXLocation", + "HXStopPolling", + "HtmxHeaderType", + "LocationType", + "PushUrl", + "PushUrlType", + "ReSwapMethod", + "ReplaceUrl", + "Reswap", + "Retarget", + "TriggerEvent", + "TriggerEventType", + "_utils", +) diff --git a/litestar/plugins/problem_details.py b/litestar/plugins/problem_details.py new file mode 100644 index 0000000000..989b298e60 --- /dev/null +++ b/litestar/plugins/problem_details.py @@ -0,0 +1,154 @@ +"""Plugin for converting exceptions into a problem details response.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, Callable, Mapping, TypeVar + +from typing_extensions import TypeAlias + +from litestar.exceptions.http_exceptions import HTTPException +from litestar.plugins.base import InitPluginProtocol +from litestar.response.base import Response + +if TYPE_CHECKING: + from litestar.config.app import AppConfig + from litestar.connection.request import Request + from litestar.types.callable_types import ExceptionHandler, ExceptionT + +ProblemDetailsExceptionT = TypeVar("ProblemDetailsExceptionT", bound="ProblemDetailsException") +ProblemDetailsExceptionHandlerType: TypeAlias = "Callable[[Request, ProblemDetailsExceptionT], Response]" +ExceptionToProblemDetailMapType: TypeAlias = ( + "Mapping[type[ExceptionT], Callable[[ExceptionT], ProblemDetailsExceptionT]]" +) + + +def _problem_details_exception_handler(request: Request[Any, Any, Any], exc: ProblemDetailsException) -> Response[Any]: + return exc.to_response(request) + + +def _create_exception_handler( + exc_to_problem_details_exc_fn: Callable[[ExceptionT], ProblemDetailsException], exc_type: type[ExceptionT] +) -> ExceptionHandler[ExceptionT]: + def _exception_handler(req: Request, exc: exc_type) -> Response: # type: ignore[valid-type] + problem_details_exc = exc_to_problem_details_exc_fn(exc) + + return problem_details_exc.to_response(req) + + return _exception_handler + + +def _http_exception_to_problem_detail_exception(exc: HTTPException) -> ProblemDetailsException: + return ProblemDetailsException( + status_code=exc.status_code, + title=exc.detail, + extra=exc.extra, + headers=exc.headers, + ) + + +class ProblemDetailsException(HTTPException): + """A problem details exception as per RFC 9457.""" + + _PROBLEM_DETAILS_MEDIA_TYPE = "application/problem+json" + + def __init__( + self, + *args: Any, + detail: str = "", + status_code: int | None = None, + headers: dict[str, str] | None = None, + extra: dict[str, Any] | list[Any] | None = None, + type_: str | None = None, + title: str | None = None, + instance: str | None = None, + ) -> None: + """Initialize ``ProblemDetailsException``. + + Args: + *args: if ``detail`` kwarg not provided, first arg should be error detail. + detail: Exception details or message. Will default to args[0] if not provided. + status_code: Exception HTTP status code. + headers: Headers to set on the response. + extra: An extra mapping to attach to the exception. + type_: The type field in the problem details. + title: The title field in the problem details. + instance: The instance field in the problem details. + """ + + super().__init__( + *args, + detail=detail, + status_code=status_code, + headers=headers, + extra=extra, + ) + + self.type_ = type_ + self.title = title + self.instance = instance + + def to_response(self, request: Request[Any, Any, Any]) -> Response[dict[str, Any]]: + """Convert the problem details exception into a ``Response.``""" + + problem_details: dict[str, Any] = {"status": self.status_code} + if self.type_ is not None: + problem_details["type"] = self.type_ + if self.title is not None: + problem_details["title"] = self.title + if self.instance is not None: + problem_details["instance"] = self.instance + if self.detail is not None: + problem_details["detail"] = self.detail + + if extra := self.extra: + if isinstance(extra, Mapping): + problem_details.update(extra) + else: + problem_details["extra"] = extra + + return Response( + problem_details, + headers=self.headers, + media_type=self._PROBLEM_DETAILS_MEDIA_TYPE, + status_code=self.status_code, + ) + + +@dataclass +class ProblemDetailsConfig: + """The configuration object for ``ProblemDetailsPlugin.``""" + + exception_handler: ProblemDetailsExceptionHandlerType = _problem_details_exception_handler + """The exception handler used for ``ProblemdetailsException.``""" + + enable_for_all_http_exceptions: bool = False + """Flag indicating whether to convert all :exc:`HTTPException` into ``ProblemDetailsException.``""" + + exception_to_problem_detail_map: ExceptionToProblemDetailMapType = field(default_factory=dict) + """A mapping to convert exceptions into ``ProblemDetailsException.`` + + All exceptions provided in this will get a custom exception handler where these exceptions + are converted into ``ProblemDetailException`` before handling them. This can be used to override + the handler for ``HTTPException`` as well. + """ + + +class ProblemDetailsPlugin(InitPluginProtocol): + """A plugin to convert exceptions into problem details as per RFC 9457.""" + + def __init__(self, config: ProblemDetailsConfig | None = None): + self.config = config or ProblemDetailsConfig() + + def on_app_init(self, app_config: AppConfig) -> AppConfig: + app_config.exception_handlers[ProblemDetailsException] = self.config.exception_handler + + if self.config.enable_for_all_http_exceptions: + app_config.exception_handlers[HTTPException] = _create_exception_handler( + _http_exception_to_problem_detail_exception, HTTPException + ) + + for exc_type, conversion_fn in self.config.exception_to_problem_detail_map.items(): + app_config.exception_handlers[exc_type] = _create_exception_handler(conversion_fn, exc_type) + + return app_config diff --git a/litestar/plugins/prometheus/__init__.py b/litestar/plugins/prometheus/__init__.py new file mode 100644 index 0000000000..b5d8076169 --- /dev/null +++ b/litestar/plugins/prometheus/__init__.py @@ -0,0 +1,5 @@ +from .config import PrometheusConfig +from .controller import PrometheusController +from .middleware import PrometheusMiddleware + +__all__ = ("PrometheusConfig", "PrometheusController", "PrometheusMiddleware") diff --git a/litestar/plugins/prometheus/config.py b/litestar/plugins/prometheus/config.py new file mode 100644 index 0000000000..49828898a3 --- /dev/null +++ b/litestar/plugins/prometheus/config.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Callable, Mapping, Sequence + +from litestar.exceptions import MissingDependencyException +from litestar.middleware.base import DefineMiddleware +from litestar.plugins.prometheus.middleware import ( + PrometheusMiddleware, +) + +__all__ = ("PrometheusConfig",) + + +try: + import prometheus_client # noqa: F401 +except ImportError as e: + raise MissingDependencyException("prometheus_client", "prometheus-client", "prometheus") from e + + +if TYPE_CHECKING: + from litestar.connection.request import Request + from litestar.types import Method, Scopes + + +@dataclass +class PrometheusConfig: + """Configuration class for the PrometheusConfig middleware.""" + + app_name: str = field(default="litestar") + """The name of the application to use in the metrics.""" + prefix: str = "litestar" + """The prefix to use for the metrics.""" + labels: Mapping[str, str | Callable] | None = field(default=None) + """A mapping of labels to add to the metrics. The values can be either a string or a callable that returns a string.""" + exemplars: Callable[[Request], dict] | None = field(default=None) + """A callable that returns a list of exemplars to add to the metrics. Only supported in opementrics-text exposition format.""" + buckets: list[str | float] | None = field(default=None) + """A list of buckets to use for the histogram.""" + excluded_http_methods: Method | Sequence[Method] | None = field(default=None) + """A list of http methods to exclude from the metrics.""" + exclude_unhandled_paths: bool = field(default=False) + """Whether to ignore requests for unhandled paths from the metrics.""" + exclude: str | list[str] | None = field(default=None) + """A pattern or list of patterns for routes to exclude from the metrics.""" + exclude_opt_key: str | None = field(default=None) + """A key or list of keys in ``opt`` with which a route handler can "opt-out" of the middleware.""" + scopes: Scopes | None = field(default=None) + """ASGI scopes processed by the middleware, if None both ``http`` and ``websocket`` will be processed.""" + middleware_class: type[PrometheusMiddleware] = field(default=PrometheusMiddleware) + """The middleware class to use. + """ + group_path: bool = field(default=False) + """Whether to group paths in the metrics to avoid cardinality explosion. + """ + + @property + def middleware(self) -> DefineMiddleware: + """Create an instance of :class:`DefineMiddleware ` that wraps with. + + [PrometheusMiddleware][litestar.plugins.prometheus.PrometheusMiddleware]. or a subclass + of this middleware. + + Returns: + An instance of ``DefineMiddleware``. + """ + return DefineMiddleware(self.middleware_class, config=self) diff --git a/litestar/plugins/prometheus/controller.py b/litestar/plugins/prometheus/controller.py new file mode 100644 index 0000000000..15f5bf1d52 --- /dev/null +++ b/litestar/plugins/prometheus/controller.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import os + +from litestar import Controller, get +from litestar.exceptions import MissingDependencyException +from litestar.response import Response + +try: + import prometheus_client # noqa: F401 +except ImportError as e: + raise MissingDependencyException("prometheus_client", "prometheus-client", "prometheus") from e + +from prometheus_client import ( + CONTENT_TYPE_LATEST, + REGISTRY, + CollectorRegistry, + generate_latest, + multiprocess, +) +from prometheus_client.openmetrics.exposition import ( + CONTENT_TYPE_LATEST as OPENMETRICS_CONTENT_TYPE_LATEST, +) +from prometheus_client.openmetrics.exposition import ( + generate_latest as openmetrics_generate_latest, +) + +__all__ = [ + "PrometheusController", +] + + +class PrometheusController(Controller): + """Controller for Prometheus endpoints.""" + + path: str = "/metrics" + """The path to expose the metrics on.""" + openmetrics_format: bool = False + """Whether to expose the metrics in OpenMetrics format.""" + + @get() + async def get(self) -> Response: + registry = REGISTRY + if "prometheus_multiproc_dir" in os.environ or "PROMETHEUS_MULTIPROC_DIR" in os.environ: + registry = CollectorRegistry() + multiprocess.MultiProcessCollector(registry) # type: ignore[no-untyped-call] + + if self.openmetrics_format: + headers = {"Content-Type": OPENMETRICS_CONTENT_TYPE_LATEST} + return Response(openmetrics_generate_latest(registry), status_code=200, headers=headers) # type: ignore[no-untyped-call] + + headers = {"Content-Type": CONTENT_TYPE_LATEST} + return Response(generate_latest(registry), status_code=200, headers=headers) diff --git a/litestar/plugins/prometheus/middleware.py b/litestar/plugins/prometheus/middleware.py new file mode 100644 index 0000000000..cd987e8ac6 --- /dev/null +++ b/litestar/plugins/prometheus/middleware.py @@ -0,0 +1,184 @@ +from __future__ import annotations + +import time +from functools import wraps +from typing import TYPE_CHECKING, Any, Callable, ClassVar, cast + +from litestar.connection.request import Request +from litestar.enums import ScopeType +from litestar.exceptions import MissingDependencyException +from litestar.middleware.base import AbstractMiddleware + +__all__ = ("PrometheusMiddleware",) + +from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR + +try: + import prometheus_client # noqa: F401 +except ImportError as e: + raise MissingDependencyException("prometheus_client", "prometheus-client", "prometheus") from e + +from prometheus_client import Counter, Gauge, Histogram + +if TYPE_CHECKING: + from prometheus_client.metrics import MetricWrapperBase + + from litestar.plugins.prometheus import PrometheusConfig + from litestar.types import ASGIApp, Message, Receive, Scope, Send + + +class PrometheusMiddleware(AbstractMiddleware): + """Prometheus Middleware.""" + + _metrics: ClassVar[dict[str, MetricWrapperBase]] = {} + + def __init__(self, app: ASGIApp, config: PrometheusConfig) -> None: + """Middleware that adds Prometheus instrumentation to the application. + + Args: + app: The ``next`` ASGI app to call. + config: An instance of :class:`PrometheusConfig <.plugins.prometheus.PrometheusConfig>` + """ + super().__init__(app=app, scopes=config.scopes, exclude=config.exclude, exclude_opt_key=config.exclude_opt_key) + self._config = config + self._kwargs: dict[str, Any] = {} + + if self._config.buckets is not None: + self._kwargs["buckets"] = self._config.buckets + + def request_count(self, labels: dict[str, str | int | float]) -> Counter: + metric_name = f"{self._config.prefix}_requests_total" + + if metric_name not in PrometheusMiddleware._metrics: + PrometheusMiddleware._metrics[metric_name] = Counter( + name=metric_name, + documentation="Total requests", + labelnames=[*labels.keys()], + ) + + return cast("Counter", PrometheusMiddleware._metrics[metric_name]) + + def request_time(self, labels: dict[str, str | int | float]) -> Histogram: + metric_name = f"{self._config.prefix}_request_duration_seconds" + + if metric_name not in PrometheusMiddleware._metrics: + PrometheusMiddleware._metrics[metric_name] = Histogram( + name=metric_name, + documentation="Request duration, in seconds", + labelnames=[*labels.keys()], + **self._kwargs, + ) + return cast("Histogram", PrometheusMiddleware._metrics[metric_name]) + + def requests_in_progress(self, labels: dict[str, str | int | float]) -> Gauge: + metric_name = f"{self._config.prefix}_requests_in_progress" + + if metric_name not in PrometheusMiddleware._metrics: + PrometheusMiddleware._metrics[metric_name] = Gauge( + name=metric_name, + documentation="Total requests currently in progress", + labelnames=[*labels.keys()], + multiprocess_mode="livesum", + ) + return cast("Gauge", PrometheusMiddleware._metrics[metric_name]) + + def requests_error_count(self, labels: dict[str, str | int | float]) -> Counter: + metric_name = f"{self._config.prefix}_requests_error_total" + + if metric_name not in PrometheusMiddleware._metrics: + PrometheusMiddleware._metrics[metric_name] = Counter( + name=metric_name, + documentation="Total errors in requests", + labelnames=[*labels.keys()], + ) + return cast("Counter", PrometheusMiddleware._metrics[metric_name]) + + def _get_extra_labels(self, request: Request[Any, Any, Any]) -> dict[str, str]: + """Get extra labels provided by the config and if they are callable, parse them. + + Args: + request: The request object. + + Returns: + A dictionary of extra labels. + """ + + return {k: str(v(request) if callable(v) else v) for k, v in (self._config.labels or {}).items()} + + def _get_default_labels(self, request: Request[Any, Any, Any]) -> dict[str, str | int | float]: + """Get default label values from the request. + + Args: + request: The request object. + + Returns: + A dictionary of default labels. + """ + + path = request.url.path + if self._config.group_path: + path = request.scope["path_template"] + return { + "method": request.method if request.scope["type"] == ScopeType.HTTP else request.scope["type"], + "path": path, + "status_code": 200, + "app_name": self._config.app_name, + } + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + """ASGI callable. + + Args: + scope: The ASGI connection scope. + receive: The ASGI receive function. + send: The ASGI send function. + + Returns: + None + """ + + request = Request[Any, Any, Any](scope, receive) + + if self._config.excluded_http_methods and request.method in self._config.excluded_http_methods: + await self.app(scope, receive, send) + return + + labels = {**self._get_default_labels(request), **self._get_extra_labels(request)} + + request_span = {"start_time": time.perf_counter(), "end_time": 0, "duration": 0, "status_code": 200} + + wrapped_send = self._get_wrapped_send(send, request_span) + + self.requests_in_progress(labels).labels(*labels.values()).inc() + + try: + await self.app(scope, receive, wrapped_send) + finally: + extra: dict[str, Any] = {} + if self._config.exemplars: + extra["exemplar"] = self._config.exemplars(request) + + self.requests_in_progress(labels).labels(*labels.values()).dec() + + labels["status_code"] = request_span["status_code"] + label_values = [*labels.values()] + + if request_span["status_code"] >= HTTP_500_INTERNAL_SERVER_ERROR: + self.requests_error_count(labels).labels(*label_values).inc(**extra) + + self.request_count(labels).labels(*label_values).inc(**extra) + self.request_time(labels).labels(*label_values).observe(request_span["duration"], **extra) + + def _get_wrapped_send(self, send: Send, request_span: dict[str, float]) -> Callable: + @wraps(send) + async def wrapped_send(message: Message) -> None: + if message["type"] == "http.response.start": + request_span["status_code"] = message["status"] + + if message["type"] == "http.response.body": + end = time.perf_counter() + request_span["duration"] = end - request_span["start_time"] + request_span["end_time"] = end + await send(message) + + return wrapped_send diff --git a/litestar/plugins/pydantic/__init__.py b/litestar/plugins/pydantic/__init__.py new file mode 100644 index 0000000000..6b275edf3a --- /dev/null +++ b/litestar/plugins/pydantic/__init__.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from litestar.plugins import InitPluginProtocol +from litestar.plugins.pydantic.dto import PydanticDTO +from litestar.plugins.pydantic.plugins.di import PydanticDIPlugin +from litestar.plugins.pydantic.plugins.init import PydanticInitPlugin +from litestar.plugins.pydantic.plugins.schema import PydanticSchemaPlugin + +if TYPE_CHECKING: + from pydantic import BaseModel + from pydantic.v1 import BaseModel as BaseModelV1 + + from litestar.config.app import AppConfig + from litestar.types.serialization import PydanticV1FieldsListType, PydanticV2FieldsListType + +__all__ = ( + "PydanticDIPlugin", + "PydanticDTO", + "PydanticInitPlugin", + "PydanticPlugin", + "PydanticSchemaPlugin", +) + + +def _model_dump(model: BaseModel | BaseModelV1, *, by_alias: bool = False) -> dict[str, Any]: + return ( + model.model_dump(mode="json", by_alias=by_alias) # pyright: ignore + if hasattr(model, "model_dump") + else {k: v.decode() if isinstance(v, bytes) else v for k, v in model.dict(by_alias=by_alias).items()} + ) + + +def _model_dump_json(model: BaseModel | BaseModelV1, by_alias: bool = False) -> str: + return ( + model.model_dump_json(by_alias=by_alias) # pyright: ignore + if hasattr(model, "model_dump_json") + else model.json(by_alias=by_alias) # pyright: ignore + ) + + +class PydanticPlugin(InitPluginProtocol): + """A plugin that provides Pydantic integration.""" + + __slots__ = ( + "exclude", + "exclude_defaults", + "exclude_none", + "exclude_unset", + "include", + "prefer_alias", + "validate_strict", + ) + + def __init__( + self, + exclude: PydanticV1FieldsListType | PydanticV2FieldsListType | None = None, + exclude_defaults: bool = False, + exclude_none: bool = False, + exclude_unset: bool = False, + include: PydanticV1FieldsListType | PydanticV2FieldsListType | None = None, + prefer_alias: bool = False, + validate_strict: bool = False, + ) -> None: + """Pydantic Plugin to support serialization / validation of Pydantic types / models + + :param exclude: Fields to exclude during serialization + :param exclude_defaults: Fields to exclude during serialization when they are set to their default value + :param exclude_none: Fields to exclude during serialization when they are set to ``None`` + :param exclude_unset: Fields to exclude during serialization when they arenot set + :param include: Fields to exclude during serialization + :param prefer_alias: Use the ``by_alias=True`` flag when dumping models + :param validate_strict: Use ``strict=True`` when calling ``.model_validate`` on Pydantic 2.x models + """ + self.exclude = exclude + self.exclude_defaults = exclude_defaults + self.exclude_none = exclude_none + self.exclude_unset = exclude_unset + self.include = include + self.prefer_alias = prefer_alias + self.validate_strict = validate_strict + + def on_app_init(self, app_config: AppConfig) -> AppConfig: + """Configure application for use with Pydantic. + + Args: + app_config: The :class:`AppConfig <.config.app.AppConfig>` instance. + """ + app_config.plugins.extend( + [ + PydanticInitPlugin( + exclude=self.exclude, + exclude_defaults=self.exclude_defaults, + exclude_none=self.exclude_none, + exclude_unset=self.exclude_unset, + include=self.include, + prefer_alias=self.prefer_alias, + validate_strict=self.validate_strict, + ), + PydanticSchemaPlugin(prefer_alias=self.prefer_alias), + PydanticDIPlugin(), + ] + ) + return app_config diff --git a/litestar/plugins/pydantic/dto.py b/litestar/plugins/pydantic/dto.py new file mode 100644 index 0000000000..b75cdc5a2e --- /dev/null +++ b/litestar/plugins/pydantic/dto.py @@ -0,0 +1,182 @@ +from __future__ import annotations + +import dataclasses +from dataclasses import replace +from typing import TYPE_CHECKING, Any, Collection, Generic, TypeVar +from warnings import warn + +from typing_extensions import Annotated, TypeAlias, override + +from litestar.dto.base_dto import AbstractDTO +from litestar.dto.data_structures import DTOFieldDefinition +from litestar.dto.field import DTO_FIELD_META_KEY, extract_dto_field +from litestar.exceptions import MissingDependencyException, ValidationException +from litestar.plugins.pydantic.utils import get_model_info, is_pydantic_2_model, is_pydantic_undefined, is_pydantic_v2 +from litestar.types.empty import Empty +from litestar.typing import FieldDefinition + +if TYPE_CHECKING: + from typing import Generator + + from litestar.dto import DTOConfig + +try: + import pydantic as _ # noqa: F401 +except ImportError as e: + raise MissingDependencyException("pydantic") from e + + +try: + import pydantic as pydantic_v2 + + if not is_pydantic_v2(pydantic_v2): + raise ImportError + + from pydantic import ValidationError as ValidationErrorV2 + from pydantic import v1 as pydantic_v1 + from pydantic.v1 import ValidationError as ValidationErrorV1 + + ModelType: TypeAlias = "pydantic_v1.BaseModel | pydantic_v2.BaseModel" # pyright: ignore[reportInvalidTypeForm,reportGeneralTypeIssues] + +except ImportError: + import pydantic as pydantic_v1 # type: ignore[no-redef] + + pydantic_v2 = Empty # type: ignore[assignment] + from pydantic import ValidationError as ValidationErrorV1 # type: ignore[assignment] + + ValidationErrorV2 = ValidationErrorV1 # type: ignore[assignment, misc] + ModelType = "pydantic_v1.BaseModel" # type: ignore[misc] + + +T = TypeVar("T", bound="ModelType | Collection[ModelType]") + + +__all__ = ("PydanticDTO",) + +_down_types: dict[Any, Any] = { + pydantic_v1.EmailStr: str, + pydantic_v1.IPvAnyAddress: str, + pydantic_v1.IPvAnyInterface: str, + pydantic_v1.IPvAnyNetwork: str, +} + +if pydantic_v2 is not Empty: # type: ignore[comparison-overlap] # pragma: no cover + _down_types.update( + { + pydantic_v2.JsonValue: Any, + pydantic_v2.EmailStr: str, + pydantic_v2.IPvAnyAddress: str, + pydantic_v2.IPvAnyInterface: str, + pydantic_v2.IPvAnyNetwork: str, + } + ) + + +def convert_validation_error(validation_error: ValidationErrorV1 | ValidationErrorV2) -> list[dict[str, Any]]: # pyright: ignore[reportInvalidTypeForm,reportGeneralTypeIssues] + error_list = validation_error.errors() + for error in error_list: + if isinstance(exception := error.get("ctx", {}).get("error"), Exception): + error["ctx"]["error"] = type(exception).__name__ # pyright: ignore[reportTypedDictNotRequiredAccess] + return error_list # type: ignore[return-value] + + +def downtype_for_data_transfer(field_definition: FieldDefinition) -> FieldDefinition: + if sub := _down_types.get(field_definition.annotation): + return FieldDefinition.from_kwarg( + annotation=Annotated[sub, field_definition.metadata], name=field_definition.name + ) + return field_definition + + +class PydanticDTO(AbstractDTO[T], Generic[T]): + """Support for domain modelling with Pydantic.""" + + @override + def decode_builtins(self, value: dict[str, Any]) -> Any: + try: + return super().decode_builtins(value) + except (ValidationErrorV2, ValidationErrorV1) as ex: + raise ValidationException(extra=convert_validation_error(ex)) from ex + + @override + def decode_bytes(self, value: bytes) -> Any: + try: + return super().decode_bytes(value) + except (ValidationErrorV2, ValidationErrorV1) as ex: + raise ValidationException(extra=convert_validation_error(ex)) from ex + + @classmethod + def generate_field_definitions( + cls, + model_type: type[pydantic_v1.BaseModel | pydantic_v2.BaseModel], # pyright: ignore[reportInvalidTypeForm,reportGeneralTypeIssues] + ) -> Generator[DTOFieldDefinition, None, None]: + model_info = get_model_info(model_type) + model_fields = model_info.model_fields + model_field_definitions = model_info.field_definitions + + for field_name, field_definition in model_field_definitions.items(): + field_definition = downtype_for_data_transfer(field_definition) + dto_field = extract_dto_field(field_definition, field_definition.extra) + + default: Any = Empty + default_factory: Any = None + if field_info := model_fields.get(field_name): + # field_info might not exist, since FieldInfo isn't provided by pydantic + # for computed fields, but we still generate a FieldDefinition for them + try: + extra = field_info.extra # type: ignore[union-attr] + except AttributeError: + extra = field_info.json_schema_extra # type: ignore[union-attr] + + if extra is not None and extra.pop(DTO_FIELD_META_KEY, None): + warn( + message="Declaring 'DTOField' via Pydantic's 'Field.extra' is deprecated. " + "Use 'Annotated', e.g., 'Annotated[str, DTOField(mark='read-only')]' instead. " + "Support for 'DTOField' in 'Field.extra' will be removed in v3.", + category=DeprecationWarning, + stacklevel=2, + ) + + if not is_pydantic_undefined(field_info.default): + default = field_info.default + elif field_definition.is_optional: + default = None + else: + default = Empty + + default_factory = ( + field_info.default_factory + if field_info.default_factory and not is_pydantic_undefined(field_info.default_factory) + else None + ) + + yield replace( + DTOFieldDefinition.from_field_definition( + field_definition=field_definition, + dto_field=dto_field, + model_name=model_type.__name__, + default_factory=default_factory, + # we don't want the constraints to be set on the DTO struct as + # constraints, but as schema metadata only, so we can let pydantic + # handle all the constraining + passthrough_constraints=False, + ), + default=default, + name=field_name, + ) + + @classmethod + def detect_nested_field(cls, field_definition: FieldDefinition) -> bool: + if pydantic_v2 is not Empty: # type: ignore[comparison-overlap] + return field_definition.is_subclass_of((pydantic_v1.BaseModel, pydantic_v2.BaseModel)) + return field_definition.is_subclass_of(pydantic_v1.BaseModel) # type: ignore[unreachable] + + @classmethod + def get_config_for_model_type(cls, config: DTOConfig, model_type: type[Any]) -> DTOConfig: + if is_pydantic_2_model(model_type) and (model_config := getattr(model_type, "model_config", None)): + if model_config.get("extra") == "forbid": + config = dataclasses.replace(config, forbid_unknown_fields=True) + elif issubclass(model_type, pydantic_v1.BaseModel) and (model_config := getattr(model_type, "Config", None)): # noqa: SIM102 + if getattr(model_config, "extra", None) == "forbid": + config = dataclasses.replace(config, forbid_unknown_fields=True) + return config diff --git a/tests/unit/test_contrib/test_attrs/__init__.py b/litestar/plugins/pydantic/plugins/__init__.py similarity index 100% rename from tests/unit/test_contrib/test_attrs/__init__.py rename to litestar/plugins/pydantic/plugins/__init__.py diff --git a/litestar/plugins/pydantic/plugins/di.py b/litestar/plugins/pydantic/plugins/di.py new file mode 100644 index 0000000000..4c4cd99a5d --- /dev/null +++ b/litestar/plugins/pydantic/plugins/di.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import inspect +from inspect import Signature +from typing import Any + +from litestar.plugins import DIPlugin +from litestar.plugins.pydantic.utils import is_pydantic_model_class + + +class PydanticDIPlugin(DIPlugin): + def has_typed_init(self, type_: Any) -> bool: + return is_pydantic_model_class(type_) + + def get_typed_init(self, type_: Any) -> tuple[Signature, dict[str, Any]]: + try: + model_fields = dict(type_.model_fields) + except AttributeError: + model_fields = {k: model_field.field_info for k, model_field in type_.__fields__.items()} + + parameters = [ + inspect.Parameter(name=field_name, kind=inspect.Parameter.KEYWORD_ONLY, annotation=Any) + for field_name in model_fields + ] + type_hints = {field_name: Any for field_name in model_fields} + return Signature(parameters), type_hints diff --git a/litestar/plugins/pydantic/plugins/init.py b/litestar/plugins/pydantic/plugins/init.py new file mode 100644 index 0000000000..fb3a31d824 --- /dev/null +++ b/litestar/plugins/pydantic/plugins/init.py @@ -0,0 +1,293 @@ +from __future__ import annotations + +from contextlib import suppress +from functools import partial +from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast +from uuid import UUID + +from msgspec import ValidationError +from typing_extensions import Buffer, TypeGuard + +from litestar._signature.types import ExtendedMsgSpecValidationError +from litestar.exceptions import MissingDependencyException +from litestar.plugins import InitPluginProtocol +from litestar.plugins.pydantic.utils import is_pydantic_v2 +from litestar.utils import is_class_and_subclass + +try: + import pydantic as _ # noqa: F401 +except ImportError as e: + raise MissingDependencyException("pydantic") from e + +try: + import pydantic as pydantic_v2 + + if not is_pydantic_v2(pydantic_v2): + raise ImportError + + from pydantic import v1 as pydantic_v1 +except ImportError: + import pydantic as pydantic_v1 # type: ignore[no-redef] + + pydantic_v2 = None # type: ignore[assignment] + + +if TYPE_CHECKING: + import pydantic as pydantic_v2_mandatory + + from litestar.config.app import AppConfig + from litestar.types.serialization import PydanticV1FieldsListType, PydanticV2FieldsListType +else: + pydantic_v2_mandatory = pydantic_v2 + + +T = TypeVar("T") + + +def _dec_pydantic_v1(model_type: type[pydantic_v1.BaseModel], value: Any) -> pydantic_v1.BaseModel: + try: + return model_type.parse_obj(value) + except pydantic_v1.ValidationError as e: + raise ExtendedMsgSpecValidationError(errors=cast("list[dict[str, Any]]", e.errors())) from e + + +def _dec_pydantic_v2( + model_type: type[pydantic_v2_mandatory.BaseModel], value: Any, strict: bool +) -> pydantic_v2_mandatory.BaseModel: + try: + return model_type.model_validate(value, strict=strict) + except pydantic_v2_mandatory.ValidationError as e: + hide_input = model_type.model_config.get("hide_input_in_errors", False) + raise ExtendedMsgSpecValidationError( + errors=cast("list[dict[str, Any]]", e.errors(include_input=not hide_input)) + ) from e + + +def _dec_pydantic_uuid( + uuid_type: type[pydantic_v1.UUID1] | type[pydantic_v1.UUID3] | type[pydantic_v1.UUID4] | type[pydantic_v1.UUID5], + value: Any, +) -> ( + type[pydantic_v1.UUID1] | type[pydantic_v1.UUID3] | type[pydantic_v1.UUID4] | type[pydantic_v1.UUID5] +): # pragma: no cover + if isinstance(value, str): + value = uuid_type(value) + + elif isinstance(value, Buffer): + value = bytes(value) + try: + value = uuid_type(value.decode()) + except ValueError: + # 16 bytes in big-endian order as the bytes argument fail + # the above check + value = uuid_type(bytes=value) + elif isinstance(value, UUID): + value = uuid_type(str(value)) + + if not isinstance(value, uuid_type): + raise ValidationError(f"Invalid UUID: {value!r}") + + if value._required_version != value.version: # pyright: ignore[reportAttributeAccessIssue] + raise ValidationError(f"Invalid UUID version: {value!r}") + + return cast( + "type[pydantic_v1.UUID1] | type[pydantic_v1.UUID3] | type[pydantic_v1.UUID4] | type[pydantic_v1.UUID5]", value + ) + + +def _is_pydantic_v1_uuid(value: Any) -> bool: # pragma: no cover + return is_class_and_subclass(value, (pydantic_v1.UUID1, pydantic_v1.UUID3, pydantic_v1.UUID4, pydantic_v1.UUID5)) + + +_base_encoders: dict[Any, Callable[[Any], Any]] = { + pydantic_v1.EmailStr: str, + pydantic_v1.NameEmail: str, + pydantic_v1.ByteSize: lambda val: val.real, +} + +if pydantic_v2 is not None: # pragma: no cover + _base_encoders.update( + { + pydantic_v2.EmailStr: str, + pydantic_v2.NameEmail: str, + pydantic_v2.ByteSize: lambda val: val.real, + } + ) + + +def is_pydantic_v1_model_class(annotation: Any) -> TypeGuard[type[pydantic_v1.BaseModel]]: + return is_class_and_subclass(annotation, pydantic_v1.BaseModel) + + +def is_pydantic_v2_model_class(annotation: Any) -> TypeGuard[type[pydantic_v2.BaseModel]]: # pyright: ignore[reportInvalidTypeForm] + return is_class_and_subclass(annotation, pydantic_v2.BaseModel) # pyright: ignore[reportOptionalMemberAccess] + + +class PydanticInitPlugin(InitPluginProtocol): + __slots__ = ( + "exclude", + "exclude_defaults", + "exclude_none", + "exclude_unset", + "include", + "prefer_alias", + "validate_strict", + ) + + def __init__( + self, + exclude: PydanticV1FieldsListType | PydanticV2FieldsListType | None = None, + exclude_defaults: bool = False, + exclude_none: bool = False, + exclude_unset: bool = False, + include: PydanticV1FieldsListType | PydanticV2FieldsListType | None = None, + prefer_alias: bool = False, + validate_strict: bool = False, + ) -> None: + """Pydantic Plugin to support serialization / validation of Pydantic types / models + + :param exclude: Fields to exclude during serialization + :param exclude_defaults: Fields to exclude during serialization when they are set to their default value + :param exclude_none: Fields to exclude during serialization when they are set to ``None`` + :param exclude_unset: Fields to exclude during serialization when they arenot set + :param include: Fields to exclude during serialization + :param prefer_alias: Use the ``by_alias=True`` flag when dumping models + :param validate_strict: Use ``strict=True`` when calling ``.model_validate`` on Pydantic 2.x models + """ + self.exclude = exclude + self.exclude_defaults = exclude_defaults + self.exclude_none = exclude_none + self.exclude_unset = exclude_unset + self.include = include + self.prefer_alias = prefer_alias + self.validate_strict = validate_strict + + @classmethod + def encoders( + cls, + exclude: PydanticV1FieldsListType | PydanticV2FieldsListType | None = None, + exclude_defaults: bool = False, + exclude_none: bool = False, + exclude_unset: bool = False, + include: PydanticV1FieldsListType | PydanticV2FieldsListType | None = None, + prefer_alias: bool = False, + ) -> dict[Any, Callable[[Any], Any]]: + encoders = { + **_base_encoders, + **cls._create_pydantic_v1_encoders( + prefer_alias=prefer_alias, + exclude=exclude, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + exclude_unset=exclude_unset, + include=include, + ), + } + if pydantic_v2 is not None: # pragma: no cover + encoders.update( + cls._create_pydantic_v2_encoders( + prefer_alias=prefer_alias, + exclude=exclude, # type: ignore[arg-type] + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + exclude_unset=exclude_unset, + include=include, # type: ignore[arg-type] + ) + ) + return encoders + + @classmethod + def decoders(cls, validate_strict: bool = False) -> list[tuple[Callable[[Any], bool], Callable[[Any, Any], Any]]]: + decoders: list[tuple[Callable[[Any], bool], Callable[[Any, Any], Any]]] = [ + (is_pydantic_v1_model_class, _dec_pydantic_v1) + ] + + if pydantic_v2 is not None: # pragma: no cover + decoders.append( + ( + is_pydantic_v2_model_class, + partial(_dec_pydantic_v2, strict=validate_strict), + ) + ) + + decoders.append((_is_pydantic_v1_uuid, _dec_pydantic_uuid)) + + return decoders + + @staticmethod + def _create_pydantic_v1_encoders( + exclude: PydanticV1FieldsListType | None = None, + exclude_defaults: bool = False, + exclude_none: bool = False, + exclude_unset: bool = False, + include: PydanticV1FieldsListType | None = None, + prefer_alias: bool = False, + ) -> dict[Any, Callable[[Any], Any]]: # pragma: no cover + return { + pydantic_v1.BaseModel: lambda model: { + k: v.decode() if isinstance(v, bytes) else v + for k, v in model.dict( + by_alias=prefer_alias, + exclude=exclude, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + exclude_unset=exclude_unset, + include=include, + ).items() + }, + pydantic_v1.SecretField: str, + pydantic_v1.StrictBool: int, + pydantic_v1.color.Color: str, # pyright: ignore[reportAttributeAccessIssue] + pydantic_v1.ConstrainedBytes: lambda val: val.decode("utf-8"), + pydantic_v1.ConstrainedDate: lambda val: val.isoformat(), + pydantic_v1.AnyUrl: str, + } + + @staticmethod + def _create_pydantic_v2_encoders( + exclude: PydanticV2FieldsListType | None = None, + exclude_defaults: bool = False, + exclude_none: bool = False, + exclude_unset: bool = False, + include: PydanticV2FieldsListType | None = None, + prefer_alias: bool = False, + ) -> dict[Any, Callable[[Any], Any]]: + encoders: dict[Any, Callable[[Any], Any]] = { + pydantic_v2.BaseModel: lambda model: model.model_dump( # pyright: ignore[reportOptionalMemberAccess] + by_alias=prefer_alias, + exclude=exclude, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + exclude_unset=exclude_unset, + include=include, + mode="json", + ), + pydantic_v2.types.SecretStr: lambda val: "**********" if val else "", # pyright: ignore[reportOptionalMemberAccess] + pydantic_v2.types.SecretBytes: lambda val: "**********" if val else "", # pyright: ignore[reportOptionalMemberAccess] + pydantic_v2.AnyUrl: str, # pyright: ignore[reportOptionalMemberAccess] + } + + with suppress(ImportError): + from pydantic_extra_types import color + + encoders[color.Color] = str + + return encoders + + def on_app_init(self, app_config: AppConfig) -> AppConfig: + app_config.type_encoders = { + **self.encoders( + prefer_alias=self.prefer_alias, + exclude=self.exclude, + exclude_defaults=self.exclude_defaults, + exclude_none=self.exclude_none, + exclude_unset=self.exclude_unset, + include=self.include, + ), + **(app_config.type_encoders or {}), + } + app_config.type_decoders = [ + *self.decoders(validate_strict=self.validate_strict), + *(app_config.type_decoders or []), + ] + + return app_config diff --git a/litestar/plugins/pydantic/plugins/schema.py b/litestar/plugins/pydantic/plugins/schema.py new file mode 100644 index 0000000000..45627babe8 --- /dev/null +++ b/litestar/plugins/pydantic/plugins/schema.py @@ -0,0 +1,270 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from litestar.exceptions import MissingDependencyException +from litestar.openapi.spec import OpenAPIFormat, OpenAPIType, Schema +from litestar.plugins import OpenAPISchemaPlugin +from litestar.plugins.pydantic.utils import ( + get_model_info, + is_pydantic_constrained_field, + is_pydantic_model_class, + is_pydantic_undefined, + is_pydantic_v2, +) +from litestar.utils import is_class_and_subclass + +try: + import pydantic as _ # noqa: F401 +except ImportError as e: + raise MissingDependencyException("pydantic") from e + +try: + import pydantic as pydantic_v2 + + if not is_pydantic_v2(pydantic_v2): + raise ImportError + + from pydantic import v1 as pydantic_v1 +except ImportError: + import pydantic as pydantic_v1 # type: ignore[no-redef] + + pydantic_v2 = None # type: ignore[assignment] + +if TYPE_CHECKING: + from litestar._openapi.schema_generation.schema import SchemaCreator + from litestar.typing import FieldDefinition + +PYDANTIC_TYPE_MAP: dict[type[Any] | None | Any, Schema] = { + pydantic_v1.ByteSize: Schema(type=OpenAPIType.INTEGER), + pydantic_v1.EmailStr: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.EMAIL), + pydantic_v1.IPvAnyAddress: Schema( + one_of=[ + Schema( + type=OpenAPIType.STRING, + format=OpenAPIFormat.IPV4, + description="IPv4 address", + ), + Schema( + type=OpenAPIType.STRING, + format=OpenAPIFormat.IPV6, + description="IPv6 address", + ), + ] + ), + pydantic_v1.IPvAnyInterface: Schema( + one_of=[ + Schema( + type=OpenAPIType.STRING, + format=OpenAPIFormat.IPV4, + description="IPv4 interface", + ), + Schema( + type=OpenAPIType.STRING, + format=OpenAPIFormat.IPV6, + description="IPv6 interface", + ), + ] + ), + pydantic_v1.IPvAnyNetwork: Schema( + one_of=[ + Schema( + type=OpenAPIType.STRING, + format=OpenAPIFormat.IPV4, + description="IPv4 network", + ), + Schema( + type=OpenAPIType.STRING, + format=OpenAPIFormat.IPV6, + description="IPv6 network", + ), + ] + ), + pydantic_v1.Json: Schema(type=OpenAPIType.OBJECT, format=OpenAPIFormat.JSON_POINTER), + pydantic_v1.NameEmail: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.EMAIL, description="Name and email"), + # removed in v2 + pydantic_v1.PyObject: Schema( + type=OpenAPIType.STRING, + description="dot separated path identifying a python object, e.g. 'decimal.Decimal'", + ), + # annotated in v2 + pydantic_v1.UUID1: Schema( + type=OpenAPIType.STRING, + format=OpenAPIFormat.UUID, + description="UUID1 string", + ), + pydantic_v1.UUID3: Schema( + type=OpenAPIType.STRING, + format=OpenAPIFormat.UUID, + description="UUID3 string", + ), + pydantic_v1.UUID4: Schema( + type=OpenAPIType.STRING, + format=OpenAPIFormat.UUID, + description="UUID4 string", + ), + pydantic_v1.UUID5: Schema( + type=OpenAPIType.STRING, + format=OpenAPIFormat.UUID, + description="UUID5 string", + ), + pydantic_v1.DirectoryPath: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URI_REFERENCE), + pydantic_v1.AnyUrl: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URL), + pydantic_v1.AnyHttpUrl: Schema( + type=OpenAPIType.STRING, format=OpenAPIFormat.URL, description="must be a valid HTTP based URL" + ), + pydantic_v1.FilePath: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URI_REFERENCE), + pydantic_v1.HttpUrl: Schema( + type=OpenAPIType.STRING, + format=OpenAPIFormat.URL, + description="must be a valid HTTP based URL", + max_length=2083, + ), + pydantic_v1.RedisDsn: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URI, description="redis DSN"), + pydantic_v1.PostgresDsn: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URI, description="postgres DSN"), + pydantic_v1.SecretBytes: Schema(type=OpenAPIType.STRING), + pydantic_v1.SecretStr: Schema(type=OpenAPIType.STRING), + pydantic_v1.StrictBool: Schema(type=OpenAPIType.BOOLEAN), + pydantic_v1.StrictBytes: Schema(type=OpenAPIType.STRING), + pydantic_v1.StrictFloat: Schema(type=OpenAPIType.NUMBER), + pydantic_v1.StrictInt: Schema(type=OpenAPIType.INTEGER), + pydantic_v1.StrictStr: Schema(type=OpenAPIType.STRING), + pydantic_v1.NegativeFloat: Schema(type=OpenAPIType.NUMBER, exclusive_maximum=0.0), + pydantic_v1.NegativeInt: Schema(type=OpenAPIType.INTEGER, exclusive_maximum=0), + pydantic_v1.NonNegativeInt: Schema(type=OpenAPIType.INTEGER, minimum=0), + pydantic_v1.NonPositiveFloat: Schema(type=OpenAPIType.NUMBER, maximum=0.0), + pydantic_v1.PaymentCardNumber: Schema(type=OpenAPIType.STRING, min_length=12, max_length=19), + pydantic_v1.PositiveFloat: Schema(type=OpenAPIType.NUMBER, exclusive_minimum=0.0), + pydantic_v1.PositiveInt: Schema(type=OpenAPIType.INTEGER, exclusive_minimum=0), +} + +if pydantic_v2 is not None: # pragma: no cover + from pydantic import networks + + PYDANTIC_TYPE_MAP.update( + { + pydantic_v2.SecretStr: Schema(type=OpenAPIType.STRING), + pydantic_v2.SecretBytes: Schema(type=OpenAPIType.STRING), + pydantic_v2.ByteSize: Schema(type=OpenAPIType.INTEGER), + pydantic_v2.EmailStr: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.EMAIL), + pydantic_v2.IPvAnyAddress: Schema( + one_of=[ + Schema( + type=OpenAPIType.STRING, + format=OpenAPIFormat.IPV4, + description="IPv4 address", + ), + Schema( + type=OpenAPIType.STRING, + format=OpenAPIFormat.IPV6, + description="IPv6 address", + ), + ] + ), + pydantic_v2.IPvAnyInterface: Schema( + one_of=[ + Schema( + type=OpenAPIType.STRING, + format=OpenAPIFormat.IPV4, + description="IPv4 interface", + ), + Schema( + type=OpenAPIType.STRING, + format=OpenAPIFormat.IPV6, + description="IPv6 interface", + ), + ] + ), + pydantic_v2.IPvAnyNetwork: Schema( + one_of=[ + Schema( + type=OpenAPIType.STRING, + format=OpenAPIFormat.IPV4, + description="IPv4 network", + ), + Schema( + type=OpenAPIType.STRING, + format=OpenAPIFormat.IPV6, + description="IPv6 network", + ), + ] + ), + pydantic_v2.Json: Schema(type=OpenAPIType.OBJECT, format=OpenAPIFormat.JSON_POINTER), + pydantic_v2.NameEmail: Schema( + type=OpenAPIType.STRING, format=OpenAPIFormat.EMAIL, description="Name and email" + ), + pydantic_v2.AnyUrl: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URL), + } + ) + if int(pydantic_v2.version.version_short().split(".")[1]) >= 10: + # These were 'Annotated' type aliases before Pydantic 2.10, where they were + # changed to proper classes. Using subscripted generics type in an 'isinstance' + # check would raise a 'TypeError' on Python <3.12 + PYDANTIC_TYPE_MAP.update( + { + networks.HttpUrl: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URL), + networks.AnyHttpUrl: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URL), + } + ) + + +_supported_types = (pydantic_v1.BaseModel, *PYDANTIC_TYPE_MAP.keys()) +if pydantic_v2 is not None: # pragma: no cover + _supported_types = (pydantic_v2.BaseModel, *_supported_types) + + +class PydanticSchemaPlugin(OpenAPISchemaPlugin): + __slots__ = ("prefer_alias",) + + def __init__(self, prefer_alias: bool = False) -> None: + self.prefer_alias = prefer_alias + + @staticmethod + def is_plugin_supported_type(value: Any) -> bool: + return isinstance(value, _supported_types) or is_class_and_subclass(value, _supported_types) # type: ignore[arg-type] + + @staticmethod + def is_undefined_sentinel(value: Any) -> bool: + return is_pydantic_undefined(value) + + @staticmethod + def is_constrained_field(field_definition: FieldDefinition) -> bool: + return is_pydantic_constrained_field(field_definition.annotation) + + def to_openapi_schema(self, field_definition: FieldDefinition, schema_creator: SchemaCreator) -> Schema: + """Given a type annotation, transform it into an OpenAPI schema class. + + Args: + field_definition: FieldDefinition instance. + schema_creator: An instance of the schema creator class + + Returns: + An :class:`OpenAPI ` instance. + """ + if schema_creator.prefer_alias != self.prefer_alias: + schema_creator.prefer_alias = True + if is_pydantic_model_class(field_definition.annotation): + return self.for_pydantic_model(field_definition=field_definition, schema_creator=schema_creator) + return PYDANTIC_TYPE_MAP[field_definition.annotation] # pragma: no cover + + @classmethod + def for_pydantic_model(cls, field_definition: FieldDefinition, schema_creator: SchemaCreator) -> Schema: # pyright: ignore + """Create a schema object for a given pydantic model class. + + Args: + field_definition: FieldDefinition instance. + schema_creator: An instance of the schema creator class + + Returns: + A schema instance. + """ + + model_info = get_model_info(field_definition.annotation, prefer_alias=schema_creator.prefer_alias) + + return schema_creator.create_component_schema( + field_definition, + required=sorted(f.name for f in model_info.field_definitions.values() if f.is_required), + property_fields=model_info.field_definitions, + title=model_info.title, + examples=None if model_info.example is None else [model_info.example], + ) diff --git a/litestar/plugins/pydantic/utils.py b/litestar/plugins/pydantic/utils.py new file mode 100644 index 0000000000..e5ef66b84e --- /dev/null +++ b/litestar/plugins/pydantic/utils.py @@ -0,0 +1,504 @@ +# mypy: strict-equality=False +# pyright: reportGeneralTypeIssues=false +from __future__ import annotations + +import datetime +import re +from dataclasses import dataclass +from inspect import isclass +from typing import TYPE_CHECKING, Any, Callable, Literal, Optional + +from typing_extensions import Annotated, get_type_hints + +from litestar.openapi.spec import Example +from litestar.params import KwargDefinition, ParameterKwarg +from litestar.types import Empty +from litestar.typing import FieldDefinition +from litestar.utils import deprecated, is_class_and_subclass, is_generic, is_undefined_sentinel +from litestar.utils.typing import ( + _substitute_typevars, + get_origin_or_inner_type, + get_safe_generic_origin, + get_type_hints_with_generics_resolved, + normalize_type_annotation, +) + +# isort: off +try: + from pydantic import v1 as pydantic_v1 + import pydantic as pydantic_v2 + from pydantic.fields import PydanticUndefined as Pydantic2Undefined # type: ignore[attr-defined] + from pydantic.v1.fields import Undefined as Pydantic1Undefined + + PYDANTIC_UNDEFINED_SENTINELS = {Pydantic1Undefined, Pydantic2Undefined} +except ImportError: + try: + import pydantic as pydantic_v1 # type: ignore[no-redef] + from pydantic.fields import Undefined as Pydantic1Undefined # type: ignore[attr-defined, no-redef] + + pydantic_v2 = Empty # type: ignore[assignment] + PYDANTIC_UNDEFINED_SENTINELS = {Pydantic1Undefined} + + except ImportError: # pyright: ignore + pydantic_v1 = Empty # type: ignore[assignment] + pydantic_v2 = Empty # type: ignore[assignment] + PYDANTIC_UNDEFINED_SENTINELS = set() +# isort: on + + +if TYPE_CHECKING: + from types import ModuleType + + from typing_extensions import TypeGuard + + +def is_pydantic_model_class( + annotation: Any, +) -> TypeGuard[type[pydantic_v1.BaseModel | pydantic_v2.BaseModel]]: # pyright: ignore + """Given a type annotation determine if the annotation is a subclass of pydantic's BaseModel. + + Args: + annotation: A type. + + Returns: + A typeguard determining whether the type is :data:`BaseModel pydantic.BaseModel>`. + """ + tests: list[bool] = [] + + if pydantic_v1 is not Empty: # pragma: no cover + tests.append(is_class_and_subclass(annotation, pydantic_v1.BaseModel)) + + if pydantic_v2 is not Empty: # pragma: no cover + tests.append(is_class_and_subclass(annotation, pydantic_v2.BaseModel)) + + return any(tests) + + +def is_pydantic_model_instance( + annotation: Any, +) -> TypeGuard[pydantic_v1.BaseModel | pydantic_v2.BaseModel]: # pyright: ignore + """Given a type annotation determine if the annotation is an instance of pydantic's BaseModel. + + Args: + annotation: A type. + + Returns: + A typeguard determining whether the type is :data:`BaseModel pydantic.BaseModel>`. + """ + tests: list[bool] = [] + + if pydantic_v1 is not Empty: # pragma: no cover + tests.append(isinstance(annotation, pydantic_v1.BaseModel)) + + if pydantic_v2 is not Empty: # pragma: no cover + tests.append(isinstance(annotation, pydantic_v2.BaseModel)) + + return any(tests) + + +def is_pydantic_constrained_field(annotation: Any) -> bool: + """Check if the given annotation is a constrained pydantic type. + + Args: + annotation: A type annotation + + Returns: + True if pydantic is installed and the type is a constrained type, otherwise False. + """ + if pydantic_v1 is Empty: # pragma: no cover + return False # type: ignore[unreachable] + + return any( + is_class_and_subclass(annotation, constrained_type) # pyright: ignore + for constrained_type in ( + pydantic_v1.ConstrainedBytes, + pydantic_v1.ConstrainedDate, + pydantic_v1.ConstrainedDecimal, + pydantic_v1.ConstrainedFloat, + pydantic_v1.ConstrainedFrozenSet, + pydantic_v1.ConstrainedInt, + pydantic_v1.ConstrainedList, + pydantic_v1.ConstrainedSet, + pydantic_v1.ConstrainedStr, + ) + ) + + +def pydantic_unwrap_and_get_origin(annotation: Any) -> Any | None: + if pydantic_v2 is Empty or (pydantic_v1 is not Empty and is_class_and_subclass(annotation, pydantic_v1.BaseModel)): + return get_origin_or_inner_type(annotation) + + origin = annotation.__pydantic_generic_metadata__["origin"] + return normalize_type_annotation(origin) + + +def pydantic_get_type_hints_with_generics_resolved( + annotation: Any, + globalns: dict[str, Any] | None = None, + localns: dict[str, Any] | None = None, + include_extras: bool = False, + model_annotations: dict[str, Any] | None = None, +) -> dict[str, Any]: + if pydantic_v2 is Empty or (pydantic_v1 is not Empty and is_class_and_subclass(annotation, pydantic_v1.BaseModel)): + return get_type_hints_with_generics_resolved(annotation, type_hints=model_annotations) + + origin = pydantic_unwrap_and_get_origin(annotation) + if origin is None: + if model_annotations is None: # pragma: no cover + model_annotations = get_type_hints( + annotation, globalns=globalns, localns=localns, include_extras=include_extras + ) + typevar_map = {p: p for p in annotation.__pydantic_generic_metadata__["parameters"]} + else: + if model_annotations is None: + model_annotations = get_type_hints( + origin, globalns=globalns, localns=localns, include_extras=include_extras + ) + args = annotation.__pydantic_generic_metadata__["args"] + parameters = origin.__pydantic_generic_metadata__["parameters"] + typevar_map = dict(zip(parameters, args)) + + return {n: _substitute_typevars(type_, typevar_map) for n, type_ in model_annotations.items()} + + +@deprecated(version="2.6.2") +def pydantic_get_unwrapped_annotation_and_type_hints(annotation: Any) -> tuple[Any, dict[str, Any]]: # pragma: no cover + """Get the unwrapped annotation and the type hints after resolving generics. + + Args: + annotation: A type annotation. + + Returns: + A tuple containing the unwrapped annotation and the type hints. + """ + + if is_generic(annotation): + origin = pydantic_unwrap_and_get_origin(annotation) + return origin or annotation, pydantic_get_type_hints_with_generics_resolved(annotation, include_extras=True) + return annotation, get_type_hints(annotation, include_extras=True) + + +def is_pydantic_2_model( + obj: type[pydantic_v1.BaseModel | pydantic_v2.BaseModel], # pyright: ignore +) -> TypeGuard[pydantic_v2.BaseModel]: # pyright: ignore + return pydantic_v2 is not Empty and issubclass(obj, pydantic_v2.BaseModel) + + +def is_pydantic_undefined(value: Any) -> bool: + return any(v is value for v in PYDANTIC_UNDEFINED_SENTINELS) + + +def create_field_definitions_for_computed_fields( + model: type[pydantic_v1.BaseModel | pydantic_v2.BaseModel], # pyright: ignore + prefer_alias: bool, +) -> dict[str, FieldDefinition]: + """Create field definitions for computed fields. + + Args: + model: A pydantic model. + prefer_alias: Whether to prefer the alias or the name of the field. + + Returns: + A dictionary containing the field definitions for the computed fields. + """ + pydantic_decorators = getattr(model, "__pydantic_decorators__", None) + if pydantic_decorators is None: + return {} + + def get_name(k: str, dec: Any) -> str: + if not dec.info.alias: + return k + return dec.info.alias if prefer_alias else k # type: ignore[no-any-return] + + return { + (name := get_name(k, dec)): FieldDefinition.from_annotation( + Annotated[ + dec.info.return_type, + KwargDefinition( + title=dec.info.title, + description=dec.info.description, + read_only=True, + examples=[Example(value=v) for v in examples] if (examples := dec.info.examples) else None, + schema_extra=dec.info.json_schema_extra, + ), + ], + name=name, + ) + for k, dec in pydantic_decorators.computed_fields.items() + } + + +def is_pydantic_v2(module: ModuleType) -> bool: + """Determine if the given module is pydantic v2. + + Given a module we expect to be a pydantic version, determine if it is pydantic v2. + + Args: + module: A module. + + Returns: + True if the module is pydantic v2, otherwise False. + """ + return bool(module.__version__.startswith("2.")) + + +@dataclass(frozen=True) +class PydanticModelInfo: + pydantic_version: Literal["1", "2"] + field_definitions: dict[str, FieldDefinition] + model_fields: dict[str, pydantic_v1.fields.FieldInfo | pydantic_v2.fields.FieldInfo] # pyright: ignore[reportInvalidTypeForm,reportGeneralTypeIssues] + title: str | None = None + example: Any | None = None + is_generic: bool = False + + +_CreateFieldDefinition = Callable[..., FieldDefinition] + + +def _create_field_definition_v1( # noqa: C901 + field_annotation: Any, + *, + field_info: pydantic_v1.fields.FieldInfo, # pyright: ignore[reportInvalidTypeForm,reportGeneralTypeIssues] + **field_definition_kwargs: Any, +) -> FieldDefinition: + kwargs: dict[str, Any] = {} + examples: list[Any] = [] + if example := field_info.extra.get("example"): + examples.append(example) + if extra_examples := field_info.extra.get("examples"): + examples.extend(extra_examples) + if examples: + kwargs["examples"] = [Example(value=e) for e in examples] + if title := field_info.title: + kwargs["title"] = title + if description := field_info.description: + kwargs["description"] = description + + kwarg_definition: KwargDefinition | None = None + + if isclass(field_annotation): + if issubclass(field_annotation, pydantic_v1.ConstrainedBytes): # pyright: ignore[reportArgumentType,reportAttributeAccessIssue] + kwarg_definition = ParameterKwarg( + min_length=field_annotation.min_length, + max_length=field_annotation.max_length, + lower_case=field_annotation.to_lower, + upper_case=field_annotation.to_upper, + **kwargs, + ) + field_definition_kwargs["raw"] = field_annotation + field_annotation = bytes + elif issubclass(field_annotation, pydantic_v1.ConstrainedStr): # pyright: ignore[reportArgumentType,reportAttributeAccessIssue] + kwarg_definition = ParameterKwarg( + min_length=field_annotation.min_length, + max_length=field_annotation.max_length, + lower_case=field_annotation.to_lower, + upper_case=field_annotation.to_upper, + pattern=field_annotation.regex.pattern + if isinstance(field_annotation.regex, re.Pattern) + else field_annotation.regex, + **kwargs, + ) + field_definition_kwargs["raw"] = field_annotation + field_annotation = str + elif issubclass(field_annotation, pydantic_v1.ConstrainedDate): # pyright: ignore[reportArgumentType,reportAttributeAccessIssue] + # TODO: The typings of ParameterKwarg need fixing. Specifically, the + # gt/ge/lt/le fields need to be typed with protocols, such that they may + # accept any type that implements the respective comparisons + + kwarg_definition = ParameterKwarg( + gt=field_annotation.gt, # type: ignore[arg-type] + ge=field_annotation.ge, # type: ignore[arg-type] + lt=field_annotation.lt, # type: ignore[arg-type] + le=field_annotation.le, # type: ignore[arg-type] + **kwargs, + ) + field_definition_kwargs["raw"] = field_annotation + field_annotation = datetime.date + elif issubclass( + field_annotation, + (pydantic_v1.ConstrainedInt, pydantic_v1.ConstrainedFloat, pydantic_v1.ConstrainedDecimal), # pyright: ignore[reportArgumentType,reportAttributeAccessIssue] + ): + kwarg_definition = ParameterKwarg( + gt=field_annotation.gt, # type: ignore[arg-type] + ge=field_annotation.ge, # type: ignore[arg-type] + lt=field_annotation.lt, # type: ignore[arg-type] + le=field_annotation.le, # type: ignore[arg-type] + multiple_of=field_annotation.multiple_of, # type: ignore[arg-type] + **kwargs, + ) + field_definition_kwargs["raw"] = field_annotation + field_annotation = field_annotation.mro()[2] + elif issubclass( + field_annotation, + (pydantic_v1.ConstrainedList, pydantic_v1.ConstrainedSet, pydantic_v1.ConstrainedFrozenSet), # pyright: ignore[reportArgumentType,reportAttributeAccessIssue] + ): + kwarg_definition = ParameterKwarg( + max_items=field_annotation.max_items, min_items=field_annotation.min_items, **kwargs + ) + field_definition_kwargs["raw"] = field_annotation + # on < 3.9, these builtins are not generic + origin = get_safe_generic_origin(None, field_annotation.__origin__) + field_annotation = origin[field_annotation.item_type] + + if kwarg_definition is None and kwargs: + kwarg_definition = ParameterKwarg(**kwargs) + + if kwarg_definition: + field_definition_kwargs["raw"] = field_annotation + field_annotation = Annotated[field_annotation, kwarg_definition] + + return FieldDefinition.from_annotation( + annotation=field_annotation, + **field_definition_kwargs, + ) + + +def _create_field_definition_v2( # noqa: C901 + field_annotation: Any, + *, + field_info: pydantic_v2.fields.FieldInfo, # pyright: ignore[reportInvalidTypeForm,reportGeneralTypeIssues] + **field_definition_kwargs: Any, +) -> FieldDefinition: + kwargs: dict[str, Any] = {} + examples: list[Any] = [] + field_meta: list[Any] = [] + + if json_schema_extra := field_info.json_schema_extra: + if callable(json_schema_extra): + raise ValueError("Callables not supported for json_schema_extra") + if json_schema_example := json_schema_extra.get("example"): + del json_schema_extra["example"] + examples.append(json_schema_example) + if json_schema_examples := json_schema_extra.get("examples"): + del json_schema_extra["examples"] + examples.extend(json_schema_examples) # type: ignore[arg-type] + if field_examples := field_info.examples: + examples.extend(field_examples) + + if examples: + if not json_schema_extra: + json_schema_extra = {} + json_schema_extra["examples"] = examples + + if description := field_info.description: + kwargs["description"] = description + + if title := field_info.title: + kwargs["title"] = title + + for meta in field_info.metadata: + if isinstance(meta, pydantic_v2.types.StringConstraints): # pyright: ignore[reportAttributeAccessIssue] + kwargs["min_length"] = meta.min_length + kwargs["max_length"] = meta.max_length + kwargs["pattern"] = meta.pattern + kwargs["lower_case"] = meta.to_lower + kwargs["upper_case"] = meta.to_upper + # forward other metadata + else: + field_meta.append(meta) + + if json_schema_extra: + kwargs["schema_extra"] = json_schema_extra + + kwargs = {k: v for k, v in kwargs.items() if v is not None} + + if kwargs: + kwarg_definition = ParameterKwarg(**kwargs) + field_meta.append(kwarg_definition) + + if field_meta: + field_definition_kwargs["raw"] = field_annotation + for meta in field_meta: + field_annotation = Annotated[field_annotation, meta] + + return FieldDefinition.from_annotation( + annotation=field_annotation, + **field_definition_kwargs, + ) + + +def get_model_info( + annotation: Any, + prefer_alias: bool = False, +) -> PydanticModelInfo: + model: type[pydantic_v1.BaseModel | pydantic_v2.BaseModel] # pyright: ignore[reportInvalidTypeForm,reportGeneralTypeIssues] + + if is_generic(annotation): + is_generic_model = True + model = pydantic_unwrap_and_get_origin(annotation) or annotation + else: + is_generic_model = False + model = annotation + + if is_pydantic_2_model(model): + model_config = model.model_config + model_field_info = model.model_fields + title = model_config.get("title") + example = model_config.get("example") + is_v2_model = True + else: + model_config = model.__config__ # type: ignore[assignment, union-attr] + model_field_info = model.__fields__ # type: ignore[assignment] + title = getattr(model_config, "title", None) + example = getattr(model_config, "example", None) + is_v2_model = False + + model_fields: dict[str, pydantic_v1.fields.FieldInfo | pydantic_v2.fields.FieldInfo] = { # pyright: ignore[reportInvalidTypeForm,reportGeneralTypeIssues] + k: getattr(f, "field_info", f) for k, f in model_field_info.items() + } + + if is_v2_model: + # extract the annotations from the FieldInfo. This allows us to skip fields + # which have been marked as private + # if there's a default factory, we wrap the field in 'Optional', to signal + # that it is not required + model_annotations = { + k: Optional[field_info.annotation] if field_info.default_factory else field_info.annotation # type: ignore[union-attr] + for k, field_info in model_fields.items() + } + + else: + # pydantic v1 requires some workarounds here + model_annotations = { + k: f.outer_type_ if f.required or f.default else Optional[f.outer_type_] + for k, f in model.__fields__.items() # type: ignore[union-attr] + } + + if is_generic_model: + # if the model is generic, resolve the type variables. We pass in the + # already extracted annotations, to keep the logic of respecting private + # fields consistent with the above + model_annotations = pydantic_get_type_hints_with_generics_resolved( + annotation, model_annotations=model_annotations, include_extras=True + ) + + create_field_definition: _CreateFieldDefinition = ( + _create_field_definition_v2 if is_v2_model else _create_field_definition_v1 # type: ignore[assignment] + ) + + property_fields = { + field_info.alias if field_info.alias and prefer_alias else k: create_field_definition( + field_annotation=model_annotations[k], + name=field_info.alias if field_info.alias and prefer_alias else k, + default=Empty + if is_undefined_sentinel(field_info.default) or is_pydantic_undefined(field_info.default) + else field_info.default, + field_info=field_info, + ) + for k, field_info in model_fields.items() + } + + computed_field_definitions = create_field_definitions_for_computed_fields( + model, + prefer_alias=prefer_alias, + ) + property_fields.update(computed_field_definitions) + + return PydanticModelInfo( + pydantic_version="2" if is_v2_model else "1", + title=title, + example=example, + field_definitions=property_fields, + is_generic=is_generic_model, + model_fields=model_fields, + ) diff --git a/litestar/plugins/sqlalchemy.py b/litestar/plugins/sqlalchemy.py index 35d8c3e7e6..dc46f0a393 100644 --- a/litestar/plugins/sqlalchemy.py +++ b/litestar/plugins/sqlalchemy.py @@ -1,76 +1,57 @@ +# ruff: noqa: TC004, F401 +# pyright: reportUnusedImport=false from __future__ import annotations -from advanced_alchemy.extensions.litestar import ( - AlembicAsyncConfig, - AlembicCommands, - AlembicSyncConfig, - AsyncSessionConfig, - EngineConfig, - SQLAlchemyAsyncConfig, - SQLAlchemyDTO, - SQLAlchemyDTOConfig, - SQLAlchemyInitPlugin, - SQLAlchemyPlugin, - SQLAlchemySerializationPlugin, - SQLAlchemySyncConfig, - SyncSessionConfig, - async_autocommit_before_send_handler, - async_autocommit_handler_maker, - async_default_before_send_handler, - async_default_handler_maker, - base, - exceptions, - filters, - mixins, - operations, - repository, - service, - sync_autocommit_before_send_handler, - sync_autocommit_handler_maker, - sync_default_before_send_handler, - sync_default_handler_maker, - types, - utils, -) +from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ( - "filters", - "utils", - "operations", - "base", - "types", - "repository", - "service", - "mixins", - "exceptions", - "async_autocommit_handler_maker", - "sync_autocommit_handler_maker", - "async_default_handler_maker", - "sync_default_handler_maker", - "sync_autocommit_before_send_handler", - "async_autocommit_before_send_handler", - "sync_default_before_send_handler", - "async_default_before_send_handler", - "AlembicCommands", "AlembicAsyncConfig", + "AlembicCommands", "AlembicSyncConfig", "AsyncSessionConfig", - "SyncSessionConfig", + # deprecated + "AuditColumns", + "BigIntAuditBase", + "BigIntBase", + "BigIntPrimaryKey", + "CommonTableAttributes", + "EngineConfig", + "SQLAlchemyAsyncConfig", "SQLAlchemyDTO", "SQLAlchemyDTOConfig", - "SQLAlchemyAsyncConfig", "SQLAlchemyInitPlugin", "SQLAlchemyPlugin", "SQLAlchemySerializationPlugin", "SQLAlchemySyncConfig", - "EngineConfig", + "SyncSessionConfig", + "UUIDAuditBase", + "UUIDBase", + "UUIDPrimaryKey", + "async_autocommit_before_send_handler", + "async_autocommit_handler_maker", + "async_default_before_send_handler", + "async_default_handler_maker", + "base", + "exceptions", + "filters", + "mixins", + "operations", + "orm_registry", + "repository", + "service", + "sync_autocommit_before_send_handler", + "sync_autocommit_handler_maker", + "sync_default_before_send_handler", + "sync_default_handler_maker", + "types", + "utils", ) def __getattr__(attr_name: str) -> object: - if attr_name in { + _deprecated_attrs = { "AuditColumns", "BigIntAuditBase", "BigIntBase", @@ -80,7 +61,21 @@ def __getattr__(attr_name: str) -> object: "UUIDBase", "UUIDPrimaryKey", "orm_registry", - }: + } + + if attr_name in _deprecated_attrs: + from advanced_alchemy.base import ( + AuditColumns, + BigIntAuditBase, + BigIntBase, + BigIntPrimaryKey, + CommonTableAttributes, + UUIDAuditBase, + UUIDBase, + UUIDPrimaryKey, + orm_registry, + ) + warn_deprecation( deprecated_name=f"litestar.plugins.sqlalchemy.{attr_name}", version="2.9.0", @@ -89,8 +84,92 @@ def __getattr__(attr_name: str) -> object: info=f"importing {attr_name} from 'litestar.plugins.sqlalchemy' is deprecated, please" f"import it from 'litestar.plugins.sqlalchemy.base.{attr_name}' instead", ) - value = globals()[attr_name] = getattr(base, attr_name) + value = globals()[attr_name] = locals()[attr_name] + return value + if attr_name in set(__all__).difference(_deprecated_attrs): + from advanced_alchemy import ( + base, + exceptions, + filters, + mixins, + operations, + repository, + service, + types, + utils, + ) + from advanced_alchemy.extensions.litestar import ( + AlembicAsyncConfig, + AlembicCommands, + AlembicSyncConfig, + AsyncSessionConfig, + EngineConfig, + SQLAlchemyAsyncConfig, + SQLAlchemyDTO, + SQLAlchemyDTOConfig, + SQLAlchemyInitPlugin, + SQLAlchemyPlugin, + SQLAlchemySerializationPlugin, + SQLAlchemySyncConfig, + SyncSessionConfig, + async_autocommit_before_send_handler, + async_autocommit_handler_maker, + async_default_before_send_handler, + async_default_handler_maker, + sync_autocommit_before_send_handler, + sync_autocommit_handler_maker, + sync_default_before_send_handler, + sync_default_handler_maker, + ) + + value = globals()[attr_name] = locals()[attr_name] return value - if attr_name in __all__: - return getattr(attr_name, attr_name) raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") + + +if TYPE_CHECKING: + from advanced_alchemy import ( + base, + exceptions, + filters, + mixins, + operations, + repository, + service, + types, + utils, + ) + from advanced_alchemy.base import ( + AuditColumns, + BigIntAuditBase, + BigIntBase, + BigIntPrimaryKey, + CommonTableAttributes, + UUIDAuditBase, + UUIDBase, + UUIDPrimaryKey, + orm_registry, + ) + from advanced_alchemy.extensions.litestar import ( + AlembicAsyncConfig, + AlembicCommands, + AlembicSyncConfig, + AsyncSessionConfig, + EngineConfig, + SQLAlchemyAsyncConfig, + SQLAlchemyDTO, + SQLAlchemyDTOConfig, + SQLAlchemyInitPlugin, + SQLAlchemyPlugin, + SQLAlchemySerializationPlugin, + SQLAlchemySyncConfig, + SyncSessionConfig, + async_autocommit_before_send_handler, + async_autocommit_handler_maker, + async_default_before_send_handler, + async_default_handler_maker, + sync_autocommit_before_send_handler, + sync_autocommit_handler_maker, + sync_default_before_send_handler, + sync_default_handler_maker, + ) diff --git a/litestar/repository/_filters.py b/litestar/repository/_filters.py index f6b787e95f..cb045bf1e5 100644 --- a/litestar/repository/_filters.py +++ b/litestar/repository/_filters.py @@ -2,9 +2,9 @@ from __future__ import annotations -from collections import abc # noqa: TCH003 +from collections import abc # noqa: TC003 from dataclasses import dataclass -from datetime import datetime # noqa: TCH003 +from datetime import datetime # noqa: TC003 from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar if TYPE_CHECKING: @@ -17,11 +17,11 @@ "CollectionFilter", "FilterTypes", "LimitOffset", - "OrderBy", - "SearchFilter", "NotInCollectionFilter", - "OnBeforeAfter", "NotInSearchFilter", + "OnBeforeAfter", + "OrderBy", + "SearchFilter", ) diff --git a/litestar/repository/filters.py b/litestar/repository/filters.py index e0cce48ab3..de30db16a3 100644 --- a/litestar/repository/filters.py +++ b/litestar/repository/filters.py @@ -29,9 +29,9 @@ "CollectionFilter", "FilterTypes", "LimitOffset", - "OrderBy", - "SearchFilter", "NotInCollectionFilter", - "OnBeforeAfter", "NotInSearchFilter", + "OnBeforeAfter", + "OrderBy", + "SearchFilter", ) diff --git a/litestar/repository/handlers.py b/litestar/repository/handlers.py index 0bc1434a55..5ddf639567 100644 --- a/litestar/repository/handlers.py +++ b/litestar/repository/handlers.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: from litestar.config.app import AppConfig -__all__ = ("signature_namespace_values", "on_app_init") +__all__ = ("on_app_init", "signature_namespace_values") signature_namespace_values = { "BeforeAfter": BeforeAfter, diff --git a/litestar/response/base.py b/litestar/response/base.py index 67eec09159..92c7c1de6b 100644 --- a/litestar/response/base.py +++ b/litestar/response/base.py @@ -43,14 +43,14 @@ class ASGIResponse: """A low-level ASGI response class.""" __slots__ = ( + "_encoded_cookies", "background", "body", "content_length", "encoding", + "headers", "is_head_response", "status_code", - "_encoded_cookies", - "headers", ) _should_set_content_length: ClassVar[bool] = True @@ -212,8 +212,8 @@ class Response(Generic[T]): "encoding", "headers", "media_type", - "status_code", "response_type_encoders", + "status_code", ) content: T diff --git a/litestar/response/file.py b/litestar/response/file.py index 1fc6f8632d..f991c3fd25 100644 --- a/litestar/response/file.py +++ b/litestar/response/file.py @@ -1,10 +1,11 @@ from __future__ import annotations import itertools +from datetime import datetime from email.utils import formatdate from inspect import iscoroutine from mimetypes import encodings_map, guess_type -from typing import TYPE_CHECKING, Any, AsyncGenerator, Coroutine, Iterable, Literal, cast +from typing import TYPE_CHECKING, Any, AsyncGenerator, Coroutine, Final, Iterable, Literal, cast from urllib.parse import quote from zlib import adler32 @@ -69,7 +70,7 @@ async def async_file_iterator( yield chunk -def create_etag_for_file(path: PathType, modified_time: float, file_size: int) -> str: +def create_etag_for_file(path: PathType, modified_time: float | None, file_size: int) -> str: """Create an etag. Notes: @@ -79,7 +80,42 @@ def create_etag_for_file(path: PathType, modified_time: float, file_size: int) - An etag. """ check = adler32(str(path).encode("utf-8")) & 0xFFFFFFFF - return f'"{modified_time}-{file_size}-{check}"' + parts = [str(file_size), str(check)] + if modified_time: + parts.insert(0, str(modified_time)) + return f'"{"-".join(parts)}"' + + +_MTIME_KEYS: Final = ( + "mtime", + "ctime", + "Last-Modified", + "updated_at", + "modification_time", + "last_changed", + "change_time", + "last_modified", + "last_updated", + "timestamp", +) + + +def get_fsspec_mtime_equivalent(info: dict[str, Any]) -> float | None: + """Return the 'mtime' or equivalent for different fsspec implementations, since they + are not standardized. + + See https://github.com/fsspec/filesystem_spec/issues/526. + """ + # inspired by https://github.com/mdshw5/pyfaidx/blob/cac82f24e9c4e334cf87a92e477b92d4615d260f/pyfaidx/__init__.py#L1318-L1345 + mtime: Any | None = next((info[key] for key in _MTIME_KEYS if key in info), None) + if mtime is None or isinstance(mtime, float): + return mtime + if isinstance(mtime, datetime): + return mtime.timestamp() + if isinstance(mtime, str): + return datetime.fromisoformat(mtime.replace("Z", "+00:00")).timestamp() + + raise ValueError(f"Unsupported mtime-type value type {type(mtime)!r}") class ASGIFileResponse(ASGIStreamingResponse): @@ -217,14 +253,16 @@ async def start_response(self, send: Send) -> None: self.content_length = fs_info["size"] self.headers.setdefault("content-length", str(self.content_length)) - self.headers.setdefault("last-modified", formatdate(fs_info["mtime"], usegmt=True)) + mtime = get_fsspec_mtime_equivalent(fs_info) # type: ignore[arg-type] + if mtime is not None: + self.headers.setdefault("last-modified", formatdate(mtime, usegmt=True)) if self.etag: self.headers.setdefault("etag", self.etag.to_header()) else: self.headers.setdefault( "etag", - create_etag_for_file(path=self.file_path, modified_time=fs_info["mtime"], file_size=fs_info["size"]), + create_etag_for_file(path=self.file_path, modified_time=mtime, file_size=fs_info["size"]), ) await super().start_response(send=send) @@ -237,10 +275,10 @@ class File(Response): "chunk_size", "content_disposition_type", "etag", + "file_info", "file_path", "file_system", "filename", - "file_info", "stat_result", ) diff --git a/litestar/response/redirect.py b/litestar/response/redirect.py index 6a070769e9..b768ed8ca9 100644 --- a/litestar/response/redirect.py +++ b/litestar/response/redirect.py @@ -1,9 +1,11 @@ from __future__ import annotations import itertools -from typing import TYPE_CHECKING, Any, Iterable, Literal +from typing import TYPE_CHECKING, Any, Iterable, Literal, Sequence +from urllib.parse import urlencode from litestar.constants import REDIRECT_ALLOWED_MEDIA_TYPES, REDIRECT_STATUS_CODES +from litestar.datastructures import MultiDict from litestar.enums import MediaType from litestar.exceptions import ImproperlyConfiguredException from litestar.response.base import ASGIResponse, Response @@ -13,6 +15,8 @@ from litestar.utils.helpers import get_enum_string_value if TYPE_CHECKING: + from collections.abc import Mapping + from litestar.app import Litestar from litestar.background_tasks import BackgroundTask, BackgroundTasks from litestar.connection import Request @@ -94,6 +98,7 @@ def __init__( media_type: str | MediaType | None = None, status_code: RedirectStatusType | None = None, type_encoders: TypeEncodersMap | None = None, + query_params: Mapping[str, str | Sequence[str]] | MultiDict | None = None, ) -> None: """Initialize the response. @@ -108,12 +113,21 @@ def __init__( status_code: An HTTP status code. The status code should be one of 301, 302, 303, 307 or 308, otherwise an exception will be raised. type_encoders: A mapping of types to callables that transform them into types supported for serialization. + query_params: A dictionary of values from which the request's query will be generated. Raises: ImproperlyConfiguredException: Either if status code is not a redirect status code or media type is not supported. """ - self.url = path + if query_params is None: + self.url = path + elif isinstance(query_params, MultiDict): + # We can't use MultiDictMixin.dict() because it's not deterministic + query_params_dict = {k: query_params.getall(k) for k in query_params} + self.url = f"{path}?{urlencode(query_params_dict, doseq=True)}" + else: + self.url = f"{path}?{urlencode(query_params, doseq=True)}" + if status_code is None: status_code = HTTP_302_FOUND super().__init__( diff --git a/litestar/response/sse.py b/litestar/response/sse.py index 48a9192b98..ca9bf991cb 100644 --- a/litestar/response/sse.py +++ b/litestar/response/sse.py @@ -19,7 +19,7 @@ class _ServerSentEventIterator(AsyncIteratorWrapper[bytes]): - __slots__ = ("content_async_iterator", "event_id", "event_type", "retry_duration", "comment_message") + __slots__ = ("comment_message", "content_async_iterator", "event_id", "event_type", "retry_duration") content_async_iterator: AsyncIterable[SSEData] diff --git a/litestar/response/template.py b/litestar/response/template.py index 6499aaec01..06743a084f 100644 --- a/litestar/response/template.py +++ b/litestar/response/template.py @@ -27,9 +27,9 @@ class Template(Response[bytes]): """Template-based response, rendering a given template into a bytes string.""" __slots__ = ( + "context", "template_name", "template_str", - "context", ) def __init__( diff --git a/litestar/router.py b/litestar/router.py index 88ac0fd567..6b9ca1c953 100644 --- a/litestar/router.py +++ b/litestar/router.py @@ -68,6 +68,7 @@ class Router: "path", "registered_route_handler_ids", "request_class", + "request_max_body_size", "response_class", "response_cookies", "response_headers", @@ -111,6 +112,7 @@ def __init__( type_decoders: TypeDecodersSequence | None = None, type_encoders: TypeEncodersMap | None = None, websocket_class: type[WebSocket] | None = None, + request_max_body_size: int | None | EmptyType = Empty, ) -> None: """Initialize a ``Router``. @@ -143,6 +145,8 @@ def __init__( with the router instance. request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as the default for all route handlers, controllers and other routers associated with the router instance. + request_max_body_size: Maximum allowed size of the request body in bytes. If this size is exceeded, + a '413 - Request Entity Too Large" error response is returned. response_class: A custom subclass of :class:`Response <.response.Response>` to be used as the default for all route handlers, controllers and other routers associated with the router instance. response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. @@ -197,6 +201,7 @@ def __init__( self.type_encoders = dict(type_encoders) if type_encoders is not None else None self.type_decoders = list(type_decoders) if type_decoders is not None else None self.websocket_class = websocket_class + self.request_max_body_size = request_max_body_size for route_handler in route_handlers or []: self.register(value=route_handler) diff --git a/litestar/routes/__init__.py b/litestar/routes/__init__.py index c8b5d3d23d..c7404f4957 100644 --- a/litestar/routes/__init__.py +++ b/litestar/routes/__init__.py @@ -3,4 +3,4 @@ from .http import HTTPRoute from .websocket import WebSocketRoute -__all__ = ("BaseRoute", "ASGIRoute", "WebSocketRoute", "HTTPRoute") +__all__ = ("ASGIRoute", "BaseRoute", "HTTPRoute", "WebSocketRoute") diff --git a/litestar/routes/base.py b/litestar/routes/base.py index 6130c41c0e..40ecd706c6 100644 --- a/litestar/routes/base.py +++ b/litestar/routes/base.py @@ -77,9 +77,9 @@ class BaseRoute(ABC): "handler_names", "methods", "path", + "path_components", "path_format", "path_parameters", - "path_components", "scope_type", ) diff --git a/litestar/routes/http.py b/litestar/routes/http.py index 99ef4afe78..6ed17cfaa2 100644 --- a/litestar/routes/http.py +++ b/litestar/routes/http.py @@ -1,11 +1,11 @@ from __future__ import annotations from itertools import chain -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any from msgspec.msgpack import decode as _decode_msgpack_plain -from litestar.datastructures.upload_file import UploadFile +from litestar.datastructures.multi_dicts import FormMultiDict from litestar.enums import HttpMethod, MediaType, ScopeType from litestar.exceptions import ClientException, ImproperlyConfiguredException, SerializationException from litestar.handlers.http_handlers import HTTPRouteHandler @@ -77,17 +77,18 @@ async def handle(self, scope: HTTPScope, receive: Receive, send: Send) -> None: if route_handler.resolve_guards(): await route_handler.authorize_connection(connection=request) - response = await self._get_response_for_request( - scope=scope, request=request, route_handler=route_handler, parameter_model=parameter_model - ) - - await response(scope, receive, send) + try: + response = await self._get_response_for_request( + scope=scope, request=request, route_handler=route_handler, parameter_model=parameter_model + ) - if after_response_handler := route_handler.resolve_after_response(): - await after_response_handler(request) + await response(scope, receive, send) - if form_data := scope.get("_form", {}): - await self._cleanup_temporary_files(form_data=cast("dict[str, Any]", form_data)) + if after_response_handler := route_handler.resolve_after_response(): + await after_response_handler(request) + finally: + if (form_data := ScopeState.from_scope(scope).form) is not Empty: + await FormMultiDict.from_form_data(form_data).close() def create_handler_map(self) -> None: """Parse the ``router_handlers`` of this route and return a mapping of @@ -169,21 +170,13 @@ async def _get_response_data( cleanup_group: DependencyCleanupGroup | None = None if parameter_model.has_kwargs and route_handler.signature_model: - kwargs = parameter_model.to_kwargs(connection=request) - - if "data" in kwargs: - try: - data = await kwargs["data"] - except SerializationException as e: - raise ClientException(str(e)) from e + try: + kwargs = await parameter_model.to_kwargs(connection=request) + except SerializationException as e: + raise ClientException(str(e)) from e - if data is Empty: - del kwargs["data"] - else: - kwargs["data"] = data - - if "body" in kwargs: - kwargs["body"] = await kwargs["body"] + if kwargs.get("data") is Empty: + del kwargs["data"] if parameter_model.dependency_batches: cleanup_group = await parameter_model.resolve_dependencies(request, kwargs) @@ -266,9 +259,3 @@ def options_handler(scope: Scope) -> Response: include_in_schema=False, sync_to_thread=False, )(options_handler) - - @staticmethod - async def _cleanup_temporary_files(form_data: dict[str, Any]) -> None: - for v in form_data.values(): - if isinstance(v, UploadFile) and not v.file.closed: - await v.close() diff --git a/litestar/routes/websocket.py b/litestar/routes/websocket.py index 3248e2a83c..67019b10b8 100644 --- a/litestar/routes/websocket.py +++ b/litestar/routes/websocket.py @@ -18,8 +18,8 @@ class WebSocketRoute(BaseRoute): """A websocket route, handling a single ``WebsocketRouteHandler``""" __slots__ = ( - "route_handler", "handler_parameter_model", + "route_handler", ) def __init__( @@ -69,7 +69,7 @@ async def handle(self, scope: WebSocketScope, receive: Receive, send: Send) -> N cleanup_group: DependencyCleanupGroup | None = None if self.handler_parameter_model.has_kwargs and self.route_handler.signature_model: - parsed_kwargs = self.handler_parameter_model.to_kwargs(connection=websocket) + parsed_kwargs = await self.handler_parameter_model.to_kwargs(connection=websocket) if self.handler_parameter_model.dependency_batches: cleanup_group = await self.handler_parameter_model.resolve_dependencies(websocket, parsed_kwargs) diff --git a/litestar/security/base.py b/litestar/security/base.py index fbe7913635..415e2dace7 100644 --- a/litestar/security/base.py +++ b/litestar/security/base.py @@ -89,10 +89,8 @@ def on_app_init(self, app_config: AppConfig) -> AppConfig: app_config.openapi_config = copy(app_config.openapi_config) if isinstance(app_config.openapi_config.components, list): app_config.openapi_config.components.append(self.openapi_components) - elif app_config.openapi_config.components: - app_config.openapi_config.components = [self.openapi_components, app_config.openapi_config.components] else: - app_config.openapi_config.components = [self.openapi_components] + app_config.openapi_config.components = [self.openapi_components, app_config.openapi_config.components] if isinstance(app_config.openapi_config.security, list): app_config.openapi_config.security.append(self.security_requirement) diff --git a/litestar/security/jwt/auth.py b/litestar/security/jwt/auth.py index c1758a9bcd..3ebf9767c5 100644 --- a/litestar/security/jwt/auth.py +++ b/litestar/security/jwt/auth.py @@ -68,6 +68,27 @@ class BaseJWTAuth(Generic[UserType, TokenT], AbstractSecurityConfig[UserType, To """ token_cls: type[Token] = Token """Target type the JWT payload will be converted into""" + accepted_audiences: Sequence[str] | None = None + """Audiences to accept when verifying the token. If given, and the audience in the + token does not match, a 401 response is returned + """ + accepted_issuers: Sequence[str] | None = None + """Issuers to accept when verifying the token. If given, and the issuer in the + token does not match, a 401 response is returned + """ + require_claims: Sequence[str] | None = None + """Require these claims to be present in the JWT payload. If any of those claims + is missing, a 401 response is returned + """ + verify_expiry: bool = True + """Verify that the value of the ``exp`` (*expiration*) claim is in the future""" + verify_not_before: bool = True + """Verify that the value of the ``nbf`` (*not before*) claim is in the past""" + strict_audience: bool = False + """Verify that the value of the ``aud`` (*audience*) claim is a single value, and + not a list of values, and matches ``audience`` exactly. Requires that + ``accepted_audiences`` is a sequence of length 1 + """ @property def openapi_components(self) -> Components: @@ -120,6 +141,12 @@ def middleware(self) -> DefineMiddleware: scopes=self.scopes, token_secret=self.token_secret, token_cls=self.token_cls, + token_issuer=self.accepted_issuers, + token_audience=self.accepted_audiences, + require_claims=self.require_claims, + verify_expiry=self.verify_expiry, + verify_not_before=self.verify_not_before, + strict_audience=self.strict_audience, ) def login( @@ -290,6 +317,27 @@ class JWTAuth(Generic[UserType, TokenT], BaseJWTAuth[UserType, TokenT]): """ token_cls: type[Token] = Token """Target type the JWT payload will be converted into""" + accepted_audiences: Sequence[str] | None = None + """Audiences to accept when verifying the token. If given, and the audience in the + token does not match, a 401 response is returned + """ + accepted_issuers: Sequence[str] | None = None + """Issuers to accept when verifying the token. If given, and the issuer in the + token does not match, a 401 response is returned + """ + require_claims: Sequence[str] | None = None + """Require these claims to be present in the JWT payload. If any of those claims + is missing, a 401 response is returned + """ + verify_expiry: bool = True + """Verify that the value of the ``exp`` (*expiration*) claim is in the future""" + verify_not_before: bool = True + """Verify that the value of the ``nbf`` (*not before*) claim is in the past""" + strict_audience: bool = False + """Verify that the value of the ``aud`` (*audience*) claim is a single value, and + not a list of values, and matches ``audience`` exactly. Requires that + ``accepted_audiences`` is a sequence of length 1 + """ @dataclass @@ -370,6 +418,27 @@ class and adds support for passing JWT tokens ``HttpOnly`` cookies. """ token_cls: type[Token] = Token """Target type the JWT payload will be converted into""" + accepted_audiences: Sequence[str] | None = None + """Audiences to accept when verifying the token. If given, and the audience in the + token does not match, a 401 response is returned + """ + accepted_issuers: Sequence[str] | None = None + """Issuers to accept when verifying the token. If given, and the issuer in the + token does not match, a 401 response is returned + """ + require_claims: Sequence[str] | None = None + """Require these claims to be present in the JWT payload. If any of those claims + is missing, a 401 response is returned + """ + verify_expiry: bool = True + """Verify that the value of the ``exp`` (*expiration*) claim is in the future""" + verify_not_before: bool = True + """Verify that the value of the ``nbf`` (*not before*) claim is in the past""" + strict_audience: bool = False + """Verify that the value of the ``aud`` (*audience*) claim is a single value, and + not a list of values, and matches ``audience`` exactly. Requires that + ``accepted_audiences`` is a sequence of length 1 + """ @property def openapi_components(self) -> Components: @@ -411,6 +480,12 @@ def middleware(self) -> DefineMiddleware: scopes=self.scopes, token_secret=self.token_secret, token_cls=self.token_cls, + token_issuer=self.accepted_issuers, + token_audience=self.accepted_audiences, + require_claims=self.require_claims, + verify_expiry=self.verify_expiry, + verify_not_before=self.verify_not_before, + strict_audience=self.strict_audience, ) def login( @@ -579,6 +654,27 @@ class OAuth2PasswordBearerAuth(Generic[UserType, TokenT], BaseJWTAuth[UserType, """ token_cls: type[Token] = Token """Target type the JWT payload will be converted into""" + accepted_audiences: Sequence[str] | None = None + """Audiences to accept when verifying the token. If given, and the audience in the + token does not match, a 401 response is returned + """ + accepted_issuers: Sequence[str] | None = None + """Issuers to accept when verifying the token. If given, and the issuer in the + token does not match, a 401 response is returned + """ + require_claims: Sequence[str] | None = None + """Require these claims to be present in the JWT payload. If any of those claims + is missing, a 401 response is returned + """ + verify_expiry: bool = True + """Verify that the value of the ``exp`` (*expiration*) claim is in the future""" + verify_not_before: bool = True + """Verify that the value of the ``nbf`` (*not before*) claim is in the past""" + strict_audience: bool = False + """Verify that the value of the ``aud`` (*audience*) claim is a single value, and + not a list of values, and matches ``audience`` exactly. Requires that + ``accepted_audiences`` is a sequence of length 1 + """ @property def middleware(self) -> DefineMiddleware: @@ -600,6 +696,12 @@ def middleware(self) -> DefineMiddleware: scopes=self.scopes, token_secret=self.token_secret, token_cls=self.token_cls, + token_issuer=self.accepted_issuers, + token_audience=self.accepted_audiences, + require_claims=self.require_claims, + verify_expiry=self.verify_expiry, + verify_not_before=self.verify_not_before, + strict_audience=self.strict_audience, ) @property diff --git a/litestar/security/jwt/middleware.py b/litestar/security/jwt/middleware.py index 1acd03d1c7..305f4011a3 100644 --- a/litestar/security/jwt/middleware.py +++ b/litestar/security/jwt/middleware.py @@ -28,9 +28,15 @@ class JWTAuthenticationMiddleware(AbstractAuthenticationMiddleware): __slots__ = ( "algorithm", "auth_header", + "require_claims", "retrieve_user_handler", - "token_secret", + "strict_audience", + "token_audience", "token_cls", + "token_issuer", + "token_secret", + "verify_expiry", + "verify_not_before", ) def __init__( @@ -45,6 +51,12 @@ def __init__( scopes: Scopes, token_secret: str, token_cls: type[Token] = Token, + token_audience: Sequence[str] | None = None, + token_issuer: Sequence[str] | None = None, + require_claims: Sequence[str] | None = None, + verify_expiry: bool = True, + verify_not_before: bool = True, + strict_audience: bool = False, ) -> None: """Check incoming requests for an encoded token in the auth header specified, and if present retrieve the user from persistence using the provided function. @@ -62,6 +74,18 @@ def __init__( token_secret: Secret for decoding the JWT. This value should be equivalent to the secret used to encode it. token_cls: Token class used when encoding / decoding JWTs + token_audience: Verify the audience when decoding the token. If the audience + in the token does not match any audience given, raise a + :exc:`NotAuthorizedException` + token_issuer: Verify the issuer when decoding the token. If the issuer in + the token does not match any issuer given, raise a + :exc:`NotAuthorizedException` + require_claims: Require these claims to be present in the JWT payload + verify_expiry: Verify that the value of the ``exp`` (*expiration*) claim is in the future + verify_not_before: Verify that the value of the ``nbf`` (*not before*) claim is in the past + strict_audience: Verify that the value of the ``aud`` (*audience*) claim is a single value, and + not a list of values, and matches ``audience`` exactly. Requires that + ``accepted_audiences`` is a sequence of length 1 """ super().__init__( app=app, @@ -75,6 +99,12 @@ def __init__( self.retrieve_user_handler = retrieve_user_handler self.token_secret = token_secret self.token_cls = token_cls + self.token_audience = token_audience + self.token_issuer = token_issuer + self.require_claims = require_claims + self.verify_expiry = verify_expiry + self.verify_not_before = verify_not_before + self.strict_audience = strict_audience async def authenticate_request(self, connection: ASGIConnection[Any, Any, Any, Any]) -> AuthenticationResult: """Given an HTTP Connection, parse the JWT api key stored in the header and retrieve the user correlating to the @@ -114,6 +144,12 @@ async def authenticate_token( encoded_token=encoded_token, secret=self.token_secret, algorithm=self.algorithm, + audience=self.token_audience, + issuer=self.token_issuer, + require_claims=self.require_claims, + verify_exp=self.verify_expiry, + verify_nbf=self.verify_not_before, + strict_audience=self.strict_audience, ) user = await self.retrieve_user_handler(token, connection) @@ -142,6 +178,12 @@ def __init__( scopes: Scopes, token_secret: str, token_cls: type[Token] = Token, + token_audience: Sequence[str] | None = None, + token_issuer: Sequence[str] | None = None, + require_claims: Sequence[str] | None = None, + verify_expiry: bool = True, + verify_not_before: bool = True, + strict_audience: bool = False, ) -> None: """Check incoming requests for an encoded token in the auth header or cookie name specified, and if present retrieves the user from persistence using the provided function. @@ -160,6 +202,18 @@ def __init__( token_secret: Secret for decoding the JWT. This value should be equivalent to the secret used to encode it. token_cls: Token class used when encoding / decoding JWTs + token_audience: Verify the audience when decoding the token. If the audience + in the token does not match any audience given, raise a + :exc:`NotAuthorizedException` + token_issuer: Verify the issuer when decoding the token. If the issuer in + the token does not match any issuer given, raise a + :exc:`NotAuthorizedException` + require_claims: Require these claims to be present in the JWT payload + verify_expiry: Verify that the value of the ``exp`` (*expiration*) claim is in the future + verify_not_before: Verify that the value of the ``nbf`` (*not before*) claim is in the past + strict_audience: Verify that the value of the ``aud`` (*audience*) claim is a single value, and + not a list of values, and matches ``audience`` exactly. Requires that + ``accepted_audiences`` is a sequence of length 1 """ super().__init__( algorithm=algorithm, @@ -172,6 +226,12 @@ def __init__( scopes=scopes, token_secret=token_secret, token_cls=token_cls, + token_audience=token_audience, + token_issuer=token_issuer, + require_claims=require_claims, + verify_expiry=verify_expiry, + verify_not_before=verify_not_before, + strict_audience=strict_audience, ) self.auth_cookie_key = auth_cookie_key diff --git a/litestar/security/jwt/token.py b/litestar/security/jwt/token.py index 46237229e0..05421657dc 100644 --- a/litestar/security/jwt/token.py +++ b/litestar/security/jwt/token.py @@ -3,7 +3,7 @@ import dataclasses from dataclasses import asdict, dataclass, field from datetime import datetime, timezone -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, TypedDict import jwt import msgspec @@ -13,8 +13,10 @@ if TYPE_CHECKING: from typing_extensions import Self - -__all__ = ("Token",) +__all__ = ( + "JWTDecodeOptions", + "Token", +) def _normalize_datetime(value: datetime) -> datetime: @@ -32,6 +34,17 @@ def _normalize_datetime(value: datetime) -> datetime: return value.replace(microsecond=0) +class JWTDecodeOptions(TypedDict, total=False): + """``options`` for PyJWTs :func:`jwt.decode`""" + + verify_aud: bool + verify_iss: bool + verify_exp: bool + verify_nbf: bool + strict_aud: bool + require: list[str] + + @dataclass class Token: """JWT Token DTO.""" @@ -72,13 +85,59 @@ def __post_init__(self) -> None: raise ImproperlyConfiguredException("iat must be a current or past time") @classmethod - def decode(cls, encoded_token: str, secret: str, algorithm: str) -> Self: - """Decode a passed in token string and returns a Token instance. + def decode_payload( + cls, + encoded_token: str, + secret: str, + algorithms: list[str], + issuer: list[str] | None = None, + audience: str | Sequence[str] | None = None, + options: JWTDecodeOptions | None = None, + ) -> Any: + """Decode and verify the JWT and return its payload""" + return jwt.decode( + jwt=encoded_token, + key=secret, + algorithms=algorithms, + issuer=issuer, + audience=audience, + options=options, # type: ignore[arg-type] + ) + + @classmethod + def decode( + cls, + encoded_token: str, + secret: str, + algorithm: str, + audience: str | Sequence[str] | None = None, + issuer: str | Sequence[str] | None = None, + require_claims: Sequence[str] | None = None, + verify_exp: bool = True, + verify_nbf: bool = True, + strict_audience: bool = False, + ) -> Self: + """Decode a passed in token string and return a Token instance. Args: encoded_token: A base64 string containing an encoded JWT. - secret: The secret with which the JWT is encoded. It may optionally be an individual JWK or JWS set dict + secret: The secret with which the JWT is encoded. algorithm: The algorithm used to encode the JWT. + audience: Verify the audience when decoding the token. If the audience in + the token does not match any audience given, raise a + :exc:`NotAuthorizedException` + issuer: Verify the issuer when decoding the token. If the issuer in the + token does not match any issuer given, raise a + :exc:`NotAuthorizedException` + require_claims: Verify that the given claims are present in the token + verify_exp: Verify that the value of the ``exp`` (*expiration*) claim is in + the future + verify_nbf: Verify that the value of the ``nbf`` (*not before*) claim is in + the past + strict_audience: Verify that the value of the ``aud`` (*audience*) claim is + a single value, and not a list of values, and matches ``audience`` + exactly. Requires the value passed to the ``audience`` to be a sequence + of length 1 Returns: A decoded Token instance. @@ -86,12 +145,34 @@ def decode(cls, encoded_token: str, secret: str, algorithm: str) -> Self: Raises: NotAuthorizedException: If the token is invalid. """ + + options: JWTDecodeOptions = { + "verify_aud": bool(audience), + "verify_iss": bool(issuer), + } + if require_claims: + options["require"] = list(require_claims) + if verify_exp is False: + options["verify_exp"] = False + if verify_nbf is False: + options["verify_nbf"] = False + if strict_audience: + if audience is None or (not isinstance(audience, str) and len(audience) != 1): + raise ValueError("When using 'strict_audience=True', 'audience' must be a sequence of length 1") + options["strict_aud"] = True + # although not documented, pyjwt requires audience to be a string if + # using the strict_aud option + if not isinstance(audience, str): + audience = audience[0] + try: - payload: dict[str, Any] = jwt.decode( - jwt=encoded_token, - key=secret, + payload = cls.decode_payload( + encoded_token=encoded_token, + secret=secret, algorithms=[algorithm], - options={"verify_aud": False}, + audience=audience, + issuer=list(issuer) if issuer else None, + options=options, ) # msgspec can do these conversions as well, but to keep backwards # compatibility, we do it ourselves, since the datetime parsing works a @@ -103,7 +184,12 @@ def decode(cls, encoded_token: str, secret: str, algorithm: str) -> Self: for key in extra_fields: extras[key] = payload.pop(key) return msgspec.convert(payload, cls, strict=False) - except (KeyError, jwt.DecodeError, ImproperlyConfiguredException, jwt.exceptions.InvalidAlgorithmError) as e: + except ( + KeyError, + jwt.exceptions.InvalidTokenError, + ImproperlyConfiguredException, + msgspec.ValidationError, + ) as e: raise NotAuthorizedException("Invalid token") from e def encode(self, secret: str, algorithm: str) -> str: diff --git a/litestar/serialization/__init__.py b/litestar/serialization/__init__.py index 0a9189eeac..54752136cb 100644 --- a/litestar/serialization/__init__.py +++ b/litestar/serialization/__init__.py @@ -9,9 +9,9 @@ ) __all__ = ( - "default_deserializer", "decode_json", "decode_msgpack", + "default_deserializer", "default_serializer", "encode_json", "encode_msgpack", diff --git a/litestar/static_files/base.py b/litestar/static_files/base.py index 7afad1fa06..896b2ebbbc 100644 --- a/litestar/static_files/base.py +++ b/litestar/static_files/base.py @@ -22,7 +22,7 @@ class StaticFiles: """ASGI App that handles file sending.""" - __slots__ = ("is_html_mode", "directories", "adapter", "send_as_attachment", "headers") + __slots__ = ("adapter", "directories", "headers", "is_html_mode", "send_as_attachment") def __init__( self, diff --git a/litestar/static_files/config.py b/litestar/static_files/config.py index 8ee77b77de..7bd0c71728 100644 --- a/litestar/static_files/config.py +++ b/litestar/static_files/config.py @@ -1,13 +1,13 @@ from __future__ import annotations from dataclasses import dataclass -from pathlib import PurePath # noqa: TCH003 +from pathlib import PurePath # noqa: TC003 from typing import TYPE_CHECKING, Any, Sequence from litestar.exceptions import ImproperlyConfiguredException from litestar.file_system import BaseLocalFileSystem from litestar.handlers import asgi, get, head -from litestar.response.file import ASGIFileResponse # noqa: TCH001 +from litestar.response.file import ASGIFileResponse # noqa: TC001 from litestar.router import Router from litestar.static_files.base import StaticFiles from litestar.utils import normalize_path, warn_deprecation diff --git a/litestar/stores/base.py b/litestar/stores/base.py index 69a63663e5..d0aa1fba52 100644 --- a/litestar/stores/base.py +++ b/litestar/stores/base.py @@ -14,7 +14,7 @@ from typing_extensions import Self -__all__ = ("Store", "NamespacedStore", "StorageObject") +__all__ = ("NamespacedStore", "StorageObject", "Store") class Store(ABC): diff --git a/litestar/stores/file.py b/litestar/stores/file.py index 0320e47464..979d256ded 100644 --- a/litestar/stores/file.py +++ b/litestar/stores/file.py @@ -28,7 +28,7 @@ def _safe_file_name(name: str) -> str: class FileStore(NamespacedStore): """File based, thread and process safe, asynchronous key/value store.""" - __slots__ = {"path": "file path", "create_directories": "flag to create directories in path"} + __slots__ = {"create_directories": "flag to create directories in path", "path": "file path"} def __init__(self, path: PathLike[str], *, create_directories: bool = False) -> None: """Initialize ``FileStorage``. diff --git a/litestar/stores/memory.py b/litestar/stores/memory.py index 1da89317aa..07c131b4be 100644 --- a/litestar/stores/memory.py +++ b/litestar/stores/memory.py @@ -17,7 +17,7 @@ class MemoryStore(Store): """In memory, atomic, asynchronous key/value store.""" - __slots__ = ("_store", "_lock") + __slots__ = ("_lock", "_store") def __init__(self) -> None: """Initialize :class:`MemoryStore`""" diff --git a/litestar/stores/registry.py b/litestar/stores/registry.py index 11a08c2008..2cfb930acd 100644 --- a/litestar/stores/registry.py +++ b/litestar/stores/registry.py @@ -18,7 +18,7 @@ def default_default_factory(name: str) -> Store: class StoreRegistry: """Registry for :class:`Store <.base.Store>` instances.""" - __slots__ = ("_stores", "_default_factory") + __slots__ = ("_default_factory", "_stores") def __init__( self, stores: dict[str, Store] | None = None, default_factory: Callable[[str], Store] = default_default_factory diff --git a/litestar/stores/valkey.py b/litestar/stores/valkey.py new file mode 100644 index 0000000000..667e305e11 --- /dev/null +++ b/litestar/stores/valkey.py @@ -0,0 +1,210 @@ +from __future__ import annotations + +from datetime import timedelta +from typing import TYPE_CHECKING, cast + +from valkey.asyncio import Valkey +from valkey.asyncio.connection import ConnectionPool + +from litestar.exceptions import ImproperlyConfiguredException +from litestar.types import Empty, EmptyType +from litestar.utils.empty import value_or_default + +from .base import NamespacedStore + +if TYPE_CHECKING: + from types import TracebackType + + +__all__ = ("ValkeyStore",) + + +class ValkeyStore(NamespacedStore): + """Valkey based, thread and process safe asynchronous key/value store.""" + + __slots__ = ( + "_delete_all_script", + "_get_and_renew_script", + "_valkey", + "handle_client_shutdown", + ) + + def __init__( + self, valkey: Valkey, namespace: str | None | EmptyType = Empty, handle_client_shutdown: bool = False + ) -> None: + """Initialize :class:`ValkeyStore` + + Args: + valkey: An :class:`valkey.asyncio.Valkey` instance + namespace: A key prefix to simulate a namespace in valkey. If not given, + defaults to ``LITESTAR``. Namespacing can be explicitly disabled by passing + ``None``. This will make :meth:`.delete_all` unavailable. + handle_client_shutdown: If ``True``, handle the shutdown of the `valkey` instance automatically during the store's lifespan. Should be set to `True` unless the shutdown is handled externally + """ + self._valkey = valkey + self.namespace: str | None = value_or_default(namespace, "LITESTAR") + self.handle_client_shutdown = handle_client_shutdown + + # script to get and renew a key in one atomic step + self._get_and_renew_script = self._valkey.register_script( + b""" + local key = KEYS[1] + local renew = tonumber(ARGV[1]) + + local data = server.call('GET', key) + local ttl = server.call('TTL', key) + + if ttl > 0 then + server.call('EXPIRE', key, renew) + end + + return data + """ + ) + + # script to delete all keys in the namespace + self._delete_all_script = self._valkey.register_script( + b""" + local cursor = 0 + + repeat + local result = server.call('SCAN', cursor, 'MATCH', ARGV[1]) + for _,key in ipairs(result[2]) do + server.call('UNLINK', key) + end + cursor = tonumber(result[1]) + until cursor == 0 + """ + ) + + async def _shutdown(self) -> None: + if self.handle_client_shutdown: + await self._valkey.aclose(close_connection_pool=True) + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self._shutdown() + + @classmethod + def with_client( + cls, + url: str = "valkey://localhost:6379", + *, + db: int | None = None, + port: int | None = None, + username: str | None = None, + password: str | None = None, + namespace: str | None | EmptyType = Empty, + ) -> ValkeyStore: + """Initialize a :class:`ValkeyStore` instance with a new class:`valkey.asyncio.Valkey` instance. + + Args: + url: Valkey URL to connect to + db: Valkey database to use + port: Valkey port to use + username: Valkey username to use + password: Valkey password to use + namespace: Virtual key namespace to use + """ + pool: ConnectionPool = ConnectionPool.from_url( + url=url, + db=db, + decode_responses=False, + port=port, + username=username, + password=password, + ) + return cls( + valkey=Valkey(connection_pool=pool), + namespace=namespace, + handle_client_shutdown=True, + ) + + def with_namespace(self, namespace: str) -> ValkeyStore: + """Return a new :class:`ValkeyStore` with a nested virtual key namespace. + The current instances namespace will serve as a prefix for the namespace, so it + can be considered the parent namespace. + """ + return type(self)( + valkey=self._valkey, + namespace=f"{self.namespace}_{namespace}" if self.namespace else namespace, + handle_client_shutdown=self.handle_client_shutdown, + ) + + def _make_key(self, key: str) -> str: + prefix = f"{self.namespace}:" if self.namespace else "" + return prefix + key + + async def set(self, key: str, value: str | bytes, expires_in: int | timedelta | None = None) -> None: + """Set a value. + + Args: + key: Key to associate the value with + value: Value to store + expires_in: Time in seconds before the key is considered expired + + Returns: + ``None`` + """ + if isinstance(value, str): + value = value.encode("utf-8") + await self._valkey.set(self._make_key(key), value, ex=expires_in) + + async def get(self, key: str, renew_for: int | timedelta | None = None) -> bytes | None: + """Get a value. + + Args: + key: Key associated with the value + renew_for: If given and the value had an initial expiry time set, renew the + expiry time for ``renew_for`` seconds. If the value has not been set + with an expiry time this is a no-op. Atomicity of this step is guaranteed + by using a lua script to execute fetch and renewal. If ``renew_for`` is + not given, the script will be bypassed so no overhead will occur + + Returns: + The value associated with ``key`` if it exists and is not expired, else + ``None`` + """ + key = self._make_key(key) + if renew_for: + if isinstance(renew_for, timedelta): + renew_for = renew_for.seconds + data = await self._get_and_renew_script(keys=[key], args=[renew_for]) + return cast("bytes | None", data) + return await self._valkey.get(key) # type: ignore[no-any-return] + + async def delete(self, key: str) -> None: + """Delete a value. + + If no such key exists, this is a no-op. + + Args: + key: Key of the value to delete + """ + await self._valkey.delete(self._make_key(key)) + + async def delete_all(self) -> None: + """Delete all stored values in the virtual key namespace. + + Raises: + ImproperlyConfiguredException: If no namespace was configured + """ + if not self.namespace: + raise ImproperlyConfiguredException("Cannot perform delete operation: No namespace configured") + + await self._delete_all_script(keys=[], args=[f"{self.namespace}*:*"]) + + async def exists(self, key: str) -> bool: + """Check if a given ``key`` exists.""" + return await self._valkey.exists(self._make_key(key)) == 1 # type: ignore[no-any-return] + + async def expires_in(self, key: str) -> int | None: + """Get the time in seconds ``key`` expires in. If no such ``key`` exists or no + expiry time was set, return ``None``. + """ + ttl = await self._valkey.ttl(self._make_key(key)) + return None if ttl == -2 else ttl diff --git a/litestar/template/__init__.py b/litestar/template/__init__.py index 5989cea229..0f0812e4e4 100644 --- a/litestar/template/__init__.py +++ b/litestar/template/__init__.py @@ -1,4 +1,4 @@ from litestar.template.base import TemplateEngineProtocol, TemplateProtocol from litestar.template.config import TemplateConfig -__all__ = ("TemplateEngineProtocol", "TemplateProtocol", "TemplateConfig") +__all__ = ("TemplateConfig", "TemplateEngineProtocol", "TemplateProtocol") diff --git a/litestar/testing/__init__.py b/litestar/testing/__init__.py index 55af446ac5..fa37819afd 100644 --- a/litestar/testing/__init__.py +++ b/litestar/testing/__init__.py @@ -1,5 +1,6 @@ from litestar.testing.client.async_client import AsyncTestClient from litestar.testing.client.base import BaseTestClient +from litestar.testing.client.subprocess_client import subprocess_async_client, subprocess_sync_client from litestar.testing.client.sync_client import TestClient from litestar.testing.helpers import create_async_test_client, create_test_client from litestar.testing.request_factory import RequestFactory @@ -8,9 +9,11 @@ __all__ = ( "AsyncTestClient", "BaseTestClient", - "create_async_test_client", - "create_test_client", "RequestFactory", "TestClient", "WebSocketTestSession", + "create_async_test_client", + "create_test_client", + "subprocess_async_client", + "subprocess_sync_client", ) diff --git a/litestar/testing/client/__init__.py b/litestar/testing/client/__init__.py index 5d03a7a9e7..b22b04e0a3 100644 --- a/litestar/testing/client/__init__.py +++ b/litestar/testing/client/__init__.py @@ -33,4 +33,4 @@ from .base import BaseTestClient from .sync_client import TestClient -__all__ = ("TestClient", "AsyncTestClient", "BaseTestClient") +__all__ = ("AsyncTestClient", "BaseTestClient", "TestClient") diff --git a/litestar/testing/client/async_client.py b/litestar/testing/client/async_client.py index 4e28bef4ac..4bf9eec087 100644 --- a/litestar/testing/client/async_client.py +++ b/litestar/testing/client/async_client.py @@ -86,6 +86,7 @@ async def __aenter__(self) -> Self: async with AsyncExitStack() as stack: self.blocking_portal = portal = stack.enter_context(self.portal()) self.lifespan_handler = LifeSpanHandler(client=self) + stack.enter_context(self.lifespan_handler) @stack.callback def reset_portal() -> None: diff --git a/litestar/testing/client/base.py b/litestar/testing/client/base.py index 9820729351..eeec983b79 100644 --- a/litestar/testing/client/base.py +++ b/litestar/testing/client/base.py @@ -97,13 +97,13 @@ class BaseTestClient(Generic[T]): blocking_portal: BlockingPortal __slots__ = ( + "_session_backend", "app", - "base_url", "backend", "backend_options", - "session_config", - "_session_backend", + "base_url", "cookies", + "session_config", ) def __init__( diff --git a/litestar/testing/client/subprocess_client.py b/litestar/testing/client/subprocess_client.py new file mode 100644 index 0000000000..f274ad2f38 --- /dev/null +++ b/litestar/testing/client/subprocess_client.py @@ -0,0 +1,70 @@ +import pathlib +import socket +import subprocess +import time +from contextlib import asynccontextmanager, contextmanager +from typing import AsyncIterator, Iterator + +import httpx + + +class StartupError(RuntimeError): + pass + + +def _get_available_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + # Bind to a free port provided by the host + try: + sock.bind(("localhost", 0)) + except OSError as e: # pragma: no cover + raise StartupError("Could not find an open port") from e + else: + port: int = sock.getsockname()[1] + return port + + +@contextmanager +def run_app(workdir: pathlib.Path, app: str) -> Iterator[str]: + """Launch a litestar application in a subprocess with a random available port.""" + port = _get_available_port() + with subprocess.Popen( + args=["litestar", "--app", app, "run", "--port", str(port)], + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + cwd=workdir, + ) as proc: + url = f"http://127.0.0.1:{port}" + for _ in range(100): # pragma: no cover + try: + httpx.get(url, timeout=0.1) + break + except httpx.TransportError: + time.sleep(1) + yield url + proc.kill() + + +@asynccontextmanager +async def subprocess_async_client(workdir: pathlib.Path, app: str) -> AsyncIterator[httpx.AsyncClient]: + """Provides an async httpx client for a litestar app launched in a subprocess. + + Args: + workdir: Path to the directory in which the app module resides. + app: Uvicorn app string that can be resolved in the provided working directory, e.g.: "app:app" + """ + with run_app(workdir=workdir, app=app) as url: + async with httpx.AsyncClient(base_url=url) as client: + yield client + + +@contextmanager +def subprocess_sync_client(workdir: pathlib.Path, app: str) -> Iterator[httpx.Client]: + """Provides a sync httpx client for a litestar app launched in a subprocess. + + Args: + workdir: Path to the directory in which the app module resides. + app: Uvicorn app string that can be resolved in the provided working directory, e.g.: "app:app" + """ + with run_app(workdir=workdir, app=app) as url, httpx.Client(base_url=url) as client: + yield client diff --git a/litestar/testing/client/sync_client.py b/litestar/testing/client/sync_client.py index 9cbfcb3d94..9c58d139d2 100644 --- a/litestar/testing/client/sync_client.py +++ b/litestar/testing/client/sync_client.py @@ -87,6 +87,7 @@ def __enter__(self) -> Self: with ExitStack() as stack: self.blocking_portal = portal = stack.enter_context(self.portal()) self.lifespan_handler = LifeSpanHandler(client=self) + stack.enter_context(self.lifespan_handler) @stack.callback def reset_portal() -> None: diff --git a/litestar/testing/life_span_handler.py b/litestar/testing/life_span_handler.py index 8ee7d22c3c..7141f4eeb5 100644 --- a/litestar/testing/life_span_handler.py +++ b/litestar/testing/life_span_handler.py @@ -1,5 +1,6 @@ from __future__ import annotations +import warnings from math import inf from typing import TYPE_CHECKING, Generic, Optional, TypeVar, cast @@ -9,6 +10,8 @@ from litestar.testing.client.base import BaseTestClient if TYPE_CHECKING: + from types import TracebackType + from litestar.types import ( LifeSpanReceiveMessage, # noqa: F401 LifeSpanSendMessage, @@ -20,24 +23,69 @@ class LifeSpanHandler(Generic[T]): - __slots__ = "stream_send", "stream_receive", "client", "task" + __slots__ = ( + "_startup_done", + "client", + "stream_receive", + "stream_send", + "task", + ) def __init__(self, client: T) -> None: self.client = client self.stream_send = StapledObjectStream[Optional["LifeSpanSendMessage"]](*create_memory_object_stream(inf)) # type: ignore[arg-type] self.stream_receive = StapledObjectStream["LifeSpanReceiveMessage"](*create_memory_object_stream(inf)) # type: ignore[arg-type] + self._startup_done = False + def _ensure_setup(self, is_safe: bool = False) -> None: + if self._startup_done: + return + + if not is_safe: + warnings.warn( + "LifeSpanHandler used with implicit startup; Use LifeSpanHandler as a context manager instead. " + "Implicit startup will be deprecated in version 3.0.", + category=DeprecationWarning, + stacklevel=2, + ) + + self._startup_done = True with self.client.portal() as portal: self.task = portal.start_task_soon(self.lifespan) portal.call(self.wait_startup) + def close(self) -> None: + with self.client.portal() as portal: + portal.call(self.stream_send.aclose) + portal.call(self.stream_receive.aclose) + + def __enter__(self) -> LifeSpanHandler: + try: + self._ensure_setup(is_safe=True) + except Exception as exc: + self.close() + raise exc + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + self.close() + async def receive(self) -> LifeSpanSendMessage: + self._ensure_setup() + message = await self.stream_send.receive() if message is None: self.task.result() return cast("LifeSpanSendMessage", message) async def wait_startup(self) -> None: + self._ensure_setup() + event: LifeSpanStartupEvent = {"type": "lifespan.startup"} await self.stream_receive.send(event) @@ -54,6 +102,8 @@ async def wait_startup(self) -> None: await self.receive() async def wait_shutdown(self) -> None: + self._ensure_setup() + async with self.stream_send: lifespan_shutdown_event: LifeSpanShutdownEvent = {"type": "lifespan.shutdown"} await self.stream_receive.send(lifespan_shutdown_event) @@ -71,6 +121,8 @@ async def wait_shutdown(self) -> None: await self.receive() async def lifespan(self) -> None: + self._ensure_setup() + scope = {"type": "lifespan"} try: await self.client.app(scope, self.stream_receive.receive, self.stream_send.send) diff --git a/litestar/testing/request_factory.py b/litestar/testing/request_factory.py index 6166e6f1ab..e25b6b0956 100644 --- a/litestar/testing/request_factory.py +++ b/litestar/testing/request_factory.py @@ -55,12 +55,12 @@ class RequestFactory: __slots__ = ( "app", - "server", + "handler_kwargs", "port", "root_path", "scheme", - "handler_kwargs", "serializer", + "server", ) def __init__( diff --git a/litestar/testing/websocket_test_session.py b/litestar/testing/websocket_test_session.py index 292e8a9eaf..38901f733b 100644 --- a/litestar/testing/websocket_test_session.py +++ b/litestar/testing/websocket_test_session.py @@ -78,7 +78,7 @@ async def receive() -> WebSocketReceiveMessage: async def send(message: WebSocketSendMessage) -> None: if message["type"] == "websocket.accept": headers = message.get("headers", []) - if headers: + if headers: # type: ignore[truthy-iterable] headers_list = list(self.scope["headers"]) headers_list.extend(headers) self.scope["headers"] = headers_list diff --git a/litestar/types/__init__.py b/litestar/types/__init__.py index 90e319277c..ef91444b88 100644 --- a/litestar/types/__init__.py +++ b/litestar/types/__init__.py @@ -142,13 +142,13 @@ "ResponseHeaders", "RouteHandlerMapItem", "RouteHandlerType", + "SSEData", "Scope", "ScopeSession", "Scopes", "Send", "Serializer", "StreamType", - "SSEData", "SyncOrAsyncUnion", "TypeDecodersSequence", "TypeEncodersMap", diff --git a/litestar/types/asgi_types.py b/litestar/types/asgi_types.py index 09a2575c60..abbcf2ae77 100644 --- a/litestar/types/asgi_types.py +++ b/litestar/types/asgi_types.py @@ -51,7 +51,6 @@ "ASGIApp", "ASGIVersion", "BaseScope", - "HeaderScope", "HTTPDisconnectEvent", "HTTPReceiveMessage", "HTTPRequestEvent", @@ -60,6 +59,7 @@ "HTTPScope", "HTTPSendMessage", "HTTPServerPushEvent", + "HeaderScope", "LifeSpanReceive", "LifeSpanReceiveMessage", "LifeSpanScope", diff --git a/litestar/types/builtin_types.py b/litestar/types/builtin_types.py index 335dedd798..151442dc7b 100644 --- a/litestar/types/builtin_types.py +++ b/litestar/types/builtin_types.py @@ -9,9 +9,9 @@ __all__ = ( "NoneType", + "TypedDictClass", "UnionType", "UnionTypes", - "TypedDictClass", ) NoneType: type[None] = type(None) diff --git a/litestar/types/helper_types.py b/litestar/types/helper_types.py index 588ae5409f..492ed089e8 100644 --- a/litestar/types/helper_types.py +++ b/litestar/types/helper_types.py @@ -25,7 +25,7 @@ T = TypeVar("T") -__all__ = ("OptionalSequence", "SyncOrAsyncUnion", "AnyIOBackend", "StreamType", "MaybePartial", "SSEData") +__all__ = ("AnyIOBackend", "MaybePartial", "OptionalSequence", "SSEData", "StreamType", "SyncOrAsyncUnion") OptionalSequence: TypeAlias = Optional[Sequence[T]] """Types 'T' as union of Sequence[T] and None.""" diff --git a/litestar/types/serialization.py b/litestar/types/serialization.py index 15ec4308ec..098d52ec5c 100644 --- a/litestar/types/serialization.py +++ b/litestar/types/serialization.py @@ -42,13 +42,13 @@ AttrsInstance = Any # type: ignore[assignment, misc] __all__ = ( - "LitestarEncodableType", - "EncodableBuiltinType", + "DataContainerType", "EncodableBuiltinCollectionType", - "EncodableStdLibType", - "EncodableStdLibIPType", + "EncodableBuiltinType", "EncodableMsgSpecType", - "DataContainerType", + "EncodableStdLibIPType", + "EncodableStdLibType", + "LitestarEncodableType", ) EncodableBuiltinType: TypeAlias = "None | bool | int | float | str | bytes | bytearray" diff --git a/litestar/typing.py b/litestar/typing.py index 9646bb0aec..37dec75825 100644 --- a/litestar/typing.py +++ b/litestar/typing.py @@ -1,22 +1,38 @@ from __future__ import annotations +import dataclasses import warnings -from collections import abc, deque +from collections import abc from copy import deepcopy from dataclasses import dataclass, is_dataclass, replace +from enum import Enum from inspect import Parameter, Signature -from typing import Any, AnyStr, Callable, Collection, ForwardRef, Literal, Mapping, Protocol, Sequence, TypeVar, cast +from typing import Any, AnyStr, Callable, Collection, ForwardRef, Literal, Mapping, TypeVar, cast + +from litestar.types import Empty + +try: + import annotated_types +except ImportError: + annotated_types = Empty # type: ignore[assignment] from msgspec import UnsetType -from typing_extensions import NewType, NotRequired, Required, Self, get_args, get_origin, get_type_hints, is_typeddict +from typing_extensions import ( + NewType, + NotRequired, + Required, + Self, + TypeAliasType, + get_args, + get_origin, + get_type_hints, + is_typeddict, +) from litestar.exceptions import ImproperlyConfiguredException, LitestarWarning -from litestar.openapi.spec import Example from litestar.params import BodyKwarg, DependencyKwarg, KwargDefinition, ParameterKwarg -from litestar.types import Empty from litestar.types.builtin_types import NoneType, UnionTypes from litestar.utils.predicates import ( - is_annotated_type, is_any, is_class_and_subclass, is_generic, @@ -37,132 +53,45 @@ T = TypeVar("T", bound=KwargDefinition) -class _KwargMetaExtractor(Protocol): - @staticmethod - def matches(annotation: Any, name: str | None, default: Any) -> bool: ... - - @staticmethod - def extract(annotation: Any, default: Any) -> Any: ... - - -_KWARG_META_EXTRACTORS: set[_KwargMetaExtractor] = set() - - -def _unpack_predicate(value: Any) -> dict[str, Any]: - try: - from annotated_types import Predicate - - if isinstance(value, Predicate): - if value.func == str.islower: - return {"lower_case": True} - if value.func == str.isupper: - return {"upper_case": True} - if value.func == str.isascii: - return {"pattern": "[[:ascii:]]"} - if value.func == str.isdigit: - return {"pattern": "[[:digit:]]"} - except ImportError: - pass - - return {} - - -def _parse_metadata(value: Any, is_sequence_container: bool, extra: dict[str, Any] | None) -> dict[str, Any]: - """Parse metadata from a value. - - Args: - value: A metadata value from annotation, namely anything stored under Annotated[x, metadata...] - is_sequence_container: Whether the type is a sequence container (list, tuple etc...) - extra: Extra key values to parse. - - Returns: - A dictionary of constraints, which fulfill the kwargs of a KwargDefinition class. - """ - extra = { - **cast("dict[str, Any]", extra or getattr(value, "extra", None) or {}), - **(getattr(value, "json_schema_extra", None) or {}), - } - example_list: list[Any] | None - if example := extra.pop("example", None): - example_list = [Example(value=example)] - elif examples := (extra.pop("examples", None) or getattr(value, "examples", None)): - example_list = [Example(value=example) for example in cast("list[str]", examples)] - else: - example_list = None - - return { - k: v - for k, v in { - "gt": getattr(value, "gt", None), - "ge": getattr(value, "ge", None), - "lt": getattr(value, "lt", None), - "le": getattr(value, "le", None), - "multiple_of": getattr(value, "multiple_of", None), - "min_length": None if is_sequence_container else getattr(value, "min_length", None), - "max_length": None if is_sequence_container else getattr(value, "max_length", None), - "description": getattr(value, "description", None), - "examples": example_list, - "title": getattr(value, "title", None), - "lower_case": getattr(value, "to_lower", None), - "upper_case": getattr(value, "to_upper", None), - "pattern": getattr(value, "regex", getattr(value, "pattern", None)), - "min_items": getattr(value, "min_items", getattr(value, "min_length", None)) - if is_sequence_container - else None, - "max_items": getattr(value, "max_items", getattr(value, "max_length", None)) - if is_sequence_container - else None, - "const": getattr(value, "const", None) is not None, - **extra, - }.items() - if v is not None - } - - -def _traverse_metadata( - metadata: Sequence[Any], is_sequence_container: bool, extra: dict[str, Any] | None -) -> dict[str, Any]: - """Recursively traverse metadata from a value. - - Args: - metadata: A list of metadata values from annotation, namely anything stored under Annotated[x, metadata...] - is_sequence_container: Whether the container is a sequence container (list, tuple etc...) - extra: Extra key values to parse. - - Returns: - A dictionary of constraints, which fulfill the kwargs of a KwargDefinition class. - """ - constraints: dict[str, Any] = {} - for value in metadata: - if isinstance(value, (list, set, frozenset, deque)): - constraints.update( - _traverse_metadata( - metadata=cast("Sequence[Any]", value), is_sequence_container=is_sequence_container, extra=extra - ) - ) - elif is_annotated_type(value) and (type_args := [v for v in get_args(value) if v is not None]): - # annotated values can be nested inside other annotated values - # this behaviour is buggy in python 3.8, hence we need to guard here. - if len(type_args) > 1: - constraints.update( - _traverse_metadata(metadata=type_args[1:], is_sequence_container=is_sequence_container, extra=extra) - ) - elif unpacked_predicate := _unpack_predicate(value): - constraints.update(unpacked_predicate) +def _annotated_types_extractor(meta: Any, is_sequence_container: bool) -> dict[str, Any]: # noqa: C901 + if annotated_types is Empty: # type: ignore[comparison-overlap] # pragma: no branch + return {} # type: ignore[unreachable] # pragma: no cover + + kwargs = {} + if isinstance(meta, annotated_types.GroupedMetadata): + for sub_meta in meta: + kwargs.update(_annotated_types_extractor(sub_meta, is_sequence_container=is_sequence_container)) + return kwargs + if isinstance(meta, annotated_types.Gt): + kwargs["gt"] = meta.gt + elif isinstance(meta, annotated_types.Ge): + kwargs["ge"] = meta.ge + elif isinstance(meta, annotated_types.Lt): + kwargs["lt"] = meta.lt + elif isinstance(meta, annotated_types.Le): + kwargs["le"] = meta.le + elif isinstance(meta, annotated_types.MultipleOf): + kwargs["multiple_of"] = meta.multiple_of + elif isinstance(meta, annotated_types.MinLen): + if is_sequence_container: + kwargs["min_items"] = meta.min_length else: - constraints.update(_parse_metadata(value=value, is_sequence_container=is_sequence_container, extra=extra)) - return constraints - - -def _create_metadata_from_type( - metadata: Sequence[Any], model: type[T], annotation: Any, extra: dict[str, Any] | None -) -> tuple[T | None, dict[str, Any]]: - is_sequence_container = is_non_string_sequence(annotation) - result = _traverse_metadata(metadata=metadata, is_sequence_container=is_sequence_container, extra=extra) - - constraints = {k: v for k, v in result.items() if k in dir(model)} - extra = {k: v for k, v in result.items() if k not in constraints} - return model(**constraints) if constraints else None, extra + kwargs["min_length"] = meta.min_length + elif isinstance(meta, annotated_types.MaxLen): + if is_sequence_container: + kwargs["max_items"] = meta.max_length + else: + kwargs["max_length"] = meta.max_length + elif isinstance(meta, annotated_types.Predicate): + if meta.func == str.islower: + kwargs["lower_case"] = True + elif meta.func == str.isupper: + kwargs["upper_case"] = True + elif meta.func == str.isascii: + kwargs["pattern"] = "[[:ascii:]]" + elif meta.func == str.isdigit: # pragma: no cover # coverage quirk: It expects a jump here for branch coverage + kwargs["pattern"] = "[[:digit:]]" + return kwargs @dataclass(frozen=True) @@ -230,29 +159,6 @@ def __eq__(self, other: Any) -> bool: def __hash__(self) -> int: return hash((self.name, self.raw, self.annotation, self.origin, self.inner_types)) - @classmethod - def _extract_metadata( - cls, annotation: Any, name: str | None, default: Any, metadata: tuple[Any, ...], extra: dict[str, Any] | None - ) -> tuple[KwargDefinition | None, dict[str, Any]]: - model = BodyKwarg if name == "data" else ParameterKwarg - - for extractor in _KWARG_META_EXTRACTORS: - if extractor.matches(annotation=annotation, name=name, default=default): - return _create_metadata_from_type( - extractor.extract(annotation=annotation, default=default), - model=model, - annotation=annotation, - extra=extra, - ) - - if any(isinstance(arg, KwargDefinition) for arg in get_args(annotation)): - return next(arg for arg in get_args(annotation) if isinstance(arg, KwargDefinition)), extra or {} - - if metadata: - return _create_metadata_from_type(metadata=metadata, model=model, annotation=annotation, extra=extra) - - return None, {} - @property def has_default(self) -> bool: """Check if the field has a default value. @@ -363,6 +269,11 @@ def is_tuple(self) -> bool: def is_new_type(self) -> bool: return isinstance(self.annotation, NewType) + @property + def is_type_alias_type(self) -> bool: + """Whether the annotation is a ``TypeAliasType``""" + return isinstance(self.annotation, TypeAliasType) + @property def is_type_var(self) -> bool: """Whether the annotation is a TypeVar or not.""" @@ -429,6 +340,10 @@ def is_typeddict_type(self) -> bool: return is_typeddict(self.origin or self.annotation) + @property + def is_enum(self) -> bool: + return self.is_subclass_of(Enum) + @property def type_(self) -> Any: """The type of the annotation with all the wrappers removed, including the generic types.""" @@ -504,7 +419,7 @@ def from_annotation(cls, annotation: Any, **kwargs: Any) -> FieldDefinition: unwrapped, metadata, wrappers = unwrap_annotation(annotation if annotation is not Empty else Any) origin = get_origin(unwrapped) - args = () if origin is abc.Callable else get_args(unwrapped) + annotation_args = () if origin is abc.Callable else get_args(unwrapped) if not kwargs.get("kwarg_definition"): if isinstance(kwargs.get("default"), (KwargDefinition, DependencyKwarg)): @@ -536,20 +451,31 @@ def from_annotation(cls, annotation: Any, **kwargs: Any) -> FieldDefinition: metadata = tuple(v for v in metadata if not isinstance(v, (KwargDefinition, DependencyKwarg))) elif (extra := kwargs.get("extra", {})) and "kwarg_definition" in extra: kwargs["kwarg_definition"] = extra.pop("kwarg_definition") - else: - kwargs["kwarg_definition"], kwargs["extra"] = cls._extract_metadata( - annotation=annotation, - name=kwargs.get("name", ""), - default=kwargs.get("default", Empty), - metadata=metadata, - extra=kwargs.get("extra"), + + # there might be additional metadata + if metadata: + kwarg_definition_merge_args = {} + is_sequence_container = is_non_string_sequence(annotation) + # extract metadata into KwargDefinition attributes + for meta in metadata: + kwarg_definition_merge_args.update( + _annotated_types_extractor(meta, is_sequence_container=is_sequence_container) ) + # if we already have a KwargDefinition, merge it with the additional metadata + if existing_kwargs_definition := kwargs.get("kwarg_definition"): + kwargs["kwarg_definition"] = dataclasses.replace( + existing_kwargs_definition, **kwarg_definition_merge_args + ) + # if not, create a new KwargDefinition + else: + model = BodyKwarg if kwargs.get("name") == "data" else ParameterKwarg + kwargs["kwarg_definition"] = model(**kwarg_definition_merge_args) kwargs.setdefault("annotation", unwrapped) - kwargs.setdefault("args", args) + kwargs.setdefault("args", annotation_args) kwargs.setdefault("default", Empty) kwargs.setdefault("extra", {}) - kwargs.setdefault("inner_types", tuple(FieldDefinition.from_annotation(arg) for arg in args)) + kwargs.setdefault("inner_types", tuple(FieldDefinition.from_annotation(arg) for arg in annotation_args)) kwargs.setdefault("instantiable_origin", get_instantiable_origin(origin, unwrapped)) kwargs.setdefault("kwarg_definition", None) kwargs.setdefault("metadata", metadata) diff --git a/litestar/utils/__init__.py b/litestar/utils/__init__.py index c15988e067..af86b83850 100644 --- a/litestar/utils/__init__.py +++ b/litestar/utils/__init__.py @@ -32,9 +32,9 @@ from .typing import get_origin_or_inner_type, make_non_optional_union __all__ = ( - "ensure_async_callable", "AsyncIteratorWrapper", "deprecated", + "ensure_async_callable", "find_index", "get_enum_string_value", "get_name", diff --git a/litestar/utils/helpers.py b/litestar/utils/helpers.py index c25fe35f25..c8202e9c2d 100644 --- a/litestar/utils/helpers.py +++ b/litestar/utils/helpers.py @@ -15,9 +15,9 @@ __all__ = ( "get_enum_string_value", "get_name", + "unique_name_for_scope", "unwrap_partial", "url_quote", - "unique_name_for_scope", ) T = TypeVar("T") diff --git a/litestar/utils/scope/state.py b/litestar/utils/scope/state.py index cc9fd31d5d..14824ef102 100644 --- a/litestar/utils/scope/state.py +++ b/litestar/utils/scope/state.py @@ -9,7 +9,7 @@ if TYPE_CHECKING: from typing_extensions import Self - from litestar.datastructures import URL, Accept, Headers + from litestar.datastructures import URL, Accept, Headers, UploadFile from litestar.types.asgi_types import Scope from litestar.types.composite_types import ExceptionHandlersMap @@ -26,6 +26,7 @@ class ScopeState: """ __slots__ = ( + "_compat_ns", "accept", "base_url", "body", @@ -47,7 +48,6 @@ class ScopeState: "response_started", "session_id", "url", - "_compat_ns", ) def __init__(self) -> None: @@ -83,7 +83,7 @@ def __init__(self) -> None: dependency_cache: dict[str, Any] | EmptyType do_cache: bool | EmptyType exception_handlers: ExceptionHandlersMap | EmptyType - form: dict[str, str | list[str]] | EmptyType + form: dict[str, str | list[str] | UploadFile] | EmptyType flash_messages: list[dict[str, str]] headers: Headers | EmptyType is_cached: bool | EmptyType diff --git a/litestar/utils/signature.py b/litestar/utils/signature.py index c387b7f93b..becd1bfea7 100644 --- a/litestar/utils/signature.py +++ b/litestar/utils/signature.py @@ -11,10 +11,10 @@ from typing_extensions import Annotated, Self, get_args, get_origin, get_type_hints from litestar import connection, datastructures, types -from litestar.exceptions import ImproperlyConfiguredException from litestar.types import Empty from litestar.typing import FieldDefinition from litestar.utils.typing import expand_type_var_in_type_hint, unwrap_annotation +from litestar.utils.warnings import warn_signature_namespace_override if TYPE_CHECKING: from typing import Sequence @@ -29,9 +29,10 @@ def _get_defaults(_: Any) -> Any: ... __all__ = ( + "ParsedSignature", "add_types_to_signature_namespace", "get_fn_type_hints", - "ParsedSignature", + "merge_signature_namespaces", ) _GLOBAL_NAMES = { @@ -183,7 +184,7 @@ class ParsedSignature: The only post-processing that occurs is the conversion of any forward referenced type annotations. """ - __slots__ = ("parameters", "return_type", "original_signature") + __slots__ = ("original_signature", "parameters", "return_type") parameters: dict[str, FieldDefinition] """A mapping of parameter names to ParsedSignatureParameter instances.""" @@ -255,14 +256,36 @@ def add_types_to_signature_namespace( signature_namespace: The signature namespace to add types to. Raises: - ImproperlyConfiguredException: If a type is already defined in the signature namespace. AttributeError: If a type does not have a `__name__` attribute. Returns: The updated signature namespace. """ - for typ in signature_types: - if (name := typ.__name__) in signature_namespace: - raise ImproperlyConfiguredException(f"Type '{name}' is already defined in the signature namespace") - signature_namespace[name] = typ + return merge_signature_namespaces( + signature_namespace=signature_namespace, + additional_signature_namespace={signature_type.__name__: signature_type for signature_type in signature_types}, + ) + + +def merge_signature_namespaces( + signature_namespace: dict[str, Any], additional_signature_namespace: dict[str, Any] +) -> dict[str, Any]: + """Add types to ith signature namespace mapping. + + Types are added mapped to their `__name__` attribute. + + Args: + signature_namespace: The signature namespace to add types to. + additional_signature_namespace: The signature namespace to merge + + Raises: + AttributeError: If a type does not have a `__name__` attribute. + + Returns: + The updated signature namespace. + """ + for signature_key, signature_type in additional_signature_namespace.items(): + if signature_key in signature_namespace and signature_namespace.get(signature_key) != signature_type: + warn_signature_namespace_override(signature_key) + signature_namespace.update(additional_signature_namespace) return signature_namespace diff --git a/litestar/utils/sync.py b/litestar/utils/sync.py index 7fc21b0182..7c91845a07 100644 --- a/litestar/utils/sync.py +++ b/litestar/utils/sync.py @@ -15,7 +15,7 @@ from litestar.concurrency import sync_to_thread from litestar.utils.predicates import is_async_callable -__all__ = ("ensure_async_callable", "AsyncIteratorWrapper", "AsyncCallable", "is_async_callable") +__all__ = ("AsyncCallable", "AsyncIteratorWrapper", "ensure_async_callable", "is_async_callable") P = ParamSpec("P") @@ -48,7 +48,7 @@ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Awaitable[T]: # pyrigh class AsyncIteratorWrapper(Generic[T]): """Asynchronous generator, wrapping an iterable or iterator.""" - __slots__ = ("iterator", "generator") + __slots__ = ("generator", "iterator") def __init__(self, iterator: Iterator[T] | Iterable[T]) -> None: """Take a sync iterator or iterable and yields values from it asynchronously. diff --git a/litestar/utils/warnings.py b/litestar/utils/warnings.py index e20484be35..2322245ce6 100644 --- a/litestar/utils/warnings.py +++ b/litestar/utils/warnings.py @@ -1,8 +1,15 @@ +from __future__ import annotations + import os import warnings +from typing import TYPE_CHECKING from litestar.exceptions import LitestarWarning -from litestar.types import AnyCallable, AnyGenerator + +if TYPE_CHECKING: + import re + + from litestar.types import AnyCallable, AnyGenerator def warn_implicit_sync_to_thread(source: AnyCallable, stacklevel: int = 2) -> None: @@ -49,3 +56,30 @@ def warn_sync_to_thread_with_generator(source: AnyGenerator, stacklevel: int = 2 def warn_pdb_on_exception(stacklevel: int = 2) -> None: warnings.warn("Python Debugger on exception enabled", category=LitestarWarning, stacklevel=stacklevel) + + +def warn_middleware_excluded_on_all_routes( + pattern: re.Pattern, + middleware_cls: type | None = None, +) -> None: + middleware_name = f" {middleware_cls.__name__!r}" if middleware_cls else "" + warnings.warn( + f"Middleware{middleware_name} exclude pattern {pattern.pattern!r} greedily " + "matches all paths, effectively disabling this middleware. If this was " + "intentional, consider removing this middleware entirely", + category=LitestarWarning, + stacklevel=2, + ) + + +def warn_signature_namespace_override(signature_key: str, stacklevel: int = 2) -> None: + if os.getenv("LITESTAR_WARN_SIGNATURE_NAMESPACE_OVERRIDE") == "0": + return + + warnings.warn( + f"Type '{signature_key}' is already defined as a different type in the signature namespace" + "If this is intentional, you can disable this warning by setting " + "LITESTAR_WARN_SIGNATURE_NAMESPACE_OVERRIDE=0", + category=LitestarWarning, + stacklevel=stacklevel, + ) diff --git a/pdm.lock b/pdm.lock deleted file mode 100644 index 84b1abbda3..0000000000 --- a/pdm.lock +++ /dev/null @@ -1,5037 +0,0 @@ -# This file is @generated by PDM. -# It is not intended for manual editing. - -[metadata] -groups = ["default", "annotated-types", "attrs", "brotli", "cli", "cryptography", "dev", "dev-contrib", "docs", "full", "jinja", "jwt", "linting", "mako", "minijinja", "opentelemetry", "piccolo", "picologging", "prometheus", "pydantic", "redis", "sqlalchemy", "standard", "structlog", "test"] -strategy = ["inherit_metadata"] -lock_version = "4.5.0" -content_hash = "sha256:d5dbd0ab12d7bfd993257269362f0dde6ee952d4566664676d81fde6903afe80" - -[[metadata.targets]] -requires_python = "~=3.8" - -[[package]] -name = "accessible-pygments" -version = "0.0.4" -summary = "A collection of accessible pygments styles" -groups = ["docs"] -dependencies = [ - "pygments>=1.5", -] -files = [ - {file = "accessible-pygments-0.0.4.tar.gz", hash = "sha256:e7b57a9b15958e9601c7e9eb07a440c813283545a20973f2574a5f453d0e953e"}, - {file = "accessible_pygments-0.0.4-py2.py3-none-any.whl", hash = "sha256:416c6d8c1ea1c5ad8701903a20fcedf953c6e720d64f33dc47bfb2d3f2fa4e8d"}, -] - -[[package]] -name = "advanced-alchemy" -version = "0.19.3" -requires_python = ">=3.8" -summary = "Ready-to-go SQLAlchemy concoctions." -groups = ["full", "sqlalchemy"] -dependencies = [ - "alembic>=1.12.0", - "eval-type-backport; python_version <= \"3.9\"", - "greenlet; sys_platform == \"darwin\"", - "sqlalchemy>=2.0.20", - "typing-extensions>=4.0.0", -] -files = [ - {file = "advanced_alchemy-0.19.3-py3-none-any.whl", hash = "sha256:0b5d78877dacae39eefca7ef27c87357d56378dd2d9a64829ffcf4a6385a5e8b"}, - {file = "advanced_alchemy-0.19.3.tar.gz", hash = "sha256:b572f809154a9680cadda26577b20269638acdc8a14cd3805d20693f26e7c97e"}, -] - -[[package]] -name = "aiosqlite" -version = "0.20.0" -requires_python = ">=3.8" -summary = "asyncio bridge to the standard sqlite3 module" -groups = ["dev"] -dependencies = [ - "typing-extensions>=4.0", -] -files = [ - {file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"}, - {file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"}, -] - -[[package]] -name = "alabaster" -version = "0.7.13" -requires_python = ">=3.6" -summary = "A configurable sidebar-enabled Sphinx theme" -groups = ["docs"] -files = [ - {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, - {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, -] - -[[package]] -name = "alembic" -version = "1.13.2" -requires_python = ">=3.8" -summary = "A database migration tool for SQLAlchemy." -groups = ["full", "sqlalchemy"] -dependencies = [ - "Mako", - "SQLAlchemy>=1.3.0", - "importlib-metadata; python_version < \"3.9\"", - "importlib-resources; python_version < \"3.9\"", - "typing-extensions>=4", -] -files = [ - {file = "alembic-1.13.2-py3-none-any.whl", hash = "sha256:6b8733129a6224a9a711e17c99b08462dbf7cc9670ba8f2e2ae9af860ceb1953"}, - {file = "alembic-1.13.2.tar.gz", hash = "sha256:1ff0ae32975f4fd96028c39ed9bb3c867fe3af956bd7bb37343b54c9fe7445ef"}, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -requires_python = ">=3.8" -summary = "Reusable constraint types to use with typing.Annotated" -groups = ["annotated-types", "dev", "full", "piccolo", "pydantic"] -dependencies = [ - "typing-extensions>=4.0.0; python_version < \"3.9\"", -] -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "anyio" -version = "4.4.0" -requires_python = ">=3.8" -summary = "High level compatibility layer for multiple asynchronous event loop implementations" -groups = ["default", "cli", "dev", "full", "linting", "standard"] -dependencies = [ - "exceptiongroup>=1.0.2; python_version < \"3.11\"", - "idna>=2.8", - "sniffio>=1.1", - "typing-extensions>=4.1; python_version < \"3.11\"", -] -files = [ - {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, - {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, -] - -[[package]] -name = "apeye" -version = "1.4.1" -requires_python = ">=3.6.1" -summary = "Handy tools for working with URLs and APIs." -groups = ["docs"] -dependencies = [ - "apeye-core>=1.0.0b2", - "domdf-python-tools>=2.6.0", - "platformdirs>=2.3.0", - "requests>=2.24.0", -] -files = [ - {file = "apeye-1.4.1-py3-none-any.whl", hash = "sha256:44e58a9104ec189bf42e76b3a7fe91e2b2879d96d48e9a77e5e32ff699c9204e"}, - {file = "apeye-1.4.1.tar.gz", hash = "sha256:14ea542fad689e3bfdbda2189a354a4908e90aee4bf84c15ab75d68453d76a36"}, -] - -[[package]] -name = "apeye-core" -version = "1.1.5" -requires_python = ">=3.6.1" -summary = "Core (offline) functionality for the apeye library." -groups = ["docs"] -dependencies = [ - "domdf-python-tools>=2.6.0", - "idna>=2.5", -] -files = [ - {file = "apeye_core-1.1.5-py3-none-any.whl", hash = "sha256:dc27a93f8c9e246b3b238c5ea51edf6115ab2618ef029b9f2d9a190ec8228fbf"}, - {file = "apeye_core-1.1.5.tar.gz", hash = "sha256:5de72ed3d00cc9b20fea55e54b7ab8f5ef8500eb33a5368bc162a5585e238a55"}, -] - -[[package]] -name = "asgiref" -version = "3.8.1" -requires_python = ">=3.8" -summary = "ASGI specs, helper code, and adapters" -groups = ["dev", "full", "opentelemetry"] -dependencies = [ - "typing-extensions>=4; python_version < \"3.11\"", -] -files = [ - {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, - {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, -] - -[[package]] -name = "async-timeout" -version = "4.0.3" -requires_python = ">=3.7" -summary = "Timeout context manager for asyncio programs" -groups = ["dev", "full", "linting", "redis"] -marker = "python_version < \"3.12.0\"" -dependencies = [ - "typing-extensions>=3.6.5; python_version < \"3.8\"", -] -files = [ - {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, - {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, -] - -[[package]] -name = "asyncpg" -version = "0.29.0" -requires_python = ">=3.8.0" -summary = "An asyncio PostgreSQL driver" -groups = ["dev", "linting"] -dependencies = [ - "async-timeout>=4.0.3; python_version < \"3.12.0\"", -] -files = [ - {file = "asyncpg-0.29.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72fd0ef9f00aeed37179c62282a3d14262dbbafb74ec0ba16e1b1864d8a12169"}, - {file = "asyncpg-0.29.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52e8f8f9ff6e21f9b39ca9f8e3e33a5fcdceaf5667a8c5c32bee158e313be385"}, - {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e6823a7012be8b68301342ba33b4740e5a166f6bbda0aee32bc01638491a22"}, - {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:746e80d83ad5d5464cfbf94315eb6744222ab00aa4e522b704322fb182b83610"}, - {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ff8e8109cd6a46ff852a5e6bab8b0a047d7ea42fcb7ca5ae6eaae97d8eacf397"}, - {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97eb024685b1d7e72b1972863de527c11ff87960837919dac6e34754768098eb"}, - {file = "asyncpg-0.29.0-cp310-cp310-win32.whl", hash = "sha256:5bbb7f2cafd8d1fa3e65431833de2642f4b2124be61a449fa064e1a08d27e449"}, - {file = "asyncpg-0.29.0-cp310-cp310-win_amd64.whl", hash = "sha256:76c3ac6530904838a4b650b2880f8e7af938ee049e769ec2fba7cd66469d7772"}, - {file = "asyncpg-0.29.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4900ee08e85af01adb207519bb4e14b1cae8fd21e0ccf80fac6aa60b6da37b4"}, - {file = "asyncpg-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a65c1dcd820d5aea7c7d82a3fdcb70e096f8f70d1a8bf93eb458e49bfad036ac"}, - {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b52e46f165585fd6af4863f268566668407c76b2c72d366bb8b522fa66f1870"}, - {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc600ee8ef3dd38b8d67421359779f8ccec30b463e7aec7ed481c8346decf99f"}, - {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:039a261af4f38f949095e1e780bae84a25ffe3e370175193174eb08d3cecab23"}, - {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6feaf2d8f9138d190e5ec4390c1715c3e87b37715cd69b2c3dfca616134efd2b"}, - {file = "asyncpg-0.29.0-cp311-cp311-win32.whl", hash = "sha256:1e186427c88225ef730555f5fdda6c1812daa884064bfe6bc462fd3a71c4b675"}, - {file = "asyncpg-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfe73ffae35f518cfd6e4e5f5abb2618ceb5ef02a2365ce64f132601000587d3"}, - {file = "asyncpg-0.29.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6011b0dc29886ab424dc042bf9eeb507670a3b40aece3439944006aafe023178"}, - {file = "asyncpg-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b544ffc66b039d5ec5a7454667f855f7fec08e0dfaf5a5490dfafbb7abbd2cfb"}, - {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d84156d5fb530b06c493f9e7635aa18f518fa1d1395ef240d211cb563c4e2364"}, - {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54858bc25b49d1114178d65a88e48ad50cb2b6f3e475caa0f0c092d5f527c106"}, - {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bde17a1861cf10d5afce80a36fca736a86769ab3579532c03e45f83ba8a09c59"}, - {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:37a2ec1b9ff88d8773d3eb6d3784dc7e3fee7756a5317b67f923172a4748a175"}, - {file = "asyncpg-0.29.0-cp312-cp312-win32.whl", hash = "sha256:bb1292d9fad43112a85e98ecdc2e051602bce97c199920586be83254d9dafc02"}, - {file = "asyncpg-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:2245be8ec5047a605e0b454c894e54bf2ec787ac04b1cb7e0d3c67aa1e32f0fe"}, - {file = "asyncpg-0.29.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0009a300cae37b8c525e5b449233d59cd9868fd35431abc470a3e364d2b85cb9"}, - {file = "asyncpg-0.29.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cad1324dbb33f3ca0cd2074d5114354ed3be2b94d48ddfd88af75ebda7c43cc"}, - {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:012d01df61e009015944ac7543d6ee30c2dc1eb2f6b10b62a3f598beb6531548"}, - {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000c996c53c04770798053e1730d34e30cb645ad95a63265aec82da9093d88e7"}, - {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e0bfe9c4d3429706cf70d3249089de14d6a01192d617e9093a8e941fea8ee775"}, - {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:642a36eb41b6313ffa328e8a5c5c2b5bea6ee138546c9c3cf1bffaad8ee36dd9"}, - {file = "asyncpg-0.29.0-cp38-cp38-win32.whl", hash = "sha256:a921372bbd0aa3a5822dd0409da61b4cd50df89ae85150149f8c119f23e8c408"}, - {file = "asyncpg-0.29.0-cp38-cp38-win_amd64.whl", hash = "sha256:103aad2b92d1506700cbf51cd8bb5441e7e72e87a7b3a2ca4e32c840f051a6a3"}, - {file = "asyncpg-0.29.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5340dd515d7e52f4c11ada32171d87c05570479dc01dc66d03ee3e150fb695da"}, - {file = "asyncpg-0.29.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e17b52c6cf83e170d3d865571ba574577ab8e533e7361a2b8ce6157d02c665d3"}, - {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f100d23f273555f4b19b74a96840aa27b85e99ba4b1f18d4ebff0734e78dc090"}, - {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48e7c58b516057126b363cec8ca02b804644fd012ef8e6c7e23386b7d5e6ce83"}, - {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9ea3f24eb4c49a615573724d88a48bd1b7821c890c2effe04f05382ed9e8810"}, - {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8d36c7f14a22ec9e928f15f92a48207546ffe68bc412f3be718eedccdf10dc5c"}, - {file = "asyncpg-0.29.0-cp39-cp39-win32.whl", hash = "sha256:797ab8123ebaed304a1fad4d7576d5376c3a006a4100380fb9d517f0b59c1ab2"}, - {file = "asyncpg-0.29.0-cp39-cp39-win_amd64.whl", hash = "sha256:cce08a178858b426ae1aa8409b5cc171def45d4293626e7aa6510696d46decd8"}, - {file = "asyncpg-0.29.0.tar.gz", hash = "sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e"}, -] - -[[package]] -name = "asyncpg-stubs" -version = "0.29.1" -requires_python = ">=3.8,<4.0" -summary = "asyncpg stubs" -groups = ["linting"] -dependencies = [ - "asyncpg<0.30,>=0.29", - "typing-extensions<5.0.0,>=4.7.0", -] -files = [ - {file = "asyncpg_stubs-0.29.1-py3-none-any.whl", hash = "sha256:cce994d5a19394249e74ae8d252bde3c77cee0ddfc776cc708b724fdb4adebb6"}, - {file = "asyncpg_stubs-0.29.1.tar.gz", hash = "sha256:686afcc0af3a2f3c8e393cd850e0de430e5a139ce82b2f28ef8f693ecdf918bf"}, -] - -[[package]] -name = "attrs" -version = "24.2.0" -requires_python = ">=3.7" -summary = "Classes Without Boilerplate" -groups = ["attrs", "dev", "full"] -dependencies = [ - "importlib-metadata; python_version < \"3.8\"", -] -files = [ - {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, - {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, -] - -[[package]] -name = "auto-pytabs" -version = "0.5.0" -requires_python = "<4.0,>=3.8" -summary = "Automatically generate code examples for different Python versions in mkdocs or Sphinx based documentations" -groups = ["docs"] -dependencies = [ - "ruff>=0.4", -] -files = [ - {file = "auto_pytabs-0.5.0-py3-none-any.whl", hash = "sha256:e59fb6d2f8b41b05d0906a322dd4bb1a86749d429483ec10036587de3657dcc8"}, - {file = "auto_pytabs-0.5.0.tar.gz", hash = "sha256:30087831c8be5b2314e663efd06c96b84c096572a060a492540f586362cc4326"}, -] - -[[package]] -name = "auto-pytabs" -version = "0.5.0" -extras = ["sphinx"] -requires_python = "<4.0,>=3.8" -summary = "Automatically generate code examples for different Python versions in mkdocs or Sphinx based documentations" -groups = ["docs"] -dependencies = [ - "auto-pytabs==0.5.0", - "sphinx>=4", -] -files = [ - {file = "auto_pytabs-0.5.0-py3-none-any.whl", hash = "sha256:e59fb6d2f8b41b05d0906a322dd4bb1a86749d429483ec10036587de3657dcc8"}, - {file = "auto_pytabs-0.5.0.tar.gz", hash = "sha256:30087831c8be5b2314e663efd06c96b84c096572a060a492540f586362cc4326"}, -] - -[[package]] -name = "autobahn" -version = "23.1.2" -requires_python = ">=3.7" -summary = "WebSocket client & server library, WAMP real-time framework" -groups = ["dev"] -dependencies = [ - "cryptography>=3.4.6", - "hyperlink>=21.0.0", - "setuptools", - "txaio>=21.2.1", -] -files = [ - {file = "autobahn-23.1.2.tar.gz", hash = "sha256:c5ef8ca7422015a1af774a883b8aef73d4954c9fcd182c9b5244e08e973f7c3a"}, -] - -[[package]] -name = "autodocsumm" -version = "0.2.13" -requires_python = ">=3.7" -summary = "Extended sphinx autodoc including automatic autosummaries" -groups = ["docs"] -dependencies = [ - "Sphinx<9.0,>=2.2", -] -files = [ - {file = "autodocsumm-0.2.13-py3-none-any.whl", hash = "sha256:bf4d82ea7acb3e7d9a3ad8c135e097eca1d3f0bd00800d7804127e848e66741d"}, - {file = "autodocsumm-0.2.13.tar.gz", hash = "sha256:ac5f0cf1adbe957acb136fe0d9e16c38fb74fcaefb45c148204aba26dbb12ee2"}, -] - -[[package]] -name = "automat" -version = "24.8.1" -requires_python = ">=3.8" -summary = "Self-service finite-state machines for the programmer on the go." -groups = ["dev"] -dependencies = [ - "typing-extensions; python_version < \"3.10\"", -] -files = [ - {file = "Automat-24.8.1-py3-none-any.whl", hash = "sha256:bf029a7bc3da1e2c24da2343e7598affaa9f10bf0ab63ff808566ce90551e02a"}, - {file = "automat-24.8.1.tar.gz", hash = "sha256:b34227cf63f6325b8ad2399ede780675083e439b20c323d376373d8ee6306d88"}, -] - -[[package]] -name = "babel" -version = "2.16.0" -requires_python = ">=3.8" -summary = "Internationalization utilities" -groups = ["docs"] -dependencies = [ - "pytz>=2015.7; python_version < \"3.9\"", -] -files = [ - {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, - {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, -] - -[[package]] -name = "backports-zoneinfo" -version = "0.2.1" -requires_python = ">=3.6" -summary = "Backport of the standard library zoneinfo module" -groups = ["dev"] -marker = "python_version < \"3.9\"" -dependencies = [ - "importlib-resources; python_version < \"3.7\"", -] -files = [ - {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, - {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, -] - -[[package]] -name = "beanie" -version = "1.26.0" -requires_python = "<4.0,>=3.7" -summary = "Asynchronous Python ODM for MongoDB" -groups = ["dev"] -dependencies = [ - "click>=7", - "lazy-model==0.2.0", - "motor<4.0.0,>=2.5.0", - "pydantic<3.0,>=1.10", - "toml", - "typing-extensions>=4.7; python_version < \"3.11\"", -] -files = [ - {file = "beanie-1.26.0-py3-none-any.whl", hash = "sha256:b45926c01d4a899c519c665c2a5f230990717e99f7fd68172a389ca33e7693b9"}, - {file = "beanie-1.26.0.tar.gz", hash = "sha256:54016f4ec71ed0ea6ce0c7946a395090c45687f254dbbe1cf06eec608383f790"}, -] - -[[package]] -name = "beautifulsoup4" -version = "4.12.3" -requires_python = ">=3.6.0" -summary = "Screen-scraping library" -groups = ["dev", "docs"] -dependencies = [ - "soupsieve>1.2", -] -files = [ - {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, - {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, -] - -[[package]] -name = "black" -version = "24.8.0" -requires_python = ">=3.8" -summary = "The uncompromising code formatter." -groups = ["full", "piccolo"] -dependencies = [ - "click>=8.0.0", - "mypy-extensions>=0.4.3", - "packaging>=22.0", - "pathspec>=0.9.0", - "platformdirs>=2", - "tomli>=1.1.0; python_version < \"3.11\"", - "typing-extensions>=4.0.1; python_version < \"3.11\"", -] -files = [ - {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, - {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, - {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, - {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, - {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, - {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, - {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, - {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, - {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, - {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, - {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, - {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, - {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, - {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, - {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, - {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, - {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, - {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, - {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, - {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, - {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, - {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, -] - -[[package]] -name = "brotli" -version = "1.1.0" -summary = "Python bindings for the Brotli compression library" -groups = ["brotli", "full"] -files = [ - {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"}, - {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"}, - {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3"}, - {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d"}, - {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e"}, - {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da"}, - {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80"}, - {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"}, - {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"}, - {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"}, - {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"}, - {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"}, - {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"}, - {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6"}, - {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd"}, - {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf"}, - {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61"}, - {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327"}, - {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd"}, - {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"}, - {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"}, - {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"}, - {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"}, - {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"}, - {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"}, - {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"}, - {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"}, - {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91"}, - {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408"}, - {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0"}, - {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc"}, - {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"}, - {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"}, - {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"}, - {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"}, - {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"}, - {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"}, - {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208"}, - {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7"}, - {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751"}, - {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48"}, - {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619"}, - {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97"}, - {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"}, - {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"}, - {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"}, - {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"}, - {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"}, - {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"}, - {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f"}, - {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9"}, - {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf"}, - {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac"}, - {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578"}, - {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474"}, - {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"}, - {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"}, - {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"}, - {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"}, - {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"}, - {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"}, -] - -[[package]] -name = "cachecontrol" -version = "0.14.0" -requires_python = ">=3.7" -summary = "httplib2 caching for requests" -groups = ["docs"] -dependencies = [ - "msgpack<2.0.0,>=0.5.2", - "requests>=2.16.0", -] -files = [ - {file = "cachecontrol-0.14.0-py3-none-any.whl", hash = "sha256:f5bf3f0620c38db2e5122c0726bdebb0d16869de966ea6a2befe92470b740ea0"}, - {file = "cachecontrol-0.14.0.tar.gz", hash = "sha256:7db1195b41c81f8274a7bbd97c956f44e8348265a1bc7641c37dfebc39f0c938"}, -] - -[[package]] -name = "cachecontrol" -version = "0.14.0" -extras = ["filecache"] -requires_python = ">=3.7" -summary = "httplib2 caching for requests" -groups = ["docs"] -dependencies = [ - "cachecontrol==0.14.0", - "filelock>=3.8.0", -] -files = [ - {file = "cachecontrol-0.14.0-py3-none-any.whl", hash = "sha256:f5bf3f0620c38db2e5122c0726bdebb0d16869de966ea6a2befe92470b740ea0"}, - {file = "cachecontrol-0.14.0.tar.gz", hash = "sha256:7db1195b41c81f8274a7bbd97c956f44e8348265a1bc7641c37dfebc39f0c938"}, -] - -[[package]] -name = "certifi" -version = "2024.7.4" -requires_python = ">=3.6" -summary = "Python package for providing Mozilla's CA Bundle." -groups = ["default", "docs", "linting"] -files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, -] - -[[package]] -name = "cffi" -version = "1.17.0" -requires_python = ">=3.8" -summary = "Foreign Function Interface for Python calling C code." -groups = ["cryptography", "dev", "full", "jwt", "linting"] -marker = "os_name == \"nt\" and implementation_name != \"pypy\" or platform_python_implementation != \"PyPy\"" -dependencies = [ - "pycparser", -] -files = [ - {file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"}, - {file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"}, - {file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"}, - {file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"}, - {file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"}, - {file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"}, - {file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"}, - {file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"}, - {file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"}, - {file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"}, - {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"}, - {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"}, - {file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"}, - {file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"}, - {file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"}, - {file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"}, - {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"}, - {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"}, - {file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"}, - {file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"}, - {file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"}, - {file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"}, - {file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"}, - {file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"}, - {file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"}, - {file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"}, - {file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"}, - {file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"}, -] - -[[package]] -name = "cfgv" -version = "3.4.0" -requires_python = ">=3.8" -summary = "Validate configuration and produce human readable error messages." -groups = ["linting"] -files = [ - {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, - {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.3.2" -requires_python = ">=3.7.0" -summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -groups = ["docs", "linting"] -files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, -] - -[[package]] -name = "click" -version = "8.1.7" -requires_python = ">=3.7" -summary = "Composable command line interface toolkit" -groups = ["default", "cli", "dev", "docs", "full", "linting", "piccolo", "standard"] -dependencies = [ - "colorama; platform_system == \"Windows\"", - "importlib-metadata; python_version < \"3.8\"", -] -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[[package]] -name = "codecov-cli" -version = "0.7.4" -requires_python = ">=3.8" -summary = "Codecov Command Line Interface" -groups = ["linting"] -dependencies = [ - "click==8.*", - "httpx==0.23.*", - "ijson==3.*", - "pyyaml==6.*", - "regex", - "responses==0.21.*", - "test-results-parser==0.1.*", - "tree-sitter==0.20.*", -] -files = [ - {file = "codecov-cli-0.7.4.tar.gz", hash = "sha256:94ef34615ec969d504d9ca5c71b608086984e7918b8efb522d26071158711453"}, - {file = "codecov_cli-0.7.4-cp311-cp311-macosx_12_6_x86_64.whl", hash = "sha256:211648b0465d84b750b2af7649185a676d1c23572bc66685807f1e8a031c1f1b"}, - {file = "codecov_cli-0.7.4-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:4ad491ba839e806f055e0dedbbe65ea3d9bc984287e022f8a9ee730a3e2c0776"}, - {file = "codecov_cli-0.7.4-cp311-cp311-win_amd64.whl", hash = "sha256:b038b07cb1355b3db847c215a3fa6163d0b0c43c633292a943ef8459c815f0c5"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -summary = "Cross-platform colored terminal text." -groups = ["default", "cli", "dev", "docs", "full", "linting", "piccolo", "standard", "test"] -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "constantly" -version = "23.10.4" -requires_python = ">=3.8" -summary = "Symbolic constants in Python" -groups = ["dev"] -files = [ - {file = "constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9"}, - {file = "constantly-23.10.4.tar.gz", hash = "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd"}, -] - -[[package]] -name = "covdefaults" -version = "2.3.0" -requires_python = ">=3.7" -summary = "A coverage plugin to provide sensible default settings" -groups = ["test"] -dependencies = [ - "coverage>=6.0.2", -] -files = [ - {file = "covdefaults-2.3.0-py2.py3-none-any.whl", hash = "sha256:2832961f6ffcfe4b57c338bc3418a3526f495c26fb9c54565409c5532f7c41be"}, - {file = "covdefaults-2.3.0.tar.gz", hash = "sha256:4e99f679f12d792bc62e5510fa3eb59546ed47bd569e36e4fddc4081c9c3ebf7"}, -] - -[[package]] -name = "coverage" -version = "7.6.1" -requires_python = ">=3.8" -summary = "Code coverage measurement for Python" -groups = ["test"] -files = [ - {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, - {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, - {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, - {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, - {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, - {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, - {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, - {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, - {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, - {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, - {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, - {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, - {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, - {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, - {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, - {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, - {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, - {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, -] - -[[package]] -name = "coverage" -version = "7.6.1" -extras = ["toml"] -requires_python = ">=3.8" -summary = "Code coverage measurement for Python" -groups = ["test"] -dependencies = [ - "coverage==7.6.1", - "tomli; python_full_version <= \"3.11.0a6\"", -] -files = [ - {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, - {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, - {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, - {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, - {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, - {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, - {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, - {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, - {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, - {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, - {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, - {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, - {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, - {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, - {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, - {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, - {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, - {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, -] - -[[package]] -name = "cryptography" -version = "43.0.0" -requires_python = ">=3.7" -summary = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -groups = ["cryptography", "dev", "full", "jwt", "linting"] -dependencies = [ - "cffi>=1.12; platform_python_implementation != \"PyPy\"", -] -files = [ - {file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"}, - {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"}, - {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"}, - {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"}, - {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"}, - {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"}, - {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"}, - {file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"}, - {file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"}, - {file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"}, - {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"}, - {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"}, - {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"}, - {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"}, - {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"}, - {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"}, - {file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"}, - {file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"}, - {file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"}, - {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"}, - {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"}, - {file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"}, - {file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"}, - {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"}, - {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"}, - {file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"}, - {file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"}, -] - -[[package]] -name = "cssutils" -version = "2.11.1" -requires_python = ">=3.8" -summary = "A CSS Cascading Style Sheets library for Python" -groups = ["docs"] -dependencies = [ - "more-itertools", -] -files = [ - {file = "cssutils-2.11.1-py3-none-any.whl", hash = "sha256:a67bfdfdff4f3867fab43698ec4897c1a828eca5973f4073321b3bccaf1199b1"}, - {file = "cssutils-2.11.1.tar.gz", hash = "sha256:0563a76513b6af6eebbe788c3bf3d01c920e46b3f90c8416738c5cfc773ff8e2"}, -] - -[[package]] -name = "daphne" -version = "4.1.2" -requires_python = ">=3.8" -summary = "Django ASGI (HTTP/WebSocket) server" -groups = ["dev"] -dependencies = [ - "asgiref<4,>=3.5.2", - "autobahn>=22.4.2", - "twisted[tls]>=22.4", -] -files = [ - {file = "daphne-4.1.2-py3-none-any.whl", hash = "sha256:618d1322bb4d875342b99dd2a10da2d9aae7ee3645f765965fdc1e658ea5290a"}, - {file = "daphne-4.1.2.tar.gz", hash = "sha256:fcbcace38eb86624ae247c7ffdc8ac12f155d7d19eafac4247381896d6f33761"}, -] - -[[package]] -name = "deprecated" -version = "1.2.14" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -summary = "Python @deprecated decorator to deprecate old python classes, functions or methods." -groups = ["dev-contrib", "full", "opentelemetry"] -dependencies = [ - "wrapt<2,>=1.10", -] -files = [ - {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, - {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, -] - -[[package]] -name = "dict2css" -version = "0.3.0.post1" -requires_python = ">=3.6" -summary = "A μ-library for constructing cascading style sheets from Python dictionaries." -groups = ["docs"] -dependencies = [ - "cssutils>=2.2.0", - "domdf-python-tools>=2.2.0", -] -files = [ - {file = "dict2css-0.3.0.post1-py3-none-any.whl", hash = "sha256:f006a6b774c3e31869015122ae82c491fd25e7de4a75607a62aa3e798f837e0d"}, - {file = "dict2css-0.3.0.post1.tar.gz", hash = "sha256:89c544c21c4ca7472c3fffb9d37d3d926f606329afdb751dc1de67a411b70719"}, -] - -[[package]] -name = "distlib" -version = "0.3.8" -summary = "Distribution utilities" -groups = ["linting"] -files = [ - {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, - {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, -] - -[[package]] -name = "dnspython" -version = "2.6.1" -requires_python = ">=3.8" -summary = "DNS toolkit" -groups = ["dev", "full", "piccolo", "pydantic"] -files = [ - {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, - {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, -] - -[[package]] -name = "docstring-parser" -version = "0.16" -requires_python = ">=3.6,<4.0" -summary = "Parse Python docstrings in reST, Google and Numpydoc format" -groups = ["full", "piccolo"] -files = [ - {file = "docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637"}, - {file = "docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e"}, -] - -[[package]] -name = "docutils" -version = "0.20.1" -requires_python = ">=3.7" -summary = "Docutils -- Python Documentation Utilities" -groups = ["docs"] -files = [ - {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, - {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, -] - -[[package]] -name = "domdf-python-tools" -version = "3.9.0" -requires_python = ">=3.6" -summary = "Helpful functions for Python 🐍 🛠️" -groups = ["docs"] -dependencies = [ - "importlib-metadata>=3.6.0; python_version < \"3.9\"", - "importlib-resources>=3.0.0; python_version < \"3.9\"", - "natsort>=7.0.1", - "typing-extensions>=3.7.4.1", -] -files = [ - {file = "domdf_python_tools-3.9.0-py3-none-any.whl", hash = "sha256:4e1ef365cbc24627d6d1e90cf7d46d8ab8df967e1237f4a26885f6986c78872e"}, - {file = "domdf_python_tools-3.9.0.tar.gz", hash = "sha256:1f8a96971178333a55e083e35610d7688cd7620ad2b99790164e1fc1a3614c18"}, -] - -[[package]] -name = "editorconfig" -version = "0.12.4" -summary = "EditorConfig File Locator and Interpreter for Python" -groups = ["cli", "full", "standard"] -files = [ - {file = "EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80"}, -] - -[[package]] -name = "email-validator" -version = "2.2.0" -requires_python = ">=3.8" -summary = "A robust email address syntax and deliverability validation library." -groups = ["full", "piccolo", "pydantic"] -dependencies = [ - "dnspython>=2.0.0", - "idna>=2.0.0", -] -files = [ - {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, - {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, -] - -[[package]] -name = "eval-type-backport" -version = "0.2.0" -requires_python = ">=3.8" -summary = "Like `typing._eval_type`, but lets older Python versions use newer typing features." -groups = ["full", "sqlalchemy"] -marker = "python_version <= \"3.9\"" -files = [ - {file = "eval_type_backport-0.2.0-py3-none-any.whl", hash = "sha256:ac2f73d30d40c5a30a80b8739a789d6bb5e49fdffa66d7912667e2015d9c9933"}, - {file = "eval_type_backport-0.2.0.tar.gz", hash = "sha256:68796cfbc7371ebf923f03bdf7bef415f3ec098aeced24e054b253a0e78f7b37"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.2.2" -requires_python = ">=3.7" -summary = "Backport of PEP 654 (exception groups)" -groups = ["default", "cli", "dev", "full", "linting", "standard", "test"] -marker = "python_version < \"3.11\"" -files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, -] - -[[package]] -name = "execnet" -version = "2.1.1" -requires_python = ">=3.8" -summary = "execnet: rapid multi-Python deployment" -groups = ["test"] -files = [ - {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, - {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, -] - -[[package]] -name = "faker" -version = "28.0.0" -requires_python = ">=3.8" -summary = "Faker is a Python package that generates fake data for you." -groups = ["default"] -dependencies = [ - "python-dateutil>=2.4", -] -files = [ - {file = "Faker-28.0.0-py3-none-any.whl", hash = "sha256:6a3a08be54c37e05f7943d7ba5211d252c1de737687a46ad6f29209d8d5db11f"}, - {file = "faker-28.0.0.tar.gz", hash = "sha256:0d3c0399204aaf8205cc1750db443474ca0436f177126b2c27b798e8336cc74f"}, -] - -[[package]] -name = "fast-query-parsers" -version = "1.0.3" -requires_python = ">=3.8" -summary = "Ultra-fast query string and url-encoded form-data parsers" -groups = ["full", "standard"] -files = [ - {file = "fast_query_parsers-1.0.3-cp38-abi3-macosx_10_7_x86_64.whl", hash = "sha256:afbf71c1b4398dacfb9d84755eb026f8e759f68a066f1f3cc19e471fc342e74f"}, - {file = "fast_query_parsers-1.0.3-cp38-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:42f26875311d1b151c3406adfa39ec2db98df111a369d75f6fa243ec8462f147"}, - {file = "fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66630ad423b5b1f5709f82a4d8482cd6aa2f3fa73d2c779ff1877f25dee08d55"}, - {file = "fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6e3d816c572a6fad1ae9b93713b2db0d3db6e8f594e035ad52361d668dd94a8"}, - {file = "fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0bdcc0ddb4cc69d823c2c0dedd8f5affc71042db39908ad2ca06261bf388cac6"}, - {file = "fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6720505f2d2a764c76bcc4f3730a9dff69d9871740e46264f6605d73f9ce3794"}, - {file = "fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e947e7251769593da93832a10861f59565a46149fa117ebdf25377e7b2853936"}, - {file = "fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55a30b7cee0a53cddf9016b86fdad87221980d5a02a6126c491bd309755e6de9"}, - {file = "fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc2b457caa38371df1a30cfdfc57bd9bfdf348367abdaf6f36533416a0b0e93"}, - {file = "fast_query_parsers-1.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5736d3c32d6ba23995fa569fe572feabcfcfc30ac9e4709e94cff6f2c456a3d1"}, - {file = "fast_query_parsers-1.0.3-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:3a6377eb0c5b172fbc77c3f96deaf1e51708b4b96d27ce173658bf11c1c00b20"}, - {file = "fast_query_parsers-1.0.3-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:7ca6be04f443a1b055e910ccad01b1d72212f269a530415df99a87c5f1e9c927"}, - {file = "fast_query_parsers-1.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a70d4d8852606f2dd5b798ab628b9d8dc6970ddfdd9e96f4543eb0cc89a74fb5"}, - {file = "fast_query_parsers-1.0.3-cp38-abi3-win32.whl", hash = "sha256:14b3fab7e9a6ac1c1efaf66c3fd2a3fd1e25ede03ed14118035e530433830a11"}, - {file = "fast_query_parsers-1.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:21ae5f3a209aee7d3b84bdcdb33dd79f39fc8cb608b3ae8cfcb78123758c1a16"}, - {file = "fast_query_parsers-1.0.3.tar.gz", hash = "sha256:5200a9e02997ad51d4d76a60ea1b256a68a184b04359540eb6310a15013df68f"}, -] - -[[package]] -name = "filelock" -version = "3.15.4" -requires_python = ">=3.8" -summary = "A platform independent file lock." -groups = ["docs", "linting"] -files = [ - {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, - {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, -] - -[[package]] -name = "fsspec" -version = "2024.6.1" -requires_python = ">=3.8" -summary = "File-system specification" -groups = ["dev"] -files = [ - {file = "fsspec-2024.6.1-py3-none-any.whl", hash = "sha256:3cb443f8bcd2efb31295a5b9fdb02aee81d8452c80d28f97a6d0959e6cee101e"}, - {file = "fsspec-2024.6.1.tar.gz", hash = "sha256:fad7d7e209dd4c1208e3bbfda706620e0da5142bebbd9c384afb95b07e798e49"}, -] - -[[package]] -name = "greenlet" -version = "3.0.3" -requires_python = ">=3.7" -summary = "Lightweight in-process concurrent programming" -groups = ["dev", "full", "sqlalchemy"] -files = [ - {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, - {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, - {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, - {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, - {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, - {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, - {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, - {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, - {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, - {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, - {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, - {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, - {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, - {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, - {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, - {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, - {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, - {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, - {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, - {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, - {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, - {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, - {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, -] - -[[package]] -name = "h11" -version = "0.14.0" -requires_python = ">=3.7" -summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -groups = ["default", "cli", "dev", "full", "linting", "standard"] -dependencies = [ - "typing-extensions; python_version < \"3.8\"", -] -files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] - -[[package]] -name = "h2" -version = "4.1.0" -requires_python = ">=3.6.1" -summary = "HTTP/2 State-Machine based protocol implementation" -groups = ["dev"] -dependencies = [ - "hpack<5,>=4.0", - "hyperframe<7,>=6.0", -] -files = [ - {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, - {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, -] - -[[package]] -name = "hiredis" -version = "3.0.0" -requires_python = ">=3.8" -summary = "Python wrapper for hiredis" -groups = ["full", "redis"] -files = [ - {file = "hiredis-3.0.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:4b182791c41c5eb1d9ed736f0ff81694b06937ca14b0d4dadde5dadba7ff6dae"}, - {file = "hiredis-3.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:13c275b483a052dd645eb2cb60d6380f1f5215e4c22d6207e17b86be6dd87ffa"}, - {file = "hiredis-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1018cc7f12824506f165027eabb302735b49e63af73eb4d5450c66c88f47026"}, - {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83a29cc7b21b746cb6a480189e49f49b2072812c445e66a9e38d2004d496b81c"}, - {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e241fab6332e8fb5f14af00a4a9c6aefa22f19a336c069b7ddbf28ef8341e8d6"}, - {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1fb8de899f0145d6c4d5d4bd0ee88a78eb980a7ffabd51e9889251b8f58f1785"}, - {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b23291951959141173eec10f8573538e9349fa27f47a0c34323d1970bf891ee5"}, - {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e421ac9e4b5efc11705a0d5149e641d4defdc07077f748667f359e60dc904420"}, - {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:77c8006c12154c37691b24ff293c077300c22944018c3ff70094a33e10c1d795"}, - {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:41afc0d3c18b59eb50970479a9c0e5544fb4b95e3a79cf2fbaece6ddefb926fe"}, - {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:04ccae6dcd9647eae6025425ab64edb4d79fde8b9e6e115ebfabc6830170e3b2"}, - {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fe91d62b0594db5ea7d23fc2192182b1a7b6973f628a9b8b2e0a42a2be721ac6"}, - {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:99516d99316062824a24d145d694f5b0d030c80da693ea6f8c4ecf71a251d8bb"}, - {file = "hiredis-3.0.0-cp310-cp310-win32.whl", hash = "sha256:562eaf820de045eb487afaa37e6293fe7eceb5b25e158b5a1974b7e40bf04543"}, - {file = "hiredis-3.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a1c81c89ed765198da27412aa21478f30d54ef69bf5e4480089d9c3f77b8f882"}, - {file = "hiredis-3.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:4664dedcd5933364756d7251a7ea86d60246ccf73a2e00912872dacbfcef8978"}, - {file = "hiredis-3.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:47de0bbccf4c8a9f99d82d225f7672b9dd690d8fd872007b933ef51a302c9fa6"}, - {file = "hiredis-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e43679eca508ba8240d016d8cca9d27342d70184773c15bea78a23c87a1922f1"}, - {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13c345e7278c210317e77e1934b27b61394fee0dec2e8bd47e71570900f75823"}, - {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00018f22f38530768b73ea86c11f47e8d4df65facd4e562bd78773bd1baef35e"}, - {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ea3a86405baa8eb0d3639ced6926ad03e07113de54cb00fd7510cb0db76a89d"}, - {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c073848d2b1d5561f3903879ccf4e1a70c9b1e7566c7bdcc98d082fa3e7f0a1d"}, - {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a8dffb5f5b3415a4669d25de48b617fd9d44b0bccfc4c2ab24b06406ecc9ecb"}, - {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:22c17c96143c2a62dfd61b13803bc5de2ac526b8768d2141c018b965d0333b66"}, - {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c3ece960008dab66c6b8bb3a1350764677ee7c74ccd6270aaf1b1caf9ccebb46"}, - {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f75999ae00a920f7dce6ecae76fa5e8674a3110e5a75f12c7a2c75ae1af53396"}, - {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e069967cbd5e1900aafc4b5943888f6d34937fc59bf8918a1a546cb729b4b1e4"}, - {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0aacc0a78e1d94d843a6d191f224a35893e6bdfeb77a4a89264155015c65f126"}, - {file = "hiredis-3.0.0-cp311-cp311-win32.whl", hash = "sha256:719c32147ba29528cb451f037bf837dcdda4ff3ddb6cdb12c4216b0973174718"}, - {file = "hiredis-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:bdc144d56333c52c853c31b4e2e52cfbdb22d3da4374c00f5f3d67c42158970f"}, - {file = "hiredis-3.0.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:484025d2eb8f6348f7876fc5a2ee742f568915039fcb31b478fd5c242bb0fe3a"}, - {file = "hiredis-3.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:fcdb552ffd97151dab8e7bc3ab556dfa1512556b48a367db94b5c20253a35ee1"}, - {file = "hiredis-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bb6f9fd92f147ba11d338ef5c68af4fd2908739c09e51f186e1d90958c68cc1"}, - {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa86bf9a0ed339ec9e8a9a9d0ae4dccd8671625c83f9f9f2640729b15e07fbfd"}, - {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e194a0d5df9456995d8f510eab9f529213e7326af6b94770abf8f8b7952ddcaa"}, - {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a1df39d74ec507d79c7a82c8063eee60bf80537cdeee652f576059b9cdd15c"}, - {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f91456507427ba36fd81b2ca11053a8e112c775325acc74e993201ea912d63e9"}, - {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9862db92ef67a8a02e0d5370f07d380e14577ecb281b79720e0d7a89aedb9ee5"}, - {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d10fcd9e0eeab835f492832b2a6edb5940e2f1230155f33006a8dfd3bd2c94e4"}, - {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:48727d7d405d03977d01885f317328dc21d639096308de126c2c4e9950cbd3c9"}, - {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e0bb6102ebe2efecf8a3292c6660a0e6fac98176af6de67f020bea1c2343717"}, - {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:df274e3abb4df40f4c7274dd3e587dfbb25691826c948bc98d5fead019dfb001"}, - {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:034925b5fb514f7b11aac38cd55b3fd7e9d3af23bd6497f3f20aa5b8ba58e232"}, - {file = "hiredis-3.0.0-cp312-cp312-win32.whl", hash = "sha256:120f2dda469b28d12ccff7c2230225162e174657b49cf4cd119db525414ae281"}, - {file = "hiredis-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:e584fe5f4e6681d8762982be055f1534e0170f6308a7a90f58d737bab12ff6a8"}, - {file = "hiredis-3.0.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:122171ff47d96ed8dd4bba6c0e41d8afaba3e8194949f7720431a62aa29d8895"}, - {file = "hiredis-3.0.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:ba9fc605ac558f0de67463fb588722878641e6fa1dabcda979e8e69ff581d0bd"}, - {file = "hiredis-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a631e2990b8be23178f655cae8ac6c7422af478c420dd54e25f2e26c29e766f1"}, - {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63482db3fadebadc1d01ad33afa6045ebe2ea528eb77ccaabd33ee7d9c2bad48"}, - {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f669212c390eebfbe03c4e20181f5970b82c5d0a0ad1df1785f7ffbe7d61150"}, - {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a49ef161739f8018c69b371528bdb47d7342edfdee9ddc75a4d8caddf45a6e"}, - {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98a152052b8878e5e43a2e3a14075218adafc759547c98668a21e9485882696c"}, - {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50a196af0ce657fcde9bf8a0bbe1032e22c64d8fcec2bc926a35e7ff68b3a166"}, - {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f2f312eef8aafc2255e3585dcf94d5da116c43ef837db91db9ecdc1bc930072d"}, - {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:6ca41fa40fa019cde42c21add74aadd775e71458051a15a352eabeb12eb4d084"}, - {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:6eecb343c70629f5af55a8b3e53264e44fa04e155ef7989de13668a0cb102a90"}, - {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:c3fdad75e7837a475900a1d3a5cc09aa024293c3b0605155da2d42f41bc0e482"}, - {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8854969e7480e8d61ed7549eb232d95082a743e94138d98d7222ba4e9f7ecacd"}, - {file = "hiredis-3.0.0-cp38-cp38-win32.whl", hash = "sha256:f114a6c86edbf17554672b050cce72abf489fe58d583c7921904d5f1c9691605"}, - {file = "hiredis-3.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:7d99b91e42217d7b4b63354b15b41ce960e27d216783e04c4a350224d55842a4"}, - {file = "hiredis-3.0.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:4c6efcbb5687cf8d2aedcc2c3ed4ac6feae90b8547427d417111194873b66b06"}, - {file = "hiredis-3.0.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5b5cff42a522a0d81c2ae7eae5e56d0ee7365e0c4ad50c4de467d8957aff4414"}, - {file = "hiredis-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:82f794d564f4bc76b80c50b03267fe5d6589e93f08e66b7a2f674faa2fa76ebc"}, - {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7a4c1791d7aa7e192f60fe028ae409f18ccdd540f8b1e6aeb0df7816c77e4a4"}, - {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2537b2cd98192323fce4244c8edbf11f3cac548a9d633dbbb12b48702f379f4"}, - {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fed69bbaa307040c62195a269f82fc3edf46b510a17abb6b30a15d7dab548df"}, - {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869f6d5537d243080f44253491bb30aa1ec3c21754003b3bddeadedeb65842b0"}, - {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d435ae89073d7cd51e6b6bf78369c412216261c9c01662e7008ff00978153729"}, - {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:204b79b30a0e6be0dc2301a4d385bb61472809f09c49f400497f1cdd5a165c66"}, - {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3ea635101b739c12effd189cc19b2671c268abb03013fd1f6321ca29df3ca625"}, - {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f359175197fd833c8dd7a8c288f1516be45415bb5c939862ab60c2918e1e1943"}, - {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac6d929cb33dd12ad3424b75725975f0a54b5b12dbff95f2a2d660c510aa106d"}, - {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:100431e04d25a522ef2c3b94f294c4219c4de3bfc7d557b6253296145a144c11"}, - {file = "hiredis-3.0.0-cp39-cp39-win32.whl", hash = "sha256:e1a9c14ae9573d172dc050a6f63a644457df5d01ec4d35a6a0f097f812930f83"}, - {file = "hiredis-3.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:54a6dd7b478e6eb01ce15b3bb5bf771e108c6c148315bf194eb2ab776a3cac4d"}, - {file = "hiredis-3.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:50da7a9edf371441dfcc56288d790985ee9840d982750580710a9789b8f4a290"}, - {file = "hiredis-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9b285ef6bf1581310b0d5e8f6ce64f790a1c40e89c660e1320b35f7515433672"}, - {file = "hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dcfa684966f25b335072115de2f920228a3c2caf79d4bfa2b30f6e4f674a948"}, - {file = "hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a41be8af1fd78ca97bc948d789a09b730d1e7587d07ca53af05758f31f4b985d"}, - {file = "hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:038756db735e417ab36ee6fd7725ce412385ed2bd0767e8179a4755ea11b804f"}, - {file = "hiredis-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fcecbd39bd42cef905c0b51c9689c39d0cc8b88b1671e7f40d4fb213423aef3a"}, - {file = "hiredis-3.0.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a131377493a59fb0f5eaeb2afd49c6540cafcfba5b0b3752bed707be9e7c4eaf"}, - {file = "hiredis-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d22c53f0ec5c18ecb3d92aa9420563b1c5d657d53f01356114978107b00b860"}, - {file = "hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8a91e9520fbc65a799943e5c970ffbcd67905744d8becf2e75f9f0a5e8414f0"}, - {file = "hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dc8043959b50141df58ab4f398e8ae84c6f9e673a2c9407be65fc789138f4a6"}, - {file = "hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51b99cfac514173d7b8abdfe10338193e8a0eccdfe1870b646009d2fb7cbe4b5"}, - {file = "hiredis-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:fa1fcad89d8a41d8dc10b1e54951ec1e161deabd84ed5a2c95c3c7213bdb3514"}, - {file = "hiredis-3.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:898636a06d9bf575d2c594129085ad6b713414038276a4bfc5db7646b8a5be78"}, - {file = "hiredis-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:466f836dbcf86de3f9692097a7a01533dc9926986022c6617dc364a402b265c5"}, - {file = "hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23142a8af92a13fc1e3f2ca1d940df3dcf2af1d176be41fe8d89e30a837a0b60"}, - {file = "hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:793c80a3d6b0b0e8196a2d5de37a08330125668c8012922685e17aa9108c33ac"}, - {file = "hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:467d28112c7faa29b7db743f40803d927c8591e9da02b6ce3d5fadc170a542a2"}, - {file = "hiredis-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:dc384874a719c767b50a30750f937af18842ee5e288afba95a5a3ed703b1515a"}, - {file = "hiredis-3.0.0.tar.gz", hash = "sha256:fed8581ae26345dea1f1e0d1a96e05041a727a45e7d8d459164583e23c6ac441"}, -] - -[[package]] -name = "hpack" -version = "4.0.0" -requires_python = ">=3.6.1" -summary = "Pure-Python HPACK header compression" -groups = ["dev"] -files = [ - {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, - {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, -] - -[[package]] -name = "html5lib" -version = "1.1" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -summary = "HTML parser based on the WHATWG HTML specification" -groups = ["docs"] -dependencies = [ - "six>=1.9", - "webencodings", -] -files = [ - {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"}, - {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"}, -] - -[[package]] -name = "httpcore" -version = "0.16.3" -requires_python = ">=3.7" -summary = "A minimal low-level HTTP client." -groups = ["default", "linting"] -dependencies = [ - "anyio<5.0,>=3.0", - "certifi", - "h11<0.15,>=0.13", - "sniffio==1.*", -] -files = [ - {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, - {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, -] - -[[package]] -name = "httptools" -version = "0.6.1" -requires_python = ">=3.8.0" -summary = "A collection of framework independent HTTP protocol utils." -groups = ["cli", "full", "standard"] -files = [ - {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"}, - {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"}, - {file = "httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58"}, - {file = "httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185"}, - {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142"}, - {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658"}, - {file = "httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b"}, - {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1"}, - {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0"}, - {file = "httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc"}, - {file = "httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2"}, - {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837"}, - {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d"}, - {file = "httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3"}, - {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0"}, - {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2"}, - {file = "httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90"}, - {file = "httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503"}, - {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84"}, - {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb"}, - {file = "httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949"}, - {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3"}, - {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb"}, - {file = "httptools-0.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97"}, - {file = "httptools-0.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"}, - {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4"}, - {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf"}, - {file = "httptools-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084"}, - {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3"}, - {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e"}, - {file = "httptools-0.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d"}, - {file = "httptools-0.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da"}, - {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81"}, - {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a"}, - {file = "httptools-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e"}, - {file = "httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a"}, -] - -[[package]] -name = "httpx" -version = "0.23.3" -requires_python = ">=3.7" -summary = "The next generation HTTP client." -groups = ["default", "linting"] -dependencies = [ - "certifi", - "httpcore<0.17.0,>=0.15.0", - "rfc3986[idna2008]<2,>=1.3", - "sniffio", -] -files = [ - {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, - {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, -] - -[[package]] -name = "httpx-sse" -version = "0.4.0" -requires_python = ">=3.8" -summary = "Consume Server-Sent Event (SSE) messages with HTTPX." -groups = ["dev-contrib"] -files = [ - {file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"}, - {file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"}, -] - -[[package]] -name = "hypercorn" -version = "0.17.3" -requires_python = ">=3.8" -summary = "A ASGI Server based on Hyper libraries and inspired by Gunicorn" -groups = ["dev"] -dependencies = [ - "exceptiongroup>=1.1.0; python_version < \"3.11\"", - "h11", - "h2>=3.1.0", - "priority", - "taskgroup; python_version < \"3.11\"", - "tomli; python_version < \"3.11\"", - "typing-extensions; python_version < \"3.11\"", - "wsproto>=0.14.0", -] -files = [ - {file = "hypercorn-0.17.3-py3-none-any.whl", hash = "sha256:059215dec34537f9d40a69258d323f56344805efb462959e727152b0aa504547"}, - {file = "hypercorn-0.17.3.tar.gz", hash = "sha256:1b37802ee3ac52d2d85270700d565787ab16cf19e1462ccfa9f089ca17574165"}, -] - -[[package]] -name = "hyperframe" -version = "6.0.1" -requires_python = ">=3.6.1" -summary = "HTTP/2 framing layer for Python" -groups = ["dev"] -files = [ - {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, - {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, -] - -[[package]] -name = "hyperlink" -version = "21.0.0" -requires_python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -summary = "A featureful, immutable, and correct URL for Python." -groups = ["dev"] -dependencies = [ - "idna>=2.5", - "typing; python_version < \"3.5\"", -] -files = [ - {file = "hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4"}, - {file = "hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b"}, -] - -[[package]] -name = "hypothesis" -version = "6.111.1" -requires_python = ">=3.8" -summary = "A library for property-based testing" -groups = ["dev"] -dependencies = [ - "attrs>=22.2.0", - "exceptiongroup>=1.0.0; python_version < \"3.11\"", - "sortedcontainers<3.0.0,>=2.1.0", -] -files = [ - {file = "hypothesis-6.111.1-py3-none-any.whl", hash = "sha256:9422adbac4b2104f6cf92dc6604b5c9df975efc08ffc7145ecc39bc617243835"}, - {file = "hypothesis-6.111.1.tar.gz", hash = "sha256:6ab6185a858fa692bf125c0d0a936134edc318bee01c05e407c71c9ead0b61c5"}, -] - -[[package]] -name = "identify" -version = "2.6.0" -requires_python = ">=3.8" -summary = "File identification library for Python" -groups = ["linting"] -files = [ - {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, - {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, -] - -[[package]] -name = "idna" -version = "3.8" -requires_python = ">=3.6" -summary = "Internationalized Domain Names in Applications (IDNA)" -groups = ["default", "cli", "dev", "docs", "full", "linting", "piccolo", "pydantic", "standard"] -files = [ - {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, - {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, -] - -[[package]] -name = "ijson" -version = "3.3.0" -summary = "Iterative JSON parser with standard Python iterator interfaces" -groups = ["linting"] -files = [ - {file = "ijson-3.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7f7a5250599c366369fbf3bc4e176f5daa28eb6bc7d6130d02462ed335361675"}, - {file = "ijson-3.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f87a7e52f79059f9c58f6886c262061065eb6f7554a587be7ed3aa63e6b71b34"}, - {file = "ijson-3.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b73b493af9e947caed75d329676b1b801d673b17481962823a3e55fe529c8b8b"}, - {file = "ijson-3.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5576415f3d76290b160aa093ff968f8bf6de7d681e16e463a0134106b506f49"}, - {file = "ijson-3.3.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e9ffe358d5fdd6b878a8a364e96e15ca7ca57b92a48f588378cef315a8b019e"}, - {file = "ijson-3.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8643c255a25824ddd0895c59f2319c019e13e949dc37162f876c41a283361527"}, - {file = "ijson-3.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:df3ab5e078cab19f7eaeef1d5f063103e1ebf8c26d059767b26a6a0ad8b250a3"}, - {file = "ijson-3.3.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3dc1fb02c6ed0bae1b4bf96971258bf88aea72051b6e4cebae97cff7090c0607"}, - {file = "ijson-3.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e9afd97339fc5a20f0542c971f90f3ca97e73d3050cdc488d540b63fae45329a"}, - {file = "ijson-3.3.0-cp310-cp310-win32.whl", hash = "sha256:844c0d1c04c40fd1b60f148dc829d3f69b2de789d0ba239c35136efe9a386529"}, - {file = "ijson-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:d654d045adafdcc6c100e8e911508a2eedbd2a1b5f93f930ba13ea67d7704ee9"}, - {file = "ijson-3.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:501dce8eaa537e728aa35810656aa00460a2547dcb60937c8139f36ec344d7fc"}, - {file = "ijson-3.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:658ba9cad0374d37b38c9893f4864f284cdcc7d32041f9808fba8c7bcaadf134"}, - {file = "ijson-3.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2636cb8c0f1023ef16173f4b9a233bcdb1df11c400c603d5f299fac143ca8d70"}, - {file = "ijson-3.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd174b90db68c3bcca273e9391934a25d76929d727dc75224bf244446b28b03b"}, - {file = "ijson-3.3.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97a9aea46e2a8371c4cf5386d881de833ed782901ac9f67ebcb63bb3b7d115af"}, - {file = "ijson-3.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c594c0abe69d9d6099f4ece17763d53072f65ba60b372d8ba6de8695ce6ee39e"}, - {file = "ijson-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8e0ff16c224d9bfe4e9e6bd0395826096cda4a3ef51e6c301e1b61007ee2bd24"}, - {file = "ijson-3.3.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0015354011303175eae7e2ef5136414e91de2298e5a2e9580ed100b728c07e51"}, - {file = "ijson-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034642558afa57351a0ffe6de89e63907c4cf6849070cc10a3b2542dccda1afe"}, - {file = "ijson-3.3.0-cp311-cp311-win32.whl", hash = "sha256:192e4b65495978b0bce0c78e859d14772e841724d3269fc1667dc6d2f53cc0ea"}, - {file = "ijson-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:72e3488453754bdb45c878e31ce557ea87e1eb0f8b4fc610373da35e8074ce42"}, - {file = "ijson-3.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:988e959f2f3d59ebd9c2962ae71b97c0df58323910d0b368cc190ad07429d1bb"}, - {file = "ijson-3.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b2f73f0d0fce5300f23a1383d19b44d103bb113b57a69c36fd95b7c03099b181"}, - {file = "ijson-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0ee57a28c6bf523d7cb0513096e4eb4dac16cd935695049de7608ec110c2b751"}, - {file = "ijson-3.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0155a8f079c688c2ccaea05de1ad69877995c547ba3d3612c1c336edc12a3a5"}, - {file = "ijson-3.3.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ab00721304af1ae1afa4313ecfa1bf16b07f55ef91e4a5b93aeaa3e2bd7917c"}, - {file = "ijson-3.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40ee3821ee90be0f0e95dcf9862d786a7439bd1113e370736bfdf197e9765bfb"}, - {file = "ijson-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3b6987a0bc3e6d0f721b42c7a0198ef897ae50579547b0345f7f02486898f5"}, - {file = "ijson-3.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:63afea5f2d50d931feb20dcc50954e23cef4127606cc0ecf7a27128ed9f9a9e6"}, - {file = "ijson-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b5c3e285e0735fd8c5a26d177eca8b52512cdd8687ca86ec77a0c66e9c510182"}, - {file = "ijson-3.3.0-cp312-cp312-win32.whl", hash = "sha256:907f3a8674e489abdcb0206723e5560a5cb1fa42470dcc637942d7b10f28b695"}, - {file = "ijson-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f890d04ad33262d0c77ead53c85f13abfb82f2c8f078dfbf24b78f59534dfdd"}, - {file = "ijson-3.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3e8d8de44effe2dbd0d8f3eb9840344b2d5b4cc284a14eb8678aec31d1b6bea8"}, - {file = "ijson-3.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9cd5c03c63ae06d4f876b9844c5898d0044c7940ff7460db9f4cd984ac7862b5"}, - {file = "ijson-3.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04366e7e4a4078d410845e58a2987fd9c45e63df70773d7b6e87ceef771b51ee"}, - {file = "ijson-3.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de7c1ddb80fa7a3ab045266dca169004b93f284756ad198306533b792774f10a"}, - {file = "ijson-3.3.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8851584fb931cffc0caa395f6980525fd5116eab8f73ece9d95e6f9c2c326c4c"}, - {file = "ijson-3.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdcfc88347fd981e53c33d832ce4d3e981a0d696b712fbcb45dcc1a43fe65c65"}, - {file = "ijson-3.3.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3917b2b3d0dbbe3296505da52b3cb0befbaf76119b2edaff30bd448af20b5400"}, - {file = "ijson-3.3.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:e10c14535abc7ddf3fd024aa36563cd8ab5d2bb6234a5d22c77c30e30fa4fb2b"}, - {file = "ijson-3.3.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3aba5c4f97f4e2ce854b5591a8b0711ca3b0c64d1b253b04ea7b004b0a197ef6"}, - {file = "ijson-3.3.0-cp38-cp38-win32.whl", hash = "sha256:b325f42e26659df1a0de66fdb5cde8dd48613da9c99c07d04e9fb9e254b7ee1c"}, - {file = "ijson-3.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:ff835906f84451e143f31c4ce8ad73d83ef4476b944c2a2da91aec8b649570e1"}, - {file = "ijson-3.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3c556f5553368dff690c11d0a1fb435d4ff1f84382d904ccc2dc53beb27ba62e"}, - {file = "ijson-3.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e4396b55a364a03ff7e71a34828c3ed0c506814dd1f50e16ebed3fc447d5188e"}, - {file = "ijson-3.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e6850ae33529d1e43791b30575070670070d5fe007c37f5d06aebc1dd152ab3f"}, - {file = "ijson-3.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36aa56d68ea8def26778eb21576ae13f27b4a47263a7a2581ab2ef58b8de4451"}, - {file = "ijson-3.3.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7ec759c4a0fc820ad5dc6a58e9c391e7b16edcb618056baedbedbb9ea3b1524"}, - {file = "ijson-3.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b51bab2c4e545dde93cb6d6bb34bf63300b7cd06716f195dd92d9255df728331"}, - {file = "ijson-3.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:92355f95a0e4da96d4c404aa3cff2ff033f9180a9515f813255e1526551298c1"}, - {file = "ijson-3.3.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8795e88adff5aa3c248c1edce932db003d37a623b5787669ccf205c422b91e4a"}, - {file = "ijson-3.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8f83f553f4cde6d3d4eaf58ec11c939c94a0ec545c5b287461cafb184f4b3a14"}, - {file = "ijson-3.3.0-cp39-cp39-win32.whl", hash = "sha256:ead50635fb56577c07eff3e557dac39533e0fe603000684eea2af3ed1ad8f941"}, - {file = "ijson-3.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:c8a9befb0c0369f0cf5c1b94178d0d78f66d9cebb9265b36be6e4f66236076b8"}, - {file = "ijson-3.3.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2af323a8aec8a50fa9effa6d640691a30a9f8c4925bd5364a1ca97f1ac6b9b5c"}, - {file = "ijson-3.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f64f01795119880023ba3ce43072283a393f0b90f52b66cc0ea1a89aa64a9ccb"}, - {file = "ijson-3.3.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a716e05547a39b788deaf22725490855337fc36613288aa8ae1601dc8c525553"}, - {file = "ijson-3.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:473f5d921fadc135d1ad698e2697025045cd8ed7e5e842258295012d8a3bc702"}, - {file = "ijson-3.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd26b396bc3a1e85f4acebeadbf627fa6117b97f4c10b177d5779577c6607744"}, - {file = "ijson-3.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:45ff05de889f3dc3d37a59d02096948ce470699f2368b32113954818b21aa74a"}, - {file = "ijson-3.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efb521090dd6cefa7aafd120581947b29af1713c902ff54336b7c7130f04c47"}, - {file = "ijson-3.3.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87c727691858fd3a1c085d9980d12395517fcbbf02c69fbb22dede8ee03422da"}, - {file = "ijson-3.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0420c24e50389bc251b43c8ed379ab3e3ba065ac8262d98beb6735ab14844460"}, - {file = "ijson-3.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8fdf3721a2aa7d96577970f5604bd81f426969c1822d467f07b3d844fa2fecc7"}, - {file = "ijson-3.3.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:891f95c036df1bc95309951940f8eea8537f102fa65715cdc5aae20b8523813b"}, - {file = "ijson-3.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed1336a2a6e5c427f419da0154e775834abcbc8ddd703004108121c6dd9eba9d"}, - {file = "ijson-3.3.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0c819f83e4f7b7f7463b2dc10d626a8be0c85fbc7b3db0edc098c2b16ac968e"}, - {file = "ijson-3.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33afc25057377a6a43c892de34d229a86f89ea6c4ca3dd3db0dcd17becae0dbb"}, - {file = "ijson-3.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7914d0cf083471856e9bc2001102a20f08e82311dfc8cf1a91aa422f9414a0d6"}, - {file = "ijson-3.3.0.tar.gz", hash = "sha256:7f172e6ba1bee0d4c8f8ebd639577bfe429dee0f3f96775a067b8bae4492d8a0"}, -] - -[[package]] -name = "imagesize" -version = "1.4.1" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -summary = "Getting image size from png/jpeg/jpeg2000/gif file" -groups = ["docs"] -files = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, -] - -[[package]] -name = "importlib-metadata" -version = "8.0.0" -requires_python = ">=3.8" -summary = "Read metadata from Python packages" -groups = ["default", "dev-contrib", "docs", "full", "opentelemetry", "sqlalchemy"] -dependencies = [ - "typing-extensions>=3.6.4; python_version < \"3.8\"", - "zipp>=0.5", -] -files = [ - {file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"}, - {file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"}, -] - -[[package]] -name = "importlib-resources" -version = "6.4.4" -requires_python = ">=3.8" -summary = "Read resources from Python packages" -groups = ["default", "docs", "full", "sqlalchemy"] -marker = "python_version < \"3.9\"" -dependencies = [ - "zipp>=3.1.0; python_version < \"3.10\"", -] -files = [ - {file = "importlib_resources-6.4.4-py3-none-any.whl", hash = "sha256:dda242603d1c9cd836c3368b1174ed74cb4049ecd209e7a1a0104620c18c5c11"}, - {file = "importlib_resources-6.4.4.tar.gz", hash = "sha256:20600c8b7361938dc0bb2d5ec0297802e575df486f5a544fa414da65e13721f7"}, -] - -[[package]] -name = "incremental" -version = "24.7.2" -requires_python = ">=3.8" -summary = "A small library that versions your Python projects." -groups = ["dev"] -dependencies = [ - "setuptools>=61.0", - "tomli; python_version < \"3.11\"", -] -files = [ - {file = "incremental-24.7.2-py3-none-any.whl", hash = "sha256:8cb2c3431530bec48ad70513931a760f446ad6c25e8333ca5d95e24b0ed7b8fe"}, - {file = "incremental-24.7.2.tar.gz", hash = "sha256:fb4f1d47ee60efe87d4f6f0ebb5f70b9760db2b2574c59c8e8912be4ebd464c9"}, -] - -[[package]] -name = "inflection" -version = "0.5.1" -requires_python = ">=3.5" -summary = "A port of Ruby on Rails inflector to Python" -groups = ["full", "piccolo"] -files = [ - {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"}, - {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, -] - -[[package]] -name = "iniconfig" -version = "2.0.0" -requires_python = ">=3.7" -summary = "brain-dead simple config-ini parsing" -groups = ["test"] -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "jinja2" -version = "3.1.4" -requires_python = ">=3.7" -summary = "A very fast and expressive template engine." -groups = ["docs", "full", "jinja", "piccolo", "standard"] -dependencies = [ - "MarkupSafe>=2.0", -] -files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, -] - -[[package]] -name = "jsbeautifier" -version = "1.15.1" -summary = "JavaScript unobfuscator and beautifier." -groups = ["cli", "full", "standard"] -dependencies = [ - "editorconfig>=0.12.2", - "six>=1.13.0", -] -files = [ - {file = "jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24"}, -] - -[[package]] -name = "lazy-model" -version = "0.2.0" -requires_python = ">=3.7,<4.0" -summary = "" -groups = ["dev"] -dependencies = [ - "pydantic>=1.9.0", -] -files = [ - {file = "lazy-model-0.2.0.tar.gz", hash = "sha256:57c0e91e171530c4fca7aebc3ac05a163a85cddd941bf7527cc46c0ddafca47c"}, - {file = "lazy_model-0.2.0-py3-none-any.whl", hash = "sha256:5a3241775c253e36d9069d236be8378288a93d4fc53805211fd152e04cc9c342"}, -] - -[[package]] -name = "litestar-sphinx-theme" -version = "0.2.0" -requires_python = ">=3.8,<4.0" -git = "https://github.com/litestar-org/litestar-sphinx-theme.git" -revision = "76b1d0e4c8afff1ad135b1917fe09cf6c1cc6c9b" -summary = "A Sphinx theme for the Litestar organization" -groups = ["docs"] -dependencies = [ - "pydata-sphinx-theme<1.0.0,>=0.13.3", - "sphinx-design<1.0.0,>=0.3.0", -] - -[[package]] -name = "livereload" -version = "2.7.0" -requires_python = ">=3.7" -summary = "Python LiveReload is an awesome tool for web developers" -groups = ["docs"] -dependencies = [ - "tornado", -] -files = [ - {file = "livereload-2.7.0-py3-none-any.whl", hash = "sha256:19bee55aff51d5ade6ede0dc709189a0f904d3b906d3ea71641ed548acff3246"}, - {file = "livereload-2.7.0.tar.gz", hash = "sha256:f4ba199ef93248902841e298670eebfe1aa9e148e19b343bc57dbf1b74de0513"}, -] - -[[package]] -name = "mako" -version = "1.3.5" -requires_python = ">=3.8" -summary = "A super-fast templating language that borrows the best ideas from the existing templating languages." -groups = ["full", "mako", "sqlalchemy"] -dependencies = [ - "MarkupSafe>=0.9.2", -] -files = [ - {file = "Mako-1.3.5-py3-none-any.whl", hash = "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a"}, - {file = "Mako-1.3.5.tar.gz", hash = "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc"}, -] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -requires_python = ">=3.8" -summary = "Python port of markdown-it. Markdown parsing, done right!" -groups = ["default"] -dependencies = [ - "mdurl~=0.1", -] -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[[package]] -name = "markupsafe" -version = "2.1.5" -requires_python = ">=3.7" -summary = "Safely add untrusted strings to HTML/XML markup." -groups = ["docs", "full", "jinja", "mako", "piccolo", "sqlalchemy", "standard"] -files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -requires_python = ">=3.7" -summary = "Markdown URL utilities" -groups = ["default"] -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "minijinja" -version = "2.0.1" -requires_python = ">=3.8" -summary = "An experimental Python binding of the Rust MiniJinja template engine." -groups = ["full", "minijinja"] -files = [ - {file = "minijinja-2.0.1-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:063b291cb31f5c33eb77bb4cb457f67f14426ca1418232b8ae9f267155d330cc"}, - {file = "minijinja-2.0.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a4e9d639dd89ce7fef86e82147082ab3c248a36950fa3fbe793685ba322c1b7"}, - {file = "minijinja-2.0.1-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a20373af4ee5430356c196c7fe5f19e3261a4fa16c944542b4de7a2349bac7a6"}, - {file = "minijinja-2.0.1-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ade637bf4826258811a785ccc4e5d41cd2bdf4ec317b1ed3daa4dbbdd020f37d"}, - {file = "minijinja-2.0.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5ec956d777e0fee8e214af48363334c04f098e986038a9e8cb92a0564f81943"}, - {file = "minijinja-2.0.1-cp38-abi3-win32.whl", hash = "sha256:039f4d1a1a73f90917cff1ed7c617eb56e2b2f91bbbdc551adaa448e1673e5c2"}, - {file = "minijinja-2.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:dca5d7689905dce340e36e47348b505c788daf297253b85a1aff506ea63ad1b8"}, - {file = "minijinja-2.0.1.tar.gz", hash = "sha256:e774beffebfb8a1ad17e638ef70917cf5e94593f79acb8a8fff7d983169f3a4e"}, -] - -[[package]] -name = "more-itertools" -version = "10.4.0" -requires_python = ">=3.8" -summary = "More routines for operating on iterables, beyond itertools" -groups = ["docs"] -files = [ - {file = "more-itertools-10.4.0.tar.gz", hash = "sha256:fe0e63c4ab068eac62410ab05cccca2dc71ec44ba8ef29916a0090df061cf923"}, - {file = "more_itertools-10.4.0-py3-none-any.whl", hash = "sha256:0f7d9f83a0a8dcfa8a2694a770590d98a67ea943e3d9f5298309a484758c4e27"}, -] - -[[package]] -name = "motor" -version = "3.5.1" -requires_python = ">=3.8" -summary = "Non-blocking MongoDB driver for Tornado or asyncio" -groups = ["dev"] -dependencies = [ - "pymongo<5,>=4.5", -] -files = [ - {file = "motor-3.5.1-py3-none-any.whl", hash = "sha256:f95a9ea0f011464235e0bd72910baa291db3a6009e617ac27b82f57885abafb8"}, - {file = "motor-3.5.1.tar.gz", hash = "sha256:1622bd7b39c3e6375607c14736f6e1d498128eadf6f5f93f8786cf17d37062ac"}, -] - -[[package]] -name = "msgpack" -version = "1.0.8" -requires_python = ">=3.8" -summary = "MessagePack serializer" -groups = ["docs"] -files = [ - {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868"}, - {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c"}, - {file = "msgpack-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659"}, - {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2"}, - {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982"}, - {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa"}, - {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128"}, - {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d"}, - {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653"}, - {file = "msgpack-1.0.8-cp310-cp310-win32.whl", hash = "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693"}, - {file = "msgpack-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce"}, - {file = "msgpack-1.0.8-cp311-cp311-win32.whl", hash = "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305"}, - {file = "msgpack-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543"}, - {file = "msgpack-1.0.8-cp312-cp312-win32.whl", hash = "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c"}, - {file = "msgpack-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd"}, - {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40"}, - {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151"}, - {file = "msgpack-1.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24"}, - {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d"}, - {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db"}, - {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77"}, - {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13"}, - {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2"}, - {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a"}, - {file = "msgpack-1.0.8-cp38-cp38-win32.whl", hash = "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c"}, - {file = "msgpack-1.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480"}, - {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a"}, - {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596"}, - {file = "msgpack-1.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d"}, - {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f"}, - {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228"}, - {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18"}, - {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8"}, - {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746"}, - {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, - {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, - {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, - {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, -] - -[[package]] -name = "msgspec" -version = "0.18.6" -requires_python = ">=3.8" -summary = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML." -groups = ["default"] -files = [ - {file = "msgspec-0.18.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77f30b0234eceeff0f651119b9821ce80949b4d667ad38f3bfed0d0ebf9d6d8f"}, - {file = "msgspec-0.18.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a76b60e501b3932782a9da039bd1cd552b7d8dec54ce38332b87136c64852dd"}, - {file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06acbd6edf175bee0e36295d6b0302c6de3aaf61246b46f9549ca0041a9d7177"}, - {file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40a4df891676d9c28a67c2cc39947c33de516335680d1316a89e8f7218660410"}, - {file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a6896f4cd5b4b7d688018805520769a8446df911eb93b421c6c68155cdf9dd5a"}, - {file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3ac4dd63fd5309dd42a8c8c36c1563531069152be7819518be0a9d03be9788e4"}, - {file = "msgspec-0.18.6-cp310-cp310-win_amd64.whl", hash = "sha256:fda4c357145cf0b760000c4ad597e19b53adf01382b711f281720a10a0fe72b7"}, - {file = "msgspec-0.18.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e77e56ffe2701e83a96e35770c6adb655ffc074d530018d1b584a8e635b4f36f"}, - {file = "msgspec-0.18.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5351afb216b743df4b6b147691523697ff3a2fc5f3d54f771e91219f5c23aaa"}, - {file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3232fabacef86fe8323cecbe99abbc5c02f7698e3f5f2e248e3480b66a3596b"}, - {file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b524df6ea9998bbc99ea6ee4d0276a101bcc1aa8d14887bb823914d9f60d07"}, - {file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:37f67c1d81272131895bb20d388dd8d341390acd0e192a55ab02d4d6468b434c"}, - {file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0feb7a03d971c1c0353de1a8fe30bb6579c2dc5ccf29b5f7c7ab01172010492"}, - {file = "msgspec-0.18.6-cp311-cp311-win_amd64.whl", hash = "sha256:41cf758d3f40428c235c0f27bc6f322d43063bc32da7b9643e3f805c21ed57b4"}, - {file = "msgspec-0.18.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d86f5071fe33e19500920333c11e2267a31942d18fed4d9de5bc2fbab267d28c"}, - {file = "msgspec-0.18.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce13981bfa06f5eb126a3a5a38b1976bddb49a36e4f46d8e6edecf33ccf11df1"}, - {file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97dec6932ad5e3ee1e3c14718638ba333befc45e0661caa57033cd4cc489466"}, - {file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad237100393f637b297926cae1868b0d500f764ccd2f0623a380e2bcfb2809ca"}, - {file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db1d8626748fa5d29bbd15da58b2d73af25b10aa98abf85aab8028119188ed57"}, - {file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d70cb3d00d9f4de14d0b31d38dfe60c88ae16f3182988246a9861259c6722af6"}, - {file = "msgspec-0.18.6-cp312-cp312-win_amd64.whl", hash = "sha256:1003c20bfe9c6114cc16ea5db9c5466e49fae3d7f5e2e59cb70693190ad34da0"}, - {file = "msgspec-0.18.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f7d9faed6dfff654a9ca7d9b0068456517f63dbc3aa704a527f493b9200b210a"}, - {file = "msgspec-0.18.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9da21f804c1a1471f26d32b5d9bc0480450ea77fbb8d9db431463ab64aaac2cf"}, - {file = "msgspec-0.18.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46eb2f6b22b0e61c137e65795b97dc515860bf6ec761d8fb65fdb62aa094ba61"}, - {file = "msgspec-0.18.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8355b55c80ac3e04885d72db515817d9fbb0def3bab936bba104e99ad22cf46"}, - {file = "msgspec-0.18.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9080eb12b8f59e177bd1eb5c21e24dd2ba2fa88a1dbc9a98e05ad7779b54c681"}, - {file = "msgspec-0.18.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cc001cf39becf8d2dcd3f413a4797c55009b3a3cdbf78a8bf5a7ca8fdb76032c"}, - {file = "msgspec-0.18.6-cp38-cp38-win_amd64.whl", hash = "sha256:fac5834e14ac4da1fca373753e0c4ec9c8069d1fe5f534fa5208453b6065d5be"}, - {file = "msgspec-0.18.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:974d3520fcc6b824a6dedbdf2b411df31a73e6e7414301abac62e6b8d03791b4"}, - {file = "msgspec-0.18.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fd62e5818731a66aaa8e9b0a1e5543dc979a46278da01e85c3c9a1a4f047ef7e"}, - {file = "msgspec-0.18.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7481355a1adcf1f08dedd9311193c674ffb8bf7b79314b4314752b89a2cf7f1c"}, - {file = "msgspec-0.18.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6aa85198f8f154cf35d6f979998f6dadd3dc46a8a8c714632f53f5d65b315c07"}, - {file = "msgspec-0.18.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e24539b25c85c8f0597274f11061c102ad6b0c56af053373ba4629772b407be"}, - {file = "msgspec-0.18.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c61ee4d3be03ea9cd089f7c8e36158786cd06e51fbb62529276452bbf2d52ece"}, - {file = "msgspec-0.18.6-cp39-cp39-win_amd64.whl", hash = "sha256:b5c390b0b0b7da879520d4ae26044d74aeee5144f83087eb7842ba59c02bc090"}, - {file = "msgspec-0.18.6.tar.gz", hash = "sha256:a59fc3b4fcdb972d09138cb516dbde600c99d07c38fd9372a6ef500d2d031b4e"}, -] - -[[package]] -name = "multidict" -version = "6.0.5" -requires_python = ">=3.7" -summary = "multidict implementation" -groups = ["default"] -files = [ - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, - {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, - {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, - {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, - {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, - {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, - {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, - {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, - {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, - {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, - {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, - {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, - {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, -] - -[[package]] -name = "mypy" -version = "1.11.1" -requires_python = ">=3.8" -summary = "Optional static typing for Python" -groups = ["linting"] -dependencies = [ - "mypy-extensions>=1.0.0", - "tomli>=1.1.0; python_version < \"3.11\"", - "typing-extensions>=4.6.0", -] -files = [ - {file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"}, - {file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"}, - {file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"}, - {file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"}, - {file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"}, - {file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"}, - {file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"}, - {file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"}, - {file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"}, - {file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"}, - {file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"}, - {file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"}, - {file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"}, - {file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"}, - {file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"}, - {file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"}, - {file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"}, - {file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"}, - {file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"}, - {file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"}, - {file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"}, - {file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"}, - {file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"}, - {file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"}, - {file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"}, - {file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"}, - {file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"}, -] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -requires_python = ">=3.5" -summary = "Type system extensions for programs checked with the mypy type checker." -groups = ["full", "linting", "piccolo"] -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "natsort" -version = "8.4.0" -requires_python = ">=3.7" -summary = "Simple yet flexible natural sorting in Python." -groups = ["docs"] -files = [ - {file = "natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c"}, - {file = "natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581"}, -] - -[[package]] -name = "nodeenv" -version = "1.9.1" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -summary = "Node.js virtual environment builder" -groups = ["linting"] -files = [ - {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, - {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, -] - -[[package]] -name = "opentelemetry-api" -version = "1.26.0" -requires_python = ">=3.8" -summary = "OpenTelemetry Python API" -groups = ["dev-contrib", "full", "opentelemetry"] -dependencies = [ - "deprecated>=1.2.6", - "importlib-metadata<=8.0.0,>=6.0", -] -files = [ - {file = "opentelemetry_api-1.26.0-py3-none-any.whl", hash = "sha256:7d7ea33adf2ceda2dd680b18b1677e4152000b37ca76e679da71ff103b943064"}, - {file = "opentelemetry_api-1.26.0.tar.gz", hash = "sha256:2bd639e4bed5b18486fef0b5a520aaffde5a18fc225e808a1ac4df363f43a1ce"}, -] - -[[package]] -name = "opentelemetry-instrumentation" -version = "0.47b0" -requires_python = ">=3.8" -summary = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" -groups = ["full", "opentelemetry"] -dependencies = [ - "opentelemetry-api~=1.4", - "setuptools>=16.0", - "wrapt<2.0.0,>=1.0.0", -] -files = [ - {file = "opentelemetry_instrumentation-0.47b0-py3-none-any.whl", hash = "sha256:88974ee52b1db08fc298334b51c19d47e53099c33740e48c4f084bd1afd052d5"}, - {file = "opentelemetry_instrumentation-0.47b0.tar.gz", hash = "sha256:96f9885e450c35e3f16a4f33145f2ebf620aea910c9fd74a392bbc0f807a350f"}, -] - -[[package]] -name = "opentelemetry-instrumentation-asgi" -version = "0.47b0" -requires_python = ">=3.8" -summary = "ASGI instrumentation for OpenTelemetry" -groups = ["full", "opentelemetry"] -dependencies = [ - "asgiref~=3.0", - "opentelemetry-api~=1.12", - "opentelemetry-instrumentation==0.47b0", - "opentelemetry-semantic-conventions==0.47b0", - "opentelemetry-util-http==0.47b0", -] -files = [ - {file = "opentelemetry_instrumentation_asgi-0.47b0-py3-none-any.whl", hash = "sha256:b798dc4957b3edc9dfecb47a4c05809036a4b762234c5071212fda39ead80ade"}, - {file = "opentelemetry_instrumentation_asgi-0.47b0.tar.gz", hash = "sha256:e78b7822c1bca0511e5e9610ec484b8994a81670375e570c76f06f69af7c506a"}, -] - -[[package]] -name = "opentelemetry-sdk" -version = "1.26.0" -requires_python = ">=3.8" -summary = "OpenTelemetry Python SDK" -groups = ["dev-contrib"] -dependencies = [ - "opentelemetry-api==1.26.0", - "opentelemetry-semantic-conventions==0.47b0", - "typing-extensions>=3.7.4", -] -files = [ - {file = "opentelemetry_sdk-1.26.0-py3-none-any.whl", hash = "sha256:feb5056a84a88670c041ea0ded9921fca559efec03905dddeb3885525e0af897"}, - {file = "opentelemetry_sdk-1.26.0.tar.gz", hash = "sha256:c90d2868f8805619535c05562d699e2f4fb1f00dbd55a86dcefca4da6fa02f85"}, -] - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.47b0" -requires_python = ">=3.8" -summary = "OpenTelemetry Semantic Conventions" -groups = ["dev-contrib", "full", "opentelemetry"] -dependencies = [ - "deprecated>=1.2.6", - "opentelemetry-api==1.26.0", -] -files = [ - {file = "opentelemetry_semantic_conventions-0.47b0-py3-none-any.whl", hash = "sha256:4ff9d595b85a59c1c1413f02bba320ce7ea6bf9e2ead2b0913c4395c7bbc1063"}, - {file = "opentelemetry_semantic_conventions-0.47b0.tar.gz", hash = "sha256:a8d57999bbe3495ffd4d510de26a97dadc1dace53e0275001b2c1b2f67992a7e"}, -] - -[[package]] -name = "opentelemetry-util-http" -version = "0.47b0" -requires_python = ">=3.8" -summary = "Web util for OpenTelemetry" -groups = ["full", "opentelemetry"] -files = [ - {file = "opentelemetry_util_http-0.47b0-py3-none-any.whl", hash = "sha256:3d3215e09c4a723b12da6d0233a31395aeb2bb33a64d7b15a1500690ba250f19"}, - {file = "opentelemetry_util_http-0.47b0.tar.gz", hash = "sha256:352a07664c18eef827eb8ddcbd64c64a7284a39dd1655e2f16f577eb046ccb32"}, -] - -[[package]] -name = "outcome" -version = "1.3.0.post0" -requires_python = ">=3.7" -summary = "Capture the outcome of Python function calls." -groups = ["dev"] -dependencies = [ - "attrs>=19.2.0", -] -files = [ - {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, - {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, -] - -[[package]] -name = "packaging" -version = "24.1" -requires_python = ">=3.8" -summary = "Core utilities for Python packages" -groups = ["docs", "full", "piccolo", "test"] -files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -requires_python = ">=3.8" -summary = "Utility library for gitignore style pattern matching of file paths." -groups = ["full", "piccolo"] -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "piccolo" -version = "1.17.0" -requires_python = ">=3.8.0" -summary = "A fast, user friendly ORM and query builder which supports asyncio." -groups = ["full", "piccolo"] -dependencies = [ - "Jinja2>=2.11.0", - "black", - "colorama>=0.4.0", - "inflection>=0.5.1", - "pydantic[email]==2.*", - "targ>=0.3.7", - "typing-extensions>=4.3.0", -] -files = [ - {file = "piccolo-1.17.0-py3-none-any.whl", hash = "sha256:b28867d6bde3b77161d3759e975afc5f6b042412439351e182c2a25a5b83f1e8"}, - {file = "piccolo-1.17.0.tar.gz", hash = "sha256:8be0f35e12c9df33c0c121d77d1c1ae797dbce5741dab7cba535cf8b09360f89"}, -] - -[[package]] -name = "picologging" -version = "0.9.3" -requires_python = ">=3.7" -summary = "A fast and lightweight logging library for Python" -groups = ["full", "picologging"] -files = [ - {file = "picologging-0.9.3-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:3d3219765be2430c4e434ff66a90147bd29db56cd24be00c5999d9827b08d479"}, - {file = "picologging-0.9.3-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:1c4755c06c37b53e70ebdc9cd2b39c835221a12c5ef149cfaad9b6073b41ee89"}, - {file = "picologging-0.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b582222a768f2bc28ca4a34aa41a008c6aa3430cf1f128b2440e93f691d0d714"}, - {file = "picologging-0.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f833de82e5fd9a981f64ebfadc3f4d12fe75ef31897e796ba04f6e28455e42c9"}, - {file = "picologging-0.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b21e46794ef29fda0033a2bcfc38ec7a5fea78a6353a5f74f7a6374c0c0d3d15"}, - {file = "picologging-0.9.3-cp310-cp310-win32.whl", hash = "sha256:3ef12744c17fb670e5028315019589fe21527d1546463ef9baf9a454dcd3d7c4"}, - {file = "picologging-0.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:09f27425175ae23cca0fb49bb55622073589cb57f73bdd2c7f72f4cf05419c3e"}, - {file = "picologging-0.9.3-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:fbd88e3dde71d72ac72dfe6a23a52e13fcdf2c9cf033e13a8f2718746fed942b"}, - {file = "picologging-0.9.3-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:542628ccdc7a1eb321e5a7533803577a943c7d84e62456b681037f7bbc8abe26"}, - {file = "picologging-0.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78ed29eea9f28d28edb39fa81b448fd27bdf510d4e4c79fe64e08742c782a965"}, - {file = "picologging-0.9.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a38eb07ef48218712ce9f860b4a27e5086a89a89af7c5bcc1eaed6fb1e3eafd"}, - {file = "picologging-0.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3495969c6a8e1e2fa8d2bc89b8478bce10716bb6617f50822667909e79478cb4"}, - {file = "picologging-0.9.3-cp311-cp311-win32.whl", hash = "sha256:c97f0ab43ee32924b33cadd176c5bfe404e85e2bc6ae5e4c713c6d4cb4c7e6f3"}, - {file = "picologging-0.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:f8d448e063b8a2cbe3c76d2d7ca2beb523779a31a8b14ee0a452ec1247ce56b8"}, - {file = "picologging-0.9.3-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:70e6957df044af10ff35d293b8ebad3f04bbfa99f88bcf4fd2a8248d1cd46320"}, - {file = "picologging-0.9.3-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:32d18fb7c089afe9c2a8cb1493021417c872f807d74f3fd6da85e43226664ccd"}, - {file = "picologging-0.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:025a6262280374413142648cf5be7944136c54a5cc84b0ff5b2416928d7aec96"}, - {file = "picologging-0.9.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:feaf33a2fd8a9431f3f4de9693569952ebce6e4c92cabe642327130a1d378b6c"}, - {file = "picologging-0.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06d315458ff92a2df6ce07540b390212d179e3ef2d08b48c356df72be8d0ce2d"}, - {file = "picologging-0.9.3-cp312-cp312-win32.whl", hash = "sha256:502a17cbf7303499e1edcb01b3503caeac204aa5d5f888d4b5ecdb113f0c25ee"}, - {file = "picologging-0.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:16f111cdf7a210eb07bd06932c9a8ff06dc830e213c8a0efbb769e586d1f3efb"}, - {file = "picologging-0.9.3-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:6672feda1d95e81694b447c27ce51d0beec86f779ed6803e2b4a522086ac7765"}, - {file = "picologging-0.9.3-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:94f71cf9a868047db5414f8358c7df51f3c1913a8f1e892f79ad334e63c85860"}, - {file = "picologging-0.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:936a23a8b98fac51a0bfc3ec23dd2c156fc6b8839a522287ca295f59a5c6dfef"}, - {file = "picologging-0.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce7aad1ccc61d65bafcfda7429d902bce53a706cd8b1ade48a4eb80900f0654e"}, - {file = "picologging-0.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251d69cf4f9d8e7f9cfb113a58d37d493555ee6380af5e4e11599c4f4b60f735"}, - {file = "picologging-0.9.3-cp38-cp38-win32.whl", hash = "sha256:f66744aa21d4741ba5e6ecc21babaa3b2cb4dc6e944a02cd5d428ebf24992751"}, - {file = "picologging-0.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:ccc190c6eb8e75fd6ce3e7a34f923731ad5a8c58e26e5a83cbc922174cc79e3b"}, - {file = "picologging-0.9.3-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:b4c7f4bf9a5fa291e0f8f8b669f6ea35943ccf2a962903472591ae377de58825"}, - {file = "picologging-0.9.3-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:ea5663f5907910405edf75d438c05b24b66fd23cb997d7d28dfaabdea9b78211"}, - {file = "picologging-0.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a629db4cbbb96e66d01d34c93d197c04cb71838830c6652033ee8d2ebe76d01f"}, - {file = "picologging-0.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:759f21dda1bf9ee52c7fa91c1faf47aa3585f4f604d5d38de02f3bcd32d7369f"}, - {file = "picologging-0.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d4bb95e708ad973217cc2cbb6e058f777ae2539a5cf131824c66fd701adfe71"}, - {file = "picologging-0.9.3-cp39-cp39-win32.whl", hash = "sha256:35e11311c71678813112e03649fa8dacad2f4ee5c3a424e515876df96c66b89b"}, - {file = "picologging-0.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:e1ab2768f4c178df653bd6cc94ca31d9b94c6d4a90012598a185e80ea66f1a8f"}, - {file = "picologging-0.9.3.tar.gz", hash = "sha256:6921f86ea0875ac85e252188627e9f04b872895327a7028e06c825ddb888a825"}, -] - -[[package]] -name = "platformdirs" -version = "4.2.2" -requires_python = ">=3.8" -summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -groups = ["docs", "full", "linting", "piccolo"] -files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, -] - -[[package]] -name = "pluggy" -version = "1.5.0" -requires_python = ">=3.8" -summary = "plugin and hook calling mechanisms for python" -groups = ["test"] -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[[package]] -name = "polyfactory" -version = "2.16.2" -requires_python = "<4.0,>=3.8" -summary = "Mock data generation factories" -groups = ["default"] -dependencies = [ - "faker", - "typing-extensions>=4.6.0", -] -files = [ - {file = "polyfactory-2.16.2-py3-none-any.whl", hash = "sha256:e5eaf97358fee07d0d8de86a93e81dc56e3be1e1514d145fea6c5f486cda6ea1"}, - {file = "polyfactory-2.16.2.tar.gz", hash = "sha256:6d0d90deb85e5bb1733ea8744c2d44eea2b31656e11b4fa73832d2e2ab5422da"}, -] - -[[package]] -name = "pre-commit" -version = "3.5.0" -requires_python = ">=3.8" -summary = "A framework for managing and maintaining multi-language pre-commit hooks." -groups = ["linting"] -dependencies = [ - "cfgv>=2.0.0", - "identify>=1.0.0", - "nodeenv>=0.11.1", - "pyyaml>=5.1", - "virtualenv>=20.10.0", -] -files = [ - {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, - {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, -] - -[[package]] -name = "priority" -version = "2.0.0" -requires_python = ">=3.6.1" -summary = "A pure-Python implementation of the HTTP/2 priority tree" -groups = ["dev"] -files = [ - {file = "priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa"}, - {file = "priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0"}, -] - -[[package]] -name = "prometheus-client" -version = "0.20.0" -requires_python = ">=3.8" -summary = "Python client for the Prometheus monitoring system." -groups = ["full", "prometheus"] -files = [ - {file = "prometheus_client-0.20.0-py3-none-any.whl", hash = "sha256:cde524a85bce83ca359cc837f28b8c0db5cac7aa653a588fd7e84ba061c329e7"}, - {file = "prometheus_client-0.20.0.tar.gz", hash = "sha256:287629d00b147a32dcb2be0b9df905da599b2d82f80377083ec8463309a4bb89"}, -] - -[[package]] -name = "psutil" -version = "6.0.0" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -summary = "Cross-platform lib for process and system monitoring in Python." -groups = ["dev"] -files = [ - {file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"}, - {file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"}, - {file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"}, - {file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"}, - {file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"}, -] - -[[package]] -name = "psycopg" -version = "3.1.20" -requires_python = ">=3.7" -summary = "PostgreSQL database adapter for Python" -groups = ["dev"] -dependencies = [ - "backports-zoneinfo>=0.2.0; python_version < \"3.9\"", - "typing-extensions>=4.1", - "tzdata; sys_platform == \"win32\"", -] -files = [ - {file = "psycopg-3.1.20-py3-none-any.whl", hash = "sha256:898a29f49ac9c903d554f5a6cdc44a8fc564325557c18f82e51f39c1f4fc2aeb"}, - {file = "psycopg-3.1.20.tar.gz", hash = "sha256:32f5862ab79f238496236f97fe374a7ab55b4b4bb839a74802026544735f9a07"}, -] - -[[package]] -name = "psycopg-binary" -version = "3.1.20" -requires_python = ">=3.7" -summary = "PostgreSQL database adapter for Python -- C optimisation distribution" -groups = ["dev"] -marker = "implementation_name != \"pypy\"" -files = [ - {file = "psycopg_binary-3.1.20-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:8dadeddb9d2dced49f2371f222db1d78b0a1c0f515c6e9c9e65c8f958c288ce1"}, - {file = "psycopg_binary-3.1.20-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:67f285eaf706712d1ac46f4a7fc27226ee6184f411e45aff4044284ac34fe3a3"}, - {file = "psycopg_binary-3.1.20-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da1b831f0a33e69bf79c4f39587167720c58c046d46ad86232f12c3e17e7c865"}, - {file = "psycopg_binary-3.1.20-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c067284df02ea7bcede5f89cc1ed76511ceaf7e560e0f79528125f1a3ef38832"}, - {file = "psycopg_binary-3.1.20-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8dbff9808ba07fba4afa0a6c823ab411f1cf9f1e27ea684bd307ed268f61a39"}, - {file = "psycopg_binary-3.1.20-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808828fc485f23082f974811cad8aa75120a6dde248453c4fba60e8780bf1841"}, - {file = "psycopg_binary-3.1.20-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:eb8479dd184b2e6bbf8aae52ca946efff0d852b2ead386c26fa6de8c92257a9b"}, - {file = "psycopg_binary-3.1.20-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:78b5932f0f6f97e143272fea16753ecd9a00cb65db2c60ac3710bea6e739e09d"}, - {file = "psycopg_binary-3.1.20-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1623073d3f6449223ec4843cf4e36d05258567d93284f9a9b97618a87b2ae4"}, - {file = "psycopg_binary-3.1.20-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c26863471abba88396281649df34dd29e70f37d695af73ef98a4a9038bbff674"}, - {file = "psycopg_binary-3.1.20-cp310-cp310-win_amd64.whl", hash = "sha256:bfc5955e3035f141a567ccc608ba65d01b97f9179ba8061f4b7ce80fe0edb327"}, - {file = "psycopg_binary-3.1.20-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:802989350fcbc783732bfef660afb34439a62727642a05e8bb9acf7d68993627"}, - {file = "psycopg_binary-3.1.20-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:01b0e39128715fc37fed6cdc50ab58278eacb75709af503eb607654030975f09"}, - {file = "psycopg_binary-3.1.20-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77af1086bedfa0729465565c636de3519079ba523d7b7ee6e8b9486beb1ee905"}, - {file = "psycopg_binary-3.1.20-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9b9562395d441e225f354e8c6303ee6993a93aaeb0dbb5b94368f3249ab2388"}, - {file = "psycopg_binary-3.1.20-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e814d69e5447a93e7b98117ec95a8ce606d3742092fd120960551ed67c376fea"}, - {file = "psycopg_binary-3.1.20-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf1c2061600235ae9b11d7ad357cab89ac583a76bdb0199f7a29ac947939c20"}, - {file = "psycopg_binary-3.1.20-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:50f1d807b4167f973a6f67bca39bf656b737f7426be158a1dc9cb0000d020744"}, - {file = "psycopg_binary-3.1.20-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4cf6ec1490232a5b208dae94a8269dc739e6762684c8658a0f3570402db934ae"}, - {file = "psycopg_binary-3.1.20-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:309c09ec50a9c5c8492c2922ee666df1e30a08b08a9b63083d0daa414eccd09c"}, - {file = "psycopg_binary-3.1.20-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e2c33a01799f93ef8c11a023df66280e39ca3c3249a2581adb2a0e5e80801088"}, - {file = "psycopg_binary-3.1.20-cp311-cp311-win_amd64.whl", hash = "sha256:2c67532057fda72579b02d9d61e9cc8975982844bd5c3c9dc7f84ce8bcac859c"}, - {file = "psycopg_binary-3.1.20-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:ef08de60f1b8503a6f6b6f5bee612de36373c09bc0e3f84409fab09e1ff72107"}, - {file = "psycopg_binary-3.1.20-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a4847fa31c8d3a6dd3536cf1e130dfcc454ed26be471ef274e4358bf7f709cda"}, - {file = "psycopg_binary-3.1.20-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b72e9c8c79dcc30e34e996079cfe0374b7c7233d2b5f6f25a0bc8872fe2babef"}, - {file = "psycopg_binary-3.1.20-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836246f3c486ef7edfce6cf6cc760173e244826ebecd54c1b63c91d4cc0341f7"}, - {file = "psycopg_binary-3.1.20-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:015f70b17539ec0ecfb0f87bcaface0c7fa1289b6e7e2313dc7cdfdc513e3235"}, - {file = "psycopg_binary-3.1.20-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f52498dc7b41fee74e971823ede4519e3a9597d416f7a2044dbe4b98cc61ff35"}, - {file = "psycopg_binary-3.1.20-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:92b61bae0ac881580faa1c89bf2167db7041cb01cc0bd686244f9c20a010036a"}, - {file = "psycopg_binary-3.1.20-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3532b8677666aadb64a4e31f6e97fe4ab71b862ab100d337faf497198339fd4d"}, - {file = "psycopg_binary-3.1.20-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f7df27f50a7db84c28e58be3df41f39618161096c3379ad68bc665a454c53e93"}, - {file = "psycopg_binary-3.1.20-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12b33c511f0be79d5a68231a10972ef9c68d954d30d176679472057ecc22891a"}, - {file = "psycopg_binary-3.1.20-cp312-cp312-win_amd64.whl", hash = "sha256:6f3c0b05fc3cbd4d99aaacf5c7afa13b086df5777b9fefb78d31bf81fc70bd04"}, - {file = "psycopg_binary-3.1.20-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:f1c78e40ba9a808b6f870f94efc3cfbf479169bf6c4f46c2b1e258a4b035b2ba"}, - {file = "psycopg_binary-3.1.20-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44306e4b1acef590dc063d63317dc0ac34fce89756723efd22bd770c1a04850c"}, - {file = "psycopg_binary-3.1.20-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8c72fe67722ab78c7f4466c79539247306cde260367a4ac42e6302c26a7d6d2"}, - {file = "psycopg_binary-3.1.20-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e1f9fa0a6404c7e405bed4f3237e4b4e9292d711deff0d870dcf66f87f0aad7"}, - {file = "psycopg_binary-3.1.20-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1851c4ed763969a613246024d37153357308eeb78889dcd6d739b7240dacd4e"}, - {file = "psycopg_binary-3.1.20-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4fc1a6bc9cf8c23d87f3c3f79517b0ee15789f183ef84d077d68c5e1fad4677a"}, - {file = "psycopg_binary-3.1.20-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:b0d3ee6ae9760545cc78d03eaa858898cc40a59ca4cc2047f198cac2d1a000cc"}, - {file = "psycopg_binary-3.1.20-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:8285827339f6221861c05c3a292e2464e114c1d0f93ef03c5756c16f3a755520"}, - {file = "psycopg_binary-3.1.20-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e1656794434574d01955f2ceaaef88b4f5edd2099205c383680fa7d8ec18496b"}, - {file = "psycopg_binary-3.1.20-cp38-cp38-win_amd64.whl", hash = "sha256:0f5313ccad37d3f3d87fc8615feeb85b6f99975a338d135d641f2d0921a393dc"}, - {file = "psycopg_binary-3.1.20-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:6902c01cf483dd60565833b04ea6a2ef4151cf9fdb88d461f914b49379470675"}, - {file = "psycopg_binary-3.1.20-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:381e096a0c7f8bcb00ca5121f335d9a2298c3a12d4d6043a4b07d9efa1816606"}, - {file = "psycopg_binary-3.1.20-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ecd8cb66716ff5be7b1ca9c318c4a843807819a245f7c87e0aadd0d0283bc36"}, - {file = "psycopg_binary-3.1.20-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7462bddd3ffd9875b6344a10c379bba820b93a7c4ca962d2e5e9673a0cf46cf5"}, - {file = "psycopg_binary-3.1.20-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43e16e3d76021db831c95d2ade55de0d059b1f17732ba818265c6fcb3b662cb1"}, - {file = "psycopg_binary-3.1.20-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:75779e3b9d86576491653e78122a0bdedb791fdf65fe1d5caa5d002560912425"}, - {file = "psycopg_binary-3.1.20-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4b9487593e511c5a6b7a0165e5bddf57efcc4d40173f2ac52e51659637840094"}, - {file = "psycopg_binary-3.1.20-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b03f6f7e512c6a8e37b10814bcb53dcd2ca0c02512a661b3aefffd7b6009e412"}, - {file = "psycopg_binary-3.1.20-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8a92fc898af4080e3cf562c02e3a8a9cb897dc70976c91e05fd064bff76928ea"}, - {file = "psycopg_binary-3.1.20-cp39-cp39-win_amd64.whl", hash = "sha256:47dd369cb4b263d29aed12ee23b37c03e58bfe656843692d109896c258c554b0"}, -] - -[[package]] -name = "psycopg-pool" -version = "3.2.2" -requires_python = ">=3.8" -summary = "Connection Pool for Psycopg" -groups = ["dev"] -dependencies = [ - "typing-extensions>=4.4", -] -files = [ - {file = "psycopg_pool-3.2.2-py3-none-any.whl", hash = "sha256:273081d0fbfaced4f35e69200c89cb8fbddfe277c38cc86c235b90a2ec2c8153"}, - {file = "psycopg_pool-3.2.2.tar.gz", hash = "sha256:9e22c370045f6d7f2666a5ad1b0caf345f9f1912195b0b25d0d3bcc4f3a7389c"}, -] - -[[package]] -name = "psycopg2-binary" -version = "2.9.9" -requires_python = ">=3.7" -summary = "psycopg2 - Python-PostgreSQL Database Adapter" -groups = ["dev"] -files = [ - {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"}, -] - -[[package]] -name = "psycopg" -version = "3.1.20" -extras = ["binary", "pool"] -requires_python = ">=3.7" -summary = "PostgreSQL database adapter for Python" -groups = ["dev"] -dependencies = [ - "psycopg-binary==3.1.20; implementation_name != \"pypy\"", - "psycopg-pool", - "psycopg==3.1.20", -] -files = [ - {file = "psycopg-3.1.20-py3-none-any.whl", hash = "sha256:898a29f49ac9c903d554f5a6cdc44a8fc564325557c18f82e51f39c1f4fc2aeb"}, - {file = "psycopg-3.1.20.tar.gz", hash = "sha256:32f5862ab79f238496236f97fe374a7ab55b4b4bb839a74802026544735f9a07"}, -] - -[[package]] -name = "pyasn1" -version = "0.6.0" -requires_python = ">=3.8" -summary = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" -groups = ["dev"] -files = [ - {file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"}, - {file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"}, -] - -[[package]] -name = "pyasn1-modules" -version = "0.4.0" -requires_python = ">=3.8" -summary = "A collection of ASN.1-based protocols modules" -groups = ["dev"] -dependencies = [ - "pyasn1<0.7.0,>=0.4.6", -] -files = [ - {file = "pyasn1_modules-0.4.0-py3-none-any.whl", hash = "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b"}, - {file = "pyasn1_modules-0.4.0.tar.gz", hash = "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6"}, -] - -[[package]] -name = "pycparser" -version = "2.22" -requires_python = ">=3.8" -summary = "C parser in Python" -groups = ["cryptography", "dev", "full", "jwt", "linting"] -marker = "os_name == \"nt\" and implementation_name != \"pypy\" or platform_python_implementation != \"PyPy\"" -files = [ - {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, -] - -[[package]] -name = "pydantic" -version = "2.8.2" -requires_python = ">=3.8" -summary = "Data validation using Python type hints" -groups = ["dev", "full", "piccolo", "pydantic"] -dependencies = [ - "annotated-types>=0.4.0", - "pydantic-core==2.20.1", - "typing-extensions>=4.12.2; python_version >= \"3.13\"", - "typing-extensions>=4.6.1; python_version < \"3.13\"", -] -files = [ - {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, - {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, -] - -[[package]] -name = "pydantic-core" -version = "2.20.1" -requires_python = ">=3.8" -summary = "Core functionality for Pydantic validation and serialization" -groups = ["dev", "full", "piccolo", "pydantic"] -dependencies = [ - "typing-extensions!=4.7.0,>=4.6.0", -] -files = [ - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, - {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, - {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, - {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, - {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, - {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, - {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, - {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, - {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, - {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, - {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, - {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, - {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, - {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, -] - -[[package]] -name = "pydantic-extra-types" -version = "2.9.0" -requires_python = ">=3.8" -summary = "Extra Pydantic types." -groups = ["full", "pydantic"] -dependencies = [ - "pydantic>=2.5.2", -] -files = [ - {file = "pydantic_extra_types-2.9.0-py3-none-any.whl", hash = "sha256:f0bb975508572ba7bf3390b7337807588463b7248587e69f43b1ad7c797530d0"}, - {file = "pydantic_extra_types-2.9.0.tar.gz", hash = "sha256:e061c01636188743bb69f368dcd391f327b8cfbfede2fe1cbb1211b06601ba3b"}, -] - -[[package]] -name = "pydantic" -version = "2.8.2" -extras = ["email"] -requires_python = ">=3.8" -summary = "Data validation using Python type hints" -groups = ["full", "piccolo"] -dependencies = [ - "email-validator>=2.0.0", - "pydantic==2.8.2", -] -files = [ - {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, - {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, -] - -[[package]] -name = "pydata-sphinx-theme" -version = "0.14.4" -requires_python = ">=3.8" -summary = "Bootstrap-based Sphinx theme from the PyData community" -groups = ["docs"] -dependencies = [ - "Babel", - "accessible-pygments", - "beautifulsoup4", - "docutils!=0.17.0", - "packaging", - "pygments>=2.7", - "sphinx>=5.0", - "typing-extensions", -] -files = [ - {file = "pydata_sphinx_theme-0.14.4-py3-none-any.whl", hash = "sha256:ac15201f4c2e2e7042b0cad8b30251433c1f92be762ddcefdb4ae68811d918d9"}, - {file = "pydata_sphinx_theme-0.14.4.tar.gz", hash = "sha256:f5d7a2cb7a98e35b9b49d3b02cec373ad28958c2ed5c9b1ffe6aff6c56e9de5b"}, -] - -[[package]] -name = "pygments" -version = "2.18.0" -requires_python = ">=3.8" -summary = "Pygments is a syntax highlighting package written in Python." -groups = ["default", "docs"] -files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, -] - -[[package]] -name = "pyjwt" -version = "2.9.0" -requires_python = ">=3.8" -summary = "JSON Web Token implementation in Python" -groups = ["full", "jwt"] -files = [ - {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, - {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, -] - -[[package]] -name = "pymongo" -version = "4.8.0" -requires_python = ">=3.8" -summary = "Python driver for MongoDB " -groups = ["dev"] -dependencies = [ - "dnspython<3.0.0,>=1.16.0", -] -files = [ - {file = "pymongo-4.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f2b7bec27e047e84947fbd41c782f07c54c30c76d14f3b8bf0c89f7413fac67a"}, - {file = "pymongo-4.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c68fe128a171493018ca5c8020fc08675be130d012b7ab3efe9e22698c612a1"}, - {file = "pymongo-4.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:920d4f8f157a71b3cb3f39bc09ce070693d6e9648fb0e30d00e2657d1dca4e49"}, - {file = "pymongo-4.8.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52b4108ac9469febba18cea50db972605cc43978bedaa9fea413378877560ef8"}, - {file = "pymongo-4.8.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:180d5eb1dc28b62853e2f88017775c4500b07548ed28c0bd9c005c3d7bc52526"}, - {file = "pymongo-4.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aec2b9088cdbceb87e6ca9c639d0ff9b9d083594dda5ca5d3c4f6774f4c81b33"}, - {file = "pymongo-4.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0cf61450feadca81deb1a1489cb1a3ae1e4266efd51adafecec0e503a8dcd84"}, - {file = "pymongo-4.8.0-cp310-cp310-win32.whl", hash = "sha256:8b18c8324809539c79bd6544d00e0607e98ff833ca21953df001510ca25915d1"}, - {file = "pymongo-4.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e5df28f74002e37bcbdfdc5109799f670e4dfef0fb527c391ff84f078050e7b5"}, - {file = "pymongo-4.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6b50040d9767197b77ed420ada29b3bf18a638f9552d80f2da817b7c4a4c9c68"}, - {file = "pymongo-4.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:417369ce39af2b7c2a9c7152c1ed2393edfd1cbaf2a356ba31eb8bcbd5c98dd7"}, - {file = "pymongo-4.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf821bd3befb993a6db17229a2c60c1550e957de02a6ff4dd0af9476637b2e4d"}, - {file = "pymongo-4.8.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9365166aa801c63dff1a3cb96e650be270da06e3464ab106727223123405510f"}, - {file = "pymongo-4.8.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc8b8582f4209c2459b04b049ac03c72c618e011d3caa5391ff86d1bda0cc486"}, - {file = "pymongo-4.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e5019f75f6827bb5354b6fef8dfc9d6c7446894a27346e03134d290eb9e758"}, - {file = "pymongo-4.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b5802151fc2b51cd45492c80ed22b441d20090fb76d1fd53cd7760b340ff554"}, - {file = "pymongo-4.8.0-cp311-cp311-win32.whl", hash = "sha256:4bf58e6825b93da63e499d1a58de7de563c31e575908d4e24876234ccb910eba"}, - {file = "pymongo-4.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:b747c0e257b9d3e6495a018309b9e0c93b7f0d65271d1d62e572747f4ffafc88"}, - {file = "pymongo-4.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e6a720a3d22b54183352dc65f08cd1547204d263e0651b213a0a2e577e838526"}, - {file = "pymongo-4.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:31e4d21201bdf15064cf47ce7b74722d3e1aea2597c6785882244a3bb58c7eab"}, - {file = "pymongo-4.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6b804bb4f2d9dc389cc9e827d579fa327272cdb0629a99bfe5b83cb3e269ebf"}, - {file = "pymongo-4.8.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f2fbdb87fe5075c8beb17a5c16348a1ea3c8b282a5cb72d173330be2fecf22f5"}, - {file = "pymongo-4.8.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd39455b7ee70aabee46f7399b32ab38b86b236c069ae559e22be6b46b2bbfc4"}, - {file = "pymongo-4.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:940d456774b17814bac5ea7fc28188c7a1338d4a233efbb6ba01de957bded2e8"}, - {file = "pymongo-4.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:236bbd7d0aef62e64caf4b24ca200f8c8670d1a6f5ea828c39eccdae423bc2b2"}, - {file = "pymongo-4.8.0-cp312-cp312-win32.whl", hash = "sha256:47ec8c3f0a7b2212dbc9be08d3bf17bc89abd211901093e3ef3f2adea7de7a69"}, - {file = "pymongo-4.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e84bc7707492f06fbc37a9f215374d2977d21b72e10a67f1b31893ec5a140ad8"}, - {file = "pymongo-4.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:519d1bab2b5e5218c64340b57d555d89c3f6c9d717cecbf826fb9d42415e7750"}, - {file = "pymongo-4.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:87075a1feb1e602e539bdb1ef8f4324a3427eb0d64208c3182e677d2c0718b6f"}, - {file = "pymongo-4.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f53429515d2b3e86dcc83dadecf7ff881e538c168d575f3688698a8707b80a"}, - {file = "pymongo-4.8.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdc20cd1e1141b04696ffcdb7c71e8a4a665db31fe72e51ec706b3bdd2d09f36"}, - {file = "pymongo-4.8.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:284d0717d1a7707744018b0b6ee7801b1b1ff044c42f7be7a01bb013de639470"}, - {file = "pymongo-4.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5bf0eb8b6ef40fa22479f09375468c33bebb7fe49d14d9c96c8fd50355188b0"}, - {file = "pymongo-4.8.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ecd71b9226bd1d49416dc9f999772038e56f415a713be51bf18d8676a0841c8"}, - {file = "pymongo-4.8.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0061af6e8c5e68b13f1ec9ad5251247726653c5af3c0bbdfbca6cf931e99216"}, - {file = "pymongo-4.8.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:658d0170f27984e0d89c09fe5c42296613b711a3ffd847eb373b0dbb5b648d5f"}, - {file = "pymongo-4.8.0-cp38-cp38-win32.whl", hash = "sha256:3ed1c316718a2836f7efc3d75b4b0ffdd47894090bc697de8385acd13c513a70"}, - {file = "pymongo-4.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:7148419eedfea9ecb940961cfe465efaba90595568a1fb97585fb535ea63fe2b"}, - {file = "pymongo-4.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8400587d594761e5136a3423111f499574be5fd53cf0aefa0d0f05b180710b0"}, - {file = "pymongo-4.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af3e98dd9702b73e4e6fd780f6925352237f5dce8d99405ff1543f3771201704"}, - {file = "pymongo-4.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de3a860f037bb51f968de320baef85090ff0bbb42ec4f28ec6a5ddf88be61871"}, - {file = "pymongo-4.8.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fc18b3a093f3db008c5fea0e980dbd3b743449eee29b5718bc2dc15ab5088bb"}, - {file = "pymongo-4.8.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18c9d8f975dd7194c37193583fd7d1eb9aea0c21ee58955ecf35362239ff31ac"}, - {file = "pymongo-4.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:408b2f8fdbeca3c19e4156f28fff1ab11c3efb0407b60687162d49f68075e63c"}, - {file = "pymongo-4.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6564780cafd6abeea49759fe661792bd5a67e4f51bca62b88faab497ab5fe89"}, - {file = "pymongo-4.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d18d86bc9e103f4d3d4f18b85a0471c0e13ce5b79194e4a0389a224bb70edd53"}, - {file = "pymongo-4.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9097c331577cecf8034422956daaba7ec74c26f7b255d718c584faddd7fa2e3c"}, - {file = "pymongo-4.8.0-cp39-cp39-win32.whl", hash = "sha256:d5428dbcd43d02f6306e1c3c95f692f68b284e6ee5390292242f509004c9e3a8"}, - {file = "pymongo-4.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:ef7225755ed27bfdb18730c68f6cb023d06c28f2b734597480fb4c0e500feb6f"}, - {file = "pymongo-4.8.0.tar.gz", hash = "sha256:454f2295875744dc70f1881e4b2eb99cdad008a33574bc8aaf120530f66c0cde"}, -] - -[[package]] -name = "pyopenssl" -version = "24.2.1" -requires_python = ">=3.7" -summary = "Python wrapper module around the OpenSSL library" -groups = ["dev"] -dependencies = [ - "cryptography<44,>=41.0.5", -] -files = [ - {file = "pyOpenSSL-24.2.1-py3-none-any.whl", hash = "sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d"}, - {file = "pyopenssl-24.2.1.tar.gz", hash = "sha256:4247f0dbe3748d560dcbb2ff3ea01af0f9a1a001ef5f7c4c647956ed8cbf0e95"}, -] - -[[package]] -name = "pyright" -version = "1.1.344" -requires_python = ">=3.7" -summary = "Command line wrapper for pyright" -groups = ["linting"] -dependencies = [ - "nodeenv>=1.6.0", - "typing-extensions>=3.7; python_version < \"3.8\"", -] -files = [ - {file = "pyright-1.1.344-py3-none-any.whl", hash = "sha256:ab7117a911ce25fcd317f42272579f9ae53a6abc8b8a15f6aa069a11281953ee"}, - {file = "pyright-1.1.344.tar.gz", hash = "sha256:ab7c962f00dd8141a5a0192c1060fb34b92d1f9047ad70dda45229938051922b"}, -] - -[[package]] -name = "pytest" -version = "7.4.4" -requires_python = ">=3.7" -summary = "pytest: simple powerful testing with Python" -groups = ["test"] -dependencies = [ - "colorama; sys_platform == \"win32\"", - "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", - "importlib-metadata>=0.12; python_version < \"3.8\"", - "iniconfig", - "packaging", - "pluggy<2.0,>=0.12", - "tomli>=1.0.0; python_version < \"3.11\"", -] -files = [ - {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, - {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, -] - -[[package]] -name = "pytest-asyncio" -version = "0.23.8" -requires_python = ">=3.8" -summary = "Pytest support for asyncio" -groups = ["test"] -dependencies = [ - "pytest<9,>=7.0.0", -] -files = [ - {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, - {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, -] - -[[package]] -name = "pytest-cov" -version = "5.0.0" -requires_python = ">=3.8" -summary = "Pytest plugin for measuring coverage." -groups = ["test"] -dependencies = [ - "coverage[toml]>=5.2.1", - "pytest>=4.6", -] -files = [ - {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, - {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, -] - -[[package]] -name = "pytest-lazy-fixtures" -version = "1.1.1" -requires_python = "<4.0,>=3.8" -summary = "Allows you to use fixtures in @pytest.mark.parametrize." -groups = ["test"] -dependencies = [ - "pytest>=7", -] -files = [ - {file = "pytest_lazy_fixtures-1.1.1-py3-none-any.whl", hash = "sha256:a4b396a361faf56c6305535fd0175ce82902ca7cf668c4d812a25ed2bcde8183"}, - {file = "pytest_lazy_fixtures-1.1.1.tar.gz", hash = "sha256:0c561f0d29eea5b55cf29b9264a3241999ffdb74c6b6e8c4ccc0bd2c934d01ed"}, -] - -[[package]] -name = "pytest-mock" -version = "3.14.0" -requires_python = ">=3.8" -summary = "Thin-wrapper around the mock package for easier use with pytest" -groups = ["test"] -dependencies = [ - "pytest>=6.2.5", -] -files = [ - {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, - {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, -] - -[[package]] -name = "pytest-rerunfailures" -version = "14.0" -requires_python = ">=3.8" -summary = "pytest plugin to re-run tests to eliminate flaky failures" -groups = ["test"] -dependencies = [ - "packaging>=17.1", - "pytest>=7.2", -] -files = [ - {file = "pytest-rerunfailures-14.0.tar.gz", hash = "sha256:4a400bcbcd3c7a4ad151ab8afac123d90eca3abe27f98725dc4d9702887d2e92"}, - {file = "pytest_rerunfailures-14.0-py3-none-any.whl", hash = "sha256:4197bdd2eaeffdbf50b5ea6e7236f47ff0e44d1def8dae08e409f536d84e7b32"}, -] - -[[package]] -name = "pytest-timeout" -version = "2.3.1" -requires_python = ">=3.7" -summary = "pytest plugin to abort hanging tests" -groups = ["test"] -dependencies = [ - "pytest>=7.0.0", -] -files = [ - {file = "pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9"}, - {file = "pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e"}, -] - -[[package]] -name = "pytest-xdist" -version = "3.6.1" -requires_python = ">=3.8" -summary = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" -groups = ["test"] -dependencies = [ - "execnet>=2.1", - "pytest>=7.0.0", -] -files = [ - {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, - {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -summary = "Extensions to the standard Python datetime module" -groups = ["default", "test"] -dependencies = [ - "six>=1.5", -] -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[[package]] -name = "python-dotenv" -version = "1.0.1" -requires_python = ">=3.8" -summary = "Read key-value pairs from a .env file and set them as environment variables" -groups = ["cli", "dev", "full", "standard"] -files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, -] - -[[package]] -name = "pytz" -version = "2024.1" -summary = "World timezone definitions, modern and historical" -groups = ["docs"] -marker = "python_version < \"3.9\"" -files = [ - {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, - {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -requires_python = ">=3.8" -summary = "YAML parser and emitter for Python" -groups = ["default", "cli", "full", "linting", "standard"] -files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, - {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, - {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, - {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, - {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, - {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "redis" -version = "5.0.8" -requires_python = ">=3.7" -summary = "Python client for Redis database and key-value store" -groups = ["full", "redis"] -dependencies = [ - "async-timeout>=4.0.3; python_full_version < \"3.11.3\"", - "importlib-metadata>=1.0; python_version < \"3.8\"", - "typing-extensions; python_version < \"3.8\"", -] -files = [ - {file = "redis-5.0.8-py3-none-any.whl", hash = "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4"}, - {file = "redis-5.0.8.tar.gz", hash = "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870"}, -] - -[[package]] -name = "redis" -version = "5.0.8" -extras = ["hiredis"] -requires_python = ">=3.7" -summary = "Python client for Redis database and key-value store" -groups = ["full", "redis"] -dependencies = [ - "hiredis>1.0.0", - "redis==5.0.8", -] -files = [ - {file = "redis-5.0.8-py3-none-any.whl", hash = "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4"}, - {file = "redis-5.0.8.tar.gz", hash = "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870"}, -] - -[[package]] -name = "regex" -version = "2024.7.24" -requires_python = ">=3.8" -summary = "Alternative regular expression module, to replace re." -groups = ["linting"] -files = [ - {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b0d3f567fafa0633aee87f08b9276c7062da9616931382993c03808bb68ce"}, - {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3426de3b91d1bc73249042742f45c2148803c111d1175b283270177fdf669024"}, - {file = "regex-2024.7.24-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f273674b445bcb6e4409bf8d1be67bc4b58e8b46fd0d560055d515b8830063cd"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23acc72f0f4e1a9e6e9843d6328177ae3074b4182167e34119ec7233dfeccf53"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65fd3d2e228cae024c411c5ccdffae4c315271eee4a8b839291f84f796b34eca"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c414cbda77dbf13c3bc88b073a1a9f375c7b0cb5e115e15d4b73ec3a2fbc6f59"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7a89eef64b5455835f5ed30254ec19bf41f7541cd94f266ab7cbd463f00c41"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19c65b00d42804e3fbea9708f0937d157e53429a39b7c61253ff15670ff62cb5"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7a5486ca56c8869070a966321d5ab416ff0f83f30e0e2da1ab48815c8d165d46"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f51f9556785e5a203713f5efd9c085b4a45aecd2a42573e2b5041881b588d1f"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4997716674d36a82eab3e86f8fa77080a5d8d96a389a61ea1d0e3a94a582cf7"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c0abb5e4e8ce71a61d9446040c1e86d4e6d23f9097275c5bd49ed978755ff0fe"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:18300a1d78cf1290fa583cd8b7cde26ecb73e9f5916690cf9d42de569c89b1ce"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:416c0e4f56308f34cdb18c3f59849479dde5b19febdcd6e6fa4d04b6c31c9faa"}, - {file = "regex-2024.7.24-cp310-cp310-win32.whl", hash = "sha256:fb168b5924bef397b5ba13aabd8cf5df7d3d93f10218d7b925e360d436863f66"}, - {file = "regex-2024.7.24-cp310-cp310-win_amd64.whl", hash = "sha256:6b9fc7e9cc983e75e2518496ba1afc524227c163e43d706688a6bb9eca41617e"}, - {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:382281306e3adaaa7b8b9ebbb3ffb43358a7bbf585fa93821300a418bb975281"}, - {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4fdd1384619f406ad9037fe6b6eaa3de2749e2e12084abc80169e8e075377d3b"}, - {file = "regex-2024.7.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d974d24edb231446f708c455fd08f94c41c1ff4f04bcf06e5f36df5ef50b95a"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec4419a3fe6cf8a4795752596dfe0adb4aea40d3683a132bae9c30b81e8d73"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb563dd3aea54c797adf513eeec819c4213d7dbfc311874eb4fd28d10f2ff0f2"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45104baae8b9f67569f0f1dca5e1f1ed77a54ae1cd8b0b07aba89272710db61e"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994448ee01864501912abf2bad9203bffc34158e80fe8bfb5b031f4f8e16da51"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fac296f99283ac232d8125be932c5cd7644084a30748fda013028c815ba3364"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e37e809b9303ec3a179085415cb5f418ecf65ec98cdfe34f6a078b46ef823ee"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:01b689e887f612610c869421241e075c02f2e3d1ae93a037cb14f88ab6a8934c"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f6442f0f0ff81775eaa5b05af8a0ffa1dda36e9cf6ec1e0d3d245e8564b684ce"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:871e3ab2838fbcb4e0865a6e01233975df3a15e6fce93b6f99d75cacbd9862d1"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c918b7a1e26b4ab40409820ddccc5d49871a82329640f5005f73572d5eaa9b5e"}, - {file = "regex-2024.7.24-cp311-cp311-win32.whl", hash = "sha256:2dfbb8baf8ba2c2b9aa2807f44ed272f0913eeeba002478c4577b8d29cde215c"}, - {file = "regex-2024.7.24-cp311-cp311-win_amd64.whl", hash = "sha256:538d30cd96ed7d1416d3956f94d54e426a8daf7c14527f6e0d6d425fcb4cca52"}, - {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fe4ebef608553aff8deb845c7f4f1d0740ff76fa672c011cc0bacb2a00fbde86"}, - {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:74007a5b25b7a678459f06559504f1eec2f0f17bca218c9d56f6a0a12bfffdad"}, - {file = "regex-2024.7.24-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7df9ea48641da022c2a3c9c641650cd09f0cd15e8908bf931ad538f5ca7919c9"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a1141a1dcc32904c47f6846b040275c6e5de0bf73f17d7a409035d55b76f289"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80c811cfcb5c331237d9bad3bea2c391114588cf4131707e84d9493064d267f9"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7214477bf9bd195894cf24005b1e7b496f46833337b5dedb7b2a6e33f66d962c"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d55588cba7553f0b6ec33130bc3e114b355570b45785cebdc9daed8c637dd440"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:558a57cfc32adcf19d3f791f62b5ff564922942e389e3cfdb538a23d65a6b610"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a512eed9dfd4117110b1881ba9a59b31433caed0c4101b361f768e7bcbaf93c5"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:86b17ba823ea76256b1885652e3a141a99a5c4422f4a869189db328321b73799"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5eefee9bfe23f6df09ffb6dfb23809f4d74a78acef004aa904dc7c88b9944b05"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:731fcd76bbdbf225e2eb85b7c38da9633ad3073822f5ab32379381e8c3c12e94"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eaef80eac3b4cfbdd6de53c6e108b4c534c21ae055d1dbea2de6b3b8ff3def38"}, - {file = "regex-2024.7.24-cp312-cp312-win32.whl", hash = "sha256:185e029368d6f89f36e526764cf12bf8d6f0e3a2a7737da625a76f594bdfcbfc"}, - {file = "regex-2024.7.24-cp312-cp312-win_amd64.whl", hash = "sha256:2f1baff13cc2521bea83ab2528e7a80cbe0ebb2c6f0bfad15be7da3aed443908"}, - {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:66b4c0731a5c81921e938dcf1a88e978264e26e6ac4ec96a4d21ae0354581ae0"}, - {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:88ecc3afd7e776967fa16c80f974cb79399ee8dc6c96423321d6f7d4b881c92b"}, - {file = "regex-2024.7.24-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64bd50cf16bcc54b274e20235bf8edbb64184a30e1e53873ff8d444e7ac656b2"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb462f0e346fcf41a901a126b50f8781e9a474d3927930f3490f38a6e73b6950"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a82465ebbc9b1c5c50738536fdfa7cab639a261a99b469c9d4c7dcbb2b3f1e57"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68a8f8c046c6466ac61a36b65bb2395c74451df2ffb8458492ef49900efed293"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac8e84fff5d27420f3c1e879ce9929108e873667ec87e0c8eeb413a5311adfe"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba2537ef2163db9e6ccdbeb6f6424282ae4dea43177402152c67ef869cf3978b"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:43affe33137fcd679bdae93fb25924979517e011f9dea99163f80b82eadc7e53"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c9bb87fdf2ab2370f21e4d5636e5317775e5d51ff32ebff2cf389f71b9b13750"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:945352286a541406f99b2655c973852da7911b3f4264e010218bbc1cc73168f2"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:8bc593dcce679206b60a538c302d03c29b18e3d862609317cb560e18b66d10cf"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3f3b6ca8eae6d6c75a6cff525c8530c60e909a71a15e1b731723233331de4169"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c51edc3541e11fbe83f0c4d9412ef6c79f664a3745fab261457e84465ec9d5a8"}, - {file = "regex-2024.7.24-cp38-cp38-win32.whl", hash = "sha256:d0a07763776188b4db4c9c7fb1b8c494049f84659bb387b71c73bbc07f189e96"}, - {file = "regex-2024.7.24-cp38-cp38-win_amd64.whl", hash = "sha256:8fd5afd101dcf86a270d254364e0e8dddedebe6bd1ab9d5f732f274fa00499a5"}, - {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0ffe3f9d430cd37d8fa5632ff6fb36d5b24818c5c986893063b4e5bdb84cdf24"}, - {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25419b70ba00a16abc90ee5fce061228206173231f004437730b67ac77323f0d"}, - {file = "regex-2024.7.24-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33e2614a7ce627f0cdf2ad104797d1f68342d967de3695678c0cb84f530709f8"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33a0021893ede5969876052796165bab6006559ab845fd7b515a30abdd990dc"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04ce29e2c5fedf296b1a1b0acc1724ba93a36fb14031f3abfb7abda2806c1535"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b16582783f44fbca6fcf46f61347340c787d7530d88b4d590a397a47583f31dd"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:836d3cc225b3e8a943d0b02633fb2f28a66e281290302a79df0e1eaa984ff7c1"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:438d9f0f4bc64e8dea78274caa5af971ceff0f8771e1a2333620969936ba10be"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:973335b1624859cb0e52f96062a28aa18f3a5fc77a96e4a3d6d76e29811a0e6e"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c5e69fd3eb0b409432b537fe3c6f44ac089c458ab6b78dcec14478422879ec5f"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fbf8c2f00904eaf63ff37718eb13acf8e178cb940520e47b2f05027f5bb34ce3"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2757ace61bc4061b69af19e4689fa4416e1a04840f33b441034202b5cd02d4"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:44fc61b99035fd9b3b9453f1713234e5a7c92a04f3577252b45feefe1b327759"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:84c312cdf839e8b579f504afcd7b65f35d60b6285d892b19adea16355e8343c9"}, - {file = "regex-2024.7.24-cp39-cp39-win32.whl", hash = "sha256:ca5b2028c2f7af4e13fb9fc29b28d0ce767c38c7facdf64f6c2cd040413055f1"}, - {file = "regex-2024.7.24-cp39-cp39-win_amd64.whl", hash = "sha256:7c479f5ae937ec9985ecaf42e2e10631551d909f203e31308c12d703922742f9"}, - {file = "regex-2024.7.24.tar.gz", hash = "sha256:9cfd009eed1a46b27c14039ad5bbc5e71b6367c5b2e6d5f5da0ea91600817506"}, -] - -[[package]] -name = "requests" -version = "2.32.3" -requires_python = ">=3.8" -summary = "Python HTTP for Humans." -groups = ["docs", "linting"] -dependencies = [ - "certifi>=2017.4.17", - "charset-normalizer<4,>=2", - "idna<4,>=2.5", - "urllib3<3,>=1.21.1", -] -files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, -] - -[[package]] -name = "responses" -version = "0.21.0" -requires_python = ">=3.7" -summary = "A utility library for mocking out the `requests` Python library." -groups = ["linting"] -dependencies = [ - "requests<3.0,>=2.0", - "typing-extensions; python_version < \"3.8\"", - "urllib3>=1.25.10", -] -files = [ - {file = "responses-0.21.0-py3-none-any.whl", hash = "sha256:2dcc863ba63963c0c3d9ee3fa9507cbe36b7d7b0fccb4f0bdfd9e96c539b1487"}, - {file = "responses-0.21.0.tar.gz", hash = "sha256:b82502eb5f09a0289d8e209e7bad71ef3978334f56d09b444253d5ad67bf5253"}, -] - -[[package]] -name = "rfc3986" -version = "1.5.0" -summary = "Validating URI References per RFC 3986" -groups = ["default", "linting"] -files = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, -] - -[[package]] -name = "rfc3986" -version = "1.5.0" -extras = ["idna2008"] -summary = "Validating URI References per RFC 3986" -groups = ["default", "linting"] -dependencies = [ - "idna", - "rfc3986==1.5.0", -] -files = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, -] - -[[package]] -name = "rich" -version = "13.7.1" -requires_python = ">=3.7.0" -summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -groups = ["default"] -dependencies = [ - "markdown-it-py>=2.2.0", - "pygments<3.0.0,>=2.13.0", - "typing-extensions<5.0,>=4.0.0; python_version < \"3.9\"", -] -files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, -] - -[[package]] -name = "rich-click" -version = "1.8.3" -requires_python = ">=3.7" -summary = "Format click help output nicely with rich" -groups = ["default"] -dependencies = [ - "click>=7", - "importlib-metadata; python_version < \"3.8\"", - "rich>=10.7", - "typing-extensions", -] -files = [ - {file = "rich_click-1.8.3-py3-none-any.whl", hash = "sha256:636d9c040d31c5eee242201b5bf4f2d358bfae4db14bb22ec1cafa717cfd02cd"}, - {file = "rich_click-1.8.3.tar.gz", hash = "sha256:6d75bdfa7aa9ed2c467789a0688bc6da23fbe3a143e19aa6ad3f8bac113d2ab3"}, -] - -[[package]] -name = "ruamel-yaml" -version = "0.18.6" -requires_python = ">=3.7" -summary = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" -groups = ["docs"] -dependencies = [ - "ruamel-yaml-clib>=0.2.7; platform_python_implementation == \"CPython\" and python_version < \"3.13\"", -] -files = [ - {file = "ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636"}, - {file = "ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b"}, -] - -[[package]] -name = "ruamel-yaml-clib" -version = "0.2.8" -requires_python = ">=3.6" -summary = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" -groups = ["docs"] -marker = "platform_python_implementation == \"CPython\" and python_version < \"3.13\"" -files = [ - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win32.whl", hash = "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win32.whl", hash = "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15"}, - {file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"}, -] - -[[package]] -name = "ruff" -version = "0.6.2" -requires_python = ">=3.7" -summary = "An extremely fast Python linter and code formatter, written in Rust." -groups = ["docs", "linting"] -files = [ - {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, - {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, - {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, - {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, - {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, - {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, - {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, -] - -[[package]] -name = "service-identity" -version = "24.1.0" -requires_python = ">=3.8" -summary = "Service identity verification for pyOpenSSL & cryptography." -groups = ["dev"] -dependencies = [ - "attrs>=19.1.0", - "cryptography", - "pyasn1", - "pyasn1-modules", -] -files = [ - {file = "service_identity-24.1.0-py3-none-any.whl", hash = "sha256:a28caf8130c8a5c1c7a6f5293faaf239bbfb7751e4862436920ee6f2616f568a"}, - {file = "service_identity-24.1.0.tar.gz", hash = "sha256:6829c9d62fb832c2e1c435629b0a8c476e1929881f28bee4d20bc24161009221"}, -] - -[[package]] -name = "setuptools" -version = "73.0.1" -requires_python = ">=3.8" -summary = "Easily download, build, install, upgrade, and uninstall Python packages" -groups = ["dev", "full", "linting", "opentelemetry"] -files = [ - {file = "setuptools-73.0.1-py3-none-any.whl", hash = "sha256:b208925fcb9f7af924ed2dc04708ea89791e24bde0d3020b27df0e116088b34e"}, - {file = "setuptools-73.0.1.tar.gz", hash = "sha256:d59a3e788ab7e012ab2c4baed1b376da6366883ee20d7a5fc426816e3d7b1193"}, -] - -[[package]] -name = "six" -version = "1.16.0" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -summary = "Python 2 and 3 compatibility utilities" -groups = ["default", "cli", "docs", "full", "standard", "test"] -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "slotscheck" -version = "0.16.5" -requires_python = ">=3.7,<4" -summary = "Ensure your __slots__ are working properly." -groups = ["linting"] -dependencies = [ - "click<9.0,>=8.0", - "importlib-metadata<6,>=1; python_version < \"3.8\"", - "tomli<3.0.0,>=0.2.6; python_version < \"3.11\"", - "typing-extensions<5,>=4.1; python_version < \"3.10\"", -] -files = [ - {file = "slotscheck-0.16.5-py3-none-any.whl", hash = "sha256:b202def7a1d4559575a6a1926aabe461bf780c1584275eff2d3ee4465c52d8c6"}, - {file = "slotscheck-0.16.5.tar.gz", hash = "sha256:6cae3e73808121cf63c1bc638c3b5ae7e10f651323ad3cf38790ce005b77e221"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -requires_python = ">=3.7" -summary = "Sniff out which async library your code is running under" -groups = ["default", "cli", "dev", "full", "linting", "standard"] -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "snowballstemmer" -version = "2.2.0" -summary = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -groups = ["docs"] -files = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] - -[[package]] -name = "sortedcontainers" -version = "2.4.0" -summary = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" -groups = ["dev"] -files = [ - {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, - {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, -] - -[[package]] -name = "soupsieve" -version = "2.6" -requires_python = ">=3.8" -summary = "A modern CSS selector implementation for Beautiful Soup." -groups = ["dev", "docs"] -files = [ - {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, - {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, -] - -[[package]] -name = "sphinx" -version = "7.1.2" -requires_python = ">=3.8" -summary = "Python documentation generator" -groups = ["docs"] -dependencies = [ - "Jinja2>=3.0", - "Pygments>=2.13", - "alabaster<0.8,>=0.7", - "babel>=2.9", - "colorama>=0.4.5; sys_platform == \"win32\"", - "docutils<0.21,>=0.18.1", - "imagesize>=1.3", - "importlib-metadata>=4.8; python_version < \"3.10\"", - "packaging>=21.0", - "requests>=2.25.0", - "snowballstemmer>=2.0", - "sphinxcontrib-applehelp", - "sphinxcontrib-devhelp", - "sphinxcontrib-htmlhelp>=2.0.0", - "sphinxcontrib-jsmath", - "sphinxcontrib-qthelp", - "sphinxcontrib-serializinghtml>=1.1.5", -] -files = [ - {file = "sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe"}, - {file = "sphinx-7.1.2.tar.gz", hash = "sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f"}, -] - -[[package]] -name = "sphinx-autobuild" -version = "2021.3.14" -requires_python = ">=3.6" -summary = "Rebuild Sphinx documentation on changes, with live-reload in the browser." -groups = ["docs"] -dependencies = [ - "colorama", - "livereload", - "sphinx", -] -files = [ - {file = "sphinx-autobuild-2021.3.14.tar.gz", hash = "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05"}, - {file = "sphinx_autobuild-2021.3.14-py3-none-any.whl", hash = "sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac"}, -] - -[[package]] -name = "sphinx-autodoc-typehints" -version = "2.0.1" -requires_python = ">=3.8" -summary = "Type hints (PEP 484) support for the Sphinx autodoc extension" -groups = ["docs"] -dependencies = [ - "sphinx>=7.1.2", -] -files = [ - {file = "sphinx_autodoc_typehints-2.0.1-py3-none-any.whl", hash = "sha256:f73ae89b43a799e587e39266672c1075b2ef783aeb382d3ebed77c38a3fc0149"}, - {file = "sphinx_autodoc_typehints-2.0.1.tar.gz", hash = "sha256:60ed1e3b2c970acc0aa6e877be42d48029a9faec7378a17838716cacd8c10b12"}, -] - -[[package]] -name = "sphinx-click" -version = "6.0.0" -requires_python = ">=3.8" -summary = "Sphinx extension that automatically documents click applications" -groups = ["docs"] -dependencies = [ - "click>=8.0", - "docutils", - "sphinx>=4.0", -] -files = [ - {file = "sphinx_click-6.0.0-py3-none-any.whl", hash = "sha256:1e0a3c83bcb7c55497751b19d07ebe56b5d7b85eb76dd399cf9061b497adc317"}, - {file = "sphinx_click-6.0.0.tar.gz", hash = "sha256:f5d664321dc0c6622ff019f1e1c84e58ce0cecfddeb510e004cf60c2a3ab465b"}, -] - -[[package]] -name = "sphinx-copybutton" -version = "0.5.2" -requires_python = ">=3.7" -summary = "Add a copy button to each of your code cells." -groups = ["docs"] -dependencies = [ - "sphinx>=1.8", -] -files = [ - {file = "sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd"}, - {file = "sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e"}, -] - -[[package]] -name = "sphinx-design" -version = "0.5.0" -requires_python = ">=3.8" -summary = "A sphinx extension for designing beautiful, view size responsive web components." -groups = ["docs"] -dependencies = [ - "sphinx<8,>=5", -] -files = [ - {file = "sphinx_design-0.5.0-py3-none-any.whl", hash = "sha256:1af1267b4cea2eedd6724614f19dcc88fe2e15aff65d06b2f6252cee9c4f4c1e"}, - {file = "sphinx_design-0.5.0.tar.gz", hash = "sha256:e8e513acea6f92d15c6de3b34e954458f245b8e761b45b63950f65373352ab00"}, -] - -[[package]] -name = "sphinx-jinja2-compat" -version = "0.3.0" -requires_python = ">=3.6" -summary = "Patches Jinja2 v3 to restore compatibility with earlier Sphinx versions." -groups = ["docs"] -dependencies = [ - "jinja2>=2.10", - "markupsafe>=1", - "standard-imghdr==3.10.14; python_version >= \"3.13\"", -] -files = [ - {file = "sphinx_jinja2_compat-0.3.0-py3-none-any.whl", hash = "sha256:b1e4006d8e1ea31013fa9946d1b075b0c8d2a42c6e3425e63542c1e9f8be9084"}, - {file = "sphinx_jinja2_compat-0.3.0.tar.gz", hash = "sha256:f3c1590b275f42e7a654e081db5e3e5fb97f515608422bde94015ddf795dfe7c"}, -] - -[[package]] -name = "sphinx-paramlinks" -version = "0.6.0" -summary = "Allows param links in Sphinx function/method descriptions to be linkable" -groups = ["docs"] -dependencies = [ - "Sphinx>=4.0.0", - "docutils", -] -files = [ - {file = "sphinx-paramlinks-0.6.0.tar.gz", hash = "sha256:746a0816860aa3fff5d8d746efcbec4deead421f152687411db1d613d29f915e"}, -] - -[[package]] -name = "sphinx-prompt" -version = "1.5.0" -summary = "Sphinx directive to add unselectable prompt" -groups = ["docs"] -dependencies = [ - "Sphinx", - "pygments", -] -files = [ - {file = "sphinx_prompt-1.5.0-py3-none-any.whl", hash = "sha256:fa4e90d8088b5a996c76087d701fc7e31175f8b9dc4aab03a507e45051067162"}, -] - -[[package]] -name = "sphinx-tabs" -version = "3.4.5" -requires_python = "~=3.7" -summary = "Tabbed views for Sphinx" -groups = ["docs"] -dependencies = [ - "docutils", - "pygments", - "sphinx", -] -files = [ - {file = "sphinx-tabs-3.4.5.tar.gz", hash = "sha256:ba9d0c1e3e37aaadd4b5678449eb08176770e0fc227e769b6ce747df3ceea531"}, - {file = "sphinx_tabs-3.4.5-py3-none-any.whl", hash = "sha256:92cc9473e2ecf1828ca3f6617d0efc0aa8acb06b08c56ba29d1413f2f0f6cf09"}, -] - -[[package]] -name = "sphinx-toolbox" -version = "3.8.0" -requires_python = ">=3.7" -summary = "Box of handy tools for Sphinx 🧰 📔" -groups = ["docs"] -dependencies = [ - "apeye>=0.4.0", - "autodocsumm>=0.2.0", - "beautifulsoup4>=4.9.1", - "cachecontrol[filecache]>=0.13.0", - "dict2css>=0.2.3", - "docutils>=0.16", - "domdf-python-tools>=2.9.0", - "filelock>=3.8.0", - "html5lib>=1.1", - "ruamel-yaml>=0.16.12", - "sphinx-autodoc-typehints>=1.11.1", - "sphinx-jinja2-compat>=0.1.0", - "sphinx-prompt>=1.1.0", - "sphinx-tabs<3.5.0,>=1.2.1", - "sphinx>=3.2.0", - "tabulate>=0.8.7", - "typing-extensions!=3.10.0.1,>=3.7.4.3", - "typing-inspect>=0.6.0; python_version < \"3.8\"", -] -files = [ - {file = "sphinx_toolbox-3.8.0-py3-none-any.whl", hash = "sha256:36f484c540569c0fb62b4197187d166443fe0708134b98c88f6ba5418d06b95e"}, - {file = "sphinx_toolbox-3.8.0.tar.gz", hash = "sha256:f6b62c7800dc2a2e2cbaf7b13ee7c5f06cbf3e1a5ad2c4b2f0744851a05afaee"}, -] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "1.0.4" -requires_python = ">=3.8" -summary = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -groups = ["docs"] -files = [ - {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, - {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, -] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "1.0.2" -requires_python = ">=3.5" -summary = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -groups = ["docs"] -files = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, -] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.0.1" -requires_python = ">=3.8" -summary = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -groups = ["docs"] -files = [ - {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, - {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, -] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -requires_python = ">=3.5" -summary = "A sphinx extension which renders display math in HTML via JavaScript" -groups = ["docs"] -files = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] - -[[package]] -name = "sphinxcontrib-mermaid" -version = "0.9.2" -requires_python = ">=3.7" -summary = "Mermaid diagrams in yours Sphinx powered docs" -groups = ["docs"] -files = [ - {file = "sphinxcontrib-mermaid-0.9.2.tar.gz", hash = "sha256:252ef13dd23164b28f16d8b0205cf184b9d8e2b714a302274d9f59eb708e77af"}, - {file = "sphinxcontrib_mermaid-0.9.2-py3-none-any.whl", hash = "sha256:6795a72037ca55e65663d2a2c1a043d636dc3d30d418e56dd6087d1459d98a5d"}, -] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "1.0.3" -requires_python = ">=3.5" -summary = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -groups = ["docs"] -files = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, -] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "1.1.5" -requires_python = ">=3.5" -summary = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -groups = ["docs"] -files = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, -] - -[[package]] -name = "sqlalchemy" -version = "2.0.32" -requires_python = ">=3.7" -summary = "Database Abstraction Library" -groups = ["full", "sqlalchemy"] -dependencies = [ - "greenlet!=0.4.17; (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\") and python_version < \"3.13\"", - "importlib-metadata; python_version < \"3.8\"", - "typing-extensions>=4.6.0", -] -files = [ - {file = "SQLAlchemy-2.0.32-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0c9045ecc2e4db59bfc97b20516dfdf8e41d910ac6fb667ebd3a79ea54084619"}, - {file = "SQLAlchemy-2.0.32-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1467940318e4a860afd546ef61fefb98a14d935cd6817ed07a228c7f7c62f389"}, - {file = "SQLAlchemy-2.0.32-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5954463675cb15db8d4b521f3566a017c8789222b8316b1e6934c811018ee08b"}, - {file = "SQLAlchemy-2.0.32-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:167e7497035c303ae50651b351c28dc22a40bb98fbdb8468cdc971821b1ae533"}, - {file = "SQLAlchemy-2.0.32-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b27dfb676ac02529fb6e343b3a482303f16e6bc3a4d868b73935b8792edb52d0"}, - {file = "SQLAlchemy-2.0.32-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bf2360a5e0f7bd75fa80431bf8ebcfb920c9f885e7956c7efde89031695cafb8"}, - {file = "SQLAlchemy-2.0.32-cp310-cp310-win32.whl", hash = "sha256:306fe44e754a91cd9d600a6b070c1f2fadbb4a1a257b8781ccf33c7067fd3e4d"}, - {file = "SQLAlchemy-2.0.32-cp310-cp310-win_amd64.whl", hash = "sha256:99db65e6f3ab42e06c318f15c98f59a436f1c78179e6a6f40f529c8cc7100b22"}, - {file = "SQLAlchemy-2.0.32-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:21b053be28a8a414f2ddd401f1be8361e41032d2ef5884b2f31d31cb723e559f"}, - {file = "SQLAlchemy-2.0.32-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b178e875a7a25b5938b53b006598ee7645172fccafe1c291a706e93f48499ff5"}, - {file = "SQLAlchemy-2.0.32-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723a40ee2cc7ea653645bd4cf024326dea2076673fc9d3d33f20f6c81db83e1d"}, - {file = "SQLAlchemy-2.0.32-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:295ff8689544f7ee7e819529633d058bd458c1fd7f7e3eebd0f9268ebc56c2a0"}, - {file = "SQLAlchemy-2.0.32-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:49496b68cd190a147118af585173ee624114dfb2e0297558c460ad7495f9dfe2"}, - {file = "SQLAlchemy-2.0.32-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:acd9b73c5c15f0ec5ce18128b1fe9157ddd0044abc373e6ecd5ba376a7e5d961"}, - {file = "SQLAlchemy-2.0.32-cp311-cp311-win32.whl", hash = "sha256:9365a3da32dabd3e69e06b972b1ffb0c89668994c7e8e75ce21d3e5e69ddef28"}, - {file = "SQLAlchemy-2.0.32-cp311-cp311-win_amd64.whl", hash = "sha256:8bd63d051f4f313b102a2af1cbc8b80f061bf78f3d5bd0843ff70b5859e27924"}, - {file = "SQLAlchemy-2.0.32-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6bab3db192a0c35e3c9d1560eb8332463e29e5507dbd822e29a0a3c48c0a8d92"}, - {file = "SQLAlchemy-2.0.32-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:19d98f4f58b13900d8dec4ed09dd09ef292208ee44cc9c2fe01c1f0a2fe440e9"}, - {file = "SQLAlchemy-2.0.32-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd33c61513cb1b7371fd40cf221256456d26a56284e7d19d1f0b9f1eb7dd7e8"}, - {file = "SQLAlchemy-2.0.32-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d6ba0497c1d066dd004e0f02a92426ca2df20fac08728d03f67f6960271feec"}, - {file = "SQLAlchemy-2.0.32-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2b6be53e4fde0065524f1a0a7929b10e9280987b320716c1509478b712a7688c"}, - {file = "SQLAlchemy-2.0.32-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:916a798f62f410c0b80b63683c8061f5ebe237b0f4ad778739304253353bc1cb"}, - {file = "SQLAlchemy-2.0.32-cp312-cp312-win32.whl", hash = "sha256:31983018b74908ebc6c996a16ad3690301a23befb643093fcfe85efd292e384d"}, - {file = "SQLAlchemy-2.0.32-cp312-cp312-win_amd64.whl", hash = "sha256:4363ed245a6231f2e2957cccdda3c776265a75851f4753c60f3004b90e69bfeb"}, - {file = "SQLAlchemy-2.0.32-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c742be912f57586ac43af38b3848f7688863a403dfb220193a882ea60e1ec3a"}, - {file = "SQLAlchemy-2.0.32-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:62e23d0ac103bcf1c5555b6c88c114089587bc64d048fef5bbdb58dfd26f96da"}, - {file = "SQLAlchemy-2.0.32-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:251f0d1108aab8ea7b9aadbd07fb47fb8e3a5838dde34aa95a3349876b5a1f1d"}, - {file = "SQLAlchemy-2.0.32-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ef18a84e5116340e38eca3e7f9eeaaef62738891422e7c2a0b80feab165905f"}, - {file = "SQLAlchemy-2.0.32-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3eb6a97a1d39976f360b10ff208c73afb6a4de86dd2a6212ddf65c4a6a2347d5"}, - {file = "SQLAlchemy-2.0.32-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0c1c9b673d21477cec17ab10bc4decb1322843ba35b481585facd88203754fc5"}, - {file = "SQLAlchemy-2.0.32-cp38-cp38-win32.whl", hash = "sha256:c41a2b9ca80ee555decc605bd3c4520cc6fef9abde8fd66b1cf65126a6922d65"}, - {file = "SQLAlchemy-2.0.32-cp38-cp38-win_amd64.whl", hash = "sha256:8a37e4d265033c897892279e8adf505c8b6b4075f2b40d77afb31f7185cd6ecd"}, - {file = "SQLAlchemy-2.0.32-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:52fec964fba2ef46476312a03ec8c425956b05c20220a1a03703537824b5e8e1"}, - {file = "SQLAlchemy-2.0.32-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:328429aecaba2aee3d71e11f2477c14eec5990fb6d0e884107935f7fb6001632"}, - {file = "SQLAlchemy-2.0.32-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85a01b5599e790e76ac3fe3aa2f26e1feba56270023d6afd5550ed63c68552b3"}, - {file = "SQLAlchemy-2.0.32-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf04784797dcdf4c0aa952c8d234fa01974c4729db55c45732520ce12dd95b4"}, - {file = "SQLAlchemy-2.0.32-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4488120becf9b71b3ac718f4138269a6be99a42fe023ec457896ba4f80749525"}, - {file = "SQLAlchemy-2.0.32-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:14e09e083a5796d513918a66f3d6aedbc131e39e80875afe81d98a03312889e6"}, - {file = "SQLAlchemy-2.0.32-cp39-cp39-win32.whl", hash = "sha256:0d322cc9c9b2154ba7e82f7bf25ecc7c36fbe2d82e2933b3642fc095a52cfc78"}, - {file = "SQLAlchemy-2.0.32-cp39-cp39-win_amd64.whl", hash = "sha256:7dd8583df2f98dea28b5cd53a1beac963f4f9d087888d75f22fcc93a07cf8d84"}, - {file = "SQLAlchemy-2.0.32-py3-none-any.whl", hash = "sha256:e567a8793a692451f706b363ccf3c45e056b67d90ead58c3bc9471af5d212202"}, - {file = "SQLAlchemy-2.0.32.tar.gz", hash = "sha256:c1b88cc8b02b6a5f0efb0345a03672d4c897dc7d92585176f88c67346f565ea8"}, -] - -[[package]] -name = "standard-imghdr" -version = "3.10.14" -summary = "Standard library imghdr redistribution. \"dead battery\"." -groups = ["docs"] -marker = "python_version >= \"3.13\"" -files = [ - {file = "standard_imghdr-3.10.14-py3-none-any.whl", hash = "sha256:cdf6883163349624dee9a81d2853a20260337c4cd41c04e99c082e01833a08e2"}, - {file = "standard_imghdr-3.10.14.tar.gz", hash = "sha256:2598fe2e7c540dbda34b233295e10957ab8dc8ac6f3bd9eaa8d38be167232e52"}, -] - -[[package]] -name = "starlette" -version = "0.38.2" -requires_python = ">=3.8" -summary = "The little ASGI library that shines." -groups = ["dev"] -dependencies = [ - "anyio<5,>=3.4.0", - "typing-extensions>=3.10.0; python_version < \"3.10\"", -] -files = [ - {file = "starlette-0.38.2-py3-none-any.whl", hash = "sha256:4ec6a59df6bbafdab5f567754481657f7ed90dc9d69b0c9ff017907dd54faeff"}, - {file = "starlette-0.38.2.tar.gz", hash = "sha256:c7c0441065252160993a1a37cf2a73bb64d271b17303e0b0c1eb7191cfb12d75"}, -] - -[[package]] -name = "structlog" -version = "24.4.0" -requires_python = ">=3.8" -summary = "Structured Logging for Python" -groups = ["full", "structlog"] -files = [ - {file = "structlog-24.4.0-py3-none-any.whl", hash = "sha256:597f61e80a91cc0749a9fd2a098ed76715a1c8a01f73e336b746504d1aad7610"}, - {file = "structlog-24.4.0.tar.gz", hash = "sha256:b27bfecede327a6d2da5fbc96bd859f114ecc398a6389d664f62085ee7ae6fc4"}, -] - -[[package]] -name = "tabulate" -version = "0.9.0" -requires_python = ">=3.7" -summary = "Pretty-print tabular data" -groups = ["docs"] -files = [ - {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, - {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, -] - -[[package]] -name = "targ" -version = "0.4.0" -requires_python = ">=3.8.0" -summary = "Build a Python CLI for your app, just using type hints and docstrings." -groups = ["full", "piccolo"] -dependencies = [ - "colorama==0.4.*", - "docstring-parser>=0.12", - "typing-inspect>=0.6.0; python_version < \"3.8\"", -] -files = [ - {file = "targ-0.4.0-py3-none-any.whl", hash = "sha256:5237524323661ffa899158d668468b5c94bb84e2d988bd216981932844da63eb"}, - {file = "targ-0.4.0.tar.gz", hash = "sha256:dcdb57945bffe5bc59570d2e41bb1adc6280c5460332c5daf300729bc88d1aba"}, -] - -[[package]] -name = "taskgroup" -version = "0.0.0a4" -summary = "backport of asyncio.TaskGroup, asyncio.Runner and asyncio.timeout" -groups = ["dev"] -marker = "python_version < \"3.11\"" -dependencies = [ - "exceptiongroup", -] -files = [ - {file = "taskgroup-0.0.0a4-py2.py3-none-any.whl", hash = "sha256:5c1bd0e4c06114e7a4128583ab75c987597d5378a33948a3b74c662b90f61277"}, - {file = "taskgroup-0.0.0a4.tar.gz", hash = "sha256:eb08902d221e27661950f2a0320ddf3f939f579279996f81fe30779bca3a159c"}, -] - -[[package]] -name = "test-results-parser" -version = "0.1.0" -requires_python = ">=3.8" -summary = "" -groups = ["linting"] -files = [ - {file = "test_results_parser-0.1.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:78457dd51966244ab144b8d726379a404075ce14cb8d0591d498293d22a7b628"}, - {file = "test_results_parser-0.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dd544e668525afdcbdf77ce5f321501ff061af0f7763d5da6766909c08c5a32"}, - {file = "test_results_parser-0.1.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66a2424b9d915de8be516789c93b73eb26168f868153811865949d32f0da64da"}, - {file = "test_results_parser-0.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c33eb93a0e5562a1172f369500174e18c9f45e8ccbdd784315d6c3cff8ce4c5"}, - {file = "test_results_parser-0.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02286c4735abde145f2a5e17e73778bdd29af2608cf01cd7b422ed23d3fbbc94"}, - {file = "test_results_parser-0.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adb185a9657f9fc1ed6501313eec897f5dc2fa6460d07f5a2e54f7aa8c365e9a"}, - {file = "test_results_parser-0.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c09b4672e15fec1ffb3059d36ba7d7a20830297a73c65af0fbaac317ec02f359"}, - {file = "test_results_parser-0.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ec0aabe0e91933a35603e5e90a2a8c49b92af3b3edc5fe37315f9cf7dc5e4aec"}, - {file = "test_results_parser-0.1.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:46ce4d77883afc9e5f7f6158fc54ddb8088dcb65df731d8a820b64ff3e2e3c62"}, - {file = "test_results_parser-0.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:bed2dd0e6b63200949046bfd13dea28e23d01fbc3796a5740c17e3f21f3c9152"}, - {file = "test_results_parser-0.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:314bd28eb9c4f5c9482ec9ce7679b4f1eb74f179227c27d4566bd8d6c3f0af70"}, - {file = "test_results_parser-0.1.0-cp310-none-win32.whl", hash = "sha256:e42d10b29609ed56199008e0047ba881f5e15ab39509d854dc5c22144ff26058"}, - {file = "test_results_parser-0.1.0-cp310-none-win_amd64.whl", hash = "sha256:f70ba9bb0550b8d1d2e3c48b74df6bb07f4a265a5deacb6cd6829955a82e65ef"}, - {file = "test_results_parser-0.1.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dc2c4f5accaa9eb5be6cb251a494f2af33ee7b31fadf94a5b77d4649efe14848"}, - {file = "test_results_parser-0.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25c147e9b14a464cf67887601b5dc7882ddf595be74ad8ad57f62131710ba561"}, - {file = "test_results_parser-0.1.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0083fa371cb12ca1b5b4dd1d0a38c29bed45f7581767e13882426ff7e7582af1"}, - {file = "test_results_parser-0.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd9eda302a7583e6da8b2ce492073cc70dcadcd5264d4410891ca2918afe854d"}, - {file = "test_results_parser-0.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2cf06980539a1c1aa7d904ba0ea6c997215689386b6b274e90e699dde9cf55b"}, - {file = "test_results_parser-0.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e89353a3de9f42b87939d525392b6f485c687f0a3c214e7fdf9f0bca1045eb"}, - {file = "test_results_parser-0.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8362eebf1df1ad22502c75c8b9a8d13520bd292fa6efb26466e1402e621238d"}, - {file = "test_results_parser-0.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:000e1cc7f5dd98ef0d6927a873bd169223a8f4f10b85f53c55e5a43c67342e9f"}, - {file = "test_results_parser-0.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:b592a301e62914a7c06bbaf9694b25f98d45f0b0dac2d8d86144fd7056225bb2"}, - {file = "test_results_parser-0.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:559dab14cc88fc10fb8a9627be3b50ce05fea7f50d95716d8d8746fdcb601c41"}, - {file = "test_results_parser-0.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8bf5944652c65e827e8698651771763fe9cea3602c7cc39358b25601cac83b52"}, - {file = "test_results_parser-0.1.0-cp311-none-win32.whl", hash = "sha256:cd629aa1c1ae3cd68b1ab77b4fa7a7e5878e18fd69b8f87306540277c389c8a6"}, - {file = "test_results_parser-0.1.0-cp311-none-win_amd64.whl", hash = "sha256:620ac0a71fa07225bec8fab9e311950a904e26ea45846d32ef4f9d9addaf236c"}, - {file = "test_results_parser-0.1.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ff72b9aa40be8ab6d8d68e6d02ac7dddec5f0e4762c5271d69fbc81de4854d09"}, - {file = "test_results_parser-0.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf7baedb05e1006effe0bfeab04934965058969dc647a63cb341370e92e6f0ca"}, - {file = "test_results_parser-0.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1046f5e90976ccfe3aa51bcb912fbf3e23991b41a8a836ee22381f3e292ddf79"}, - {file = "test_results_parser-0.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61070f24114fa96d6102ad02e6a4ed7acd9e962963687657ab3b835925cd381f"}, - {file = "test_results_parser-0.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd488964632e1a55b4472547f23c3681b925e7828b955e0dbf46a69edd6d4fb7"}, - {file = "test_results_parser-0.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76c01f61d28c620fda604cfd8a87e4885884fa77ad0db80c84bfc93212f10027"}, - {file = "test_results_parser-0.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4e289d55afc1f7440dad3c56485dcab86800d922669a6ab2b10f113a10196a73"}, - {file = "test_results_parser-0.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e628b80b734f8b3aa1b2464a935b34cd06de22286ea14ad4cc0a6b3b09fc1601"}, - {file = "test_results_parser-0.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:69614e69b830dcb2f9a0e7c3cbc1b7efdd399e317087cd2be7f8ce0ab45ed182"}, - {file = "test_results_parser-0.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:557b5b2766631f0f9caedb6a70d3dcd1628bcd6a8c5d5545c641b9e5c645cb17"}, - {file = "test_results_parser-0.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db57956b1f7d546196c2ea2ead2469bca70f5e51053e3e5ab0f11fa051b134a2"}, - {file = "test_results_parser-0.1.0-cp312-none-win32.whl", hash = "sha256:592564e7ccf2febe72ead637a90b8bb8c1d7b5ad77000cf2991442c22b4a45ac"}, - {file = "test_results_parser-0.1.0-cp312-none-win_amd64.whl", hash = "sha256:c7c063f565d0eda32e7dacef75b38d120412d934543f6826e0a6d3f3778c4ef9"}, - {file = "test_results_parser-0.1.0-cp38-cp38-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e7acb420f740e09c25970dddb3d51cc30270ec92c0cdf22b79f6fa94934246d8"}, - {file = "test_results_parser-0.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62b0b1b9c06d89ef139543878c0de3c8401ede38793ae5d2beabf150fa108f20"}, - {file = "test_results_parser-0.1.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad619bdb6d1eef0eaa56a8b14e22cc5781d08432c36d05c24eafe14889c1ae7"}, - {file = "test_results_parser-0.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1acdb3a52795c556235cf7ee977afb2e9969a8cd12eb24a070ac859363df7004"}, - {file = "test_results_parser-0.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d3b931f99726b37c7e29e4dfc233963a152c39ffaf1a433598c2014a45b29fca"}, - {file = "test_results_parser-0.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1603383ad6c3245dc2f7407a24742c99a0f5fed86a1322752ce6b35daf800caf"}, - {file = "test_results_parser-0.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:74dbe45d3e848d9253eebeca08af38523da2a7e9a16829bbee59287a50e1ffef"}, - {file = "test_results_parser-0.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3bd0c8bc467538d01e23168c34429ae92fb5adc5ddc23655cd10bad94c3b01ab"}, - {file = "test_results_parser-0.1.0-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:587111a3af0f38f8ffc3c6dc8d7f5d2c5b67dd5d83e1d57c6d5c2ef5d1b6b749"}, - {file = "test_results_parser-0.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:a48a11ce684ee09d15aefd1e22ff0969f86af6c94ddf11281d504d7319601d6e"}, - {file = "test_results_parser-0.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:15ac06f83f81bd2f39a929e043fcb44caa8da601ec7b03fa864e00b69a320f7d"}, - {file = "test_results_parser-0.1.0-cp38-none-win32.whl", hash = "sha256:9ec2d62c148dbbf43eb9dcad861f54101cc4f3e070acf131fe4bb59989b7bad8"}, - {file = "test_results_parser-0.1.0-cp38-none-win_amd64.whl", hash = "sha256:7223dda9ced5c2be5939667f7fbf7de08e0020bd2d6a6e8c1799a10c02d00fb2"}, - {file = "test_results_parser-0.1.0-cp39-cp39-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:632232864025913a0e71365732cfe507cf3b1262000cc1753cb97139ed8c493e"}, - {file = "test_results_parser-0.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a64a894c8bb2542925ba8fc9199939161eea4d5d56ea59b21e9627185f658a63"}, - {file = "test_results_parser-0.1.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:db6e4dbafdf7bec092cd34cd971634fc1842e26d2936636f684892c4755afab9"}, - {file = "test_results_parser-0.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:179ff5c2d470a1edcd1965a9cec03b00b615d6dc31d57bb85b233b52d4a54076"}, - {file = "test_results_parser-0.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a72194747797654397ad615c38ac0e9c9ef56bbc078a2005110d5010a06ee6c"}, - {file = "test_results_parser-0.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:384ecace49bfc68a2b33dd8266598430ccd56a579e7157ede9b1c5ffbe9c286f"}, - {file = "test_results_parser-0.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5abf2505bb09500e09cbe54cc8bfbcfbc501a8f986afdbcffd13ebab449693e5"}, - {file = "test_results_parser-0.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1bd6f46369ae86412f9bfdd453788d32c84d1ceb9ef0056081229ce384fbbf89"}, - {file = "test_results_parser-0.1.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a32b3afb5509d06652ab08d242c655854431d25c7059b865d5c8bb376ba83278"}, - {file = "test_results_parser-0.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9ff24b1f6f2ee0d29f261d9f1ee5752030ac90ab5875d954d5208ea3ab8a1b41"}, - {file = "test_results_parser-0.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2e48ac37891c5a4b43e2d5ab45727c03b0323fc5a3ee6b671fc9aed012394a57"}, - {file = "test_results_parser-0.1.0-cp39-none-win32.whl", hash = "sha256:f181a3c80807207463c946dd500e21ad506143f5f4daa48054cd3ec04c0e3de2"}, - {file = "test_results_parser-0.1.0-cp39-none-win_amd64.whl", hash = "sha256:d90300ead999d0e7313debbc1773853ffc37aa17139f017bdf3b1645f80c808b"}, - {file = "test_results_parser-0.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b4732d69e9550bb31043e73d1d3acdc8eb30b76414607fc220d28e1867ef70e"}, - {file = "test_results_parser-0.1.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6e039e16391d0a8056b9536d0318b8044e1335b1164917272ccbea82e4ded3c0"}, - {file = "test_results_parser-0.1.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:673d4f04dbdb932caa3d8c7b9a026e56c2d878b3474ed2d814ca42bc8f4066a9"}, - {file = "test_results_parser-0.1.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a39c5f6ccf5c0d742baf99149fd0edc3cef1abbfc6c66d38466fc08a67f090f"}, - {file = "test_results_parser-0.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d28b3dfee937e43c9256fa484110f8a1ea8bfcb8baeef67aabe3f2c930ae335"}, - {file = "test_results_parser-0.1.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b11832276f16612f2e99c80d901c8998598604dc5b281cdbf5adf0b4253505b9"}, - {file = "test_results_parser-0.1.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:a53a6798f6de9afc674064498f890c6ac2dfb70212293b8ea4f7acec5042e3d9"}, - {file = "test_results_parser-0.1.0-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:e22b8ad9e62138f7384a86bfad78edc16474f18cfd2e2cd9e65b55474a245345"}, - {file = "test_results_parser-0.1.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:f421ee9abcc664e0d9713cff60ba020b7390abe03262b22c1c5acbae7b235223"}, - {file = "test_results_parser-0.1.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5134f3175586de645a2775cef6519343daaae243d0301d44ee7867075f581a7"}, - {file = "test_results_parser-0.1.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e41d2c6ef9a154e1dd4dbd83de906aaab7a9fdd33be7e871110401cde35af57"}, - {file = "test_results_parser-0.1.0-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:924100e5cb43c8b42bc63946061a8899c750244cb97881f1828ccde06265b214"}, - {file = "test_results_parser-0.1.0-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13b9a4d61e1404f5d3889716fdee90018d330a722680b6cf2fc381f298134c3a"}, - {file = "test_results_parser-0.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd7952ba5dfc97970ead49722b7ea61318b29602d0814f215efa52317e896486"}, - {file = "test_results_parser-0.1.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:61efc73848f901f5af295420f8346fa09c4337ceb8b769dff9134e388b1275e9"}, - {file = "test_results_parser-0.1.0-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:70451bbaa38c94ffe3ba6fc7c11d30b56b7e84d8cc3bb891580f43d87f3a5f49"}, - {file = "test_results_parser-0.1.0-pp38-pypy38_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:7bfae8192331c1d11364967d9c7978efb9fad0a033bff42f4f758d846f50a274"}, - {file = "test_results_parser-0.1.0-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:3821020b4ffe3e8db51b12fba39381b9537b426acc590a5fdfb77ca96396448f"}, - {file = "test_results_parser-0.1.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2efb560cb6d5a1623dcd8e5c58adc62b1bc236ff226983116c8dedb1357255fb"}, - {file = "test_results_parser-0.1.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b3080b3b90014c1296ec7d5ef78300fce204f33a71865598921fed888f62e0f"}, - {file = "test_results_parser-0.1.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:998ed8d9979dc40c8ace7b1e6e2a5c901b1e36519a7ad35d1ca728135d48d789"}, - {file = "test_results_parser-0.1.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6bfb712e6e1bff252ed2a5de5bce527a17a0a96666fd827e8515acaa5d2c064b"}, - {file = "test_results_parser-0.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8d58e4e2f56bd032dd9a6e509da79f7509887cc386f2a736eeec2c2024cdc32"}, - {file = "test_results_parser-0.1.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43a46bc8de43e44da870a4ed939ed8545c80210f090bb545498868dbbb3ba291"}, - {file = "test_results_parser-0.1.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:97f8d6e0a5959b0c5a3180288c576136e4334167f4adef8eebebca3462d8f292"}, - {file = "test_results_parser-0.1.0-pp39-pypy39_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:960e2eeaccf6794e5963afc05aa920ff17d66bbe96b870150e2fa214d7db5c21"}, - {file = "test_results_parser-0.1.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:682ef893c500634a89a0b7b0da247354921de02ccbe9c541796d4fa016722317"}, - {file = "test_results_parser-0.1.0.tar.gz", hash = "sha256:0034281a4b406d7f072fc5ac1f5e44660e3c23bc92f2e7284862ee097f9626ee"}, -] - -[[package]] -name = "time-machine" -version = "2.15.0" -requires_python = ">=3.8" -summary = "Travel through time in your tests." -groups = ["test"] -dependencies = [ - "python-dateutil", -] -files = [ - {file = "time_machine-2.15.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:892d016789b59950989b2db188dcd46cf16d34e8daf2343e33b679b0c5fd1001"}, - {file = "time_machine-2.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4428bdae507996aa3fdeb4727bca09e26306fa64a502e7335207252684516cbf"}, - {file = "time_machine-2.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0302568338c8bd333ed0698231dbb781b70ead1a5579b4ac734b9bf88313229f"}, - {file = "time_machine-2.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18fc4740073e67071472c48355775ec6d1b93af5c675524b7de2474e0dcd8741"}, - {file = "time_machine-2.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:768d33b484a35da93731cc99bdc926b539240a78673216cdc6306833d9072350"}, - {file = "time_machine-2.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:73a8c8160d2a170dadcad5b82fb5ee53236a19cec0996651cf4d21da0a2574d5"}, - {file = "time_machine-2.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:09fd839a321a92aa8183206c383b9725eaf4e0a28a70e4cb87db292b352eeefb"}, - {file = "time_machine-2.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:838a6d117739f1ae6ecc45ec630fa694f41a85c0d07b1f3b1db2a6cc52c1808b"}, - {file = "time_machine-2.15.0-cp310-cp310-win32.whl", hash = "sha256:d24d2ec74923b49bce7618e3e7762baa6be74e624d9829d5632321de102bf386"}, - {file = "time_machine-2.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:95c8e7036cf442480d0bf6f5fde371e1eb6dbbf5391d7bdb8db73bd8a732b538"}, - {file = "time_machine-2.15.0-cp310-cp310-win_arm64.whl", hash = "sha256:660810cd27a8a94cb5e845e8f28a95e70b01ff0c45466d394c4a0cba5a0ae279"}, - {file = "time_machine-2.15.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:674097dd54a0bbd555e7927092c74428c4c07268ad52bca38cfccc3214707e50"}, - {file = "time_machine-2.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4e83fd6112808d1d14d1a57397c6fa3bd71bb2f3b8800036e12366e3680819b9"}, - {file = "time_machine-2.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b095a1de40ca1afaeae8df3f45e26b645094a1912e6e6871e725fcf06ecdb74a"}, - {file = "time_machine-2.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4601fe7a6b74c6fd9207e614d9db2a20dd4befd4d314677a0feac13a67189707"}, - {file = "time_machine-2.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:245ef73f9927b7d4909d554a6a0284dbc5dee9730adea599e430b37c9e9fa203"}, - {file = "time_machine-2.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:704abc7f3403584cca9c01c5809812e0bd70632ea4251389fae4f45e11aad94f"}, - {file = "time_machine-2.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6425001e50a0c82108caed438233066cea04d42a8fc9c49bfcf081a5b96e5b4e"}, - {file = "time_machine-2.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5d4073b754f90b19f28d036ec5143d3fca3a75e4d4241d78790a6178b00bb373"}, - {file = "time_machine-2.15.0-cp311-cp311-win32.whl", hash = "sha256:8817b0f7d7830215261b18db83c9c3ef1da6bb64da5c292d7c70b9a46e5a6745"}, - {file = "time_machine-2.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:ddad27a62df2ea47b7b483009fbfcf167a71d702cbd8e2eefd9ddc1c93146658"}, - {file = "time_machine-2.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f021aa2dbd8fbfe54d3fa2258518129108b7496922b3bcff2cf5991078eec67"}, - {file = "time_machine-2.15.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a22f47c34ee1fcf7d93a8c5c93135499aac879d9d5d8f820bd28571a30fdabcd"}, - {file = "time_machine-2.15.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b684f8ecdeacd6baabc17b15ac1b054ca62029193e6c5367ef00b3516671de80"}, - {file = "time_machine-2.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f7add997684bc6141e1c80f6ba0c38ffe316ba277a4074e61b1b7b4f5a172bf"}, - {file = "time_machine-2.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31af56399bf7c9ef76a3f7b6d9471dffa8f06ee373c194a374b69523f9061de9"}, - {file = "time_machine-2.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5b94cba3edfc54bcb3ab5be616a2f50fa48be438e5af970824efdf882d1bc31"}, - {file = "time_machine-2.15.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3862dda89bdb05f9d521b08fdcb24b19a7dd9f559ae324f4301ba7a07b6eea64"}, - {file = "time_machine-2.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e1790481a6b9ce38888f22ce30710244067898c3ac4805a0e061e381f3db3506"}, - {file = "time_machine-2.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a731c03bc00552ee6cc685a59616d36003124e7e04c6ddf65c2c47f1c3d85480"}, - {file = "time_machine-2.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e6776840aea3ff5ab6924b50117957da62db51b109b3b491c0d5817a804b1a8e"}, - {file = "time_machine-2.15.0-cp312-cp312-win32.whl", hash = "sha256:9479530e3fce65f6149058071fa4df8150025f15b43b103445f619842981a87c"}, - {file = "time_machine-2.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:b5f3ab4185c1f72010846ca9fccb08349e23a2b52982a18d9870e848ce9f1c86"}, - {file = "time_machine-2.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:c0473dfa8f17c6a9a250b2bd6a5b62af3aa7d22518f701649115f1085d5e35ab"}, - {file = "time_machine-2.15.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f50f10058b884d45cd8a50423bf561b1f9f9df7058abeb8b318700c8bcf4bb54"}, - {file = "time_machine-2.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:df6f618b98f0848fd8d07039541e10f23db679d8283f8719e870a98e1ef8e639"}, - {file = "time_machine-2.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52468a0784544eba708c0ae6bc5e8c5dcfd685495a60f7f74028662c984bd9cd"}, - {file = "time_machine-2.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c08800c28160f4d32ca510128b4e201a43c813e7a2dd53178fa79ebe050eba13"}, - {file = "time_machine-2.15.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65d395211736d9844537a530287a7c64b9fda1d353e899a0e1723986a0859154"}, - {file = "time_machine-2.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b177d334a35bf2ce103bfe4e0e416e4ee824dd33386ea73fa7491c17cc61897"}, - {file = "time_machine-2.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9a6a9342fae113b12aab42c790880c549d9ba695b8deff27ee08096eedd67569"}, - {file = "time_machine-2.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bcbb25029ee8756f10c6473cea5ef21707a1d9a8752cdf29fad3a5f34aa4a313"}, - {file = "time_machine-2.15.0-cp313-cp313-win32.whl", hash = "sha256:29b988b1f09f2a083b12b6b054787b799ae91ee15bb0e9de3e48f880e4d68674"}, - {file = "time_machine-2.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:d828721dcbcb94b904a6b25df67c2513ecd24cd9e36694f38b9f0fa71c7c6103"}, - {file = "time_machine-2.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:008bd668d933b1a029c81805bcdc0132390c2545b103cf8e6709e3adbc37989d"}, - {file = "time_machine-2.15.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e99689f6c6b9ca6e2fc7a75d140e38c5a7985dab61fe1f4e506268f7e9844e05"}, - {file = "time_machine-2.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:671e88a6209a1cf415dc0f8c67d2b2d3b55b436cc63801a518f9800ebd752959"}, - {file = "time_machine-2.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b2d28daf4cabc698aafb12135525d87dc1f2f893cbd29a8a6fe0d8d36d1342c"}, - {file = "time_machine-2.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cd9f057457d12604be18b623bcd5ae7d0b917ad66cb510ee1135d5f123666e2"}, - {file = "time_machine-2.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97dc6793e512a62ba9eab250134a2e67372c16ae9948e73d27c2ef355356e2e1"}, - {file = "time_machine-2.15.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0630a32e9ebcf2fac3704365b31e271fef6eabd6fedfa404cd8dbd244f7fc84d"}, - {file = "time_machine-2.15.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:617c9a92d8d8f60d5ef39e76596620503752a09f834a218e5b83be352fdd6c91"}, - {file = "time_machine-2.15.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3f7eadd820e792de33a9ec91f8178a2b9088e4e8b9a166953419ddc4ec5f7cfe"}, - {file = "time_machine-2.15.0-cp38-cp38-win32.whl", hash = "sha256:b7b647684eb2e1fd1e5e6b101249d5fe9d6117c117b5e336ad8dd75af48d2d1f"}, - {file = "time_machine-2.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b48abd7745caec1a78a16a048966cde14ff6ccb04d471a7201532648d3f77d14"}, - {file = "time_machine-2.15.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8c2b1c91b437133c672e374857eccb1dd2c2d9f8477ae3b35138382d5ef19846"}, - {file = "time_machine-2.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:79bf1ef6850182e09d86e61fa31717da56014a3b2234afb025fca1f2a43ac07b"}, - {file = "time_machine-2.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:658ea8477fa020f08435fb7277635eb0b50cd5206b9d4cbe10e9a5466b01f855"}, - {file = "time_machine-2.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c947135750d20f35acac290c34f1acf5771fc166a3fbc0e3816a97c756aaa5f5"}, - {file = "time_machine-2.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dee3a0dd1866988c49a5d00564404db9bcdf49ca92f9c4e8b6c99609d64e698"}, - {file = "time_machine-2.15.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c596920d6017702a36e3a43fd8110a84e87d6229f30b84bd5640cbae9b5145da"}, - {file = "time_machine-2.15.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:014589d0edd4aa14f8d63985745565e8cbbe48461d6c004a96000b47f6b44e78"}, - {file = "time_machine-2.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5ff655716cd13a242eef8cf5d368074e8b396ff86508a5933e7cff4f2b3eb3c2"}, - {file = "time_machine-2.15.0-cp39-cp39-win32.whl", hash = "sha256:1168eebd7af7e6e3e2fd378c16ca917b97dd81c89a1f1f9e1daa985c81699d90"}, - {file = "time_machine-2.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:c344eb09fcfbf71e5b5847d4f188fec98e1c3a976125ef571eac5f1c39e7a5e5"}, - {file = "time_machine-2.15.0-cp39-cp39-win_arm64.whl", hash = "sha256:899f1a856b3bebb82b6cbc3c0014834b583b83f246b28e462a031ec1b766130b"}, - {file = "time_machine-2.15.0.tar.gz", hash = "sha256:ebd2e63baa117ded04b978813fcd1279d3fc6be2149c9cac75c716b6f1db774c"}, -] - -[[package]] -name = "toml" -version = "0.10.2" -requires_python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -summary = "Python Library for Tom's Obvious, Minimal Language" -groups = ["dev"] -files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] - -[[package]] -name = "tomli" -version = "2.0.1" -requires_python = ">=3.7" -summary = "A lil' TOML parser" -groups = ["dev", "full", "linting", "piccolo", "test"] -marker = "python_version < \"3.11\"" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - -[[package]] -name = "tornado" -version = "6.4.1" -requires_python = ">=3.8" -summary = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." -groups = ["docs"] -files = [ - {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"}, - {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"}, - {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"}, - {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"}, - {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, -] - -[[package]] -name = "tree-sitter" -version = "0.20.4" -requires_python = ">=3.3" -summary = "Python bindings for the Tree-Sitter parsing library" -groups = ["linting"] -dependencies = [ - "setuptools>=60.0.0; python_version >= \"3.12\"", -] -files = [ - {file = "tree_sitter-0.20.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c259b9bcb596e54f54713eb3951226fc834d65289940f4bfdcdf519f08e8e876"}, - {file = "tree_sitter-0.20.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:88da7e2e4c69881cd63916cc24ae0b809f96aae331da45b418ae6b2d1ed2ca19"}, - {file = "tree_sitter-0.20.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66a68b156ba131e9d8dff4a1f72037f4b368cc50c58f18905a91743ae1d1c795"}, - {file = "tree_sitter-0.20.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae28e25d551f406807011487bdfb9728041e656b30b554fa7f3391ab64ed69f9"}, - {file = "tree_sitter-0.20.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36b10c9c69e825ba65cf9b0f77668bf33e70d2a5764b64ad6f133f8cc9220f09"}, - {file = "tree_sitter-0.20.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7c18c64ddd44b75b7e1660b9793753eda427e4b145b6216d4b2d2e9b200c74f2"}, - {file = "tree_sitter-0.20.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e9e9e594bbefb76ad9ea256f5c87eba7591b4758854d3df83ce4df415933a006"}, - {file = "tree_sitter-0.20.4-cp310-cp310-win32.whl", hash = "sha256:b4755229dc18644fe48bcab974bde09b171fcb6ef625d3cb5ece5c6198f4223e"}, - {file = "tree_sitter-0.20.4-cp310-cp310-win_amd64.whl", hash = "sha256:f792684cee8a46d9194d9f4223810e54ccc704470c5777538d59fbde0a4c91bf"}, - {file = "tree_sitter-0.20.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9d22ee75f45836554ee6a11e50dd8f9827941e67c49fce9a0790245b899811a9"}, - {file = "tree_sitter-0.20.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a0ffd76dd991ba745bb5d0ba1d583bec85726d3ddef8c9685dc8636a619adde"}, - {file = "tree_sitter-0.20.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:060d4e5b803be0975f1ac46e54a292eab0701296ccd912f6cdac3f7331e29143"}, - {file = "tree_sitter-0.20.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:822e02366dbf223697b2b56b8f91aa5b60571f9fe7c998988a381db1c69604e9"}, - {file = "tree_sitter-0.20.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:527ca72c6a8f60fa719af37fa86f58b7ad0e07b8f74d1c1c7e926c5c888a7e6b"}, - {file = "tree_sitter-0.20.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a418ca71309ea7052e076f08d623f33f58eae01a8e8cdc1e6d3a01b5b8ddebfe"}, - {file = "tree_sitter-0.20.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:08c3ba2561b61a83c28ca06a0bce2a5ffcfb6b39f9d27a45e5ebd9cad2bedb7f"}, - {file = "tree_sitter-0.20.4-cp311-cp311-win32.whl", hash = "sha256:8d04c75a389b2de94952d602264852acff8cd3ed1ccf8a2492a080973d5ddd58"}, - {file = "tree_sitter-0.20.4-cp311-cp311-win_amd64.whl", hash = "sha256:ba9215c0e7529d9eb370528e5d99b7389d14a7eae94f07d14fa9dab18f267c62"}, - {file = "tree_sitter-0.20.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c4c1af5ed4306071d30970c83ec882520a7bf5d8053996dbc4aa5c59238d4990"}, - {file = "tree_sitter-0.20.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9d70bfa550cf22c9cea9b3c0d18b889fc4f2a7e9dcf1d6cc93f49fa9d4a94954"}, - {file = "tree_sitter-0.20.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6de537bca0641775d8d175d37303d54998980fc0d997dd9aa89e16b415bf0cc3"}, - {file = "tree_sitter-0.20.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b1c0f8c0e3e50267566f5116cdceedf4e23e8c08b55ef3becbe954a11b16e84"}, - {file = "tree_sitter-0.20.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef2ee6d9bb8e21713949e5ff769ed670fe1217f95b7eeb6c675788438c1e6e"}, - {file = "tree_sitter-0.20.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b6fd1c881ab0de5faa67168db2d001eee32be5482cb4e0b21b217689a05b6fe4"}, - {file = "tree_sitter-0.20.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bf47047420021d50aec529cb66387c90562350b499ddf56ecef1fc8255439e30"}, - {file = "tree_sitter-0.20.4-cp312-cp312-win32.whl", hash = "sha256:c16b48378041fc9702b6aa3480f2ffa49ca8ea58141a862acd569e5a0679655f"}, - {file = "tree_sitter-0.20.4-cp312-cp312-win_amd64.whl", hash = "sha256:973e871167079a1b1d7304d361449253efbe2a6974728ad563cf407bd02ddccb"}, - {file = "tree_sitter-0.20.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f42fd1104efaad8151370f1936e2a488b7337a5d24544a9ab59ba4c4010b1272"}, - {file = "tree_sitter-0.20.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7859717c5d62ee386b3d036cab8ed0f88f8c027b6b4ae476a55a8c5fb8aab713"}, - {file = "tree_sitter-0.20.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fdd361fe1cc68db68b4d85165641275e34b86cc26b2bab932790204fa14824dc"}, - {file = "tree_sitter-0.20.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b8d7539075606027b67764543463ff2bc4e52f4158ef6dc419c9f5625aa5383"}, - {file = "tree_sitter-0.20.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78e76307f05aca6cde72f3307b4d53701f34ae45f2248ceb83d1626051e201fd"}, - {file = "tree_sitter-0.20.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dd8c352f4577f61098d06cf3feb7fd214259f41b5036b81003860ed54d16b448"}, - {file = "tree_sitter-0.20.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:281f3e5382d1bd7fccc88d1afe68c915565bc24f8b8dd4844079d46c7815b8a7"}, - {file = "tree_sitter-0.20.4-cp38-cp38-win32.whl", hash = "sha256:6a77ac3cdcddd80cdd1fd394318bff99f94f37e08d235aaefccb87e1224946e5"}, - {file = "tree_sitter-0.20.4-cp38-cp38-win_amd64.whl", hash = "sha256:8eee8adf54033dc48eab84b040f4d7b32355a964c4ae0aae5dfbdc4dbc3364ca"}, - {file = "tree_sitter-0.20.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e89f6508e30fce05e2c724725d022db30d877817b9d64f933506ffb3a3f4a2c2"}, - {file = "tree_sitter-0.20.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7fb6286bb1fae663c45ff0700ec88fb9b50a81eed2bae8a291f95fcf8cc19547"}, - {file = "tree_sitter-0.20.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11e93f8b4bbae04070416a82257a7ab2eb0afb76e093ae3ea73bd63b792f6846"}, - {file = "tree_sitter-0.20.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8250725c5f78929aeb2c71db5dca76f1ef448389ca16f9439161f90978bb8478"}, - {file = "tree_sitter-0.20.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d404a8ca9de9b0843844f0cd4d423f46bc46375ab8afb63b1d8ec01201457ac8"}, - {file = "tree_sitter-0.20.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0f2422c9ee70ba972dfc3943746e6cf7fc03725a866908950245bda9ccfc7301"}, - {file = "tree_sitter-0.20.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:21a937942e4729abbe778a609d2c218574436cb351c36fba89ef3c8c6066ec78"}, - {file = "tree_sitter-0.20.4-cp39-cp39-win32.whl", hash = "sha256:427a9a39360cc1816e28f8182550e478e4ba983595a2565ab9dfe32ea1b03fd7"}, - {file = "tree_sitter-0.20.4-cp39-cp39-win_amd64.whl", hash = "sha256:7095bb9aff297fa9c6026bf8914fd295997d714d1a6ee9a1edf7282c772f9f64"}, - {file = "tree_sitter-0.20.4-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:859260b90f0e3867ae840e39f54e830f607b3bc531bc21deeeeaa8a30cbb89ad"}, - {file = "tree_sitter-0.20.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dfc14be73cf46126660a3aecdd0396e69562ad1a902245225ca7bd29649594e"}, - {file = "tree_sitter-0.20.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec46355bf3ff23f54d5e365871ffd3e05cfbc65d1b36a8be7c0bcbda30a1d43"}, - {file = "tree_sitter-0.20.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d933a942fde39876b99c36f12aa3764e4a555ae9366c10ce6cca8c16341c1bbf"}, - {file = "tree_sitter-0.20.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:36f8adf2126f496cf376b6e4b707cba061c25beb17841727eef6f0e083e53e1f"}, - {file = "tree_sitter-0.20.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:841efb40c116ab0a066924925409a8a4dcffeb39a151c0b2a1c2abe56ad4fb42"}, - {file = "tree_sitter-0.20.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2051e8a70fd8426f27a43dad71d11929a62ce30a9b1eb65bba0ed79e82481592"}, - {file = "tree_sitter-0.20.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:99a3c2824d4cfcffd9f961176891426bde2cb36ece5280c61480be93319c23c4"}, - {file = "tree_sitter-0.20.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:72830dc85a10430eca3d56739b7efcd7a05459c8d425f08c1aee6179ab7f13a9"}, - {file = "tree_sitter-0.20.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4992dd226055b6cd0a4f5661c66b799a73d3eff716302e0f7ab06594ee12d49f"}, - {file = "tree_sitter-0.20.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a66d95bbf92175cdc295d6d77f330942811f02e3aaf3fc64431cb749683b2f7d"}, - {file = "tree_sitter-0.20.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a25b1087e4f7825b2458dacf5f4b0be2938f78e850e822edca1ff4994b56081a"}, - {file = "tree_sitter-0.20.4.tar.gz", hash = "sha256:6adb123e2f3e56399bbf2359924633c882cc40ee8344885200bca0922f713be5"}, -] - -[[package]] -name = "trio" -version = "0.26.2" -requires_python = ">=3.8" -summary = "A friendly Python library for async concurrency and I/O" -groups = ["dev"] -dependencies = [ - "attrs>=23.2.0", - "cffi>=1.14; os_name == \"nt\" and implementation_name != \"pypy\"", - "exceptiongroup; python_version < \"3.11\"", - "idna", - "outcome", - "sniffio>=1.3.0", - "sortedcontainers", -] -files = [ - {file = "trio-0.26.2-py3-none-any.whl", hash = "sha256:c5237e8133eb0a1d72f09a971a55c28ebe69e351c783fc64bc37db8db8bbe1d0"}, - {file = "trio-0.26.2.tar.gz", hash = "sha256:0346c3852c15e5c7d40ea15972c4805689ef2cb8b5206f794c9c19450119f3a4"}, -] - -[[package]] -name = "twisted" -version = "24.7.0" -requires_python = ">=3.8.0" -summary = "An asynchronous networking framework written in Python" -groups = ["dev"] -dependencies = [ - "attrs>=21.3.0", - "automat>=0.8.0", - "constantly>=15.1", - "hyperlink>=17.1.1", - "incremental>=24.7.0", - "typing-extensions>=4.2.0", - "zope-interface>=5", -] -files = [ - {file = "twisted-24.7.0-py3-none-any.whl", hash = "sha256:734832ef98108136e222b5230075b1079dad8a3fc5637319615619a7725b0c81"}, - {file = "twisted-24.7.0.tar.gz", hash = "sha256:5a60147f044187a127ec7da96d170d49bcce50c6fd36f594e60f4587eff4d394"}, -] - -[[package]] -name = "twisted" -version = "24.7.0" -extras = ["tls"] -requires_python = ">=3.8.0" -summary = "An asynchronous networking framework written in Python" -groups = ["dev"] -dependencies = [ - "idna>=2.4", - "pyopenssl>=21.0.0", - "service-identity>=18.1.0", - "twisted==24.7.0", -] -files = [ - {file = "twisted-24.7.0-py3-none-any.whl", hash = "sha256:734832ef98108136e222b5230075b1079dad8a3fc5637319615619a7725b0c81"}, - {file = "twisted-24.7.0.tar.gz", hash = "sha256:5a60147f044187a127ec7da96d170d49bcce50c6fd36f594e60f4587eff4d394"}, -] - -[[package]] -name = "txaio" -version = "23.1.1" -requires_python = ">=3.7" -summary = "Compatibility API between asyncio/Twisted/Trollius" -groups = ["dev"] -files = [ - {file = "txaio-23.1.1-py2.py3-none-any.whl", hash = "sha256:aaea42f8aad50e0ecfb976130ada140797e9dcb85fad2cf72b0f37f8cefcb490"}, - {file = "txaio-23.1.1.tar.gz", hash = "sha256:f9a9216e976e5e3246dfd112ad7ad55ca915606b60b84a757ac769bd404ff704"}, -] - -[[package]] -name = "types-beautifulsoup4" -version = "4.12.0.20240511" -requires_python = ">=3.8" -summary = "Typing stubs for beautifulsoup4" -groups = ["linting"] -dependencies = [ - "types-html5lib", -] -files = [ - {file = "types-beautifulsoup4-4.12.0.20240511.tar.gz", hash = "sha256:004f6096fdd83b19cdbf6cb10e4eae57b10205eccc365d0a69d77da836012e28"}, - {file = "types_beautifulsoup4-4.12.0.20240511-py3-none-any.whl", hash = "sha256:7ceda66a93ba28d759d5046d7fec9f4cad2f563a77b3a789efc90bcadafeefd1"}, -] - -[[package]] -name = "types-cffi" -version = "1.16.0.20240331" -requires_python = ">=3.8" -summary = "Typing stubs for cffi" -groups = ["linting"] -dependencies = [ - "types-setuptools", -] -files = [ - {file = "types-cffi-1.16.0.20240331.tar.gz", hash = "sha256:b8b20d23a2b89cfed5f8c5bc53b0cb8677c3aac6d970dbc771e28b9c698f5dee"}, - {file = "types_cffi-1.16.0.20240331-py3-none-any.whl", hash = "sha256:a363e5ea54a4eb6a4a105d800685fde596bc318089b025b27dee09849fe41ff0"}, -] - -[[package]] -name = "types-html5lib" -version = "1.1.11.20240806" -requires_python = ">=3.8" -summary = "Typing stubs for html5lib" -groups = ["linting"] -files = [ - {file = "types-html5lib-1.1.11.20240806.tar.gz", hash = "sha256:8060dc98baf63d6796a765bbbc809fff9f7a383f6e3a9add526f814c086545ef"}, - {file = "types_html5lib-1.1.11.20240806-py3-none-any.whl", hash = "sha256:575c4fd84ba8eeeaa8520c7e4c7042b7791f5ec3e9c0a5d5c418124c42d9e7e4"}, -] - -[[package]] -name = "types-psutil" -version = "6.0.0.20240621" -requires_python = ">=3.8" -summary = "Typing stubs for psutil" -groups = ["linting"] -files = [ - {file = "types-psutil-6.0.0.20240621.tar.gz", hash = "sha256:1be027326c42ff51ebd65255a5146f9dc57e5cf8c4f9519a88b3f3f6a7fcd00e"}, - {file = "types_psutil-6.0.0.20240621-py3-none-any.whl", hash = "sha256:b02f05d2c4141cd5926d82d8b56e4292a4d8f483d8a3400b73edf153834a3c64"}, -] - -[[package]] -name = "types-pyasn1" -version = "0.6.0.20240402" -requires_python = ">=3.8" -summary = "Typing stubs for pyasn1" -groups = ["linting"] -files = [ - {file = "types-pyasn1-0.6.0.20240402.tar.gz", hash = "sha256:5d54dcb33f69dd269071ca098e923ac20c5f03c814631fa7f3ed9ee035a5da3a"}, - {file = "types_pyasn1-0.6.0.20240402-py3-none-any.whl", hash = "sha256:848d01e7313c200acc035a8b3d377fe7b2aecbe77f2be49eb160a7f82835aaaf"}, -] - -[[package]] -name = "types-pyopenssl" -version = "24.1.0.20240722" -requires_python = ">=3.8" -summary = "Typing stubs for pyOpenSSL" -groups = ["linting"] -dependencies = [ - "cryptography>=35.0.0", - "types-cffi", -] -files = [ - {file = "types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39"}, - {file = "types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54"}, -] - -[[package]] -name = "types-python-jose" -version = "3.3.4.20240106" -requires_python = ">=3.8" -summary = "Typing stubs for python-jose" -groups = ["linting"] -dependencies = [ - "types-pyasn1", -] -files = [ - {file = "types-python-jose-3.3.4.20240106.tar.gz", hash = "sha256:b18cf8c5080bbfe1ef7c3b707986435d9efca3e90889acb6a06f65e06bc3405a"}, - {file = "types_python_jose-3.3.4.20240106-py3-none-any.whl", hash = "sha256:b515a6c0c61f5e2a53bc93e3a2b024cbd42563e2e19cbde9fd1c2cc2cfe77ccc"}, -] - -[[package]] -name = "types-pyyaml" -version = "6.0.12.20240808" -requires_python = ">=3.8" -summary = "Typing stubs for PyYAML" -groups = ["linting"] -files = [ - {file = "types-PyYAML-6.0.12.20240808.tar.gz", hash = "sha256:b8f76ddbd7f65440a8bda5526a9607e4c7a322dc2f8e1a8c405644f9a6f4b9af"}, - {file = "types_PyYAML-6.0.12.20240808-py3-none-any.whl", hash = "sha256:deda34c5c655265fc517b546c902aa6eed2ef8d3e921e4765fe606fe2afe8d35"}, -] - -[[package]] -name = "types-redis" -version = "4.6.0.20240819" -requires_python = ">=3.8" -summary = "Typing stubs for redis" -groups = ["linting"] -dependencies = [ - "cryptography>=35.0.0", - "types-pyOpenSSL", -] -files = [ - {file = "types-redis-4.6.0.20240819.tar.gz", hash = "sha256:08f51f550ad41d0152bd98d77ac9d6d8f761369121710a213642f6036b9a7183"}, - {file = "types_redis-4.6.0.20240819-py3-none-any.whl", hash = "sha256:86db9af6f0033154e12bc22c77236cef0907b995fda8c9f0f0eacd59943ed2fc"}, -] - -[[package]] -name = "types-setuptools" -version = "73.0.0.20240822" -requires_python = ">=3.8" -summary = "Typing stubs for setuptools" -groups = ["linting"] -files = [ - {file = "types-setuptools-73.0.0.20240822.tar.gz", hash = "sha256:3a060681098eb3fbc2fea0a86f7f6af6aa1ca71906039d88d891ea2cecdd4dbf"}, - {file = "types_setuptools-73.0.0.20240822-py3-none-any.whl", hash = "sha256:b9eba9b68546031317a0fa506d4973641d987d74f79e7dd8369ad4f7a93dea17"}, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -requires_python = ">=3.8" -summary = "Backported and Experimental Type Hints for Python 3.8+" -groups = ["default", "annotated-types", "cli", "dev", "dev-contrib", "docs", "full", "linting", "opentelemetry", "piccolo", "pydantic", "sqlalchemy", "standard"] -files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] - -[[package]] -name = "tzdata" -version = "2024.1" -requires_python = ">=2" -summary = "Provider of IANA time zone data" -groups = ["dev"] -marker = "sys_platform == \"win32\"" -files = [ - {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, - {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, -] - -[[package]] -name = "urllib3" -version = "2.2.2" -requires_python = ">=3.8" -summary = "HTTP library with thread-safe connection pooling, file post, and more." -groups = ["docs", "linting"] -files = [ - {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, - {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, -] - -[[package]] -name = "uvicorn" -version = "0.30.6" -requires_python = ">=3.8" -summary = "The lightning-fast ASGI server." -groups = ["cli", "full", "standard"] -dependencies = [ - "click>=7.0", - "h11>=0.8", - "typing-extensions>=4.0; python_version < \"3.11\"", -] -files = [ - {file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"}, - {file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"}, -] - -[[package]] -name = "uvicorn" -version = "0.30.6" -extras = ["standard"] -requires_python = ">=3.8" -summary = "The lightning-fast ASGI server." -groups = ["cli", "full", "standard"] -dependencies = [ - "colorama>=0.4; sys_platform == \"win32\"", - "httptools>=0.5.0", - "python-dotenv>=0.13", - "pyyaml>=5.1", - "uvicorn==0.30.6", - "uvloop!=0.15.0,!=0.15.1,>=0.14.0; (sys_platform != \"cygwin\" and sys_platform != \"win32\") and platform_python_implementation != \"PyPy\"", - "watchfiles>=0.13", - "websockets>=10.4", -] -files = [ - {file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"}, - {file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"}, -] - -[[package]] -name = "uvloop" -version = "0.20.0" -requires_python = ">=3.8.0" -summary = "Fast implementation of asyncio event loop on top of libuv" -groups = ["cli", "full", "standard"] -marker = "sys_platform != \"win32\"" -files = [ - {file = "uvloop-0.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9ebafa0b96c62881d5cafa02d9da2e44c23f9f0cd829f3a32a6aff771449c996"}, - {file = "uvloop-0.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:35968fc697b0527a06e134999eef859b4034b37aebca537daeb598b9d45a137b"}, - {file = "uvloop-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b16696f10e59d7580979b420eedf6650010a4a9c3bd8113f24a103dfdb770b10"}, - {file = "uvloop-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b04d96188d365151d1af41fa2d23257b674e7ead68cfd61c725a422764062ae"}, - {file = "uvloop-0.20.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:94707205efbe809dfa3a0d09c08bef1352f5d3d6612a506f10a319933757c006"}, - {file = "uvloop-0.20.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89e8d33bb88d7263f74dc57d69f0063e06b5a5ce50bb9a6b32f5fcbe655f9e73"}, - {file = "uvloop-0.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e50289c101495e0d1bb0bfcb4a60adde56e32f4449a67216a1ab2750aa84f037"}, - {file = "uvloop-0.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e237f9c1e8a00e7d9ddaa288e535dc337a39bcbf679f290aee9d26df9e72bce9"}, - {file = "uvloop-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:746242cd703dc2b37f9d8b9f173749c15e9a918ddb021575a0205ec29a38d31e"}, - {file = "uvloop-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82edbfd3df39fb3d108fc079ebc461330f7c2e33dbd002d146bf7c445ba6e756"}, - {file = "uvloop-0.20.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:80dc1b139516be2077b3e57ce1cb65bfed09149e1d175e0478e7a987863b68f0"}, - {file = "uvloop-0.20.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f44af67bf39af25db4c1ac27e82e9665717f9c26af2369c404be865c8818dcf"}, - {file = "uvloop-0.20.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4b75f2950ddb6feed85336412b9a0c310a2edbcf4cf931aa5cfe29034829676d"}, - {file = "uvloop-0.20.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:77fbc69c287596880ecec2d4c7a62346bef08b6209749bf6ce8c22bbaca0239e"}, - {file = "uvloop-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6462c95f48e2d8d4c993a2950cd3d31ab061864d1c226bbf0ee2f1a8f36674b9"}, - {file = "uvloop-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:649c33034979273fa71aa25d0fe120ad1777c551d8c4cd2c0c9851d88fcb13ab"}, - {file = "uvloop-0.20.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a609780e942d43a275a617c0839d85f95c334bad29c4c0918252085113285b5"}, - {file = "uvloop-0.20.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aea15c78e0d9ad6555ed201344ae36db5c63d428818b4b2a42842b3870127c00"}, - {file = "uvloop-0.20.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f0e94b221295b5e69de57a1bd4aeb0b3a29f61be6e1b478bb8a69a73377db7ba"}, - {file = "uvloop-0.20.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fee6044b64c965c425b65a4e17719953b96e065c5b7e09b599ff332bb2744bdf"}, - {file = "uvloop-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:265a99a2ff41a0fd56c19c3838b29bf54d1d177964c300dad388b27e84fd7847"}, - {file = "uvloop-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10c2956efcecb981bf9cfb8184d27d5d64b9033f917115a960b83f11bfa0d6b"}, - {file = "uvloop-0.20.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e7d61fe8e8d9335fac1bf8d5d82820b4808dd7a43020c149b63a1ada953d48a6"}, - {file = "uvloop-0.20.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2beee18efd33fa6fdb0976e18475a4042cd31c7433c866e8a09ab604c7c22ff2"}, - {file = "uvloop-0.20.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8c36fdf3e02cec92aed2d44f63565ad1522a499c654f07935c8f9d04db69e95"}, - {file = "uvloop-0.20.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a0fac7be202596c7126146660725157d4813aa29a4cc990fe51346f75ff8fde7"}, - {file = "uvloop-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d0fba61846f294bce41eb44d60d58136090ea2b5b99efd21cbdf4e21927c56a"}, - {file = "uvloop-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95720bae002ac357202e0d866128eb1ac82545bcf0b549b9abe91b5178d9b541"}, - {file = "uvloop-0.20.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:36c530d8fa03bfa7085af54a48f2ca16ab74df3ec7108a46ba82fd8b411a2315"}, - {file = "uvloop-0.20.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e97152983442b499d7a71e44f29baa75b3b02e65d9c44ba53b10338e98dedb66"}, - {file = "uvloop-0.20.0.tar.gz", hash = "sha256:4603ca714a754fc8d9b197e325db25b2ea045385e8a3ad05d3463de725fdf469"}, -] - -[[package]] -name = "virtualenv" -version = "20.26.3" -requires_python = ">=3.7" -summary = "Virtual Python Environment builder" -groups = ["linting"] -dependencies = [ - "distlib<1,>=0.3.7", - "filelock<4,>=3.12.2", - "importlib-metadata>=6.6; python_version < \"3.8\"", - "platformdirs<5,>=3.9.1", -] -files = [ - {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, - {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, -] - -[[package]] -name = "watchfiles" -version = "0.23.0" -requires_python = ">=3.8" -summary = "Simple, modern and high performance file watching and code reload in python." -groups = ["cli", "full", "standard"] -dependencies = [ - "anyio>=3.0.0", -] -files = [ - {file = "watchfiles-0.23.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:bee8ce357a05c20db04f46c22be2d1a2c6a8ed365b325d08af94358e0688eeb4"}, - {file = "watchfiles-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ccd3011cc7ee2f789af9ebe04745436371d36afe610028921cab9f24bb2987b"}, - {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb02d41c33be667e6135e6686f1bb76104c88a312a18faa0ef0262b5bf7f1a0f"}, - {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf12ac34c444362f3261fb3ff548f0037ddd4c5bb85f66c4be30d2936beb3c5"}, - {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0b2c25040a3c0ce0e66c7779cc045fdfbbb8d59e5aabfe033000b42fe44b53e"}, - {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf2be4b9eece4f3da8ba5f244b9e51932ebc441c0867bd6af46a3d97eb068d6"}, - {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40cb8fa00028908211eb9f8d47744dca21a4be6766672e1ff3280bee320436f1"}, - {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f48c917ffd36ff9a5212614c2d0d585fa8b064ca7e66206fb5c095015bc8207"}, - {file = "watchfiles-0.23.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9d183e3888ada88185ab17064079c0db8c17e32023f5c278d7bf8014713b1b5b"}, - {file = "watchfiles-0.23.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9837edf328b2805346f91209b7e660f65fb0e9ca18b7459d075d58db082bf981"}, - {file = "watchfiles-0.23.0-cp310-none-win32.whl", hash = "sha256:296e0b29ab0276ca59d82d2da22cbbdb39a23eed94cca69aed274595fb3dfe42"}, - {file = "watchfiles-0.23.0-cp310-none-win_amd64.whl", hash = "sha256:4ea756e425ab2dfc8ef2a0cb87af8aa7ef7dfc6fc46c6f89bcf382121d4fff75"}, - {file = "watchfiles-0.23.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:e397b64f7aaf26915bf2ad0f1190f75c855d11eb111cc00f12f97430153c2eab"}, - {file = "watchfiles-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4ac73b02ca1824ec0a7351588241fd3953748d3774694aa7ddb5e8e46aef3e3"}, - {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130a896d53b48a1cecccfa903f37a1d87dbb74295305f865a3e816452f6e49e4"}, - {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c5e7803a65eb2d563c73230e9d693c6539e3c975ccfe62526cadde69f3fda0cf"}, - {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1aa4cc85202956d1a65c88d18c7b687b8319dbe6b1aec8969784ef7a10e7d1a"}, - {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87f889f6e58849ddb7c5d2cb19e2e074917ed1c6e3ceca50405775166492cca8"}, - {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37fd826dac84c6441615aa3f04077adcc5cac7194a021c9f0d69af20fb9fa788"}, - {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee7db6e36e7a2c15923072e41ea24d9a0cf39658cb0637ecc9307b09d28827e1"}, - {file = "watchfiles-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2368c5371c17fdcb5a2ea71c5c9d49f9b128821bfee69503cc38eae00feb3220"}, - {file = "watchfiles-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:857af85d445b9ba9178db95658c219dbd77b71b8264e66836a6eba4fbf49c320"}, - {file = "watchfiles-0.23.0-cp311-none-win32.whl", hash = "sha256:1d636c8aeb28cdd04a4aa89030c4b48f8b2954d8483e5f989774fa441c0ed57b"}, - {file = "watchfiles-0.23.0-cp311-none-win_amd64.whl", hash = "sha256:46f1d8069a95885ca529645cdbb05aea5837d799965676e1b2b1f95a4206313e"}, - {file = "watchfiles-0.23.0-cp311-none-win_arm64.whl", hash = "sha256:e495ed2a7943503766c5d1ff05ae9212dc2ce1c0e30a80d4f0d84889298fa304"}, - {file = "watchfiles-0.23.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1db691bad0243aed27c8354b12d60e8e266b75216ae99d33e927ff5238d270b5"}, - {file = "watchfiles-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:62d2b18cb1edaba311fbbfe83fb5e53a858ba37cacb01e69bc20553bb70911b8"}, - {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e087e8fdf1270d000913c12e6eca44edd02aad3559b3e6b8ef00f0ce76e0636f"}, - {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd41d5c72417b87c00b1b635738f3c283e737d75c5fa5c3e1c60cd03eac3af77"}, - {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e5f3ca0ff47940ce0a389457b35d6df601c317c1e1a9615981c474452f98de1"}, - {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6991e3a78f642368b8b1b669327eb6751439f9f7eaaa625fae67dd6070ecfa0b"}, - {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f7252f52a09f8fa5435dc82b6af79483118ce6bd51eb74e6269f05ee22a7b9f"}, - {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e01bcb8d767c58865207a6c2f2792ad763a0fe1119fb0a430f444f5b02a5ea0"}, - {file = "watchfiles-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8e56fbcdd27fce061854ddec99e015dd779cae186eb36b14471fc9ae713b118c"}, - {file = "watchfiles-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bd3e2d64500a6cad28bcd710ee6269fbeb2e5320525acd0cfab5f269ade68581"}, - {file = "watchfiles-0.23.0-cp312-none-win32.whl", hash = "sha256:eb99c954291b2fad0eff98b490aa641e128fbc4a03b11c8a0086de8b7077fb75"}, - {file = "watchfiles-0.23.0-cp312-none-win_amd64.whl", hash = "sha256:dccc858372a56080332ea89b78cfb18efb945da858fabeb67f5a44fa0bcb4ebb"}, - {file = "watchfiles-0.23.0-cp312-none-win_arm64.whl", hash = "sha256:6c21a5467f35c61eafb4e394303720893066897fca937bade5b4f5877d350ff8"}, - {file = "watchfiles-0.23.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ba31c32f6b4dceeb2be04f717811565159617e28d61a60bb616b6442027fd4b9"}, - {file = "watchfiles-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85042ab91814fca99cec4678fc063fb46df4cbb57b4835a1cc2cb7a51e10250e"}, - {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24655e8c1c9c114005c3868a3d432c8aa595a786b8493500071e6a52f3d09217"}, - {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6b1a950ab299a4a78fd6369a97b8763732bfb154fdb433356ec55a5bce9515c1"}, - {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8d3c5cd327dd6ce0edfc94374fb5883d254fe78a5e9d9dfc237a1897dc73cd1"}, - {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ff785af8bacdf0be863ec0c428e3288b817e82f3d0c1d652cd9c6d509020dd0"}, - {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02b7ba9d4557149410747353e7325010d48edcfe9d609a85cb450f17fd50dc3d"}, - {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a1b05c0afb2cd2f48c1ed2ae5487b116e34b93b13074ed3c22ad5c743109f0"}, - {file = "watchfiles-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:109a61763e7318d9f821b878589e71229f97366fa6a5c7720687d367f3ab9eef"}, - {file = "watchfiles-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9f8e6bb5ac007d4a4027b25f09827ed78cbbd5b9700fd6c54429278dacce05d1"}, - {file = "watchfiles-0.23.0-cp313-none-win32.whl", hash = "sha256:f46c6f0aec8d02a52d97a583782d9af38c19a29900747eb048af358a9c1d8e5b"}, - {file = "watchfiles-0.23.0-cp313-none-win_amd64.whl", hash = "sha256:f449afbb971df5c6faeb0a27bca0427d7b600dd8f4a068492faec18023f0dcff"}, - {file = "watchfiles-0.23.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:2dddc2487d33e92f8b6222b5fb74ae2cfde5e8e6c44e0248d24ec23befdc5366"}, - {file = "watchfiles-0.23.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e75695cc952e825fa3e0684a7f4a302f9128721f13eedd8dbd3af2ba450932b8"}, - {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2537ef60596511df79b91613a5bb499b63f46f01a11a81b0a2b0dedf645d0a9c"}, - {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20b423b58f5fdde704a226b598a2d78165fe29eb5621358fe57ea63f16f165c4"}, - {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b98732ec893975455708d6fc9a6daab527fc8bbe65be354a3861f8c450a632a4"}, - {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee1f5fcbf5bc33acc0be9dd31130bcba35d6d2302e4eceafafd7d9018c7755ab"}, - {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f195338a5a7b50a058522b39517c50238358d9ad8284fd92943643144c0c03"}, - {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:524fcb8d59b0dbee2c9b32207084b67b2420f6431ed02c18bd191e6c575f5c48"}, - {file = "watchfiles-0.23.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0eff099a4df36afaa0eea7a913aa64dcf2cbd4e7a4f319a73012210af4d23810"}, - {file = "watchfiles-0.23.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a8323daae27ea290ba3350c70c836c0d2b0fb47897fa3b0ca6a5375b952b90d3"}, - {file = "watchfiles-0.23.0-cp38-none-win32.whl", hash = "sha256:aafea64a3ae698695975251f4254df2225e2624185a69534e7fe70581066bc1b"}, - {file = "watchfiles-0.23.0-cp38-none-win_amd64.whl", hash = "sha256:c846884b2e690ba62a51048a097acb6b5cd263d8bd91062cd6137e2880578472"}, - {file = "watchfiles-0.23.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a753993635eccf1ecb185dedcc69d220dab41804272f45e4aef0a67e790c3eb3"}, - {file = "watchfiles-0.23.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6bb91fa4d0b392f0f7e27c40981e46dda9eb0fbc84162c7fb478fe115944f491"}, - {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1f67312efa3902a8e8496bfa9824d3bec096ff83c4669ea555c6bdd213aa516"}, - {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ca6b71dcc50d320c88fb2d88ecd63924934a8abc1673683a242a7ca7d39e781"}, - {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aec5c29915caf08771d2507da3ac08e8de24a50f746eb1ed295584ba1820330"}, - {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1733b9bc2c8098c6bdb0ff7a3d7cb211753fecb7bd99bdd6df995621ee1a574b"}, - {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02ff5d7bd066c6a7673b17c8879cd8ee903078d184802a7ee851449c43521bdd"}, - {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18e2de19801b0eaa4c5292a223effb7cfb43904cb742c5317a0ac686ed604765"}, - {file = "watchfiles-0.23.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8ada449e22198c31fb013ae7e9add887e8d2bd2335401abd3cbc55f8c5083647"}, - {file = "watchfiles-0.23.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3af1b05361e1cc497bf1be654a664750ae61f5739e4bb094a2be86ec8c6db9b6"}, - {file = "watchfiles-0.23.0-cp39-none-win32.whl", hash = "sha256:486bda18be5d25ab5d932699ceed918f68eb91f45d018b0343e3502e52866e5e"}, - {file = "watchfiles-0.23.0-cp39-none-win_amd64.whl", hash = "sha256:d2d42254b189a346249424fb9bb39182a19289a2409051ee432fb2926bad966a"}, - {file = "watchfiles-0.23.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6a9265cf87a5b70147bfb2fec14770ed5b11a5bb83353f0eee1c25a81af5abfe"}, - {file = "watchfiles-0.23.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9f02a259fcbbb5fcfe7a0805b1097ead5ba7a043e318eef1db59f93067f0b49b"}, - {file = "watchfiles-0.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ebaebb53b34690da0936c256c1cdb0914f24fb0e03da76d185806df9328abed"}, - {file = "watchfiles-0.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd257f98cff9c6cb39eee1a83c7c3183970d8a8d23e8cf4f47d9a21329285cee"}, - {file = "watchfiles-0.23.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aba037c1310dd108411d27b3d5815998ef0e83573e47d4219f45753c710f969f"}, - {file = "watchfiles-0.23.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:a96ac14e184aa86dc43b8a22bb53854760a58b2966c2b41580de938e9bf26ed0"}, - {file = "watchfiles-0.23.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11698bb2ea5e991d10f1f4f83a39a02f91e44e4bd05f01b5c1ec04c9342bf63c"}, - {file = "watchfiles-0.23.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efadd40fca3a04063d40c4448c9303ce24dd6151dc162cfae4a2a060232ebdcb"}, - {file = "watchfiles-0.23.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:556347b0abb4224c5ec688fc58214162e92a500323f50182f994f3ad33385dcb"}, - {file = "watchfiles-0.23.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1cf7f486169986c4b9d34087f08ce56a35126600b6fef3028f19ca16d5889071"}, - {file = "watchfiles-0.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f18de0f82c62c4197bea5ecf4389288ac755896aac734bd2cc44004c56e4ac47"}, - {file = "watchfiles-0.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:532e1f2c491274d1333a814e4c5c2e8b92345d41b12dc806cf07aaff786beb66"}, - {file = "watchfiles-0.23.0.tar.gz", hash = "sha256:9338ade39ff24f8086bb005d16c29f8e9f19e55b18dcb04dfa26fcbc09da497b"}, -] - -[[package]] -name = "webencodings" -version = "0.5.1" -summary = "Character encoding aliases for legacy web content" -groups = ["docs"] -files = [ - {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, - {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, -] - -[[package]] -name = "websockets" -version = "13.0" -requires_python = ">=3.8" -summary = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -groups = ["cli", "full", "standard"] -files = [ - {file = "websockets-13.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ad4fa707ff9e2ffee019e946257b5300a45137a58f41fbd9a4db8e684ab61528"}, - {file = "websockets-13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6fd757f313c13c34dae9f126d3ba4cf97175859c719e57c6a614b781c86b617e"}, - {file = "websockets-13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cbac2eb7ce0fac755fb983c9247c4a60c4019bcde4c0e4d167aeb17520cc7ef1"}, - {file = "websockets-13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4b83cf7354cbbc058e97b3e545dceb75b8d9cf17fd5a19db419c319ddbaaf7a"}, - {file = "websockets-13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9202c0010c78fad1041e1c5285232b6508d3633f92825687549540a70e9e5901"}, - {file = "websockets-13.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6566e79c8c7cbea75ec450f6e1828945fc5c9a4769ceb1c7b6e22470539712"}, - {file = "websockets-13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e7fcad070dcd9ad37a09d89a4cbc2a5e3e45080b88977c0da87b3090f9f55ead"}, - {file = "websockets-13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a8f7d65358a25172db00c69bcc7df834155ee24229f560d035758fd6613111a"}, - {file = "websockets-13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:63b702fb31e3f058f946ccdfa551f4d57a06f7729c369e8815eb18643099db37"}, - {file = "websockets-13.0-cp310-cp310-win32.whl", hash = "sha256:3a20cf14ba7b482c4a1924b5e061729afb89c890ca9ed44ac4127c6c5986e424"}, - {file = "websockets-13.0-cp310-cp310-win_amd64.whl", hash = "sha256:587245f0704d0bb675f919898d7473e8827a6d578e5a122a21756ca44b811ec8"}, - {file = "websockets-13.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:06df8306c241c235075d2ae77367038e701e53bc8c1bb4f6644f4f53aa6dedd0"}, - {file = "websockets-13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85a1f92a02f0b8c1bf02699731a70a8a74402bb3f82bee36e7768b19a8ed9709"}, - {file = "websockets-13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9ed02c604349068d46d87ef4c2012c112c791f2bec08671903a6bb2bd9c06784"}, - {file = "websockets-13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b89849171b590107f6724a7b0790736daead40926ddf47eadf998b4ff51d6414"}, - {file = "websockets-13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:939a16849d71203628157a5e4a495da63967c744e1e32018e9b9e2689aca64d4"}, - {file = "websockets-13.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad818cdac37c0ad4c58e51cb4964eae4f18b43c4a83cb37170b0d90c31bd80cf"}, - {file = "websockets-13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cbfe82a07596a044de78bb7a62519e71690c5812c26c5f1d4b877e64e4f46309"}, - {file = "websockets-13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e07e76c49f39c5b45cbd7362b94f001ae209a3ea4905ae9a09cfd53b3c76373d"}, - {file = "websockets-13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:372f46a0096cfda23c88f7e42349a33f8375e10912f712e6b496d3a9a557290f"}, - {file = "websockets-13.0-cp311-cp311-win32.whl", hash = "sha256:376a43a4fd96725f13450d3d2e98f4f36c3525c562ab53d9a98dd2950dca9a8a"}, - {file = "websockets-13.0-cp311-cp311-win_amd64.whl", hash = "sha256:2be1382a4daa61e2f3e2be3b3c86932a8db9d1f85297feb6e9df22f391f94452"}, - {file = "websockets-13.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5407c34776b9b77bd89a5f95eb0a34aaf91889e3f911c63f13035220eb50107"}, - {file = "websockets-13.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4782ec789f059f888c1e8fdf94383d0e64b531cffebbf26dd55afd53ab487ca4"}, - {file = "websockets-13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c8feb8e19ef65c9994e652c5b0324abd657bedd0abeb946fb4f5163012c1e730"}, - {file = "websockets-13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f3d2e20c442b58dbac593cb1e02bc02d149a86056cc4126d977ad902472e3b"}, - {file = "websockets-13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e39d393e0ab5b8bd01717cc26f2922026050188947ff54fe6a49dc489f7750b7"}, - {file = "websockets-13.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f661a4205741bdc88ac9c2b2ec003c72cee97e4acd156eb733662ff004ba429"}, - {file = "websockets-13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:384129ad0490e06bab2b98c1da9b488acb35bb11e2464c728376c6f55f0d45f3"}, - {file = "websockets-13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:df5c0eff91f61b8205a6c9f7b255ff390cdb77b61c7b41f79ca10afcbb22b6cb"}, - {file = "websockets-13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:02cc9bb1a887dac0e08bf657c5d00aa3fac0d03215d35a599130c2034ae6663a"}, - {file = "websockets-13.0-cp312-cp312-win32.whl", hash = "sha256:d9726d2c9bd6aed8cb994d89b3910ca0079406edce3670886ec828a73e7bdd53"}, - {file = "websockets-13.0-cp312-cp312-win_amd64.whl", hash = "sha256:fa0839f35322f7b038d8adcf679e2698c3a483688cc92e3bd15ee4fb06669e9a"}, - {file = "websockets-13.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:da7e501e59857e8e3e9d10586139dc196b80445a591451ca9998aafba1af5278"}, - {file = "websockets-13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a00e1e587c655749afb5b135d8d3edcfe84ec6db864201e40a882e64168610b3"}, - {file = "websockets-13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a7fbf2a8fe7556a8f4e68cb3e736884af7bf93653e79f6219f17ebb75e97d8f0"}, - {file = "websockets-13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ea9c9c7443a97ea4d84d3e4d42d0e8c4235834edae652993abcd2aff94affd7"}, - {file = "websockets-13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35c2221b539b360203f3f9ad168e527bf16d903e385068ae842c186efb13d0ea"}, - {file = "websockets-13.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:358d37c5c431dd050ffb06b4b075505aae3f4f795d7fff9794e5ed96ce99b998"}, - {file = "websockets-13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:038e7a0f1bfafc7bf52915ab3506b7a03d1e06381e9f60440c856e8918138151"}, - {file = "websockets-13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fd038bc9e2c134847f1e0ce3191797fad110756e690c2fdd9702ed34e7a43abb"}, - {file = "websockets-13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93b8c2008f372379fb6e5d2b3f7c9ec32f7b80316543fd3a5ace6610c5cde1b0"}, - {file = "websockets-13.0-cp313-cp313-win32.whl", hash = "sha256:851fd0afb3bc0b73f7c5b5858975d42769a5fdde5314f4ef2c106aec63100687"}, - {file = "websockets-13.0-cp313-cp313-win_amd64.whl", hash = "sha256:7d14901fdcf212804970c30ab9ee8f3f0212e620c7ea93079d6534863444fb4e"}, - {file = "websockets-13.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ae7a519a56a714f64c3445cabde9fc2fc927e7eae44f413eae187cddd9e54178"}, - {file = "websockets-13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5575031472ca87302aeb2ce2c2349f4c6ea978c86a9d1289bc5d16058ad4c10a"}, - {file = "websockets-13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9895df6cd0bfe79d09bcd1dbdc03862846f26fbd93797153de954306620c1d00"}, - {file = "websockets-13.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4de299c947a54fca9ce1c5fd4a08eb92ffce91961becb13bd9195f7c6e71b47"}, - {file = "websockets-13.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05c25f7b849702950b6fd0e233989bb73a0d2bc83faa3b7233313ca395205f6d"}, - {file = "websockets-13.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ede95125a30602b1691a4b1da88946bf27dae283cf30f22cd2cb8ca4b2e0d119"}, - {file = "websockets-13.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:addf0a16e4983280efed272d8cb3b2e05f0051755372461e7d966b80a6554e16"}, - {file = "websockets-13.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:06b3186e97bf9a33921fa60734d5ed90f2a9b407cce8d23c7333a0984049ef61"}, - {file = "websockets-13.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:eae368cac85adc4c7dc3b0d5f84ffcca609d658db6447387300478e44db70796"}, - {file = "websockets-13.0-cp38-cp38-win32.whl", hash = "sha256:337837ac788d955728b1ab01876d72b73da59819a3388e1c5e8e05c3999f1afa"}, - {file = "websockets-13.0-cp38-cp38-win_amd64.whl", hash = "sha256:f66e00e42f25ca7e91076366303e11c82572ca87cc5aae51e6e9c094f315ab41"}, - {file = "websockets-13.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:94c1c02721139fe9940b38d28fb15b4b782981d800d5f40f9966264fbf23dcc8"}, - {file = "websockets-13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bd4ba86513430513e2aa25a441bb538f6f83734dc368a2c5d18afdd39097aa33"}, - {file = "websockets-13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a1ab8f0e0cadc5be5f3f9fa11a663957fecbf483d434762c8dfb8aa44948944a"}, - {file = "websockets-13.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3670def5d3dfd5af6f6e2b3b243ea8f1f72d8da1ef927322f0703f85c90d9603"}, - {file = "websockets-13.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6058b6be92743358885ad6dcdecb378fde4a4c74d4dd16a089d07580c75a0e80"}, - {file = "websockets-13.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:516062a0a8ef5ecbfa4acbaec14b199fc070577834f9fe3d40800a99f92523ca"}, - {file = "websockets-13.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:da7e918d82e7bdfc6f66d31febe1b2e28a1ca3387315f918de26f5e367f61572"}, - {file = "websockets-13.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9cc7f35dcb49a4e32db82a849fcc0714c4d4acc9d2273aded2d61f87d7f660b7"}, - {file = "websockets-13.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f5737c53eb2c8ed8f64b50d3dafd3c1dae739f78aa495a288421ac1b3de82717"}, - {file = "websockets-13.0-cp39-cp39-win32.whl", hash = "sha256:265e1f0d3f788ce8ef99dca591a1aec5263b26083ca0934467ad9a1d1181067c"}, - {file = "websockets-13.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d70c89e3d3b347a7c4d3c33f8d323f0584c9ceb69b82c2ef8a174ca84ea3d4a"}, - {file = "websockets-13.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:602cbd010d8c21c8475f1798b705bb18567eb189c533ab5ef568bc3033fdf417"}, - {file = "websockets-13.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:bf8eb5dca4f484a60f5327b044e842e0d7f7cdbf02ea6dc4a4f811259f1f1f0b"}, - {file = "websockets-13.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d795c1802d99a643bf689b277e8604c14b5af1bc0a31dade2cd7a678087212"}, - {file = "websockets-13.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:788bc841d250beccff67a20a5a53a15657a60111ef9c0c0a97fbdd614fae0fe2"}, - {file = "websockets-13.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7334752052532c156d28b8eaf3558137e115c7871ea82adff69b6d94a7bee273"}, - {file = "websockets-13.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7a1963302947332c3039e3f66209ec73b1626f8a0191649e0713c391e9f5b0d"}, - {file = "websockets-13.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2e1cf4e1eb84b4fd74a47688e8b0940c89a04ad9f6937afa43d468e71128cd68"}, - {file = "websockets-13.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:c026ee729c4ce55708a14b839ba35086dfae265fc12813b62d34ce33f4980c1c"}, - {file = "websockets-13.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5f9d23fbbf96eefde836d9692670bfc89e2d159f456d499c5efcf6a6281c1af"}, - {file = "websockets-13.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ad684cb7efce227d756bae3e8484f2e56aa128398753b54245efdfbd1108f2c"}, - {file = "websockets-13.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1e10b3fbed7be4a59831d3a939900e50fcd34d93716e433d4193a4d0d1d335d"}, - {file = "websockets-13.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d42a818e634f789350cd8fb413a3f5eec1cf0400a53d02062534c41519f5125c"}, - {file = "websockets-13.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e5ba5e9b332267d0f2c33ede390061850f1ac3ee6cd1bdcf4c5ea33ead971966"}, - {file = "websockets-13.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f9af457ed593e35f467140d8b61d425495b127744a9d65d45a366f8678449a23"}, - {file = "websockets-13.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcea3eb58c09c3a31cc83b45c06d5907f02ddaf10920aaa6443975310f699b95"}, - {file = "websockets-13.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c210d1460dc8d326ffdef9703c2f83269b7539a1690ad11ae04162bc1878d33d"}, - {file = "websockets-13.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b32f38bc81170fd56d0482d505b556e52bf9078b36819a8ba52624bd6667e39e"}, - {file = "websockets-13.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:81a11a1ddd5320429db47c04d35119c3e674d215173d87aaeb06ae80f6e9031f"}, - {file = "websockets-13.0-py3-none-any.whl", hash = "sha256:dbbac01e80aee253d44c4f098ab3cc17c822518519e869b284cfbb8cd16cc9de"}, - {file = "websockets-13.0.tar.gz", hash = "sha256:b7bf950234a482b7461afdb2ec99eee3548ec4d53f418c7990bb79c620476602"}, -] - -[[package]] -name = "wrapt" -version = "1.16.0" -requires_python = ">=3.6" -summary = "Module for decorators, wrappers and monkey patching." -groups = ["dev-contrib", "full", "opentelemetry"] -files = [ - {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, - {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, - {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, - {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, - {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, - {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, - {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, - {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, - {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, - {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, - {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, - {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, - {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, - {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, -] - -[[package]] -name = "wsproto" -version = "1.2.0" -requires_python = ">=3.7.0" -summary = "WebSockets state-machine based protocol implementation" -groups = ["dev"] -dependencies = [ - "h11<1,>=0.9.0", -] -files = [ - {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, - {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, -] - -[[package]] -name = "zipp" -version = "3.20.0" -requires_python = ">=3.8" -summary = "Backport of pathlib-compatible object wrapper for zip files" -groups = ["default", "dev-contrib", "docs", "full", "opentelemetry", "sqlalchemy"] -files = [ - {file = "zipp-3.20.0-py3-none-any.whl", hash = "sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d"}, - {file = "zipp-3.20.0.tar.gz", hash = "sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31"}, -] - -[[package]] -name = "zope-interface" -version = "7.0.1" -requires_python = ">=3.8" -summary = "Interfaces for Python" -groups = ["dev"] -dependencies = [ - "setuptools", -] -files = [ - {file = "zope.interface-7.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec4e87e6fdc511a535254daa122c20e11959ce043b4e3425494b237692a34f1c"}, - {file = "zope.interface-7.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51d5713e8e38f2d3ec26e0dfdca398ed0c20abda2eb49ffc15a15a23eb8e5f6d"}, - {file = "zope.interface-7.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea8d51e5eb29e57d34744369cd08267637aa5a0fefc9b5d33775ab7ff2ebf2e3"}, - {file = "zope.interface-7.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55bbcc74dc0c7ab489c315c28b61d7a1d03cf938cc99cc58092eb065f120c3a5"}, - {file = "zope.interface-7.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10ebac566dd0cec66f942dc759d46a994a2b3ba7179420f0e2130f88f8a5f400"}, - {file = "zope.interface-7.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:7039e624bcb820f77cc2ff3d1adcce531932990eee16121077eb51d9c76b6c14"}, - {file = "zope.interface-7.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03bd5c0db82237bbc47833a8b25f1cc090646e212f86b601903d79d7e6b37031"}, - {file = "zope.interface-7.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f52050c6a10d4a039ec6f2c58e5b3ade5cc570d16cf9d102711e6b8413c90e6"}, - {file = "zope.interface-7.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af0b33f04677b57843d529b9257a475d2865403300b48c67654c40abac2f9f24"}, - {file = "zope.interface-7.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696c2a381fc7876b3056711717dba5eddd07c2c9e5ccd50da54029a1293b6e43"}, - {file = "zope.interface-7.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f89a420cf5a6f2aa7849dd59e1ff0e477f562d97cf8d6a1ee03461e1eec39887"}, - {file = "zope.interface-7.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:b59deb0ddc7b431e41d720c00f99d68b52cb9bd1d5605a085dc18f502fe9c47f"}, - {file = "zope.interface-7.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52f5253cca1b35eaeefa51abd366b87f48f8714097c99b131ba61f3fdbbb58e7"}, - {file = "zope.interface-7.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88d108d004e0df25224de77ce349a7e73494ea2cb194031f7c9687e68a88ec9b"}, - {file = "zope.interface-7.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c203d82069ba31e1f3bc7ba530b2461ec86366cd4bfc9b95ec6ce58b1b559c34"}, - {file = "zope.interface-7.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3495462bc0438b76536a0e10d765b168ae636092082531b88340dc40dcd118"}, - {file = "zope.interface-7.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192b7a792e3145ed880ff6b1a206fdb783697cfdb4915083bfca7065ec845e60"}, - {file = "zope.interface-7.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:400d06c9ec8dbcc96f56e79376297e7be07a315605c9a2208720da263d44d76f"}, - {file = "zope.interface-7.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c1dff87b30fd150c61367d0e2cdc49bb55f8b9fd2a303560bbc24b951573ae1"}, - {file = "zope.interface-7.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f749ca804648d00eda62fe1098f229b082dfca930d8bad8386e572a6eafa7525"}, - {file = "zope.interface-7.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ec212037becf6d2f705b7ed4538d56980b1e7bba237df0d8995cbbed29961dc"}, - {file = "zope.interface-7.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d33cb526efdc235a2531433fc1287fcb80d807d5b401f9b801b78bf22df560dd"}, - {file = "zope.interface-7.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b419f2144e1762ab845f20316f1df36b15431f2622ebae8a6d5f7e8e712b413c"}, - {file = "zope.interface-7.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03f1452d5d1f279184d5bdb663a3dc39902d9320eceb63276240791e849054b6"}, - {file = "zope.interface-7.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ba4b3638d014918b918aa90a9c8370bd74a03abf8fcf9deb353b3a461a59a84"}, - {file = "zope.interface-7.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc0615351221926a36a0fbcb2520fb52e0b23e8c22a43754d9cb8f21358c33c0"}, - {file = "zope.interface-7.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:ce6cbb852fb8f2f9bb7b9cdca44e2e37bce783b5f4c167ff82cb5f5128163c8f"}, - {file = "zope.interface-7.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5566fd9271c89ad03d81b0831c37d46ae5e2ed211122c998637130159a120cf1"}, - {file = "zope.interface-7.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da0cef4d7e3f19c3bd1d71658d6900321af0492fee36ec01b550a10924cffb9c"}, - {file = "zope.interface-7.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f32ca483e6ade23c7caaee9d5ee5d550cf4146e9b68d2fb6c68bac183aa41c37"}, - {file = "zope.interface-7.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da21e7eec49252df34d426c2ee9cf0361c923026d37c24728b0fa4cc0599fd03"}, - {file = "zope.interface-7.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a8195b99e650e6f329ce4e5eb22d448bdfef0406404080812bc96e2a05674cb"}, - {file = "zope.interface-7.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:19c829d52e921b9fe0b2c0c6a8f9a2508c49678ee1be598f87d143335b6a35dc"}, - {file = "zope.interface-7.0.1.tar.gz", hash = "sha256:f0f5fda7cbf890371a59ab1d06512da4f2c89a6ea194e595808123c863c38eff"}, -] diff --git a/pyproject.toml b/pyproject.toml index 31dcc870c0..b70fd43b55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,19 +31,22 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ - "anyio>=3", - "httpx>=0.22", - "exceptiongroup; python_version < \"3.11\"", - "importlib-metadata; python_version < \"3.10\"", - "importlib-resources>=5.12.0; python_version < \"3.9\"", - "msgspec>=0.18.2", - "multidict>=6.0.2", - "polyfactory>=2.6.3", - "pyyaml", - "typing-extensions", - "click", - "rich>=13.0.0", - "rich-click", + "anyio>=3", + "httpx>=0.22", + "exceptiongroup; python_version < \"3.11\"", + "importlib-metadata; python_version < \"3.10\"", + "importlib-resources>=5.12.0; python_version < \"3.9\"", + "msgspec>=0.18.2", + "multidict>=6.0.2", + "polyfactory>=2.6.3", + "pyyaml", + "typing-extensions", + "click", + "rich>=13.0.0", + "rich-click", + "multipart>=1.2.0", + # default litestar plugins + "litestar-htmx>=0.4.0" ] description = "Litestar - A production-ready, highly performant, extensible ASGI API Framework" keywords = ["api", "rest", "asgi", "litestar", "starlite"] @@ -60,7 +63,7 @@ maintainers = [ name = "litestar" readme = "README.md" requires-python = ">=3.8,<4.0" -version = "2.10.0" +version = "2.13.0" [project.urls] Blog = "https://blog.litestar.dev" @@ -80,7 +83,7 @@ brotli = ["brotli"] cli = ["jsbeautifier", "uvicorn[standard]", "uvloop>=0.18.0; sys_platform != 'win32'"] cryptography = ["cryptography"] full = [ - "litestar[annotated-types,attrs,brotli,cli,cryptography,jinja,jwt,mako,minijinja,opentelemetry,piccolo,picologging,prometheus,pydantic,redis,sqlalchemy,standard,structlog]", + "litestar[annotated-types,attrs,brotli,cli,cryptography,jinja,jwt,mako,minijinja,opentelemetry,piccolo,picologging,prometheus,pydantic,redis,sqlalchemy,standard,structlog,valkey]", ] jinja = ["jinja2>=3.1.2"] jwt = [ @@ -95,6 +98,7 @@ picologging = ["picologging"] prometheus = ["prometheus-client"] pydantic = ["pydantic", "email-validator", "pydantic-extra-types"] redis = ["redis[hiredis]>=4.4.4"] +valkey = ["valkey[libvalkey]>=6.0.2"] sqlalchemy = ["advanced-alchemy>=0.2.2"] standard = ["jinja2", "jsbeautifier", "uvicorn[standard]", "uvloop>=0.18.0; sys_platform != 'win32'", "fast-query-parsers>=1.0.2"] structlog = ["structlog"] @@ -102,33 +106,20 @@ structlog = ["structlog"] [project.scripts] litestar = "litestar.__main__:run_cli" -[tool.hatch.metadata] -allow-direct-references = true [tool.hatch.build.targets.sdist] include = [ 'docs/PYPI_README.md', - '/Makefile', '/litestar', - '/tests', - '/CHANGELOG.rst', ] -[tool.pdm] -ignore_package_warnings = [ - "alabaster", - "sphinxcontrib-*", - "sphinx-*", - "sphinx", - "pre-commit", - "pydata-sphinx-*", - "slotscheck", - "autobahn", - "accessible-pygments" -] -[tool.pdm.dev-dependencies] +[tool.uv] +default-groups = ["dev", "linting", "test", "docs"] + +[dependency-groups] dev = [ + "litestar[full]", "beanie>=1.21.0", "beautifulsoup4", "fsspec", @@ -144,19 +135,21 @@ dev = [ "psutil>=5.9.8", "hypercorn>=0.16.0", "daphne>=4.0.0", + "opentelemetry-sdk", + "httpx-sse" ] -dev-contrib = ["opentelemetry-sdk", "httpx-sse"] + docs = [ - "sphinx>=7.1.2", - "sphinx-autobuild>=2021.3.14", - "sphinx-copybutton>=0.5.2", - "sphinx-toolbox>=3.5.0", - "sphinx-design>=0.5.0", - "sphinx-click>=4.4.0", - "sphinxcontrib-mermaid>=0.9.2", - "auto-pytabs[sphinx]>=0.5.0", - "litestar-sphinx-theme @ git+https://github.com/litestar-org/litestar-sphinx-theme.git", - "sphinx-paramlinks>=0.6.0", + "sphinx>=7.1.2", + "sphinx-autobuild>=2021.3.14", + "sphinx-copybutton>=0.5.2", + "sphinx-toolbox>=3.5.0", + "sphinx-design>=0.5.0", + "sphinx-click>=4.4.0", + "sphinxcontrib-mermaid>=0.9.2", + "auto-pytabs[sphinx]>=0.5.0", + "litestar-sphinx-theme @ git+https://github.com/litestar-org/litestar-sphinx-theme.git", + "sphinx-paramlinks>=0.6.0", ] linting = [ "ruff>=0.2.1", @@ -167,7 +160,6 @@ linting = [ "pyright==1.1.344", "asyncpg-stubs", "types-beautifulsoup4", - "types-python-jose", "types-pyyaml", "types-redis", "types-psutil", @@ -185,23 +177,17 @@ test = [ "time-machine", ] -[tool.pdm.scripts] -ci = {composite = ["lint", "test"]} -docs-serve = "sphinx-autobuild docs docs/_build/ -j auto --watch litestar --watch docs --watch tests --port 8002" -lint = "pre-commit run --all-files" -test = "pytest tests docs/examples" - [build-system] build-backend = "hatchling.build" requires = ["hatchling"] [tool.codespell] ignore-words-list = "selectin" -skip = 'pdm.lock,docs/examples/contrib/sqlalchemy/us_state_lookup.json' +skip = 'uv.lock,docs/examples/contrib/sqlalchemy/us_state_lookup.json' [tool.coverage.run] concurrency = ["multiprocessing", "thread"] -omit = ["*/tests/*", "*/litestar/plugins/sqlalchemy.py"] +omit = ["*/tests/*", "*/litestar/plugins/sqlalchemy.py", "*/litestar/_kwargs/types.py"] parallel = true plugins = ["covdefaults"] source = ["litestar"] @@ -218,6 +204,9 @@ fail_under = 50 addopts = "--strict-markers --strict-config --dist=loadgroup -m 'not server_integration'" asyncio_mode = "auto" filterwarnings = [ + "error", + # https://github.com/pytest-dev/pytest-asyncio/issues/724 + "default:.*socket.socket:pytest.PytestUnraisableExceptionWarning", "ignore::trio.TrioDeprecationWarning:anyio._backends._trio*:", "ignore::DeprecationWarning:pkg_resources.*", "ignore::DeprecationWarning:google.rpc", @@ -243,24 +232,34 @@ xfail_strict = true [tool.mypy] packages = ["litestar", "tests"] plugins = ["pydantic.mypy"] +enable_error_code = [ + "truthy-bool", + "truthy-iterable", + "unused-awaitable", + "ignore-without-code", + "possibly-undefined", + "redundant-self", +] python_version = "3.8" disallow_any_generics = false -disallow_untyped_decorators = true -enable_error_code = "ignore-without-code" -implicit_reexport = false show_error_codes = true strict = true -warn_redundant_casts = true -warn_return_any = true warn_unreachable = true -warn_unused_configs = true -warn_unused_ignores = true +local_partial_types = true [[tool.mypy.overrides]] ignore_errors = true module = ["tests.examples.*", "tests.docker_service_fixtures"] +[[tool.mypy.overrides]] +module = ["tests.*"] +disable_error_code = ["truthy-bool"] + +[[tool.mypy.overrides]] +disable_error_code = ["assignment"] +module = ["tests.unit.test_logging.*"] + [[tool.mypy.overrides]] disallow_untyped_decorators = false module = ["tests.unit.test_kwargs.test_reserved_kwargs_injection"] @@ -269,6 +268,14 @@ module = ["tests.unit.test_kwargs.test_reserved_kwargs_injection"] module = ["tests.unit.test_contrib.test_repository"] strict_equality = false +[[tool.mypy.overrides]] +module = ["tests.unit.test_plugins.test_pydantic.test_openapi","litestar._asgi.routing_trie.traversal"] +disable_error_code = "index, union-attr" + +[[tool.mypy.overrides]] +module = ["tests.unit.test_channels.test_subscriber", "tests.unit.test_response.test_streaming_response"] +disable_error_code = "arg-type, comparison-overlap, unreachable" + [[tool.mypy.overrides]] ignore_missing_imports = true module = [ @@ -280,6 +287,29 @@ module = [ "exceptiongroup", ] +[[tool.mypy.overrides]] +warn_unused_ignores = false +module = [ + "litestar.contrib.sqlalchemy.*", + "litestar.plugins.pydantic.*", + "tests.unit.test_contrib.test_sqlalchemy", + "tests.unit.test_contrib.test_pydantic.*", + "tests.unit.test_logging.test_logging_config", + "litestar.openapi.spec.base", + "litestar.utils.helpers", + "litestar.channels.plugin", + "litestar.handlers.http_handlers._utils" +] + +[[tool.mypy.overrides]] +warn_unused_ignores = false +module = [ + "litestar.openapi.spec.base", + "litestar._asgi.routin_trie.traversal", + "litestar.plugins.pydantic.plugins.int", +] +disable_error_code = "arg-type" + [tool.pydantic-mypy] init_forbid_extra = true init_typed = true @@ -294,8 +324,10 @@ exclude = [ "docs", "tests/examples", "tests/docker_service_fixtures.py", - "litestar/contrib/pydantic/pydantic_dto_factory.py", - "litestar/contrib/pydantic/pydantic_init_plugin.py", + "litestar/plugins/pydantic/plugins/di.py", + "litestar/plugins/pydantic/plugins/init.py", + "litestar/plugins/pydantic/plugins/schema.py", + "litestar/plugins/pydantic/dto_factory.py", "tests/unit/test_contrib/test_sqlalchemy.py", ] include = ["litestar", "tests"] @@ -308,6 +340,19 @@ exclude-classes = """ ( # github.com/python/cpython/pull/106771 (^litestar.events.emitter:BaseEventEmitterBackend) + # review these as time permits + |(^litestar.connection.base:ASGIConnection) + |(^litestar.datastructures.state:ImmutableState) + |(^litestar.datastructures.state:State) + |(^litestar.dto.base_dto:AbstractDTO) + |(^litestar.dto.data_structures:DTOData) + |(^litestar.middleware.session.base:BaseSessionBackend) + |(^litestar.pagination:ClassicPagination) + |(^litestar.pagination:CursorPagination) + |(^litestar.response.base:Response) + |(^litestar.testing.client.base:BaseTestClient) + |(^litestar.testing.life_span_handler:LifeSpanHandler) + |(^litestar.utils.sync:AsyncIteratorWrapper) ) """ @@ -347,7 +392,7 @@ lint.select = [ "SIM", # flake8-simplify "T10", # flake8-debugger "T20", # flake8-print - "TCH", # flake8-type-checking + "TC", # flake8-type-checking "TID", # flake8-tidy-imports "UP", # pyupgrade "W", # pycodestyle - warning @@ -373,6 +418,7 @@ lint.ignore = [ "PLW2901", # pylint - for loop variable overwritten by assignment target "RUF012", # Ruff-specific rule - annotated with classvar "ISC001", # Ruff formatter incompatible + "CPY001", # ruff - copyright notice at the top of the file ] src = ["litestar", "tests", "docs/examples"] target-version = "py38" @@ -397,7 +443,7 @@ classmethod-decorators = [ known-first-party = ["litestar", "tests", "examples"] [tool.ruff.lint.per-file-ignores] -"docs/**/*.*" = ["S", "B", "DTZ", "A", "TCH", "ERA", "D", "RET"] +"docs/**/*.*" = ["S", "B", "DTZ", "A", "TC", "ERA", "D", "RET"] "docs/examples/**" = ["T201"] "docs/examples/application_hooks/before_send_hook.py" = ["UP006"] "docs/examples/contrib/sqlalchemy/plugins/**/*.*" = ["UP006"] @@ -406,6 +452,7 @@ known-first-party = ["litestar", "tests", "examples"] "litestar/_openapi/schema_generation/schema.py" = ["C901"] "litestar/exceptions/*.*" = ["N818"] "litestar/handlers/**/*.*" = ["N801"] +"litestar/handlers/websocket_handlers/listener.py" = ["B027"] "litestar/params.py" = ["N802"] "test_apps/**/*.*" = ["D", "TRY", "EM", "S", "PTH"] "tests/**/*.*" = [ @@ -429,7 +476,7 @@ known-first-party = ["litestar", "tests", "examples"] "S", "S101", "SIM", - "TCH", + "TC", "TRY", "E721", ] diff --git a/tests/conftest.py b/tests/conftest.py index ca3032f680..de243a37e2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,6 +18,8 @@ from redis.asyncio import Redis as AsyncRedis from redis.client import Redis from time_machine import travel +from valkey.asyncio import Valkey as AsyncValkey +from valkey.client import Valkey from litestar.logging import LoggingConfig from litestar.middleware.session import SessionMiddleware @@ -29,6 +31,7 @@ from litestar.stores.file import FileStore from litestar.stores.memory import MemoryStore from litestar.stores.redis import RedisStore +from litestar.stores.valkey import ValkeyStore from litestar.testing import RequestFactory from tests.helpers import not_none @@ -82,6 +85,11 @@ def redis_store(redis_client: AsyncRedis) -> RedisStore: return RedisStore(redis=redis_client) +@pytest.fixture() +def valkey_store(valkey_client: AsyncValkey) -> ValkeyStore: + return ValkeyStore(valkey=valkey_client) + + @pytest.fixture() def memory_store() -> MemoryStore: return MemoryStore() @@ -105,7 +113,12 @@ def file_store_create_directories_flag_false(tmp_path: Path) -> FileStore: @pytest.fixture( - params=[pytest.param("redis_store", marks=pytest.mark.xdist_group("redis")), "memory_store", "file_store"] + params=[ + pytest.param("redis_store", marks=pytest.mark.xdist_group("redis")), + pytest.param("valkey_store", marks=pytest.mark.xdist_group("valkey")), + "memory_store", + "file_store", + ] ) def store(request: FixtureRequest) -> Store: return cast("Store", request.getfixturevalue(request.param)) @@ -216,6 +229,7 @@ def inner( "route_handler": route_handler, "user": user, "session": session, + "headers": [], **kwargs, } return cast("Scope", scope) @@ -326,6 +340,20 @@ async def redis_client(docker_ip: str, redis_service: None) -> AsyncGenerator[As pass +@pytest.fixture() +async def valkey_client(docker_ip: str, valkey_service: None) -> AsyncGenerator[AsyncValkey, None]: + # this is to get around some weirdness with pytest-asyncio and valkey interaction + # on 3.8 and 3.9 + + Valkey(host=docker_ip, port=6381).flushall() + client: AsyncValkey = AsyncValkey(host=docker_ip, port=6381) + yield client + try: + await client.aclose() + except RuntimeError: + pass + + @pytest.fixture(autouse=True) def _patch_openapi_config(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr("litestar.app.DEFAULT_OPENAPI_CONFIG", OpenAPIConfig(title="Litestar API", version="1.0.0")) diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index b3d25160ee..2c3b69a283 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -12,3 +12,8 @@ services: restart: always ports: - "6397:6379" # use a non-standard port here + valkey: + image: valkey/valkey:latest + restart: always + ports: + - "6381:6379" # also a non-standard port diff --git a/tests/docker_service_fixtures.py b/tests/docker_service_fixtures.py index 6efca36979..84e78d48e6 100644 --- a/tests/docker_service_fixtures.py +++ b/tests/docker_service_fixtures.py @@ -13,6 +13,8 @@ import pytest from redis.asyncio import Redis as AsyncRedis from redis.exceptions import ConnectionError as RedisConnectionError +from valkey.asyncio import Valkey as AsyncValkey +from valkey.exceptions import ConnectionError as ValkeyConnectionError from litestar.utils import ensure_async_callable @@ -127,6 +129,21 @@ async def redis_service(docker_services: DockerServiceRegistry) -> None: await docker_services.start("redis", check=redis_responsive) +async def valkey_responsive(host: str) -> bool: + client: AsyncValkey = AsyncValkey(host=host, port=6381) + try: + return await client.ping() + except (ConnectionError, ValkeyConnectionError): + return False + finally: + await client.aclose() + + +@pytest.fixture() +async def valkey_service(docker_services: DockerServiceRegistry) -> None: + await docker_services.start("valkey", check=valkey_responsive) + + async def postgres_responsive(host: str) -> bool: try: conn = await asyncpg.connect( diff --git a/tests/e2e/test_dependency_injection/test_http_handler_dependency_injection.py b/tests/e2e/test_dependency_injection/test_http_handler_dependency_injection.py index b5015f92b0..86803f0a81 100644 --- a/tests/e2e/test_dependency_injection/test_http_handler_dependency_injection.py +++ b/tests/e2e/test_dependency_injection/test_http_handler_dependency_injection.py @@ -10,6 +10,7 @@ if TYPE_CHECKING: from litestar.connection import Request + from litestar.datastructures.state import State def router_first_dependency() -> bool: @@ -21,12 +22,12 @@ async def router_second_dependency() -> bool: return False -def controller_first_dependency(headers: Dict[str, Any]) -> dict: +def controller_first_dependency(headers: Dict[str, Any]) -> Dict[Any, Any]: assert headers return {} -async def controller_second_dependency(request: "Request") -> dict: +async def controller_second_dependency(request: "Request[Any, Any, State]") -> Dict[Any, Any]: assert request await sleep(0) return {} @@ -60,7 +61,7 @@ class FirstController(Controller): "first": Provide(local_method_first_dependency, sync_to_thread=False), }, ) - def test_method(self, first: int, second: dict, third: bool) -> None: + def test_method(self, first: int, second: Dict[Any, Any], third: bool) -> None: assert isinstance(first, int) assert isinstance(second, dict) assert not third @@ -109,7 +110,7 @@ class SecondController(Controller): path = "/second" @get() - def test_method(self, first: dict) -> None: + def test_method(self, first: Dict[Any, Any]) -> None: pass with create_test_client([first_controller, SecondController]) as client: diff --git a/tests/e2e/test_dependency_injection/test_websocket_handler_dependency_injection.py b/tests/e2e/test_dependency_injection/test_websocket_handler_dependency_injection.py index 86cdcc2fc5..a95437c7fe 100644 --- a/tests/e2e/test_dependency_injection/test_websocket_handler_dependency_injection.py +++ b/tests/e2e/test_dependency_injection/test_websocket_handler_dependency_injection.py @@ -5,6 +5,7 @@ from litestar import Controller, websocket from litestar.connection import WebSocket +from litestar.datastructures import State from litestar.di import Provide from litestar.exceptions import WebSocketDisconnect from litestar.testing import create_test_client @@ -19,12 +20,12 @@ async def router_second_dependency() -> bool: return False -def controller_first_dependency(headers: Dict[str, Any]) -> dict: +def controller_first_dependency(headers: Dict[str, Any]) -> Dict[Any, Any]: assert headers return {} -async def controller_second_dependency(socket: WebSocket) -> dict: +async def controller_second_dependency(socket: WebSocket[Any, Any, Any]) -> Dict[Any, Any]: assert socket await sleep(0) return {} @@ -56,7 +57,9 @@ class FirstController(Controller): "first": Provide(local_method_first_dependency, sync_to_thread=False), }, ) - async def test_method(self, socket: WebSocket, first: int, second: dict, third: bool) -> None: + async def test_method( + self, socket: WebSocket[Any, Any, Any], first: int, second: Dict[Any, Any], third: bool + ) -> None: await socket.accept() msg = await socket.receive_json() assert msg @@ -87,7 +90,7 @@ def test_function_dependency_injection() -> None: "third": Provide(local_method_second_dependency, sync_to_thread=False), }, ) - async def test_function(socket: WebSocket, first: int, second: bool, third: str) -> None: + async def test_function(socket: WebSocket[Any, Any, State], first: int, second: bool, third: str) -> None: await socket.accept() assert socket msg = await socket.receive_json() @@ -113,7 +116,7 @@ class SecondController(Controller): path = "/second" @websocket() - async def test_method(self, socket: WebSocket, first: dict) -> None: + async def test_method(self, socket: WebSocket[Any, Any, Any], _: Dict[Any, Any]) -> None: await socket.accept() client = create_test_client([FirstController, SecondController]) diff --git a/tests/e2e/test_life_cycle_hooks/test_after_request.py b/tests/e2e/test_life_cycle_hooks/test_after_request.py index 3fec69d13e..7be3d6d522 100644 --- a/tests/e2e/test_life_cycle_hooks/test_after_request.py +++ b/tests/e2e/test_life_cycle_hooks/test_after_request.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Any, Dict, Optional import pytest @@ -8,19 +8,19 @@ from litestar.types import AfterRequestHookHandler -def sync_after_request_handler(response: Response) -> Response: +def sync_after_request_handler(response: Response[Dict[str, str]]) -> Response[Dict[str, str]]: assert isinstance(response, Response) response.content = {"hello": "moon"} return response -async def async_after_request_handler(response: Response) -> Response: +async def async_after_request_handler(response: Response[Dict[str, str]]) -> Response[Dict[str, str]]: assert isinstance(response, Response) response.content = {"hello": "moon"} return response -async def async_after_request_handler_with_hello_world(response: Response) -> Response: +async def async_after_request_handler_with_hello_world(response: Response[Dict[str, str]]) -> Response[Dict[str, str]]: assert isinstance(response, Response) response.content = {"hello": "world"} return response @@ -34,9 +34,11 @@ async def async_after_request_handler_with_hello_world(response: Response) -> Re [async_after_request_handler, {"hello": "moon"}], ], ) -def test_after_request_handler_called(after_request: Optional[AfterRequestHookHandler], expected: dict) -> None: +def test_after_request_handler_called( + after_request: Optional[AfterRequestHookHandler], expected: Dict[str, str] +) -> None: @get(after_request=after_request) - def handler() -> dict: + def handler() -> Dict[str, str]: return {"hello": "world"} with create_test_client(route_handlers=handler) as client: @@ -63,7 +65,7 @@ def test_after_request_handler_resolution( router_after_request_handler: Optional[AfterRequestHookHandler], controller_after_request_handler: Optional[AfterRequestHookHandler], method_after_request_handler: Optional[AfterRequestHookHandler], - expected: dict, + expected: Dict[str, str], ) -> None: class MyController(Controller): path = "/hello" @@ -71,7 +73,7 @@ class MyController(Controller): after_request = controller_after_request_handler @get(after_request=method_after_request_handler) - def hello(self) -> dict: + def hello(self) -> Dict[str, str]: return {"hello": "world"} router = Router(path="/greetings", route_handlers=[MyController], after_request=router_after_request_handler) @@ -82,12 +84,12 @@ def hello(self) -> dict: def test_after_request_handles_handlers_that_return_responses() -> None: - def after_request(response: Response) -> Response: + def after_request(response: Response[Any]) -> Response[Any]: response.headers["Custom-Header-Name"] = "Custom Header Value" return response @get("/") - def handler() -> Response: + def handler() -> Response[str]: return Response("test") with create_test_client(handler, after_request=after_request) as client: diff --git a/tests/e2e/test_life_cycle_hooks/test_before_request.py b/tests/e2e/test_life_cycle_hooks/test_before_request.py index 5efea5516c..47de0a58a8 100644 --- a/tests/e2e/test_life_cycle_hooks/test_before_request.py +++ b/tests/e2e/test_life_cycle_hooks/test_before_request.py @@ -1,37 +1,38 @@ -from typing import Optional +from typing import Any, Dict, Optional import pytest from litestar import Controller, Request, Response, Router, get +from litestar.datastructures import State from litestar.testing import create_test_client from litestar.types import AnyCallable, BeforeRequestHookHandler -def sync_before_request_handler_with_return_value(request: Request) -> dict: +def sync_before_request_handler_with_return_value(request: Request[Any, Any, State]) -> Dict[str, str]: assert isinstance(request, Request) return {"hello": "moon"} -async def async_before_request_handler_with_return_value(request: Request) -> dict: +async def async_before_request_handler_with_return_value(request: Request[Any, Any, State]) -> Dict[str, str]: assert isinstance(request, Request) return {"hello": "moon"} -def sync_before_request_handler_without_return_value(request: Request) -> None: +def sync_before_request_handler_without_return_value(request: Request[Any, Any, State]) -> None: assert isinstance(request, Request) -async def async_before_request_handler_without_return_value(request: Request) -> None: +async def async_before_request_handler_without_return_value(request: Request[Any, Any, State]) -> None: assert isinstance(request, Request) -def sync_after_request_handler(response: Response) -> Response: +def sync_after_request_handler(response: Response[Dict[str, str]]) -> Response[Dict[str, str]]: assert isinstance(response, Response) response.content = {"hello": "moon"} return response -async def async_after_request_handler(response: Response) -> Response: +async def async_after_request_handler(response: Response[Dict[str, str]]) -> Response[Dict[str, str]]: assert isinstance(response, Response) response.content = {"hello": "moon"} return response @@ -47,9 +48,9 @@ async def async_after_request_handler(response: Response) -> Response: (async_before_request_handler_without_return_value, {"hello": "world"}), ), ) -def test_before_request_handler_called(before_request: Optional[AnyCallable], expected: dict) -> None: +def test_before_request_handler_called(before_request: Optional[AnyCallable], expected: Dict[str, str]) -> None: @get(before_request=before_request) - def handler() -> dict: + def handler() -> Dict[str, str]: return {"hello": "world"} with create_test_client(route_handlers=handler) as client: @@ -94,7 +95,7 @@ def test_before_request_handler_resolution( router_before_request_handler: Optional[BeforeRequestHookHandler], controller_before_request_handler: Optional[BeforeRequestHookHandler], method_before_request_handler: Optional[BeforeRequestHookHandler], - expected: dict, + expected: Dict[str, str], ) -> None: class MyController(Controller): path = "/hello" @@ -102,7 +103,7 @@ class MyController(Controller): before_request = controller_before_request_handler @get(before_request=method_before_request_handler) - def hello(self) -> dict: + def hello(self) -> Dict[str, str]: return {"hello": "world"} router = Router(path="/greetings", route_handlers=[MyController], before_request=router_before_request_handler) diff --git a/tests/e2e/test_openapi/test_spec_headers.py b/tests/e2e/test_openapi/test_spec_headers.py index cb0b52216f..7a9b4fd7b5 100644 --- a/tests/e2e/test_openapi/test_spec_headers.py +++ b/tests/e2e/test_openapi/test_spec_headers.py @@ -19,8 +19,6 @@ def test_included_header_fields() -> None: assert app1.openapi_schema.to_schema()["paths"]["/"]["get"]["responses"]["200"]["headers"] == { "X-Version": { - "allowEmptyValue": False, - "allowReserved": False, "deprecated": False, "description": "Test", "required": False, diff --git a/tests/e2e/test_response_caching.py b/tests/e2e/test_response_caching.py index 7573178154..4623028431 100644 --- a/tests/e2e/test_response_caching.py +++ b/tests/e2e/test_response_caching.py @@ -1,7 +1,7 @@ import gzip import random from datetime import timedelta -from typing import TYPE_CHECKING, Optional, Type, Union +from typing import TYPE_CHECKING, Any, Optional, Type, TypeVar, Union from unittest.mock import MagicMock from uuid import uuid4 @@ -11,6 +11,7 @@ from litestar import Litestar, Request, Response, get, post from litestar.config.compression import CompressionConfig from litestar.config.response_cache import CACHE_FOREVER, ResponseCacheConfig +from litestar.datastructures import State from litestar.enums import CompressionEncoding from litestar.middleware.response_cache import ResponseCacheMiddleware from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_400_BAD_REQUEST, HTTP_500_INTERNAL_SERVER_ERROR @@ -22,13 +23,15 @@ if TYPE_CHECKING: from time_machine import Coordinates +T = TypeVar("T") + @pytest.fixture() def mock() -> MagicMock: return MagicMock(return_value=str(random.random())) -def after_request_handler(response: "Response") -> "Response": +def after_request_handler(response: "Response[T]") -> "Response[T]": response.headers["unique-identifier"] = str(uuid4()) return response @@ -132,7 +135,7 @@ async def handler() -> None: @pytest.mark.parametrize("sync_to_thread", (True, False)) async def test_custom_cache_key(sync_to_thread: bool, anyio_backend: str, mock: MagicMock) -> None: - def custom_cache_key_builder(request: Request) -> str: + def custom_cache_key_builder(request: Request[Any, Any, State]) -> str: return f"{request.url.path}:::cached" @get("/cached", sync_to_thread=sync_to_thread, cache=True, cache_key_builder=custom_cache_key_builder) @@ -262,7 +265,7 @@ def test_default_do_response_cache_predicate( mock: MagicMock, response: Union[int, Type[RuntimeError]], should_cache: bool ) -> None: @get("/", cache=True) - def handler() -> Response: + def handler() -> Response[None]: mock() if isinstance(response, int): return Response(None, status_code=response) diff --git a/tests/e2e/test_routing/conftest.py b/tests/e2e/test_routing/conftest.py index eaa178e1ad..1295f57647 100644 --- a/tests/e2e/test_routing/conftest.py +++ b/tests/e2e/test_routing/conftest.py @@ -1,4 +1,3 @@ -import subprocess import time from pathlib import Path from typing import Callable, List @@ -16,16 +15,13 @@ def runner(app: str, server_command: List[str]) -> None: tmp_path.joinpath("app.py").write_text(app) monkeypatch.chdir(tmp_path) - proc = psutil.Popen( - server_command, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - ) + proc = psutil.Popen(server_command) def kill() -> None: for child in proc.children(recursive=True): child.kill() proc.kill() + proc.wait() request.addfinalizer(kill) diff --git a/tests/examples/test_contrib/prometheus/test_prometheus_exporter_example.py b/tests/examples/test_contrib/prometheus/test_prometheus_exporter_example.py index 7a45636e76..512537b7f0 100644 --- a/tests/examples/test_contrib/prometheus/test_prometheus_exporter_example.py +++ b/tests/examples/test_contrib/prometheus/test_prometheus_exporter_example.py @@ -3,8 +3,8 @@ import pytest from prometheus_client import REGISTRY -from litestar import get -from litestar.contrib.prometheus import PrometheusMiddleware +from litestar import Controller, Litestar, Request, get +from litestar.plugins.prometheus import PrometheusMiddleware from litestar.status_codes import HTTP_200_OK from litestar.testing import TestClient @@ -20,7 +20,8 @@ def clear_collectors() -> None: @pytest.mark.parametrize( "group_path, route_path, route_template, expected_path", [ - (True, "/test/litestar", "test/{name:str}", "/test/{name}"), + (True, "/test/litestar", "test/litestar", "/test/litestar"), + (False, "/test/litestar", "test/litestar", "/test/litestar"), (True, "/test/litestar", "test/{name:str}", "/test/{name}"), (False, "/test/litestar", "test/{name:str}", "/test/litestar"), ( @@ -40,7 +41,7 @@ def clear_collectors() -> None: def test_prometheus_exporter_example( group_path: bool, route_path: str, route_template: str, expected_path: str ) -> None: - from docs.examples.contrib.prometheus.using_prometheus_exporter import create_app + from docs.examples.plugins.prometheus.using_prometheus_exporter import create_app app = create_app(group_path=group_path) @@ -59,3 +60,25 @@ def home(name: str) -> Dict[str, Any]: assert metrics_exporter_response.status_code == HTTP_200_OK metrics = metrics_exporter_response.content.decode() assert expected_path in metrics + + +def test_correct_population_path_template() -> None: + class TestController(Controller): + path = "/prefix" + + @get("/{id_:int}") + async def b(self, request: Request, id_: int) -> str: + return request.scope["path_template"] + + @get("/{id_:int}/postfix") + async def a(self, request: Request, id_: int) -> str: + return request.scope["path_template"] + + app = Litestar([TestController]) + + with TestClient(app) as client: + without_postfix_resp = client.get("/prefix/1") + with_postfix_resp = client.get("/prefix/1/postfix") + + assert without_postfix_resp.content.decode() == "/prefix/{id_}" + assert with_postfix_resp.content.decode() == "/prefix/{id_}/postfix" diff --git a/tests/examples/test_contrib/prometheus/test_prometheus_exporter_example_with_extra_config.py b/tests/examples/test_contrib/prometheus/test_prometheus_exporter_example_with_extra_config.py index 3b85e69224..937cf3f2d6 100644 --- a/tests/examples/test_contrib/prometheus/test_prometheus_exporter_example_with_extra_config.py +++ b/tests/examples/test_contrib/prometheus/test_prometheus_exporter_example_with_extra_config.py @@ -3,7 +3,7 @@ from prometheus_client import REGISTRY from litestar import get -from litestar.contrib.prometheus import PrometheusMiddleware +from litestar.plugins.prometheus import PrometheusMiddleware from litestar.status_codes import HTTP_200_OK from litestar.testing import TestClient @@ -17,7 +17,7 @@ def clear_collectors() -> None: def test_prometheus_exporter_with_extra_config_example() -> None: - from docs.examples.contrib.prometheus.using_prometheus_exporter_with_extra_configs import app + from docs.examples.plugins.prometheus.using_prometheus_exporter_with_extra_configs import app clear_collectors() diff --git a/tests/examples/test_contrib/test_sqlalchemy/test_sqlalchemy_examples.py b/tests/examples/test_contrib/test_sqlalchemy/test_sqlalchemy_examples.py index 0aa531bd33..55b48776c9 100644 --- a/tests/examples/test_contrib/test_sqlalchemy/test_sqlalchemy_examples.py +++ b/tests/examples/test_contrib/test_sqlalchemy/test_sqlalchemy_examples.py @@ -1,14 +1,32 @@ +from pathlib import Path + import pytest +from pytest import MonkeyPatch +from sqlalchemy.ext.asyncio import create_async_engine +from sqlalchemy.pool import NullPool +from litestar.plugins.sqlalchemy import AsyncSessionConfig, SQLAlchemyAsyncConfig from litestar.testing import TestClient pytestmark = pytest.mark.xdist_group("sqlalchemy_examples") -def test_sqlalchemy_declarative_models() -> None: - from docs.examples.contrib.sqlalchemy.sqlalchemy_declarative_models import app +async def test_sqlalchemy_declarative_models(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: + engine = create_async_engine("sqlite+aiosqlite:///test.sqlite", poolclass=NullPool) + + session_config = AsyncSessionConfig(expire_on_commit=False) + sqlalchemy_config = SQLAlchemyAsyncConfig( + session_config=session_config, + create_all=True, + engine_instance=engine, + ) # Create 'async_session' dependency. + from docs.examples.contrib.sqlalchemy import sqlalchemy_declarative_models - with TestClient(app) as client: + monkeypatch.setattr(sqlalchemy_declarative_models, "sqlalchemy_config", sqlalchemy_config) + async with engine.begin() as connection: + await connection.run_sync(sqlalchemy_declarative_models.Author.metadata.create_all) + await connection.commit() + with TestClient(sqlalchemy_declarative_models.app) as client: response = client.get("/authors") assert response.status_code == 200 assert len(response.json()) > 0 diff --git a/tests/examples/test_responses/test_json_suffix_responses.py b/tests/examples/test_responses/test_json_suffix_responses.py index f853600087..57a245b8e7 100644 --- a/tests/examples/test_responses/test_json_suffix_responses.py +++ b/tests/examples/test_responses/test_json_suffix_responses.py @@ -12,4 +12,4 @@ def test_json_suffix_responses() -> None: "type": "Server delusion", "status": 418, } - assert res.headers["content-type"] == "application/problem+json" + assert res.headers["content-type"] == "application/vnd.example.resource+json" diff --git a/tests/examples/test_security/test_jwt/test_verify_issuer_audience.py b/tests/examples/test_security/test_jwt/test_verify_issuer_audience.py new file mode 100644 index 0000000000..c6dec15017 --- /dev/null +++ b/tests/examples/test_security/test_jwt/test_verify_issuer_audience.py @@ -0,0 +1,18 @@ +from litestar.testing import TestClient + + +def test_app() -> None: + from docs.examples.security.jwt.verify_issuer_audience import app, jwt_auth + + valid_token = jwt_auth.create_token( + "foo", + token_audience=jwt_auth.accepted_audiences[0], + token_issuer=jwt_auth.accepted_issuers[0], + ) + invalid_token = jwt_auth.create_token("foo") + + with TestClient(app) as client: + response = client.get("/", headers={"Authorization": jwt_auth.format_auth_header(valid_token)}) + assert response.status_code == 200 + response = client.get("/", headers={"Authorization": jwt_auth.format_auth_header(invalid_token)}) + assert response.status_code == 401 diff --git a/tests/helpers.py b/tests/helpers.py index b541ed28ec..353a592eb5 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,14 +1,16 @@ from __future__ import annotations import atexit +import importlib.util import inspect import logging import random import sys from contextlib import AbstractContextManager, contextmanager +from pathlib import Path from typing import Any, AsyncContextManager, Awaitable, ContextManager, Generator, TypeVar, cast, overload -import picologging +import pytest from _pytest.logging import LogCaptureHandler, _LiveLoggingNullHandler from litestar._openapi.schema_generation import SchemaCreator @@ -90,9 +92,9 @@ def cleanup_logging_impl() -> Generator: # Don't interfere with PyTest handler config if not isinstance(std_handler, (_LiveLoggingNullHandler, LogCaptureHandler)): std_root_logger.removeHandler(std_handler) - + picologging = pytest.importorskip("picologging") # Reset root logger (`picologging` module) - pico_root_logger: picologging.Logger = picologging.getLogger() + pico_root_logger: picologging.Logger = picologging.getLogger() # type: ignore[name-defined,unused-ignore] # pyright: ignore[reportPrivateUsage,reportGeneralTypeIssues,reportAssignmentType,reportInvalidTypeForm] for pico_handler in pico_root_logger.handlers: pico_root_logger.removeHandler(pico_handler) @@ -111,3 +113,10 @@ def cleanup_logging_impl() -> Generator: def not_none(val: T | None) -> T: assert val is not None return val + + +def purge_module(module_names: list[str], path: str | Path) -> None: + for name in module_names: + if name in sys.modules: + del sys.modules[name] + Path(importlib.util.cache_from_source(path)).unlink(missing_ok=True) # type: ignore[arg-type] diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index 20b9a4a116..084e0572fa 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -6,7 +6,7 @@ from collections.abc import AsyncGenerator from contextlib import asynccontextmanager from dataclasses import fields -from typing import TYPE_CHECKING, Callable, List, Tuple +from typing import TYPE_CHECKING, Any, Callable, List, Tuple from unittest.mock import MagicMock, Mock, PropertyMock import pytest @@ -258,7 +258,7 @@ def test_using_custom_http_exception_handler() -> None: @get("/{param:int}") def my_route_handler(param: int) -> None: ... - def my_custom_handler(_: Request, __: Exception) -> Response: + def my_custom_handler(_: Request[Any, Any, State], __: Exception) -> Response[str]: return Response(content="custom message", media_type=MediaType.TEXT, status_code=HTTP_400_BAD_REQUEST) with create_test_client(my_route_handler, exception_handlers={NotFoundException: my_custom_handler}) as client: @@ -454,6 +454,11 @@ def test_use_dto_codegen_feature_flag_warns() -> None: Litestar(experimental_features=[ExperimentalFeatures.DTO_CODEGEN]) +def test_use_future_feature_flag_warns() -> None: + app = Litestar(experimental_features=[ExperimentalFeatures.FUTURE]) + assert app.experimental_features == frozenset([ExperimentalFeatures.FUTURE]) + + def test_using_custom_path_parameter() -> None: @get() def my_route_handler() -> None: ... diff --git a/tests/unit/test_channels/test_plugin.py b/tests/unit/test_channels/test_plugin.py index 3a75e4c482..9e36633211 100644 --- a/tests/unit/test_channels/test_plugin.py +++ b/tests/unit/test_channels/test_plugin.py @@ -17,8 +17,7 @@ from litestar.exceptions import ImproperlyConfiguredException, LitestarException from litestar.testing import TestClient, create_test_client from litestar.types.asgi_types import WebSocketMode - -from .util import get_from_stream +from tests.unit.test_channels.util import get_from_stream @pytest.fixture( diff --git a/tests/unit/test_connection/test_connection_caching.py b/tests/unit/test_connection/test_connection_caching.py index 43c2fe9865..acbf46e706 100644 --- a/tests/unit/test_connection/test_connection_caching.py +++ b/tests/unit/test_connection/test_connection_caching.py @@ -5,7 +5,7 @@ import pytest -from litestar import Request +from litestar import Request, post from litestar.testing import RequestFactory from litestar.types import Empty, HTTPReceiveMessage, Scope from litestar.utils.scope.state import ScopeState @@ -17,11 +17,15 @@ async def test_multiple_request_object_data_caching(create_scope: Callable[..., https://github.com/litestar-org/litestar/issues/2727 """ + @post("/", request_max_body_size=None) + async def handler() -> None: + pass + async def test_receive() -> HTTPReceiveMessage: mock() return {"type": "http.request", "body": b"abc", "more_body": False} - scope = create_scope() + scope = create_scope(route_handler=handler) request_1 = Request[Any, Any, Any](scope, test_receive) request_2 = Request[Any, Any, Any](scope, test_receive) assert (await request_1.body()) == b"abc" @@ -121,6 +125,8 @@ def check_get_mock() -> None: get_mock.assert_has_calls([call(state_key), call("headers")]) elif state_key == "form": get_mock.assert_has_calls([call(state_key), call("content_type")]) + elif state_key == "body": + get_mock.assert_has_calls([call(state_key), call("headers")]) else: get_mock.assert_called_once_with(state_key) @@ -136,6 +142,8 @@ def check_set_mock() -> None: set_mock.assert_has_calls([call("content_type", ANY), call(state_key, ANY)]) elif state_key in {"accept", "cookies", "content_type"}: set_mock.assert_has_calls([call("headers", ANY), call(state_key, ANY)]) + elif state_key == "body": + set_mock.assert_has_calls([call("headers", ANY), call(state_key, ANY)]) else: set_mock.assert_called_once_with(state_key, ANY) diff --git a/tests/unit/test_connection/test_request.py b/tests/unit/test_connection/test_request.py index 7393211647..688f5250a7 100644 --- a/tests/unit/test_connection/test_request.py +++ b/tests/unit/test_connection/test_request.py @@ -11,9 +11,9 @@ import pytest -from litestar import MediaType, Request, asgi, get, post -from litestar.connection.base import empty_send -from litestar.datastructures import Address, Cookie +from litestar import MediaType, Request, get, post +from litestar.connection.base import AuthT, StateT, UserT, empty_send +from litestar.datastructures import Address, Cookie, State from litestar.exceptions import ( InternalServerException, LitestarException, @@ -24,6 +24,7 @@ from litestar.response.base import ASGIResponse from litestar.serialization import encode_json, encode_msgpack from litestar.static_files.config import StaticFilesConfig +from litestar.status_codes import HTTP_400_BAD_REQUEST, HTTP_413_REQUEST_ENTITY_TOO_LARGE from litestar.testing import TestClient, create_test_client if TYPE_CHECKING: @@ -32,7 +33,7 @@ from litestar.types import ASGIApp, Receive, Scope, Send -@get("/", sync_to_thread=False) +@get("/", sync_to_thread=False, request_max_body_size=None) def _route_handler() -> None: pass @@ -44,40 +45,40 @@ def scope_fixture(create_scope: Callable[..., Scope]) -> Scope: async def test_request_empty_body_to_json(anyio_backend: str, scope: Scope) -> None: with patch.object(Request, "body", return_value=b""): - request_empty_payload: Request = Request(scope=scope) + request_empty_payload: Request[Any, Any, State] = Request(scope=scope) request_json = await request_empty_payload.json() assert request_json is None async def test_request_invalid_body_to_json(anyio_backend: str, scope: Scope) -> None: with patch.object(Request, "body", return_value=b"invalid"), pytest.raises(SerializationException): - request_empty_payload: Request = Request(scope=scope) + request_empty_payload: Request[Any, Any, State] = Request(scope=scope) await request_empty_payload.json() async def test_request_valid_body_to_json(anyio_backend: str, scope: Scope) -> None: with patch.object(Request, "body", return_value=b'{"test": "valid"}'): - request_empty_payload: Request = Request(scope=scope) + request_empty_payload: Request[Any, Any, State] = Request(scope=scope) request_json = await request_empty_payload.json() assert request_json == {"test": "valid"} async def test_request_empty_body_to_msgpack(anyio_backend: str, scope: Scope) -> None: with patch.object(Request, "body", return_value=b""): - request_empty_payload: Request = Request(scope=scope) + request_empty_payload: Request[Any, Any, State] = Request(scope=scope) request_msgpack = await request_empty_payload.msgpack() assert request_msgpack is None async def test_request_invalid_body_to_msgpack(anyio_backend: str, scope: Scope) -> None: with patch.object(Request, "body", return_value=b"invalid"), pytest.raises(SerializationException): - request_empty_payload: Request = Request(scope=scope) + request_empty_payload: Request[Any, Any, State] = Request(scope=scope) await request_empty_payload.msgpack() async def test_request_valid_body_to_msgpack(anyio_backend: str, scope: Scope) -> None: with patch.object(Request, "body", return_value=encode_msgpack({"test": "valid"})): - request_empty_payload: Request = Request(scope=scope) + request_empty_payload: Request[Any, Any, State] = Request(scope=scope) request_msgpack = await request_empty_payload.msgpack() assert request_msgpack == {"test": "valid"} @@ -88,11 +89,11 @@ def proxy() -> None: pass @get(path="/test", signature_namespace={"dict": Dict}) - def root(request: Request) -> dict[str, str]: + def root(request: Request[Any, Any, State]) -> dict[str, str]: return {"url": request.url_for("proxy")} @get(path="/test-none", signature_namespace={"dict": Dict}) - def test_none(request: Request) -> dict[str, str]: + def test_none(request: Request[Any, Any, State]) -> dict[str, str]: return {"url": request.url_for("none")} with create_test_client(route_handlers=[proxy, root, test_none]) as client: @@ -105,11 +106,11 @@ def test_none(request: Request) -> dict[str, str]: def test_request_asset_url(tmp_path: Path) -> None: @get(path="/resolver", signature_namespace={"dict": Dict}) - def resolver(request: Request) -> dict[str, str]: + def resolver(request: Request[Any, Any, State]) -> dict[str, str]: return {"url": request.url_for_static_asset("js", "main.js")} @get(path="/resolver-none", signature_namespace={"dict": Dict}) - def resolver_none(request: Request) -> dict[str, str]: + def resolver_none(request: Request[Any, Any, State]) -> dict[str, str]: return {"url": request.url_for_static_asset("none", "main.js")} with create_test_client( @@ -127,7 +128,7 @@ def test_route_handler_property() -> None: value: Any = {} @get("/") - def handler(request: Request) -> None: + def handler(request: Request[Any, Any, State]) -> None: value["handler"] = request.route_handler with create_test_client(route_handlers=[handler]) as client: @@ -138,13 +139,13 @@ def handler(request: Request) -> None: def test_custom_request_class() -> None: value: Any = {} - class MyRequest(Request): + class MyRequest(Request[UserT, AuthT, StateT]): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.scope["called"] = True # type: ignore[typeddict-unknown-key] @get("/", signature_types=[MyRequest]) - def handler(request: MyRequest) -> None: + def handler(request: MyRequest[Any, Any, State]) -> None: value["called"] = request.scope.get("called") with create_test_client(route_handlers=[handler], request_class=MyRequest) as client: @@ -154,7 +155,7 @@ def handler(request: MyRequest) -> None: def test_request_url() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: - request = Request[Any, Any, Any](scope, receive) + request = Request[Any, Any, State](scope, receive) data = {"method": request.method, "url": str(request.url)} response = ASGIResponse(body=encode_json(data)) await response(scope, receive, send) @@ -169,7 +170,7 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None: def test_request_query_params() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: - request = Request[Any, Any, Any](scope, receive) + request = Request[Any, Any, State](scope, receive) params = dict(request.query_params) response = ASGIResponse(body=encode_json({"params": params})) await response(scope, receive, send) @@ -181,7 +182,7 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None: def test_request_headers() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: - request = Request[Any, Any, Any](scope, receive) + request = Request[Any, Any, State](scope, receive) headers = dict(request.headers) response = ASGIResponse(body=encode_json({"headers": headers})) await response(scope, receive, send) @@ -201,7 +202,7 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None: def test_request_accept_header() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: - request = Request[Any, Any, Any](scope, receive) + request = Request[Any, Any, State](scope, receive) response = ASGIResponse(body=encode_json({"accepted_types": list(request.accept)})) await response(scope, receive, send) @@ -225,61 +226,56 @@ def test_request_client( scope.update(scope_values) # type: ignore[typeddict-item] if "client" not in scope_values: del scope["client"] # type: ignore[misc] - client = Request[Any, Any, Any](scope).client + client = Request[Any, Any, State](scope).client assert client == expected_client def test_request_body() -> None: - async def app(scope: Scope, receive: Receive, send: Send) -> None: - request = Request[Any, Any, Any](scope, receive) + @post("/") + async def handler(request: Request) -> bytes: body = await request.body() - response = ASGIResponse(body=encode_json({"body": body.decode()})) - await response(scope, receive, send) + return encode_json({"body": body.decode()}) - client = TestClient(app) + with create_test_client([handler]) as client: + response = client.post("/") + assert response.json() == {"body": ""} - response = client.get("/") - assert response.json() == {"body": ""} - - response = client.post("/", json={"a": "123"}) - assert response.json() == {"body": '{"a": "123"}'} + response = client.post("/", json={"a": "123"}) + assert response.json() == {"body": '{"a":"123"}'} - response = client.post("/", content="abc") - assert response.json() == {"body": "abc"} + response = client.post("/", content="abc") + assert response.json() == {"body": "abc"} def test_request_stream() -> None: - async def app(scope: Scope, receive: Receive, send: Send) -> None: - request = Request[Any, Any, Any](scope, receive) + @post("/") + async def handler(request: Request) -> bytes: body = b"" async for chunk in request.stream(): body += chunk - response = ASGIResponse(body=encode_json({"body": body.decode()})) - await response(scope, receive, send) + return encode_json({"body": body.decode()}) - client = TestClient(app) - - response = client.get("/") - assert response.json() == {"body": ""} + with create_test_client([handler]) as client: + response = client.post("/") + assert response.json() == {"body": ""} - response = client.post("/", json={"a": "123"}) - assert response.json() == {"body": '{"a": "123"}'} + response = client.post("/", json={"a": "123"}) + assert response.json() == {"body": '{"a":"123"}'} - response = client.post("/", content="abc") - assert response.json() == {"body": "abc"} + response = client.post("/", content="abc") + assert response.json() == {"body": "abc"} def test_request_form_urlencoded() -> None: - async def app(scope: Scope, receive: Receive, send: Send) -> None: - request = Request[Any, Any, Any](scope, receive) + @post("/") + async def handler(request: Request) -> bytes: form = await request.form() - response = ASGIResponse(body=encode_json({"form": dict(form)})) - await response(scope, receive, send) - client = TestClient(app) + return encode_json({"form": dict(form)}) - response = client.post("/", data={"abc": "123 @"}) - assert response.json() == {"form": {"abc": "123 @"}} + with create_test_client([handler]) as client: + response = client.post("/", data={"abc": "123 @"}) + assert response.json() == {"form": {"abc": "123 @"}} def test_request_form_urlencoded_multi_keys() -> None: @@ -301,24 +297,22 @@ async def handler(request: Request) -> int: def test_request_body_then_stream() -> None: - async def app(scope: Any, receive: Receive, send: Send) -> None: - request = Request[Any, Any, Any](scope, receive) + @post("/") + async def handler(request: Request) -> bytes: body = await request.body() chunks = b"" async for chunk in request.stream(): chunks += chunk - response = ASGIResponse(body=encode_json({"body": body.decode(), "stream": chunks.decode()})) - await response(scope, receive, send) + return encode_json({"body": body.decode(), "stream": chunks.decode()}) - client = TestClient(app) - - response = client.post("/", content="abc") - assert response.json() == {"body": "abc", "stream": "abc"} + with create_test_client([handler]) as client: + response = client.post("/", content="abc") + assert response.json() == {"body": "abc", "stream": "abc"} def test_request_stream_then_body() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: - request = Request[Any, Any, Any](scope, receive) + request = Request[Any, Any, State](scope, receive) chunks = b"" async for chunk in request.stream(): chunks += chunk @@ -329,19 +323,27 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None: response = ASGIResponse(body=encode_json({"body": body.decode(), "stream": chunks.decode()})) await response(scope, receive, send) - client = TestClient(app) + @post("/") + async def handler(request: Request) -> bytes: + chunks = b"" + async for chunk in request.stream(): + chunks += chunk + try: + body = await request.body() + except InternalServerException: + body = b"" + return encode_json({"body": body.decode(), "stream": chunks.decode()}) - response = client.post("/", content="abc") - assert response.json() == {"body": "", "stream": "abc"} + with create_test_client([handler]) as client: + response = client.post("/", content="abc") + assert response.json() == {"body": "", "stream": "abc"} def test_request_json() -> None: - @asgi("/") - async def handler(scope: Scope, receive: Receive, send: Send) -> None: - request = Request[Any, Any, Any](scope, receive) + @post("/") + async def handler(request: Request) -> bytes: data = await request.json() - response = ASGIResponse(body=encode_json({"json": data})) - await response(scope, receive, send) + return encode_json({"json": data}) with create_test_client(handler) as client: response = client.post("/", json={"a": "123"}) @@ -350,7 +352,7 @@ async def handler(scope: Scope, receive: Receive, send: Send) -> None: def test_request_raw_path() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: - request = Request[Any, Any, Any](scope, receive) + request = Request[Any, Any, State](scope, receive) path = str(request.scope["path"]) raw_path = str(request.scope["raw_path"]) response = ASGIResponse(body=f"{path}, {raw_path}".encode(), media_type=MediaType.TEXT) @@ -361,11 +363,12 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None: assert response.text == "/he/llo, b'/he%2Fllo'" -def test_request_without_setting_receive() -> None: +def test_request_without_setting_receive(create_scope: Callable[..., Scope]) -> None: """If Request is instantiated without the 'receive' channel, then .body() is not available.""" async def app(scope: Scope, receive: Receive, send: Send) -> None: - request = Request[Any, Any, Any](scope) + scope.update(create_scope(route_handler=_route_handler)) # type: ignore[typeddict-item] + request = Request[Any, Any, State](scope) try: data = await request.json() except RuntimeError: @@ -382,10 +385,10 @@ async def test_request_disconnect(create_scope: Callable[..., Scope]) -> None: """If a client disconnect occurs while reading request body then InternalServerException should be raised.""" async def app(scope: Scope, receive: Receive, send: Send) -> None: - request = Request[Any, Any, Any](scope, receive) + request = Request[Any, Any, State](scope, receive) await request.body() - async def receiver() -> dict: + async def receiver() -> dict[str, str]: return {"type": "http.disconnect"} with pytest.raises(InternalServerException): @@ -398,10 +401,10 @@ async def receiver() -> dict: def test_request_state() -> None: @get("/", signature_namespace={"dict": Dict}) - def handler(request: Request[Any, Any, Any]) -> dict[Any, Any]: + def handler(request: Request[Any, Any, State]) -> dict[Any, Any]: request.state.test = 1 assert request.state.test == 1 - return request.state.dict() # type: ignore[no-any-return] + return request.state.dict() with create_test_client(handler) as client: response = client.get("/") @@ -410,7 +413,7 @@ def handler(request: Request[Any, Any, Any]) -> dict[Any, Any]: def test_request_cookies() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: - request = Request[Any, Any, Any](scope, receive) + request = Request[Any, Any, State](scope, receive) mycookie = request.cookies.get("mycookie") if mycookie: asgi_response = ASGIResponse(body=mycookie.encode("utf-8"), media_type="text/plain") @@ -431,20 +434,19 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None: def test_chunked_encoding() -> None: - async def app(scope: Scope, receive: Receive, send: Send) -> None: - request = Request[Any, Any, Any](scope, receive) + @post("/") + async def handler(request: Request) -> bytes: body = await request.body() - response = ASGIResponse(body=encode_json({"body": body.decode()})) - await response(scope, receive, send) + return encode_json({"body": body.decode()}) - client = TestClient(app) + with create_test_client([handler]) as client: - def post_body() -> Generator[bytes, None, None]: - yield b"foo" - yield b"bar" + def post_body() -> Generator[bytes, None, None]: + yield b"foo" + yield b"bar" - response = client.post("/", content=post_body()) - assert response.json() == {"body": "foobar"} + response = client.post("/", content=post_body()) + assert response.json() == {"body": "foobar"} def test_request_send_push_promise() -> None: @@ -452,7 +454,7 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None: # the server is push-enabled scope["extensions"]["http.response.push"] = {} # type: ignore[index] - request = Request[Any, Any, Any](scope, receive, send) + request = Request[Any, Any, State](scope, receive, send) await request.send_push_promise("/style.css") response = ASGIResponse(body=encode_json({"json": "OK"})) @@ -470,7 +472,7 @@ def test_request_send_push_promise_without_push_extension() -> None: """ async def app(scope: Scope, receive: Receive, send: Send) -> None: - request = Request[Any, Any, Any](scope) + request = Request[Any, Any, State](scope) with pytest.warns(LitestarWarning, match="Attempted to send a push promise"): await request.send_push_promise("/style.css") @@ -490,7 +492,7 @@ def test_request_send_push_promise_without_push_extension_raises() -> None: """ async def app(scope: Scope, receive: Receive, send: Send) -> None: - request = Request[Any, Any, Any](scope) + request = Request[Any, Any, State](scope) with pytest.raises(LitestarException, match="Attempted to send a push promise"): await request.send_push_promise("/style.css", raise_if_unavailable=True) @@ -512,7 +514,7 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None: scope["extensions"]["http.response.push"] = {} # type: ignore[index] data = "OK" - request = Request[Any, Any, Any](scope) + request = Request[Any, Any, State](scope) try: await request.send_push_promise("/style.css") except RuntimeError: @@ -535,12 +537,12 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: def test_state() -> None: - def before_request(request: Request) -> None: + def before_request(request: Request[Any, Any, State]) -> None: assert request.state.main == 1 request.state.main = 2 @get(path="/", signature_namespace={"dict": Dict}) - async def get_state(request: Request) -> dict[str, str]: + async def get_state(request: Request[Any, Any, State]) -> dict[str, str]: return {"state": request.state.main} with create_test_client( @@ -548,3 +550,74 @@ async def get_state(request: Request) -> dict[str, str]: ) as client: response = client.get("/") assert response.json() == {"state": 2} + + +def test_request_body_exceeds_content_length() -> None: + @post("/") + def handler(body: bytes) -> None: + pass + + with create_test_client([handler]) as client: + response = client.post("/", headers={"content-length": "1"}, content=b"ab") + assert response.status_code == HTTP_400_BAD_REQUEST + assert response.json() == {"status_code": 400, "detail": "Malformed request"} + + +def test_request_body_exceeds_max_request_body_size() -> None: + @post("/one", request_max_body_size=1) + async def handler_one(request: Request) -> None: + await request.body() + + @post("/two", request_max_body_size=1) + async def handler_two(body: bytes) -> None: + pass + + with create_test_client([handler_one, handler_two]) as client: + response = client.post("/one", headers={"content-length": "2"}, content=b"ab") + assert response.status_code == HTTP_413_REQUEST_ENTITY_TOO_LARGE + + response = client.post("/two", headers={"content-length": "2"}, content=b"ab") + assert response.status_code == HTTP_413_REQUEST_ENTITY_TOO_LARGE + + +def test_request_body_exceeds_max_request_body_size_chunked() -> None: + @post("/one", request_max_body_size=1) + async def handler_one(request: Request) -> None: + assert request.headers["transfer-encoding"] == "chunked" + await request.body() + + @post("/two", request_max_body_size=1) + async def handler_two(body: bytes, request: Request) -> None: + assert request.headers["transfer-encoding"] == "chunked" + await request.body() + + def generator() -> Generator[bytes, None, None]: + yield b"1" + yield b"2" + + with create_test_client([handler_one, handler_two]) as client: + response = client.post("/one", content=generator()) + assert response.status_code == HTTP_413_REQUEST_ENTITY_TOO_LARGE + + response = client.post("/two", content=generator()) + assert response.status_code == HTTP_413_REQUEST_ENTITY_TOO_LARGE + + +def test_request_content_length() -> None: + @post("/") + def handler(request: Request) -> dict: + return {"content-length": request.content_length} + + with create_test_client([handler]) as client: + assert client.post("/", content=b"1").json() == {"content-length": 1} + + +def test_request_invalid_content_length() -> None: + @post("/") + def handler(request: Request) -> dict: + return {"content-length": request.content_length} + + with create_test_client([handler]) as client: + response = client.post("/", content=b"1", headers={"content-length": "a"}) + assert response.status_code == HTTP_400_BAD_REQUEST + assert response.json() == {"detail": "Invalid content-length: 'a'", "status_code": 400} diff --git a/tests/unit/test_connection/test_websocket.py b/tests/unit/test_connection/test_websocket.py index 5e7a2e2d38..2fe96f9eb9 100644 --- a/tests/unit/test_connection/test_websocket.py +++ b/tests/unit/test_connection/test_websocket.py @@ -12,6 +12,7 @@ import pytest from litestar.connection import WebSocket +from litestar.datastructures import State from litestar.datastructures.headers import Headers from litestar.exceptions import WebSocketDisconnect, WebSocketException from litestar.handlers.websocket_handlers import websocket @@ -27,7 +28,7 @@ @pytest.mark.parametrize("mode", ["text", "binary"]) def test_websocket_send_receive_json(mode: Literal["text", "binary"]) -> None: @websocket(path="/") - async def websocket_handler(socket: WebSocket) -> None: + async def websocket_handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() recv = await socket.receive_json(mode=mode) await socket.send_json({"message": recv}, mode=mode) @@ -43,7 +44,7 @@ def test_route_handler_property() -> None: value: Any = {} @websocket("/") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() value["handler"] = socket.route_handler await socket.close() @@ -57,7 +58,7 @@ async def handler(socket: WebSocket) -> None: ) async def test_accept_set_headers(headers: Any) -> None: @websocket("/") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept(headers=headers) await socket.send_text("abc") await socket.close() @@ -69,7 +70,7 @@ async def handler(socket: WebSocket) -> None: async def test_custom_request_class() -> None: value: Any = {} - class MyWebSocket(WebSocket[Any, Any, Any]): + class MyWebSocket(WebSocket[Any, Any, State]): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.scope["called"] = True # type: ignore[typeddict-unknown-key] @@ -86,7 +87,7 @@ async def handler(socket: MyWebSocket) -> None: def test_websocket_url() -> None: @websocket("/123") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() await socket.send_json({"url": str(socket.url)}) await socket.close() @@ -108,7 +109,7 @@ async def handler(socket: WebSocket) -> None: def test_websocket_binary_json() -> None: @websocket("/123") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() message = await socket.receive_json(mode="binary") await socket.send_json(message, mode="binary") @@ -121,7 +122,7 @@ async def handler(socket: WebSocket) -> None: def test_websocket_query_params() -> None: @websocket("/") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: query_params = dict(socket.query_params) await socket.accept() await socket.send_json({"params": query_params}) @@ -133,7 +134,7 @@ async def handler(socket: WebSocket) -> None: def test_websocket_headers() -> None: @websocket("/") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: headers = dict(socket.headers) await socket.accept() await socket.send_json({"headers": headers}) @@ -154,7 +155,7 @@ async def handler(socket: WebSocket) -> None: def test_websocket_port() -> None: @websocket("/123") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() await socket.send_json({"port": socket.url.port}) await socket.close() @@ -165,7 +166,7 @@ async def handler(socket: WebSocket) -> None: def test_websocket_send_and_receive_text() -> None: @websocket("/") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() data = await socket.receive_text() await socket.send_text(f"Message was: {data}") @@ -178,7 +179,7 @@ async def handler(socket: WebSocket) -> None: def test_websocket_send_and_receive_bytes() -> None: @websocket("/") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() data = await socket.receive_bytes() await socket.send_bytes(b"Message was: " + data) @@ -191,7 +192,7 @@ async def handler(socket: WebSocket) -> None: def test_websocket_send_and_receive_json() -> None: @websocket("/") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() data = await socket.receive_json() await socket.send_json({"message": data}) @@ -206,7 +207,7 @@ def test_send_msgpack() -> None: test_data = {"message": "hello, world"} @websocket("/") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() await socket.send_msgpack(test_data) await socket.close() @@ -221,7 +222,7 @@ def test_receive_msgpack() -> None: callback = MagicMock() @websocket("/") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() data = await socket.receive_msgpack() callback(data) @@ -249,7 +250,7 @@ def test_iter_data(mode: WebSocketMode, data: list[str | bytes]) -> None: values = [] @websocket("/") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() values.extend(await consume_gen(socket.iter_data(mode=mode), 2)) await socket.close() @@ -267,7 +268,7 @@ def test_iter_json(mode: WebSocketMode) -> None: values = [] @websocket("/") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() values.extend(await consume_gen(socket.iter_json(mode=mode), 2)) await socket.close() @@ -284,7 +285,7 @@ def test_iter_msgpack() -> None: values = [] @websocket("/") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() values.extend(await consume_gen(socket.iter_msgpack(), 2)) await socket.close() @@ -299,18 +300,18 @@ async def handler(socket: WebSocket) -> None: def test_websocket_concurrency_pattern() -> None: stream_send, stream_receive = anyio.create_memory_object_stream() # type: ignore[var-annotated] - async def reader(socket: WebSocket[Any, Any, Any]) -> None: + async def reader(socket: WebSocket[Any, Any, State]) -> None: async with stream_send: json_data = await socket.receive_json() await stream_send.send(json_data) - async def writer(socket: WebSocket[Any, Any, Any]) -> None: + async def writer(socket: WebSocket[Any, Any, State]) -> None: async with stream_receive: async for message in stream_receive: await socket.send_json(message) @websocket("/") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() async with anyio.create_task_group() as task_group: task_group.start_soon(reader, socket) @@ -327,7 +328,7 @@ def test_client_close() -> None: close_code = None @websocket("/") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: nonlocal close_code await socket.accept() try: @@ -342,7 +343,7 @@ async def handler(socket: WebSocket) -> None: def test_application_close() -> None: @websocket("/") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() await socket.close(WS_1001_GOING_AWAY) @@ -353,7 +354,7 @@ async def handler(socket: WebSocket) -> None: def test_rejected_connection() -> None: @websocket("/") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.close(WS_1001_GOING_AWAY) with pytest.raises(WebSocketDisconnect) as exc, create_test_client(handler).websocket_connect("/"): @@ -363,7 +364,7 @@ async def handler(socket: WebSocket) -> None: def test_subprotocol() -> None: @websocket("/") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: assert socket.scope["subprotocols"] == ["soap", "wamp"] await socket.accept(subprotocols="wamp") await socket.close() @@ -374,7 +375,7 @@ async def handler(socket: WebSocket) -> None: def test_additional_headers() -> None: @websocket("/") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept(headers=[(b"additional", b"header")]) await socket.close() @@ -384,7 +385,7 @@ async def handler(socket: WebSocket) -> None: def test_no_additional_headers() -> None: @websocket("/") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() await socket.close() @@ -402,7 +403,7 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None: def test_duplicate_disconnect() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: - socket = WebSocket[Any, Any, Any](scope, receive=receive, send=send) + socket = WebSocket[Any, Any, State](scope, receive=receive, send=send) await socket.accept() message = await socket.receive() assert message["type"] == "websocket.disconnect" @@ -414,7 +415,7 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None: def test_websocket_close_reason() -> None: @websocket("/") - async def handler(socket: WebSocket) -> None: + async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() await socket.close(code=WS_1001_GOING_AWAY, reason="Going Away") @@ -426,7 +427,7 @@ async def handler(socket: WebSocket) -> None: def test_receive_text_before_accept() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: - socket = WebSocket[Any, Any, Any](scope, receive=receive, send=send) + socket = WebSocket[Any, Any, State](scope, receive=receive, send=send) await socket.receive_text() with pytest.raises(WebSocketException), TestClient(app).websocket_connect("/"): @@ -435,7 +436,7 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None: def test_receive_bytes_before_accept() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: - socket = WebSocket[Any, Any, Any](scope, receive=receive, send=send) + socket = WebSocket[Any, Any, State](scope, receive=receive, send=send) await socket.receive_bytes() with pytest.raises(WebSocketException), TestClient(app).websocket_connect("/"): @@ -444,7 +445,7 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None: def test_receive_json_before_accept() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: - socket = WebSocket[Any, Any, Any](scope, receive=receive, send=send) + socket = WebSocket[Any, Any, State](scope, receive=receive, send=send) await socket.receive_json() with pytest.raises(WebSocketException), TestClient(app).websocket_connect("/"): diff --git a/tests/unit/test_contrib/conftest.py b/tests/unit/test_contrib/conftest.py index 5d849f75d0..28ece35c77 100644 --- a/tests/unit/test_contrib/conftest.py +++ b/tests/unit/test_contrib/conftest.py @@ -1,15 +1,9 @@ from __future__ import annotations -from dataclasses import replace from typing import TYPE_CHECKING -from unittest.mock import ANY import pytest -from litestar.dto import DTOField, Mark -from litestar.dto.data_structures import DTOFieldDefinition -from litestar.typing import FieldDefinition - if TYPE_CHECKING: from typing import Callable @@ -17,79 +11,3 @@ @pytest.fixture def int_factory() -> Callable[[], int]: return lambda: 2 - - -@pytest.fixture -def expected_field_defs(int_factory: Callable[[], int]) -> list[DTOFieldDefinition]: - return [ - DTOFieldDefinition.from_field_definition( - field_definition=FieldDefinition.from_kwarg( - annotation=int, - name="a", - ), - model_name=ANY, - default_factory=None, - dto_field=DTOField(), - ), - replace( - DTOFieldDefinition.from_field_definition( - field_definition=FieldDefinition.from_kwarg( - annotation=int, - name="b", - ), - model_name=ANY, - default_factory=None, - dto_field=DTOField(mark=Mark.READ_ONLY), - ), - metadata=ANY, - type_wrappers=ANY, - raw=ANY, - kwarg_definition=ANY, - ), - replace( - DTOFieldDefinition.from_field_definition( - field_definition=FieldDefinition.from_kwarg( - annotation=int, - name="c", - ), - model_name=ANY, - default_factory=None, - dto_field=DTOField(), - ), - metadata=ANY, - type_wrappers=ANY, - raw=ANY, - kwarg_definition=ANY, - ), - replace( - DTOFieldDefinition.from_field_definition( - field_definition=FieldDefinition.from_kwarg( - annotation=int, - name="d", - default=1, - ), - model_name=ANY, - default_factory=None, - dto_field=DTOField(), - ), - metadata=ANY, - type_wrappers=ANY, - raw=ANY, - kwarg_definition=ANY, - ), - replace( - DTOFieldDefinition.from_field_definition( - field_definition=FieldDefinition.from_kwarg( - annotation=int, - name="e", - ), - model_name=ANY, - default_factory=int_factory, - dto_field=DTOField(), - ), - metadata=ANY, - type_wrappers=ANY, - raw=ANY, - kwarg_definition=ANY, - ), - ] diff --git a/tests/unit/test_contrib/test_attrs.py b/tests/unit/test_contrib/test_attrs.py new file mode 100644 index 0000000000..5881c9a873 --- /dev/null +++ b/tests/unit/test_contrib/test_attrs.py @@ -0,0 +1,44 @@ +# ruff: noqa: TC004, F401 +from __future__ import annotations + +import sys +import warnings +from importlib.util import cache_from_source +from pathlib import Path + +import pytest + +from litestar.contrib import attrs as contrib_attrs +from litestar.plugins import attrs as plugin_attrs + + +def purge_module(module_names: list[str], path: str | Path) -> None: + for name in module_names: + if name in sys.modules: + del sys.modules[name] + Path(cache_from_source(str(path))).unlink(missing_ok=True) + + +def test_contrib_attrs_deprecation_warning() -> None: + """Test that importing from contrib.attrs raises a deprecation warning.""" + purge_module(["litestar.contrib.attrs"], __file__) + with pytest.warns( + DeprecationWarning, match="importing AttrsSchemaPlugin from 'litestar.contrib.attrs' is deprecated" + ): + from litestar.contrib.attrs import AttrsSchemaPlugin + + +def test_contrib_attrs_schema_deprecation_warning() -> None: + """Test that importing from contrib.attrs raises a deprecation warning.""" + purge_module(["litestar.contrib.attrs.attrs_schema_plugin"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing AttrsSchemaPlugin from 'litestar.contrib.attrs.attrs_schema_plugin' is deprecated", + ): + from litestar.contrib.attrs.attrs_schema_plugin import AttrsSchemaPlugin + + +def test_functionality_parity() -> None: + """Test that the functionality is identical between contrib and plugin versions.""" + assert contrib_attrs.AttrsSchemaPlugin is plugin_attrs.AttrsSchemaPlugin + assert contrib_attrs.is_attrs_class is plugin_attrs.is_attrs_class diff --git a/tests/unit/test_contrib/test_htmx/test_htmx_deprecations.py b/tests/unit/test_contrib/test_htmx/test_htmx_deprecations.py new file mode 100644 index 0000000000..1113479f60 --- /dev/null +++ b/tests/unit/test_contrib/test_htmx/test_htmx_deprecations.py @@ -0,0 +1,103 @@ +# ruff: noqa: TC004, F401 +# pyright: reportUnusedImport=false +import importlib +import sys +from pathlib import Path +from typing import List, Union + +import pytest + + +def purge_module(module_names: List[str], path: Union[str, Path]) -> None: + for name in module_names: + if name in sys.modules: + del sys.modules[name] + Path(importlib.util.cache_from_source(path)).unlink(missing_ok=True) # type: ignore[arg-type] + + +def test_deprecated_htmx_request() -> None: + purge_module(["litestar.contrib.htmx.request"], __file__) + with pytest.warns( + DeprecationWarning, match="importing HTMXDetails from 'litestar.contrib.htmx.request' is deprecated" + ): + from litestar.contrib.htmx.request import HTMXDetails + + purge_module(["litestar.contrib.htmx.request"], __file__) + with pytest.warns( + DeprecationWarning, match="importing HTMXDetails from 'litestar.contrib.htmx.request' is deprecated" + ): + from litestar.contrib.htmx.request import HTMXDetails + + +def test_deprecated_htmx_response() -> None: + purge_module(["litestar.contrib.htmx.response"], __file__) + with pytest.warns( + DeprecationWarning, match="importing HTMXTemplate from 'litestar.contrib.htmx.response' is deprecated" + ): + from litestar.contrib.htmx.response import HTMXTemplate + + purge_module(["litestar.contrib.htmx.request"], __file__) + with pytest.warns( + DeprecationWarning, match="importing HXLocation from 'litestar.contrib.htmx.response' is deprecated" + ): + from litestar.contrib.htmx.response import HXLocation + + purge_module(["litestar.contrib.htmx.request"], __file__) + with pytest.warns( + DeprecationWarning, match="importing HXStopPolling from 'litestar.contrib.htmx.response' is deprecated" + ): + from litestar.contrib.htmx.response import HXStopPolling + + purge_module(["litestar.contrib.htmx.request"], __file__) + with pytest.warns( + DeprecationWarning, match="importing ClientRedirect from 'litestar.contrib.htmx.response' is deprecated" + ): + from litestar.contrib.htmx.response import ClientRedirect + purge_module(["litestar.contrib.htmx.request"], __file__) + with pytest.warns( + DeprecationWarning, match="importing ClientRefresh from 'litestar.contrib.htmx.response' is deprecated" + ): + from litestar.contrib.htmx.response import ClientRefresh + purge_module(["litestar.contrib.htmx.request"], __file__) + with pytest.warns( + DeprecationWarning, match="importing PushUrl from 'litestar.contrib.htmx.response' is deprecated" + ): + from litestar.contrib.htmx.response import PushUrl + purge_module(["litestar.contrib.htmx.request"], __file__) + with pytest.warns( + DeprecationWarning, match="importing ReplaceUrl from 'litestar.contrib.htmx.response' is deprecated" + ): + from litestar.contrib.htmx.response import ReplaceUrl + purge_module(["litestar.contrib.htmx.request"], __file__) + with pytest.warns(DeprecationWarning, match="importing Reswap from 'litestar.contrib.htmx.response' is deprecated"): + from litestar.contrib.htmx.response import Reswap + purge_module(["litestar.contrib.htmx.request"], __file__) + with pytest.warns( + DeprecationWarning, match="importing Retarget from 'litestar.contrib.htmx.response' is deprecated" + ): + from litestar.contrib.htmx.response import Retarget + purge_module(["litestar.contrib.htmx.request"], __file__) + with pytest.warns( + DeprecationWarning, match="importing TriggerEvent from 'litestar.contrib.htmx.response' is deprecated" + ): + from litestar.contrib.htmx.response import TriggerEvent + + +def test_deprecated_htmx_types() -> None: + purge_module(["litestar.contrib.htmx.types"], __file__) + with pytest.warns( + DeprecationWarning, match="importing HtmxHeaderType from 'litestar.contrib.htmx.types' is deprecated" + ): + from litestar.contrib.htmx.types import HtmxHeaderType + + purge_module(["litestar.contrib.htmx.types"], __file__) + with pytest.warns( + DeprecationWarning, match="importing TriggerEventType from 'litestar.contrib.htmx.types' is deprecated" + ): + from litestar.contrib.htmx.types import TriggerEventType + + purge_module(["litestar.contrib.htmx.request"], __file__) + with pytest.warns( + DeprecationWarning, match="importing LocationType from 'litestar.contrib.htmx.types' is deprecated" + ): + from litestar.contrib.htmx.types import LocationType diff --git a/tests/unit/test_contrib/test_htmx/test_htmx_request.py b/tests/unit/test_contrib/test_htmx/test_htmx_request.py index 70bc2551f2..40ba87695e 100644 --- a/tests/unit/test_contrib/test_htmx/test_htmx_request.py +++ b/tests/unit/test_contrib/test_htmx/test_htmx_request.py @@ -1,8 +1,8 @@ from typing import Any, Optional from litestar import MediaType, get -from litestar.contrib.htmx._utils import HTMXHeaders from litestar.contrib.htmx.request import HTMXRequest +from litestar.plugins.htmx import HTMXHeaders from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client diff --git a/tests/unit/test_contrib/test_msgspec.py b/tests/unit/test_contrib/test_msgspec.py index 9c28ed4ba9..9c0f77ddf6 100644 --- a/tests/unit/test_contrib/test_msgspec.py +++ b/tests/unit/test_contrib/test_msgspec.py @@ -1,11 +1,15 @@ from __future__ import annotations +from dataclasses import replace from typing import TYPE_CHECKING +from unittest.mock import ANY +import pytest from msgspec import Meta, Struct, field from typing_extensions import Annotated -from litestar.dto import DTOField, MsgspecDTO, dto_field +from litestar import Litestar, post +from litestar.dto import DTOField, Mark, MsgspecDTO, dto_field from litestar.dto.data_structures import DTOFieldDefinition from litestar.typing import FieldDefinition @@ -13,6 +17,82 @@ from typing import Callable +@pytest.fixture +def expected_field_defs(int_factory: Callable[[], int]) -> list[DTOFieldDefinition]: + return [ + DTOFieldDefinition.from_field_definition( + field_definition=FieldDefinition.from_kwarg( + annotation=int, + name="a", + ), + model_name=ANY, + default_factory=None, + dto_field=DTOField(), + ), + replace( + DTOFieldDefinition.from_field_definition( + field_definition=FieldDefinition.from_kwarg( + annotation=int, + name="b", + ), + model_name=ANY, + default_factory=None, + dto_field=DTOField(mark=Mark.READ_ONLY), + ), + metadata=ANY, + type_wrappers=ANY, + raw=ANY, + kwarg_definition=ANY, + ), + replace( + DTOFieldDefinition.from_field_definition( + field_definition=FieldDefinition.from_kwarg( + annotation=int, + name="c", + ), + model_name=ANY, + default_factory=None, + dto_field=DTOField(), + ), + metadata=ANY, + type_wrappers=ANY, + raw=ANY, + kwarg_definition=ANY, + ), + replace( + DTOFieldDefinition.from_field_definition( + field_definition=FieldDefinition.from_kwarg( + annotation=int, + name="d", + default=1, + ), + model_name=ANY, + default_factory=None, + dto_field=DTOField(), + ), + metadata=ANY, + type_wrappers=ANY, + raw=ANY, + kwarg_definition=ANY, + ), + replace( + DTOFieldDefinition.from_field_definition( + field_definition=FieldDefinition.from_kwarg( + annotation=int, + name="e", + ), + model_name=ANY, + default_factory=int_factory, + dto_field=DTOField(), + ), + metadata=ANY, + type_wrappers=ANY, + raw=ANY, + kwarg_definition=ANY, + ), + ] + + def test_field_definition_generation( int_factory: Callable[[], int], expected_field_defs: list[DTOFieldDefinition] ) -> None: @@ -52,3 +132,64 @@ class Model(Struct): fields = list(dto_type.generate_field_definitions(Model)) assert fields[0].dto_field == DTOField("read-only") assert fields[1].dto_field == DTOField("read-only") + + +def test_tag_field_included_in_schema() -> None: + # default tag field, default tag value + class Model(Struct, tag=True): + regular_field: str + + # default tag field, custom tag value + class Model2(Struct, tag=2): + regular_field: str + + # custom tag field, custom tag value + class Model3(Struct, tag_field="foo", tag="bar"): + regular_field: str + + @post("/1") + def handler(data: Model) -> None: + return None + + @post("/2") + def handler_2(data: Model2) -> None: + return None + + @post("/3") + def handler_3(data: Model3) -> None: + return None + + components = Litestar( + [handler, handler_2, handler_3], + signature_types=[Model, Model2, Model3], + ).openapi_schema.components.to_schema()["schemas"] + + assert components["test_tag_field_included_in_schema.Model"] == { + "properties": { + "regular_field": {"type": "string"}, + "type": {"type": "string", "const": "Model"}, + }, + "type": "object", + "required": ["regular_field", "type"], + "title": "Model", + } + + assert components["test_tag_field_included_in_schema.Model2"] == { + "properties": { + "regular_field": {"type": "string"}, + "type": {"type": "integer", "const": 2}, + }, + "type": "object", + "required": ["regular_field", "type"], + "title": "Model2", + } + + assert components["test_tag_field_included_in_schema.Model3"] == { + "properties": { + "regular_field": {"type": "string"}, + "foo": {"type": "string", "const": "bar"}, + }, + "type": "object", + "required": ["foo", "regular_field"], + "title": "Model3", + } diff --git a/tests/unit/test_contrib/test_opentelemetry.py b/tests/unit/test_contrib/test_opentelemetry.py index f34907ef7d..d358a6394e 100644 --- a/tests/unit/test_contrib/test_opentelemetry.py +++ b/tests/unit/test_contrib/test_opentelemetry.py @@ -97,6 +97,7 @@ def handler() -> dict: } metric_data = reader.get_metrics_data() + assert metric_data assert metric_data.resource_metrics resource_metrics = metric_data.resource_metrics[0] diff --git a/tests/unit/test_contrib/test_piccolo_orm/test_piccolo_orm_dto.py b/tests/unit/test_contrib/test_piccolo_orm/test_piccolo_orm_dto.py index c382ae1ba4..7363573673 100644 --- a/tests/unit/test_contrib/test_piccolo_orm/test_piccolo_orm_dto.py +++ b/tests/unit/test_contrib/test_piccolo_orm/test_piccolo_orm_dto.py @@ -139,9 +139,14 @@ def test_piccolo_dto_openapi_spec_generation() -> None: assert concert_schema assert concert_schema.to_schema() == { "properties": { - "band_1": {"oneOf": [{"type": "null"}, {"type": "integer"}]}, - "band_2": {"oneOf": [{"type": "null"}, {"type": "integer"}]}, - "venue": {"oneOf": [{"type": "null"}, {"type": "integer"}]}, + "band_1": {"oneOf": [{"type": "integer"}, {"type": "null"}]}, + "band_2": { + "oneOf": [ + {"type": "integer"}, + {"type": "null"}, + ] + }, + "venue": {"oneOf": [{"type": "integer"}, {"type": "null"}]}, }, "required": [], "title": "CreateConcertConcertRequestBody", @@ -152,10 +157,10 @@ def test_piccolo_dto_openapi_spec_generation() -> None: assert record_studio_schema assert record_studio_schema.to_schema() == { "properties": { - "facilities": {"oneOf": [{"type": "null"}, {"type": "string"}]}, - "facilities_b": {"oneOf": [{"type": "null"}, {"type": "string"}]}, - "microphones": {"oneOf": [{"type": "null"}, {"items": {"type": "string"}, "type": "array"}]}, - "id": {"oneOf": [{"type": "null"}, {"type": "integer"}]}, + "facilities": {"oneOf": [{"type": "string"}, {"type": "null"}]}, + "facilities_b": {"oneOf": [{"type": "string"}, {"type": "null"}]}, + "microphones": {"oneOf": [{"items": {"type": "string"}, "type": "array"}, {"type": "null"}]}, + "id": {"oneOf": [{"type": "integer"}, {"type": "null"}]}, }, "required": [], "title": "RetrieveStudioRecordingStudioResponseBody", @@ -166,8 +171,8 @@ def test_piccolo_dto_openapi_spec_generation() -> None: assert venue_schema assert venue_schema.to_schema() == { "properties": { - "id": {"oneOf": [{"type": "null"}, {"type": "integer"}]}, - "name": {"oneOf": [{"type": "null"}, {"type": "string"}]}, + "id": {"oneOf": [{"type": "integer"}, {"type": "null"}]}, + "name": {"oneOf": [{"type": "string"}, {"type": "null"}]}, }, "required": [], "title": "RetrieveVenuesVenueResponseBody", diff --git a/tests/unit/test_contrib/test_prometheus.py b/tests/unit/test_contrib/test_prometheus.py index e9d82f7430..48bb483642 100644 --- a/tests/unit/test_contrib/test_prometheus.py +++ b/tests/unit/test_contrib/test_prometheus.py @@ -1,217 +1,63 @@ -import re -import time -from http.client import HTTPException +# ruff: noqa: TC004, F401 +from __future__ import annotations + +import importlib +import sys +from importlib.util import cache_from_source from pathlib import Path -from typing import Any import pytest -from _pytest.monkeypatch import MonkeyPatch -from prometheus_client import REGISTRY -from pytest_mock import MockerFixture - -from litestar import get, post, websocket_listener -from litestar.contrib.prometheus import PrometheusConfig, PrometheusController, PrometheusMiddleware -from litestar.status_codes import HTTP_200_OK -from litestar.testing import create_test_client - - -def create_config(**kwargs: Any) -> PrometheusConfig: - collectors = list(REGISTRY._collector_to_names.keys()) - for collector in collectors: - REGISTRY.unregister(collector) - - PrometheusMiddleware._metrics = {} - return PrometheusConfig(**kwargs) - - -@pytest.mark.flaky(reruns=5) -def test_prometheus_exporter_metrics_with_http() -> None: - config = create_config() - - @get("/duration") - def duration_handler() -> dict: - time.sleep(0.1) - return {"hello": "world"} - - @get("/error") - def handler_error() -> dict: - raise HTTPException("Error Occurred") - - with create_test_client( - [duration_handler, handler_error, PrometheusController], middleware=[config.middleware] - ) as client: - client.get("/error") - client.get("/duration") - metrics_exporter_response = client.get("/metrics") - - assert metrics_exporter_response.status_code == HTTP_200_OK - metrics = metrics_exporter_response.content.decode() - - assert ( - """litestar_request_duration_seconds_sum{app_name="litestar",method="GET",path="/duration",status_code="200"}""" - in metrics - ) - - assert ( - """litestar_requests_error_total{app_name="litestar",method="GET",path="/error",status_code="500"} 1.0""" - in metrics - ) - - assert ( - """litestar_request_duration_seconds_bucket{app_name="litestar",le="0.005",method="GET",path="/error",status_code="500"} 1.0""" - in metrics - ) - - assert ( - """litestar_requests_in_progress{app_name="litestar",method="GET",path="/metrics",status_code="200"} 1.0""" - in metrics - ) - - assert ( - """litestar_requests_in_progress{app_name="litestar",method="GET",path="/duration",status_code="200"} 0.0""" - in metrics - ) - - duration_metric_matches = re.findall( - r"""litestar_request_duration_seconds_sum{app_name="litestar",method="GET",path="/duration",status_code="200"} (\d+\.\d+)""", - metrics, - ) - - assert duration_metric_matches != [] - assert round(float(duration_metric_matches[0]), 1) == 0.1 - - client.get("/duration") - metrics = client.get("/metrics").content.decode() - - assert ( - """litestar_requests_total{app_name="litestar",method="GET",path="/duration",status_code="200"} 2.0""" - in metrics - ) - - assert ( - """litestar_requests_in_progress{app_name="litestar",method="GET",path="/error",status_code="200"} 0.0""" - in metrics - ) - - assert ( - """litestar_requests_in_progress{app_name="litestar",method="GET",path="/metrics",status_code="200"} 1.0""" - in metrics - ) - - -def test_prometheus_middleware_configurations() -> None: - labels = {"foo": "bar", "baz": lambda a: "qux"} - - config = create_config( - app_name="litestar_test", - prefix="litestar_rocks", - labels=labels, - buckets=[0.1, 0.5, 1.0], - excluded_http_methods=["POST"], - ) - - @get("/test") - def test() -> dict: - return {"hello": "world"} - - @post("/ignore") - def ignore() -> dict: - return {"hello": "world"} - - with create_test_client([test, ignore, PrometheusController], middleware=[config.middleware]) as client: - client.get("/test") - client.post("/ignore") - metrics_exporter_response = client.get("/metrics") - - assert metrics_exporter_response.status_code == HTTP_200_OK - metrics = metrics_exporter_response.content.decode() - - assert ( - """litestar_rocks_requests_total{app_name="litestar_test",baz="qux",foo="bar",method="GET",path="/test",status_code="200"} 1.0""" - in metrics - ) - - assert ( - """litestar_rocks_requests_total{app_name="litestar_test",baz="qux",foo="bar",method="POST",path="/ignore",status_code="201"} 1.0""" - not in metrics - ) - - assert ( - """litestar_rocks_request_duration_seconds_bucket{app_name="litestar_test",baz="qux",foo="bar",le="0.1",method="GET",path="/test",status_code="200"} 1.0""" - in metrics - ) - - assert ( - """litestar_rocks_request_duration_seconds_bucket{app_name="litestar_test",baz="qux",foo="bar",le="0.5",method="GET",path="/test",status_code="200"} 1.0""" - in metrics - ) - - assert ( - """litestar_rocks_request_duration_seconds_bucket{app_name="litestar_test",baz="qux",foo="bar",le="1.0",method="GET",path="/test",status_code="200"} 1.0""" - in metrics - ) - - -def test_prometheus_controller_configurations() -> None: - config = create_config( - exemplars=lambda a: {"trace_id": "1234"}, - ) - - class CustomPrometheusController(PrometheusController): - path: str = "/metrics/custom" - openmetrics_format: bool = True - - @get("/test") - def test() -> dict: - return {"hello": "world"} - - with create_test_client([test, CustomPrometheusController], middleware=[config.middleware]) as client: - client.get("/test") - - metrics_exporter_response = client.get("/metrics/custom") - - assert metrics_exporter_response.status_code == HTTP_200_OK - metrics = metrics_exporter_response.content.decode() - - assert ( - """litestar_requests_total{app_name="litestar",method="GET",path="/test",status_code="200"} 1.0 # {trace_id="1234"} 1.0""" - in metrics - ) - - -def test_prometheus_with_websocket() -> None: - config = create_config() - - @websocket_listener("/test") - def test(data: str) -> dict: - return {"hello": data} - - with create_test_client([test, PrometheusController], middleware=[config.middleware]) as client: - with client.websocket_connect("/test") as websocket: - websocket.send_text("litestar") - websocket.receive_json() - - metrics_exporter_response = client.get("/metrics") - - assert metrics_exporter_response.status_code == HTTP_200_OK - metrics = metrics_exporter_response.content.decode() - - assert ( - """litestar_requests_total{app_name="litestar",method="websocket",path="/test",status_code="200"} 1.0""" - in metrics - ) - - -@pytest.mark.parametrize("env_var", ["PROMETHEUS_MULTIPROC_DIR", "prometheus_multiproc_dir"]) -def test_procdir(monkeypatch: MonkeyPatch, tmp_path: Path, mocker: MockerFixture, env_var: str) -> None: - proc_dir = tmp_path / "something" - proc_dir.mkdir() - monkeypatch.setenv(env_var, str(proc_dir)) - config = create_config() - mock_registry = mocker.patch("litestar.contrib.prometheus.controller.CollectorRegistry") - mock_collector = mocker.patch("litestar.contrib.prometheus.controller.multiprocess.MultiProcessCollector") - with create_test_client([PrometheusController], middleware=[config.middleware]) as client: - client.get("/metrics") - mock_collector.assert_called_once_with(mock_registry.return_value) +def purge_module(module_names: list[str], path: str | Path) -> None: + for name in module_names: + if name in sys.modules: + del sys.modules[name] + Path(cache_from_source(str(path))).unlink(missing_ok=True) + + +def test_deprecated_prometheus_imports() -> None: + purge_module(["litestar.contrib.prometheus"], __file__) + with pytest.warns( + DeprecationWarning, match="importing PrometheusMiddleware from 'litestar.contrib.prometheus' is deprecated" + ): + from litestar.contrib.prometheus import PrometheusMiddleware + + purge_module(["litestar.contrib.prometheus"], __file__) + with pytest.warns( + DeprecationWarning, match="importing PrometheusConfig from 'litestar.contrib.prometheus' is deprecated" + ): + from litestar.contrib.prometheus import PrometheusConfig + + purge_module(["litestar.contrib.prometheus"], __file__) + with pytest.warns( + DeprecationWarning, match="importing PrometheusController from 'litestar.contrib.prometheus' is deprecated" + ): + from litestar.contrib.prometheus import PrometheusController + + +def test_deprecated_prometheus_middleware_imports() -> None: + purge_module(["litestar.contrib.prometheus.middleware"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing PrometheusMiddleware from 'litestar.contrib.prometheus.middleware' is deprecated", + ): + from litestar.contrib.prometheus.middleware import PrometheusMiddleware + + +def test_deprecated_prometheus_config_imports() -> None: + purge_module(["litestar.contrib.prometheus.config"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing PrometheusConfig from 'litestar.contrib.prometheus.config' is deprecated", + ): + from litestar.contrib.prometheus.config import PrometheusConfig + + +def test_deprecated_prometheus_controller_imports() -> None: + purge_module(["litestar.contrib.prometheus.controller"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing PrometheusController from 'litestar.contrib.prometheus.controller' is deprecated", + ): + from litestar.contrib.prometheus.controller import PrometheusController diff --git a/tests/unit/test_contrib/test_pydantic.py b/tests/unit/test_contrib/test_pydantic.py new file mode 100644 index 0000000000..337978884c --- /dev/null +++ b/tests/unit/test_contrib/test_pydantic.py @@ -0,0 +1,119 @@ +# ruff: noqa: TC004, F401 +from __future__ import annotations + +import importlib +import sys +from importlib.util import cache_from_source +from pathlib import Path + +import pytest + + +def purge_module(module_names: list[str], path: str | Path) -> None: + for name in module_names: + if name in sys.modules: + del sys.modules[name] + Path(cache_from_source(str(path))).unlink(missing_ok=True) + + +def test_deprecated_pydantic_imports() -> None: + purge_module(["litestar.contrib.pydantic"], __file__) + with pytest.warns(DeprecationWarning, match="importing PydanticDTO from 'litestar.contrib.pydantic' is deprecated"): + from litestar.contrib.pydantic import PydanticDTO + + purge_module(["litestar.contrib.pydantic"], __file__) + with pytest.warns( + DeprecationWarning, match="importing PydanticInitPlugin from 'litestar.contrib.pydantic' is deprecated" + ): + from litestar.contrib.pydantic import PydanticInitPlugin + + purge_module(["litestar.contrib.pydantic"], __file__) + with pytest.warns( + DeprecationWarning, match="importing PydanticSchemaPlugin from 'litestar.contrib.pydantic' is deprecated" + ): + from litestar.contrib.pydantic import PydanticSchemaPlugin + + purge_module(["litestar.contrib.pydantic"], __file__) + with pytest.warns( + DeprecationWarning, match="importing PydanticPlugin from 'litestar.contrib.pydantic' is deprecated" + ): + from litestar.contrib.pydantic import PydanticPlugin + + purge_module(["litestar.contrib.pydantic"], __file__) + with pytest.warns( + DeprecationWarning, match="importing PydanticDIPlugin from 'litestar.contrib.pydantic' is deprecated" + ): + from litestar.contrib.pydantic import PydanticDIPlugin + + +def test_deprecated_pydantic_dto_factory_imports() -> None: + purge_module(["litestar.contrib.pydantic.pydantic_dto_factory"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing PydanticDTO from 'litestar.contrib.pydantic' is deprecated", + ): + from litestar.contrib.pydantic import PydanticDTO + + +def test_deprecated_pydantic_init_plugin_imports() -> None: + purge_module(["litestar.contrib.pydantic.pydantic_init_plugin"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing PydanticInitPlugin from 'litestar.contrib.pydantic' is deprecated", + ): + from litestar.contrib.pydantic import PydanticInitPlugin + + +def test_deprecated_pydantic_schema_plugin_imports() -> None: + purge_module(["litestar.contrib.pydantic.pydantic_schema_plugin"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing PydanticSchemaPlugin from 'litestar.contrib.pydantic' is deprecated", + ): + from litestar.contrib.pydantic import PydanticSchemaPlugin + + +def test_deprecated_pydantic_di_plugin_imports() -> None: + purge_module(["litestar.contrib.pydantic"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing PydanticDIPlugin from 'litestar.contrib.pydantic' is deprecated", + ): + from litestar.contrib.pydantic import PydanticDIPlugin + + +def test_deprecated_pydantic_utils_imports() -> None: + purge_module(["litestar.contrib.pydantic.utils"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing get_model_info from 'litestar.contrib.pydantic.utils' is deprecated", + ): + from litestar.contrib.pydantic.utils import get_model_info + + purge_module(["litestar.contrib.pydantic.utils"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing is_pydantic_constrained_field from 'litestar.contrib.pydantic.utils' is deprecated", + ): + from litestar.contrib.pydantic.utils import is_pydantic_constrained_field + + purge_module(["litestar.contrib.pydantic.utils"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing is_pydantic_model_class from 'litestar.contrib.pydantic.utils' is deprecated", + ): + from litestar.contrib.pydantic.utils import is_pydantic_model_class + + purge_module(["litestar.contrib.pydantic.utils"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing is_pydantic_undefined from 'litestar.contrib.pydantic.utils' is deprecated", + ): + from litestar.contrib.pydantic.utils import is_pydantic_undefined + + purge_module(["litestar.contrib.pydantic.utils"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing is_pydantic_v2 from 'litestar.contrib.pydantic.utils' is deprecated", + ): + from litestar.contrib.pydantic.utils import is_pydantic_v2 diff --git a/tests/unit/test_contrib/test_sqlalchemy.py b/tests/unit/test_contrib/test_sqlalchemy.py index d5d95307d6..304568da5a 100644 --- a/tests/unit/test_contrib/test_sqlalchemy.py +++ b/tests/unit/test_contrib/test_sqlalchemy.py @@ -1,41 +1,60 @@ +# ruff: noqa: TC004, F401 +# pyright: reportUnusedImport=false from __future__ import annotations +import importlib +import sys +from pathlib import Path + import pytest +from advanced_alchemy import exceptions as advanced_alchemy_exceptions from advanced_alchemy import repository as advanced_alchemy_repo from advanced_alchemy import types as advanced_alchemy_types from advanced_alchemy.repository import typing as advanced_alchemy_typing from sqlalchemy import Engine, create_engine from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine -from litestar.contrib.sqlalchemy.plugins.init.config.asyncio import SQLAlchemyAsyncConfig -from litestar.contrib.sqlalchemy.plugins.init.config.sync import SQLAlchemySyncConfig + +def purge_module(module_names: list[str], path: str | Path) -> None: + for name in module_names: + if name in sys.modules: + del sys.modules[name] + Path(importlib.util.cache_from_source(path)).unlink(missing_ok=True) # type: ignore[arg-type] def test_create_engine_with_engine_instance() -> None: + from litestar.contrib.sqlalchemy.plugins.init.config.sync import SQLAlchemySyncConfig + engine = create_engine("sqlite:///:memory:") config = SQLAlchemySyncConfig(engine_instance=engine) with pytest.deprecated_call(): - assert engine is config.create_engine() + assert engine is config.create_engine() # type: ignore[attr-defined] def test_create_engine_with_connection_string() -> None: + from litestar.contrib.sqlalchemy.plugins.init.config.sync import SQLAlchemySyncConfig + config = SQLAlchemySyncConfig(connection_string="sqlite:///:memory:") with pytest.deprecated_call(): - engine = config.create_engine() + engine = config.create_engine() # type: ignore[attr-defined] assert isinstance(engine, Engine) def test_async_create_engine_with_engine_instance() -> None: + from litestar.contrib.sqlalchemy.plugins.init.config.asyncio import SQLAlchemyAsyncConfig + engine = create_async_engine("sqlite+aiosqlite:///:memory:") config = SQLAlchemyAsyncConfig(engine_instance=engine) with pytest.deprecated_call(): - assert engine is config.create_engine() + assert engine is config.create_engine() # type: ignore[attr-defined] def test_async_create_engine_with_connection_string() -> None: + from litestar.contrib.sqlalchemy.plugins.init.config.asyncio import SQLAlchemyAsyncConfig + config = SQLAlchemyAsyncConfig(connection_string="sqlite+aiosqlite:///:memory:") with pytest.deprecated_call(): - engine = config.create_engine() + engine = config.create_engine() # type: ignore[attr-defined] assert isinstance(engine, AsyncEngine) @@ -48,17 +67,309 @@ def test_repository_re_exports() -> None: ) from litestar.contrib.sqlalchemy.repository import types as repository_types + assert wrap_sqlalchemy_exception is advanced_alchemy_exceptions.wrap_sqlalchemy_exception assert SQLAlchemySyncRepository is advanced_alchemy_repo.SQLAlchemySyncRepository assert SQLAlchemyAsyncRepository is advanced_alchemy_repo.SQLAlchemyAsyncRepository - assert wrap_sqlalchemy_exception is advanced_alchemy_repo._util.wrap_sqlalchemy_exception - - assert repository_types.ModelT is advanced_alchemy_typing.ModelT - assert repository_types.RowT is advanced_alchemy_typing.RowT - assert repository_types.SQLAlchemyAsyncRepositoryT is advanced_alchemy_typing.SQLAlchemyAsyncRepositoryT - assert repository_types.SQLAlchemySyncRepositoryT is advanced_alchemy_typing.SQLAlchemySyncRepositoryT + assert repository_types.ModelT is advanced_alchemy_typing.ModelT # pyright: ignore[reportGeneralTypeIssues] + assert repository_types.RowT is advanced_alchemy_typing.RowT # pyright: ignore[reportGeneralTypeIssues] + assert repository_types.SQLAlchemyAsyncRepositoryT is advanced_alchemy_typing.SQLAlchemyAsyncRepositoryT # pyright: ignore[reportGeneralTypeIssues] + assert repository_types.SQLAlchemySyncRepositoryT is advanced_alchemy_typing.SQLAlchemySyncRepositoryT # pyright: ignore[reportGeneralTypeIssues] assert types.GUID is advanced_alchemy_types.GUID assert types.ORA_JSONB is advanced_alchemy_types.ORA_JSONB assert types.BigIntIdentity is advanced_alchemy_types.BigIntIdentity assert types.DateTimeUTC is advanced_alchemy_types.DateTimeUTC assert types.JsonB is advanced_alchemy_types.JsonB + + +def test_deprecated_sqlalchemy_imports() -> None: + purge_module(["litestar.contrib.sqlalchemy"], __file__) + with pytest.warns( + DeprecationWarning, match="importing SQLAlchemyAsyncRepository from 'litestar.contrib.sqlalchemy' is deprecated" + ): + from litestar.contrib.sqlalchemy import SQLAlchemyAsyncRepository + purge_module(["litestar.contrib.sqlalchemy"], __file__) + with pytest.warns( + DeprecationWarning, match="importing SQLAlchemySyncRepository from 'litestar.contrib.sqlalchemy' is deprecated" + ): + from litestar.contrib.sqlalchemy import SQLAlchemySyncRepository + purge_module(["litestar.contrib.sqlalchemy"], __file__) + with pytest.warns(DeprecationWarning, match="importing ModelT from 'litestar.contrib.sqlalchemy' is deprecated"): + from litestar.contrib.sqlalchemy import ModelT + purge_module(["litestar.contrib.sqlalchemy"], __file__) + with pytest.warns( + DeprecationWarning, match="importing wrap_sqlalchemy_exception from 'litestar.contrib.sqlalchemy' is deprecated" + ): + from litestar.contrib.sqlalchemy import wrap_sqlalchemy_exception + + +def test_deprecated_sqlalchemy_plugins_imports() -> None: + purge_module(["litestar.contrib.sqlalchemy.plugins"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing AsyncSessionConfig from 'litestar.contrib.sqlalchemy.plugins' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins import AsyncSessionConfig + purge_module(["litestar.contrib.sqlalchemy.plugins"], __file__) + with pytest.warns( + DeprecationWarning, match="importing EngineConfig from 'litestar.contrib.sqlalchemy.plugins' is deprecated" + ): + from litestar.contrib.sqlalchemy.plugins import EngineConfig + purge_module(["litestar.contrib.sqlalchemy.plugins"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing GenericSQLAlchemyConfig from 'litestar.contrib.sqlalchemy.plugins' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins import GenericSQLAlchemyConfig + purge_module(["litestar.contrib.sqlalchemy.plugins"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing SQLAlchemyAsyncConfig from 'litestar.contrib.sqlalchemy.plugins' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins import SQLAlchemyAsyncConfig + purge_module(["litestar.contrib.sqlalchemy.plugins"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing SQLAlchemyInitPlugin from 'litestar.contrib.sqlalchemy.plugins' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins import SQLAlchemyInitPlugin + + +def test_deprecated_sqlalchemy_plugins_init_imports() -> None: + purge_module(["litestar.contrib.sqlalchemy.plugins.init"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing AsyncSessionConfig from 'litestar.contrib.sqlalchemy.plugins.init' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.init import AsyncSessionConfig + purge_module(["litestar.contrib.sqlalchemy.plugins.init"], __file__) + with pytest.warns( + DeprecationWarning, match="importing EngineConfig from 'litestar.contrib.sqlalchemy.plugins.init' is deprecated" + ): + from litestar.contrib.sqlalchemy.plugins.init import EngineConfig + purge_module(["litestar.contrib.sqlalchemy.plugins.init"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing GenericSQLAlchemyConfig from 'litestar.contrib.sqlalchemy.plugins.init' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.init import GenericSQLAlchemyConfig + purge_module(["litestar.contrib.sqlalchemy.plugins.init"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing SQLAlchemyAsyncConfig from 'litestar.contrib.sqlalchemy.plugins.init' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.init import SQLAlchemyAsyncConfig + + +def test_deprecated_sqlalchemy_plugins_init_config_imports() -> None: + purge_module(["litestar.contrib.sqlalchemy.plugins.init.config"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing AsyncSessionConfig from 'litestar.contrib.sqlalchemy.plugins.init.config' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.init.config import AsyncSessionConfig + purge_module(["litestar.contrib.sqlalchemy.plugins.init.config"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing EngineConfig from 'litestar.contrib.sqlalchemy.plugins.init.config' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.init.config import EngineConfig + purge_module(["litestar.contrib.sqlalchemy.plugins.init.config"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing GenericSQLAlchemyConfig from 'litestar.contrib.sqlalchemy.plugins.init.config' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.init.config import GenericSQLAlchemyConfig + purge_module(["litestar.contrib.sqlalchemy.plugins.init.config"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing SQLAlchemyAsyncConfig from 'litestar.contrib.sqlalchemy.plugins.init.config' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.init.config import SQLAlchemyAsyncConfig + + +def test_deprecated_sqlalchemy_plugins_init_config_common_imports() -> None: + purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.common"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing SESSION_SCOPE_KEY from 'litestar.contrib.sqlalchemy.plugins.init.config.common' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.init.config.common import SESSION_SCOPE_KEY + purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.common"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing SESSION_TERMINUS_ASGI_EVENTS from 'litestar.contrib.sqlalchemy.plugins.init.config.common' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.init.config.common import SESSION_TERMINUS_ASGI_EVENTS + purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.common"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing GenericSQLAlchemyConfig from 'litestar.contrib.sqlalchemy.plugins.init.config.common' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.init.config.common import GenericSQLAlchemyConfig + + +def test_deprecated_sqlalchemy_plugins_init_config_sync_imports() -> None: + purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.sync"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing SQLAlchemySyncConfig from 'litestar.contrib.sqlalchemy.plugins.init.config.sync' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.init.config.sync import SQLAlchemySyncConfig + purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.sync"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing AlembicSyncConfig from 'litestar.contrib.sqlalchemy.plugins.init.config.sync' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.init.config.sync import AlembicSyncConfig + purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.sync"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing SyncSessionConfig from 'litestar.contrib.sqlalchemy.plugins.init.config.sync' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.init.config.sync import SyncSessionConfig + + +def test_deprecated_sqlalchemy_plugins_init_config_asyncio_imports() -> None: + purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.asyncio"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing SQLAlchemyAsyncConfig from 'litestar.contrib.sqlalchemy.plugins.init.config.asyncio' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.init.config.asyncio import SQLAlchemyAsyncConfig + purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.asyncio"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing AlembicAsyncConfig from 'litestar.contrib.sqlalchemy.plugins.init.config.asyncio' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.init.config.asyncio import AlembicAsyncConfig + purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.asyncio"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing AsyncSessionConfig from 'litestar.contrib.sqlalchemy.plugins.init.config.asyncio' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.init.config.asyncio import AsyncSessionConfig + + +def test_deprecated_sqlalchemy_plugins_init_config_engine_imports() -> None: + purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.engine"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing EngineConfig from 'litestar.contrib.sqlalchemy.plugins.init.config.engine' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.init.config.engine import EngineConfig + + +def test_deprecated_sqlalchemy_dto_imports() -> None: + purge_module(["litestar.contrib.sqlalchemy.dto"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing SQLAlchemyDTOConfig from 'litestar.contrib.sqlalchemy.dto' is deprecated", + ): + from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTOConfig + + +def test_deprecated_sqlalchemy_plugins_init_plugin_imports() -> None: + purge_module(["litestar.contrib.sqlalchemy.plugins.init.plugin"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing SQLAlchemyInitPlugin from 'litestar.contrib.sqlalchemy.plugins.init' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.init.plugin import SQLAlchemyInitPlugin + + +def test_deprecated_sqlalchemy_plugins_serialization_imports() -> None: + purge_module(["litestar.contrib.sqlalchemy.plugins.serialization"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing SQLAlchemySerializationPlugin from 'litestar.contrib.sqlalchemy.plugins.serialization' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.serialization import SQLAlchemySerializationPlugin + + +def test_deprecated_sqlalchemy_repository_async_imports() -> None: + purge_module(["litestar.contrib.sqlalchemy.repository._async"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing SQLAlchemyAsyncRepository from 'litestar.contrib.sqlalchemy.repository._async' is deprecated", + ): + from litestar.contrib.sqlalchemy.repository._async import SQLAlchemyAsyncRepository + + +def test_deprecated_sqlalchemy_repository_sync_imports() -> None: + purge_module(["litestar.contrib.sqlalchemy.repository._sync"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing SQLAlchemySyncRepository from 'litestar.contrib.sqlalchemy.repository._sync' is deprecated", + ): + from litestar.contrib.sqlalchemy.repository._sync import SQLAlchemySyncRepository + + +def test_deprecated_sqlalchemy_base_imports() -> None: + purge_module(["litestar.contrib.sqlalchemy.base"], __file__) + with pytest.warns( + DeprecationWarning, + match="from 'litestar.contrib.sqlalchemy.base' is deprecated", + ): + from litestar.contrib.sqlalchemy.base import ( + AuditColumns, + BigIntAuditBase, + BigIntBase, + BigIntPrimaryKey, + CommonTableAttributes, + ModelProtocol, + UUIDAuditBase, + UUIDBase, + UUIDPrimaryKey, + create_registry, + orm_registry, + touch_updated_timestamp, + ) + + +def test_deprecated_sqlalchemy_plugins_init_config_asyncio_handlers() -> None: + purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.asyncio"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing default_before_send_handler from 'litestar.contrib.sqlalchemy.plugins.init.config.asyncio' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.init.config.asyncio import default_before_send_handler + + purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.asyncio"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing autocommit_before_send_handler from 'litestar.contrib.sqlalchemy.plugins.init.config.asyncio' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.init.config.asyncio import autocommit_before_send_handler + + +def test_deprecated_sqlalchemy_plugins_init_config_sync_handlers() -> None: + purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.sync"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing default_before_send_handler from 'litestar.contrib.sqlalchemy.plugins.init.config.sync' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.init.config.sync import default_before_send_handler + + purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.sync"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing autocommit_before_send_handler from 'litestar.contrib.sqlalchemy.plugins.init.config.sync' is deprecated", + ): + from litestar.contrib.sqlalchemy.plugins.init.config.sync import autocommit_before_send_handler + + +def test_deprecated_sqlalchemy_repository_util_imports() -> None: + purge_module(["litestar.contrib.sqlalchemy.repository._util"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing wrap_sqlalchemy_exception from 'litestar.contrib.sqlalchemy.repository._util' is deprecated", + ): + from litestar.contrib.sqlalchemy.repository._util import wrap_sqlalchemy_exception + + purge_module(["litestar.contrib.sqlalchemy.repository._util"], __file__) + with pytest.warns( + DeprecationWarning, + match="importing get_instrumented_attr from 'litestar.contrib.sqlalchemy.repository._util' is deprecated", + ): + from litestar.contrib.sqlalchemy.repository._util import get_instrumented_attr diff --git a/tests/unit/test_datastructures/test_multi_dicts.py b/tests/unit/test_datastructures/test_multi_dicts.py index 78fec65f69..7ec7f386ca 100644 --- a/tests/unit/test_datastructures/test_multi_dicts.py +++ b/tests/unit/test_datastructures/test_multi_dicts.py @@ -1,7 +1,8 @@ from __future__ import annotations +from unittest.mock import patch + import pytest -from pytest_mock import MockerFixture from litestar.datastructures import UploadFile from litestar.datastructures.multi_dicts import FormMultiDict, ImmutableMultiDict, MultiDict @@ -34,20 +35,19 @@ def test_immutable_multi_dict_as_mutable() -> None: assert multi.mutable_copy().dict() == MultiDict(data).dict() -async def test_form_multi_dict_close(mocker: MockerFixture) -> None: - close = mocker.patch("litestar.datastructures.multi_dicts.UploadFile.close") - +async def test_form_multi_dict_close() -> None: multi = FormMultiDict( [ ("foo", UploadFile(filename="foo", content_type="text/plain")), ("bar", UploadFile(filename="foo", content_type="text/plain")), ] ) - + with patch("litestar.datastructures.multi_dicts.UploadFile.close") as mock_close: + await multi.close() + assert mock_close.call_count == 2 + # calls the real UploadFile.close method to clean up await multi.close() - assert close.call_count == 2 - @pytest.mark.parametrize("type_", [MultiDict, ImmutableMultiDict]) def test_copy(type_: type[MultiDict | ImmutableMultiDict]) -> None: diff --git a/tests/unit/test_dto/test_factory/test_integration.py b/tests/unit/test_dto/test_factory/test_integration.py index 8dd561bf75..8beefb3351 100644 --- a/tests/unit/test_dto/test_factory/test_integration.py +++ b/tests/unit/test_dto/test_factory/test_integration.py @@ -56,11 +56,13 @@ def handler(data: User = Body(media_type=RequestEncodingType.URL_ENCODED)) -> Us async def test_multipart_encoded_form_data(use_experimental_dto_backend: bool) -> None: + default_file = UploadFile(content_type="text/plain", filename="forbidden", file_data=b"forbidden") + @dataclass class Payload: file: UploadFile forbidden: UploadFile = field( - default=UploadFile(content_type="text/plain", filename="forbidden", file_data=b"forbidden"), + default=default_file, metadata=dto_field("read-only"), ) @@ -78,6 +80,8 @@ async def handler(data: Payload = Body(media_type=RequestEncodingType.MULTI_PART ) assert response.content == b"forbidden" + await default_file.close() + def test_renamed_field(use_experimental_dto_backend: bool) -> None: @dataclass @@ -967,7 +971,7 @@ def test_openapi_schema_for_type_with_custom_generic_type( from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from litestar import Litestar, get -from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO +from litestar.plugins.sqlalchemy import SQLAlchemyDTO from litestar.dto import DTOConfig T = TypeVar("T") @@ -999,7 +1003,7 @@ def get_users() -> WithCount[User]: """ ) openapi = cast("Litestar", module.app).openapi_schema - schema = openapi.components.schemas["WithCount[litestar.dto._backend.GetUsersUserResponseBody]"] + schema = openapi.components.schemas["WithCount_litestar.dto._backend.GetUsersUserResponseBody_"] assert not_none(schema.properties).keys() == {"count", "data"} model_schema = openapi.components.schemas["GetUsersUserResponseBody"] assert not_none(model_schema.properties).keys() == {"id", "name"} diff --git a/tests/unit/test_handlers/test_http_handlers/test_resolution.py b/tests/unit/test_handlers/test_http_handlers/test_resolution.py new file mode 100644 index 0000000000..2f328005ea --- /dev/null +++ b/tests/unit/test_handlers/test_http_handlers/test_resolution.py @@ -0,0 +1,66 @@ +import pytest + +from litestar import Controller, Litestar, Router, post +from litestar.exceptions import ImproperlyConfiguredException +from litestar.types import Empty + + +def test_resolve_request_max_body_size() -> None: + @post("/1") + def router_handler() -> None: + pass + + @post("/2") + def app_handler() -> None: + pass + + class MyController(Controller): + request_max_body_size = 2 + + @post("/3") + def controller_handler(self) -> None: + pass + + router = Router("/", route_handlers=[router_handler], request_max_body_size=1) + app = Litestar(route_handlers=[app_handler, router, MyController], request_max_body_size=3) + assert router_handler.resolve_request_max_body_size() == 1 + assert app_handler.resolve_request_max_body_size() == 3 + assert ( + next(r for r in app.routes if r.path == "/3").route_handler_map["POST"][0].resolve_request_max_body_size() == 2 # type: ignore[union-attr] + ) + + +def test_resolve_request_max_body_size_none() -> None: + @post("/1", request_max_body_size=None) + def router_handler() -> None: + pass + + Litestar([router_handler]) + assert router_handler.resolve_request_max_body_size() is None + + +def test_resolve_request_max_body_size_app_default() -> None: + @post("/") + def router_handler() -> None: + pass + + app = Litestar(route_handlers=[router_handler]) + + assert router_handler.resolve_request_max_body_size() == app.request_max_body_size == 10_000_000 + + +def test_resolve_request_max_body_size_empty_on_all_layers_raises() -> None: + @post("/") + def handler_one() -> None: + pass + + Litestar([handler_one], request_max_body_size=Empty) # type: ignore[arg-type] + with pytest.raises(ImproperlyConfiguredException): + handler_one.resolve_request_max_body_size() + + @post("/") + def handler_two() -> None: + pass + + with pytest.raises(ImproperlyConfiguredException): + handler_two.resolve_request_max_body_size() diff --git a/tests/unit/test_handlers/test_http_handlers/test_validations.py b/tests/unit/test_handlers/test_http_handlers/test_validations.py index 5395c77295..d7be2958d4 100644 --- a/tests/unit/test_handlers/test_http_handlers/test_validations.py +++ b/tests/unit/test_handlers/test_http_handlers/test_validations.py @@ -1,12 +1,14 @@ from pathlib import Path from types import ModuleType -from typing import Callable, Dict +from typing import Any, Callable, Dict, List import pytest +from typing_extensions import Annotated -from litestar import HttpMethod, Litestar, WebSocket, delete, get, route +from litestar import HttpMethod, Litestar, WebSocket, delete, get, patch, post, put, route from litestar.exceptions import ImproperlyConfiguredException, ValidationException from litestar.handlers.http_handlers import HTTPRouteHandler +from litestar.params import Body from litestar.response import File, Redirect from litestar.status_codes import ( HTTP_100_CONTINUE, @@ -144,3 +146,22 @@ def no_response_handler() -> {return_annotation}: return Litestar(route_handlers=[module.no_response_handler]) + + +@pytest.mark.parametrize("decorator", [post, put, patch]) +def test_body_param_with_non_bytes_annotation_raises(decorator: Callable[..., Any]) -> None: + def handler_fn(body: List[str]) -> None: + pass + + with pytest.raises(ImproperlyConfiguredException, match="Invalid type annotation for 'body' parameter"): + Litestar([decorator()(handler_fn)]) + + +@pytest.mark.parametrize("decorator", [post, put, patch]) +def test_body_param_with_metadata_allowed(decorator: Callable[..., Any]) -> None: + def handler_fn(body: Annotated[bytes, Body(title="something")]) -> None: + pass + + # we expect no error here, even though the type isn't directly 'bytes' but has + # metadata attached to it + Litestar([decorator()(handler_fn)]) diff --git a/tests/unit/test_handlers/test_websocket_handlers/test_listeners.py b/tests/unit/test_handlers/test_websocket_handlers/test_listeners.py index 08c74690d4..f6afec0a2f 100644 --- a/tests/unit/test_handlers/test_websocket_handlers/test_listeners.py +++ b/tests/unit/test_handlers/test_websocket_handlers/test_listeners.py @@ -394,10 +394,10 @@ def some_dependency() -> str: class Listener(WebsocketListener): path = "/{name: str}" - def on_accept(self, name: str, state: State, query: dict, some: str) -> None: # type: ignore[override] + def on_accept(self, name: str, state: State, query: dict, some: str) -> None: # pyright: ignore on_accept_mock(name=name, state=state, query=query, some=some) - def on_disconnect(self, name: str, state: State, query: dict, some: str) -> None: # type: ignore[override] + def on_disconnect(self, name: str, state: State, query: dict, some: str) -> None: # pyright: ignore on_disconnect_mock(name=name, state=state, query=query, some=some) def on_receive(self, data: bytes) -> None: # pyright: ignore diff --git a/tests/unit/test_handlers/test_websocket_handlers/test_stream.py b/tests/unit/test_handlers/test_websocket_handlers/test_stream.py new file mode 100644 index 0000000000..aeb6222c8c --- /dev/null +++ b/tests/unit/test_handlers/test_websocket_handlers/test_stream.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +import asyncio +import dataclasses +from typing import AsyncGenerator, Dict, Generator +from unittest.mock import MagicMock + +import pytest + +from litestar import Controller, Litestar, WebSocket +from litestar.dto import DataclassDTO, dto_field +from litestar.exceptions import ImproperlyConfiguredException +from litestar.handlers.websocket_handlers import websocket_stream +from litestar.testing import create_test_client + + +def test_websocket_stream() -> None: + @websocket_stream("/") + async def handler(socket: WebSocket) -> AsyncGenerator[str, None]: + yield "foo" + yield "bar" + + with create_test_client([handler]) as client, client.websocket_connect("/") as ws: + assert ws.receive_text(timeout=0.1) == "foo" + assert ws.receive_text(timeout=0.1) == "bar" + + +def test_websocket_stream_in_controller() -> None: + class MyController(Controller): + @websocket_stream("/") + async def handler(self, socket: WebSocket) -> AsyncGenerator[str, None]: + yield "foo" + + with create_test_client([MyController]) as client, client.websocket_connect("/") as ws: + assert ws.receive_text(timeout=0.1) == "foo" + + +def test_websocket_stream_without_socket() -> None: + @websocket_stream("/") + async def handler() -> AsyncGenerator[str, None]: + yield "foo" + + with create_test_client([handler]) as client, client.websocket_connect("/") as ws: + assert ws.receive_text(timeout=0.1) == "foo" + + +def test_websocket_stream_dependency_injection() -> None: + async def provide_hello() -> str: + return "hello" + + # ensure we can inject dependencies + @websocket_stream("/1", dependencies={"greeting": provide_hello}) + async def handler_one(greeting: str) -> AsyncGenerator[str, None]: + yield greeting + + # ensure dependency injection also works with 'socket' present + @websocket_stream("/2", dependencies={"greeting": provide_hello}) + async def handler_two(socket: WebSocket, greeting: str) -> AsyncGenerator[str, None]: + yield greeting + + with create_test_client([handler_one, handler_two]) as client: + with client.websocket_connect("/1") as ws: + assert ws.receive_text(timeout=0.1) == "hello" + + with client.websocket_connect("/2") as ws: + assert ws.receive_text(timeout=0.1) == "hello" + + +def test_websocket_stream_dependencies_cleaned_up_after_stream_close() -> None: + mock = MagicMock() + + async def dep() -> AsyncGenerator[str, None]: + yield "foo" + mock() + + @websocket_stream( + "/", + dependencies={"message": dep}, + listen_for_disconnect=False, + ) + async def handler(socket: WebSocket, message: str) -> AsyncGenerator[str, None]: + yield "one" + await socket.receive_text() + yield message + + with create_test_client([handler]) as client, client.websocket_connect("/") as ws: + assert ws.receive_text(timeout=0.1) == "one" + assert mock.call_count == 0 + ws.send_text("") + assert ws.receive_text(timeout=0.1) == "foo" + + assert mock.call_count == 1 + + +def test_websocket_stream_handle_disconnect() -> None: + @websocket_stream("/") + async def handler() -> AsyncGenerator[str, None]: + while True: + yield "foo" + # sleep for longer than our read-timeout to ensure we're disconnecting prematurely + await asyncio.sleep(1) + + with create_test_client([handler]) as client, client.websocket_connect("/") as ws: + assert ws.receive_text(timeout=0.1) == "foo" + + with create_test_client([handler]) as client, client.websocket_connect("/") as ws: + # ensure we still disconnect even after receiving some data + ws.send_text("") + assert ws.receive_text(timeout=0.1) == "foo" + + +def test_websocket_stream_send_json() -> None: + @websocket_stream("/") + async def handler() -> AsyncGenerator[Dict[str, str], None]: # noqa: UP006 + yield {"hello": "there"} + yield {"and": "goodbye"} + + with create_test_client([handler]) as client, client.websocket_connect("/") as ws: + assert ws.receive_json(timeout=0.1) == {"hello": "there"} + assert ws.receive_json(timeout=0.1) == {"and": "goodbye"} + + +def test_websocket_stream_send_json_with_dto() -> None: + @dataclasses.dataclass + class Event: + id: int = dataclasses.field(metadata=dto_field("private")) + content: str + + @websocket_stream("/", return_dto=DataclassDTO[Event]) + async def handler() -> AsyncGenerator[Event, None]: + yield Event(id=1, content="hello") + + with create_test_client([handler], signature_types=[Event]) as client, client.websocket_connect("/") as ws: + assert ws.receive_json(timeout=0.1) == {"content": "hello"} + + +def test_raises_if_stream_fn_does_not_return_async_generator() -> None: + with pytest.raises(ImproperlyConfiguredException): + + @websocket_stream("/") # type: ignore[arg-type] + def foo() -> Generator[bytes, None, None]: + yield b"" + + Litestar([foo]) + + with pytest.raises(ImproperlyConfiguredException): + + @websocket_stream("/") # type: ignore[arg-type] + def foo() -> bytes: + return b"" + + Litestar([foo]) diff --git a/tests/unit/test_kwargs/test_cookie_params.py b/tests/unit/test_kwargs/test_cookie_params.py index 0a23c6eafa..7111fe0f70 100644 --- a/tests/unit/test_kwargs/test_cookie_params.py +++ b/tests/unit/test_kwargs/test_cookie_params.py @@ -1,8 +1,9 @@ from typing import Optional, Type import pytest +from typing_extensions import Annotated -from litestar import get +from litestar import get, post from litestar.params import Parameter, ParameterKwarg from litestar.status_codes import HTTP_200_OK, HTTP_400_BAD_REQUEST from litestar.testing import create_test_client @@ -36,3 +37,13 @@ def test_method(special_cookie: t_type = param) -> None: # type: ignore[valid-t client.cookies = param_dict # type: ignore[assignment] response = client.get(test_path) assert response.status_code == expected_code, response.json() + + +def test_cookie_param_with_post() -> None: + # https://github.com/litestar-org/litestar/issues/3734 + @post() + async def handler(data: str, secret: Annotated[str, Parameter(cookie="x-secret")]) -> None: + return None + + with create_test_client([handler], raise_server_exceptions=True) as client: + assert client.post("/", json={}).status_code == 400 diff --git a/tests/unit/test_kwargs/test_header_params.py b/tests/unit/test_kwargs/test_header_params.py index 8a654ba779..f281647bcc 100644 --- a/tests/unit/test_kwargs/test_header_params.py +++ b/tests/unit/test_kwargs/test_header_params.py @@ -1,8 +1,9 @@ from typing import Dict, Optional, Union import pytest +from typing_extensions import Annotated -from litestar import get +from litestar import get, post from litestar.params import Parameter, ParameterKwarg from litestar.status_codes import HTTP_200_OK, HTTP_400_BAD_REQUEST from litestar.testing import create_test_client @@ -37,3 +38,13 @@ def test_method(special_header: t_type = param) -> None: # type: ignore[valid-t assert response.status_code == HTTP_400_BAD_REQUEST, response.json() else: assert response.status_code == HTTP_200_OK, response.json() + + +def test_header_param_with_post() -> None: + # https://github.com/litestar-org/litestar/issues/3734 + @post() + async def handler(data: str, secret: Annotated[str, Parameter(header="x-secret")]) -> None: + return None + + with create_test_client([handler], raise_server_exceptions=True) as client: + assert client.post("/", json={}).status_code == 400 diff --git a/tests/unit/test_kwargs/test_multipart_data.py b/tests/unit/test_kwargs/test_multipart_data.py index fe67da01c5..9b2d295f6a 100644 --- a/tests/unit/test_kwargs/test_multipart_data.py +++ b/tests/unit/test_kwargs/test_multipart_data.py @@ -186,7 +186,11 @@ def test_multipart_request_multiple_files_with_headers(tmpdir: Any) -> None: "filename": "test2.txt", "content": "", "content_type": "text/plain", - "headers": [["content-disposition", "form-data"], ["x-custom", "f2"], ["content-type", "text/plain"]], + "headers": [ + ["content-disposition", 'form-data; name="test2"; filename="test2.txt"'], + ["x-custom", "f2"], + ["content-type", "text/plain"], + ], }, } @@ -292,6 +296,7 @@ def test_multipart_request_without_charset_for_filename() -> None: } +@pytest.mark.xfail(reason="filename* is deprecated and should not be used according to RFC-7578") def test_multipart_request_with_asterisks_filename() -> None: with create_test_client(form_handler) as client: response = client.post( @@ -456,13 +461,14 @@ async def hello_world(data: Optional[UploadFile] = Body(media_type=RequestEncodi @pytest.mark.parametrize("limit", (1000, 100, 10)) def test_multipart_form_part_limit(limit: int) -> None: @post("/", signature_types=[UploadFile]) - async def hello_world(data: List[UploadFile] = Body(media_type=RequestEncodingType.MULTI_PART)) -> None: - assert len(data) == limit + async def hello_world(data: List[UploadFile] = Body(media_type=RequestEncodingType.MULTI_PART)) -> dict: + return {"limit": len(data)} with create_test_client(route_handlers=[hello_world], multipart_form_part_limit=limit) as client: data = {str(i): "a" for i in range(limit)} response = client.post("/", files=data) assert response.status_code == HTTP_201_CREATED + assert response.json() == {"limit": limit} data = {str(i): "a" for i in range(limit)} data[str(limit + 1)] = "b" @@ -577,3 +583,18 @@ async def form_(request: Request, data: Annotated[AddProductFormMsgspec, Body(me headers={"Content-Type": "multipart/form-data; boundary=1f35df74046888ceaa62d8a534a076dd"}, ) assert response.status_code == HTTP_201_CREATED + + +def test_invalid_multipart_raises_client_error() -> None: + with create_test_client(form_handler) as client: + response = client.post( + "/form", + content=( + b"--20b303e711c4ab8c443184ac833ab00f\r\n" + b"Content-Disposition: form-data; " + b'name="value"\r\n\r\n' + b"--20b303e711c4ab8c44318833ab00f--\r\n" + ), + headers={"Content-Type": "multipart/form-data; charset=utf-8; boundary=20b303e711c4ab8c443184ac833ab00f"}, + ) + assert response.status_code == HTTP_400_BAD_REQUEST diff --git a/tests/unit/test_kwargs/test_query_params.py b/tests/unit/test_kwargs/test_query_params.py index a8541c844b..a2b60c4cb7 100644 --- a/tests/unit/test_kwargs/test_query_params.py +++ b/tests/unit/test_kwargs/test_query_params.py @@ -10,8 +10,9 @@ from urllib.parse import urlencode import pytest +from typing_extensions import Annotated -from litestar import MediaType, Request, get +from litestar import MediaType, Request, get, post from litestar.datastructures import MultiDict from litestar.di import Provide from litestar.params import Parameter @@ -221,3 +222,13 @@ def handler(page_size_dep: int) -> str: response = client.get("/?pageSize=1") assert response.status_code == HTTP_200_OK, response.text assert response.text == "1" + + +def test_query_params_with_post() -> None: + # https://github.com/litestar-org/litestar/issues/3734 + @post() + async def handler(data: str, secret: Annotated[str, Parameter(query="x-secret")]) -> None: + return None + + with create_test_client([handler], raise_server_exceptions=True) as client: + assert client.post("/", json={}).status_code == 400 diff --git a/tests/unit/test_logging/test_logging_config.py b/tests/unit/test_logging/test_logging_config.py index c4b73d8994..3423184efc 100644 --- a/tests/unit/test_logging/test_logging_config.py +++ b/tests/unit/test_logging/test_logging_config.py @@ -1,14 +1,13 @@ +import importlib import logging import sys import time from importlib.util import find_spec from logging.handlers import QueueHandler from queue import Queue -from types import ModuleType -from typing import TYPE_CHECKING, Any, Dict, Generator, Optional +from typing import TYPE_CHECKING, Any, Dict, Generator, Optional, Union, cast from unittest.mock import patch -import picologging import pytest from _pytest.logging import LogCaptureHandler, _LiveLoggingNullHandler @@ -21,9 +20,6 @@ default_handlers, default_picologging_handlers, ) -from litestar.logging.picologging import QueueListenerHandler as PicologgingQueueListenerHandler -from litestar.logging.standard import LoggingQueueListener -from litestar.logging.standard import QueueListenerHandler as StandardQueueListenerHandler from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client from tests.helpers import cleanup_logging_impl @@ -34,6 +30,7 @@ @pytest.fixture(autouse=True) def cleanup_logging() -> Generator: + _ = pytest.importorskip("picologging") with cleanup_logging_impl(): yield @@ -123,23 +120,54 @@ def test_dictconfig_on_startup(logging_module: str, dict_config_not_called: str) @pytest.mark.parametrize( - "logging_module, expected_handler_class, expected_listener_class", + "logging_module_str, expected_handler_class_str, expected_listener_class_str", [ [ - logging, - QueueHandler if sys.version_info >= (3, 12, 0) else StandardQueueListenerHandler, - LoggingQueueListener, + "logging", + "logging.handlers.QueueHandler" + if sys.version_info >= (3, 12, 0) + else "litestar.logging.standard.QueueListenerHandler", + "litestar.logging.standard.LoggingQueueListener", ], [ - picologging, - PicologgingQueueListenerHandler, - picologging.handlers.QueueListener, # pyright: ignore[reportGeneralTypeIssues] + "picologging", + "litestar.logging.picologging.QueueListenerHandler", + "picologging.handlers.QueueListener", # pyright: ignore[reportGeneralTypeIssues,reportAttributeAccessIssue] ], ], ) def test_default_queue_listener_handler( - logging_module: ModuleType, expected_handler_class: Any, expected_listener_class: Any, capsys: "CaptureFixture[str]" + logging_module_str: str, + expected_handler_class_str: Union[str, Any], + expected_listener_class_str: str, + capsys: "CaptureFixture[str]", ) -> None: + logging_module = importlib.import_module(logging_module_str) + if expected_handler_class_str == "litestar.logging.standard.QueueListenerHandler": + from litestar.logging.standard import QueueListenerHandler + + expected_handler_class = QueueListenerHandler + elif expected_handler_class_str == "litestar.logging.picologging.QueueListenerHandler": + from litestar.logging.picologging import QueueListenerHandler + + expected_handler_class = QueueListenerHandler + elif expected_handler_class_str == "logging.handlers.QueueHandler": + from logging.handlers import QueueHandler as QueueListenerHandler + + expected_handler_class = QueueListenerHandler + else: + expected_handler_class = importlib.import_module(expected_handler_class_str) + if expected_listener_class_str == "litestar.logging.standard.LoggingQueueListener": + from litestar.logging.standard import LoggingQueueListener + + expected_listener_class = LoggingQueueListener + elif expected_listener_class_str == "picologging.handlers.QueueListener": + from picologging.handlers import QueueListener # pyright: ignore[reportMissingImports] + + expected_listener_class = QueueListener + else: + expected_listener_class = importlib.import_module(expected_listener_class_str) + def wait_log_queue(queue: Any, sleep_time: float = 0.1, max_retries: int = 5) -> None: retry = 0 while queue.qsize() > 0 and retry < max_retries: @@ -168,7 +196,7 @@ def assert_log(queue: Any, expected: str, count: Optional[int] = None) -> None: logger = get_logger("test_logger") assert type(logger) is logging_module.Logger - handler = logger.handlers[0] # pyright: ignore[reportGeneralTypeIssues] + handler = logger.handlers[0] # pyright: ignore[reportGeneralTypeIssues,reportAttributeAccessIssue] assert type(handler) is expected_handler_class assert type(handler.queue) is Queue @@ -193,14 +221,34 @@ def test_get_logger_without_logging_config() -> None: @pytest.mark.parametrize( - "logging_module, expected_handler_class", + "logging_module_str, expected_handler_class_str", [ - [logging, QueueHandler if sys.version_info >= (3, 12, 0) else StandardQueueListenerHandler], - [picologging, PicologgingQueueListenerHandler], + [ + "logging", + "logging.handlers.QueueHandler" + if sys.version_info >= (3, 12, 0) + else "litestar.logging.standard.QueueListenerHandler", + ], + ["picologging", "litestar.logging.picologging.QueueListenerHandler"], ], ) -def test_default_loggers(logging_module: ModuleType, expected_handler_class: Any) -> None: - with create_test_client(logging_config=LoggingConfig(logging_module=logging_module.__name__)) as client: +def test_default_loggers(logging_module_str: str, expected_handler_class_str: str) -> None: + logging_module = importlib.import_module(logging_module_str) + if expected_handler_class_str == "litestar.logging.standard.QueueListenerHandler": + from litestar.logging.standard import QueueListenerHandler + + expected_handler_class = QueueListenerHandler + elif expected_handler_class_str == "litestar.logging.picologging.QueueListenerHandler": + from litestar.logging.picologging import QueueListenerHandler + + expected_handler_class = QueueListenerHandler + elif expected_handler_class_str == "logging.handlers.QueueHandler": + from logging.handlers import QueueHandler as QueueListenerHandler + + expected_handler_class = QueueListenerHandler + else: + expected_handler_class = importlib.import_module(expected_handler_class_str) + with create_test_client(logging_config=LoggingConfig(logging_module=logging_module_str)) as client: root_logger = client.app.get_logger() assert isinstance(root_logger, logging_module.Logger) assert root_logger.name == "root" @@ -216,28 +264,50 @@ def test_default_loggers(logging_module: ModuleType, expected_handler_class: Any @pytest.mark.parametrize( - "logging_module, expected_handler_class", + "logging_module_str, expected_handler_class_str", [ - ["logging", QueueHandler if sys.version_info >= (3, 12, 0) else StandardQueueListenerHandler], - ["picologging", PicologgingQueueListenerHandler], + [ + "logging", + "logging.handlers.QueueHandler" + if sys.version_info >= (3, 12, 0) + else "litestar.logging.standard.QueueListenerHandler", + ], + ["picologging", "litestar.logging.picologging.QueueListenerHandler"], ], ) -def test_connection_logger(logging_module: str, expected_handler_class: Any) -> None: +def test_connection_logger(logging_module_str: str, expected_handler_class_str: str) -> None: + logging_module = importlib.import_module(logging_module_str) + if expected_handler_class_str == "litestar.logging.standard.QueueListenerHandler": + from litestar.logging.standard import QueueListenerHandler + + expected_handler_class = QueueListenerHandler + elif expected_handler_class_str == "litestar.logging.picologging.QueueListenerHandler": + from litestar.logging.picologging import QueueListenerHandler + + expected_handler_class = QueueListenerHandler + elif expected_handler_class_str == "logging.handlers.QueueHandler": + from logging.handlers import QueueHandler as QueueListenerHandler + + expected_handler_class = QueueListenerHandler + else: + expected_handler_class = importlib.import_module(expected_handler_class_str) + @get("/") def handler(request: Request) -> Dict[str, bool]: return {"isinstance": isinstance(request.logger.handlers[0], expected_handler_class)} # type: ignore[attr-defined] with create_test_client( route_handlers=[handler], - logging_config=LoggingConfig(logging_module=logging_module), + logging_config=LoggingConfig(logging_module=logging_module.__name__), ) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.json()["isinstance"] -@pytest.mark.parametrize("logging_module", [logging, picologging, None]) -def test_validation(logging_module: Optional[ModuleType]) -> None: +@pytest.mark.parametrize("logging_module_str", ["logging", "picologging", None]) +def test_validation(logging_module_str: Optional[str]) -> None: + logging_module = importlib.import_module(logging_module_str) if logging_module_str else None if logging_module is None: logging_config = LoggingConfig( formatters={}, @@ -267,32 +337,54 @@ def test_validation(logging_module: Optional[ModuleType]) -> None: @pytest.mark.parametrize( - "logging_module, expected_handler_class", + "logging_module_str, expected_handler_class_str", [ - [logging, QueueHandler if sys.version_info >= (3, 12, 0) else StandardQueueListenerHandler], - [picologging, PicologgingQueueListenerHandler], + [ + "logging", + "logging.handlers.QueueHandler" + if sys.version_info >= (3, 12, 0) + else "litestar.logging.standard.QueueListenerHandler", + ], + ["picologging", "litestar.logging.picologging.QueueListenerHandler"], ], ) -def test_root_logger(logging_module: ModuleType, expected_handler_class: Any) -> None: +def test_root_logger(logging_module_str: str, expected_handler_class_str: str) -> None: + logging_module = importlib.import_module(logging_module_str) + if expected_handler_class_str == "litestar.logging.standard.QueueListenerHandler": + from litestar.logging.standard import QueueListenerHandler + + expected_handler_class = QueueListenerHandler + elif expected_handler_class_str == "litestar.logging.picologging.QueueListenerHandler": + from litestar.logging.picologging import QueueListenerHandler + + expected_handler_class = QueueListenerHandler + elif expected_handler_class_str == "logging.handlers.QueueHandler": + from logging.handlers import QueueHandler as QueueListenerHandler + + expected_handler_class = QueueListenerHandler + else: + expected_handler_class = importlib.import_module(expected_handler_class_str) + logging_config = LoggingConfig(logging_module=logging_module.__name__) get_logger = logging_config.configure() root_logger = get_logger() assert root_logger.name == "root" # type: ignore[attr-defined] assert isinstance(root_logger, logging_module.Logger) - root_logger_handler = root_logger.handlers[0] # pyright: ignore[reportGeneralTypeIssues] + root_logger_handler = root_logger.handlers[0] # pyright: ignore[reportGeneralTypeIssues,reportAttributeAccessIssue] assert root_logger_handler.name == "queue_listener" - assert isinstance(root_logger_handler, expected_handler_class) + assert isinstance(root_logger_handler, cast("Any", expected_handler_class)) -@pytest.mark.parametrize("logging_module", [logging, picologging]) -def test_root_logger_no_config(logging_module: ModuleType) -> None: - logging_config = LoggingConfig(logging_module=logging_module.__name__, configure_root_logger=False) +@pytest.mark.parametrize("logging_module_str", ["logging", "picologging"]) +def test_root_logger_no_config(logging_module_str: str) -> None: + logging_module = importlib.import_module(logging_module_str) + logging_config = LoggingConfig(logging_module=logging_module_str, configure_root_logger=False) get_logger = logging_config.configure() root_logger = get_logger() assert isinstance(root_logger, logging_module.Logger) - handlers = root_logger.handlers # pyright: ignore[reportGeneralTypeIssues] + handlers = root_logger.handlers # pyright: ignore[reportGeneralTypeIssues,reportAttributeAccessIssue] if logging_module == logging: # pytest automatically configures some handlers for handler in handlers: @@ -302,20 +394,44 @@ def test_root_logger_no_config(logging_module: ModuleType) -> None: @pytest.mark.parametrize( - "logging_module, configure_root_logger, expected_root_logger_handler_class", + "logging_module_str, configure_root_logger, expected_root_logger_handler_class_str", [ - [logging, True, QueueHandler if sys.version_info >= (3, 12, 0) else StandardQueueListenerHandler], - [logging, False, None], - [picologging, True, PicologgingQueueListenerHandler], - [picologging, False, None], + [ + "logging", + True, + "logging.handlers.QueueHandler" + if sys.version_info >= (3, 12, 0) + else "litestar.logging.standard.QueueListenerHandler", + ], + ["logging", False, None], + ["picologging", True, "litestar.logging.picologging.QueueListenerHandler"], + ["picologging", False, None], ], ) def test_customizing_handler( - logging_module: ModuleType, + logging_module_str: str, configure_root_logger: bool, - expected_root_logger_handler_class: Any, + expected_root_logger_handler_class_str: "Optional[str]", capsys: "CaptureFixture[str]", ) -> None: + logging_module = importlib.import_module(logging_module_str) + if expected_root_logger_handler_class_str is None: + expected_root_logger_handler_class = None + elif expected_root_logger_handler_class_str == "litestar.logging.standard.QueueListenerHandler": + from litestar.logging.standard import QueueListenerHandler + + expected_root_logger_handler_class = QueueListenerHandler + elif expected_root_logger_handler_class_str == "litestar.logging.picologging.QueueListenerHandler": + from litestar.logging.picologging import QueueListenerHandler + + expected_root_logger_handler_class = QueueListenerHandler + elif expected_root_logger_handler_class_str == "logging.handlers.QueueHandler": + from logging.handlers import QueueHandler as QueueListenerHandler + + expected_root_logger_handler_class = QueueListenerHandler + else: + expected_root_logger_handler_class = importlib.import_module(expected_root_logger_handler_class_str) + log_format = "%(levelname)s :: %(name)s :: %(message)s" logging_config = LoggingConfig( @@ -348,7 +464,7 @@ def test_customizing_handler( # picologging seems to be broken, cannot make it log on stdout? # https://github.com/microsoft/picologging/issues/205 - if logging_module == picologging: + if logging_module_str == "picologging": del logging_config.handlers["console_stdout"]["stream"] get_logger = logging_config.configure() @@ -356,9 +472,9 @@ def test_customizing_handler( if configure_root_logger is True: assert isinstance(root_logger, logging_module.Logger) - assert root_logger.level == logging_module.INFO # pyright: ignore[reportGeneralTypeIssues] + assert root_logger.level == logging_module.INFO # pyright: ignore[reportGeneralTypeIssues,reportAttributeAccessIssue] - root_logger_handler = root_logger.handlers[0] # pyright: ignore[reportGeneralTypeIssues] + root_logger_handler = root_logger.handlers[0] # pyright: ignore[reportGeneralTypeIssues,reportAttributeAccessIssue] assert root_logger_handler.name == "queue_listener" assert type(root_logger_handler) is expected_root_logger_handler_class @@ -366,7 +482,8 @@ def test_customizing_handler( formatter = root_logger_handler.listener.handlers[0].formatter # type: ignore[attr-defined] else: formatter = root_logger_handler.formatter - assert formatter._fmt == log_format + if formatter is not None: + assert formatter._fmt == log_format else: # Root logger shouldn't be configured but pytest adds some handlers (for the standard `logging` module) for handler in root_logger.handlers: # type: ignore[attr-defined] @@ -381,7 +498,7 @@ def assert_logger(logger: Any) -> None: assert logger.handlers[0].formatter._fmt == log_format logger.info("Hello from '%s'", logging_module.__name__) - if logging_module == picologging: + if logging_module_str == "picologging": log_output = capsys.readouterr().err.strip() else: log_output = capsys.readouterr().out.strip() diff --git a/tests/unit/test_middleware/test_base_middleware.py b/tests/unit/test_middleware/test_base_middleware.py index 7316320fc1..b3c403f6e3 100644 --- a/tests/unit/test_middleware/test_base_middleware.py +++ b/tests/unit/test_middleware/test_base_middleware.py @@ -1,8 +1,11 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List, Union +from warnings import catch_warnings + +import pytest from litestar import MediaType, asgi, get from litestar.datastructures.headers import MutableScopeHeaders -from litestar.exceptions import ValidationException +from litestar.exceptions import LitestarWarning, ValidationException from litestar.middleware import AbstractMiddleware, DefineMiddleware from litestar.response.base import ASGIResponse from litestar.status_codes import HTTP_400_BAD_REQUEST @@ -121,6 +124,30 @@ def third_handler() -> dict: assert "test" in response.headers +@pytest.mark.parametrize("excludes", ["/", ["/", "/foo"], "/*", "/.*"]) +def test_exclude_by_pattern_warns_if_exclude_all(excludes: Union[str, List[str]]) -> None: + class SubclassMiddleware(AbstractMiddleware): + exclude = excludes + + async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: + await self.app(scope, receive, send) + + with pytest.warns(LitestarWarning, match="Middleware 'SubclassMiddleware' exclude pattern"): + create_test_client(middleware=[SubclassMiddleware]) + + +def test_exclude_doesnt_warn_on_non_greedy_pattern() -> None: + class SubclassMiddleware(AbstractMiddleware): + exclude = "^/$" + + async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: + await self.app(scope, receive, send) + + with catch_warnings(record=True) as warnings: + create_test_client(middleware=[SubclassMiddleware]) + assert len(warnings) == 0 + + def test_exclude_by_opt_key() -> None: class SubclassMiddleware(AbstractMiddleware): exclude_opt_key = "exclude_route" diff --git a/tests/unit/test_middleware/test_csrf_middleware.py b/tests/unit/test_middleware/test_csrf_middleware.py index e7b7500081..e45f724747 100644 --- a/tests/unit/test_middleware/test_csrf_middleware.py +++ b/tests/unit/test_middleware/test_csrf_middleware.py @@ -242,6 +242,31 @@ def post_handler2(data: dict = Body(media_type=RequestEncodingType.URL_ENCODED)) assert response.json() == data +def test_csrf_middleware_exclude_from_set_cookies() -> None: + # https://github.com/litestar-org/litestar/issues/3688 + # middleware should be bypassed completely when excluded, so no cookies should be set + + @get("/protected-handler") + def get_handler() -> dict: + return {} + + @get("/unprotected-handler") + def get_handler2() -> dict: + return {} + + with create_test_client( + route_handlers=[get_handler, get_handler2], + csrf_config=CSRFConfig(secret=str(urandom(10)), exclude=["unprotected-handler"]), + ) as client: + response = client.get("/unprotected-handler") + assert response.status_code == HTTP_200_OK + assert "set-cookie" not in response.headers + + response = client.get("/protected-handler") + assert response.status_code == HTTP_200_OK + assert "set-cookie" in response.headers + + def test_csrf_middleware_configure_name_for_exclude_from_check_via_opts() -> None: @post("/handler", exclude_from_csrf=True) def post_handler(data: dict = Body(media_type=RequestEncodingType.URL_ENCODED)) -> dict: diff --git a/tests/unit/test_middleware/test_rate_limit_middleware.py b/tests/unit/test_middleware/test_rate_limit_middleware.py index e2a30af9a4..c3f452efee 100644 --- a/tests/unit/test_middleware/test_rate_limit_middleware.py +++ b/tests/unit/test_middleware/test_rate_limit_middleware.py @@ -28,33 +28,46 @@ async def test_rate_limiting(unit: DurationUnit) -> None: def handler() -> None: return None - config = RateLimitConfig(rate_limit=(unit, 1)) + config = RateLimitConfig(rate_limit=(unit, 2)) cache_key = "RateLimitMiddleware::testclient" app = Litestar(route_handlers=[handler], middleware=[config.middleware]) store = app.stores.get("rate_limit") with travel(datetime.utcnow, tick=False) as frozen_time, TestClient(app=app) as client: response = client.get("/") - assert response.status_code == HTTP_200_OK + cached_value = await store.get(cache_key) assert cached_value cache_object = CacheObject(**decode_json(value=cached_value)) assert len(cache_object.history) == 1 - assert response.headers.get(config.rate_limit_policy_header_key) == f"1; w={DURATION_VALUES[unit]}" - assert response.headers.get(config.rate_limit_limit_header_key) == "1" - assert response.headers.get(config.rate_limit_remaining_header_key) == "0" - assert response.headers.get(config.rate_limit_reset_header_key) == str(int(time()) - cache_object.reset) - + assert response.status_code == HTTP_200_OK + assert response.headers.get(config.rate_limit_policy_header_key) == f"2; w={DURATION_VALUES[unit]}" + assert response.headers.get(config.rate_limit_limit_header_key) == "2" + assert response.headers.get(config.rate_limit_remaining_header_key) == "1" + # Since the time is frozen, no time has passed. + # Therefore, the remaining seconds for the current quota window should be the same as the entire window length. + assert response.headers.get(config.rate_limit_reset_header_key) == str(DURATION_VALUES[unit]) + + # Move time one second before the end of the quota window for the next request frozen_time.shift(DURATION_VALUES[unit] - 1) + response = client.get("/") + + assert response.status_code == HTTP_200_OK + assert response.headers.get(config.rate_limit_policy_header_key) == f"2; w={DURATION_VALUES[unit]}" + assert response.headers.get(config.rate_limit_limit_header_key) == "2" + assert response.headers.get(config.rate_limit_remaining_header_key) == "0" + assert response.headers.get(config.rate_limit_reset_header_key) == "1" response = client.get("/") + assert response.status_code == HTTP_429_TOO_MANY_REQUESTS - assert response.headers.get(config.rate_limit_policy_header_key) == f"1; w={DURATION_VALUES[unit]}" - assert response.headers.get(config.rate_limit_limit_header_key) == "1" + assert response.headers.get(config.rate_limit_policy_header_key) == f"2; w={DURATION_VALUES[unit]}" + assert response.headers.get(config.rate_limit_limit_header_key) == "2" assert response.headers.get(config.rate_limit_remaining_header_key) == "0" - assert response.headers.get(config.rate_limit_reset_header_key) == str(int(time()) - cache_object.reset) + assert response.headers.get(config.rate_limit_reset_header_key) == "1" + # Move time one second so that a new quota window starts frozen_time.shift(1) response = client.get("/") @@ -225,3 +238,22 @@ def handler() -> None: response = client.get("/src/static/test.css") assert response.status_code == HTTP_200_OK assert response.text == "styles content" + + +async def test_rate_limiting_works_with_cache() -> None: + @get("/", cache=True) + def handler() -> None: + return None + + config = RateLimitConfig(rate_limit=("minute", 2)) + app = Litestar(route_handlers=[handler], middleware=[config.middleware]) + + with TestClient(app=app) as client: + response = client.get("/") + assert response.headers.get(config.rate_limit_remaining_header_key) == "1" + + response = client.get("/") + assert response.headers.get(config.rate_limit_remaining_header_key) == "0" + + response = client.get("/") + assert response.status_code == HTTP_429_TOO_MANY_REQUESTS diff --git a/tests/unit/test_openapi/conftest.py b/tests/unit/test_openapi/conftest.py index 20dfeb6c7a..6ec46f8d86 100644 --- a/tests/unit/test_openapi/conftest.py +++ b/tests/unit/test_openapi/conftest.py @@ -10,7 +10,7 @@ from litestar.openapi.spec.example import Example from litestar.params import Parameter from tests.models import DataclassPerson, DataclassPersonFactory, DataclassPet -from tests.unit.test_openapi.utils import Gender, PetException +from tests.unit.test_openapi.utils import Gender, LuckyNumber, PetException class PartialDataclassPersonDTO(DataclassDTO[DataclassPerson]): @@ -45,8 +45,9 @@ def get_persons( from_date: Optional[Union[int, datetime, date]] = None, to_date: Optional[Union[int, datetime, date]] = None, gender: Optional[Union[Gender, List[Gender]]] = Parameter( - examples=[Example(value="M"), Example(value=["M", "O"])] + examples=[Example(value=Gender.MALE), Example(value=[Gender.MALE, Gender.OTHER])] ), + lucky_number: Optional[LuckyNumber] = Parameter(examples=[Example(value=LuckyNumber.SEVEN)]), # header parameter secret_header: str = Parameter(header="secret"), # cookie parameter diff --git a/tests/unit/test_openapi/test_config.py b/tests/unit/test_openapi/test_config.py index c16eeb9a41..705c2e00d8 100644 --- a/tests/unit/test_openapi/test_config.py +++ b/tests/unit/test_openapi/test_config.py @@ -30,14 +30,10 @@ def test_merged_components_correct() -> None: "one": { "required": False, "deprecated": False, - "allowEmptyValue": False, - "allowReserved": False, }, "two": { "required": False, "deprecated": False, - "allowEmptyValue": False, - "allowReserved": False, }, }, } diff --git a/tests/unit/test_openapi/test_datastructures.py b/tests/unit/test_openapi/test_datastructures.py index 6e5e6a92c5..0d1dd8df7a 100644 --- a/tests/unit/test_openapi/test_datastructures.py +++ b/tests/unit/test_openapi/test_datastructures.py @@ -1,9 +1,16 @@ from __future__ import annotations +from typing import Dict, Generic, List, TypeVar + +import msgspec import pytest -from litestar._openapi.datastructures import SchemaRegistry +from litestar._openapi.datastructures import SchemaRegistry, _get_normalized_schema_key +from litestar.exceptions import ImproperlyConfiguredException from litestar.openapi.spec import Reference, Schema +from litestar.params import KwargDefinition +from litestar.typing import FieldDefinition +from tests.models import DataclassPerson @pytest.fixture() @@ -11,28 +18,125 @@ def schema_registry() -> SchemaRegistry: return SchemaRegistry() -def test_get_schema_for_key(schema_registry: SchemaRegistry) -> None: +def test_get_schema_for_field_definition(schema_registry: SchemaRegistry) -> None: assert not schema_registry._schema_key_map assert not schema_registry._schema_reference_map assert not schema_registry._model_name_groups - key = ("a", "b", "c") - schema = schema_registry.get_schema_for_key(key) + field = FieldDefinition.from_annotation(str) + schema = schema_registry.get_schema_for_field_definition(field) + key = _get_normalized_schema_key(field) assert isinstance(schema, Schema) assert key in schema_registry._schema_key_map assert not schema_registry._schema_reference_map - assert len(schema_registry._model_name_groups["c"]) == 1 - assert schema_registry._model_name_groups["c"][0].schema is schema - assert schema_registry.get_schema_for_key(key) is schema + assert len(schema_registry._model_name_groups[key[-1]]) == 1 + assert schema_registry._model_name_groups[key[-1]][0].schema is schema + assert schema_registry.get_schema_for_field_definition(field) is schema -def test_get_reference_for_key(schema_registry: SchemaRegistry) -> None: +def test_get_reference_for_field_definition(schema_registry: SchemaRegistry) -> None: assert not schema_registry._schema_key_map assert not schema_registry._schema_reference_map assert not schema_registry._model_name_groups - key = ("a", "b", "c") - assert schema_registry.get_reference_for_key(key) is None - schema_registry.get_schema_for_key(key) - reference = schema_registry.get_reference_for_key(key) + field = FieldDefinition.from_annotation(str) + key = _get_normalized_schema_key(field) + + assert schema_registry.get_reference_for_field_definition(field) is None + schema_registry.get_schema_for_field_definition(field) + reference = schema_registry.get_reference_for_field_definition(field) assert isinstance(reference, Reference) assert id(reference) in schema_registry._schema_reference_map assert reference in schema_registry._schema_key_map[key].references + + +def test_get_normalized_schema_key() -> None: + class LocalClass(msgspec.Struct): + id: str + + T = TypeVar("T") + + # replace each of the long strings with underscores with a tuple of strings split at each underscore + assert _get_normalized_schema_key(FieldDefinition.from_annotation(LocalClass)) == ( + "tests", + "unit", + "test_openapi", + "test_datastructures", + "test_get_normalized_schema_key.LocalClass", + ) + + assert _get_normalized_schema_key(FieldDefinition.from_annotation(DataclassPerson)) == ( + "tests", + "models", + "DataclassPerson", + ) + + builtin_dict = Dict[str, List[int]] + assert _get_normalized_schema_key(FieldDefinition.from_annotation(builtin_dict)) == ( + "typing", + "Dict_str_typing.List_int_", + ) + + builtin_with_custom = Dict[str, DataclassPerson] + assert _get_normalized_schema_key(FieldDefinition.from_annotation(builtin_with_custom)) == ( + "typing", + "Dict_str_tests.models.DataclassPerson_", + ) + + class LocalGeneric(Generic[T]): + pass + + assert _get_normalized_schema_key(FieldDefinition.from_annotation(LocalGeneric)) == ( + "tests", + "unit", + "test_openapi", + "test_datastructures", + "test_get_normalized_schema_key.LocalGeneric", + ) + + generic_int = LocalGeneric[int] + generic_str = LocalGeneric[str] + + assert _get_normalized_schema_key(FieldDefinition.from_annotation(generic_int)) == ( + "tests", + "unit", + "test_openapi", + "test_datastructures", + "test_get_normalized_schema_key.LocalGeneric_int_", + ) + + assert _get_normalized_schema_key(FieldDefinition.from_annotation(generic_str)) == ( + "tests", + "unit", + "test_openapi", + "test_datastructures", + "test_get_normalized_schema_key.LocalGeneric_str_", + ) + + assert _get_normalized_schema_key(FieldDefinition.from_annotation(generic_int)) != _get_normalized_schema_key( + FieldDefinition.from_annotation(generic_str) + ) + + +def test_raise_on_override_for_same_field_definition() -> None: + registry = SchemaRegistry() + schema = registry.get_schema_for_field_definition( + FieldDefinition.from_annotation(str, kwarg_definition=KwargDefinition(schema_component_key="foo")) + ) + # registering the same thing again with the same name should work + assert ( + registry.get_schema_for_field_definition( + FieldDefinition.from_annotation(str, kwarg_definition=KwargDefinition(schema_component_key="foo")) + ) + is schema + ) + # registering the same *type* with a different name should result in a different schema + assert ( + registry.get_schema_for_field_definition( + FieldDefinition.from_annotation(str, kwarg_definition=KwargDefinition(schema_component_key="bar")) + ) + is not schema + ) + # registering a different type with a previously used name should raise an exception + with pytest.raises(ImproperlyConfiguredException): + registry.get_schema_for_field_definition( + FieldDefinition.from_annotation(int, kwarg_definition=KwargDefinition(schema_component_key="foo")) + ) diff --git a/tests/unit/test_openapi/test_endpoints.py b/tests/unit/test_openapi/test_endpoints.py index 7ad694fb70..6e0230a7e3 100644 --- a/tests/unit/test_openapi/test_endpoints.py +++ b/tests/unit/test_openapi/test_endpoints.py @@ -39,7 +39,7 @@ def test_default_redoc_cdn_urls( def test_default_swagger_ui_cdn_urls( person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig ) -> None: - default_swagger_ui_version = "5.1.3" + default_swagger_ui_version = "5.18.2" default_swagger_bundles = [ f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{default_swagger_ui_version}/swagger-ui.css", f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{default_swagger_ui_version}/swagger-ui-bundle.js", diff --git a/tests/unit/test_openapi/test_integration.py b/tests/unit/test_openapi/test_integration.py index 31af60e5fa..92f6cc118a 100644 --- a/tests/unit/test_openapi/test_integration.py +++ b/tests/unit/test_openapi/test_integration.py @@ -331,7 +331,7 @@ def handler_foo_int() -> Foo[int]: "200": { "description": "Request fulfilled, document follows", "headers": {}, - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Foo[str]"}}}, + "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Foo_str_"}}}, } }, "deprecated": False, @@ -345,7 +345,7 @@ def handler_foo_int() -> Foo[int]: "200": { "description": "Request fulfilled, document follows", "headers": {}, - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Foo[int]"}}}, + "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Foo_int_"}}}, } }, "deprecated": False, @@ -354,13 +354,13 @@ def handler_foo_int() -> Foo[int]: }, "components": { "schemas": { - "Foo[str]": { + "Foo_str_": { "properties": {"foo": {"type": "string"}}, "type": "object", "required": ["foo"], "title": "Foo[str]", }, - "Foo[int]": { + "Foo_int_": { "properties": {"foo": {"type": "integer"}}, "type": "object", "required": ["foo"], diff --git a/tests/unit/test_openapi/test_parameters.py b/tests/unit/test_openapi/test_parameters.py index 08505d0672..663f2b2efe 100644 --- a/tests/unit/test_openapi/test_parameters.py +++ b/tests/unit/test_openapi/test_parameters.py @@ -15,12 +15,13 @@ from litestar.exceptions import ImproperlyConfiguredException from litestar.handlers import HTTPRouteHandler from litestar.openapi import OpenAPIConfig -from litestar.openapi.spec import Example, OpenAPI, Schema +from litestar.openapi.spec import Example, OpenAPI, Reference, Schema from litestar.openapi.spec.enums import OpenAPIType from litestar.params import Dependency, Parameter from litestar.routes import BaseRoute from litestar.testing import create_test_client from litestar.utils import find_index +from tests.unit.test_openapi.utils import Gender, LuckyNumber if TYPE_CHECKING: from litestar.openapi.spec.parameter import Parameter as OpenAPIParameter @@ -49,8 +50,10 @@ def test_create_parameters(person_controller: Type[Controller]) -> None: ExampleFactory.seed_random(10) parameters = _create_parameters(app=Litestar(route_handlers=[person_controller]), path="/{service_id}/person") - assert len(parameters) == 9 - page, name, service_id, page_size, from_date, to_date, gender, secret_header, cookie_value = tuple(parameters) + assert len(parameters) == 10 + page, name, service_id, page_size, from_date, to_date, gender, lucky_number, secret_header, cookie_value = tuple( + parameters + ) assert service_id.name == "service_id" assert service_id.param_in == ParamType.PATH @@ -104,23 +107,15 @@ def test_create_parameters(person_controller: Type[Controller]) -> None: assert is_schema_value(gender.schema) assert gender.schema == Schema( one_of=[ - Schema(type=OpenAPIType.NULL), - Schema( - type=OpenAPIType.STRING, - enum=["M", "F", "O", "A"], - examples=["M"], - ), + Reference(ref="#/components/schemas/tests_unit_test_openapi_utils_Gender"), Schema( type=OpenAPIType.ARRAY, - items=Schema( - type=OpenAPIType.STRING, - enum=["M", "F", "O", "A"], - examples=["F"], - ), - examples=[["A"]], + items=Reference(ref="#/components/schemas/tests_unit_test_openapi_utils_Gender"), + examples=[[Gender.MALE]], ), + Schema(type=OpenAPIType.NULL), ], - examples=["M", ["M", "O"]], + examples=[Gender.MALE, [Gender.MALE, Gender.OTHER]], ) assert not gender.required @@ -136,6 +131,18 @@ def test_create_parameters(person_controller: Type[Controller]) -> None: assert cookie_value.required assert cookie_value.schema.examples + assert lucky_number.param_in == ParamType.QUERY + assert lucky_number.name == "lucky_number" + assert is_schema_value(lucky_number.schema) + assert lucky_number.schema == Schema( + one_of=[ + Reference(ref="#/components/schemas/tests_unit_test_openapi_utils_LuckyNumber"), + Schema(type=OpenAPIType.NULL), + ], + examples=[LuckyNumber.SEVEN], + ) + assert not lucky_number.required + def test_deduplication_for_param_where_key_and_type_are_equal() -> None: class BaseDep: @@ -397,8 +404,8 @@ async def handler( app = Litestar([handler]) assert app.openapi_schema.paths["/{path_param}"].get.parameters[0].schema.type == OpenAPIType.STRING # type: ignore[index, union-attr] assert app.openapi_schema.paths["/{path_param}"].get.parameters[1].schema.one_of == [ # type: ignore[index, union-attr] - Schema(type=OpenAPIType.NULL), Schema(type=OpenAPIType.STRING), + Schema(type=OpenAPIType.NULL), ] assert app.openapi_schema.paths["/{path_param}"].get.parameters[2].schema.type == OpenAPIType.STRING # type: ignore[index, union-attr] assert ( @@ -438,3 +445,33 @@ async def handler( testmodel_schema_name = app.openapi_schema.paths["/"].get.parameters[0].schema.value # type: ignore[index, union-attr] assert app.openapi_schema.components.schemas[testmodel_schema_name].properties["param"].type == OpenAPIType.STRING # type: ignore[index, union-attr] + + +def test_query_param_only_properties() -> None: + # https://github.com/litestar-org/litestar/issues/3908 + @get("/{path_param:str}") + def handler( + path_param: str, + query_param: str, + header_param: Annotated[str, Parameter(header="header_param")], + cookie_param: Annotated[str, Parameter(cookie="cookie_param")], + ) -> None: + pass + + app = Litestar([handler]) + params = {p.name: p for p in app.openapi_schema.paths["/{path_param}"].get.parameters} # type: ignore[union-attr, index] + + for key in ["path_param", "header_param", "cookie_param"]: + schema = params[key].to_schema() + assert "allowEmptyValue" not in schema + assert "allowReserved" not in schema + + assert params["query_param"].to_schema() == { + "name": "query_param", + "in": "query", + "schema": {"type": "string"}, + "required": True, + "deprecated": False, + "allowEmptyValue": False, + "allowReserved": False, + } diff --git a/tests/unit/test_openapi/test_plugins.py b/tests/unit/test_openapi/test_plugins.py new file mode 100644 index 0000000000..2f601157d3 --- /dev/null +++ b/tests/unit/test_openapi/test_plugins.py @@ -0,0 +1,62 @@ +from litestar import Litestar +from litestar.config.csrf import CSRFConfig +from litestar.openapi.config import OpenAPIConfig +from litestar.openapi.plugins import RapidocRenderPlugin, SwaggerRenderPlugin +from litestar.testing import TestClient + +rapidoc_fragment = ".addEventListener('before-try'," +swagger_fragment = "requestInterceptor:" + + +def test_rapidoc_csrf() -> None: + app = Litestar( + csrf_config=CSRFConfig(secret="litestar"), + openapi_config=OpenAPIConfig( + title="Litestar Example", + version="0.0.1", + render_plugins=[RapidocRenderPlugin()], + ), + ) + + with TestClient(app=app) as client: + resp = client.get("/schema/rapidoc") + assert resp.status_code == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + assert rapidoc_fragment in resp.text + + +def test_swagger_ui_csrf() -> None: + app = Litestar( + csrf_config=CSRFConfig(secret="litestar"), + openapi_config=OpenAPIConfig( + title="Litestar Example", + version="0.0.1", + render_plugins=[SwaggerRenderPlugin()], + ), + ) + + with TestClient(app=app) as client: + resp = client.get("/schema/swagger") + assert resp.status_code == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + assert swagger_fragment in resp.text + + +def test_plugins_csrf_httponly() -> None: + app = Litestar( + csrf_config=CSRFConfig(secret="litestar", cookie_httponly=True), + openapi_config=OpenAPIConfig( + title="Litestar Example", + version="0.0.1", + render_plugins=[RapidocRenderPlugin(), SwaggerRenderPlugin()], + ), + ) + + with TestClient(app=app) as client: + resp = client.get("/schema/rapidoc") + assert resp.status_code == 200 + assert rapidoc_fragment not in resp.text + + resp = client.get("/schema/swagger") + assert resp.status_code == 200 + assert swagger_fragment not in resp.text diff --git a/tests/unit/test_openapi/test_responses.py b/tests/unit/test_openapi/test_responses.py index 148fe28a71..1681740e3a 100644 --- a/tests/unit/test_openapi/test_responses.py +++ b/tests/unit/test_openapi/test_responses.py @@ -5,7 +5,7 @@ from http import HTTPStatus from pathlib import Path from types import ModuleType -from typing import Any, Callable, Dict, TypedDict +from typing import Any, Callable, Dict, TypedDict, TypeVar from unittest.mock import MagicMock import pytest @@ -31,7 +31,6 @@ from litestar.openapi.spec import Example, OpenAPIHeader, OpenAPIMediaType, Reference, Schema from litestar.openapi.spec.enums import OpenAPIType from litestar.response import File, Redirect, Stream, Template -from litestar.response.base import T from litestar.routes import HTTPRoute from litestar.status_codes import ( HTTP_200_OK, @@ -44,6 +43,9 @@ from tests.models import DataclassPerson, DataclassPersonFactory from tests.unit.test_openapi.utils import PetException +T = TypeVar("T") + + CreateFactoryFixture: TypeAlias = "Callable[..., ResponseFactory]" @@ -186,10 +188,12 @@ def handler() -> list: assert isinstance(response.headers, dict) assert isinstance(response.headers["special-header"], OpenAPIHeader) - assert response.headers["special-header"].description == "super-duper special" - headers_schema = response.headers["special-header"].schema - assert isinstance(headers_schema, Schema) - assert headers_schema.type == OpenAPIType.STRING + assert response.headers["special-header"].to_schema() == { + "schema": {"type": "string"}, + "description": "super-duper special", + "required": False, + "deprecated": False, + } def test_create_success_response_with_cookies(create_factory: CreateFactoryFixture) -> None: @@ -361,6 +365,7 @@ class UnknownError(TypedDict): 401: ResponseSpec(data_container=AuthenticationError, description="Authentication error"), 500: ResponseSpec(data_container=ServerError, generate_examples=False, media_type=MediaType.TEXT), 505: ResponseSpec(data_container=UnknownError), + 900: ResponseSpec(data_container=UnknownError, media_type="application/vnd.custom"), } ) def handler() -> DataclassPerson: @@ -398,6 +403,13 @@ def handler() -> DataclassPerson: assert third_response[0] == "505" assert third_response[1].description == "Additional response" + fourth_response = next(responses) + assert fourth_response[0] == "900" + assert fourth_response[1].description == "Additional response" + custom_media_type_content = fourth_response[1].content.get("application/vnd.custom") # type: ignore[union-attr] + assert custom_media_type_content + assert isinstance(custom_media_type_content, OpenAPIMediaType) + with pytest.raises(StopIteration): next(responses) @@ -528,3 +540,25 @@ def handler() -> File: response = create_factory(handler).create_success_response() assert next(iter(response.content.values())).schema.content_media_type == expected # type: ignore[union-attr] + + +def test_response_header_deprecated_properties() -> None: + assert ResponseHeader(name="foo", value="bar").allow_empty_value is False + assert ResponseHeader(name="foo", value="bar").allow_reserved is False + + with pytest.warns(DeprecationWarning, match="property is invalid for headers"): + ResponseHeader(name="foo", value="bar", allow_empty_value=True) + + with pytest.warns(DeprecationWarning, match="property is invalid for headers"): + ResponseHeader(name="foo", value="bar", allow_reserved=True) + + +def test_header_deprecated_properties() -> None: + assert OpenAPIHeader().allow_empty_value is False + assert OpenAPIHeader().allow_reserved is False + + with pytest.warns(DeprecationWarning, match="property is invalid for headers"): + OpenAPIHeader(allow_empty_value=True) + + with pytest.warns(DeprecationWarning, match="property is invalid for headers"): + OpenAPIHeader(allow_reserved=True) diff --git a/tests/unit/test_openapi/test_schema.py b/tests/unit/test_openapi/test_schema.py index 251dd31d7a..4606687027 100644 --- a/tests/unit/test_openapi/test_schema.py +++ b/tests/unit/test_openapi/test_schema.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from datetime import date, datetime, timezone from enum import Enum, auto -from typing import ( # type: ignore[attr-defined] +from typing import ( TYPE_CHECKING, Any, Dict, @@ -13,15 +13,14 @@ Tuple, TypedDict, TypeVar, - Union, - _GenericAlias, # pyright: ignore + Union, # pyright: ignore ) import annotated_types import msgspec import pytest from msgspec import Struct -from typing_extensions import Annotated, TypeAlias +from typing_extensions import Annotated, TypeAlias, TypeAliasType from litestar import Controller, MediaType, get, post from litestar._openapi.schema_generation.plugins import openapi_schema_plugins @@ -29,10 +28,10 @@ KWARG_DEFINITION_ATTRIBUTE_TO_OPENAPI_PROPERTY_MAP, SchemaCreator, ) -from litestar._openapi.schema_generation.utils import _get_normalized_schema_key, _type_or_first_not_none_inner_type from litestar.app import DEFAULT_OPENAPI_CONFIG, Litestar from litestar.di import Provide from litestar.enums import ParamType +from litestar.exceptions import ImproperlyConfiguredException from litestar.openapi.spec import ExternalDocumentation, OpenAPIType, Reference from litestar.openapi.spec.example import Example from litestar.openapi.spec.parameter import Parameter as OpenAPIParameter @@ -40,7 +39,6 @@ from litestar.pagination import ClassicPagination, CursorPagination, OffsetPagination from litestar.params import KwargDefinition, Parameter, ParameterKwarg from litestar.testing import create_test_client -from litestar.types.builtin_types import NoneType from litestar.typing import FieldDefinition from litestar.utils.helpers import get_name from tests.helpers import get_schema_for_field_definition @@ -88,58 +86,68 @@ def test_process_schema_result() -> None: assert getattr(schema, schema_key) == getattr(kwarg_definition, signature_key) -def test_get_normalized_schema_key() -> None: - class LocalClass(msgspec.Struct): - id: str - - # replace each of the long strings with underscores with a tuple of strings split at each underscore - assert ( - "tests", - "unit", - "test_openapi", - "test_schema", - "test_get_normalized_schema_key.LocalClass", - ) == _get_normalized_schema_key(LocalClass) - - assert ("tests", "models", "DataclassPerson") == _get_normalized_schema_key(DataclassPerson) +def test_override_schema_component_key() -> None: + @dataclass + class Data: + pass - builtin_dict = Dict[str, List[int]] - assert ("typing", "Dict[str, typing.List[int]]") == _get_normalized_schema_key(builtin_dict) + @post("/") + def handler( + data: Data, + ) -> Annotated[Data, Parameter(schema_component_key="not_data")]: + return Data() + + @get("/") + def handler_2() -> Annotated[Data, Parameter(schema_component_key="not_data")]: + return Data() + + app = Litestar([handler, handler_2]) + schema = app.openapi_schema.to_schema() + # we expect the annotated / non-annotated to generate independent components + assert schema["paths"]["/"]["post"]["requestBody"]["content"]["application/json"] == { + "schema": {"$ref": "#/components/schemas/test_override_schema_component_key.Data"} + } + assert schema["paths"]["/"]["post"]["responses"]["201"]["content"] == { + "application/json": {"schema": {"$ref": "#/components/schemas/not_data"}} + } + # a response with the same type and the same name should reference the same component + assert schema["paths"]["/"]["get"]["responses"]["200"]["content"] == { + "application/json": {"schema": {"$ref": "#/components/schemas/not_data"}} + } + assert app.openapi_schema.to_schema()["components"] == { + "schemas": { + "not_data": {"properties": {}, "type": "object", "required": [], "title": "Data"}, + "test_override_schema_component_key.Data": { + "properties": {}, + "type": "object", + "required": [], + "title": "Data", + }, + } + } - builtin_with_custom = Dict[str, DataclassPerson] - assert ("typing", "Dict[str, tests.models.DataclassPerson]") == _get_normalized_schema_key(builtin_with_custom) - class LocalGeneric(Generic[T]): +def test_override_schema_component_key_raise_if_keys_are_not_unique() -> None: + @dataclass + class Data: pass - assert ( - "tests", - "unit", - "test_openapi", - "test_schema", - "test_get_normalized_schema_key.LocalGeneric", - ) == _get_normalized_schema_key(LocalGeneric) - - generic_int = LocalGeneric[int] - generic_str = LocalGeneric[str] + @dataclass + class Data2: + pass - assert ( - "tests", - "unit", - "test_openapi", - "test_schema", - "test_get_normalized_schema_key.LocalGeneric[int]", - ) == _get_normalized_schema_key(generic_int) + @post("/") + def handler( + data: Data, + ) -> Annotated[Data, Parameter(schema_component_key="not_data")]: + return Data() - assert ( - "tests", - "unit", - "test_openapi", - "test_schema", - "test_get_normalized_schema_key.LocalGeneric[str]", - ) == _get_normalized_schema_key(generic_str) + @get("/") + def handler_2() -> Annotated[Data2, Parameter(schema_component_key="not_data")]: + return Data2() - assert _get_normalized_schema_key(generic_int) != _get_normalized_schema_key(generic_str) + with pytest.raises(ImproperlyConfiguredException, match="Schema component keys must be unique"): + Litestar([handler, handler_2]).openapi_schema.to_schema() def test_dependency_schema_generation() -> None: @@ -285,16 +293,27 @@ class Foo(TypedDict): def test_create_schema_from_msgspec_annotated_type() -> None: class Lookup(msgspec.Struct): - id: Annotated[str, msgspec.Meta(max_length=16, examples=["example"], description="description", title="title")] + str_field: Annotated[ + str, + msgspec.Meta(max_length=16, examples=["example"], description="description", title="title", pattern=r"\w+"), + ] + bytes_field: Annotated[bytes, msgspec.Meta(max_length=2, min_length=1)] + default_field: Annotated[str, msgspec.Meta(min_length=1)] = "a" schema = get_schema_for_field_definition(FieldDefinition.from_kwarg(name="Lookup", annotation=Lookup)) - assert schema.properties["id"].type == OpenAPIType.STRING # type: ignore[index, union-attr] - assert schema.properties["id"].examples == ["example"] # type: ignore[index, union-attr] - assert schema.properties["id"].description == "description" # type: ignore[index] - assert schema.properties["id"].title == "title" # type: ignore[index, union-attr] - assert schema.properties["id"].max_length == 16 # type: ignore[index, union-attr] - assert schema.required == ["id"] + assert schema.properties["str_field"].type == OpenAPIType.STRING # type: ignore[index, union-attr] + assert schema.properties["str_field"].examples == ["example"] # type: ignore[index, union-attr] + assert schema.properties["str_field"].description == "description" # type: ignore[index] + assert schema.properties["str_field"].title == "title" # type: ignore[index, union-attr] + assert schema.properties["str_field"].max_length == 16 # type: ignore[index, union-attr] + assert sorted(schema.required) == sorted(["str_field", "bytes_field"]) # type: ignore[arg-type] + assert schema.properties["bytes_field"].to_schema() == { # type: ignore[index] + "contentEncoding": "utf-8", + "maxLength": 2, + "minLength": 1, + "type": "string", + } def test_annotated_types() -> None: @@ -306,10 +325,10 @@ class MyDataclass: constrained_int: Annotated[int, annotated_types.Gt(1), annotated_types.Lt(10)] constrained_float: Annotated[float, annotated_types.Ge(1), annotated_types.Le(10)] constrained_date: Annotated[date, annotated_types.Interval(gt=historical_date, lt=today)] - constrained_lower_case: Annotated[str, annotated_types.LowerCase] - constrained_upper_case: Annotated[str, annotated_types.UpperCase] - constrained_is_ascii: Annotated[str, annotated_types.IsAscii] - constrained_is_digit: Annotated[str, annotated_types.IsDigits] + constrained_lower_case: annotated_types.LowerCase[str] + constrained_upper_case: annotated_types.UpperCase[str] + constrained_is_ascii: annotated_types.IsAscii[str] + constrained_is_digit: annotated_types.IsDigit[str] schema = get_schema_for_field_definition(FieldDefinition.from_kwarg(name="MyDataclass", annotation=MyDataclass)) @@ -370,7 +389,7 @@ class TypedDictGeneric(TypedDict, Generic[T]): @pytest.mark.parametrize("cls", annotations) def test_schema_generation_with_generic_classes(cls: Any) -> None: expected_foo_schema = Schema(type=OpenAPIType.INTEGER) - expected_optional_foo_schema = Schema(one_of=[Schema(type=OpenAPIType.NULL), Schema(type=OpenAPIType.INTEGER)]) + expected_optional_foo_schema = Schema(one_of=[Schema(type=OpenAPIType.INTEGER), Schema(type=OpenAPIType.NULL)]) properties = get_schema_for_field_definition( FieldDefinition.from_kwarg(name=get_name(cls), annotation=cls) @@ -424,7 +443,7 @@ def test_schema_generation_with_generic_classes_constrained() -> None: ) def test_schema_generation_with_pagination(annotation: Any) -> None: expected_foo_schema = Schema(type=OpenAPIType.INTEGER) - expected_optional_foo_schema = Schema(one_of=[Schema(type=OpenAPIType.NULL), Schema(type=OpenAPIType.INTEGER)]) + expected_optional_foo_schema = Schema(one_of=[Schema(type=OpenAPIType.INTEGER), Schema(type=OpenAPIType.NULL)]) properties = get_schema_for_field_definition(FieldDefinition.from_annotation(annotation).inner_types[-1]).properties @@ -451,13 +470,87 @@ def test_schema_tuple_with_union() -> None: def test_optional_enum() -> None: class Foo(Enum): + A = 1 + B = "b" + + creator = SchemaCreator(plugins=openapi_schema_plugins) + schema = creator.for_field_definition(FieldDefinition.from_annotation(Optional[Foo])) + assert isinstance(schema, Schema) + assert schema.type is None + assert schema.one_of is not None + null_schema = schema.one_of[1] + assert isinstance(null_schema, Schema) + assert null_schema.type is not None + assert null_schema.type is OpenAPIType.NULL + enum_ref = schema.one_of[0] + assert isinstance(enum_ref, Reference) + assert enum_ref.ref == "#/components/schemas/tests_unit_test_openapi_test_schema_test_optional_enum.Foo" + enum_schema = creator.schema_registry.from_reference(enum_ref).schema + assert enum_schema.type + assert set(enum_schema.type) == {OpenAPIType.INTEGER, OpenAPIType.STRING} + assert enum_schema.enum + assert enum_schema.enum[0] == 1 + assert enum_schema.enum[1] == "b" + + +def test_optional_str_specified_enum() -> None: + class StringEnum(str, Enum): + A = "a" + B = "b" + + creator = SchemaCreator(plugins=openapi_schema_plugins) + schema = creator.for_field_definition(FieldDefinition.from_annotation(Optional[StringEnum])) + assert isinstance(schema, Schema) + assert schema.type is None + assert schema.one_of is not None + + enum_ref = schema.one_of[0] + assert isinstance(enum_ref, Reference) + assert ( + enum_ref.ref + == "#/components/schemas/tests_unit_test_openapi_test_schema_test_optional_str_specified_enum.StringEnum" + ) + enum_schema = creator.schema_registry.from_reference(enum_ref).schema + assert enum_schema.type + assert enum_schema.type == OpenAPIType.STRING + assert enum_schema.enum + assert enum_schema.enum[0] == "a" + assert enum_schema.enum[1] == "b" + + null_schema = schema.one_of[1] + assert isinstance(null_schema, Schema) + assert null_schema.type is not None + assert null_schema.type is OpenAPIType.NULL + + +def test_optional_int_specified_enum() -> None: + class IntEnum(int, Enum): A = 1 B = 2 - schema = get_schema_for_field_definition(FieldDefinition.from_annotation(Optional[Foo])) - assert schema.type is not None - assert set(schema.type) == {OpenAPIType.INTEGER, OpenAPIType.NULL} - assert schema.enum == [1, 2, None] + creator = SchemaCreator(plugins=openapi_schema_plugins) + schema = creator.for_field_definition(FieldDefinition.from_annotation(Optional[IntEnum])) + assert isinstance(schema, Schema) + assert schema.type is None + assert schema.one_of is not None + + enum_ref = schema.one_of[0] + assert isinstance(enum_ref, Reference) + assert ( + enum_ref.ref + == "#/components/schemas/tests_unit_test_openapi_test_schema_test_optional_int_specified_enum.IntEnum" + ) + enum_schema = creator.schema_registry.from_reference(enum_ref).schema + assert enum_schema.type + assert enum_schema.type == OpenAPIType.INTEGER + assert enum_schema.enum + assert enum_schema.enum[0] == 1 + assert enum_schema.enum[1] == 2 + + null_schema = schema.one_of[1] + assert isinstance(null_schema, Schema) + assert null_schema.type is not None + assert null_schema.type is OpenAPIType.NULL def test_optional_literal() -> None: @@ -467,24 +560,6 @@ def test_optional_literal() -> None: assert schema.enum == [1, None] -@pytest.mark.parametrize( - ("in_type", "out_type"), - [ - (FieldDefinition.from_annotation(Optional[int]), int), - (FieldDefinition.from_annotation(Union[None, int]), int), - (FieldDefinition.from_annotation(int), int), - # hack to create a union of NoneType, NoneType to hit a branch for coverage - (FieldDefinition.from_annotation(_GenericAlias(Union, (NoneType, NoneType))), ValueError), - ], -) -def test_type_or_first_not_none_inner_type_utility(in_type: Any, out_type: Any) -> None: - if out_type is ValueError: - with pytest.raises(out_type): - _type_or_first_not_none_inner_type(in_type) - else: - assert _type_or_first_not_none_inner_type(in_type) == out_type - - def test_not_generating_examples_property() -> None: with_examples = SchemaCreator(generate_examples=True) without_examples = with_examples.not_generating_examples @@ -554,9 +629,9 @@ class ModelB(base_type): # type: ignore[no-redef, misc] FieldDefinition.from_kwarg(name="Lookup", annotation=Union[ModelA, ModelB, None]) ) assert schema.one_of == [ - Schema(type=OpenAPIType.NULL), Reference(ref="#/components/schemas/tests_unit_test_openapi_test_schema_test_type_union_with_none.ModelA"), Reference("#/components/schemas/tests_unit_test_openapi_test_schema_test_type_union_with_none.ModelB"), + Schema(type=OpenAPIType.NULL), ] @@ -615,3 +690,32 @@ async def handler(dep: str) -> None: assert param.name == f"param{i}" assert param.required is True assert param.param_in is ParamType.PATH + + +def test_type_alias_type() -> None: + @get("/") + def handler(query_param: Annotated[TypeAliasType("IntAlias", int), Parameter(description="foo")]) -> None: # type: ignore[valid-type] + pass + + app = Litestar([handler]) + param = app.openapi_schema.paths["/"].get.parameters[0] # type: ignore[index, union-attr] + assert param.schema.type is OpenAPIType.INTEGER # type: ignore[union-attr] + # ensure other attributes than the plain type are carried over correctly + assert param.description == "foo" + + +@pytest.mark.skipif(sys.version_info < (3, 12), reason="type keyword not available before 3.12") +def test_type_alias_type_keyword() -> None: + ctx: Dict[str, Any] = {} + exec("type IntAlias = int", ctx, None) + annotation = ctx["IntAlias"] + + @get("/") + def handler(query_param: Annotated[annotation, Parameter(description="foo")]) -> None: # type: ignore[valid-type] + pass + + app = Litestar([handler]) + param = app.openapi_schema.paths["/"].get.parameters[0] # type: ignore[union-attr, index] + assert param.schema.type is OpenAPIType.INTEGER # type: ignore[union-attr] + # ensure other attributes than the plain type are carried over correctly + assert param.description == "foo" diff --git a/tests/unit/test_openapi/test_spec_generation.py b/tests/unit/test_openapi/test_spec_generation.py index f64bd3f569..9601e7a93a 100644 --- a/tests/unit/test_openapi/test_spec_generation.py +++ b/tests/unit/test_openapi/test_spec_generation.py @@ -27,7 +27,7 @@ def handler(data: cls) -> cls: "first_name": {"type": "string"}, "last_name": {"type": "string"}, "id": {"type": "string"}, - "optional": {"oneOf": [{"type": "null"}, {"type": "string"}]}, + "optional": {"oneOf": [{"type": "string"}, {"type": "null"}]}, "complex": { "type": "object", "additionalProperties": { @@ -37,11 +37,11 @@ def handler(data: cls) -> cls: }, "pets": { "oneOf": [ - {"type": "null"}, { "items": {"$ref": "#/components/schemas/DataclassPet"}, "type": "array", }, + {"type": "null"}, ] }, }, @@ -189,8 +189,8 @@ def test_recursive_schema_generation( "properties": { "a": {"$ref": "#/components/schemas/A"}, "b": {"$ref": "#/components/schemas/B"}, - "opt_a": {"oneOf": [{"type": "null"}, {"$ref": "#/components/schemas/A"}]}, - "opt_b": {"oneOf": [{"type": "null"}, {"$ref": "#/components/schemas/B"}]}, + "opt_a": {"oneOf": [{"$ref": "#/components/schemas/A"}, {"type": "null"}]}, + "opt_b": {"oneOf": [{"$ref": "#/components/schemas/B"}, {"type": "null"}]}, "list_a": {"items": {"$ref": "#/components/schemas/A"}, "type": "array"}, "list_b": {"items": {"$ref": "#/components/schemas/B"}, "type": "array"}, }, @@ -202,8 +202,8 @@ def test_recursive_schema_generation( "properties": { "a": {"$ref": "#/components/schemas/A"}, "b": {"$ref": "#/components/schemas/B"}, - "opt_a": {"oneOf": [{"type": "null"}, {"$ref": "#/components/schemas/A"}]}, - "opt_b": {"oneOf": [{"type": "null"}, {"$ref": "#/components/schemas/B"}]}, + "opt_a": {"oneOf": [{"$ref": "#/components/schemas/A"}, {"type": "null"}]}, + "opt_b": {"oneOf": [{"$ref": "#/components/schemas/B"}, {"type": "null"}]}, "list_a": {"items": {"$ref": "#/components/schemas/A"}, "type": "array"}, "list_b": {"items": {"$ref": "#/components/schemas/B"}, "type": "array"}, }, diff --git a/tests/unit/test_openapi/test_typescript_converter/test_converter.py b/tests/unit/test_openapi/test_typescript_converter/test_converter.py index 0241f7152d..eb4d42054d 100644 --- a/tests/unit/test_openapi/test_typescript_converter/test_converter.py +++ b/tests/unit/test_openapi/test_typescript_converter/test_converter.py @@ -334,6 +334,7 @@ def test_openapi_to_typescript_converter(person_controller: Type[Controller], pe export interface QueryParameters { from_date?: null | number | string | string; gender?: "A" | "F" | "M" | "O" | ("A" | "F" | "M" | "O")[] | null; + lucky_number?: 2 | 7 | null; name?: null | string | string[]; page: number; pageSize: number; diff --git a/tests/unit/test_openapi/utils.py b/tests/unit/test_openapi/utils.py index 5190870795..a368c4f0fd 100644 --- a/tests/unit/test_openapi/utils.py +++ b/tests/unit/test_openapi/utils.py @@ -12,3 +12,8 @@ class Gender(str, Enum): FEMALE = "F" OTHER = "O" ANY = "A" + + +class LuckyNumber(int, Enum): + TWO = 2 + SEVEN = 7 diff --git a/tests/unit/test_plugins/test_attrs/__init__.py b/tests/unit/test_plugins/test_attrs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/test_contrib/test_attrs/test_inject_attrs_class.py b/tests/unit/test_plugins/test_attrs/test_inject_attrs_class.py similarity index 100% rename from tests/unit/test_contrib/test_attrs/test_inject_attrs_class.py rename to tests/unit/test_plugins/test_attrs/test_inject_attrs_class.py diff --git a/tests/unit/test_contrib/test_attrs/test_schema_plugin.py b/tests/unit/test_plugins/test_attrs/test_schema_plugin.py similarity index 89% rename from tests/unit/test_contrib/test_attrs/test_schema_plugin.py rename to tests/unit/test_plugins/test_attrs/test_schema_plugin.py index b83a3a5844..d73aa030c5 100644 --- a/tests/unit/test_contrib/test_attrs/test_schema_plugin.py +++ b/tests/unit/test_plugins/test_attrs/test_schema_plugin.py @@ -3,9 +3,9 @@ from attrs import define from typing_extensions import Annotated -from litestar.contrib.attrs.attrs_schema_plugin import AttrsSchemaPlugin from litestar.openapi.spec import OpenAPIType from litestar.openapi.spec.schema import Schema +from litestar.plugins.attrs import AttrsSchemaPlugin from litestar.typing import FieldDefinition from litestar.utils.helpers import get_name from tests.helpers import get_schema_for_field_definition @@ -23,7 +23,7 @@ class AttrsGeneric(Generic[T]): def test_schema_generation_with_generic_classes() -> None: cls = AttrsGeneric[int] expected_foo_schema = Schema(type=OpenAPIType.INTEGER) - expected_optional_foo_schema = Schema(one_of=[Schema(type=OpenAPIType.NULL), Schema(type=OpenAPIType.INTEGER)]) + expected_optional_foo_schema = Schema(one_of=[Schema(type=OpenAPIType.INTEGER), Schema(type=OpenAPIType.NULL)]) field_definition = FieldDefinition.from_kwarg(name=get_name(cls), annotation=cls) properties = get_schema_for_field_definition(field_definition, plugins=[AttrsSchemaPlugin()]).properties diff --git a/tests/unit/test_contrib/test_attrs/test_schema_spec_generation.py b/tests/unit/test_plugins/test_attrs/test_schema_spec_generation.py similarity index 95% rename from tests/unit/test_contrib/test_attrs/test_schema_spec_generation.py rename to tests/unit/test_plugins/test_attrs/test_schema_spec_generation.py index fb7a229ae1..341f2e8fac 100644 --- a/tests/unit/test_contrib/test_attrs/test_schema_spec_generation.py +++ b/tests/unit/test_plugins/test_attrs/test_schema_spec_generation.py @@ -29,7 +29,7 @@ def handler(data: Person) -> Person: "first_name": {"type": "string"}, "last_name": {"type": "string"}, "id": {"type": "string"}, - "optional": {"oneOf": [{"type": "null"}, {"type": "string"}]}, + "optional": {"oneOf": [{"type": "string"}, {"type": "null"}]}, "complex": { "type": "object", "additionalProperties": { @@ -39,11 +39,11 @@ def handler(data: Person) -> Person: }, "pets": { "oneOf": [ - {"type": "null"}, { "items": {"$ref": "#/components/schemas/DataclassPet"}, "type": "array", }, + {"type": "null"}, ] }, }, diff --git a/tests/unit/test_contrib/test_attrs/test_signature.py b/tests/unit/test_plugins/test_attrs/test_signature.py similarity index 100% rename from tests/unit/test_contrib/test_attrs/test_signature.py rename to tests/unit/test_plugins/test_attrs/test_signature.py diff --git a/tests/unit/test_plugins/test_base.py b/tests/unit/test_plugins/test_base.py index 8598eb5c64..c8d06658c8 100644 --- a/tests/unit/test_plugins/test_base.py +++ b/tests/unit/test_plugins/test_base.py @@ -7,11 +7,11 @@ from litestar import Litestar, MediaType, get from litestar.constants import UNDEFINED_SENTINELS -from litestar.contrib.attrs import AttrsSchemaPlugin -from litestar.contrib.pydantic import PydanticDIPlugin, PydanticInitPlugin, PydanticPlugin, PydanticSchemaPlugin -from litestar.contrib.sqlalchemy.plugins import SQLAlchemySerializationPlugin from litestar.plugins import CLIPluginProtocol, InitPluginProtocol, OpenAPISchemaPlugin, PluginRegistry +from litestar.plugins.attrs import AttrsSchemaPlugin from litestar.plugins.core import MsgspecDIPlugin +from litestar.plugins.pydantic import PydanticDIPlugin, PydanticInitPlugin, PydanticPlugin, PydanticSchemaPlugin +from litestar.plugins.sqlalchemy import SQLAlchemySerializationPlugin from litestar.testing import create_test_client from litestar.typing import FieldDefinition @@ -91,7 +91,7 @@ def on_cli_init(self, cli: Group) -> None: pydantic_plugin = PydanticPlugin() with pytest.raises(KeyError): PluginRegistry([CLIPlugin()]).get( - "litestar2.contrib.pydantic.PydanticPlugin" + "litestar2.plugins.pydantic.PydanticPlugin" ) # not a fqdn. should fail # type: ignore[list-item] PluginRegistry([]).get("CLIPlugin") # not a fqdn. should fail # type: ignore[list-item] @@ -99,7 +99,7 @@ def on_cli_init(self, cli: Group) -> None: assert PluginRegistry([cli_plugin, pydantic_plugin]).get(PydanticPlugin) is pydantic_plugin assert PluginRegistry([cli_plugin, pydantic_plugin]).get("PydanticPlugin") is pydantic_plugin assert ( - PluginRegistry([cli_plugin, pydantic_plugin]).get("litestar.contrib.pydantic.PydanticPlugin") is pydantic_plugin + PluginRegistry([cli_plugin, pydantic_plugin]).get("litestar.plugins.pydantic.PydanticPlugin") is pydantic_plugin ) diff --git a/tests/unit/test_plugins/test_problem_details.py b/tests/unit/test_plugins/test_problem_details.py new file mode 100644 index 0000000000..14a0e3ce6c --- /dev/null +++ b/tests/unit/test_plugins/test_problem_details.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +from http import HTTPStatus +from typing import Any + +import pytest + +from litestar import get +from litestar.exceptions.http_exceptions import HTTPException, ValidationException +from litestar.plugins.problem_details import ProblemDetailsConfig, ProblemDetailsException, ProblemDetailsPlugin +from litestar.testing.helpers import create_test_client + + +@pytest.mark.parametrize( + ("exception", "expected"), + [ + ( + ProblemDetailsException(), + { + "status": 500, + "detail": HTTPStatus(500).phrase, + }, + ), + ( + ProblemDetailsException(status_code=400, detail="validation error", instance="https://example.net/error"), + { + "status": 400, + "detail": "validation error", + "instance": "https://example.net/error", + }, + ), + ( + ProblemDetailsException( + status_code=400, + detail="validation error", + extra={"error": "must be positive integer", "pointer": "#age"}, + ), + { + "status": 400, + "detail": "validation error", + "error": "must be positive integer", + "pointer": "#age", + }, + ), + ( + ProblemDetailsException( + status_code=400, + detail="validation error", + extra=[{"error": "must be positive integer", "pointer": "#age"}], + ), + { + "status": 400, + "detail": "validation error", + "extra": [{"error": "must be positive integer", "pointer": "#age"}], + }, + ), + ( + ProblemDetailsException(type_="https://example.net/validation-error"), + { + "type": "https://example.net/validation-error", + "status": 500, + "detail": HTTPStatus(500).phrase, + }, + ), + ], +) +def test_raising_problem_details_exception(exception: ProblemDetailsException, expected: dict[str, Any]) -> None: + @get("/") + async def get_foo() -> None: + raise exception + + with create_test_client([get_foo], plugins=[ProblemDetailsPlugin()]) as client: + response = client.get("/") + + assert response.headers["content-type"] == "application/problem+json" + assert response.json() == expected + assert response.status_code == expected["status"] + + +@pytest.mark.parametrize("enable", (True, False)) +def test_enable_for_all_http_exceptions(enable: bool) -> None: + @get("/") + async def get_foo() -> None: + raise HTTPException() + + config = ProblemDetailsConfig(enable_for_all_http_exceptions=enable) + with create_test_client([get_foo], plugins=[ProblemDetailsPlugin(config)]) as client: + response = client.get("/") + + if enable: + assert response.headers["content-type"] == "application/problem+json" + else: + assert response.headers["content-type"] != "application/problem+json" + + +def test_exception_to_problem_detail_map() -> None: + def validation_exception_to_problem_details_exception(exc: ValidationException) -> ProblemDetailsException: + return ProblemDetailsException( + type_="validation-error", detail=exc.detail, extra=exc.extra, status_code=exc.status_code + ) + + @get("/") + async def get_foo() -> None: + raise ValidationException(detail="Not enough balance", extra=errors) + + errors = {"accounts": ["/account/1", "/account/2"]} + config = ProblemDetailsConfig( + exception_to_problem_detail_map={ValidationException: validation_exception_to_problem_details_exception} + ) + + with create_test_client([get_foo], plugins=[ProblemDetailsPlugin(config)]) as client: + response = client.get("/") + + assert response.status_code == 400 + assert response.headers["content-type"] == "application/problem+json" + assert response.json() == { + "type": "validation-error", + "status": 400, + "detail": "Not enough balance", + "accounts": ["/account/1", "/account/2"], + } diff --git a/tests/unit/test_plugins/test_prometheus.py b/tests/unit/test_plugins/test_prometheus.py new file mode 100644 index 0000000000..894e19e17e --- /dev/null +++ b/tests/unit/test_plugins/test_prometheus.py @@ -0,0 +1,217 @@ +import re +import time +from http.client import HTTPException +from pathlib import Path +from typing import Any + +import pytest +from _pytest.monkeypatch import MonkeyPatch +from prometheus_client import REGISTRY +from pytest_mock import MockerFixture + +from litestar import get, post, websocket_listener +from litestar.plugins.prometheus import PrometheusConfig, PrometheusController, PrometheusMiddleware +from litestar.status_codes import HTTP_200_OK +from litestar.testing import create_test_client + + +def create_config(**kwargs: Any) -> PrometheusConfig: + collectors = list(REGISTRY._collector_to_names.keys()) + for collector in collectors: + REGISTRY.unregister(collector) + + PrometheusMiddleware._metrics = {} + return PrometheusConfig(**kwargs) + + +@pytest.mark.flaky(reruns=5) +def test_prometheus_exporter_metrics_with_http() -> None: + config = create_config() + + @get("/duration") + def duration_handler() -> dict: + time.sleep(0.1) + return {"hello": "world"} + + @get("/error") + def handler_error() -> dict: + raise HTTPException("Error Occurred") + + with create_test_client( + [duration_handler, handler_error, PrometheusController], middleware=[config.middleware] + ) as client: + client.get("/error") + client.get("/duration") + metrics_exporter_response = client.get("/metrics") + + assert metrics_exporter_response.status_code == HTTP_200_OK + metrics = metrics_exporter_response.content.decode() + + assert ( + """litestar_request_duration_seconds_sum{app_name="litestar",method="GET",path="/duration",status_code="200"}""" + in metrics + ) + + assert ( + """litestar_requests_error_total{app_name="litestar",method="GET",path="/error",status_code="500"} 1.0""" + in metrics + ) + + assert ( + """litestar_request_duration_seconds_bucket{app_name="litestar",le="0.005",method="GET",path="/error",status_code="500"} 1.0""" + in metrics + ) + + assert ( + """litestar_requests_in_progress{app_name="litestar",method="GET",path="/metrics",status_code="200"} 1.0""" + in metrics + ) + + assert ( + """litestar_requests_in_progress{app_name="litestar",method="GET",path="/duration",status_code="200"} 0.0""" + in metrics + ) + + duration_metric_matches = re.findall( + r"""litestar_request_duration_seconds_sum{app_name="litestar",method="GET",path="/duration",status_code="200"} (\d+\.\d+)""", + metrics, + ) + + assert duration_metric_matches != [] + assert round(float(duration_metric_matches[0]), 1) == 0.1 + + client.get("/duration") + metrics = client.get("/metrics").content.decode() + + assert ( + """litestar_requests_total{app_name="litestar",method="GET",path="/duration",status_code="200"} 2.0""" + in metrics + ) + + assert ( + """litestar_requests_in_progress{app_name="litestar",method="GET",path="/error",status_code="200"} 0.0""" + in metrics + ) + + assert ( + """litestar_requests_in_progress{app_name="litestar",method="GET",path="/metrics",status_code="200"} 1.0""" + in metrics + ) + + +def test_prometheus_middleware_configurations() -> None: + labels = {"foo": "bar", "baz": lambda a: "qux"} + + config = create_config( + app_name="litestar_test", + prefix="litestar_rocks", + labels=labels, + buckets=[0.1, 0.5, 1.0], + excluded_http_methods=["POST"], + ) + + @get("/test") + def test() -> dict: + return {"hello": "world"} + + @post("/ignore") + def ignore() -> dict: + return {"hello": "world"} + + with create_test_client([test, ignore, PrometheusController], middleware=[config.middleware]) as client: + client.get("/test") + client.post("/ignore") + metrics_exporter_response = client.get("/metrics") + + assert metrics_exporter_response.status_code == HTTP_200_OK + metrics = metrics_exporter_response.content.decode() + + assert ( + """litestar_rocks_requests_total{app_name="litestar_test",baz="qux",foo="bar",method="GET",path="/test",status_code="200"} 1.0""" + in metrics + ) + + assert ( + """litestar_rocks_requests_total{app_name="litestar_test",baz="qux",foo="bar",method="POST",path="/ignore",status_code="201"} 1.0""" + not in metrics + ) + + assert ( + """litestar_rocks_request_duration_seconds_bucket{app_name="litestar_test",baz="qux",foo="bar",le="0.1",method="GET",path="/test",status_code="200"} 1.0""" + in metrics + ) + + assert ( + """litestar_rocks_request_duration_seconds_bucket{app_name="litestar_test",baz="qux",foo="bar",le="0.5",method="GET",path="/test",status_code="200"} 1.0""" + in metrics + ) + + assert ( + """litestar_rocks_request_duration_seconds_bucket{app_name="litestar_test",baz="qux",foo="bar",le="1.0",method="GET",path="/test",status_code="200"} 1.0""" + in metrics + ) + + +def test_prometheus_controller_configurations() -> None: + config = create_config( + exemplars=lambda a: {"trace_id": "1234"}, + ) + + class CustomPrometheusController(PrometheusController): + path: str = "/metrics/custom" + openmetrics_format: bool = True + + @get("/test") + def test() -> dict: + return {"hello": "world"} + + with create_test_client([test, CustomPrometheusController], middleware=[config.middleware]) as client: + client.get("/test") + + metrics_exporter_response = client.get("/metrics/custom") + + assert metrics_exporter_response.status_code == HTTP_200_OK + metrics = metrics_exporter_response.content.decode() + + assert ( + """litestar_requests_total{app_name="litestar",method="GET",path="/test",status_code="200"} 1.0 # {trace_id="1234"} 1.0""" + in metrics + ) + + +def test_prometheus_with_websocket() -> None: + config = create_config() + + @websocket_listener("/test") + def test(data: str) -> dict: + return {"hello": data} + + with create_test_client([test, PrometheusController], middleware=[config.middleware]) as client: + with client.websocket_connect("/test") as websocket: + websocket.send_text("litestar") + websocket.receive_json() + + metrics_exporter_response = client.get("/metrics") + + assert metrics_exporter_response.status_code == HTTP_200_OK + metrics = metrics_exporter_response.content.decode() + + assert ( + """litestar_requests_total{app_name="litestar",method="websocket",path="/test",status_code="200"} 1.0""" + in metrics + ) + + +@pytest.mark.parametrize("env_var", ["PROMETHEUS_MULTIPROC_DIR", "prometheus_multiproc_dir"]) +def test_procdir(monkeypatch: MonkeyPatch, tmp_path: Path, mocker: MockerFixture, env_var: str) -> None: + proc_dir = tmp_path / "something" + proc_dir.mkdir() + monkeypatch.setenv(env_var, str(proc_dir)) + config = create_config() + mock_registry = mocker.patch("litestar.plugins.prometheus.controller.CollectorRegistry") + mock_collector = mocker.patch("litestar.plugins.prometheus.controller.multiprocess.MultiProcessCollector") + + with create_test_client([PrometheusController], middleware=[config.middleware]) as client: + client.get("/metrics") + + mock_collector.assert_called_once_with(mock_registry.return_value) diff --git a/tests/unit/test_contrib/test_pydantic/__init__.py b/tests/unit/test_plugins/test_pydantic/__init__.py similarity index 100% rename from tests/unit/test_contrib/test_pydantic/__init__.py rename to tests/unit/test_plugins/test_pydantic/__init__.py diff --git a/tests/unit/test_contrib/test_pydantic/conftest.py b/tests/unit/test_plugins/test_pydantic/conftest.py similarity index 63% rename from tests/unit/test_contrib/test_pydantic/conftest.py rename to tests/unit/test_plugins/test_pydantic/conftest.py index f56f677ab4..4cf7d59b64 100644 --- a/tests/unit/test_contrib/test_pydantic/conftest.py +++ b/tests/unit/test_plugins/test_pydantic/conftest.py @@ -1,21 +1,18 @@ from __future__ import annotations +from typing import Callable + import pydantic import pytest from pydantic import v1 as pydantic_v1 from pytest import FixtureRequest -from litestar.contrib.pydantic.pydantic_init_plugin import ( # type: ignore[attr-defined] - _KWARG_META_EXTRACTORS, - ConstrainedFieldMetaExtractor, -) - from . import PydanticVersion -@pytest.fixture(autouse=True, scope="session") -def ensure_metadata_extractor_is_added() -> None: - _KWARG_META_EXTRACTORS.add(ConstrainedFieldMetaExtractor) +@pytest.fixture +def int_factory() -> Callable[[], int]: + return lambda: 2 @pytest.fixture(params=["v1", "v2"]) diff --git a/tests/unit/test_contrib/test_pydantic/models.py b/tests/unit/test_plugins/test_pydantic/models.py similarity index 100% rename from tests/unit/test_contrib/test_pydantic/models.py rename to tests/unit/test_plugins/test_pydantic/models.py diff --git a/tests/unit/test_contrib/test_pydantic/test_beanie_integration.py b/tests/unit/test_plugins/test_pydantic/test_beanie_integration.py similarity index 55% rename from tests/unit/test_contrib/test_pydantic/test_beanie_integration.py rename to tests/unit/test_plugins/test_pydantic/test_beanie_integration.py index 60ab77259b..cec66869e3 100644 --- a/tests/unit/test_contrib/test_pydantic/test_beanie_integration.py +++ b/tests/unit/test_plugins/test_pydantic/test_beanie_integration.py @@ -1,15 +1,13 @@ -from typing import TYPE_CHECKING, Optional, Type +from typing import Optional import beanie +from pydantic import BaseModel -if TYPE_CHECKING: - from pydantic import BaseModel +from litestar.plugins.pydantic import PydanticDTO -from litestar.contrib.pydantic import PydanticDTO - -def test_generate_field_definitions_from_beanie_models(base_model: "Type[BaseModel]") -> None: - class Category(base_model): # type: ignore[valid-type, misc] +def test_generate_field_definitions_from_beanie_models() -> None: + class Category(BaseModel): name: str description: str diff --git a/tests/unit/test_contrib/test_pydantic/test_dto.py b/tests/unit/test_plugins/test_pydantic/test_dto.py similarity index 94% rename from tests/unit/test_contrib/test_pydantic/test_dto.py rename to tests/unit/test_plugins/test_pydantic/test_dto.py index 782d915902..fe7cbab991 100644 --- a/tests/unit/test_contrib/test_pydantic/test_dto.py +++ b/tests/unit/test_plugins/test_pydantic/test_dto.py @@ -8,8 +8,8 @@ from typing_extensions import Annotated, Literal from litestar import Request, post -from litestar.contrib.pydantic import PydanticDTO, _model_dump_json from litestar.dto import DTOConfig +from litestar.plugins.pydantic import PydanticDTO, _model_dump_json from litestar.testing import create_test_client from litestar.types import Empty from litestar.typing import FieldDefinition @@ -61,7 +61,7 @@ class Model(base_model): # type: ignore[misc, valid-type] def test_detect_nested_field_pydantic_v1(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr("litestar.contrib.pydantic.pydantic_dto_factory.pydantic_v2", Empty) + monkeypatch.setattr("litestar.plugins.pydantic.dto.pydantic_v2", Empty) class Model(pydantic_v1.BaseModel): a: str @@ -75,7 +75,7 @@ def test_pydantic_field_descriptions(create_module: Callable[[str], ModuleType]) module = create_module( """ from litestar import Litestar, get -from litestar.contrib.pydantic import PydanticDTO +from litestar.plugins.pydantic import PydanticDTO from litestar.dto import DTOConfig from pydantic import BaseModel, Field from typing_extensions import Annotated @@ -83,7 +83,7 @@ def test_pydantic_field_descriptions(create_module: Callable[[str], ModuleType]) class User(BaseModel): id: Annotated[ int, - Field(description="This is a test (id description)."), + Field(description="This is a test (id description).", gt=1), ] class DataCollectionDTO(PydanticDTO[User]): @@ -102,6 +102,7 @@ def get_user() -> User: component_schema = schema.components.schemas["GetUserUserResponseBody"] assert component_schema.properties is not None assert component_schema.properties["id"].description == "This is a test (id description)." + assert component_schema.properties["id"].exclusive_minimum == 1 # type: ignore[union-attr] @pytest.mark.parametrize( diff --git a/tests/unit/test_contrib/test_pydantic/test_inject_pydantic.py b/tests/unit/test_plugins/test_pydantic/test_inject_pydantic.py similarity index 100% rename from tests/unit/test_contrib/test_pydantic/test_inject_pydantic.py rename to tests/unit/test_plugins/test_pydantic/test_inject_pydantic.py diff --git a/tests/unit/test_contrib/test_pydantic/test_integration.py b/tests/unit/test_plugins/test_pydantic/test_integration.py similarity index 86% rename from tests/unit/test_contrib/test_pydantic/test_integration.py rename to tests/unit/test_plugins/test_pydantic/test_integration.py index 9a21447f05..64a7bc6674 100644 --- a/tests/unit/test_contrib/test_pydantic/test_integration.py +++ b/tests/unit/test_plugins/test_pydantic/test_integration.py @@ -6,14 +6,13 @@ from pydantic import v1 as pydantic_v1 from typing_extensions import Annotated -from litestar import post -from litestar.contrib.pydantic import PydanticInitPlugin, PydanticPlugin -from litestar.contrib.pydantic.pydantic_dto_factory import PydanticDTO +from litestar import get, post from litestar.enums import RequestEncodingType from litestar.params import Body, Parameter +from litestar.plugins.pydantic import PydanticDTO, PydanticInitPlugin, PydanticPlugin from litestar.status_codes import HTTP_400_BAD_REQUEST from litestar.testing import create_test_client -from tests.unit.test_contrib.test_pydantic.models import PydanticPerson, PydanticV1Person +from tests.unit.test_plugins.test_pydantic.models import PydanticPerson, PydanticV1Person from . import BaseModelType, PydanticVersion @@ -215,8 +214,6 @@ class Config: underscore_fields_are_private = True _field: str = pydantic_v2.PrivateAttr() - # include an invalid annotation here to ensure we never touch those fields - _underscore_field: "foo" # type: ignore[name-defined] # noqa: F821 bar: str @@ -388,3 +385,55 @@ async def handler(data: Model) -> None: with create_test_client([handler], plugins=plugins) as client: res = client.post("/", json={"test_bool": "YES"}) assert res.status_code == 400 if expect_error else 201 + + +def test_model_defaults(pydantic_version: PydanticVersion) -> None: + lib = pydantic_v1 if pydantic_version == "v1" else pydantic_v2 + + class Model(lib.BaseModel): # type: ignore[misc, name-defined] + a: int + b: int = lib.Field(default=1) + c: int = lib.Field(default_factory=lambda: 3) + + @post("/") + async def handler(data: Model) -> Dict[str, int]: + return {"a": data.a, "b": data.b, "c": data.c} + + with create_test_client([handler]) as client: + schema = client.app.openapi_schema.components.schemas["test_model_defaults.Model"] + res = client.post("/", json={"a": 5}) + assert res.status_code == 201 + assert res.json() == {"a": 5, "b": 1, "c": 3} + assert schema.required == ["a"] + assert schema.properties["b"].default == 1 + assert schema.properties["c"].default is None + + +@pytest.mark.parametrize("with_dto", [True, False]) +def test_v2_computed_fields(with_dto: bool) -> None: + # https://github.com/litestar-org/litestar/issues/3656 + + class Model(pydantic_v2.BaseModel): + foo: int = 1 + + @pydantic_v2.computed_field + def bar(self) -> int: + return 2 + + @pydantic_v2.computed_field(examples=[1], json_schema_extra={"title": "this is computed"}) + def baz(self) -> int: + return 3 + + @get("/", return_dto=PydanticDTO[Model] if with_dto else None) + async def handler() -> Model: + return Model() + + component_name = "HandlerModelResponseBody" if with_dto else "test_v2_computed_fields.Model" + + with create_test_client([handler]) as client: + schema = client.app.openapi_schema.components.schemas[component_name] + res = client.get("/") + assert list(schema.properties.keys()) == ["foo", "bar", "baz"] + assert schema.properties["baz"].title == "this is computed" + assert schema.properties["baz"].examples == [1] + assert res.json() == {"foo": 1, "bar": 2, "baz": 3} diff --git a/tests/unit/test_contrib/test_pydantic/test_openapi.py b/tests/unit/test_plugins/test_pydantic/test_openapi.py similarity index 51% rename from tests/unit/test_contrib/test_pydantic/test_openapi.py rename to tests/unit/test_plugins/test_pydantic/test_openapi.py index b0921f00ac..01e36fb21b 100644 --- a/tests/unit/test_contrib/test_pydantic/test_openapi.py +++ b/tests/unit/test_plugins/test_pydantic/test_openapi.py @@ -1,31 +1,26 @@ -from datetime import date, datetime, timedelta, timezone +# pyright: reportOptionalSubscript=false, reportGeneralTypeIssues=false +from datetime import date, timedelta from decimal import Decimal from types import ModuleType -from typing import Any, Callable, Pattern, Type, Union, cast +from typing import Any, Callable, Dict, List, Optional, Pattern, Type, Union, cast +import annotated_types import pydantic as pydantic_v2 import pytest from pydantic import v1 as pydantic_v1 from typing_extensions import Annotated -from litestar import Litestar, post -from litestar._openapi.schema_generation.constrained_fields import ( - create_date_constrained_field_schema, - create_numerical_constrained_field_schema, - create_string_constrained_field_schema, -) +from litestar import Litestar, get, post from litestar._openapi.schema_generation.schema import SchemaCreator -from litestar.contrib.pydantic import PydanticPlugin, PydanticSchemaPlugin from litestar.openapi import OpenAPIConfig from litestar.openapi.spec import Reference, Schema from litestar.openapi.spec.enums import OpenAPIFormat, OpenAPIType -from litestar.params import KwargDefinition -from litestar.status_codes import HTTP_200_OK +from litestar.plugins.pydantic import PydanticPlugin, PydanticSchemaPlugin from litestar.testing import TestClient, create_test_client from litestar.typing import FieldDefinition from litestar.utils import is_class_and_subclass from tests.helpers import get_schema_for_field_definition -from tests.unit.test_contrib.test_pydantic.models import ( +from tests.unit.test_plugins.test_pydantic.models import ( PydanticDataclassPerson, PydanticPerson, PydanticV1DataclassPerson, @@ -60,6 +55,9 @@ pydantic_v2.constr(min_length=1), pydantic_v2.constr(min_length=10), pydantic_v2.constr(min_length=10, max_length=100), +] + +constrained_bytes_v2 = [ pydantic_v2.conbytes(min_length=1), pydantic_v2.conbytes(min_length=10), pydantic_v2.conbytes(min_length=10, max_length=100), @@ -124,23 +122,59 @@ ] +@pytest.fixture() +def schema_creator(plugin: PydanticSchemaPlugin) -> SchemaCreator: + return SchemaCreator(plugins=[plugin]) + + +@pytest.fixture() +def plugin() -> PydanticSchemaPlugin: + return PydanticSchemaPlugin() + + @pytest.mark.parametrize("annotation", constrained_collection_v1) -def test_create_collection_constrained_field_schema_pydantic_v1(annotation: Any) -> None: - schema = SchemaCreator().for_collection_constrained_field(FieldDefinition.from_annotation(annotation)) - assert schema.type == OpenAPIType.ARRAY - assert schema.items.type == OpenAPIType.INTEGER # type: ignore[union-attr] - assert schema.min_items == annotation.min_items - assert schema.max_items == annotation.max_items +def test_create_collection_constrained_field_schema_pydantic_v1( + annotation: Any, + schema_creator: SchemaCreator, + plugin: PydanticSchemaPlugin, +) -> None: + class Model(pydantic_v1.BaseModel): + field: annotation + + schema = schema_creator.for_plugin(FieldDefinition.from_annotation(Model), plugin).properties["field"] # pyright: ignore[reportAttributeAccessIssue] + + assert schema.type == OpenAPIType.ARRAY # pyright: ignore[reportAttributeAccessIssue] + assert schema.items.type == OpenAPIType.INTEGER # type: ignore[union-attr] # pyright: ignore[reportAttributeAccessIssue] + assert schema.min_items == annotation.min_items # pyright: ignore[reportAttributeAccessIssue] + assert schema.max_items == annotation.max_items # pyright: ignore[reportAttributeAccessIssue] + +@pytest.mark.parametrize("make_constraint", [pydantic_v2.conlist, pydantic_v2.conset, pydantic_v2.confrozenset]) +@pytest.mark.parametrize( + "min_length, max_length", + [ + (None, None), + (1, None), + (1, 1), + (None, 1), + ], +) +def test_create_collection_constrained_field_schema_pydantic_v2( + make_constraint: Callable[..., Any], + min_length: Optional[int], + max_length: Optional[int], + schema_creator: SchemaCreator, + plugin: PydanticSchemaPlugin, +) -> None: + class Model(pydantic_v2.BaseModel): + field: make_constraint(int, min_length=min_length, max_length=max_length) # type: ignore[valid-type] -@pytest.mark.parametrize("annotation", constrained_collection_v2) -def test_create_collection_constrained_field_schema_pydantic_v2(annotation: Any) -> None: - field_definition = FieldDefinition.from_annotation(annotation) - schema = SchemaCreator().for_collection_constrained_field(field_definition) - assert schema.type == OpenAPIType.ARRAY + schema = schema_creator.for_plugin(FieldDefinition.from_annotation(Model), plugin).properties["field"] # pyright: ignore[reportAttributeAccessIssue] + + assert schema.type == OpenAPIType.ARRAY # pyright: ignore[reportAttributeAccessIssue] assert schema.items.type == OpenAPIType.INTEGER # type: ignore[union-attr] - assert any(getattr(m, "min_length", None) == schema.min_items for m in field_definition.metadata if m) - assert any(getattr(m, "max_length", None) == schema.max_items for m in field_definition.metadata if m) + assert schema.min_items == min_length # pyright: ignore[reportAttributeAccessIssue] + assert schema.max_items == max_length # pyright: ignore[reportAttributeAccessIssue] @pytest.fixture() @@ -154,45 +188,65 @@ def conlist(pydantic_version: PydanticVersion) -> Any: def test_create_collection_constrained_field_schema_sub_fields( - pydantic_version: PydanticVersion, conset: Any, conlist: Any + pydantic_version: PydanticVersion, + conset: Any, + conlist: Any, + schema_creator: SchemaCreator, + plugin: PydanticSchemaPlugin, ) -> None: - for pydantic_fn in [conset, conlist]: - if pydantic_version == "v1": - annotation = pydantic_fn(Union[str, int], min_items=1, max_items=10) - else: - annotation = pydantic_fn(Union[str, int], min_length=1, max_length=10) - field_definition = FieldDefinition.from_annotation(annotation) - schema = SchemaCreator().for_collection_constrained_field(field_definition) - assert schema.type == OpenAPIType.ARRAY - assert schema.max_items == 10 - assert schema.min_items == 1 - assert isinstance(schema.items, Schema) - assert schema.items.one_of is not None - - def _get_schema_type(s: Any) -> OpenAPIType: - assert isinstance(s, Schema) - assert isinstance(s.type, OpenAPIType) - return s.type + if pydantic_version == "v1": + + class Modelv1(pydantic_v1.BaseModel): + set_field: conset(Union[str, int], min_items=1, max_items=10) # type: ignore[valid-type] + list_field: conlist(Union[str, int], min_items=1, max_items=10) # type: ignore[valid-type] + + model_schema = schema_creator.for_plugin(FieldDefinition.from_annotation(Modelv1), plugin) + else: + + class Modelv2(pydantic_v2.BaseModel): + set_field: conset(Union[str, int], min_length=1, max_length=10) # type: ignore[valid-type] + list_field: conlist(Union[str, int], min_length=1, max_length=10) # type: ignore[valid-type] + + model_schema = schema_creator.for_plugin(FieldDefinition.from_annotation(Modelv2), plugin) + + def _get_schema_type(s: Any) -> OpenAPIType: + assert isinstance(s, Schema) + assert isinstance(s.type, OpenAPIType) + return s.type + + for field_name in ["set_field", "list_field"]: + schema = model_schema.properties[field_name] # pyright: ignore[reportAttributeAccessIssue] + + assert schema.type == OpenAPIType.ARRAY # pyright: ignore[reportAttributeAccessIssue] + assert schema.max_items == 10 # pyright: ignore[reportAttributeAccessIssue] + assert schema.min_items == 1 # pyright: ignore[reportAttributeAccessIssue] + assert isinstance(schema.items, Schema) # pyright: ignore[reportAttributeAccessIssue] + assert schema.items.one_of is not None # pyright: ignore[reportAttributeAccessIssue] # https://github.com/litestar-org/litestar/pull/2570#issuecomment-1788122570 - assert {_get_schema_type(s) for s in schema.items.one_of} == {OpenAPIType.STRING, OpenAPIType.INTEGER} - if pydantic_fn is conset: - # set should have uniqueItems always - assert schema.unique_items + assert {_get_schema_type(s) for s in schema.items.one_of} == {OpenAPIType.STRING, OpenAPIType.INTEGER} # pyright: ignore[reportAttributeAccessIssue] + + # set should have uniqueItems always + assert model_schema.properties["set_field"].unique_items # pyright: ignore[reportAttributeAccessIssue] @pytest.mark.parametrize("annotation", constrained_string_v1) -def test_create_string_constrained_field_schema_pydantic_v1(annotation: Any) -> None: - field_definition = FieldDefinition.from_annotation(annotation) +def test_create_string_constrained_field_schema_pydantic_v1( + annotation: Any, + schema_creator: SchemaCreator, + plugin: PydanticSchemaPlugin, +) -> None: + class Model(pydantic_v1.BaseModel): + field: annotation - assert isinstance(field_definition.kwarg_definition, KwargDefinition) - schema = create_string_constrained_field_schema(field_definition.annotation, field_definition.kwarg_definition) - assert schema.type == OpenAPIType.STRING + schema = schema_creator.for_plugin(FieldDefinition.from_annotation(Model), plugin).properties["field"] # pyright: ignore[reportAttributeAccessIssue] - assert schema.min_length == annotation.min_length - assert schema.max_length == annotation.max_length + assert schema.type == OpenAPIType.STRING # pyright: ignore[reportAttributeAccessIssue] + + assert schema.min_length == annotation.min_length # pyright: ignore[reportAttributeAccessIssue] + assert schema.max_length == annotation.max_length # pyright: ignore[reportAttributeAccessIssue] if pattern := getattr(annotation, "regex", None): - assert schema.pattern == pattern.pattern if isinstance(pattern, Pattern) else pattern + assert schema.pattern == pattern.pattern if isinstance(pattern, Pattern) else pattern # pyright: ignore[reportAttributeAccessIssue] if annotation.to_lower: assert schema.description if annotation.to_upper: @@ -200,137 +254,160 @@ def test_create_string_constrained_field_schema_pydantic_v1(annotation: Any) -> @pytest.mark.parametrize("annotation", constrained_string_v2) -def test_create_string_constrained_field_schema_pydantic_v2(annotation: Any) -> None: - field_definition = FieldDefinition.from_annotation(annotation) - - assert isinstance(field_definition.kwarg_definition, KwargDefinition) - schema = create_string_constrained_field_schema(field_definition.annotation, field_definition.kwarg_definition) - assert schema.type == OpenAPIType.STRING - - assert any(getattr(m, "min_length", None) == schema.min_length for m in field_definition.metadata if m) - assert any(getattr(m, "max_length", None) == schema.max_length for m in field_definition.metadata if m) - if pattern := getattr(annotation, "regex", getattr(annotation, "pattern", None)): - assert schema.pattern == pattern.pattern if isinstance(pattern, Pattern) else pattern - if any(getattr(m, "to_lower", getattr(m, "to_upper", None)) for m in field_definition.metadata if m): - assert schema.description +def test_create_string_constrained_field_schema_pydantic_v2( + annotation: Any, + schema_creator: SchemaCreator, + plugin: PydanticSchemaPlugin, +) -> None: + constraint: pydantic_v2.types.StringConstraints = annotation.__metadata__[0] + + class Model(pydantic_v2.BaseModel): + field: annotation + + schema = schema_creator.for_plugin(FieldDefinition.from_annotation(Model), plugin).properties["field"] # pyright: ignore[reportAttributeAccessIssue] + + assert schema.type == OpenAPIType.STRING # pyright: ignore[reportAttributeAccessIssue] + assert schema.min_length == constraint.min_length # pyright: ignore[reportAttributeAccessIssue] + assert schema.max_length == constraint.max_length # pyright: ignore[reportAttributeAccessIssue] + assert schema.pattern == constraint.pattern # pyright: ignore[reportAttributeAccessIssue] + if constraint.to_upper: + assert schema.description == "must be in upper case" + if constraint.to_lower: + assert schema.description == "must be in lower case" + + +@pytest.mark.parametrize("annotation", constrained_bytes_v2) +def test_create_byte_constrained_field_schema_pydantic_v2( + annotation: Any, + schema_creator: SchemaCreator, + plugin: PydanticSchemaPlugin, +) -> None: + constraint: annotated_types.Len = annotation.__metadata__[1] + + class Model(pydantic_v2.BaseModel): + field: annotation + + schema = schema_creator.for_plugin(FieldDefinition.from_annotation(Model), plugin).properties["field"] # pyright: ignore[reportAttributeAccessIssue] + + assert schema.type == OpenAPIType.STRING # pyright: ignore[reportAttributeAccessIssue] + assert schema.min_length == constraint.min_length # pyright: ignore[reportAttributeAccessIssue] + assert schema.max_length == constraint.max_length # pyright: ignore[reportAttributeAccessIssue] @pytest.mark.parametrize("annotation", constrained_numbers_v1) -def test_create_numerical_constrained_field_schema_pydantic_v1(annotation: Any) -> None: +def test_create_numerical_constrained_field_schema_pydantic_v1( + annotation: Any, + schema_creator: SchemaCreator, + plugin: PydanticSchemaPlugin, +) -> None: from pydantic.v1.types import ConstrainedDecimal, ConstrainedFloat, ConstrainedInt annotation = cast(Union[ConstrainedInt, ConstrainedFloat, ConstrainedDecimal], annotation) - field_definition = FieldDefinition.from_annotation(annotation) + class Model(pydantic_v1.BaseModel): + field: annotation + + schema = schema_creator.for_plugin(FieldDefinition.from_annotation(Model), plugin).properties["field"] # pyright: ignore[reportAttributeAccessIssue] - assert isinstance(field_definition.kwarg_definition, KwargDefinition) - schema = create_numerical_constrained_field_schema(field_definition.annotation, field_definition.kwarg_definition) assert ( - schema.type == OpenAPIType.INTEGER if is_class_and_subclass(annotation, ConstrainedInt) else OpenAPIType.NUMBER + schema.type == OpenAPIType.INTEGER if is_class_and_subclass(annotation, ConstrainedInt) else OpenAPIType.NUMBER # pyright: ignore[reportAttributeAccessIssue] ) - assert schema.exclusive_minimum == annotation.gt - assert schema.minimum == annotation.ge - assert schema.exclusive_maximum == annotation.lt - assert schema.maximum == annotation.le - assert schema.multiple_of == annotation.multiple_of + assert schema.exclusive_minimum == annotation.gt # pyright: ignore[reportAttributeAccessIssue] + assert schema.minimum == annotation.ge # pyright: ignore[reportAttributeAccessIssue] + assert schema.exclusive_maximum == annotation.lt # pyright: ignore[reportAttributeAccessIssue] + assert schema.maximum == annotation.le # pyright: ignore[reportAttributeAccessIssue] + assert schema.multiple_of == annotation.multiple_of # pyright: ignore[reportAttributeAccessIssue] + + +@pytest.mark.parametrize( + "make_constraint, constraint_kwargs", + [ + (pydantic_v2.conint, {"gt": 10, "lt": 100}), + (pydantic_v2.conint, {"ge": 10, "le": 100}), + (pydantic_v2.conint, {"ge": 10, "le": 100, "multiple_of": 7}), + (pydantic_v2.confloat, {"gt": 10, "lt": 100}), + (pydantic_v2.confloat, {"ge": 10, "le": 100}), + (pydantic_v2.confloat, {"ge": 10, "le": 100, "multiple_of": 4.2}), + (pydantic_v2.confloat, {"gt": 10, "lt": 100, "multiple_of": 10}), + (pydantic_v2.condecimal, {"gt": Decimal("10"), "lt": Decimal("100")}), + (pydantic_v2.condecimal, {"ge": Decimal("10"), "le": Decimal("100")}), + (pydantic_v2.condecimal, {"gt": Decimal("10"), "lt": Decimal("100"), "multiple_of": Decimal("5")}), + (pydantic_v2.condecimal, {"ge": Decimal("10"), "le": Decimal("100"), "multiple_of": Decimal("2")}), + ], +) +def test_create_numerical_constrained_field_schema_pydantic_v2( + make_constraint: Any, + constraint_kwargs: Dict[str, Any], + schema_creator: SchemaCreator, + plugin: PydanticSchemaPlugin, +) -> None: + annotation = make_constraint(**constraint_kwargs) + class Model(pydantic_v1.BaseModel): + field: annotation # type: ignore[valid-type] -@pytest.mark.parametrize("annotation", constrained_numbers_v2) -def test_create_numerical_constrained_field_schema_pydantic_v2(annotation: Any) -> None: - field_definition = FieldDefinition.from_annotation(annotation) + schema = schema_creator.for_plugin(FieldDefinition.from_annotation(Model), plugin).properties["field"] # pyright: ignore[reportAttributeAccessIssue] - assert isinstance(field_definition.kwarg_definition, KwargDefinition) - schema = create_numerical_constrained_field_schema(field_definition.annotation, field_definition.kwarg_definition) - assert schema.type == OpenAPIType.INTEGER if is_class_and_subclass(annotation, int) else OpenAPIType.NUMBER - assert any(getattr(m, "gt", None) == schema.exclusive_minimum for m in field_definition.metadata if m) - assert any(getattr(m, "ge", None) == schema.minimum for m in field_definition.metadata if m) - assert any(getattr(m, "lt", None) == schema.exclusive_maximum for m in field_definition.metadata if m) - assert any(getattr(m, "le", None) == schema.maximum for m in field_definition.metadata if m) - assert any(getattr(m, "multiple_of", None) == schema.multiple_of for m in field_definition.metadata if m) + assert schema.type == OpenAPIType.INTEGER if is_class_and_subclass(annotation, int) else OpenAPIType.NUMBER # pyright: ignore[reportAttributeAccessIssue] + assert schema.exclusive_minimum == constraint_kwargs.get("gt") # pyright: ignore[reportAttributeAccessIssue] + assert schema.minimum == constraint_kwargs.get("ge") # pyright: ignore[reportAttributeAccessIssue] + assert schema.exclusive_maximum == constraint_kwargs.get("lt") # pyright: ignore[reportAttributeAccessIssue] + assert schema.maximum == constraint_kwargs.get("le") # pyright: ignore[reportAttributeAccessIssue] + assert schema.multiple_of == constraint_kwargs.get("multiple_of") # pyright: ignore[reportAttributeAccessIssue] @pytest.mark.parametrize("annotation", constrained_dates_v1) -def test_create_date_constrained_field_schema_pydantic_v1(annotation: Any) -> None: - field_definition = FieldDefinition.from_annotation(annotation) +def test_create_date_constrained_field_schema_pydantic_v1( + annotation: Any, + schema_creator: SchemaCreator, + plugin: PydanticSchemaPlugin, +) -> None: + class Model(pydantic_v1.BaseModel): + field: annotation - assert isinstance(field_definition.kwarg_definition, KwargDefinition) - schema = create_date_constrained_field_schema(field_definition.annotation, field_definition.kwarg_definition) - assert schema.type == OpenAPIType.STRING - assert schema.format == OpenAPIFormat.DATE - assert ( - datetime.fromtimestamp(schema.exclusive_minimum, tz=timezone.utc) if schema.exclusive_minimum else None - ) == ( - datetime.fromordinal(annotation.gt.toordinal()).replace(tzinfo=timezone.utc) - if annotation.gt is not None - else None - ) - assert (datetime.fromtimestamp(schema.minimum, tz=timezone.utc) if schema.minimum else None) == ( - datetime.fromordinal(annotation.ge.toordinal()).replace(tzinfo=timezone.utc) - if annotation.ge is not None - else None - ) - assert ( - datetime.fromtimestamp(schema.exclusive_maximum, tz=timezone.utc) if schema.exclusive_maximum else None - ) == ( - datetime.fromordinal(annotation.lt.toordinal()).replace(tzinfo=timezone.utc) - if annotation.lt is not None - else None - ) - assert (datetime.fromtimestamp(schema.maximum, tz=timezone.utc) if schema.maximum else None) == ( - datetime.fromordinal(annotation.le.toordinal()).replace(tzinfo=timezone.utc) - if annotation.le is not None - else None - ) + schema = schema_creator.for_plugin(FieldDefinition.from_annotation(Model), plugin).properties["field"] # pyright: ignore[reportAttributeAccessIssue] + assert schema.type == OpenAPIType.STRING # pyright: ignore[reportAttributeAccessIssue] + assert schema.format == OpenAPIFormat.DATE # pyright: ignore[reportAttributeAccessIssue] + if gt := annotation.gt: + assert date.fromtimestamp(schema.exclusive_minimum) == gt # type: ignore[arg-type] # pyright: ignore[reportArgumentType] + if ge := annotation.ge: + assert date.fromtimestamp(schema.minimum) == ge # type: ignore[arg-type] + if lt := annotation.lt: + assert date.fromtimestamp(schema.exclusive_maximum) == lt # type: ignore[arg-type] + if le := annotation.le: + assert date.fromtimestamp(schema.maximum) == le # type: ignore[arg-type] -@pytest.mark.parametrize("annotation", constrained_dates_v2) -def test_create_date_constrained_field_schema_pydantic_v2(annotation: Any) -> None: - field_definition = FieldDefinition.from_annotation(annotation) - assert isinstance(field_definition.kwarg_definition, KwargDefinition) - schema = create_date_constrained_field_schema(field_definition.annotation, field_definition.kwarg_definition) - assert schema.type == OpenAPIType.STRING - assert schema.format == OpenAPIFormat.DATE - assert any( - ( - datetime.fromordinal(getattr(m, "gt", None).toordinal()).replace(tzinfo=timezone.utc) # type: ignore[union-attr] - if getattr(m, "gt", None) is not None - else None - ) - == (datetime.fromtimestamp(schema.exclusive_minimum, tz=timezone.utc) if schema.exclusive_minimum else None) - for m in field_definition.metadata - if m - ) - assert any( - ( - datetime.fromordinal(getattr(m, "ge", None).toordinal()).replace(tzinfo=timezone.utc) # type: ignore[union-attr] - if getattr(m, "ge", None) is not None - else None - ) - == (datetime.fromtimestamp(schema.minimum, tz=timezone.utc) if schema.minimum else None) - for m in field_definition.metadata - if m - ) - assert any( - ( - datetime.fromordinal(getattr(m, "lt", None).toordinal()).replace(tzinfo=timezone.utc) # type: ignore[union-attr] - if getattr(m, "lt", None) is not None - else None - ) - == (datetime.fromtimestamp(schema.exclusive_maximum, tz=timezone.utc) if schema.exclusive_maximum else None) - for m in field_definition.metadata - if m - ) - assert any( - ( - datetime.fromordinal(getattr(m, "le", None).toordinal()).replace(tzinfo=timezone.utc) # type: ignore[union-attr] - if getattr(m, "le", None) is not None - else None - ) - == (datetime.fromtimestamp(schema.maximum, tz=timezone.utc) if schema.maximum else None) - for m in field_definition.metadata - if m - ) +@pytest.mark.parametrize( + "constraints", + [ + {"gt": date.today() - timedelta(days=10), "lt": date.today() + timedelta(days=100)}, + {"ge": date.today() - timedelta(days=10), "le": date.today() + timedelta(days=100)}, + {"gt": date.today() - timedelta(days=10), "lt": date.today() + timedelta(days=100)}, + {"ge": date.today() - timedelta(days=10), "le": date.today() + timedelta(days=100)}, + ], +) +def test_create_date_constrained_field_schema_pydantic_v2( + constraints: Dict[str, Any], + schema_creator: SchemaCreator, + plugin: PydanticSchemaPlugin, +) -> None: + class Model(pydantic_v2.BaseModel): + field: pydantic_v2.condate(**constraints) # type: ignore[valid-type] + + schema = schema_creator.for_plugin(FieldDefinition.from_annotation(Model), plugin).properties["field"] # pyright: ignore[reportAttributeAccessIssue] + assert schema.type == OpenAPIType.STRING # pyright: ignore[reportAttributeAccessIssue] + assert schema.format == OpenAPIFormat.DATE # pyright: ignore[reportAttributeAccessIssue] + + if gt := constraints.get("gt"): + assert date.fromtimestamp(schema.exclusive_minimum) == gt # type: ignore[arg-type] + if ge := constraints.get("ge"): + assert date.fromtimestamp(schema.minimum) == ge # type: ignore[arg-type] + if lt := constraints.get("lt"): + assert date.fromtimestamp(schema.exclusive_maximum) == lt # type: ignore[arg-type] + if le := constraints.get("le"): + assert date.fromtimestamp(schema.maximum) == le # type: ignore[arg-type] @pytest.mark.parametrize( @@ -340,15 +417,37 @@ def test_create_date_constrained_field_schema_pydantic_v2(annotation: Any) -> No *constrained_collection_v1, *constrained_string_v1, *constrained_dates_v1, + ], +) +def test_create_constrained_field_schema_v1( + annotation: Any, + schema_creator: SchemaCreator, + plugin: PydanticSchemaPlugin, +) -> None: + class Model(pydantic_v1.BaseModel): + field: annotation + + assert schema_creator.for_plugin(FieldDefinition.from_annotation(Model), plugin).properties["field"] # pyright: ignore[reportAttributeAccessIssue] + + +@pytest.mark.parametrize( + "annotation", + [ *constrained_numbers_v2, *constrained_collection_v2, *constrained_string_v2, *constrained_dates_v2, ], ) -def test_create_constrained_field_schema(annotation: Any) -> None: - schema = SchemaCreator().for_constrained_field(FieldDefinition.from_annotation(annotation)) - assert schema +def test_create_constrained_field_schema_v2( + annotation: Any, + schema_creator: SchemaCreator, + plugin: PydanticSchemaPlugin, +) -> None: + class Model(pydantic_v2.BaseModel): + field: annotation + + assert schema_creator.for_plugin(FieldDefinition.from_annotation(Model), plugin).properties["field"] # type: ignore[index, union-attr] @pytest.mark.parametrize("cls", (PydanticPerson, PydanticDataclassPerson, PydanticV1Person, PydanticV1DataclassPerson)) @@ -366,7 +465,7 @@ def handler(data: cls) -> cls: "first_name": {"type": "string"}, "last_name": {"type": "string"}, "id": {"type": "string"}, - "optional": {"oneOf": [{"type": "null"}, {"type": "string"}]}, + "optional": {"oneOf": [{"type": "string"}, {"type": "null"}]}, "complex": { "type": "object", "additionalProperties": { @@ -377,11 +476,11 @@ def handler(data: cls) -> cls: "union": {"oneOf": [{"type": "integer"}, {"items": {"type": "string"}, "type": "array"}]}, "pets": { "oneOf": [ - {"type": "null"}, { "items": {"$ref": "#/components/schemas/DataclassPet"}, "type": "array", }, + {"type": "null"}, ] }, }, @@ -391,8 +490,7 @@ def handler(data: cls) -> cls: } -@pytest.mark.parametrize("create_examples", (True, False)) -def test_schema_generation_v1(create_examples: bool) -> None: +def test_schema_generation_v1() -> None: class Lookup(pydantic_v1.BaseModel): id: Annotated[ str, @@ -401,35 +499,30 @@ class Lookup(pydantic_v1.BaseModel): max_length=16, description="A unique identifier", example="e4eaaaf2-d142-11e1-b3e4-080027620cdd", # pyright: ignore + examples=["31", "32"], ), ] + with_title: str = pydantic_v1.Field(title="WITH_title") @post("/example") async def example_route() -> Lookup: - return Lookup(id="1234567812345678") - - with create_test_client( - route_handlers=[example_route], - openapi_config=OpenAPIConfig( - title="Example API", - version="1.0.0", - create_examples=create_examples, - ), - signature_namespace={"Lookup": Lookup}, - ) as client: - response = client.get("/schema/openapi.json") - assert response.status_code == HTTP_200_OK - assert response.json()["components"]["schemas"]["test_schema_generation_v1.Lookup"]["properties"]["id"] == { - "description": "A unique identifier", - "examples": ["e4eaaaf2-d142-11e1-b3e4-080027620cdd"], - "maxLength": 16, - "minLength": 12, - "type": "string", - } + return Lookup(id="1234567812345678", with_title="1") + + app = Litestar([example_route]) + schema = app.openapi_schema.to_schema() + lookup_schema = schema["components"]["schemas"]["test_schema_generation_v1.Lookup"]["properties"] + + assert lookup_schema["id"] == { + "description": "A unique identifier", + "examples": ["e4eaaaf2-d142-11e1-b3e4-080027620cdd", "31", "32"], + "maxLength": 16, + "minLength": 12, + "type": "string", + } + assert lookup_schema["with_title"] == {"title": "WITH_title", "type": "string"} -@pytest.mark.parametrize("create_examples", (True, False)) -def test_schema_generation_v2(create_examples: bool) -> None: +def test_schema_generation_v2() -> None: class Lookup(pydantic_v2.BaseModel): id: Annotated[ str, @@ -437,40 +530,83 @@ class Lookup(pydantic_v2.BaseModel): min_length=12, max_length=16, description="A unique identifier", - json_schema_extra={"example": "e4eaaaf2-d142-11e1-b3e4-080027620cdd"}, + # we expect these examples to be merged + json_schema_extra={"example": "e4eaaaf2-d142-11e1-b3e4-080027620cdd", "examples": ["31"]}, + examples=["32"], ), ] + # title should work if given on the field + with_title: str = pydantic_v2.Field(title="WITH_title") + # or as an extra + with_extra_title: str = pydantic_v2.Field(json_schema_extra={"title": "WITH_extra"}) + # moreover, we allow json_schema_extra to use names that exactly match the JSONSchema spec + without_duplicates: List[str] = pydantic_v2.Field(json_schema_extra={"uniqueItems": True}) @post("/example") async def example_route() -> Lookup: - return Lookup(id="1234567812345678") + return Lookup(id="1234567812345678", with_title="1", with_extra_title="2", without_duplicates=[]) + + app = Litestar([example_route]) + schema = app.openapi_schema.to_schema() + lookup_schema = schema["components"]["schemas"]["test_schema_generation_v2.Lookup"]["properties"] + + assert lookup_schema["id"] == { + "description": "A unique identifier", + "examples": ["e4eaaaf2-d142-11e1-b3e4-080027620cdd", "31", "32"], + "maxLength": 16, + "minLength": 12, + "type": "string", + } + assert lookup_schema["with_title"] == {"title": "WITH_title", "type": "string"} + assert lookup_schema["with_extra_title"] == {"title": "WITH_extra", "type": "string"} + assert lookup_schema["without_duplicates"] == {"type": "array", "items": {"type": "string"}, "uniqueItems": True} + - with create_test_client( - route_handlers=[example_route], +def test_create_examples(pydantic_version: PydanticVersion) -> None: + lib = pydantic_v1 if pydantic_version == "v1" else pydantic_v2 + + class Model(lib.BaseModel): # type: ignore[name-defined, misc] + foo: str = lib.Field(examples=["32"]) + bar: str + + @get("/example") + async def handler() -> Model: + return Model(foo="1", bar="2") + + app = Litestar( + [handler], openapi_config=OpenAPIConfig( - title="Example API", - version="1.0.0", - create_examples=create_examples, + title="Test", + version="0", + create_examples=True, ), - signature_namespace={"Lookup": Lookup}, - ) as client: - response = client.get("/schema/openapi.json") - assert response.status_code == HTTP_200_OK - assert response.json()["components"]["schemas"]["test_schema_generation_v2.Lookup"]["properties"]["id"] == { - "description": "A unique identifier", - "examples": ["e4eaaaf2-d142-11e1-b3e4-080027620cdd"], - "maxLength": 16, - "minLength": 12, - "type": "string", - } + ) + schema = app.openapi_schema.to_schema() + lookup_schema = schema["components"]["schemas"]["test_create_examples.Model"]["properties"] + + assert lookup_schema["foo"]["examples"] == ["32"] + assert lookup_schema["bar"]["examples"] + + +def test_v2_json_schema_extra_callable_raises() -> None: + class Model(pydantic_v2.BaseModel): + field: str = pydantic_v2.Field(json_schema_extra=lambda e: None) + + @get("/example") + def handler() -> Model: + return Model(field="1") + + app = Litestar([handler]) + with pytest.raises(ValueError, match="Callables not supported"): + app.openapi_schema def test_schema_by_alias(base_model: AnyBaseModelType, pydantic_version: PydanticVersion) -> None: - class RequestWithAlias(base_model): # type: ignore[misc, valid-type] - first: str = (pydantic_v1.Field if pydantic_version == "v1" else pydantic_v2.Field)(alias="second") # type: ignore[operator] + class RequestWithAlias(base_model): # type: ignore[valid-type,misc] + first: str = (pydantic_v1.Field if pydantic_version == "v1" else pydantic_v2.Field)(alias="second") - class ResponseWithAlias(base_model): # type: ignore[misc, valid-type] - first: str = (pydantic_v1.Field if pydantic_version == "v1" else pydantic_v2.Field)(alias="second") # type: ignore[operator] + class ResponseWithAlias(base_model): # type: ignore[valid-type,misc] + first: str = (pydantic_v1.Field if pydantic_version == "v1" else pydantic_v2.Field)(alias="second") @post("/", signature_types=[RequestWithAlias, ResponseWithAlias]) def handler(data: RequestWithAlias) -> ResponseWithAlias: @@ -502,10 +638,10 @@ def handler(data: RequestWithAlias) -> ResponseWithAlias: def test_schema_by_alias_plugin_override(base_model: AnyBaseModelType, pydantic_version: PydanticVersion) -> None: class RequestWithAlias(base_model): # type: ignore[misc, valid-type] - first: str = (pydantic_v1.Field if pydantic_version == "v1" else pydantic_v2.Field)(alias="second") # type: ignore[operator] + first: str = (pydantic_v1.Field if pydantic_version == "v1" else pydantic_v2.Field)(alias="second") class ResponseWithAlias(base_model): # type: ignore[misc, valid-type] - first: str = (pydantic_v1.Field if pydantic_version == "v1" else pydantic_v2.Field)(alias="second") # type: ignore[operator] + first: str = (pydantic_v1.Field if pydantic_version == "v1" else pydantic_v2.Field)(alias="second") @post("/", signature_types=[RequestWithAlias, ResponseWithAlias]) def handler(data: RequestWithAlias) -> ResponseWithAlias: @@ -581,7 +717,7 @@ class Model(pydantic_v2.BaseModel): assert value.examples == ["example"] -def test_create_schema_for_field_v2__examples() -> None: +def test_create_schema_for_field_v2_examples() -> None: class Model(pydantic_v2.BaseModel): value: str = pydantic_v2.Field( title="title", description="description", max_length=16, json_schema_extra={"examples": ["example"]} diff --git a/tests/unit/test_contrib/test_pydantic/test_plugin_serialization.py b/tests/unit/test_plugins/test_pydantic/test_plugin_serialization.py similarity index 98% rename from tests/unit/test_contrib/test_pydantic/test_plugin_serialization.py rename to tests/unit/test_plugins/test_pydantic/test_plugin_serialization.py index 528820e709..9bcccfd3ec 100644 --- a/tests/unit/test_contrib/test_pydantic/test_plugin_serialization.py +++ b/tests/unit/test_plugins/test_pydantic/test_plugin_serialization.py @@ -13,9 +13,8 @@ from pydantic.v1.color import Color as ColorV1 from pydantic_extra_types.color import Color as ColorV2 -from litestar.contrib.pydantic import _model_dump, _model_dump_json -from litestar.contrib.pydantic.pydantic_init_plugin import PydanticInitPlugin from litestar.exceptions import SerializationException +from litestar.plugins.pydantic import PydanticInitPlugin, _model_dump, _model_dump_json from litestar.serialization import ( decode_json, decode_msgpack, @@ -158,7 +157,7 @@ def model(pydantic_version: PydanticVersion) -> ModelV1 | ModelV2: ) return ModelV2( path=Path("example"), - email_str=pydantic_v2.parse_obj_as(pydantic_v2.EmailStr, "info@example.org"), + email_str=pydantic_v2.parse_obj_as(pydantic_v2.EmailStr, "info@example.org"), # pyright: ignore[reportArgumentType] name_email=pydantic_v2.NameEmail("info", "info@example.org"), color=ColorV2("rgb(255, 255, 255)"), bytesize=pydantic_v2.ByteSize(100), diff --git a/tests/unit/test_contrib/test_pydantic/test_pydantic_dto_factory.py b/tests/unit/test_plugins/test_pydantic/test_pydantic_dto_factory.py similarity index 51% rename from tests/unit/test_contrib/test_pydantic/test_pydantic_dto_factory.py rename to tests/unit/test_plugins/test_pydantic/test_pydantic_dto_factory.py index ee2ecdc467..e609bbaddb 100644 --- a/tests/unit/test_contrib/test_pydantic/test_pydantic_dto_factory.py +++ b/tests/unit/test_plugins/test_pydantic/test_pydantic_dto_factory.py @@ -1,15 +1,16 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from dataclasses import replace +from typing import TYPE_CHECKING, Callable, Optional +from unittest.mock import ANY import pydantic as pydantic_v2 import pytest from pydantic import v1 as pydantic_v1 from typing_extensions import Annotated -from litestar.contrib.pydantic import PydanticDTO -from litestar.dto import DTOField, dto_field -from litestar.dto.data_structures import DTOFieldDefinition +from litestar.dto import DTOField, DTOFieldDefinition, Mark, dto_field +from litestar.plugins.pydantic import PydanticDTO from litestar.typing import FieldDefinition from . import PydanticVersion @@ -18,6 +19,88 @@ from typing import Callable +@pytest.fixture +def expected_field_defs(int_factory: Callable[[], int]) -> list[DTOFieldDefinition]: + return [ + DTOFieldDefinition.from_field_definition( + field_definition=FieldDefinition.from_kwarg( + annotation=int, + name="a", + ), + model_name=ANY, + default_factory=None, + dto_field=DTOField(), + passthrough_constraints=False, + ), + replace( + DTOFieldDefinition.from_field_definition( + field_definition=FieldDefinition.from_kwarg( + annotation=int, + name="b", + ), + model_name=ANY, + default_factory=None, + dto_field=DTOField(mark=Mark.READ_ONLY), + ), + metadata=ANY, + type_wrappers=ANY, + raw=ANY, + kwarg_definition=ANY, + passthrough_constraints=False, + ), + replace( + DTOFieldDefinition.from_field_definition( + field_definition=FieldDefinition.from_kwarg( + annotation=int, + name="c", + ), + model_name=ANY, + default_factory=None, + dto_field=DTOField(), + ), + metadata=ANY, + type_wrappers=ANY, + raw=ANY, + kwarg_definition=ANY, + passthrough_constraints=False, + ), + replace( + DTOFieldDefinition.from_field_definition( + field_definition=FieldDefinition.from_kwarg( + annotation=int, + name="d", + default=1, + ), + model_name=ANY, + default_factory=None, + dto_field=DTOField(), + ), + metadata=ANY, + type_wrappers=ANY, + raw=ANY, + kwarg_definition=ANY, + passthrough_constraints=False, + ), + replace( + DTOFieldDefinition.from_field_definition( + field_definition=FieldDefinition.from_kwarg( + annotation=Optional[int], + name="e", + ), + model_name=ANY, + default_factory=int_factory, + dto_field=DTOField(), + ), + default=None, + metadata=ANY, + type_wrappers=ANY, + raw=ANY, + kwarg_definition=ANY, + passthrough_constraints=False, + ), + ] + + def test_field_definition_generation_v1( int_factory: Callable[[], int], expected_field_defs: list[DTOFieldDefinition], @@ -87,7 +170,7 @@ class Model(pydantic_v1.BaseModel): # pyright: ignore else: class Model(pydantic_v2.BaseModel): # type: ignore[no-redef] - a: int = pydantic_v2.Field(**dto_field("read-only")) # type: ignore[arg-type, pydantic-field] + a: int = pydantic_v2.Field(**dto_field("read-only")) # type: ignore[call-overload] with pytest.warns(DeprecationWarning): next(PydanticDTO.generate_field_definitions(Model)) diff --git a/tests/unit/test_contrib/test_pydantic/test_schema_plugin.py b/tests/unit/test_plugins/test_pydantic/test_schema_plugin.py similarity index 83% rename from tests/unit/test_contrib/test_pydantic/test_schema_plugin.py rename to tests/unit/test_plugins/test_pydantic/test_schema_plugin.py index df43853fd6..aee70c84a4 100644 --- a/tests/unit/test_contrib/test_pydantic/test_schema_plugin.py +++ b/tests/unit/test_plugins/test_pydantic/test_schema_plugin.py @@ -8,10 +8,11 @@ from pydantic.v1.generics import GenericModel from typing_extensions import Annotated +from litestar import Litestar, post from litestar._openapi.schema_generation import SchemaCreator -from litestar.contrib.pydantic.pydantic_schema_plugin import PydanticSchemaPlugin from litestar.openapi.spec import OpenAPIType from litestar.openapi.spec.schema import Schema +from litestar.plugins.pydantic import PydanticSchemaPlugin from litestar.typing import FieldDefinition from litestar.utils.helpers import get_name from tests.helpers import get_schema_for_field_definition @@ -37,7 +38,7 @@ def test_schema_generation_with_generic_classes(model: Type[Union[PydanticV1Gene field_definition = FieldDefinition.from_kwarg(name=get_name(cls), annotation=cls) properties = get_schema_for_field_definition(field_definition, plugins=[PydanticSchemaPlugin()]).properties expected_foo_schema = Schema(type=OpenAPIType.INTEGER) - expected_optional_foo_schema = Schema(one_of=[Schema(type=OpenAPIType.NULL), Schema(type=OpenAPIType.INTEGER)]) + expected_optional_foo_schema = Schema(one_of=[Schema(type=OpenAPIType.INTEGER), Schema(type=OpenAPIType.NULL)]) assert properties assert properties["foo"] == expected_foo_schema @@ -88,7 +89,7 @@ class Config: _field: str = pydantic_v1.PrivateAttr() # include an invalid annotation here to ensure we never touch those fields - _underscore_field: "foo" # type: ignore[name-defined] # noqa: F821 + _underscore_field: str = "foo" class V1GenericModelWithPrivateFields(pydantic_v1.generics.GenericModel, Generic[T]): # pyright: ignore @@ -97,19 +98,19 @@ class Config: _field: str = pydantic_v1.PrivateAttr() # include an invalid annotation here to ensure we never touch those fields - _underscore_field: "foo" # type: ignore[name-defined] # noqa: F821 + _underscore_field: str = "foo" class V2ModelWithPrivateFields(pydantic_v2.BaseModel): _field: str = pydantic_v2.PrivateAttr() # include an invalid annotation here to ensure we never touch those fields - _underscore_field: "foo" # type: ignore[name-defined] # noqa: F821 + _underscore_field: str = "foo" class V2GenericModelWithPrivateFields(pydantic_v2.BaseModel, Generic[T]): _field: str = pydantic_v2.PrivateAttr() # include an invalid annotation here to ensure we never touch those fields - _underscore_field: "foo" # type: ignore[name-defined] # noqa: F821 + _underscore_field: str = "foo" @pytest.mark.parametrize( @@ -127,3 +128,21 @@ def test_exclude_private_fields(model_class: Type[Union[pydantic_v1.BaseModel, p FieldDefinition.from_annotation(model_class), schema_creator=SchemaCreator(plugins=[PydanticSchemaPlugin()]) ) assert not schema.properties + + +def test_v1_constrained_str_with_default_factory_does_not_generate_title() -> None: + # https://github.com/litestar-org/litestar/issues/3710 + class Model(pydantic_v1.BaseModel): + test_str: str = pydantic_v1.Field(default_factory=str, max_length=600) + + @post(path="/") + async def test(data: Model) -> str: + return "success" + + schema = Litestar(route_handlers=[test]).openapi_schema.to_schema() + assert ( + "title" + not in schema["components"]["schemas"][ + "test_v1_constrained_str_with_default_factory_does_not_generate_title.Model" + ]["properties"]["test_str"]["oneOf"][1] + ) diff --git a/tests/unit/test_contrib/test_pydantic/test_utils.py b/tests/unit/test_plugins/test_pydantic/test_utils.py similarity index 92% rename from tests/unit/test_contrib/test_pydantic/test_utils.py rename to tests/unit/test_plugins/test_pydantic/test_utils.py index 470a42807b..80d9c92ad2 100644 --- a/tests/unit/test_contrib/test_pydantic/test_utils.py +++ b/tests/unit/test_plugins/test_pydantic/test_utils.py @@ -3,7 +3,7 @@ import pytest from pydantic import BaseModel -from litestar.contrib.pydantic.utils import pydantic_get_type_hints_with_generics_resolved +from litestar.plugins.pydantic.utils import pydantic_get_type_hints_with_generics_resolved T = TypeVar("T") diff --git a/tests/unit/test_plugins/test_sqlalchemy.py b/tests/unit/test_plugins/test_sqlalchemy.py index 69512e1a9b..8100f8377d 100644 --- a/tests/unit/test_plugins/test_sqlalchemy.py +++ b/tests/unit/test_plugins/test_sqlalchemy.py @@ -1,3 +1,4 @@ +import pytest from advanced_alchemy.extensions import litestar as sa_litestar from advanced_alchemy.extensions.litestar import base as sa_base from advanced_alchemy.extensions.litestar import exceptions as sa_exceptions @@ -38,12 +39,13 @@ def test_re_exports() -> None: assert sqlalchemy.SyncSessionConfig is sa_litestar.SyncSessionConfig # deprecated, to be removed later - assert sqlalchemy.AuditColumns is sa_base.AuditColumns - assert sqlalchemy.BigIntAuditBase is sa_base.BigIntAuditBase - assert sqlalchemy.BigIntBase is sa_base.BigIntBase - assert sqlalchemy.BigIntPrimaryKey is sa_base.BigIntPrimaryKey - assert sqlalchemy.CommonTableAttributes is sa_base.CommonTableAttributes - assert sqlalchemy.UUIDAuditBase is sa_base.UUIDAuditBase - assert sqlalchemy.UUIDBase is sa_base.UUIDBase - assert sqlalchemy.UUIDPrimaryKey is sa_base.UUIDPrimaryKey - assert sqlalchemy.orm_registry is sa_base.orm_registry + with pytest.warns(DeprecationWarning): + assert sqlalchemy.AuditColumns is sa_base.AuditColumns + assert sqlalchemy.BigIntAuditBase is sa_base.BigIntAuditBase + assert sqlalchemy.BigIntBase is sa_base.BigIntBase + assert sqlalchemy.BigIntPrimaryKey is sa_base.BigIntPrimaryKey + assert sqlalchemy.CommonTableAttributes is sa_base.CommonTableAttributes + assert sqlalchemy.UUIDAuditBase is sa_base.UUIDAuditBase + assert sqlalchemy.UUIDBase is sa_base.UUIDBase + assert sqlalchemy.UUIDPrimaryKey is sa_base.UUIDPrimaryKey + assert sqlalchemy.orm_registry is sa_base.orm_registry diff --git a/tests/unit/test_response/test_file_response.py b/tests/unit/test_response/test_file_response.py index 8e2a442630..bc416772cb 100644 --- a/tests/unit/test_response/test_file_response.py +++ b/tests/unit/test_response/test_file_response.py @@ -1,4 +1,5 @@ import os +from datetime import datetime, timezone from email.utils import formatdate from os import stat, urandom from pathlib import Path @@ -13,7 +14,7 @@ from litestar.exceptions import ImproperlyConfiguredException from litestar.file_system import BaseLocalFileSystem, FileSystemAdapter from litestar.response.file import ASGIFileResponse, File, async_file_iterator -from litestar.status_codes import HTTP_200_OK +from litestar.status_codes import HTTP_200_OK, HTTP_500_INTERNAL_SERVER_ERROR from litestar.testing import create_test_client from litestar.types import FileSystemProtocol @@ -95,6 +96,118 @@ def handler() -> File: assert response.headers["last-modified"].lower() == formatdate(path.stat().st_mtime, usegmt=True).lower() +@pytest.mark.parametrize( + "mtime,expected_last_modified", + [ + pytest.param( + datetime(2000, 1, 2, 3, 4, 5, tzinfo=timezone.utc).timestamp(), + "Sun, 02 Jan 2000 03:04:05 GMT", + id="timestamp", + ), + pytest.param( + datetime(2000, 1, 2, 3, 4, 5, tzinfo=timezone.utc), "Sun, 02 Jan 2000 03:04:05 GMT", id="datetime" + ), + pytest.param( + datetime(2000, 1, 2, 3, 4, 5, tzinfo=timezone.utc).isoformat(), + "Sun, 02 Jan 2000 03:04:05 GMT", + id="isoformat", + ), + ], +) +@pytest.mark.parametrize( + "mtime_key", + [ + "mtime", + "ctime", + "Last-Modified", + "updated_at", + "modification_time", + "last_changed", + "change_time", + "last_modified", + "last_updated", + "timestamp", + ], +) +def test_file_response_last_modified_file_info_formats( + tmpdir: Path, mtime: Any, mtime_key: str, expected_last_modified: str +) -> None: + path = Path(tmpdir / "file.txt") + path.write_bytes(b"") + file_info = {"name": "file.txt", "size": 0, "type": "file", mtime_key: mtime} + + @get("/") + def handler() -> File: + return File( + path=path, + filename="image.png", + file_info=file_info, # type: ignore[arg-type] + ) + + with create_test_client(handler) as client: + response = client.get("/") + assert response.status_code == HTTP_200_OK + assert response.headers["last-modified"].lower() == expected_last_modified.lower() + + +def test_file_response_last_modified_unsupported_mtime_type(tmpdir: Path) -> None: + path = Path(tmpdir / "file.txt") + path.write_bytes(b"") + file_info = {"name": "file.txt", "size": 0, "type": "file", "last_updated": object()} + + @get("/") + def handler() -> File: + return File( + path=path, + filename="image.png", + file_info=file_info, # type: ignore[arg-type] + ) + + with create_test_client(handler) as client: + response = client.get("/") + assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR + assert "last-modified" not in response.headers + + +def test_file_response_last_modified_mtime_not_given(tmpdir: Path) -> None: + path = Path(tmpdir / "file.txt") + path.write_bytes(b"") + file_info = {"name": "file.txt", "size": 0, "type": "file"} + + @get("/") + def handler() -> File: + return File( + path=path, + filename="image.png", + file_info=file_info, # type: ignore[arg-type] + ) + + with create_test_client(handler) as client: + response = client.get("/") + assert response.status_code == HTTP_200_OK + assert "last-modified" not in response.headers + + +def test_file_response_etag_without_mtime(tmpdir: Path) -> None: + path = Path(tmpdir / "file.txt") + path.write_bytes(b"") + file_info = {"name": "file.txt", "size": 0, "type": "file"} + + @get("/") + def handler() -> File: + return File( + path=path, + filename="image.png", + file_info=file_info, # type: ignore[arg-type] + ) + + with create_test_client(handler) as client: + response = client.get("/") + assert response.status_code == HTTP_200_OK + # we expect etag to only have 2 parts here because no mtime was given + assert len(response.headers.get("etag", "").split("-")) == 2 + + async def test_file_response_with_directory_raises_error(tmpdir: Path) -> None: with pytest.raises(ImproperlyConfiguredException): asgi_response = ASGIFileResponse(file_path=tmpdir, filename="example.png") diff --git a/tests/unit/test_response/test_redirect_response.py b/tests/unit/test_response/test_redirect_response.py index fc1919697a..4058a85904 100644 --- a/tests/unit/test_response/test_redirect_response.py +++ b/tests/unit/test_response/test_redirect_response.py @@ -4,11 +4,14 @@ their API. """ -from typing import TYPE_CHECKING, Optional +from __future__ import annotations + +from typing import TYPE_CHECKING import pytest from litestar import get +from litestar.datastructures import MultiDict from litestar.exceptions import ImproperlyConfiguredException from litestar.response.base import ASGIResponse from litestar.response.redirect import ASGIRedirectResponse, Redirect @@ -20,7 +23,7 @@ def test_redirect_response() -> None: - async def app(scope: "Scope", receive: "Receive", send: "Send") -> None: + async def app(scope: Scope, receive: Receive, send: Send) -> None: if scope["path"] == "/": response = ASGIResponse(body=b"hello, world", media_type="text/plain") else: @@ -34,7 +37,7 @@ async def app(scope: "Scope", receive: "Receive", send: "Send") -> None: def test_quoting_redirect_response() -> None: - async def app(scope: "Scope", receive: "Receive", send: "Send") -> None: + async def app(scope: Scope, receive: Receive, send: Send) -> None: if scope["path"] == "/test/": response = ASGIResponse(body=b"hello, world", media_type="text/plain") else: @@ -48,7 +51,7 @@ async def app(scope: "Scope", receive: "Receive", send: "Send") -> None: def test_redirect_response_content_length_header() -> None: - async def app(scope: "Scope", receive: "Receive", send: "Send") -> None: + async def app(scope: Scope, receive: Receive, send: Send) -> None: if scope["path"] == "/": response = ASGIResponse(body=b"hello", media_type="text/plain") else: @@ -67,7 +70,7 @@ def test_redirect_response_status_validation() -> None: def test_redirect_response_html_media_type() -> None: - async def app(scope: "Scope", receive: "Receive", send: "Send") -> None: + async def app(scope: Scope, receive: Receive, send: Send) -> None: if scope["path"] == "/": response = ASGIResponse(body=b"hello") else: @@ -95,7 +98,7 @@ def test_redirect_response_media_type_validation() -> None: (308, 308), ], ) -def test_redirect_dynamic_status_code(status_code: Optional[int], expected_status_code: int) -> None: +def test_redirect_dynamic_status_code(status_code: int | None, expected_status_code: int) -> None: @get("/") def handler() -> Redirect: return Redirect(path="/something-else", status_code=status_code) # type: ignore[arg-type] @@ -107,8 +110,22 @@ def handler() -> Redirect: assert res.status_code == expected_status_code +@pytest.mark.parametrize( + "query_params", [{"single": "a", "list": ["b", "c"]}, MultiDict([("single", "a"), ("list", "b"), ("list", "c")])] +) +def test_redirect_with_query_params(query_params: dict[str, str | list[str]] | MultiDict) -> None: + @get("/") + def handler() -> Redirect: + return Redirect(path="/something-else", query_params=query_params) + + with create_test_client([handler]) as client: + location_header = client.get("/", follow_redirects=False).headers["location"] + expected = "/something-else?single=a&list=b&list=c" + assert location_header == expected + + @pytest.mark.parametrize("handler_status_code", [301, 307, None]) -def test_redirect(handler_status_code: Optional[int]) -> None: +def test_redirect(handler_status_code: int | None) -> None: @get("/", status_code=handler_status_code) def handler() -> Redirect: return Redirect(path="/something-else", status_code=handler_status_code) # type: ignore[arg-type] diff --git a/tests/unit/test_security/test_jwt/test_auth.py b/tests/unit/test_security/test_jwt/test_auth.py index e314350483..d3aff92829 100644 --- a/tests/unit/test_security/test_jwt/test_auth.py +++ b/tests/unit/test_security/test_jwt/test_auth.py @@ -2,19 +2,21 @@ import secrets import string from datetime import datetime, timedelta, timezone -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple from uuid import uuid4 +import jwt import msgspec import pytest from hypothesis import given, settings from hypothesis.strategies import dictionaries, integers, none, one_of, sampled_from, text, timedeltas +from typing_extensions import TypeAlias from litestar import Litestar, Request, Response, get from litestar.security.jwt import JWTAuth, JWTCookieAuth, OAuth2PasswordBearerAuth, Token from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_401_UNAUTHORIZED from litestar.stores.memory import MemoryStore -from litestar.testing import create_test_client +from litestar.testing import TestClient, create_test_client from tests.models import User, UserFactory if TYPE_CHECKING: @@ -545,3 +547,296 @@ def handler() -> Response[User]: response = client.get("/") assert response.status_code == HTTP_201_CREATED assert response.json() is None + + +async def test_jwt_auth_validation_error_returns_not_authorized() -> None: + # if the value of a field has an invalid type, msgspec will raise a 'ValidationError'. + # this should still result in a '401' status response + async def retrieve_user_handler(token: Token, _: "ASGIConnection") -> Any: + return object() + + token_secret = secrets.token_hex() + + jwt_auth = JWTAuth[Any]( + token_secret=token_secret, + retrieve_user_handler=retrieve_user_handler, + ) + + @get("/", middleware=[jwt_auth.middleware]) + def handler() -> None: + return None + + header = jwt_auth.format_auth_header( + jwt.encode( + { + "sub": "foo", + "exp": (datetime.now() + timedelta(days=1)).timestamp(), + "iat": datetime.now().timestamp(), + "iss": {"foo": "bar"}, + }, + key=token_secret, + ), + ) + + with create_test_client(route_handlers=[handler]) as client: + response = client.get("/", headers={"Authorization": header}) + assert response.status_code == 401 + + +@pytest.mark.parametrize( + "accepted_issuers, signing_issuer, expected_status_code", + [ + (["issuer_a"], "issuer_a", 200), + (["issuer_a", "issuer_b"], "issuer_a", 200), + (["issuer_a", "issuer_b"], "issuer_b", 200), + (["issuer_b"], "issuer_a", 401), + ], +) +@pytest.mark.parametrize("auth_cls", [JWTAuth, JWTCookieAuth, OAuth2PasswordBearerAuth]) +async def test_jwt_auth_verify_issuer( + auth_cls: Any, + accepted_issuers: List[str], + signing_issuer: str, + expected_status_code: int, +) -> None: + async def retrieve_user_handler(token: Token, _: "ASGIConnection") -> Any: + return object() + + token_secret = secrets.token_hex() + + if auth_cls is OAuth2PasswordBearerAuth: + jwt_auth = auth_cls( + token_secret=token_secret, + retrieve_user_handler=retrieve_user_handler, + token_url="http://testserver.local", + accepted_issuers=accepted_issuers, + ) + else: + jwt_auth = auth_cls[Any]( + token_secret=token_secret, + retrieve_user_handler=retrieve_user_handler, + accepted_issuers=accepted_issuers, + ) + + @get("/", middleware=[jwt_auth.middleware]) + def handler() -> None: + return None + + header = jwt_auth.format_auth_header( + jwt_auth.create_token( + identifier="foo", + token_issuer=signing_issuer, + ), + ) + + with create_test_client(route_handlers=[handler]) as client: + response = client.get("/", headers={"Authorization": header}) + assert response.status_code == expected_status_code + + +@pytest.mark.parametrize( + "accepted_audiences, token_audience, expected_status_code", + [ + (["audience_a"], "audience_a", 200), + (["audience_a", "audience_b"], "audience_a", 200), + (["audience_a", "audience_b"], "audience_b", 200), + (["audience_b"], "audience_a", 401), + ], +) +@pytest.mark.parametrize("auth_cls", [JWTAuth, JWTCookieAuth, OAuth2PasswordBearerAuth]) +async def test_jwt_auth_verify_audience( + auth_cls: Any, + accepted_audiences: List[str], + token_audience: str, + expected_status_code: int, +) -> None: + async def retrieve_user_handler(token: Token, _: "ASGIConnection") -> Any: + return object() + + token_secret = secrets.token_hex() + + if auth_cls is OAuth2PasswordBearerAuth: + jwt_auth = auth_cls( + token_secret=token_secret, + retrieve_user_handler=retrieve_user_handler, + token_url="http://testserver.local", + accepted_audiences=accepted_audiences, + ) + else: + jwt_auth = auth_cls[Any]( + token_secret=token_secret, + retrieve_user_handler=retrieve_user_handler, + accepted_audiences=accepted_audiences, + ) + + @get("/", middleware=[jwt_auth.middleware]) + def handler() -> None: + return None + + header = jwt_auth.format_auth_header( + jwt_auth.create_token( + identifier="foo", + token_audience=token_audience, + ), + ) + + with create_test_client(route_handlers=[handler]) as client: + response = client.get("/", headers={"Authorization": header}) + assert response.status_code == expected_status_code + + +CreateJWTApp: TypeAlias = Callable[..., Tuple[JWTAuth, TestClient]] + + +@pytest.fixture() +def create_jwt_app(auth_cls: Any, request: pytest.FixtureRequest) -> CreateJWTApp: + def create(**kwargs: Any) -> Tuple[JWTAuth, TestClient]: + async def retrieve_user_handler(token: Token, _: "ASGIConnection") -> Any: + return object() + + if auth_cls is OAuth2PasswordBearerAuth: + jwt_auth = auth_cls( + token_secret=secrets.token_hex(), + retrieve_user_handler=retrieve_user_handler, + token_url="http://testserver.local", + **kwargs, + ) + else: + jwt_auth = auth_cls[Any]( + token_secret=secrets.token_hex(), retrieve_user_handler=retrieve_user_handler, **kwargs + ) + + @get("/", middleware=[jwt_auth.middleware]) + def handler() -> None: + return None + + client = create_test_client(route_handlers=[handler]).__enter__() + request.addfinalizer(client.__exit__) + + return jwt_auth, client + + return create + + +@pytest.fixture(params=[JWTAuth, JWTCookieAuth, OAuth2PasswordBearerAuth]) +def auth_cls(request: pytest.FixtureRequest) -> Any: + return request.param + + +@pytest.mark.parametrize( + "accepted_audiences, token_audience, expected_status_code", + [ + (["audience_a"], "audience_a", 200), + ("audience_a", "audience_a", 200), + (["audience_a"], ["audience_a", "audience_b"], 401), + (["audience_b"], "audience_a", 401), + ], +) +async def test_jwt_auth_strict_audience( + accepted_audiences: List[str], + token_audience: str, + expected_status_code: int, + create_jwt_app: CreateJWTApp, +) -> None: + jwt_auth, client = create_jwt_app(strict_audience=True, accepted_audiences=accepted_audiences) + + header = jwt_auth.format_auth_header( + jwt_auth.create_token( + identifier="foo", + token_audience=token_audience, + ), + ) + + response = client.get("/", headers={"Authorization": header}) + assert response.status_code == expected_status_code + + +@pytest.mark.parametrize( + "require_claims, token_claims, expected_status_code", + [ + (["aud"], {"token_audience": "foo"}, 200), + (["aud"], {}, 401), + ([], {}, 200), + ], +) +async def test_jwt_auth_require_claims( + require_claims: List[str], + token_claims: Dict[str, str], + expected_status_code: int, + create_jwt_app: CreateJWTApp, +) -> None: + jwt_auth, client = create_jwt_app(require_claims=require_claims) + + header = jwt_auth.format_auth_header( + jwt_auth.create_token( + identifier="foo", + **token_claims, # type: ignore[arg-type] + ), + ) + + response = client.get("/", headers={"Authorization": header}) + assert response.status_code == expected_status_code + + +@pytest.mark.parametrize( + "token_expiration, verify_expiry, expected_status_code", + [ + pytest.param((datetime.now(tz=timezone.utc) + timedelta(days=1)).timestamp(), True, 200, id="valid-verify"), + pytest.param((datetime.now(tz=timezone.utc) + timedelta(days=1)).timestamp(), False, 200, id="valid-no_verify"), + pytest.param( + (datetime.now(tz=timezone.utc) - timedelta(days=1)).timestamp(), False, 200, id="invalid-no_verify" + ), + pytest.param((datetime.now(tz=timezone.utc) - timedelta(days=1)).timestamp(), True, 401, id="invalid-verify"), + ], +) +async def test_jwt_auth_verify_exp( + token_expiration: datetime, + verify_expiry: bool, + expected_status_code: int, + create_jwt_app: CreateJWTApp, +) -> None: + @dataclasses.dataclass + class CustomToken(Token): + def __post_init__(self) -> None: + pass + + jwt_auth, client = create_jwt_app(verify_expiry=verify_expiry, token_cls=CustomToken) + + header = jwt_auth.format_auth_header( + CustomToken( + sub="foo", + exp=token_expiration, + ).encode(jwt_auth.token_secret, jwt_auth.algorithm), + ) + + response = client.get("/", headers={"Authorization": header}) + assert response.status_code == expected_status_code + + +@pytest.mark.parametrize( + "token_nbf, verify_not_before, expected_status_code", + [ + pytest.param((datetime.now(tz=timezone.utc) - timedelta(days=1)).timestamp(), True, 200, id="valid-verify"), + pytest.param((datetime.now(tz=timezone.utc) - timedelta(days=1)).timestamp(), False, 200, id="valid-no_verify"), + pytest.param( + (datetime.now(tz=timezone.utc) + timedelta(days=1)).timestamp(), False, 200, id="invalid-no_verify" + ), + pytest.param((datetime.now(tz=timezone.utc) + timedelta(days=1)).timestamp(), True, 401, id="invalid-verify"), + ], +) +async def test_jwt_auth_verify_nbf( + token_nbf: datetime, + verify_not_before: bool, + expected_status_code: int, + create_jwt_app: CreateJWTApp, +) -> None: + @dataclasses.dataclass() + class CustomToken(Token): + nbf: Optional[float] = None + + jwt_auth, client = create_jwt_app(verify_not_before=verify_not_before, token_cls=CustomToken) + + header = jwt_auth.format_auth_header(jwt_auth.create_token("foo", nbf=token_nbf)) + + response = client.get("/", headers={"Authorization": header}) + assert response.status_code == expected_status_code diff --git a/tests/unit/test_security/test_jwt/test_token.py b/tests/unit/test_security/test_jwt/test_token.py index ac8c5658db..1637ab99f6 100644 --- a/tests/unit/test_security/test_jwt/test_token.py +++ b/tests/unit/test_security/test_jwt/test_token.py @@ -1,8 +1,11 @@ +from __future__ import annotations + +import dataclasses import secrets import sys from dataclasses import asdict from datetime import datetime, timedelta, timezone -from typing import Any, Dict, Optional +from typing import Any, Sequence from uuid import uuid4 import jwt @@ -12,6 +15,7 @@ from litestar.exceptions import ImproperlyConfiguredException, NotAuthorizedException from litestar.security.jwt import Token +from litestar.security.jwt.token import JWTDecodeOptions @pytest.mark.parametrize("algorithm", ["HS256", "HS384", "HS512"]) @@ -23,10 +27,10 @@ @pytest.mark.parametrize("token_extras", [None, {"email": "test@test.com"}]) def test_token( algorithm: str, - token_issuer: Optional[str], - token_audience: Optional[str], - token_unique_jwt_id: Optional[str], - token_extras: Optional[Dict[str, Any]], + token_issuer: str | None, + token_audience: str | None, + token_unique_jwt_id: str | None, + token_extras: dict[str, Any] | None, ) -> None: token_secret = secrets.token_hex() token = Token( @@ -164,3 +168,57 @@ def test_extra_fields() -> None: encoded_token = jwt.encode(payload=raw_token, key=token_secret, algorithm="HS256") token = Token.decode(encoded_token=encoded_token, secret=token_secret, algorithm="HS256") assert token.extras == {} + + +@pytest.mark.parametrize("audience", [None, ["foo", "bar"]]) +def test_strict_aud_with_multiple_audiences_raises(audience: str | list[str]) -> None: + with pytest.raises(ValueError, match="When using 'strict_audience=True'"): + Token.decode( + "", + secret="", + algorithm="HS256", + audience=audience, + strict_audience=True, + ) + + +@pytest.mark.parametrize("audience", ["foo", ["foo", "bar"]]) +def test_strict_aud_with_one_element_sequence(audience: str | list[str]) -> None: + # when validating with strict audience, PyJWT requires that the 'audience' parameter + # is passed as a string - one element lists are not allowed. Since we allow these + # generally, we convert them to a string in this case + secret = secrets.token_hex() + encoded = Token(exp=datetime.now() + timedelta(days=1), sub="foo", aud="foo").encode(secret, "HS256") + Token.decode( + encoded, + secret=secret, + algorithm="HS256", + audience=["foo"], + strict_audience=True, + ) + + +def test_custom_decode_payload() -> None: + @dataclasses.dataclass + class CustomToken(Token): + @classmethod + def decode_payload( + cls, + encoded_token: str, + secret: str, + algorithms: list[str], + issuer: list[str] | None = None, + audience: str | Sequence[str] | None = None, + options: JWTDecodeOptions | None = None, + ) -> Any: + payload = super().decode_payload( + encoded_token=encoded_token, + secret=secret, + algorithms=algorithms, + ) + payload["sub"] = "some-random-value" + return payload + + _secret = secrets.token_hex() + encoded = CustomToken(exp=datetime.now() + timedelta(days=1), sub="foo").encode(_secret, "HS256") + assert CustomToken.decode(encoded, secret=_secret, algorithm="HS256").sub == "some-random-value" diff --git a/tests/unit/test_security/test_security.py b/tests/unit/test_security/test_security.py index e727b428c5..60102196f7 100644 --- a/tests/unit/test_security/test_security.py +++ b/tests/unit/test_security/test_security.py @@ -48,6 +48,7 @@ def test_abstract_security_config_sets_dependencies(session_backend_config_memor assert client.app.dependencies.get("value") +@pytest.mark.filterwarnings("ignore:Middleware 'SessionAuthMiddleware' exclude pattern") def test_abstract_security_config_registers_route_handlers( session_backend_config_memory: ServerSideSessionConfig, ) -> None: diff --git a/tests/unit/test_signature/test_validation.py b/tests/unit/test_signature/test_validation.py index acc8f97cae..23f1cb0318 100644 --- a/tests/unit/test_signature/test_validation.py +++ b/tests/unit/test_signature/test_validation.py @@ -78,6 +78,20 @@ def test(dep: int, param: int, optional_dep: Optional[int] = Dependency()) -> No } +def test_invalid_path_parameter() -> None: + @get("/{param:int}") + def test(param: Annotated[int, Parameter(le=10)]) -> None: ... + + with create_test_client(route_handlers=[test]) as client: + response = client.get("/11") + + assert response.json() == { + "detail": "Validation failed for GET /11", + "extra": [{"key": "param", "message": "Expected `int` <= 10", "source": "path"}], + "status_code": 400, + } + + def test_client_backend_error_precedence_over_server_error() -> None: dependencies = { "dep": Provide(lambda: "thirteen", sync_to_thread=False), diff --git a/tests/unit/test_stores.py b/tests/unit/test_stores.py index f28b71017c..a37b836d52 100644 --- a/tests/unit/test_stores.py +++ b/tests/unit/test_stores.py @@ -19,9 +19,11 @@ from litestar.stores.memory import MemoryStore from litestar.stores.redis import RedisStore from litestar.stores.registry import StoreRegistry +from litestar.stores.valkey import ValkeyStore if TYPE_CHECKING: from redis.asyncio import Redis + from valkey.asyncio import Valkey from litestar.stores.base import NamespacedStore, Store @@ -31,6 +33,11 @@ def mock_redis() -> None: patch("litestar.Store.redis_backend.Redis") +@pytest.fixture() +def mock_valkey() -> None: + patch("litestar.Store.valkey_backend.Valkey") + + async def test_get(store: Store) -> None: key = "key" value = b"value" @@ -70,6 +77,8 @@ async def test_expires(store: Store, frozen_datetime: Coordinates) -> None: # shifting time does not affect the Redis instance # this is done to emulate auto-expiration await store._redis.expire(f"{store.namespace}:foo", 0) + if isinstance(store, ValkeyStore): + await store._valkey.expire(f"{store.namespace}:foo", 0) stored_value = await store.get("foo") @@ -79,7 +88,7 @@ async def test_expires(store: Store, frozen_datetime: Coordinates) -> None: @pytest.mark.flaky(reruns=5) @pytest.mark.parametrize("renew_for", [10, timedelta(seconds=10)]) async def test_get_and_renew(store: Store, renew_for: int | timedelta, frozen_datetime: Coordinates) -> None: - if isinstance(store, RedisStore): + if isinstance(store, (RedisStore, ValkeyStore)): pytest.skip() await store.set("foo", b"bar", expires_in=1) @@ -108,6 +117,22 @@ async def test_get_and_renew_redis(redis_store: RedisStore, renew_for: int | tim assert stored_value is not None +@pytest.mark.flaky(reruns=5) +@pytest.mark.parametrize("renew_for", [10, timedelta(seconds=10)]) +@pytest.mark.xdist_group("valkey") +async def test_get_and_renew_valkey(valkey_store: ValkeyStore, renew_for: int | timedelta) -> None: + # we can't sleep() in frozen datetime, and frozen datetime doesn't affect the redis + # instance, so we test this separately + await valkey_store.set("foo", b"bar", expires_in=1) + await valkey_store.get("foo", renew_for=renew_for) + + await asyncio.sleep(1.1) + + stored_value = await valkey_store.get("foo") + + assert stored_value is not None + + async def test_delete(store: Store) -> None: key = "key" await store.set(key, b"value", 60) @@ -152,7 +177,7 @@ async def test_delete_all(store: Store) -> None: async def test_expires_in(store: Store, frozen_datetime: Coordinates) -> None: - if not isinstance(store, RedisStore): + if not isinstance(store, (RedisStore, ValkeyStore)): pytest.xfail("bug in FileStore and MemoryStore") assert await store.expires_in("foo") is None @@ -165,7 +190,10 @@ async def test_expires_in(store: Store, frozen_datetime: Coordinates) -> None: assert expiration is not None assert math.ceil(expiration / 10) == 1 - await store._redis.expire(f"{store.namespace}:foo", 0) + if isinstance(store, RedisStore): + await store._redis.expire(f"{store.namespace}:foo", 0) + elif isinstance(store, ValkeyStore): + await store._valkey.expire(f"{store.namespace}:foo", 0) expiration = await store.expires_in("foo") assert expiration is None @@ -181,6 +209,17 @@ def test_redis_with_client_default(connection_pool_from_url_mock: Mock, mock_red assert backend._redis is mock_redis.return_value +@patch("litestar.stores.valkey.Valkey") +@patch("litestar.stores.valkey.ConnectionPool.from_url") +def test_valkey_with_client_default(connection_pool_from_url_mock: Mock, mock_valkey: Mock) -> None: + backend = ValkeyStore.with_client() + connection_pool_from_url_mock.assert_called_once_with( + url="valkey://localhost:6379", db=None, port=None, username=None, password=None, decode_responses=False + ) + mock_valkey.assert_called_once_with(connection_pool=connection_pool_from_url_mock.return_value) + assert backend._valkey is mock_valkey.return_value + + @patch("litestar.stores.redis.Redis") @patch("litestar.stores.redis.ConnectionPool.from_url") def test_redis_with_non_default(connection_pool_from_url_mock: Mock, mock_redis: Mock) -> None: @@ -197,6 +236,22 @@ def test_redis_with_non_default(connection_pool_from_url_mock: Mock, mock_redis: assert backend._redis is mock_redis.return_value +@patch("litestar.stores.valkey.Valkey") +@patch("litestar.stores.valkey.ConnectionPool.from_url") +def test_valkey_with_non_default(connection_pool_from_url_mock: Mock, mock_valkey: Mock) -> None: + url = "valkey://localhost" + db = 2 + port = 1234 + username = "user" + password = "password" + backend = ValkeyStore.with_client(url=url, db=db, port=port, username=username, password=password) + connection_pool_from_url_mock.assert_called_once_with( + url=url, db=db, port=port, username=username, password=password, decode_responses=False + ) + mock_valkey.assert_called_once_with(connection_pool=connection_pool_from_url_mock.return_value) + assert backend._valkey is mock_valkey.return_value + + @pytest.mark.xdist_group("redis") async def test_redis_delete_all(redis_store: RedisStore) -> None: await redis_store._redis.set("test_key", b"test_value") @@ -216,6 +271,25 @@ async def test_redis_delete_all(redis_store: RedisStore) -> None: assert stored_value == b"test_value" # check it doesn't delete other values +@pytest.mark.xdist_group("valkey") +async def test_valkey_delete_all(valkey_store: ValkeyStore) -> None: + await valkey_store._valkey.set("test_key", b"test_value") + + keys = [] + for i in range(10): + key = f"key-{i}" + keys.append(key) + await valkey_store.set(key, b"value", expires_in=10 if i % 2 else None) + + await valkey_store.delete_all() + + for key in keys: + assert await valkey_store.get(key) is None + + stored_value = await valkey_store._valkey.get("test_key") + assert stored_value == b"test_value" # check it doesn't delete other values + + @pytest.mark.xdist_group("redis") async def test_redis_delete_all_no_namespace_raises(redis_client: Redis) -> None: redis_store = RedisStore(redis=redis_client, namespace=None) @@ -224,12 +298,26 @@ async def test_redis_delete_all_no_namespace_raises(redis_client: Redis) -> None await redis_store.delete_all() +@pytest.mark.xdist_group("valkey") +async def test_valkey_delete_all_no_namespace_raises(valkey_client: Valkey) -> None: + valkey_store = ValkeyStore(valkey=valkey_client, namespace=None) + + with pytest.raises(ImproperlyConfiguredException): + await valkey_store.delete_all() + + @pytest.mark.xdist_group("redis") def test_redis_namespaced_key(redis_store: RedisStore) -> None: assert redis_store.namespace == "LITESTAR" assert redis_store._make_key("foo") == "LITESTAR:foo" +@pytest.mark.xdist_group("valkey") +def test_valkey_namespaced_key(valkey_store: ValkeyStore) -> None: + assert valkey_store.namespace == "LITESTAR" + assert valkey_store._make_key("foo") == "LITESTAR:foo" + + @pytest.mark.xdist_group("redis") def test_redis_with_namespace(redis_store: RedisStore) -> None: namespaced_test = redis_store.with_namespace("TEST") @@ -239,12 +327,27 @@ def test_redis_with_namespace(redis_store: RedisStore) -> None: assert namespaced_test._redis is redis_store._redis +@pytest.mark.xdist_group("valkey") +def test_valkey_with_namespace(valkey_store: ValkeyStore) -> None: + namespaced_test = valkey_store.with_namespace("TEST") + namespaced_test_foo = namespaced_test.with_namespace("FOO") + assert namespaced_test.namespace == "LITESTAR_TEST" + assert namespaced_test_foo.namespace == "LITESTAR_TEST_FOO" + assert namespaced_test._valkey is valkey_store._valkey + + @pytest.mark.xdist_group("redis") def test_redis_namespace_explicit_none(redis_client: Redis) -> None: assert RedisStore.with_client(url="redis://127.0.0.1", namespace=None).namespace is None assert RedisStore(redis=redis_client, namespace=None).namespace is None +@pytest.mark.xdist_group("valkey") +def test_valkey_namespace_explicit_none(valkey_client: Valkey) -> None: + assert ValkeyStore.with_client(url="redis://127.0.0.1", namespace=None).namespace is None + assert ValkeyStore(valkey=valkey_client, namespace=None).namespace is None + + async def test_file_init_directory(file_store: FileStore) -> None: shutil.rmtree(file_store.path) await file_store.set("foo", b"bar") @@ -280,7 +383,13 @@ def test_file_with_namespace_invalid_namespace_char(file_store: FileStore, inval file_store.with_namespace(f"foo{invalid_char}") -@pytest.fixture(params=[pytest.param("redis_store", marks=pytest.mark.xdist_group("redis")), "file_store"]) +@pytest.fixture( + params=[ + pytest.param("redis_store", marks=pytest.mark.xdist_group("redis")), + pytest.param("valkey_store", marks=pytest.mark.xdist_group("valkey")), + "file_store", + ] +) def namespaced_store(request: FixtureRequest) -> NamespacedStore: return cast("NamespacedStore", request.getfixturevalue(request.param)) @@ -393,3 +502,17 @@ async def test_redis_store_with_client_shutdown(redis_service: None) -> None: for x in redis_store._redis.connection_pool._available_connections + list(redis_store._redis.connection_pool._in_use_connections) ) + + +@pytest.mark.xdist_group("valkey") +async def test_valkey_store_with_client_shutdown(valkey_service: None) -> None: + valkey_store = ValkeyStore.with_client(url="valkey://localhost:6381") + assert await valkey_store._valkey.ping() + # remove the private shutdown and the assert below fails + # the check on connection is a mimic of https://github.com/redis/redis-py/blob/d529c2ad8d2cf4dcfb41bfd93ea68cfefd81aa66/tests/test_asyncio/test_connection_pool.py#L35-L39 + await valkey_store._shutdown() + assert not any( + x.is_connected + for x in valkey_store._valkey.connection_pool._available_connections + + list(valkey_store._valkey.connection_pool._in_use_connections) + ) diff --git a/tests/unit/test_template/test_template.py b/tests/unit/test_template/test_template.py index 4466524da5..b0d70d1f11 100644 --- a/tests/unit/test_template/test_template.py +++ b/tests/unit/test_template/test_template.py @@ -123,7 +123,12 @@ def index() -> Template: ) as client: res = client.get("/") assert res.status_code == 200 - assert res.headers["content-type"].startswith(expected_type) + if expected_type == MediaType.XML.value: + assert res.headers["content-type"].startswith(expected_type) or res.headers["content-type"].startswith( + "text/xml" + ) + else: + assert res.headers["content-type"].startswith(expected_type) def test_before_request_handler_content_type(tmp_path: Path) -> None: diff --git a/tests/unit/test_testing/test_lifespan_handler.py b/tests/unit/test_testing/test_lifespan_handler.py index 132b642710..f04f91a959 100644 --- a/tests/unit/test_testing/test_lifespan_handler.py +++ b/tests/unit/test_testing/test_lifespan_handler.py @@ -4,13 +4,16 @@ from litestar.testing.life_span_handler import LifeSpanHandler from litestar.types import Receive, Scope, Send +pytestmark = pytest.mark.filterwarnings("default") + async def test_wait_startup_invalid_event() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: await send({"type": "lifespan.startup.something_unexpected"}) # type: ignore[typeddict-item] with pytest.raises(RuntimeError, match="Received unexpected ASGI message type"): - LifeSpanHandler(TestClient(app)) + with LifeSpanHandler(TestClient(app)): + pass async def test_wait_shutdown_invalid_event() -> None: @@ -18,7 +21,17 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None: await send({"type": "lifespan.startup.complete"}) # type: ignore[typeddict-item] await send({"type": "lifespan.shutdown.something_unexpected"}) # type: ignore[typeddict-item] - handler = LifeSpanHandler(TestClient(app)) + with LifeSpanHandler(TestClient(app)) as handler: + with pytest.raises(RuntimeError, match="Received unexpected ASGI message type"): + await handler.wait_shutdown() - with pytest.raises(RuntimeError, match="Received unexpected ASGI message type"): + +async def test_implicit_startup() -> None: + async def app(scope: Scope, receive: Receive, send: Send) -> None: + await send({"type": "lifespan.startup.complete"}) # type: ignore[typeddict-item] + await send({"type": "lifespan.shutdown.complete"}) # type: ignore[typeddict-item] + + with pytest.warns(DeprecationWarning): + handler = LifeSpanHandler(TestClient(app)) await handler.wait_shutdown() + handler.close() diff --git a/tests/unit/test_testing/test_sub_client/__init__.py b/tests/unit/test_testing/test_sub_client/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/test_testing/test_sub_client/demo.py b/tests/unit/test_testing/test_sub_client/demo.py new file mode 100644 index 0000000000..dce3de56cb --- /dev/null +++ b/tests/unit/test_testing/test_sub_client/demo.py @@ -0,0 +1,28 @@ +""" +Assemble components into an app that shall be tested +""" + +import asyncio +from typing import AsyncIterator + +from litestar import Litestar, get +from litestar.response import ServerSentEvent + + +@get("/notify/{topic:str}") +async def get_notified(topic: str) -> ServerSentEvent: + async def generator() -> AsyncIterator[str]: + yield topic + while True: + await asyncio.sleep(0.1) + + return ServerSentEvent(generator(), event_type="Notifier") + + +def create_test_app() -> Litestar: + return Litestar( + route_handlers=[get_notified], + ) + + +app = create_test_app() diff --git a/tests/unit/test_testing/test_sub_client/test_subprocess_client.py b/tests/unit/test_testing/test_sub_client/test_subprocess_client.py new file mode 100644 index 0000000000..4096ee1a61 --- /dev/null +++ b/tests/unit/test_testing/test_sub_client/test_subprocess_client.py @@ -0,0 +1,54 @@ +""" +Test the app running in a subprocess +""" + +import asyncio +import pathlib +import sys +from typing import AsyncIterator, Iterator + +import httpx +import httpx_sse +import pytest + +from litestar.testing import subprocess_async_client, subprocess_sync_client + +if sys.platform == "win32": + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + + +ROOT = pathlib.Path(__file__).parent + + +@pytest.fixture(name="async_client", scope="session") +async def fx_async_client() -> AsyncIterator[httpx.AsyncClient]: + async with subprocess_async_client(workdir=ROOT, app="demo:app") as client: + yield client + + +@pytest.fixture(name="sync_client", scope="session") +def fx_sync_client() -> Iterator[httpx.Client]: + with subprocess_sync_client(workdir=ROOT, app="demo:app") as client: + yield client + + +async def test_subprocess_async_client(async_client: httpx.AsyncClient) -> None: + """Demonstrates functionality of the async client with an infinite SSE source that cannot be tested with the + regular async test client. + """ + + async with httpx_sse.aconnect_sse(async_client, "GET", "/notify/hello") as event_source: + async for event in event_source.aiter_sse(): + assert event.data == "hello" + break + + +def test_subprocess_sync_client(sync_client: httpx.Client) -> None: + """Demonstrates functionality of the async client with an infinite SSE source that cannot be tested with the + regular async test client. + """ + + with httpx_sse.connect_sse(sync_client, "GET", "/notify/hello") as event_source: + for event in event_source.iter_sse(): + assert event.data == "hello" + break diff --git a/tests/unit/test_testing/test_test_client.py b/tests/unit/test_testing/test_test_client.py index 31840e2220..f35b9ce0e9 100644 --- a/tests/unit/test_testing/test_test_client.py +++ b/tests/unit/test_testing/test_test_client.py @@ -42,9 +42,17 @@ def test_client_cls(request: FixtureRequest) -> Type[AnyTestClient]: return cast(Type[AnyTestClient], request.param) +@pytest.mark.parametrize( + "anyio_backend", + [ + pytest.param("asyncio"), + pytest.param("trio", marks=pytest.mark.xfail(reason="Known issue with trio backend", strict=False)), + ], +) @pytest.mark.parametrize("with_domain", [False, True]) async def test_test_client_set_session_data( with_domain: bool, + anyio_backend: str, session_backend_config: "BaseBackendConfig", test_client_backend: "AnyIOBackend", test_client_cls: Type[AnyTestClient], @@ -55,7 +63,7 @@ async def test_test_client_set_session_data( session_backend_config.domain = "testserver.local" @get(path="/test") - def get_session_data(request: Request) -> Dict[str, Any]: + async def get_session_data(request: Request) -> Dict[str, Any]: return request.session app = Litestar(route_handlers=[get_session_data], middleware=[session_backend_config.middleware]) @@ -67,9 +75,17 @@ def get_session_data(request: Request) -> Dict[str, Any]: assert session_data == (await maybe_async(client.get("/test"))).json() # type: ignore[attr-defined] -@pytest.mark.parametrize("with_domain", [False, True]) +@pytest.mark.parametrize( + "anyio_backend", + [ + pytest.param("asyncio"), + pytest.param("trio", marks=pytest.mark.xfail(reason="Known issue with trio backend", strict=False)), + ], +) +@pytest.mark.parametrize("with_domain", [True, False]) async def test_test_client_get_session_data( with_domain: bool, + anyio_backend: str, session_backend_config: "BaseBackendConfig", test_client_backend: "AnyIOBackend", store: Store, @@ -81,7 +97,7 @@ async def test_test_client_get_session_data( session_backend_config.domain = "testserver.local" @post(path="/test") - def set_session_data(request: Request) -> None: + async def set_session_data(request: Request) -> None: request.session.update(session_data) app = Litestar( diff --git a/tests/unit/test_typing.py b/tests/unit/test_typing.py index 14fcef575c..bb2334f913 100644 --- a/tests/unit/test_typing.py +++ b/tests/unit/test_typing.py @@ -4,15 +4,14 @@ from dataclasses import dataclass from typing import Any, ForwardRef, Generic, List, Optional, Tuple, TypeVar, Union -import annotated_types import msgspec import pytest -from typing_extensions import Annotated, NotRequired, Required, TypedDict, get_type_hints +from typing_extensions import Annotated, NotRequired, Required, TypeAliasType, TypedDict, get_type_hints from litestar import get from litestar.exceptions import LitestarWarning from litestar.params import DependencyKwarg, KwargDefinition, Parameter, ParameterKwarg -from litestar.typing import FieldDefinition, _unpack_predicate +from litestar.typing import FieldDefinition from .test_utils.test_signature import T, _check_field_definition, field_definition_int, test_type_hints @@ -440,20 +439,6 @@ def test_field_definition_get_type_hints_dont_resolve_generics( ) -@pytest.mark.parametrize( - "predicate, expected_meta", - [ - (annotated_types.LowerCase.__metadata__[0], {"lower_case": True}), # pyright: ignore - (annotated_types.UpperCase.__metadata__[0], {"upper_case": True}), # pyright: ignore - (annotated_types.IsAscii.__metadata__[0], {"pattern": "[[:ascii:]]"}), # pyright: ignore - (annotated_types.IsDigits.__metadata__[0], {"pattern": "[[:digit:]]"}), # pyright: ignore - (object(), {}), - ], -) -def test_unpack_predicate(predicate: Any, expected_meta: dict[str, Any]) -> None: - assert _unpack_predicate(predicate) == expected_meta - - def test_warn_ambiguous_default_values() -> None: with pytest.warns(LitestarWarning, match="Ambiguous default values"): FieldDefinition.from_annotation(Annotated[int, Parameter(default=1)], default=2) @@ -476,3 +461,17 @@ def handler(foo: Annotated[int, Parameter(default=1)]) -> None: (record,) = warnings assert record.category == DeprecationWarning assert "Deprecated default value specification" in str(record.message) + + +def test_is_type_alias_type() -> None: + field_definition = FieldDefinition.from_annotation(TypeAliasType("IntAlias", int)) # pyright: ignore + assert field_definition.is_type_alias_type + + +@pytest.mark.skipif(sys.version_info < (3, 12), reason="type keyword not available before 3.12") +def test_unwrap_type_alias_type_keyword() -> None: + ctx: dict[str, Any] = {} + exec("type IntAlias = int", ctx, None) + annotation = ctx["IntAlias"] + field_definition = FieldDefinition.from_annotation(annotation) + assert field_definition.is_type_alias_type diff --git a/tests/unit/test_utils/test_signature.py b/tests/unit/test_utils/test_signature.py index 8f0de7c38d..4926147e8e 100644 --- a/tests/unit/test_utils/test_signature.py +++ b/tests/unit/test_utils/test_signature.py @@ -3,6 +3,7 @@ from __future__ import annotations import inspect +import warnings from inspect import Parameter from types import ModuleType from typing import Any, Callable, Generic, List, Optional, TypeVar, Union @@ -12,6 +13,7 @@ from litestar import Controller, Router, post from litestar.exceptions import ImproperlyConfiguredException +from litestar.exceptions.base_exceptions import LitestarWarning from litestar.file_system import BaseLocalFileSystem from litestar.static_files import StaticFiles from litestar.types.asgi_types import Receive, Scope, Send @@ -156,16 +158,27 @@ def test_add_types_to_signature_namespace() -> None: assert ns == {"int": int, "str": str} -def test_add_types_to_signature_namespace_with_existing_types() -> None: +def test_add_types_to_signature_namespace_no_warn(monkeypatch: pytest.MonkeyPatch) -> None: """Test add_types_to_signature_namespace with existing types.""" - ns = add_types_to_signature_namespace([str], {"int": int}) - assert ns == {"int": int, "str": str} + monkeypatch.delenv("LITESTAR_WARN_SIGNATURE_NAMESPACE_OVERRIDE", raising=False) + with warnings.catch_warnings(): + warnings.simplefilter("error") + add_types_to_signature_namespace([int], {"int": int}) -def test_add_types_to_signature_namespace_with_existing_types_raises() -> None: +def test_add_types_to_signature_namespace_with_existing_types_warn(monkeypatch: pytest.MonkeyPatch) -> None: """Test add_types_to_signature_namespace with existing types raises.""" - with pytest.raises(ImproperlyConfiguredException): - add_types_to_signature_namespace([int], {"int": int}) + monkeypatch.delenv("LITESTAR_WARN_SIGNATURE_NAMESPACE_OVERRIDE", raising=False) + with pytest.warns(LitestarWarning): + add_types_to_signature_namespace([int], {"int": str}) + + +def test_add_types_to_signature_namespace_warn_disabled(monkeypatch: pytest.MonkeyPatch) -> None: + """Test add_types_to_signature_namespace with existing types.""" + monkeypatch.setenv("LITESTAR_WARN_SIGNATURE_NAMESPACE_OVERRIDE", "0") + with warnings.catch_warnings(): + warnings.simplefilter("error") + add_types_to_signature_namespace([int], {"int": str}) @pytest.mark.parametrize( diff --git a/tools/prepare_release.py b/tools/prepare_release.py index c0cee01765..7083873de5 100644 --- a/tools/prepare_release.py +++ b/tools/prepare_release.py @@ -21,7 +21,7 @@ _github_sponsors = "[GitHub Sponsors](https://github.com/sponsors/litestar-org/)" -class PullRequest(msgspec.Struct): +class PullRequest(msgspec.Struct, kw_only=True): title: str number: int body: str @@ -127,10 +127,15 @@ async def get_closing_issues_references(self, pr_number: int) -> list[int]: for edge in data["data"]["repository"]["pullRequest"]["closingIssuesReferences"]["edges"] ] - async def _get_pr_info_for_pr(self, number: int) -> PRInfo: + async def _get_pr_info_for_pr(self, number: int) -> PRInfo | None: res = await self._api_client.get(f"/pulls/{number}") res.raise_for_status() - pr = msgspec.convert(res.json(), type=PullRequest) + data = res.json() + if not data["body"]: + data["body"] = "" + if not data: + return None + pr = msgspec.convert(data, type=PullRequest) cc_prefix, clean_title = pr.title.split(":", maxsplit=1) cc_type = cc_prefix.split("(", maxsplit=1)[0].lower() @@ -157,6 +162,8 @@ async def get_prs(self) -> dict[str, list[PRInfo]]: prs = defaultdict(list) for pr in pulls: + if not pr: + continue if pr.user.type != "Bot": prs[pr.cc_type].append(pr) return prs @@ -313,7 +320,7 @@ def build_changelog_entry(release_info: ReleaseInfo, interactive: bool = False) for prs in release_info.pull_requests.values(): for pr in prs: cc_type = pr.cc_type - if cc_type in change_types or (interactive and click.confirm(f"Ignore PR #{pr.number} {pr.title!r}?")): + if cc_type in change_types or (interactive and click.confirm(f"Include PR #{pr.number} {pr.title!r}?")): doc.add_change(pr) else: click.secho(f"Ignoring change with type {cc_type}", fg="yellow") diff --git a/tools/pypi_readme.py b/tools/pypi_readme.py index d0a7a44246..f814d51838 100644 --- a/tools/pypi_readme.py +++ b/tools/pypi_readme.py @@ -5,14 +5,14 @@ def generate_pypi_readme() -> None: - source = Path("README.md").read_text() + source = Path("README.md").read_text(encoding="utf-8") output = re.sub(r"[\w\W]*", PYPI_BANNER, source) output = re.sub(r"[\w\W]*", "", output) output = re.sub(r"", "", output) # ensure a newline here so the other pre-commit hooks don't complain output = output.strip() + "\n" - Path("docs/PYPI_README.md").write_text(output) + Path("docs/PYPI_README.md").write_text(output, encoding="utf-8") if __name__ == "__main__": diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000..9dcc5cc9c1 --- /dev/null +++ b/uv.lock @@ -0,0 +1,4783 @@ +version = 1 +requires-python = ">=3.8, <4.0" +resolution-markers = [ + "python_full_version < '3.13' and sys_platform != 'win32'", + "python_full_version >= '3.13' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.13' and sys_platform == 'win32'", +] + +[[package]] +name = "accessible-pygments" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/50/7055ebd9b7928eca202768bcf5f8f69d8d69dec1767c956c08f055c5b31e/accessible-pygments-0.0.4.tar.gz", hash = "sha256:e7b57a9b15958e9601c7e9eb07a440c813283545a20973f2574a5f453d0e953e", size = 11650 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/d7/45cfa326d945e411c7e02764206845b05f8f5766aa7ebc812ef3bc4138cd/accessible_pygments-0.0.4-py2.py3-none-any.whl", hash = "sha256:416c6d8c1ea1c5ad8701903a20fcedf953c6e720d64f33dc47bfb2d3f2fa4e8d", size = 29320 }, +] + +[[package]] +name = "advanced-alchemy" +version = "0.26.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alembic" }, + { name = "eval-type-backport", marker = "python_full_version < '3.10'" }, + { name = "greenlet", marker = "sys_platform == 'darwin'" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/94/8783c58213448ae3fe44615e3efd2eb5bfc3d90a8fe47d29f9e6164681f2/advanced_alchemy-0.26.2.tar.gz", hash = "sha256:b56a9c42b7c1b1ab322cccb39b5fd0601232850b10191337f0504debc71735d2", size = 983000 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/ef/35219f6be810e636fbe26e05af6c767d02de825075b7e633f49cf886b355/advanced_alchemy-0.26.2-py3-none-any.whl", hash = "sha256:1f9b1207e757076e13a41782e76ac32f50ab5851a88d40f27321005cd46b6b94", size = 147848 }, +] + +[[package]] +name = "aiosqlite" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/3a/22ff5415bf4d296c1e92b07fd746ad42c96781f13295a074d58e77747848/aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7", size = 21691 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c4/c93eb22025a2de6b83263dfe3d7df2e19138e345bca6f18dba7394120930/aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6", size = 15564 }, +] + +[[package]] +name = "alabaster" +version = "0.7.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/71/a8ee96d1fd95ca04a0d2e2d9c4081dac4c2d2b12f7ddb899c8cb9bfd1532/alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2", size = 11454 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/88/c7083fc61120ab661c5d0b82cb77079fc1429d3f913a456c1c82cf4658f7/alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3", size = 13857 }, +] + +[[package]] +name = "alembic" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.9'" }, + { name = "importlib-resources", marker = "python_full_version < '3.9'" }, + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/1e/8cb8900ba1b6360431e46fb7a89922916d3a1b017a8908a7c0499cc7e5f6/alembic-1.14.0.tar.gz", hash = "sha256:b00892b53b3642d0b8dbedba234dbf1924b69be83a9a769d5a624b01094e304b", size = 1916172 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/06/8b505aea3d77021b18dcbd8133aa1418f1a1e37e432a465b14c46b2c0eaa/alembic-1.14.0-py3-none-any.whl", hash = "sha256:99bd884ca390466db5e27ffccff1d179ec5c05c965cfefc0607e69f9e411cb25", size = 233482 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.9'" }, +] +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.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/f9/9a7ce600ebe7804daf90d4d48b1c0510a4561ddce43a596be46676f82343/anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b", size = 171293 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/b4/f7e396030e3b11394436358ca258a81d6010106582422f23443c16ca1873/anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f", size = 89766 }, +] + +[[package]] +name = "apeye" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apeye-core" }, + { name = "domdf-python-tools" }, + { name = "platformdirs" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/6b/cc65e31843d7bfda8313a9dc0c77a21e8580b782adca53c7cb3e511fe023/apeye-1.4.1.tar.gz", hash = "sha256:14ea542fad689e3bfdbda2189a354a4908e90aee4bf84c15ab75d68453d76a36", size = 99219 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/7b/2d63664777b3e831ac1b1d8df5bbf0b7c8bee48e57115896080890527b1b/apeye-1.4.1-py3-none-any.whl", hash = "sha256:44e58a9104ec189bf42e76b3a7fe91e2b2879d96d48e9a77e5e32ff699c9204e", size = 107989 }, +] + +[[package]] +name = "apeye-core" +version = "1.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "domdf-python-tools" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/4c/4f108cfd06923bd897bf992a6ecb6fb122646ee7af94d7f9a64abd071d4c/apeye_core-1.1.5.tar.gz", hash = "sha256:5de72ed3d00cc9b20fea55e54b7ab8f5ef8500eb33a5368bc162a5585e238a55", size = 96511 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/9f/fa9971d2a0c6fef64c87ba362a493a4f230eff4ea8dfb9f4c7cbdf71892e/apeye_core-1.1.5-py3-none-any.whl", hash = "sha256:dc27a93f8c9e246b3b238c5ea51edf6115ab2618ef029b9f2d9a190ec8228fbf", size = 99286 }, +] + +[[package]] +name = "asgiref" +version = "3.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828 }, +] + +[[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'" }, +] +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 }, + { url = "https://files.pythonhosted.org/packages/82/0a/71e58396323b70e2e65cc8e9b48d87837bd405cf40585e51d0a78dea1124/asyncpg-0.30.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:29ff1fc8b5bf724273782ff8b4f57b0f8220a1b2324184846b39d1ab4122031d", size = 671916 }, + { url = "https://files.pythonhosted.org/packages/fc/2c/1ac00d77a31c62684332b74a478390e6976803a49bc5038064f4ba0cecc0/asyncpg-0.30.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64e899bce0600871b55368b8483e5e3e7f1860c9482e7f12e0a771e747988168", size = 644256 }, + { url = "https://files.pythonhosted.org/packages/96/aa/c698df40084474cd4afc3f967cc7353dfecad9b4a0a7fbd8f9bcf1f9ac7a/asyncpg-0.30.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b290f4726a887f75dcd1b3006f484252db37602313f806e9ffc4e5996cfe5cb", size = 3339515 }, + { url = "https://files.pythonhosted.org/packages/5f/32/db782ec573549ccac59ca23832d4dc045408571b1df37d9209ac86e22298/asyncpg-0.30.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f86b0e2cd3f1249d6fe6fd6cfe0cd4538ba994e2d8249c0491925629b9104d0f", size = 3367592 }, + { url = "https://files.pythonhosted.org/packages/80/da/77118d538ca70256955e5e137225f075906593b03793b4defb2b80a8401a/asyncpg-0.30.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:393af4e3214c8fa4c7b86da6364384c0d1b3298d45803375572f415b6f673f38", size = 3302393 }, + { url = "https://files.pythonhosted.org/packages/b7/50/7adbd4f47e75af969148df58e279e25e5a4c0f9f059cde8710df42180882/asyncpg-0.30.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fd4406d09208d5b4a14db9a9dbb311b6d7aeeab57bded7ed2f8ea41aeef39b34", size = 3434078 }, + { url = "https://files.pythonhosted.org/packages/52/49/fc25f8a28bc337824f4bfea8abd8ffa8057f3d0980d85d82cba3ed37f841/asyncpg-0.30.0-cp38-cp38-win32.whl", hash = "sha256:0b448f0150e1c3b96cb0438a0d0aa4871f1472e58de14a3ec320dbb2798fb0d4", size = 569762 }, + { url = "https://files.pythonhosted.org/packages/a7/07/cc33b589a31e1e539c7970666e52daaac4e4266fc78a3e78dd927057b936/asyncpg-0.30.0-cp38-cp38-win_amd64.whl", hash = "sha256:f23b836dd90bea21104f69547923a02b167d999ce053f3d502081acea2fba15b", size = 628443 }, + { url = "https://files.pythonhosted.org/packages/b4/82/d94f3ed6921136a0ef40a825740eda19437ccdad7d92d924302dca1d5c9e/asyncpg-0.30.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f4e83f067b35ab5e6371f8a4c93296e0439857b4569850b178a01385e82e9ad", size = 673026 }, + { url = "https://files.pythonhosted.org/packages/4e/db/7db8b73c5d86ec9a21807f405e0698f8f637a8a3ca14b7b6fd4259b66bcf/asyncpg-0.30.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5df69d55add4efcd25ea2a3b02025b669a285b767bfbf06e356d68dbce4234ff", size = 644732 }, + { url = "https://files.pythonhosted.org/packages/eb/a0/1f1910659d08050cb3e8f7d82b32983974798d7fd4ddf7620b8e2023d4ac/asyncpg-0.30.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3479a0d9a852c7c84e822c073622baca862d1217b10a02dd57ee4a7a081f708", size = 2911761 }, + { url = "https://files.pythonhosted.org/packages/4d/53/5aa0d92488ded50bab2b6626430ed9743b0b7e2d864a2b435af1ccbf219a/asyncpg-0.30.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26683d3b9a62836fad771a18ecf4659a30f348a561279d6227dab96182f46144", size = 2946595 }, + { url = "https://files.pythonhosted.org/packages/c5/cd/d6d548d8ee721f4e0f7fbbe509bbac140d556c2e45814d945540c96cf7d4/asyncpg-0.30.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1b982daf2441a0ed314bd10817f1606f1c28b1136abd9e4f11335358c2c631cb", size = 2890135 }, + { url = "https://files.pythonhosted.org/packages/46/f0/28df398b685dabee20235e24880e1f6486d84ae7e6b0d11bdebc17740e7a/asyncpg-0.30.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1c06a3a50d014b303e5f6fc1e5f95eb28d2cee89cf58384b700da621e5d5e547", size = 3011889 }, + { url = "https://files.pythonhosted.org/packages/c8/07/8c7ffe6fe8bccff9b12fcb6410b1b2fa74b917fd8b837806a40217d5228b/asyncpg-0.30.0-cp39-cp39-win32.whl", hash = "sha256:1b11a555a198b08f5c4baa8f8231c74a366d190755aa4f99aacec5970afe929a", size = 569406 }, + { url = "https://files.pythonhosted.org/packages/05/51/f59e4df6d9b8937530d4b9fdee1598b93db40c631fe94ff3ce64207b7a95/asyncpg-0.30.0-cp39-cp39-win_amd64.whl", hash = "sha256:8b684a3c858a83cd876f05958823b68e8d14ec01bb0c0d14a6704c5bf9711773", size = 626581 }, +] + +[[package]] +name = "asyncpg-stubs" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asyncpg" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/fb/08e27995b5444c888d58040203a2a73b9151855e267d171b3aff69033e7d/asyncpg_stubs-0.30.0.tar.gz", hash = "sha256:8bfe20f1b1e24a19674152ec9abbcc2df72c01e78af696f44fc275d56fe335ba", size = 20946 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/92/fb8ba4baca7f02ae627ad1f3b84fff8c550c93bd71fd7f993e6792d5718e/asyncpg_stubs-0.30.0-py3-none-any.whl", hash = "sha256:1eac258c10fc45a781729913a2fcfba775888bed160ae47f55fe0964d639e9cd", size = 26816 }, +] + +[[package]] +name = "attrs" +version = "24.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 }, +] + +[[package]] +name = "auto-pytabs" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ruff" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/ff/f5752f43f659ee62dd563af5bb0fe0a63111c3ff4708e9596279385f52bb/auto_pytabs-0.5.0.tar.gz", hash = "sha256:30087831c8be5b2314e663efd06c96b84c096572a060a492540f586362cc4326", size = 15362 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/df/e76dc1261882283f7ae93ebbf75438e85d8bb713a51dbbd5d17fef29e607/auto_pytabs-0.5.0-py3-none-any.whl", hash = "sha256:e59fb6d2f8b41b05d0906a322dd4bb1a86749d429483ec10036587de3657dcc8", size = 13748 }, +] + +[package.optional-dependencies] +sphinx = [ + { name = "sphinx" }, +] + +[[package]] +name = "autobahn" +version = "23.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "hyperlink" }, + { name = "setuptools" }, + { name = "txaio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/99/b6e0ffa0e8bafe9dfae1c9ab46d44d07317cbf297fbf8f07aff8a80e5bd8/autobahn-23.1.2.tar.gz", hash = "sha256:c5ef8ca7422015a1af774a883b8aef73d4954c9fcd182c9b5244e08e973f7c3a", size = 480717 } + +[[package]] +name = "autodocsumm" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/96/92afe8a7912b327c01f0a8b6408c9556ee13b1aba5b98d587ac7327ff32d/autodocsumm-0.2.14.tar.gz", hash = "sha256:2839a9d4facc3c4eccd306c08695540911042b46eeafcdc3203e6d0bab40bc77", size = 46357 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/bc/3f66af9beb683728e06ca08797e4e9d3e44f432f339718cae3ba856a9cad/autodocsumm-0.2.14-py3-none-any.whl", hash = "sha256:3bad8717fc5190802c60392a7ab04b9f3c97aa9efa8b3780b3d81d615bfe5dc0", size = 14640 }, +] + +[[package]] +name = "automat" +version = "24.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/2d/ede4ad7fc34ab4482389fa3369d304f2fa22e50770af706678f6a332fa82/automat-24.8.1.tar.gz", hash = "sha256:b34227cf63f6325b8ad2399ede780675083e439b20c323d376373d8ee6306d88", size = 128679 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/cc/55a32a2c98022d88812b5986d2a92c4ff3ee087e83b712ebc703bba452bf/Automat-24.8.1-py3-none-any.whl", hash = "sha256:bf029a7bc3da1e2c24da2343e7598affaa9f10bf0ab63ff808566ce90551e02a", size = 42585 }, +] + +[[package]] +name = "babel" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytz", marker = "python_full_version < '3.9'" }, +] +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 = "backports-zoneinfo" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/85/475e514c3140937cf435954f78dedea1861aeab7662d11de232bdaa90655/backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2", size = 74098 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/6d/eca004eeadcbf8bd64cc96feb9e355536147f0577420b44d80c7cac70767/backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987", size = 35816 }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9b1b920a6a95652463143943fa3b8c000cb0b932ab463764a6f2a2416560/backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1", size = 72147 }, + { url = "https://files.pythonhosted.org/packages/1a/ab/3e941e3fcf1b7d3ab3d0233194d99d6a0ed6b24f8f956fc81e47edc8c079/backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9", size = 74033 }, + { url = "https://files.pythonhosted.org/packages/c0/34/5fdb0a3a28841d215c255be8fc60b8666257bb6632193c86fd04b63d4a31/backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328", size = 36803 }, + { url = "https://files.pythonhosted.org/packages/78/cc/e27fd6493bbce8dbea7e6c1bc861fe3d3bc22c4f7c81f4c3befb8ff5bfaf/backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6", size = 38967 }, +] + +[[package]] +name = "beanie" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "lazy-model" }, + { name = "motor" }, + { name = "pydantic" }, + { name = "toml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/d1/ebd474f9e552e32378bad032e2dcce9ba78473b488ef29c9295b1e8d5c23/beanie-1.27.0.tar.gz", hash = "sha256:a5eee40f1e52214afeb8558c0823d7504856884770c3d56fc3cd5765efb87314", size = 169370 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/4d/9b302c451625e3b570b0dcafd157d92b633f96b4b17eca1c88a081b1a7b9/beanie-1.27.0-py3-none-any.whl", hash = "sha256:2cc6762bdd59b9040dd004ecbc7d4fd5ddd22e52743915e38d1f0f92f276bcaf", size = 84066 }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/ca/824b1195773ce6166d388573fc106ce56d4a805bd7427b624e063596ec58/beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", size = 581181 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed", size = 147925 }, +] + +[[package]] +name = "black" +version = "24.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/b0/46fb0d4e00372f4a86a6f8efa3cb193c9f64863615e39010b1477e010578/black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f", size = 644810 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/6e/74e29edf1fba3887ed7066930a87f698ffdcd52c5dbc263eabb06061672d/black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6", size = 1632092 }, + { url = "https://files.pythonhosted.org/packages/ab/49/575cb6c3faee690b05c9d11ee2e8dba8fbd6d6c134496e644c1feb1b47da/black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb", size = 1457529 }, + { url = "https://files.pythonhosted.org/packages/7a/b4/d34099e95c437b53d01c4aa37cf93944b233066eb034ccf7897fa4e5f286/black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42", size = 1757443 }, + { url = "https://files.pythonhosted.org/packages/87/a0/6d2e4175ef364b8c4b64f8441ba041ed65c63ea1db2720d61494ac711c15/black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a", size = 1418012 }, + { url = "https://files.pythonhosted.org/packages/08/a6/0a3aa89de9c283556146dc6dbda20cd63a9c94160a6fbdebaf0918e4a3e1/black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1", size = 1615080 }, + { url = "https://files.pythonhosted.org/packages/db/94/b803d810e14588bb297e565821a947c108390a079e21dbdcb9ab6956cd7a/black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af", size = 1438143 }, + { url = "https://files.pythonhosted.org/packages/a5/b5/f485e1bbe31f768e2e5210f52ea3f432256201289fd1a3c0afda693776b0/black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4", size = 1738774 }, + { url = "https://files.pythonhosted.org/packages/a8/69/a000fc3736f89d1bdc7f4a879f8aaf516fb03613bb51a0154070383d95d9/black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af", size = 1427503 }, + { url = "https://files.pythonhosted.org/packages/a2/a8/05fb14195cfef32b7c8d4585a44b7499c2a4b205e1662c427b941ed87054/black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368", size = 1646132 }, + { url = "https://files.pythonhosted.org/packages/41/77/8d9ce42673e5cb9988f6df73c1c5c1d4e9e788053cccd7f5fb14ef100982/black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed", size = 1448665 }, + { url = "https://files.pythonhosted.org/packages/cc/94/eff1ddad2ce1d3cc26c162b3693043c6b6b575f538f602f26fe846dfdc75/black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018", size = 1762458 }, + { url = "https://files.pythonhosted.org/packages/28/ea/18b8d86a9ca19a6942e4e16759b2fa5fc02bbc0eb33c1b866fcd387640ab/black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2", size = 1436109 }, + { url = "https://files.pythonhosted.org/packages/9f/d4/ae03761ddecc1a37d7e743b89cccbcf3317479ff4b88cfd8818079f890d0/black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd", size = 1617322 }, + { url = "https://files.pythonhosted.org/packages/14/4b/4dfe67eed7f9b1ddca2ec8e4418ea74f0d1dc84d36ea874d618ffa1af7d4/black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2", size = 1442108 }, + { url = "https://files.pythonhosted.org/packages/97/14/95b3f91f857034686cae0e73006b8391d76a8142d339b42970eaaf0416ea/black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e", size = 1745786 }, + { url = "https://files.pythonhosted.org/packages/95/54/68b8883c8aa258a6dde958cd5bdfada8382bec47c5162f4a01e66d839af1/black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920", size = 1426754 }, + { url = "https://files.pythonhosted.org/packages/13/b2/b3f24fdbb46f0e7ef6238e131f13572ee8279b70f237f221dd168a9dba1a/black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c", size = 1631706 }, + { url = "https://files.pythonhosted.org/packages/d9/35/31010981e4a05202a84a3116423970fd1a59d2eda4ac0b3570fbb7029ddc/black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e", size = 1457429 }, + { url = "https://files.pythonhosted.org/packages/27/25/3f706b4f044dd569a20a4835c3b733dedea38d83d2ee0beb8178a6d44945/black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47", size = 1756488 }, + { url = "https://files.pythonhosted.org/packages/63/72/79375cd8277cbf1c5670914e6bd4c1b15dea2c8f8e906dc21c448d0535f0/black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb", size = 1417721 }, + { url = "https://files.pythonhosted.org/packages/27/1e/83fa8a787180e1632c3d831f7e58994d7aaf23a0961320d21e84f922f919/black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed", size = 206504 }, +] + +[[package]] +name = "brotli" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", size = 7372270 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/3a/dbf4fb970c1019a57b5e492e1e0eae745d32e59ba4d6161ab5422b08eefe/Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752", size = 873045 }, + { url = "https://files.pythonhosted.org/packages/dd/11/afc14026ea7f44bd6eb9316d800d439d092c8d508752055ce8d03086079a/Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9", size = 446218 }, + { url = "https://files.pythonhosted.org/packages/36/83/7545a6e7729db43cb36c4287ae388d6885c85a86dd251768a47015dfde32/Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3", size = 2903872 }, + { url = "https://files.pythonhosted.org/packages/32/23/35331c4d9391fcc0f29fd9bec2c76e4b4eeab769afbc4b11dd2e1098fb13/Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d", size = 2941254 }, + { url = "https://files.pythonhosted.org/packages/3b/24/1671acb450c902edb64bd765d73603797c6c7280a9ada85a195f6b78c6e5/Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e", size = 2857293 }, + { url = "https://files.pythonhosted.org/packages/d5/00/40f760cc27007912b327fe15bf6bfd8eaecbe451687f72a8abc587d503b3/Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da", size = 3002385 }, + { url = "https://files.pythonhosted.org/packages/b8/cb/8aaa83f7a4caa131757668c0fb0c4b6384b09ffa77f2fba9570d87ab587d/Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80", size = 2911104 }, + { url = "https://files.pythonhosted.org/packages/bc/c4/65456561d89d3c49f46b7fbeb8fe6e449f13bdc8ea7791832c5d476b2faf/Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d", size = 2809981 }, + { url = "https://files.pythonhosted.org/packages/05/1b/cf49528437bae28abce5f6e059f0d0be6fecdcc1d3e33e7c54b3ca498425/Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0", size = 2935297 }, + { url = "https://files.pythonhosted.org/packages/81/ff/190d4af610680bf0c5a09eb5d1eac6e99c7c8e216440f9c7cfd42b7adab5/Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e", size = 2930735 }, + { url = "https://files.pythonhosted.org/packages/80/7d/f1abbc0c98f6e09abd3cad63ec34af17abc4c44f308a7a539010f79aae7a/Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c", size = 2933107 }, + { url = "https://files.pythonhosted.org/packages/34/ce/5a5020ba48f2b5a4ad1c0522d095ad5847a0be508e7d7569c8630ce25062/Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1", size = 2845400 }, + { url = "https://files.pythonhosted.org/packages/44/89/fa2c4355ab1eecf3994e5a0a7f5492c6ff81dfcb5f9ba7859bd534bb5c1a/Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2", size = 3031985 }, + { url = "https://files.pythonhosted.org/packages/af/a4/79196b4a1674143d19dca400866b1a4d1a089040df7b93b88ebae81f3447/Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec", size = 2927099 }, + { url = "https://files.pythonhosted.org/packages/e9/54/1c0278556a097f9651e657b873ab08f01b9a9ae4cac128ceb66427d7cd20/Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2", size = 333172 }, + { url = "https://files.pythonhosted.org/packages/f7/65/b785722e941193fd8b571afd9edbec2a9b838ddec4375d8af33a50b8dab9/Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128", size = 357255 }, + { url = "https://files.pythonhosted.org/packages/96/12/ad41e7fadd5db55459c4c401842b47f7fee51068f86dd2894dd0dcfc2d2a/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", size = 873068 }, + { url = "https://files.pythonhosted.org/packages/95/4e/5afab7b2b4b61a84e9c75b17814198ce515343a44e2ed4488fac314cd0a9/Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", size = 446244 }, + { url = "https://files.pythonhosted.org/packages/9d/e6/f305eb61fb9a8580c525478a4a34c5ae1a9bcb12c3aee619114940bc513d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", size = 2906500 }, + { url = "https://files.pythonhosted.org/packages/3e/4f/af6846cfbc1550a3024e5d3775ede1e00474c40882c7bf5b37a43ca35e91/Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", size = 2943950 }, + { url = "https://files.pythonhosted.org/packages/b3/e7/ca2993c7682d8629b62630ebf0d1f3bb3d579e667ce8e7ca03a0a0576a2d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61", size = 2918527 }, + { url = "https://files.pythonhosted.org/packages/b3/96/da98e7bedc4c51104d29cc61e5f449a502dd3dbc211944546a4cc65500d3/Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", size = 2845489 }, + { url = "https://files.pythonhosted.org/packages/e8/ef/ccbc16947d6ce943a7f57e1a40596c75859eeb6d279c6994eddd69615265/Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", size = 2914080 }, + { url = "https://files.pythonhosted.org/packages/80/d6/0bd38d758d1afa62a5524172f0b18626bb2392d717ff94806f741fcd5ee9/Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", size = 2813051 }, + { url = "https://files.pythonhosted.org/packages/14/56/48859dd5d129d7519e001f06dcfbb6e2cf6db92b2702c0c2ce7d97e086c1/Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", size = 2938172 }, + { url = "https://files.pythonhosted.org/packages/3d/77/a236d5f8cd9e9f4348da5acc75ab032ab1ab2c03cc8f430d24eea2672888/Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8", size = 2933023 }, + { url = "https://files.pythonhosted.org/packages/f1/87/3b283efc0f5cb35f7f84c0c240b1e1a1003a5e47141a4881bf87c86d0ce2/Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f", size = 2935871 }, + { url = "https://files.pythonhosted.org/packages/f3/eb/2be4cc3e2141dc1a43ad4ca1875a72088229de38c68e842746b342667b2a/Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757", size = 2847784 }, + { url = "https://files.pythonhosted.org/packages/66/13/b58ddebfd35edde572ccefe6890cf7c493f0c319aad2a5badee134b4d8ec/Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0", size = 3034905 }, + { url = "https://files.pythonhosted.org/packages/84/9c/bc96b6c7db824998a49ed3b38e441a2cae9234da6fa11f6ed17e8cf4f147/Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b", size = 2929467 }, + { url = "https://files.pythonhosted.org/packages/e7/71/8f161dee223c7ff7fea9d44893fba953ce97cf2c3c33f78ba260a91bcff5/Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50", size = 333169 }, + { url = "https://files.pythonhosted.org/packages/02/8a/fece0ee1057643cb2a5bbf59682de13f1725f8482b2c057d4e799d7ade75/Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1", size = 357253 }, + { url = "https://files.pythonhosted.org/packages/5c/d0/5373ae13b93fe00095a58efcbce837fd470ca39f703a235d2a999baadfbc/Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28", size = 815693 }, + { url = "https://files.pythonhosted.org/packages/8e/48/f6e1cdf86751300c288c1459724bfa6917a80e30dbfc326f92cea5d3683a/Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f", size = 422489 }, + { url = "https://files.pythonhosted.org/packages/06/88/564958cedce636d0f1bed313381dfc4b4e3d3f6015a63dae6146e1b8c65c/Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", size = 873081 }, + { url = "https://files.pythonhosted.org/packages/58/79/b7026a8bb65da9a6bb7d14329fd2bd48d2b7f86d7329d5cc8ddc6a90526f/Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", size = 446244 }, + { url = "https://files.pythonhosted.org/packages/e5/18/c18c32ecea41b6c0004e15606e274006366fe19436b6adccc1ae7b2e50c2/Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", size = 2906505 }, + { url = "https://files.pythonhosted.org/packages/08/c8/69ec0496b1ada7569b62d85893d928e865df29b90736558d6c98c2031208/Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", size = 2944152 }, + { url = "https://files.pythonhosted.org/packages/ab/fb/0517cea182219d6768113a38167ef6d4eb157a033178cc938033a552ed6d/Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", size = 2919252 }, + { url = "https://files.pythonhosted.org/packages/c7/53/73a3431662e33ae61a5c80b1b9d2d18f58dfa910ae8dd696e57d39f1a2f5/Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", size = 2845955 }, + { url = "https://files.pythonhosted.org/packages/55/ac/bd280708d9c5ebdbf9de01459e625a3e3803cce0784f47d633562cf40e83/Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", size = 2914304 }, + { url = "https://files.pythonhosted.org/packages/76/58/5c391b41ecfc4527d2cc3350719b02e87cb424ef8ba2023fb662f9bf743c/Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", size = 2814452 }, + { url = "https://files.pythonhosted.org/packages/c7/4e/91b8256dfe99c407f174924b65a01f5305e303f486cc7a2e8a5d43c8bec3/Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", size = 2938751 }, + { url = "https://files.pythonhosted.org/packages/5a/a6/e2a39a5d3b412938362bbbeba5af904092bf3f95b867b4a3eb856104074e/Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", size = 2933757 }, + { url = "https://files.pythonhosted.org/packages/13/f0/358354786280a509482e0e77c1a5459e439766597d280f28cb097642fc26/Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9", size = 2936146 }, + { url = "https://files.pythonhosted.org/packages/80/f7/daf538c1060d3a88266b80ecc1d1c98b79553b3f117a485653f17070ea2a/Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb", size = 2848055 }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0eaa0585c4077d3c2d1edf322d8e97aabf317941d3a72d7b3ad8bce004b0/Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111", size = 3035102 }, + { url = "https://files.pythonhosted.org/packages/d8/63/1c1585b2aa554fe6dbce30f0c18bdbc877fa9a1bf5ff17677d9cca0ac122/Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839", size = 2930029 }, + { url = "https://files.pythonhosted.org/packages/5f/3b/4e3fd1893eb3bbfef8e5a80d4508bec17a57bb92d586c85c12d28666bb13/Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", size = 333276 }, + { url = "https://files.pythonhosted.org/packages/3d/d5/942051b45a9e883b5b6e98c041698b1eb2012d25e5948c58d6bf85b1bb43/Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", size = 357255 }, + { url = "https://files.pythonhosted.org/packages/0a/9f/fb37bb8ffc52a8da37b1c03c459a8cd55df7a57bdccd8831d500e994a0ca/Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", size = 815681 }, + { url = "https://files.pythonhosted.org/packages/06/b3/dbd332a988586fefb0aa49c779f59f47cae76855c2d00f450364bb574cac/Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", size = 422475 }, + { url = "https://files.pythonhosted.org/packages/bb/80/6aaddc2f63dbcf2d93c2d204e49c11a9ec93a8c7c63261e2b4bd35198283/Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", size = 2906173 }, + { url = "https://files.pythonhosted.org/packages/ea/1d/e6ca79c96ff5b641df6097d299347507d39a9604bde8915e76bf026d6c77/Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", size = 2943803 }, + { url = "https://files.pythonhosted.org/packages/ac/a3/d98d2472e0130b7dd3acdbb7f390d478123dbf62b7d32bda5c830a96116d/Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", size = 2918946 }, + { url = "https://files.pythonhosted.org/packages/c4/a5/c69e6d272aee3e1423ed005d8915a7eaa0384c7de503da987f2d224d0721/Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", size = 2845707 }, + { url = "https://files.pythonhosted.org/packages/58/9f/4149d38b52725afa39067350696c09526de0125ebfbaab5acc5af28b42ea/Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", size = 2936231 }, + { url = "https://files.pythonhosted.org/packages/5a/5a/145de884285611838a16bebfdb060c231c52b8f84dfbe52b852a15780386/Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", size = 2848157 }, + { url = "https://files.pythonhosted.org/packages/50/ae/408b6bfb8525dadebd3b3dd5b19d631da4f7d46420321db44cd99dcf2f2c/Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", size = 3035122 }, + { url = "https://files.pythonhosted.org/packages/af/85/a94e5cfaa0ca449d8f91c3d6f78313ebf919a0dbd55a100c711c6e9655bc/Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", size = 2930206 }, + { url = "https://files.pythonhosted.org/packages/c2/f0/a61d9262cd01351df22e57ad7c34f66794709acab13f34be2675f45bf89d/Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", size = 333804 }, + { url = "https://files.pythonhosted.org/packages/7e/c1/ec214e9c94000d1c1974ec67ced1c970c148aa6b8d8373066123fc3dbf06/Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", size = 358517 }, + { url = "https://files.pythonhosted.org/packages/34/1b/16114a20c0a43c20331f03431178ed8b12280b12c531a14186da0bc5b276/Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3", size = 873053 }, + { url = "https://files.pythonhosted.org/packages/36/49/2afe4aa5a23a13dad4c7160ae574668eec58b3c80b56b74a826cebff7ab8/Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208", size = 446211 }, + { url = "https://files.pythonhosted.org/packages/10/9d/6463edb80a9e0a944f70ed0c4d41330178526626d7824f729e81f78a3f24/Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7", size = 2904604 }, + { url = "https://files.pythonhosted.org/packages/a4/bd/cfaac88c14f97d9e1f2e51a304c3573858548bb923d011b19f76b295f81c/Brotli-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751", size = 2941707 }, + { url = "https://files.pythonhosted.org/packages/60/3f/2618fa887d7af6828246822f10d9927244dab22db7a96ec56041a2fd1fbd/Brotli-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48", size = 2672420 }, + { url = "https://files.pythonhosted.org/packages/e7/41/1c6d15c8d5b55db2c3c249c64c352c8a1bc97f5e5c55183f5930866fc012/Brotli-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619", size = 2757410 }, + { url = "https://files.pythonhosted.org/packages/6c/5b/ca72fd8aa1278dfbb12eb320b6e409aefabcd767b85d607c9d54c9dadd1a/Brotli-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97", size = 2911143 }, + { url = "https://files.pythonhosted.org/packages/b1/53/110657f4017d34a2e9a96d9630a388ad7e56092023f1d46d11648c6c0bce/Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a", size = 2809968 }, + { url = "https://files.pythonhosted.org/packages/3f/2a/fbc95429b45e4aa4a3a3a815e4af11772bfd8ef94e883dcff9ceaf556662/Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088", size = 2935402 }, + { url = "https://files.pythonhosted.org/packages/4e/52/02acd2992e5a2c10adf65fa920fad0c29e11e110f95eeb11bcb20342ecd2/Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596", size = 2931208 }, + { url = "https://files.pythonhosted.org/packages/6b/35/5d258d1aeb407e1fc6fcbbff463af9c64d1ecc17042625f703a1e9d22ec5/Brotli-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7", size = 2933171 }, + { url = "https://files.pythonhosted.org/packages/cc/58/b25ca26492da9880e517753967685903c6002ddc2aade93d6e56df817b30/Brotli-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5", size = 2845347 }, + { url = "https://files.pythonhosted.org/packages/12/cf/91b84beaa051c9376a22cc38122dc6fbb63abcebd5a4b8503e9c388de7b1/Brotli-1.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943", size = 3031668 }, + { url = "https://files.pythonhosted.org/packages/38/05/04a57ba75aed972be0c6ad5f2f5ea34c83f5fecf57787cc6e54aac21a323/Brotli-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a", size = 2926949 }, + { url = "https://files.pythonhosted.org/packages/c9/2f/fbe6938f33d2cd9b7d7fb591991eb3fb57ffa40416bb873bbbacab60a381/Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b", size = 333179 }, + { url = "https://files.pythonhosted.org/packages/39/a5/9322c8436072e77b8646f6bde5e19ee66f62acf7aa01337ded10777077fa/Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0", size = 357254 }, + { url = "https://files.pythonhosted.org/packages/1b/aa/aa6e0c9848ee4375514af0b27abf470904992939b7363ae78fc8aca8a9a8/Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a", size = 873048 }, + { url = "https://files.pythonhosted.org/packages/ae/32/38bba1a8bef9ecb1cda08439fd28d7e9c51aff13b4783a4f1610da90b6c2/Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f", size = 446207 }, + { url = "https://files.pythonhosted.org/packages/3c/6a/14cc20ddc53efc274601c8195791a27cfb7acc5e5134e0f8c493a8b8821a/Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9", size = 2903803 }, + { url = "https://files.pythonhosted.org/packages/9a/26/62b2d894d4e82d7a7f4e0bb9007a42bbc765697a5679b43186acd68d7a79/Brotli-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf", size = 2941149 }, + { url = "https://files.pythonhosted.org/packages/a9/ca/00d55bbdd8631236c61777742d8a8454cf6a87eb4125cad675912c68bec7/Brotli-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac", size = 2672253 }, + { url = "https://files.pythonhosted.org/packages/e2/e6/4a730f6e5b5d538e92d09bc51bf69119914f29a222f9e1d65ae4abb27a4e/Brotli-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578", size = 2757005 }, + { url = "https://files.pythonhosted.org/packages/cb/6b/8cf297987fe3c1bf1c87f0c0b714af2ce47092b8d307b9f6ecbc65f98968/Brotli-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474", size = 2910658 }, + { url = "https://files.pythonhosted.org/packages/2c/1f/be9443995821c933aad7159803f84ef4923c6f5b72c2affd001192b310fc/Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c", size = 2809728 }, + { url = "https://files.pythonhosted.org/packages/76/2f/213bab6efa902658c80a1247142d42b138a27ccdd6bade49ca9cd74e714a/Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d", size = 2935043 }, + { url = "https://files.pythonhosted.org/packages/27/89/bbb14fa98e895d1e601491fba54a5feec167d262f0d3d537a3b0d4cd0029/Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59", size = 2930639 }, + { url = "https://files.pythonhosted.org/packages/14/87/03a6d6e1866eddf9f58cc57e35befbeb5514da87a416befe820150cae63f/Brotli-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419", size = 2932834 }, + { url = "https://files.pythonhosted.org/packages/a4/d5/e5f85e04f75144d1a89421ba432def6bdffc8f28b04f5b7d540bbd03362c/Brotli-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2", size = 2845213 }, + { url = "https://files.pythonhosted.org/packages/99/bf/25ef07add7afbb1aacd4460726a1a40370dfd60c0810b6f242a6d3871d7e/Brotli-1.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f", size = 3031573 }, + { url = "https://files.pythonhosted.org/packages/55/22/948a97bda5c9dc9968d56b9ed722d9727778db43739cf12ef26ff69be94d/Brotli-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb", size = 2926885 }, + { url = "https://files.pythonhosted.org/packages/31/ba/e53d107399b535ef89deb6977dd8eae468e2dde7b1b74c6cbe2c0e31fda2/Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64", size = 333171 }, + { url = "https://files.pythonhosted.org/packages/99/b3/f7b3af539f74b82e1c64d28685a5200c631cc14ae751d37d6ed819655627/Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467", size = 357258 }, +] + +[[package]] +name = "cachecontrol" +version = "0.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msgpack" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/23/db12e0b6b241e33f77f7cce01a06b4cc6f8071728656cc0ea262d2a14dad/cachecontrol-0.14.1.tar.gz", hash = "sha256:06ef916a1e4eb7dba9948cdfc9c76e749db2e02104a9a1277e8b642591a0f717", size = 28928 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/aa/481eb52af52aae093c61c181f2308779973ffd6f0f5f6c0881b2138f3087/cachecontrol-0.14.1-py3-none-any.whl", hash = "sha256:65e3abd62b06382ce3894df60dde9e0deb92aeb734724f68fa4f3b91e97206b9", size = 22085 }, +] + +[package.optional-dependencies] +filecache = [ + { name = "filelock" }, +] + +[[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" }, +] +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 }, + { url = "https://files.pythonhosted.org/packages/48/08/15bf6b43ae9bd06f6b00ad8a91f5a8fe1069d4c9fab550a866755402724e/cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", size = 182457 }, + { url = "https://files.pythonhosted.org/packages/c2/5b/f1523dd545f92f7df468e5f653ffa4df30ac222f3c884e51e139878f1cb5/cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", size = 425932 }, + { url = "https://files.pythonhosted.org/packages/53/93/7e547ab4105969cc8c93b38a667b82a835dd2cc78f3a7dad6130cfd41e1d/cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", size = 448585 }, + { url = "https://files.pythonhosted.org/packages/56/c4/a308f2c332006206bb511de219efeff090e9d63529ba0a77aae72e82248b/cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", size = 456268 }, + { url = "https://files.pythonhosted.org/packages/ca/5b/b63681518265f2f4060d2b60755c1c77ec89e5e045fc3773b72735ddaad5/cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", size = 436592 }, + { url = "https://files.pythonhosted.org/packages/bb/19/b51af9f4a4faa4a8ac5a0e5d5c2522dcd9703d07fac69da34a36c4d960d3/cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", size = 446512 }, + { url = "https://files.pythonhosted.org/packages/e2/63/2bed8323890cb613bbecda807688a31ed11a7fe7afe31f8faaae0206a9a3/cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", size = 171576 }, + { url = "https://files.pythonhosted.org/packages/2f/70/80c33b044ebc79527447fd4fbc5455d514c3bb840dede4455de97da39b4d/cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", size = 181229 }, + { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220 }, + { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605 }, + { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 }, + { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200 }, + { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565 }, + { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635 }, + { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218 }, + { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486 }, + { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911 }, + { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632 }, + { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820 }, + { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, +] + +[[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 = "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/86/f4/ccab93e631e7293cca82f9f7ba39783c967f823a0000df2d8dd743cad74f/charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", size = 193961 }, + { url = "https://files.pythonhosted.org/packages/94/d4/2b21cb277bac9605026d2d91a4a8872bc82199ed11072d035dc674c27223/charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", size = 124507 }, + { url = "https://files.pythonhosted.org/packages/9a/e0/a7c1fcdff20d9c667342e0391cfeb33ab01468d7d276b2c7914b371667cc/charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", size = 119298 }, + { url = "https://files.pythonhosted.org/packages/70/de/1538bb2f84ac9940f7fa39945a5dd1d22b295a89c98240b262fc4b9fcfe0/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", size = 139328 }, + { url = "https://files.pythonhosted.org/packages/e9/ca/288bb1a6bc2b74fb3990bdc515012b47c4bc5925c8304fc915d03f94b027/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", size = 149368 }, + { url = "https://files.pythonhosted.org/packages/aa/75/58374fdaaf8406f373e508dab3486a31091f760f99f832d3951ee93313e8/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", size = 141944 }, + { url = "https://files.pythonhosted.org/packages/32/c8/0bc558f7260db6ffca991ed7166494a7da4fda5983ee0b0bfc8ed2ac6ff9/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", size = 143326 }, + { url = "https://files.pythonhosted.org/packages/0e/dd/7f6fec09a1686446cee713f38cf7d5e0669e0bcc8288c8e2924e998cf87d/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", size = 146171 }, + { url = "https://files.pythonhosted.org/packages/4c/a8/440f1926d6d8740c34d3ca388fbd718191ec97d3d457a0677eb3aa718fce/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", size = 139711 }, + { url = "https://files.pythonhosted.org/packages/e9/7f/4b71e350a3377ddd70b980bea1e2cc0983faf45ba43032b24b2578c14314/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", size = 148348 }, + { url = "https://files.pythonhosted.org/packages/1e/70/17b1b9202531a33ed7ef41885f0d2575ae42a1e330c67fddda5d99ad1208/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", size = 151290 }, + { url = "https://files.pythonhosted.org/packages/44/30/574b5b5933d77ecb015550aafe1c7d14a8cd41e7e6c4dcea5ae9e8d496c3/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", size = 149114 }, + { url = "https://files.pythonhosted.org/packages/0b/11/ca7786f7e13708687443082af20d8341c02e01024275a28bc75032c5ce5d/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", size = 143856 }, + { url = "https://files.pythonhosted.org/packages/f9/c2/1727c1438256c71ed32753b23ec2e6fe7b6dff66a598f6566cfe8139305e/charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", size = 94333 }, + { url = "https://files.pythonhosted.org/packages/09/c8/0e17270496a05839f8b500c1166e3261d1226e39b698a735805ec206967b/charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", size = 101454 }, + { url = "https://files.pythonhosted.org/packages/54/2f/28659eee7f5d003e0f5a3b572765bf76d6e0fe6601ab1f1b1dd4cba7e4f1/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", size = 196326 }, + { url = "https://files.pythonhosted.org/packages/d1/18/92869d5c0057baa973a3ee2af71573be7b084b3c3d428fe6463ce71167f8/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", size = 125614 }, + { url = "https://files.pythonhosted.org/packages/d6/27/327904c5a54a7796bb9f36810ec4173d2df5d88b401d2b95ef53111d214e/charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", size = 120450 }, + { url = "https://files.pythonhosted.org/packages/a4/23/65af317914a0308495133b2d654cf67b11bbd6ca16637c4e8a38f80a5a69/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", size = 140135 }, + { url = "https://files.pythonhosted.org/packages/f2/41/6190102ad521a8aa888519bb014a74251ac4586cde9b38e790901684f9ab/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", size = 150413 }, + { url = "https://files.pythonhosted.org/packages/7b/ab/f47b0159a69eab9bd915591106859f49670c75f9a19082505ff16f50efc0/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", size = 142992 }, + { url = "https://files.pythonhosted.org/packages/28/89/60f51ad71f63aaaa7e51a2a2ad37919985a341a1d267070f212cdf6c2d22/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", size = 144871 }, + { url = "https://files.pythonhosted.org/packages/0c/48/0050550275fea585a6e24460b42465020b53375017d8596c96be57bfabca/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", size = 146756 }, + { url = "https://files.pythonhosted.org/packages/dc/b5/47f8ee91455946f745e6c9ddbb0f8f50314d2416dd922b213e7d5551ad09/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", size = 141034 }, + { url = "https://files.pythonhosted.org/packages/84/79/5c731059ebab43e80bf61fa51666b9b18167974b82004f18c76378ed31a3/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", size = 149434 }, + { url = "https://files.pythonhosted.org/packages/ca/f3/0719cd09fc4dc42066f239cb3c48ced17fc3316afca3e2a30a4756fe49ab/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", size = 152443 }, + { url = "https://files.pythonhosted.org/packages/f7/0e/c6357297f1157c8e8227ff337e93fd0a90e498e3d6ab96b2782204ecae48/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", size = 150294 }, + { url = "https://files.pythonhosted.org/packages/54/9a/acfa96dc4ea8c928040b15822b59d0863d6e1757fba8bd7de3dc4f761c13/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", size = 145314 }, + { url = "https://files.pythonhosted.org/packages/73/1c/b10a63032eaebb8d7bcb8544f12f063f41f5f463778ac61da15d9985e8b6/charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", size = 94724 }, + { url = "https://files.pythonhosted.org/packages/c5/77/3a78bf28bfaa0863f9cfef278dbeadf55efe064eafff8c7c424ae3c4c1bf/charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", size = 102159 }, + { 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 = "codecov-cli" +version = "0.1.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "httpx" }, + { name = "ijson" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "smart-open" }, + { name = "tree-sitter" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/02/eb826787da15c87d970ac817ad4d97c61a80ee434cc2aac0ba25fce45ec4/codecov-cli-0.1.13.tar.gz", hash = "sha256:4f4dd59469f9324803f98bd8573ce78636e8802ffc99e967f0fb63ca30df028b", size = 261659 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/20/70dfcf53af8471404f571388accd02b9be7521e5354376ebc8a2799c588f/codecov_cli-0.1.13-cp310-cp310-macosx_12_6_x86_64.whl", hash = "sha256:13c87a1e55eab7bf981eac4bacafe5e06af7dc5aebe087de53981716b4da28df", size = 276278 }, + { url = "https://files.pythonhosted.org/packages/4b/3a/2f7526e8be590638701c1d3797835e30743914d0e80b63b56acd3c33a7cf/codecov_cli-0.1.13-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:6acd8bb8e34519f19b83c82bc216cafe06b1c086fd4f3f8f83f503a8b1f11aab", size = 276280 }, + { url = "https://files.pythonhosted.org/packages/9d/e9/d18725208ac8d2fdb0c158795b1a0f4c5d8a4f9f3a445cb8a07ae2615030/codecov_cli-0.1.13-cp310-cp310-win_amd64.whl", hash = "sha256:43239e136bc4b3ef4450c1d6c1976ab78de6d40ee4f16a23ea5d1eed2995a97b", size = 276271 }, +] + +[[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 = "constantly" +version = "23.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/6f/cb2a94494ff74aa9528a36c5b1422756330a75a8367bf20bd63171fc324d/constantly-23.10.4.tar.gz", hash = "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd", size = 13300 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/40/c199d095151addf69efdb4b9ca3a4f20f70e20508d6222bffb9b76f58573/constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9", size = 13547 }, +] + +[[package]] +name = "covdefaults" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/ee/9a6f2611f72e4c5657ae5542a510cf4164d2c673687c0ea73bb1cbd85b4d/covdefaults-2.3.0.tar.gz", hash = "sha256:4e99f679f12d792bc62e5510fa3eb59546ed47bd569e36e4fddc4081c9c3ebf7", size = 4835 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/4c/823bc951445aa97e5a1b7e337690db3abf85212c8d138e170922e7916ac8/covdefaults-2.3.0-py2.py3-none-any.whl", hash = "sha256:2832961f6ffcfe4b57c338bc3418a3526f495c26fb9c54565409c5532f7c41be", size = 5144 }, +] + +[[package]] +name = "coverage" +version = "7.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690 }, + { url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127 }, + { url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654 }, + { url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598 }, + { url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732 }, + { url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816 }, + { url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325 }, + { url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418 }, + { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343 }, + { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136 }, + { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796 }, + { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244 }, + { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279 }, + { url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859 }, + { url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549 }, + { url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477 }, + { url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134 }, + { url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910 }, + { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348 }, + { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230 }, + { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983 }, + { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221 }, + { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342 }, + { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371 }, + { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455 }, + { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924 }, + { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252 }, + { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897 }, + { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606 }, + { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373 }, + { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007 }, + { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269 }, + { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886 }, + { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037 }, + { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038 }, + { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690 }, + { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765 }, + { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611 }, + { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671 }, + { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368 }, + { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758 }, + { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035 }, + { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839 }, + { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569 }, + { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927 }, + { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401 }, + { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301 }, + { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598 }, + { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307 }, + { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453 }, + { url = "https://files.pythonhosted.org/packages/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", size = 206674 }, + { url = "https://files.pythonhosted.org/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", size = 207101 }, + { url = "https://files.pythonhosted.org/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", size = 236554 }, + { url = "https://files.pythonhosted.org/packages/a6/94/d3055aa33d4e7e733d8fa309d9adf147b4b06a82c1346366fc15a2b1d5fa/coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", size = 234440 }, + { url = "https://files.pythonhosted.org/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", size = 235889 }, + { url = "https://files.pythonhosted.org/packages/f4/63/df50120a7744492710854860783d6819ff23e482dee15462c9a833cc428a/coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", size = 235142 }, + { url = "https://files.pythonhosted.org/packages/3a/5d/9d0acfcded2b3e9ce1c7923ca52ccc00c78a74e112fc2aee661125b7843b/coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", size = 233805 }, + { url = "https://files.pythonhosted.org/packages/c4/56/50abf070cb3cd9b1dd32f2c88f083aab561ecbffbcd783275cb51c17f11d/coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", size = 234655 }, + { url = "https://files.pythonhosted.org/packages/25/ee/b4c246048b8485f85a2426ef4abab88e48c6e80c74e964bea5cd4cd4b115/coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", size = 209296 }, + { url = "https://files.pythonhosted.org/packages/5c/1c/96cf86b70b69ea2b12924cdf7cabb8ad10e6130eab8d767a1099fbd2a44f/coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", size = 210137 }, + { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688 }, + { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120 }, + { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249 }, + { url = "https://files.pythonhosted.org/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237 }, + { url = "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311 }, + { url = "https://files.pythonhosted.org/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453 }, + { url = "https://files.pythonhosted.org/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958 }, + { url = "https://files.pythonhosted.org/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938 }, + { url = "https://files.pythonhosted.org/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352 }, + { url = "https://files.pythonhosted.org/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153 }, + { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[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/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/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 = "cssutils" +version = "2.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/9f/329d26121fe165be44b1dfff21aa0dc348f04633931f1d20ed6cf448a236/cssutils-2.11.1.tar.gz", hash = "sha256:0563a76513b6af6eebbe788c3bf3d01c920e46b3f90c8416738c5cfc773ff8e2", size = 711657 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/ec/bb273b7208c606890dc36540fe667d06ce840a6f62f9fae7e658fcdc90fb/cssutils-2.11.1-py3-none-any.whl", hash = "sha256:a67bfdfdff4f3867fab43698ec4897c1a828eca5973f4073321b3bccaf1199b1", size = 385747 }, +] + +[[package]] +name = "daphne" +version = "4.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "autobahn" }, + { name = "twisted", extra = ["tls"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/c1/aedf180beb12395835cba791ce7239b8880009d9d37564d72b7590cde605/daphne-4.1.2.tar.gz", hash = "sha256:fcbcace38eb86624ae247c7ffdc8ac12f155d7d19eafac4247381896d6f33761", size = 37882 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/d6/466f9219281472ecc269ab1d351c5b22a3cfca2d52f72881917949e414df/daphne-4.1.2-py3-none-any.whl", hash = "sha256:618d1322bb4d875342b99dd2a10da2d9aae7ee3645f765965fdc1e658ea5290a", size = 30940 }, +] + +[[package]] +name = "deprecated" +version = "1.2.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/a3/53e7d78a6850ffdd394d7048a31a6f14e44900adedf190f9a165f6b69439/deprecated-1.2.15.tar.gz", hash = "sha256:683e561a90de76239796e6b6feac66b99030d2dd3fcf61ef996330f14bbb9b0d", size = 2977612 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/8f/c7f227eb42cfeaddce3eb0c96c60cbca37797fa7b34f8e1aeadf6c5c0983/Deprecated-1.2.15-py2.py3-none-any.whl", hash = "sha256:353bc4a8ac4bfc96800ddab349d89c25dec1079f65fd53acdcc1e0b975b21320", size = 9941 }, +] + +[[package]] +name = "dict2css" +version = "0.3.0.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cssutils" }, + { name = "domdf-python-tools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/eb/776eef1f1aa0188c0fc165c3a60b71027539f71f2eedc43ad21b060e9c39/dict2css-0.3.0.post1.tar.gz", hash = "sha256:89c544c21c4ca7472c3fffb9d37d3d926f606329afdb751dc1de67a411b70719", size = 7845 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/47/290daabcf91628f4fc0e17c75a1690b354ba067066cd14407712600e609f/dict2css-0.3.0.post1-py3-none-any.whl", hash = "sha256:f006a6b774c3e31869015122ae82c491fd25e7de4a75607a62aa3e798f837e0d", size = 25647 }, +] + +[[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.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/7d/c871f55054e403fdfd6b8f65fd6d1c4e147ed100d3e9f9ba1fe695403939/dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc", size = 332727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/a1/8c5287991ddb8d3e4662f71356d9656d91ab3a36618c3dd11b280df0d255/dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50", size = 307696 }, +] + +[[package]] +name = "docstring-parser" +version = "0.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/12/9c22a58c0b1e29271051222d8906257616da84135af9ed167c9e28f85cb3/docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e", size = 26565 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/7c/e9fcff7623954d86bdc17782036cbf715ecab1bec4847c008557affe1ca8/docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637", size = 36533 }, +] + +[[package]] +name = "docutils" +version = "0.20.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/53/a5da4f2c5739cf66290fac1431ee52aff6851c7c8ffd8264f13affd7bcdd/docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b", size = 2058365 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/87/f238c0670b94533ac0353a4e2a1a771a0cc73277b88bff23d3ae35a256c1/docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6", size = 572666 }, +] + +[[package]] +name = "domdf-python-tools" +version = "3.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.9'" }, + { name = "importlib-resources", marker = "python_full_version < '3.9'" }, + { name = "natsort" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/78/974e10c583ba9d2302e748c9585313a7f2c7ba00e4f600324f432e38fe68/domdf_python_tools-3.9.0.tar.gz", hash = "sha256:1f8a96971178333a55e083e35610d7688cd7620ad2b99790164e1fc1a3614c18", size = 103792 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/e9/7447a88b217650a74927d3444a89507986479a69b83741900eddd34167fe/domdf_python_tools-3.9.0-py3-none-any.whl", hash = "sha256:4e1ef365cbc24627d6d1e90cf7d46d8ab8df967e1237f4a26885f6986c78872e", size = 127106 }, +] + +[[package]] +name = "editorconfig" +version = "0.12.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/85/7b5c2fac7fdc37d959fab714b13b9acb75884490dcc0e8b1dc5e64105084/EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80", size = 13278 } + +[[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 = "eval-type-backport" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/ca/1601a9fa588867fe2ab6c19ed4c936929160d08a86597adf61bbd443fe57/eval_type_backport-0.2.0.tar.gz", hash = "sha256:68796cfbc7371ebf923f03bdf7bef415f3ec098aeced24e054b253a0e78f7b37", size = 8977 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/ac/aa3d8e0acbcd71140420bc752d7c9779cf3a2a3bb1d7ef30944e38b2cd39/eval_type_backport-0.2.0-py3-none-any.whl", hash = "sha256:ac2f73d30d40c5a30a80b8739a789d6bb5e49fdffa66d7912667e2015d9c9933", size = 5855 }, +] + +[[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 = "execnet" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", size = 166524 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612 }, +] + +[[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-query-parsers" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/20/3a00b889a196e8dc5bede2f168d4a14edc8b5bccc3978a9f497f0f863e79/fast_query_parsers-1.0.3.tar.gz", hash = "sha256:5200a9e02997ad51d4d76a60ea1b256a68a184b04359540eb6310a15013df68f", size = 25275 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/18/4179ac7064b4216ca42f2ed6f74e71254454acf2ec25ce6bb3ffbfda4aa6/fast_query_parsers-1.0.3-cp38-abi3-macosx_10_7_x86_64.whl", hash = "sha256:afbf71c1b4398dacfb9d84755eb026f8e759f68a066f1f3cc19e471fc342e74f", size = 766210 }, + { url = "https://files.pythonhosted.org/packages/c5/21/c8c160f61a740efc4577079eb5747a6b2cb8d1168a84a0bfda6044113768/fast_query_parsers-1.0.3-cp38-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:42f26875311d1b151c3406adfa39ec2db98df111a369d75f6fa243ec8462f147", size = 1466147 }, + { url = "https://files.pythonhosted.org/packages/51/5b/b10719598dbd14201271efd0b950c6a09efa0a3f6246fec3c192c6b7a8d2/fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66630ad423b5b1f5709f82a4d8482cd6aa2f3fa73d2c779ff1877f25dee08d55", size = 764016 }, + { url = "https://files.pythonhosted.org/packages/75/06/8861197982909bec00b180527df1e0e9791715271bfb84c8be389b6bf077/fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6e3d816c572a6fad1ae9b93713b2db0d3db6e8f594e035ad52361d668dd94a8", size = 729912 }, + { url = "https://files.pythonhosted.org/packages/f0/35/7a9a0c50588033edd9efba48f21e251dfcf77eaec2aff470988f622fbd3a/fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0bdcc0ddb4cc69d823c2c0dedd8f5affc71042db39908ad2ca06261bf388cac6", size = 1003340 }, + { url = "https://files.pythonhosted.org/packages/41/9b/5a42ddd23b85357be6764e14daa607d9b16bc6a395aae2c1cc2077e0a11d/fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6720505f2d2a764c76bcc4f3730a9dff69d9871740e46264f6605d73f9ce3794", size = 969496 }, + { url = "https://files.pythonhosted.org/packages/c3/9f/4dfa29d74276fa07c40689bfaa3b21d057249314aeb20150f0f41373d16d/fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e947e7251769593da93832a10861f59565a46149fa117ebdf25377e7b2853936", size = 939972 }, + { url = "https://files.pythonhosted.org/packages/74/34/950b6d799839c11e93566aef426b67f0a446c4906e45e592026fde894459/fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55a30b7cee0a53cddf9016b86fdad87221980d5a02a6126c491bd309755e6de9", size = 828557 }, + { url = "https://files.pythonhosted.org/packages/81/a8/ee95263abc9806c81d77be8a3420d1f4dde467a10030dde8b0fa0e63f700/fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc2b457caa38371df1a30cfdfc57bd9bfdf348367abdaf6f36533416a0b0e93", size = 863119 }, + { url = "https://files.pythonhosted.org/packages/05/d4/5eb8c9d400230b9a45a0ce47a443e9fe37b0902729f9440adef677af1f0d/fast_query_parsers-1.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5736d3c32d6ba23995fa569fe572feabcfcfc30ac9e4709e94cff6f2c456a3d1", size = 911046 }, + { url = "https://files.pythonhosted.org/packages/f8/b8/bf5e44588f6ebd81d0c53ba49c79999dc54cb0fe81ad6dde6fed2cd45b56/fast_query_parsers-1.0.3-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:3a6377eb0c5b172fbc77c3f96deaf1e51708b4b96d27ce173658bf11c1c00b20", size = 962966 }, + { url = "https://files.pythonhosted.org/packages/6f/a9/132572b9f40c2635fdedb7a1cb6cedd9c880f8ffbbfdd6215ee493bb6936/fast_query_parsers-1.0.3-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:7ca6be04f443a1b055e910ccad01b1d72212f269a530415df99a87c5f1e9c927", size = 965422 }, + { url = "https://files.pythonhosted.org/packages/ea/58/942327d3f2694b8f1a2fffaaaef1cc3147571852473a80070ebd6156a62e/fast_query_parsers-1.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a70d4d8852606f2dd5b798ab628b9d8dc6970ddfdd9e96f4543eb0cc89a74fb5", size = 967734 }, + { url = "https://files.pythonhosted.org/packages/0a/e3/21bc18edc003b54a2069eb854b9f92cacb5acc99e03c609487a23a673755/fast_query_parsers-1.0.3-cp38-abi3-win32.whl", hash = "sha256:14b3fab7e9a6ac1c1efaf66c3fd2a3fd1e25ede03ed14118035e530433830a11", size = 646366 }, + { url = "https://files.pythonhosted.org/packages/ae/4b/07fe4d7b5c458bdde9b0bfd8e8cb5762341af6c9727b43c2331c0cb0dbc3/fast_query_parsers-1.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:21ae5f3a209aee7d3b84bdcdb33dd79f39fc8cb608b3ae8cfcb78123758c1a16", size = 689717 }, +] + +[[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 = "fsspec" +version = "2024.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/52/f16a068ebadae42526484c31f4398e62962504e5724a8ba5dc3409483df2/fsspec-2024.10.0.tar.gz", hash = "sha256:eda2d8a4116d4f2429db8550f2457da57279247dd930bb12f821b58391359493", size = 286853 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/b2/454d6e7f0158951d8a78c2e1eb4f69ae81beb8dca5fee9809c6c99e9d0d0/fsspec-2024.10.0-py3-none-any.whl", hash = "sha256:03b9a6785766a4de40368b88906366755e2819e758b83705c88cd7cb5fe81871", size = 179641 }, +] + +[[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 }, + { url = "https://files.pythonhosted.org/packages/97/83/bdf5f69fcf304065ec7cf8fc7c08248479cfed9bcca02bf0001c07e000aa/greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9", size = 271017 }, + { url = "https://files.pythonhosted.org/packages/31/4a/2d4443adcb38e1e90e50c653a26b2be39998ea78ca1a4cf414dfdeb2e98b/greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111", size = 642888 }, + { url = "https://files.pythonhosted.org/packages/5a/c9/b5d9ac1b932aa772dd1eb90a8a2b30dbd7ad5569dcb7fdac543810d206b4/greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81", size = 655451 }, + { url = "https://files.pythonhosted.org/packages/a8/18/218e21caf7caba5b2236370196eaebc00987d4a2b2d3bf63cc4d4dd5a69f/greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba", size = 651409 }, + { url = "https://files.pythonhosted.org/packages/a7/25/de419a2b22fa6e18ce3b2a5adb01d33ec7b2784530f76fa36ba43d8f0fac/greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8", size = 650661 }, + { url = "https://files.pythonhosted.org/packages/d8/88/0ce16c0afb2d71d85562a7bcd9b092fec80a7767ab5b5f7e1bbbca8200f8/greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1", size = 605959 }, + { url = "https://files.pythonhosted.org/packages/5a/10/39a417ad0afb0b7e5b150f1582cdeb9416f41f2e1df76018434dfac4a6cc/greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd", size = 1132341 }, + { url = "https://files.pythonhosted.org/packages/9f/f5/e9b151ddd2ed0508b7a47bef7857e46218dbc3fd10e564617a3865abfaac/greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7", size = 1159409 }, + { url = "https://files.pythonhosted.org/packages/86/97/2c86989ca4e0f089fbcdc9229c972a01ef53abdafd5ae89e0f3dcdcd4adb/greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef", size = 281126 }, + { url = "https://files.pythonhosted.org/packages/d3/50/7b7a3e10ed82c760c1fd8d3167a7c95508e9fdfc0b0604f05ed1a9a9efdc/greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d", size = 298285 }, + { url = "https://files.pythonhosted.org/packages/8c/82/8051e82af6d6b5150aacb6789a657a8afd48f0a44d8e91cb72aaaf28553a/greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3", size = 270027 }, + { url = "https://files.pythonhosted.org/packages/f9/74/f66de2785880293780eebd18a2958aeea7cbe7814af1ccef634f4701f846/greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42", size = 634822 }, + { url = "https://files.pythonhosted.org/packages/68/23/acd9ca6bc412b02b8aa755e47b16aafbe642dde0ad2f929f836e57a7949c/greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f", size = 646866 }, + { url = "https://files.pythonhosted.org/packages/a9/ab/562beaf8a53dc9f6b2459f200e7bc226bb07e51862a66351d8b7817e3efd/greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437", size = 641985 }, + { url = "https://files.pythonhosted.org/packages/03/d3/1006543621f16689f6dc75f6bcf06e3c23e044c26fe391c16c253623313e/greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145", size = 641268 }, + { url = "https://files.pythonhosted.org/packages/2f/c1/ad71ce1b5f61f900593377b3f77b39408bce5dc96754790311b49869e146/greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c", size = 597376 }, + { url = "https://files.pythonhosted.org/packages/f7/ff/183226685b478544d61d74804445589e069d00deb8ddef042699733950c7/greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e", size = 1123359 }, + { url = "https://files.pythonhosted.org/packages/c0/8b/9b3b85a89c22f55f315908b94cd75ab5fed5973f7393bbef000ca8b2c5c1/greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e", size = 1147458 }, + { url = "https://files.pythonhosted.org/packages/b8/1c/248fadcecd1790b0ba793ff81fa2375c9ad6442f4c748bf2cc2e6563346a/greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c", size = 281131 }, + { url = "https://files.pythonhosted.org/packages/ae/02/e7d0aef2354a38709b764df50b2b83608f0621493e47f47694eb80922822/greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22", size = 298306 }, +] + +[[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 = "h2" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/32/fec683ddd10629ea4ea46d206752a95a2d8a48c22521edd70b142488efe1/h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb", size = 2145593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e5/db6d438da759efbb488c4f3fbdab7764492ff3c3f953132efa6b9f0e9e53/h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d", size = 57488 }, +] + +[[package]] +name = "hiredis" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/80/740fb0dfa7a42416ce8376490f41dcdb1e5deed9c3739dfe4200fad865a9/hiredis-3.0.0.tar.gz", hash = "sha256:fed8581ae26345dea1f1e0d1a96e05041a727a45e7d8d459164583e23c6ac441", size = 87581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/cc/41521d38c77f404c31e08a0118f369f37dc6a9e19cf315dbbc8b0b8afaba/hiredis-3.0.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:4b182791c41c5eb1d9ed736f0ff81694b06937ca14b0d4dadde5dadba7ff6dae", size = 81483 }, + { url = "https://files.pythonhosted.org/packages/99/35/0138fe68b0da01ea91ad67910577905b7f4a34b5c11e2f665d44067c52df/hiredis-3.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:13c275b483a052dd645eb2cb60d6380f1f5215e4c22d6207e17b86be6dd87ffa", size = 44763 }, + { url = "https://files.pythonhosted.org/packages/45/53/64fa74d43c17a406c2dc3cb4f1a3729ac00c5451f31f5940ca577b24afa9/hiredis-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1018cc7f12824506f165027eabb302735b49e63af73eb4d5450c66c88f47026", size = 42452 }, + { url = "https://files.pythonhosted.org/packages/af/b8/40c58b7db70e3850adeac85d5fca67e2fce6bf15c2705ca6af9c8bb32b5d/hiredis-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83a29cc7b21b746cb6a480189e49f49b2072812c445e66a9e38d2004d496b81c", size = 165712 }, + { url = "https://files.pythonhosted.org/packages/ff/8e/7afd36941d58cb0a7f0142ba3a043a5b3743dfff60596e98b355fb048113/hiredis-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e241fab6332e8fb5f14af00a4a9c6aefa22f19a336c069b7ddbf28ef8341e8d6", size = 176842 }, + { url = "https://files.pythonhosted.org/packages/ff/39/482970200e65cdcea037a595083e145fc089b8368312f6f2b0d3c5a7c266/hiredis-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1fb8de899f0145d6c4d5d4bd0ee88a78eb980a7ffabd51e9889251b8f58f1785", size = 166127 }, + { url = "https://files.pythonhosted.org/packages/3a/2b/655e8b4b54ff28c88e2ac536d4aa24c9119c6160169c043351a91db69bca/hiredis-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b23291951959141173eec10f8573538e9349fa27f47a0c34323d1970bf891ee5", size = 165983 }, + { url = "https://files.pythonhosted.org/packages/81/d8/bc917412f95da9904a83a04263aa2760051c118d0199eac7250623bfcf17/hiredis-3.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e421ac9e4b5efc11705a0d5149e641d4defdc07077f748667f359e60dc904420", size = 162249 }, + { url = "https://files.pythonhosted.org/packages/77/93/d6585264bb50f9f79537429fa90f4a2a5c29fd5e70d57dec7705ff161a7c/hiredis-3.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:77c8006c12154c37691b24ff293c077300c22944018c3ff70094a33e10c1d795", size = 160013 }, + { url = "https://files.pythonhosted.org/packages/48/a5/302868a60e963c1b768bd5622f125f5b38a3ea084bdcb374c9251dcc7c02/hiredis-3.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:41afc0d3c18b59eb50970479a9c0e5544fb4b95e3a79cf2fbaece6ddefb926fe", size = 159315 }, + { url = "https://files.pythonhosted.org/packages/82/77/c02d516ab8f31d85378916055dbf980ef7ca431d93ba1f7ac11ac4304863/hiredis-3.0.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:04ccae6dcd9647eae6025425ab64edb4d79fde8b9e6e115ebfabc6830170e3b2", size = 171008 }, + { url = "https://files.pythonhosted.org/packages/e1/28/c080805a340b418b1d022fa58465e365636c0ed201837e0fe70cc7beb0d3/hiredis-3.0.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fe91d62b0594db5ea7d23fc2192182b1a7b6973f628a9b8b2e0a42a2be721ac6", size = 163290 }, + { url = "https://files.pythonhosted.org/packages/6a/f9/caacca69987de597487360565e34dfd191ab23ce147144c13df1f2db6c8d/hiredis-3.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:99516d99316062824a24d145d694f5b0d030c80da693ea6f8c4ecf71a251d8bb", size = 161037 }, + { url = "https://files.pythonhosted.org/packages/88/3a/0d560473ca21facc1de5ba538f655aeae71303afd71f2a5e35fadee0c698/hiredis-3.0.0-cp310-cp310-win32.whl", hash = "sha256:562eaf820de045eb487afaa37e6293fe7eceb5b25e158b5a1974b7e40bf04543", size = 20034 }, + { url = "https://files.pythonhosted.org/packages/9c/af/23c2ce80faffb0ceb1775fe4581829c229400d6faacc0e2567ae179e8bc2/hiredis-3.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a1c81c89ed765198da27412aa21478f30d54ef69bf5e4480089d9c3f77b8f882", size = 21863 }, + { url = "https://files.pythonhosted.org/packages/42/3e/502e2ce2487673214fbb4cc733b1a279bc71309a689803d9ba8ad6f2fa8f/hiredis-3.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:4664dedcd5933364756d7251a7ea86d60246ccf73a2e00912872dacbfcef8978", size = 81442 }, + { url = "https://files.pythonhosted.org/packages/18/0b/171d85b2ee0ac51f94e993a323beffdb6b273b838a4f86d9abaaca22e2f7/hiredis-3.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:47de0bbccf4c8a9f99d82d225f7672b9dd690d8fd872007b933ef51a302c9fa6", size = 44742 }, + { url = "https://files.pythonhosted.org/packages/6a/67/466e0b16caff07bc8df8f3ff8b0b279f81066e0fb6a201b0ec66288fe5a4/hiredis-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e43679eca508ba8240d016d8cca9d27342d70184773c15bea78a23c87a1922f1", size = 42424 }, + { url = "https://files.pythonhosted.org/packages/01/50/e1f21e1cc9426bdf62e9ca8106294fbc3e5d27ddbae2e85e47fb9f251d1b/hiredis-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13c345e7278c210317e77e1934b27b61394fee0dec2e8bd47e71570900f75823", size = 166331 }, + { url = "https://files.pythonhosted.org/packages/98/40/8d8e4e15045ce066570f82f49604c6273b186eda1e5c9b93b450dd25d7b9/hiredis-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00018f22f38530768b73ea86c11f47e8d4df65facd4e562bd78773bd1baef35e", size = 177350 }, + { url = "https://files.pythonhosted.org/packages/5d/9c/f7b6d7afa2bd9c6671de853069222d9d874725e387100dfb0f1a22aab122/hiredis-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ea3a86405baa8eb0d3639ced6926ad03e07113de54cb00fd7510cb0db76a89d", size = 166794 }, + { url = "https://files.pythonhosted.org/packages/53/0c/1076e0c045412081ec44dc81969373cda15c093a0692e10f2941e154e583/hiredis-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c073848d2b1d5561f3903879ccf4e1a70c9b1e7566c7bdcc98d082fa3e7f0a1d", size = 166566 }, + { url = "https://files.pythonhosted.org/packages/05/69/e081b023f86b0128fcf9f76c8ed5a5f9426895ad86de234b0332c18a57b8/hiredis-3.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a8dffb5f5b3415a4669d25de48b617fd9d44b0bccfc4c2ab24b06406ecc9ecb", size = 162561 }, + { url = "https://files.pythonhosted.org/packages/96/e0/7f957fb2158c6f6800b6faa2f90bedcc485ca038a2d42166761d400683a3/hiredis-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:22c17c96143c2a62dfd61b13803bc5de2ac526b8768d2141c018b965d0333b66", size = 160472 }, + { url = "https://files.pythonhosted.org/packages/5c/31/d68020aa6276bd1a7436ece96d540ad17c204d97285639e0757ef1c3d430/hiredis-3.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c3ece960008dab66c6b8bb3a1350764677ee7c74ccd6270aaf1b1caf9ccebb46", size = 159705 }, + { url = "https://files.pythonhosted.org/packages/f7/68/5d101f8ffd764a96c2b959815adebb1e4b7e06db68122f9d3dbbc19b81eb/hiredis-3.0.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f75999ae00a920f7dce6ecae76fa5e8674a3110e5a75f12c7a2c75ae1af53396", size = 171498 }, + { url = "https://files.pythonhosted.org/packages/83/86/66131743a2012f668f84aa2eddc07e7b2462b4a07a753b27125f14e4b8bc/hiredis-3.0.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e069967cbd5e1900aafc4b5943888f6d34937fc59bf8918a1a546cb729b4b1e4", size = 163951 }, + { url = "https://files.pythonhosted.org/packages/a5/ea/58976d9c21086975a90c7fa2337591ea3903eeb55083e366b5ea36b99ca5/hiredis-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0aacc0a78e1d94d843a6d191f224a35893e6bdfeb77a4a89264155015c65f126", size = 161566 }, + { url = "https://files.pythonhosted.org/packages/39/69/cdb255e3d37f82f31f4b7b2db5bbd8500eae8d22c0d7992fe474fd02babd/hiredis-3.0.0-cp311-cp311-win32.whl", hash = "sha256:719c32147ba29528cb451f037bf837dcdda4ff3ddb6cdb12c4216b0973174718", size = 20037 }, + { url = "https://files.pythonhosted.org/packages/9d/cf/40d209e0458ac28a26973d1449df2922c7b8259f7f88d7738d11c87f9ff6/hiredis-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:bdc144d56333c52c853c31b4e2e52cfbdb22d3da4374c00f5f3d67c42158970f", size = 21862 }, + { url = "https://files.pythonhosted.org/packages/ae/09/0a3eace00115d8c82a8e7d8e58e60aacec10334f4f1512f09ffbac3252e3/hiredis-3.0.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:484025d2eb8f6348f7876fc5a2ee742f568915039fcb31b478fd5c242bb0fe3a", size = 81540 }, + { url = "https://files.pythonhosted.org/packages/1c/e8/1a7a5ded4fb11e91aafc5ba5518392f22883d54e79c4b47f188fb712ea46/hiredis-3.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:fcdb552ffd97151dab8e7bc3ab556dfa1512556b48a367db94b5c20253a35ee1", size = 44814 }, + { url = "https://files.pythonhosted.org/packages/3b/f5/4e055dc9b55484644afb18063f28649cdbd19be4f15bc152bd633dccd6f7/hiredis-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bb6f9fd92f147ba11d338ef5c68af4fd2908739c09e51f186e1d90958c68cc1", size = 42478 }, + { url = "https://files.pythonhosted.org/packages/65/7b/e06f55b9dcdf10cb6b3f08d7917d3080096cd83deaef1bd4927720fbb280/hiredis-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa86bf9a0ed339ec9e8a9a9d0ae4dccd8671625c83f9f9f2640729b15e07fbfd", size = 168303 }, + { url = "https://files.pythonhosted.org/packages/f4/16/081e90137bb896acd9dc2e1e68480cc84d652af4d959e75e52d6ce9dd602/hiredis-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e194a0d5df9456995d8f510eab9f529213e7326af6b94770abf8f8b7952ddcaa", size = 179151 }, + { url = "https://files.pythonhosted.org/packages/1e/0f/f5aba1c82977f4b639e5b450c0d8685333f1200cd1972647eb3f4d972e55/hiredis-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a1df39d74ec507d79c7a82c8063eee60bf80537cdeee652f576059b9cdd15c", size = 168580 }, + { url = "https://files.pythonhosted.org/packages/60/86/aa24c20f6d3038bf244bc60a2fe8cde61fb3c0d6a82e2bed30b08d55f96c/hiredis-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f91456507427ba36fd81b2ca11053a8e112c775325acc74e993201ea912d63e9", size = 169147 }, + { url = "https://files.pythonhosted.org/packages/6e/03/a4c7a28b6320ef3e36062c1c51e9d66e889c9e09ee7d7ae38b8a2ffdb365/hiredis-3.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9862db92ef67a8a02e0d5370f07d380e14577ecb281b79720e0d7a89aedb9ee5", size = 164722 }, + { url = "https://files.pythonhosted.org/packages/cd/66/d60106b56ba0ddd9789656d204a577591ff0cd91ab94178bb96c84d0d918/hiredis-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d10fcd9e0eeab835f492832b2a6edb5940e2f1230155f33006a8dfd3bd2c94e4", size = 162561 }, + { url = "https://files.pythonhosted.org/packages/6a/30/f33f2b782096efe9fe6b24c67a4df13b5055d9c859f615a74fb4f18cce41/hiredis-3.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:48727d7d405d03977d01885f317328dc21d639096308de126c2c4e9950cbd3c9", size = 161388 }, + { url = "https://files.pythonhosted.org/packages/45/02/34d9b151f9ea4655bfe00e0230f7db8fd8a52c7b7bd728efdf1c17655860/hiredis-3.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e0bb6102ebe2efecf8a3292c6660a0e6fac98176af6de67f020bea1c2343717", size = 173561 }, + { url = "https://files.pythonhosted.org/packages/cf/54/68285d208918b6d83e32d872d8dcbf8d479ed2c74b863b836e48a2702a3f/hiredis-3.0.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:df274e3abb4df40f4c7274dd3e587dfbb25691826c948bc98d5fead019dfb001", size = 165914 }, + { url = "https://files.pythonhosted.org/packages/56/4f/5f36865f9f032caf00d603ff9cbde21506d2b1e0e0ce0b5d2ce2851411c9/hiredis-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:034925b5fb514f7b11aac38cd55b3fd7e9d3af23bd6497f3f20aa5b8ba58e232", size = 163968 }, + { url = "https://files.pythonhosted.org/packages/d3/ee/c38693bd1dbce34806ecc3536dc425e87e420030de7018194865511860c2/hiredis-3.0.0-cp312-cp312-win32.whl", hash = "sha256:120f2dda469b28d12ccff7c2230225162e174657b49cf4cd119db525414ae281", size = 20189 }, + { url = "https://files.pythonhosted.org/packages/4e/67/f50b45071bb8652fa9a28a84ee470a02042fb7a096a16f3c08842f2a5c2b/hiredis-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:e584fe5f4e6681d8762982be055f1534e0170f6308a7a90f58d737bab12ff6a8", size = 21971 }, + { url = "https://files.pythonhosted.org/packages/1a/f4/d0c39512eee1a4f3bd6b14bc0ab3f8e13b45a68be58b41916468ecffd2e4/hiredis-3.0.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:122171ff47d96ed8dd4bba6c0e41d8afaba3e8194949f7720431a62aa29d8895", size = 81496 }, + { url = "https://files.pythonhosted.org/packages/b7/57/1bf54704603c6edef75a1311b43468f01cd78908e2823c0646dbf08255ed/hiredis-3.0.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:ba9fc605ac558f0de67463fb588722878641e6fa1dabcda979e8e69ff581d0bd", size = 44770 }, + { url = "https://files.pythonhosted.org/packages/e8/a4/7f4826236ff3cafd94aa2bdb31498f16949929adf05d56fe85cbc32abfc7/hiredis-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a631e2990b8be23178f655cae8ac6c7422af478c420dd54e25f2e26c29e766f1", size = 42459 }, + { url = "https://files.pythonhosted.org/packages/1a/92/bc29d66789c6cf6e3ba21589f0e918893feb9dc096fda0a6a8ac775db7cb/hiredis-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63482db3fadebadc1d01ad33afa6045ebe2ea528eb77ccaabd33ee7d9c2bad48", size = 166802 }, + { url = "https://files.pythonhosted.org/packages/eb/de/97204c87a023d0f6bdd25c65bb1b2c1ce69b96aeac16a810df068cd28cfb/hiredis-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f669212c390eebfbe03c4e20181f5970b82c5d0a0ad1df1785f7ffbe7d61150", size = 177667 }, + { url = "https://files.pythonhosted.org/packages/bb/fe/a421f3cf94099c5d0493dac1761506c9e4a6d7445021e5bd5b384a97109a/hiredis-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a49ef161739f8018c69b371528bdb47d7342edfdee9ddc75a4d8caddf45a6e", size = 167172 }, + { url = "https://files.pythonhosted.org/packages/98/7f/834353b508fd183d5440a812773d8695b2c6878fd4dbd87199d18a1b44a3/hiredis-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98a152052b8878e5e43a2e3a14075218adafc759547c98668a21e9485882696c", size = 166944 }, + { url = "https://files.pythonhosted.org/packages/ef/6f/afda01cad5d8f212b58445c4a21e1c87c634d6617e98e928e63ce8b340dd/hiredis-3.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50a196af0ce657fcde9bf8a0bbe1032e22c64d8fcec2bc926a35e7ff68b3a166", size = 162703 }, + { url = "https://files.pythonhosted.org/packages/86/b9/6dd603b027f5b1ce370b4179412ca8e1d2b1e5f61a9cb359981056215139/hiredis-3.0.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f2f312eef8aafc2255e3585dcf94d5da116c43ef837db91db9ecdc1bc930072d", size = 160278 }, + { url = "https://files.pythonhosted.org/packages/4c/9c/4444140eccbaddd77217657040d80056ee822917d67806884ac7bf776a16/hiredis-3.0.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:6ca41fa40fa019cde42c21add74aadd775e71458051a15a352eabeb12eb4d084", size = 159515 }, + { url = "https://files.pythonhosted.org/packages/da/b8/49d4685ba10e5d808b0736b5a478c50011590c23a8998f83219aa812d918/hiredis-3.0.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:6eecb343c70629f5af55a8b3e53264e44fa04e155ef7989de13668a0cb102a90", size = 171232 }, + { url = "https://files.pythonhosted.org/packages/1b/98/6864287631dd1e2acce42bae26c25ac58f9ff1874e460d825def4f550ebe/hiredis-3.0.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:c3fdad75e7837a475900a1d3a5cc09aa024293c3b0605155da2d42f41bc0e482", size = 163478 }, + { url = "https://files.pythonhosted.org/packages/33/31/7d75a335f4d744439c3c694c5aeb5e8257d846013aee5580f59633c2871b/hiredis-3.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8854969e7480e8d61ed7549eb232d95082a743e94138d98d7222ba4e9f7ecacd", size = 161171 }, + { url = "https://files.pythonhosted.org/packages/57/92/1a870e1fcab1e70221e4d47f0b26749760c4c9daebf825603544b8a56373/hiredis-3.0.0-cp38-cp38-win32.whl", hash = "sha256:f114a6c86edbf17554672b050cce72abf489fe58d583c7921904d5f1c9691605", size = 20022 }, + { url = "https://files.pythonhosted.org/packages/0d/6b/5d1853b9f6db1cf40c765930279a02e5a2536b1073a65395e752120981cc/hiredis-3.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:7d99b91e42217d7b4b63354b15b41ce960e27d216783e04c4a350224d55842a4", size = 21888 }, + { url = "https://files.pythonhosted.org/packages/d7/3b/d4719b058647b59d23962dc4de9fc4b4730101d5c3539606aafc827f965b/hiredis-3.0.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:4c6efcbb5687cf8d2aedcc2c3ed4ac6feae90b8547427d417111194873b66b06", size = 81465 }, + { url = "https://files.pythonhosted.org/packages/61/fe/472c2cfdcca138584dd49fa53e0a5cba03d90739d37582fb2303ca41066c/hiredis-3.0.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5b5cff42a522a0d81c2ae7eae5e56d0ee7365e0c4ad50c4de467d8957aff4414", size = 44756 }, + { url = "https://files.pythonhosted.org/packages/5b/fa/179e62c6c909fe23064769e3668cf4456b81091be7ad026d36c43b5851cb/hiredis-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:82f794d564f4bc76b80c50b03267fe5d6589e93f08e66b7a2f674faa2fa76ebc", size = 42439 }, + { url = "https://files.pythonhosted.org/packages/f9/c7/34e337f18ce599afc0fb26cc0200dfad4f83cdc9bed2f4c62002d8a5d34c/hiredis-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7a4c1791d7aa7e192f60fe028ae409f18ccdd540f8b1e6aeb0df7816c77e4a4", size = 164975 }, + { url = "https://files.pythonhosted.org/packages/cf/66/934e046ff490b87b77963cf8dfe50a3b40cea28dbb32254c768b0854f225/hiredis-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2537b2cd98192323fce4244c8edbf11f3cac548a9d633dbbb12b48702f379f4", size = 176144 }, + { url = "https://files.pythonhosted.org/packages/a9/5c/43ebeb2e58b655f8acee72a863cb151b46d1ae1a767b65da0f231cf54f97/hiredis-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fed69bbaa307040c62195a269f82fc3edf46b510a17abb6b30a15d7dab548df", size = 165549 }, + { url = "https://files.pythonhosted.org/packages/de/23/e4e372e6a1afa07db9d15b0baa264cc4e9e420e77e676cda8244aae0f4e5/hiredis-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869f6d5537d243080f44253491bb30aa1ec3c21754003b3bddeadedeb65842b0", size = 165333 }, + { url = "https://files.pythonhosted.org/packages/ee/01/7fa60640541d697f666b1fc71b5e5cd03999547a824036eb6f4b7fc7107f/hiredis-3.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d435ae89073d7cd51e6b6bf78369c412216261c9c01662e7008ff00978153729", size = 161659 }, + { url = "https://files.pythonhosted.org/packages/b6/15/c793034d32be8d135491ccd049e5441ff95f3e07d7e49045d2ef528b9fad/hiredis-3.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:204b79b30a0e6be0dc2301a4d385bb61472809f09c49f400497f1cdd5a165c66", size = 159668 }, + { url = "https://files.pythonhosted.org/packages/b2/8a/d9dfb08be4fae5e2226a3f0e9ad2f0d5be81d4b1dd8c8dcd06ac7290c325/hiredis-3.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3ea635101b739c12effd189cc19b2671c268abb03013fd1f6321ca29df3ca625", size = 158695 }, + { url = "https://files.pythonhosted.org/packages/dd/cd/32ca226e2452b62e3422956ba6fad6566182d91fd3fe74402e5e17923e84/hiredis-3.0.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f359175197fd833c8dd7a8c288f1516be45415bb5c939862ab60c2918e1e1943", size = 170483 }, + { url = "https://files.pythonhosted.org/packages/12/0d/21002b893e9f5980b7de9afe35cd21653ae8aab2cb4cda1ba8a2d8380d97/hiredis-3.0.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac6d929cb33dd12ad3424b75725975f0a54b5b12dbff95f2a2d660c510aa106d", size = 162768 }, + { url = "https://files.pythonhosted.org/packages/79/b9/ba7c9ae8711d54f34e6c35bcfc546fd44c77deb832b07a649397be6df88d/hiredis-3.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:100431e04d25a522ef2c3b94f294c4219c4de3bfc7d557b6253296145a144c11", size = 160452 }, + { url = "https://files.pythonhosted.org/packages/04/17/913e1f784bed3f144f546416d905aff36c2daf0cad875c2bf88187ffff29/hiredis-3.0.0-cp39-cp39-win32.whl", hash = "sha256:e1a9c14ae9573d172dc050a6f63a644457df5d01ec4d35a6a0f097f812930f83", size = 20020 }, + { url = "https://files.pythonhosted.org/packages/e5/09/e6590cfdaf8ef465570e16cd49fe1b5fa84d173b57d24fcc9efbaf166542/hiredis-3.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:54a6dd7b478e6eb01ce15b3bb5bf771e108c6c148315bf194eb2ab776a3cac4d", size = 21891 }, + { url = "https://files.pythonhosted.org/packages/6c/26/fee1a29d7d0cbb76e27ac0914bb17565b1d7cfa24d58922010a667190afc/hiredis-3.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:50da7a9edf371441dfcc56288d790985ee9840d982750580710a9789b8f4a290", size = 39805 }, + { url = "https://files.pythonhosted.org/packages/c7/da/4e9fadc0615958b58e6632d6e85375062f80b60b268b21fa3f449aeee02e/hiredis-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9b285ef6bf1581310b0d5e8f6ce64f790a1c40e89c660e1320b35f7515433672", size = 36883 }, + { url = "https://files.pythonhosted.org/packages/cf/d5/cc88b23e466ee070e0109a3e7d7e7835608ad90f80d8415bf7c8c726e71d/hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dcfa684966f25b335072115de2f920228a3c2caf79d4bfa2b30f6e4f674a948", size = 47867 }, + { url = "https://files.pythonhosted.org/packages/09/5b/848006ee860cf543a8b964c17ef04a61ea16967c9b5f173557286ae1afd2/hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a41be8af1fd78ca97bc948d789a09b730d1e7587d07ca53af05758f31f4b985d", size = 48254 }, + { url = "https://files.pythonhosted.org/packages/91/41/ef57d7f6f324ea5052d707a510093ec61fde8c5f271029116490790168cf/hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:038756db735e417ab36ee6fd7725ce412385ed2bd0767e8179a4755ea11b804f", size = 55556 }, + { url = "https://files.pythonhosted.org/packages/81/52/150658b3006241f2de243e2ccb7f94cfeb74a855435e872dbde7d87f6842/hiredis-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fcecbd39bd42cef905c0b51c9689c39d0cc8b88b1671e7f40d4fb213423aef3a", size = 21938 }, + { url = "https://files.pythonhosted.org/packages/1a/d7/68088ce94cb4e346e4c0729788c9894238c27e8718283a21a4b76c6235bd/hiredis-3.0.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a131377493a59fb0f5eaeb2afd49c6540cafcfba5b0b3752bed707be9e7c4eaf", size = 39848 }, + { url = "https://files.pythonhosted.org/packages/97/dd/e25dcef9004eaf433575056bf555db12e70f96f3784acc1f38f20d9d8258/hiredis-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d22c53f0ec5c18ecb3d92aa9420563b1c5d657d53f01356114978107b00b860", size = 36899 }, + { url = "https://files.pythonhosted.org/packages/b9/0e/1b2e0cab33fbfbdb3a72aeec6e429252a87c6f5c3325fa55da090165a564/hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8a91e9520fbc65a799943e5c970ffbcd67905744d8becf2e75f9f0a5e8414f0", size = 47971 }, + { url = "https://files.pythonhosted.org/packages/3d/03/04a2ceeb865e4a52a7ddf4e2f247aa0b17e2c94e82dba9af5786b655633d/hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dc8043959b50141df58ab4f398e8ae84c6f9e673a2c9407be65fc789138f4a6", size = 48342 }, + { url = "https://files.pythonhosted.org/packages/7d/79/9e8f4da2541486d6c7912e4df374eaf15b7c186e46af54fea521c941c5e8/hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51b99cfac514173d7b8abdfe10338193e8a0eccdfe1870b646009d2fb7cbe4b5", size = 55646 }, + { url = "https://files.pythonhosted.org/packages/12/e8/af81ed090f44775917e65d648fbd07aeb8c734f0dcab8500f049e2a04772/hiredis-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:fa1fcad89d8a41d8dc10b1e54951ec1e161deabd84ed5a2c95c3c7213bdb3514", size = 21941 }, + { url = "https://files.pythonhosted.org/packages/14/98/dd848a92e3306be35cc8e868528b685b996c6dd02aa6f163c87325a5d061/hiredis-3.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:898636a06d9bf575d2c594129085ad6b713414038276a4bfc5db7646b8a5be78", size = 39797 }, + { url = "https://files.pythonhosted.org/packages/52/6c/2dc71e2af62d9405ce9bcfbab3bbaba626f90dbb7c56990e657cf829def2/hiredis-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:466f836dbcf86de3f9692097a7a01533dc9926986022c6617dc364a402b265c5", size = 36848 }, + { url = "https://files.pythonhosted.org/packages/56/9d/3e11b6167f792eb673c8ab9817fc55046ac4414f5339d81d9152f5c165cd/hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23142a8af92a13fc1e3f2ca1d940df3dcf2af1d176be41fe8d89e30a837a0b60", size = 47837 }, + { url = "https://files.pythonhosted.org/packages/52/b4/dce71d1528c03d731cbe55d6f4b5ea52020481c53fa5b0d92dc37d9c26c1/hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:793c80a3d6b0b0e8196a2d5de37a08330125668c8012922685e17aa9108c33ac", size = 48224 }, + { url = "https://files.pythonhosted.org/packages/61/0c/e17779b8789b35780054d76e0d1d2de043a02078be0996c9ff515e00be68/hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:467d28112c7faa29b7db743f40803d927c8591e9da02b6ce3d5fadc170a542a2", size = 55536 }, + { url = "https://files.pythonhosted.org/packages/da/a6/8e64ab752619273d65d7630fdfc29353e0a48fe4b19599d072533ff7997e/hiredis-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:dc384874a719c767b50a30750f937af18842ee5e288afba95a5a3ed703b1515a", size = 21917 }, +] + +[[package]] +name = "hpack" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/9b/fda93fb4d957db19b0f6b370e79d586b3e8528b20252c729c476a2c02954/hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095", size = 49117 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/34/e8b383f35b77c402d28563d2b8f83159319b509bc5f760b15d60b0abf165/hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c", size = 32611 }, +] + +[[package]] +name = "html5lib" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/b6/b55c3f49042f1df3dcd422b7f224f939892ee94f22abcf503a9b7339eaf2/html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f", size = 272215 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", size = 112173 }, +] + +[[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 }, + { url = "https://files.pythonhosted.org/packages/c2/73/e4877dfa233da9912062e49efd74d9f5deae95b4b736eb99742f8d751074/httptools-0.6.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d3f0d369e7ffbe59c4b6116a44d6a8eb4783aae027f2c0b366cf0aa964185dba", size = 202417 }, + { url = "https://files.pythonhosted.org/packages/04/06/24f105db5254d9689d9126ca09cd55c471241f26549041f33aea91a4c77e/httptools-0.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:94978a49b8f4569ad607cd4946b759d90b285e39c0d4640c6b36ca7a3ddf2efc", size = 105139 }, + { url = "https://files.pythonhosted.org/packages/32/c6/3623958d7899c439d5aeadcc936c3354baaf2d797e07670ccddbae5c4398/httptools-0.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dc6a8e399e15ea525305a2ddba998b0af5caa2566bcd79dcbe8948181eeaff", size = 455956 }, + { url = "https://files.pythonhosted.org/packages/a0/cf/3de90444de495cbab24e648278a4fecb36c5bbf9ecdeeff09fca69e94ca9/httptools-0.6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab9ba8dcf59de5181f6be44a77458e45a578fc99c31510b8c65b7d5acc3cf490", size = 453707 }, + { url = "https://files.pythonhosted.org/packages/33/92/f0928f8bae0a07d75bddff71835e554762974502165ea5ea78c624e3533e/httptools-0.6.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43", size = 434037 }, + { url = "https://files.pythonhosted.org/packages/d4/2b/618e8f2cf8b266a046c4524f4c214919762a9da4617e8b02da406e3747bc/httptools-0.6.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d54efd20338ac52ba31e7da78e4a72570cf729fac82bc31ff9199bedf1dc7440", size = 434347 }, + { url = "https://files.pythonhosted.org/packages/a3/59/b7dc35b45ae31d692427f15870ff9ab082e667b96c5606fda2cd7b385687/httptools-0.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:df959752a0c2748a65ab5387d08287abf6779ae9165916fe053e68ae1fbdc47f", size = 89888 }, + { url = "https://files.pythonhosted.org/packages/51/b1/4fc6f52afdf93b7c4304e21f6add9e981e4f857c2fa622a55dfe21b6059e/httptools-0.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003", size = 201123 }, + { url = "https://files.pythonhosted.org/packages/c2/01/e6ecb40ac8fdfb76607c7d3b74a41b464458d5c8710534d8f163b0c15f29/httptools-0.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab", size = 104507 }, + { url = "https://files.pythonhosted.org/packages/dc/24/c70c34119d209bf08199d938dc9c69164f585ed3029237b4bdb90f673cb9/httptools-0.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547", size = 449615 }, + { url = "https://files.pythonhosted.org/packages/2b/62/e7f317fed3703bd81053840cacba4e40bcf424b870e4197f94bd1cf9fe7a/httptools-0.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9", size = 448819 }, + { url = "https://files.pythonhosted.org/packages/2a/13/68337d3be6b023260139434c49d7aa466aaa98f9aee7ed29270ac7dde6a2/httptools-0.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076", size = 422093 }, + { url = "https://files.pythonhosted.org/packages/fc/b3/3a1bc45be03dda7a60c7858e55b6cd0489a81613c1908fb81cf21d34ae50/httptools-0.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd", size = 423898 }, + { url = "https://files.pythonhosted.org/packages/05/72/2ddc2ae5f7ace986f7e68a326215b2e7c32e32fd40e6428fa8f1d8065c7e/httptools-0.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6", size = 89552 }, +] + +[[package]] +name = "httpx" +version = "0.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/df/676b7cf674dd1bdc71a64ad393c89879f75e4a0ab8395165b498262ae106/httpx-0.28.0.tar.gz", hash = "sha256:0858d3bab51ba7e386637f22a61d8ccddaeec5f3fe4209da3a6168dbb91573e0", size = 141307 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/fb/a19866137577ba60c6d8b69498dc36be479b13ba454f691348ddf428f185/httpx-0.28.0-py3-none-any.whl", hash = "sha256:dc0b419a0cfeb6e8b34e85167c0da2671206f5095f1baa9663d23bcfd6b535fc", size = 73551 }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, +] + +[[package]] +name = "hypercorn" +version = "0.17.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "h11" }, + { name = "h2" }, + { name = "priority" }, + { name = "taskgroup", marker = "python_full_version < '3.11'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "wsproto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/3a/df6c27642e0dcb7aff688ca4be982f0fb5d89f2afd3096dc75347c16140f/hypercorn-0.17.3.tar.gz", hash = "sha256:1b37802ee3ac52d2d85270700d565787ab16cf19e1462ccfa9f089ca17574165", size = 44409 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/3b/dfa13a8d96aa24e40ea74a975a9906cfdc2ab2f4e3b498862a57052f04eb/hypercorn-0.17.3-py3-none-any.whl", hash = "sha256:059215dec34537f9d40a69258d323f56344805efb462959e727152b0aa504547", size = 61742 }, +] + +[[package]] +name = "hyperframe" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/2a/4747bff0a17f7281abe73e955d60d80aae537a5d203f417fa1c2e7578ebb/hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914", size = 25008 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/de/85a784bcc4a3779d1753a7ec2dee5de90e18c7bcf402e71b51fcf150b129/hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15", size = 12389 }, +] + +[[package]] +name = "hyperlink" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638 }, +] + +[[package]] +name = "hypothesis" +version = "6.113.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/32/6513cd7256f38c19a6c8a1d5ce9792bcd35c7f11651989994731f0e97672/hypothesis-6.113.0.tar.gz", hash = "sha256:5556ac66fdf72a4ccd5d237810f7cf6bdcd00534a4485015ef881af26e20f7c7", size = 408897 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/fa/4acb477b86a94571958bd337eae5baf334d21b8c98a04b594d0dad381ba8/hypothesis-6.113.0-py3-none-any.whl", hash = "sha256:d539180eb2bb71ed28a23dfe94e67c851f9b09f3ccc4125afad43f17e32e2bad", size = 469790 }, +] + +[[package]] +name = "identify" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bb/25024dbcc93516c492b75919e76f389bac754a3e4248682fba32b250c880/identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98", size = 99097 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/0c/4ef72754c050979fdcc06c744715ae70ea37e734816bb6514f79df77a42f/identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", size = 98972 }, +] + +[[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 = "ijson" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/83/28e9e93a3a61913e334e3a2e78ea9924bb9f9b1ac45898977f9d9dd6133f/ijson-3.3.0.tar.gz", hash = "sha256:7f172e6ba1bee0d4c8f8ebd639577bfe429dee0f3f96775a067b8bae4492d8a0", size = 60079 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/89/96e3608499b4a500b9bc27aa8242704e675849dd65bdfa8682b00a92477e/ijson-3.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7f7a5250599c366369fbf3bc4e176f5daa28eb6bc7d6130d02462ed335361675", size = 85009 }, + { url = "https://files.pythonhosted.org/packages/e4/7e/1098503500f5316c5f7912a51c91aca5cbc609c09ce4ecd9c4809983c560/ijson-3.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f87a7e52f79059f9c58f6886c262061065eb6f7554a587be7ed3aa63e6b71b34", size = 57796 }, + { url = "https://files.pythonhosted.org/packages/78/f7/27b8c27a285628719ff55b68507581c86b551eb162ce810fe51e3e1a25f2/ijson-3.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b73b493af9e947caed75d329676b1b801d673b17481962823a3e55fe529c8b8b", size = 57218 }, + { url = "https://files.pythonhosted.org/packages/0c/c5/1698094cb6a336a223c30e1167cc1b15cdb4bfa75399c1a2eb82fa76cc3c/ijson-3.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5576415f3d76290b160aa093ff968f8bf6de7d681e16e463a0134106b506f49", size = 117153 }, + { url = "https://files.pythonhosted.org/packages/4b/21/c206dda0945bd832cc9b0894596b0efc2cb1819a0ac61d8be1429ac09494/ijson-3.3.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e9ffe358d5fdd6b878a8a364e96e15ca7ca57b92a48f588378cef315a8b019e", size = 110781 }, + { url = "https://files.pythonhosted.org/packages/f4/f5/2d733e64577109a9b255d14d031e44a801fa20df9ccc58b54a31e8ecf9e6/ijson-3.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8643c255a25824ddd0895c59f2319c019e13e949dc37162f876c41a283361527", size = 114527 }, + { url = "https://files.pythonhosted.org/packages/8d/a8/78bfee312aa23417b86189a65f30b0edbceaee96dc6a616cc15f611187d1/ijson-3.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:df3ab5e078cab19f7eaeef1d5f063103e1ebf8c26d059767b26a6a0ad8b250a3", size = 116824 }, + { url = "https://files.pythonhosted.org/packages/5d/a4/aff410f7d6aa1a77ee2ab2d6a2d2758422726270cb149c908a9baf33cf58/ijson-3.3.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3dc1fb02c6ed0bae1b4bf96971258bf88aea72051b6e4cebae97cff7090c0607", size = 112647 }, + { url = "https://files.pythonhosted.org/packages/77/ee/2b5122dc4713f5a954267147da36e7156240ca21b04ed5295bc0cabf0fbe/ijson-3.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e9afd97339fc5a20f0542c971f90f3ca97e73d3050cdc488d540b63fae45329a", size = 114156 }, + { url = "https://files.pythonhosted.org/packages/b3/d7/ad3b266490b60c6939e8a07fd8e4b7e2002aea08eaa9572a016c3e3a9129/ijson-3.3.0-cp310-cp310-win32.whl", hash = "sha256:844c0d1c04c40fd1b60f148dc829d3f69b2de789d0ba239c35136efe9a386529", size = 48931 }, + { url = "https://files.pythonhosted.org/packages/0b/68/b9e1c743274c8a23dddb12d2ed13b5f021f6d21669d51ff7fa2e9e6c19df/ijson-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:d654d045adafdcc6c100e8e911508a2eedbd2a1b5f93f930ba13ea67d7704ee9", size = 50965 }, + { url = "https://files.pythonhosted.org/packages/fd/df/565ba72a6f4b2c833d051af8e2228cfa0b1fef17bb44995c00ad27470c52/ijson-3.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:501dce8eaa537e728aa35810656aa00460a2547dcb60937c8139f36ec344d7fc", size = 85041 }, + { url = "https://files.pythonhosted.org/packages/f0/42/1361eaa57ece921d0239881bae6a5e102333be5b6e0102a05ec3caadbd5a/ijson-3.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:658ba9cad0374d37b38c9893f4864f284cdcc7d32041f9808fba8c7bcaadf134", size = 57829 }, + { url = "https://files.pythonhosted.org/packages/f5/b0/143dbfe12e1d1303ea8d8cd6f40e95cea8f03bcad5b79708614a7856c22e/ijson-3.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2636cb8c0f1023ef16173f4b9a233bcdb1df11c400c603d5f299fac143ca8d70", size = 57217 }, + { url = "https://files.pythonhosted.org/packages/0d/80/b3b60c5e5be2839365b03b915718ca462c544fdc71e7a79b7262837995ef/ijson-3.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd174b90db68c3bcca273e9391934a25d76929d727dc75224bf244446b28b03b", size = 121878 }, + { url = "https://files.pythonhosted.org/packages/8d/eb/7560fafa4d40412efddf690cb65a9bf2d3429d6035e544103acbf5561dc4/ijson-3.3.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97a9aea46e2a8371c4cf5386d881de833ed782901ac9f67ebcb63bb3b7d115af", size = 115620 }, + { url = "https://files.pythonhosted.org/packages/51/2b/5a34c7841388dce161966e5286931518de832067cd83e6f003d93271e324/ijson-3.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c594c0abe69d9d6099f4ece17763d53072f65ba60b372d8ba6de8695ce6ee39e", size = 119200 }, + { url = "https://files.pythonhosted.org/packages/3e/b7/1d64fbec0d0a7b0c02e9ad988a89614532028ead8bb52a2456c92e6ee35a/ijson-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8e0ff16c224d9bfe4e9e6bd0395826096cda4a3ef51e6c301e1b61007ee2bd24", size = 121107 }, + { url = "https://files.pythonhosted.org/packages/d4/b9/01044f09850bc545ffc85b35aaec473d4f4ca2b6667299033d252c1b60dd/ijson-3.3.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0015354011303175eae7e2ef5136414e91de2298e5a2e9580ed100b728c07e51", size = 116658 }, + { url = "https://files.pythonhosted.org/packages/fb/0d/53856b61f3d952d299d1695c487e8e28058d01fa2adfba3d6d4b4660c242/ijson-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034642558afa57351a0ffe6de89e63907c4cf6849070cc10a3b2542dccda1afe", size = 118186 }, + { url = "https://files.pythonhosted.org/packages/95/2d/5bd86e2307dd594840ee51c4e32de953fee837f028acf0f6afb08914cd06/ijson-3.3.0-cp311-cp311-win32.whl", hash = "sha256:192e4b65495978b0bce0c78e859d14772e841724d3269fc1667dc6d2f53cc0ea", size = 48938 }, + { url = "https://files.pythonhosted.org/packages/55/e1/4ba2b65b87f67fb19d698984d92635e46d9ce9dd748ce7d009441a586710/ijson-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:72e3488453754bdb45c878e31ce557ea87e1eb0f8b4fc610373da35e8074ce42", size = 50972 }, + { url = "https://files.pythonhosted.org/packages/8a/4d/3992f7383e26a950e02dc704bc6c5786a080d5c25fe0fc5543ef477c1883/ijson-3.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:988e959f2f3d59ebd9c2962ae71b97c0df58323910d0b368cc190ad07429d1bb", size = 84550 }, + { url = "https://files.pythonhosted.org/packages/1b/cc/3d4372e0d0b02a821b982f1fdf10385512dae9b9443c1597719dd37769a9/ijson-3.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b2f73f0d0fce5300f23a1383d19b44d103bb113b57a69c36fd95b7c03099b181", size = 57572 }, + { url = "https://files.pythonhosted.org/packages/02/de/970d48b1ff9da5d9513c86fdd2acef5cb3415541c8069e0d92a151b84adb/ijson-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0ee57a28c6bf523d7cb0513096e4eb4dac16cd935695049de7608ec110c2b751", size = 56902 }, + { url = "https://files.pythonhosted.org/packages/5e/a0/4537722c8b3b05e82c23dfe09a3a64dd1e44a013a5ca58b1e77dfe48b2f1/ijson-3.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0155a8f079c688c2ccaea05de1ad69877995c547ba3d3612c1c336edc12a3a5", size = 127400 }, + { url = "https://files.pythonhosted.org/packages/b2/96/54956062a99cf49f7a7064b573dcd756da0563ce57910dc34e27a473d9b9/ijson-3.3.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ab00721304af1ae1afa4313ecfa1bf16b07f55ef91e4a5b93aeaa3e2bd7917c", size = 118786 }, + { url = "https://files.pythonhosted.org/packages/07/74/795319531c5b5504508f595e631d592957f24bed7ff51a15bc4c61e7b24c/ijson-3.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40ee3821ee90be0f0e95dcf9862d786a7439bd1113e370736bfdf197e9765bfb", size = 126288 }, + { url = "https://files.pythonhosted.org/packages/69/6a/e0cec06fbd98851d5d233b59058c1dc2ea767c9bb6feca41aa9164fff769/ijson-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3b6987a0bc3e6d0f721b42c7a0198ef897ae50579547b0345f7f02486898f5", size = 129569 }, + { url = "https://files.pythonhosted.org/packages/2a/4f/82c0d896d8dcb175f99ced7d87705057bcd13523998b48a629b90139a0dc/ijson-3.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:63afea5f2d50d931feb20dcc50954e23cef4127606cc0ecf7a27128ed9f9a9e6", size = 121508 }, + { url = "https://files.pythonhosted.org/packages/2b/b6/8973474eba4a917885e289d9e138267d3d1f052c2d93b8c968755661a42d/ijson-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b5c3e285e0735fd8c5a26d177eca8b52512cdd8687ca86ec77a0c66e9c510182", size = 127896 }, + { url = "https://files.pythonhosted.org/packages/94/25/00e66af887adbbe70002e0479c3c2340bdfa17a168e25d4ab5a27b53582d/ijson-3.3.0-cp312-cp312-win32.whl", hash = "sha256:907f3a8674e489abdcb0206723e5560a5cb1fa42470dcc637942d7b10f28b695", size = 49272 }, + { url = "https://files.pythonhosted.org/packages/25/a2/e187beee237808b2c417109ae0f4f7ee7c81ecbe9706305d6ac2a509cc45/ijson-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f890d04ad33262d0c77ead53c85f13abfb82f2c8f078dfbf24b78f59534dfdd", size = 51272 }, + { url = "https://files.pythonhosted.org/packages/b6/c0/a597a720a9f4890121f063d898c707f564ac372fc7a3fc8d044d453566e5/ijson-3.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3e8d8de44effe2dbd0d8f3eb9840344b2d5b4cc284a14eb8678aec31d1b6bea8", size = 85061 }, + { url = "https://files.pythonhosted.org/packages/58/9f/3b0ae9ed8ddb551b3ef10d11592d6fcb70e2a47279d8af5c80464b361be4/ijson-3.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9cd5c03c63ae06d4f876b9844c5898d0044c7940ff7460db9f4cd984ac7862b5", size = 57814 }, + { url = "https://files.pythonhosted.org/packages/f7/1c/3b74fc0f71a830a1f6b258a414263f779d7f94b15ae70c12bae858b6655d/ijson-3.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04366e7e4a4078d410845e58a2987fd9c45e63df70773d7b6e87ceef771b51ee", size = 57224 }, + { url = "https://files.pythonhosted.org/packages/54/b5/1a73769bb003bd3500d5ba720c471fc85b806a3184b214a7cccd6e7e0f0f/ijson-3.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de7c1ddb80fa7a3ab045266dca169004b93f284756ad198306533b792774f10a", size = 118327 }, + { url = "https://files.pythonhosted.org/packages/45/ee/8d82cb62d6306b6f1d5fbbb0fea7652ca2f345dec2c5f38830b587bc2af1/ijson-3.3.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8851584fb931cffc0caa395f6980525fd5116eab8f73ece9d95e6f9c2c326c4c", size = 112236 }, + { url = "https://files.pythonhosted.org/packages/d0/5a/8d56c9806a551b7dec97c081b3a23bf88ada527cef266647681b176e20fe/ijson-3.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdcfc88347fd981e53c33d832ce4d3e981a0d696b712fbcb45dcc1a43fe65c65", size = 115955 }, + { url = "https://files.pythonhosted.org/packages/08/f8/7fa4370ec5b16aa74dcf149812d80c077a3aa73b819a4f6e1fc4bf44c43a/ijson-3.3.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3917b2b3d0dbbe3296505da52b3cb0befbaf76119b2edaff30bd448af20b5400", size = 117681 }, + { url = "https://files.pythonhosted.org/packages/dd/a9/1f4f62c774763d2bf11cf8f3d378cd7836c7f3921c8e30d9934dd2776808/ijson-3.3.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:e10c14535abc7ddf3fd024aa36563cd8ab5d2bb6234a5d22c77c30e30fa4fb2b", size = 113630 }, + { url = "https://files.pythonhosted.org/packages/d2/ba/0e804b8bceca6027c6d3c6718ed5d280c4a3bdc2a5ade4c5438e5d12bea6/ijson-3.3.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3aba5c4f97f4e2ce854b5591a8b0711ca3b0c64d1b253b04ea7b004b0a197ef6", size = 114948 }, + { url = "https://files.pythonhosted.org/packages/28/46/b57ccd4d5ee7b008a6b8ea3c0267e6c6f004bd804fbcdc2b07c55ce681f2/ijson-3.3.0-cp38-cp38-win32.whl", hash = "sha256:b325f42e26659df1a0de66fdb5cde8dd48613da9c99c07d04e9fb9e254b7ee1c", size = 48980 }, + { url = "https://files.pythonhosted.org/packages/24/18/0707991e3b160b96e50d3425745986c1a0f8afd346b175a5716b71fa28cc/ijson-3.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:ff835906f84451e143f31c4ce8ad73d83ef4476b944c2a2da91aec8b649570e1", size = 50992 }, + { url = "https://files.pythonhosted.org/packages/43/ba/d7a3259db956332f17ba93be2980db020e10c1bd01f610ff7d980b281fbd/ijson-3.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3c556f5553368dff690c11d0a1fb435d4ff1f84382d904ccc2dc53beb27ba62e", size = 85069 }, + { url = "https://files.pythonhosted.org/packages/a4/79/97b47b9110fc5ef92d004e615526de6d16af436e7374098004fa79242440/ijson-3.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e4396b55a364a03ff7e71a34828c3ed0c506814dd1f50e16ebed3fc447d5188e", size = 57818 }, + { url = "https://files.pythonhosted.org/packages/9d/e7/69ddad6389f4d96c095e89c80b765189facfa2cb51f72f3b6fdfe4dcb815/ijson-3.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e6850ae33529d1e43791b30575070670070d5fe007c37f5d06aebc1dd152ab3f", size = 57228 }, + { url = "https://files.pythonhosted.org/packages/88/84/ba713c8e4f13b0642d7295cc94924fb21e9f26c1fbf71d47fe16f03904f6/ijson-3.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36aa56d68ea8def26778eb21576ae13f27b4a47263a7a2581ab2ef58b8de4451", size = 116369 }, + { url = "https://files.pythonhosted.org/packages/a0/27/ed16f80f7be403f2e4892b1c5eecf18c5bff57cbb23c4b059b9eb0b369cc/ijson-3.3.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7ec759c4a0fc820ad5dc6a58e9c391e7b16edcb618056baedbedbb9ea3b1524", size = 109994 }, + { url = "https://files.pythonhosted.org/packages/5d/90/5071a6f491663d3bf1f4f59acfc6d29ea0e0d1aa13a16f06f03fcc4f3497/ijson-3.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b51bab2c4e545dde93cb6d6bb34bf63300b7cd06716f195dd92d9255df728331", size = 113745 }, + { url = "https://files.pythonhosted.org/packages/de/e3/e39b7a24c156a5d70c39ffb8383231593e549d2e42dda834758f3934fea8/ijson-3.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:92355f95a0e4da96d4c404aa3cff2ff033f9180a9515f813255e1526551298c1", size = 115930 }, + { url = "https://files.pythonhosted.org/packages/f3/7a/cd669bf1c65b6b99f4d326e425ef89c02abe62abc36c134e021d8193ecfd/ijson-3.3.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8795e88adff5aa3c248c1edce932db003d37a623b5787669ccf205c422b91e4a", size = 111869 }, + { url = "https://files.pythonhosted.org/packages/dd/34/69074a83f3769f527c81952c002ae55e7c43814d1fb71621ada79f2e57b7/ijson-3.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8f83f553f4cde6d3d4eaf58ec11c939c94a0ec545c5b287461cafb184f4b3a14", size = 113322 }, + { url = "https://files.pythonhosted.org/packages/e3/d8/2762aac7d749ed443a7c3e25ad071fe143f21ea5f3f33e184e2cf8026c86/ijson-3.3.0-cp39-cp39-win32.whl", hash = "sha256:ead50635fb56577c07eff3e557dac39533e0fe603000684eea2af3ed1ad8f941", size = 48961 }, + { url = "https://files.pythonhosted.org/packages/b0/9a/16a68841edea8168a58b200d7b46a7670349ecd35a75bcb96fd84092f603/ijson-3.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:c8a9befb0c0369f0cf5c1b94178d0d78f66d9cebb9265b36be6e4f66236076b8", size = 50985 }, + { url = "https://files.pythonhosted.org/packages/c3/28/2e1cf00abe5d97aef074e7835b86a94c9a06be4629a0e2c12600792b51ba/ijson-3.3.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2af323a8aec8a50fa9effa6d640691a30a9f8c4925bd5364a1ca97f1ac6b9b5c", size = 54308 }, + { url = "https://files.pythonhosted.org/packages/04/d2/8c541c28da4f931bac8177e251efe2b6902f7c486d2d4bdd669eed4ff5c0/ijson-3.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f64f01795119880023ba3ce43072283a393f0b90f52b66cc0ea1a89aa64a9ccb", size = 66010 }, + { url = "https://files.pythonhosted.org/packages/d0/02/8fec0b9037a368811dba7901035e8e0973ebda308f57f30c42101a16a5f7/ijson-3.3.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a716e05547a39b788deaf22725490855337fc36613288aa8ae1601dc8c525553", size = 66770 }, + { url = "https://files.pythonhosted.org/packages/47/23/90c61f978c83647112460047ea0137bde9c7fe26600ce255bb3e17ea7a21/ijson-3.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:473f5d921fadc135d1ad698e2697025045cd8ed7e5e842258295012d8a3bc702", size = 64159 }, + { url = "https://files.pythonhosted.org/packages/20/af/aab1a36072590af62d848f03981f1c587ca40a391fc61e418e388d8b0d46/ijson-3.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd26b396bc3a1e85f4acebeadbf627fa6117b97f4c10b177d5779577c6607744", size = 51095 }, + { url = "https://files.pythonhosted.org/packages/23/96/1912c04d8fb7af01c641543c93959219f537bf0a3436d976257bbbff76ba/ijson-3.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:45ff05de889f3dc3d37a59d02096948ce470699f2368b32113954818b21aa74a", size = 54327 }, + { url = "https://files.pythonhosted.org/packages/89/d0/06c80770772336518b5cbc03c4230068c6b8ffba4d4196d2f71cc5a24f64/ijson-3.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efb521090dd6cefa7aafd120581947b29af1713c902ff54336b7c7130f04c47", size = 65988 }, + { url = "https://files.pythonhosted.org/packages/b4/50/3cde97b553df46eb7baf75e67a8440866f18111cd5e1f3c517dc5f95af4d/ijson-3.3.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87c727691858fd3a1c085d9980d12395517fcbbf02c69fbb22dede8ee03422da", size = 66731 }, + { url = "https://files.pythonhosted.org/packages/c7/2b/4de19c5e73e50d36259bd86e4d776d59779fdeda2238bd2a4744f87af797/ijson-3.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0420c24e50389bc251b43c8ed379ab3e3ba065ac8262d98beb6735ab14844460", size = 64264 }, + { url = "https://files.pythonhosted.org/packages/c0/c6/d7824be98f0da83dbcb6d153e553c527d48e69e1cd005f8e30ff51b1a18a/ijson-3.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8fdf3721a2aa7d96577970f5604bd81f426969c1822d467f07b3d844fa2fecc7", size = 51166 }, + { url = "https://files.pythonhosted.org/packages/ee/38/7e1988ff3b6eb4fc9f3639ac7bbb7ae3a37d574f212635e3bf0106b6d78d/ijson-3.3.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:891f95c036df1bc95309951940f8eea8537f102fa65715cdc5aae20b8523813b", size = 54336 }, + { url = "https://files.pythonhosted.org/packages/e6/8d/556e94b4f7e0c68a35597036ad9329b3edadfc6da260c749e2b55b310798/ijson-3.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed1336a2a6e5c427f419da0154e775834abcbc8ddd703004108121c6dd9eba9d", size = 66028 }, + { url = "https://files.pythonhosted.org/packages/ba/bb/3ef5b0298e8e4524ed9aa338ec224cb159b5f9b8cace05be3a6c5c01bd10/ijson-3.3.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0c819f83e4f7b7f7463b2dc10d626a8be0c85fbc7b3db0edc098c2b16ac968e", size = 66796 }, + { url = "https://files.pythonhosted.org/packages/2e/c1/d1507639ad7a9f1673a16a6e0993524a65d85e4f65cde1097039c3dfdaba/ijson-3.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33afc25057377a6a43c892de34d229a86f89ea6c4ca3dd3db0dcd17becae0dbb", size = 64215 }, + { url = "https://files.pythonhosted.org/packages/1b/36/92ea416ff6383e66d83a576347b7edd9b0aa22cd3bd16c42dbb3608a105b/ijson-3.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7914d0cf083471856e9bc2001102a20f08e82311dfc8cf1a91aa422f9414a0d6", size = 51107 }, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 }, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, +] + +[[package]] +name = "importlib-resources" +version = "6.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/be/f3e8c6081b684f176b761e6a2fef02a0be939740ed6f54109a2951d806f3/importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065", size = 43372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/6a/4604f9ae2fa62ef47b9de2fa5ad599589d28c9fd1d335f32759813dfa91e/importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717", size = 36115 }, +] + +[[package]] +name = "incremental" +version = "24.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/87/156b374ff6578062965afe30cc57627d35234369b3336cf244b240c8d8e6/incremental-24.7.2.tar.gz", hash = "sha256:fb4f1d47ee60efe87d4f6f0ebb5f70b9760db2b2574c59c8e8912be4ebd464c9", size = 28157 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/38/221e5b2ae676a3938c2c1919131410c342b6efc2baffeda395dd66eeca8f/incremental-24.7.2-py3-none-any.whl", hash = "sha256:8cb2c3431530bec48ad70513931a760f446ad6c25e8333ca5d95e24b0ed7b8fe", size = 20516 }, +] + +[[package]] +name = "inflection" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/7e/691d061b7329bc8d54edbf0ec22fbfb2afe61facb681f9aaa9bff7a27d04/inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", size = 15091 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454 }, +] + +[[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 = "jsbeautifier" +version = "1.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "editorconfig" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/3e/dd37e1a7223247e3ef94714abf572415b89c4e121c4af48e9e4c392e2ca0/jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24", size = 75606 } + +[[package]] +name = "lazy-model" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/9e/c60681be72f03845c209a86d5ce0404540c8d1818fc29bc64fc95220de5c/lazy-model-0.2.0.tar.gz", hash = "sha256:57c0e91e171530c4fca7aebc3ac05a163a85cddd941bf7527cc46c0ddafca47c", size = 8152 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/13/e37962a20f7051b2d6d286c3feb85754f9ea8c4cac302927971e910cc9f6/lazy_model-0.2.0-py3-none-any.whl", hash = "sha256:5a3241775c253e36d9069d236be8378288a93d4fc53805211fd152e04cc9c342", size = 13719 }, +] + +[[package]] +name = "libvalkey" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/49/57857ba9d02ba4df3bc1e71044f599c82ea590e928328e6b512dbf720228/libvalkey-4.0.1.tar.gz", hash = "sha256:fe60ef535bc826fc35f4019228a0a46bdce8b41fd6013a7591e822a8a17c3170", size = 109005 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/ac/c7b21f810c17527f77c8bd4145e21568a95a4eccc32bce8df4c5ba5d8863/libvalkey-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e180a893ac62e340a63e18c6dcc91fc756c9f2291b47b35ee1febec68c6d13c", size = 43044 }, + { url = "https://files.pythonhosted.org/packages/e9/67/84c88cf0e8df1d68da9a2e7f6a79e818031a7ced1b70d66c60041e8dd7b5/libvalkey-4.0.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:f51c08cae774071ea354658f9a9bb7ffb7b1743661011a28668650b130e0d063", size = 82094 }, + { url = "https://files.pythonhosted.org/packages/7a/b3/4a7bf5a0275674cb17e0204c05e16356c94406256d811317e6ba87e1f234/libvalkey-4.0.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:98a37d1cb5f4c3dde6646968b0a624d3051fd99176583a5245641050e931a682", size = 44753 }, + { url = "https://files.pythonhosted.org/packages/25/2f/aee00783de3ad3fbbadca23692f854f4b9f4b545c55db19657a4330e1bb4/libvalkey-4.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01c4051c3b772bd3032ca66c96f229412622ef0bef344f9ad645221f56082573", size = 170126 }, + { url = "https://files.pythonhosted.org/packages/1d/56/c8f80d3fa881cf79d20b537dcbc3ab5c8776db51cfa50b2a04a3191d027d/libvalkey-4.0.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf424fe1f45462ae4fea5f88b250ae86d7217a9662cfe5cd8a25208268129833", size = 165552 }, + { url = "https://files.pythonhosted.org/packages/f1/0c/e697bc18740d95dd0a3104404a0fbaaf4f1d463ac6b7ed2f6fc0c8be4b6a/libvalkey-4.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea9bdd8fc54de6ceea9e28dc28b327a443423dd1d110bb9fa1d67f02626a8679", size = 181753 }, + { url = "https://files.pythonhosted.org/packages/07/f6/49e0b1eb0cc9aee67d711386cbda604ea67752d17d9f94b62ea9866716af/libvalkey-4.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:641eed2e36408b8ba553c4606b2cfbee05286183249c76841188897d595c6639", size = 170657 }, + { url = "https://files.pythonhosted.org/packages/b4/07/205b2e63936f880d0ff8cc7cc27059aa698a0d4f02dc9194854eadce985c/libvalkey-4.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbf76b1b51bd5fe23dd09d0b7599cf6ee7a074e73a1933910e5faa1741408708", size = 170331 }, + { url = "https://files.pythonhosted.org/packages/35/93/31734676641cb36a3ac23ffedc89b328a63a2f00eba80fc0554e2dd93872/libvalkey-4.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:75918faf78b2728ef8a73438361c328d70757cdb0e8bf57fa636e0776f302d4e", size = 164756 }, + { url = "https://files.pythonhosted.org/packages/b7/fd/1f8476e45792c1bd6bb364e7f04c0274caa37767d4ed12658b1d863874cf/libvalkey-4.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:aa678090591e1c28a5f0647ce69531752e8f75e83a03e8963500941475898ccf", size = 162825 }, + { url = "https://files.pythonhosted.org/packages/23/03/c293870a74b88d8ce2c5fed2a049eb2ec2373e29ccd3eb3df32217746e00/libvalkey-4.0.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a1eff0939e0577ddc6b8b359a846c0a83cb7ed3b0688fe98f8f8cf3ba8aa04b7", size = 176181 }, + { url = "https://files.pythonhosted.org/packages/6e/b0/8624fa9195c4af93044860c9685b7667c1b7dd9bff6b62f815e65a512f24/libvalkey-4.0.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:b3ac608744fc2727eb87cdd7613f8d64b18a210b1661706d2b2de09fffd3d2d0", size = 167755 }, + { url = "https://files.pythonhosted.org/packages/f6/66/74f722d90e37addf2b1235ce8294d50751c1a406a7dd05e7b4893f474daa/libvalkey-4.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e8801cf0274b2a6b0d19ea47de351e5ce67579b8503c4f9905ab53b52fbf270e", size = 165462 }, + { url = "https://files.pythonhosted.org/packages/18/a1/16512251a897ad7022787ae395c2ecaf48449f7fce170d6656c5a27df795/libvalkey-4.0.1-cp310-cp310-win32.whl", hash = "sha256:a9438415f500c1b65fe258f102b004ef690db142a74d681d10fd82e344dc947f", size = 19468 }, + { url = "https://files.pythonhosted.org/packages/5b/a8/7819672c42b470c67a994db05dd876b88ad97d5e4ae3152a7e49172b0b1b/libvalkey-4.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:cd3495a5c4c7f04c26bd5c34feb15c13da2dab5a349756a3f42f2a15521a5197", size = 21354 }, + { url = "https://files.pythonhosted.org/packages/c4/7a/d7e4726c9a08c703fd4e824c7c644ae6c5f0ee3f1b99474a524f0149b77f/libvalkey-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f342da7200765da30e8a6a540722a8e9e689b0b0604e067290d308981d93826f", size = 43043 }, + { url = "https://files.pythonhosted.org/packages/27/ae/30ba1da48e143da9c1fa0e4cf4899bbd4402b0a0c8f6c355aa76e89b6490/libvalkey-4.0.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:0db70261f8843007ea995f7adf0d619780380ac3abc4c1ac44ad4f3e885d5594", size = 82093 }, + { url = "https://files.pythonhosted.org/packages/84/da/1c2b524ad44a7c24d7e62bb63d995bb8e59863c0f88844778c109604f83f/libvalkey-4.0.1-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:785c73ba7177777d9af01f48aa3344099815cdae3fef12c5d0f35b9b392f35bf", size = 44754 }, + { url = "https://files.pythonhosted.org/packages/f7/1f/954f1ac80371dc50efaada4ca133219e2edb265c136c7fa2af1821caf5b2/libvalkey-4.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d56ed4c6c17bfb65bf4fe0745fdb3ae6bd1111af6171294497173e3a45226d7", size = 170152 }, + { url = "https://files.pythonhosted.org/packages/cf/3f/1fbe055702041bcf2a85e056955219102e8b0aa3967482a5d68f7a3bb8c8/libvalkey-4.0.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d99b4adae993b9d8f057e150e5a2f938823d17286abcbc5cca0cb4741c530ddf", size = 165334 }, + { url = "https://files.pythonhosted.org/packages/49/ad/ef273ef578fbac6e3b336c2138e457893654da0a40c75fcc76bf28be4761/libvalkey-4.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d803812b4933a1926479aff4057f06b4332977b796038b4309d98546c56edf5", size = 181816 }, + { url = "https://files.pythonhosted.org/packages/2e/87/3b681951477e98e135dee6f8b570779875a96a0367ad886327610f9134ee/libvalkey-4.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b473dc3d005a9b57e4445cb2a9ab48f8a26ea90889458ef3cb4d3dd7b23b5a26", size = 170687 }, + { url = "https://files.pythonhosted.org/packages/13/4c/d384395d2378e6481cdcb1fb7c6e6d0a3ae0feb3dffaefbd6c1eb39cb3d1/libvalkey-4.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:195f7e78cb6d2c391dac2d0fc1bf7e65555ebad856e0c36bcc4986e0b3b6c616", size = 170453 }, + { url = "https://files.pythonhosted.org/packages/8f/d8/1c64a5704ef4f843e2ffab8d815f1c918445f92e38660a6d44c7c64d1fe1/libvalkey-4.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:79b446abdb18aefc984214de68ac5f50164550a00b703b81c2b9d9c1618f4a13", size = 164803 }, + { url = "https://files.pythonhosted.org/packages/e1/65/6d3004b011a799781f379bdba531a3c19bc5f8cd929bafa96fc066a59b12/libvalkey-4.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3453cd138a43cdcce32cbbbdc99d99472fb7905e56df8ff2f73dac5be70f0657", size = 162855 }, + { url = "https://files.pythonhosted.org/packages/fb/b8/35c8f8cedfeaf2ddb261f09e9a618a3e5ce44c8d4ea29e19ce4c1ae175d7/libvalkey-4.0.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a9acf658749ee324750643df040a62401d479de9a4507ca8f69bcf02df1b189", size = 176207 }, + { url = "https://files.pythonhosted.org/packages/fd/ea/428c9404f41bce80d946ed904b5749a5b3ac2f2a6a1a48f4da1c9b58f8b2/libvalkey-4.0.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ebfe6976a10ab6fb84d885622a39ff580803f3244a048b75fb63a97048cb894c", size = 167761 }, + { url = "https://files.pythonhosted.org/packages/2f/97/b0e5fc755c78ac23a6e7a5c81288e7b44131becc11af07ef087f54054adf/libvalkey-4.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:860862322eceb3ed2ff2031663ee42d9ff4146226af3734f818b54a70339c440", size = 165463 }, + { url = "https://files.pythonhosted.org/packages/c7/fd/9700a1edec4ebacbcb7ccdf55ac6548f2a5693b016768d721c0d520753ad/libvalkey-4.0.1-cp311-cp311-win32.whl", hash = "sha256:dd96985818cc1ddc8882dda67fa1cf711db37d0a24a4cd70897fd83a7377a11c", size = 19475 }, + { url = "https://files.pythonhosted.org/packages/85/25/d59dbdf8cb16d5c1f9215fc7cf66d2cbca6d05008eda1104b321df785647/libvalkey-4.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:4a1174d53d949f38128efc038b2d77cb79c4db127f5707ad350de0cda3aa9451", size = 21358 }, + { url = "https://files.pythonhosted.org/packages/2b/62/fb85f94411890d233c74aad7b581bb65a0f809b5757446a280a8fa42a50d/libvalkey-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3d5da92598f8e6a82a5e3dedd49dae39fce6b3089030e17681e2b73df4a6d89", size = 43057 }, + { url = "https://files.pythonhosted.org/packages/ed/cb/6f7614cab744f0e4e0eae583b2997bf22ffee4aa333e26786b91342986b6/libvalkey-4.0.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ce623bb8c37beb16d0f2c7c5b7080a3172dae4030e3bcd71326c7731883f766f", size = 82198 }, + { url = "https://files.pythonhosted.org/packages/cf/de/9515ba0f436c3e54331b66cf7652c03fc73688e0b6f22853a6fc2cc8aa23/libvalkey-4.0.1-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:00adc978a791e944e2f6b442212cd3494a8d683ed215ff785dc9384968b212b6", size = 44839 }, + { url = "https://files.pythonhosted.org/packages/21/f1/dd28a89f6c89e4fbcd95be18a04758f6f57fc69d38a7bc22a59377b277fc/libvalkey-4.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:827ea20b5ee1d99cf3d2c10da24c356c55836dd6ff1689b3fbf190b5bffe1605", size = 173314 }, + { url = "https://files.pythonhosted.org/packages/c6/17/debc72596eb3e4c27a4ae1a5b5636e99b7b5e606c072c8816358ab69fb7f/libvalkey-4.0.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f81f7d806e5dd826532c0a4b6d8bc91a485fba65a3767cfdeb796b493ac59c8c", size = 167968 }, + { url = "https://files.pythonhosted.org/packages/fd/00/451b234f5125e0b9396d7ca4b9d2ab9785e21475f4da60ff019e48912f73/libvalkey-4.0.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65fe45323bbabee8d770127c0a763182a0d540a8c1afe6537d97dcc021fc26c4", size = 184371 }, + { url = "https://files.pythonhosted.org/packages/09/c1/e10266e11034af9194feacec78221bb01db6961b662303a980c77a61f159/libvalkey-4.0.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c20ec7a26163dad23b0dfdbb81dd13ae3aa4083942b438c07dadaed88fa0ca6c", size = 173422 }, + { url = "https://files.pythonhosted.org/packages/88/ba/fe3b25281e41546ea96c41b7899d2a83a262463432280e07daed44beb7f5/libvalkey-4.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c0d5b4b01c2e4e5bad8339d8b585f42c0b10fb05a6e4469c15c43d925be6285", size = 173617 }, + { url = "https://files.pythonhosted.org/packages/05/b8/a75b6edcaabdc6245ee719357d307e2fccb375ca09d012327fbc1ef68771/libvalkey-4.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c151a43b250b6e6412c5b0a75093c49d248bbe541db91d2d7f04fd523ea616b3", size = 167110 }, + { url = "https://files.pythonhosted.org/packages/a9/ae/602254b8865a0fb21df3f3cd57815ca7e6049cd79318bfb426449b661cee/libvalkey-4.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c8f59bb57f8e02e287da54a2e1250b9d591131922c006b873e5e2cad50fc36c", size = 164932 }, + { url = "https://files.pythonhosted.org/packages/0f/c6/116f5432c8234630079f3dadcf48828db41c2bfcdfaeb36ef197a2efa380/libvalkey-4.0.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:89e79fade6e6953c786b0e2d255a2930733f5d9e7ef07cf47a4eb0db4eabba5e", size = 178905 }, + { url = "https://files.pythonhosted.org/packages/ce/00/ec095e022b7e5c2035787ad7389e0a11157ceb98f4ceae7fc44805907be0/libvalkey-4.0.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56495ab791f539dc3ee30378f9783f017e000ad8b03751ad2426003f74eee0bc", size = 170350 }, + { url = "https://files.pythonhosted.org/packages/a5/84/d8bd771b07854c58cbf8926d98fed1701a4dcbe97ef31e8fea63416fc461/libvalkey-4.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:df9d7ba691c49c632bdc953b5c0af5c50231d0ae3bb0397688f63257a12786c0", size = 168125 }, + { url = "https://files.pythonhosted.org/packages/45/88/00d5d2d0960d0023c52d25d5ae87d47b5aec995241788be5c424826727aa/libvalkey-4.0.1-cp312-cp312-win32.whl", hash = "sha256:a39ad585b3d2d48d6f5b60941f9d6a5f3f30a396ae129db15bf626316f71594f", size = 19651 }, + { url = "https://files.pythonhosted.org/packages/8f/57/a3f837524d63ed927f6ab3da3be2050f6c838279c796d5b44b617cad0047/libvalkey-4.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:7b39754c9cdf7fe704c636a2ea179c17229566e7c79af453df3a604b98879dc3", size = 21451 }, + { url = "https://files.pythonhosted.org/packages/51/3c/8571abc9b8a78281312b99348bccb35dfa161a3a3e9b963afdd473beea40/libvalkey-4.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f72346eca7408cfd6d6f407e7e548040b310ed6fdaec7d9ea67b49f20dc90a9e", size = 43065 }, + { url = "https://files.pythonhosted.org/packages/8e/08/197217ff273fde5670b84682a2d67fe372890d14595ae0a15284626975ba/libvalkey-4.0.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:66b4558ca5b8fa48fde40dfc79547779d78c5397906f5ea5671b9a75f0952ff4", size = 82206 }, + { url = "https://files.pythonhosted.org/packages/51/e1/a806533c315cf758812e4f609548ccbb51c10221e2a3c6f9aa6e17633ee9/libvalkey-4.0.1-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:33d29f8d826b59e972a8502e8547d5fef7b1a1376fa0884cf1360c15977bca50", size = 44840 }, + { url = "https://files.pythonhosted.org/packages/7b/c8/9908fdb4a5661bef8972cf94d149f5c5199c3835bba1a6f523225e2d6c37/libvalkey-4.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8adea3c5824937c1e94bb1d5bc30c57661e8bbcb1a79e8ead77b45bbe206f488", size = 173200 }, + { url = "https://files.pythonhosted.org/packages/80/d2/3b6c235393137b16dacddb04ac6b632f3998cd10cb52b1e34b2eb7bdcabf/libvalkey-4.0.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50e956e1bc0ab21e2479fb5729456ae74de808a1be799b9163bf7a25029eeb41", size = 168270 }, + { url = "https://files.pythonhosted.org/packages/79/b3/844123ae52d63d9ec97f4ad026054a0b616d83fd886adcfd2f8484939044/libvalkey-4.0.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56d6db545639a5fbb1c634621634a7f87845b7867056b1460a8299cc489e3364", size = 184290 }, + { url = "https://files.pythonhosted.org/packages/7c/13/390d1f1fac2167e5e4531cbc090ec99e974271fc05e4c0c8597db593596d/libvalkey-4.0.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b83e133826ca50506c9f49b5c4fd64ef0dc3bbc6e85bb18b781a08320bf3f0db", size = 173279 }, + { url = "https://files.pythonhosted.org/packages/6c/9f/74deb4ea77efb2cbf24c2a7364628b93f3bd02bff7203162c88e9bfe8f13/libvalkey-4.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e58b6dcea57df7ee8d80f914ed8895141fbb53d6f344b310ebe6cae3e407d0f", size = 173436 }, + { url = "https://files.pythonhosted.org/packages/db/e6/9bb87d6d2fe4ba4da960104356c2b165766cb1d420ef1d7f0f671729f3ab/libvalkey-4.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35576248ac379e755cf40c4ebe6bf735f278d46b5e449d0c8ea9f66869e3a8d4", size = 167050 }, + { url = "https://files.pythonhosted.org/packages/2e/aa/35eda3faa3a7e9a5702c1833cf2ff0fdb024661d8342b24393198db968a2/libvalkey-4.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2be9cd8533638be94956567602554bbffa65d6fc8e758cd628f317a58cbcafda", size = 164934 }, + { url = "https://files.pythonhosted.org/packages/ba/54/6439403407317d1228f8b2d5e38917ff162cd86b371bf28953e727674b20/libvalkey-4.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a03058988844b5a56f296d0d5608bbbe38df1b875246d6c6d7b442af793b5197", size = 178943 }, + { url = "https://files.pythonhosted.org/packages/f5/54/8628c445f9683d2b0f2df3cf2084ec48917d33ccf3f857b2b4b25c6ba3a8/libvalkey-4.0.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:885a2ca644a2fdaf555e9fdab2bbe7de0f91de4e2a07726751efa35417736d55", size = 170398 }, + { url = "https://files.pythonhosted.org/packages/e1/86/9780ad4d818fbb63efb3763041109fbdbe20a901413effa70a8fcf0ec56b/libvalkey-4.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:90fb5252391a8a9a3bb2a4eadd3f24a8af1032821d818e620da16854fb52503e", size = 168104 }, + { url = "https://files.pythonhosted.org/packages/61/b2/8ec653c1e1cb85a8135f6413be90e40c1d3c6732f5933f4d3990f1777d09/libvalkey-4.0.1-cp313-cp313-win32.whl", hash = "sha256:ac6d515ece9c769ce8a0692fcb0d2ceeb5a71503a7db0cbfef0c255b5fb56b19", size = 19657 }, + { url = "https://files.pythonhosted.org/packages/61/92/f0b490a7bd7749aeed9e0dfca1f73e860a82ccb3ddb729591e4324c54367/libvalkey-4.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:125ff9fdba066a97e4bbdea69649184a5f5fcb832106bbaca81b80ff8dbee55c", size = 21461 }, + { url = "https://files.pythonhosted.org/packages/eb/9b/16cf35d50004c918da27cd9f33e27a1037690b523e8d4e8244b497988d8a/libvalkey-4.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9f389b48f37e7d14fd42eb47e52b799c1624edafc2b9398b9fe2f4e204d072a4", size = 43052 }, + { url = "https://files.pythonhosted.org/packages/f6/19/865dc1a17f8a4b4f5c63cb926bf30cfff61c69446b27f83a9c7b8da65d5e/libvalkey-4.0.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:200b135258f6ada4aaacf8499e2d2d3484b39a03b2178f64d4da16eb39bcbf77", size = 82130 }, + { url = "https://files.pythonhosted.org/packages/b4/03/c12e79b6dfc1d9f90315288bf81e7c4df5ddbfeb22ade52c61d5eacb93ec/libvalkey-4.0.1-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:4aaa2b8ef0a3cc1a72abbb29667be2321ea5cd31f3b131ea0a35e4ee86caea6f", size = 44774 }, + { url = "https://files.pythonhosted.org/packages/d7/84/c6fa981a5a922dccf2c6eebb0d9cad3fbb3122c7d21ad3d9f444f67cc97f/libvalkey-4.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91143449ee77ca072799759469df20ff538b82e0c538e3f3a4afbe5f1c7bfeaf", size = 172594 }, + { url = "https://files.pythonhosted.org/packages/ad/28/e1745166d26c73cb7a6fa1df5f51f0326a4e7e79fd8a9948f81a750132e9/libvalkey-4.0.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e266a71b747cf5f8c8882966c58c080a61f8cb772bbf6dbb2d67530034ebd611", size = 167113 }, + { url = "https://files.pythonhosted.org/packages/b1/4e/61c0b3392f8f97bc169c3f9eaee8053ec514e3ee85ed3fea0b3ee0cc6072/libvalkey-4.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9bb157a7fdc91b49d274eeb7961fcdb03d32380d1c25b9bcbb4dd490944539e", size = 183759 }, + { url = "https://files.pythonhosted.org/packages/e5/d0/43c41bbbbc05e781396508ad689e3b5507701ba3c57da204bee0486bfb3b/libvalkey-4.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be5dae7eebe303f549b9cab63fb9d6b1a1730b00e5011b0cb3d3403dc2d70ad9", size = 172950 }, + { url = "https://files.pythonhosted.org/packages/80/3e/1a03a45f9dfbb634da3db90ec96c92df56fa1a844cbd7a479f790e6b513f/libvalkey-4.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60466c4bfd4d55ce6233d951627a4eacdae6888de0885695b5b6f3deafde57bf", size = 172691 }, + { url = "https://files.pythonhosted.org/packages/3c/2f/65e4a05ed217f5e59a5677c5c7fea110f3a343349f0c1b8425132f4a285c/libvalkey-4.0.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f448af77c485fabb2f93928ca759f9813bc8999f2fb0560d9c2e4870aa6e0edc", size = 165533 }, + { url = "https://files.pythonhosted.org/packages/16/6a/4cb7e3770694029c8289e041734f96b0d0203a36d6330bc1276979815191/libvalkey-4.0.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:cc4354d075614a736e62d8283fe173df66e55b5260e6c29ff851a1d3680a5d1e", size = 163574 }, + { url = "https://files.pythonhosted.org/packages/26/b7/d49605d0e0a0f998f9addf724fd20ac2142bbb3c714e80f34406d7cee1fc/libvalkey-4.0.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:6445163e102f10217532b6547edcc239f2dd0bced13fd982da10b352d0771b21", size = 176969 }, + { url = "https://files.pythonhosted.org/packages/ad/04/876ea16da27d5000d6b4d763a0fa0b43a06fb4e45b70acb70f12eeef3f84/libvalkey-4.0.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:aa162ddb08a5d9a3b2d08f6ebe92385a049077c9b4e168e5171c615fbc8155b8", size = 168622 }, + { url = "https://files.pythonhosted.org/packages/2b/4d/e208e3335988ae2d35ca07f9e7b4718ccc2c0eef7e50ca6c4149c37c37df/libvalkey-4.0.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:48cc9f0fd6724780949bddbbadda9ee338b63a8923202bd7df0e2de4feea63bf", size = 166221 }, + { url = "https://files.pythonhosted.org/packages/2b/81/828b1db1fb2dbcda215bb3806b89d2493e6c4a7ab4b529c7bcbdc6e1cc96/libvalkey-4.0.1-cp38-cp38-win32.whl", hash = "sha256:471ac81196e1bf5d00069be2bc6fee4f52744b0a3c219b51f3d3115a7903e190", size = 19490 }, + { url = "https://files.pythonhosted.org/packages/7f/a8/c34155613194fa9cbf406dd0cb05b965b8280c52d00af12062ddb609b711/libvalkey-4.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:3ff7f2e78c0e195d862cb9bda3b3fe16183713220d5f2faae653d9b187249c9b", size = 21368 }, + { url = "https://files.pythonhosted.org/packages/ce/17/d432510a70089a9781b81dd94d649d156f8af9db0c8b9603702b0ec95dcc/libvalkey-4.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e9c5975aae213a3c7251c3e8989c78b2ee39be5eace45a7d99fadc6a4a5a7bc4", size = 43036 }, + { url = "https://files.pythonhosted.org/packages/49/88/64f54ed1ce28fc1c27353ff676b8673820384af72bafa12930566c0d8843/libvalkey-4.0.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:c3ce037eeb03682c1dea8435bc0b9c118dba105cd8dca353b4a49cd741fff60f", size = 82089 }, + { url = "https://files.pythonhosted.org/packages/e4/2c/a64b3aa6fceb32026f965d983251b59a30017dd973c243ed0050400cebba/libvalkey-4.0.1-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:d9ff6deda50245842b901501e282be0399fd6c5289ae9f7a6cc5c62a2e5303d7", size = 44751 }, + { url = "https://files.pythonhosted.org/packages/a0/9a/aa4cc4075fa05d98607011efd98888bf7e9eb9c7278bc9e3ea14206cc3e8/libvalkey-4.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd77106af55d6c0fb588fe58c2daea2ccf05ffab3713266b66157add022b9623", size = 169574 }, + { url = "https://files.pythonhosted.org/packages/bd/7c/870dc7b68a0166bc216c403157e25fdd29e34ed1429b6821903cac31ad81/libvalkey-4.0.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff5e5559b3aa04f1cd630b07f320ff0bd336471e5c74ef27a30e198b1a35b7c8", size = 164870 }, + { url = "https://files.pythonhosted.org/packages/87/6c/525d00c790cfae5fc196d2518de878e15d4b54459dc71aed5137e8bf6282/libvalkey-4.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70deceeb55973d6b5911b06f7ceadeea022a2496a75aa9774453f68b70ab924b", size = 181172 }, + { url = "https://files.pythonhosted.org/packages/b0/b6/b0e6a1f509a6a11b3f925038d4e3d0fd9da18d56d3be34ed41b8a44d6c8b/libvalkey-4.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce6b42ab06ba28d7fc8d24b8da9eccda4033e9342b6d4731c823998ec0294a09", size = 170052 }, + { url = "https://files.pythonhosted.org/packages/ca/c0/bcedb0f40ee4d12694f3c7d6809d7de8b8205636fe291f1b7daf6094d8cc/libvalkey-4.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c38f42b6632fdfd39c0e361d004c0c93f9059b6c3b36626f903a371abbb8af8b", size = 169800 }, + { url = "https://files.pythonhosted.org/packages/57/2e/40dfde852a9dde4baf961531a294d02152d89c4bbd7fc72ccf798c92b951/libvalkey-4.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d2175fd547761e67f00141b7d22ea6ba730da6ae72f182b92d122ed6b9371f27", size = 164293 }, + { url = "https://files.pythonhosted.org/packages/b7/4f/34c6bd1097b33cc0eeeb514b0992639f6e5df9edf86d424d9ffc09dd6d32/libvalkey-4.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:13856099dd591714975ee4399f1c6a1b87d4a7a79960681b641b57ee70bcc5a3", size = 162324 }, + { url = "https://files.pythonhosted.org/packages/83/a8/31fd517128120e4df4aa0209519ac6a467d38ecbc898dae9100a1f288ac7/libvalkey-4.0.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:fa4dc913f28d799e755fbf2bd411bc0e2f342b9a4cdc2336aab68419b405e17d", size = 175654 }, + { url = "https://files.pythonhosted.org/packages/8d/f6/b46976335a946e3f3c1cfe8155bcb2b3ad0b6496ec87a8ca90f4da561a56/libvalkey-4.0.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:be54d0ce94ff65ff8eede2ab78635e0f582f27db3e9143a1e132910896157684", size = 167234 }, + { url = "https://files.pythonhosted.org/packages/3b/08/df3cdaed3bcd15a9470fbb2511de9b3237e3b956a1c8d835b75ff1d56c0e/libvalkey-4.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6c7500ddd11760372c3d9ace0efea0cf00834857be7f7da6321ba9a0ba01379f", size = 164994 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/7342449b847165e773ccf320189161ec26663dbc93fcc5099539a270ae09/libvalkey-4.0.1-cp39-cp39-win32.whl", hash = "sha256:e5b7cf073a416f2be03b6aebe19d626f36a1350da9170dc2947d8364a77d6c3d", size = 19489 }, + { url = "https://files.pythonhosted.org/packages/cd/9d/64a33e7141ef8cb63078a3b3e4e4821adfff401198cfa536679ca78e0247/libvalkey-4.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:d050e7ac0906fc1650c5675b0a68da53181425937986559d129da665382e444a", size = 21367 }, + { url = "https://files.pythonhosted.org/packages/39/7d/8dec58f9f2f0f4eb8b1b861a4ee5ef2c8e7b63a46f0ffbba274f5170125b/libvalkey-4.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:75528a996263e57880d12a5b1421d90f3b63d8f65e1229af02075079ba17be0a", size = 37210 }, + { url = "https://files.pythonhosted.org/packages/77/d9/857376c3e0af4988d1b8ad13e8ee964dcfae9860e1917febd4f6b0819feb/libvalkey-4.0.1-pp310-pypy310_pp73-macosx_11_0_x86_64.whl", hash = "sha256:4ee4f42a117cc172286f3180fdd2db9d74e7d3f28f97466e29d69e7c126eb084", size = 39638 }, + { url = "https://files.pythonhosted.org/packages/c2/ae/9c0fcf578a56d860a9e056e67fbdff54b33952a88b63c384bc05b0cfe215/libvalkey-4.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb120643254a1b44fee2d07a9cb2eef80e65635ac6cd82af7c76b4c09941575e", size = 48760 }, + { url = "https://files.pythonhosted.org/packages/ff/3d/76fa39c775c33e356be5b7b687b85d7fea512e1fd314a17b75f143e85b67/libvalkey-4.0.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e62c472774dd38d9e5809d4bea07ec6309a088e2102ddce8beef5a3e0d68b76", size = 56262 }, + { url = "https://files.pythonhosted.org/packages/5a/cb/60131ef56e5152829c834c9c55cc5facb0a1988ba909c23d43f13bb65e0d/libvalkey-4.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a73ce687cda35c1a07c24bfb09b3b877c3d74837b59674cded032d03d12c1e55", size = 48920 }, + { url = "https://files.pythonhosted.org/packages/e8/ac/69d01a8e2ad5c94bd261784b8e8c2be3992b48492009baf56fcd16d1ab15/libvalkey-4.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1cd593e96281b80f5831292b9befe631a88415c68ad0748fe3d69101a093c501", size = 21366 }, + { url = "https://files.pythonhosted.org/packages/28/15/39c3c69ee642ea1271be2c92ca8e628e251b898d98306953b70a37d46310/libvalkey-4.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:a50f5f7482aa64038aa93ba12c45bffd2950a5485288558adc80ebc409b20a21", size = 37244 }, + { url = "https://files.pythonhosted.org/packages/92/82/8801ecc1d4f875b7f218cc6ad9345cf7ef3c0e9eec2ada6c7fc34b6c5609/libvalkey-4.0.1-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:bda1b59ed326fc0fe909b4be38297d37cba1ee332913f1ab1a8f17bd9791e2c9", size = 39663 }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0458e8387f860c96c64242c8c78118d228a54a619a64b59d918c5a44bdf6/libvalkey-4.0.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f111810607172cada546003d444e9f0d7b2c9d1a5f305e56d365bda89adbce8d", size = 48790 }, + { url = "https://files.pythonhosted.org/packages/26/d6/e79606ad905d9ef0df36b5a4c3d933636467eef4f16dcbc719a6800de02d/libvalkey-4.0.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a2cf10a178140e94116ba467c209d38bc73962211a666e5b85e018194dd3c67e", size = 56353 }, + { url = "https://files.pythonhosted.org/packages/cb/a9/c62cfa984c864088c22d9b326abdfd83b561d28f8f2eaf9863e6189d3349/libvalkey-4.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c1b982292085c1b14b487d348b6dd572a1e2cf9b892b96bae8b74d21304f4e4", size = 48991 }, + { url = "https://files.pythonhosted.org/packages/c7/8e/322f5eedf6b9e14fa6b9749197b9f8a486736fa29b1b42f26ef0f7e5497c/libvalkey-4.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3a5f27e938173df8dda85c7f76d0f92cfb8a5a6f59ac094da133e70bd5526ccf", size = 21380 }, + { url = "https://files.pythonhosted.org/packages/03/86/f6e93a4f19df53f381d66bcd8ef9aa256c660d0eb924e58b0fdccec250b5/libvalkey-4.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:55a5b78467c0605817e12469630e627c3cccccb44ff810c4943337fbf3979529", size = 37185 }, + { url = "https://files.pythonhosted.org/packages/68/c1/cc3658e45979a9cfe0e03df32b863a9ed75d1cedbc12147644ba6e4c406a/libvalkey-4.0.1-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:af5a237ed6fec3c9977dd57248f11d9ab71a2d8a8954eb76f65171b7454f9f23", size = 39605 }, + { url = "https://files.pythonhosted.org/packages/76/37/4e13d72109f2e946f9d472b6e9564c332a983d20ef1c1ddc54794d02dc17/libvalkey-4.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5ce282d3b888bbabb71eb065defb66383f0775fb1f12da42edfff1800d85336", size = 48703 }, + { url = "https://files.pythonhosted.org/packages/2f/3c/76bfbca61ca46414c2c59a8006a88f04f8653c99a2f41ccc1b0663a8c005/libvalkey-4.0.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c2feb0686f51def52095d3429404db5092efb1edb336a961acc4887389c177a", size = 56226 }, + { url = "https://files.pythonhosted.org/packages/29/bb/f245b3b517b9ca1f41a4e3dc3600fdec3c59765d18c1ca079de44b3e0f6e/libvalkey-4.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30b9048d4f3745ecaa7e82aa985dbe57b8f96c6f5586767b9afd63d1c3021295", size = 48891 }, + { url = "https://files.pythonhosted.org/packages/2a/54/c256395784535d31cf398f5a32a4fc2dac31003c28491fe7868e3ea07cd5/libvalkey-4.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2e321a8af5ea12281898744f41cf7f8c4008770dcc12f8f0afbc5f0b3cd40c4f", size = 21367 }, +] + +[[package]] +name = "litestar" +version = "2.13.0" +source = { editable = "." } +dependencies = [ + { name = "anyio" }, + { name = "click" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "httpx" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "importlib-resources", marker = "python_full_version < '3.9'" }, + { name = "litestar-htmx" }, + { name = "msgspec" }, + { name = "multidict" }, + { name = "multipart" }, + { name = "polyfactory" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "rich-click" }, + { name = "typing-extensions" }, +] + +[package.optional-dependencies] +annotated-types = [ + { name = "annotated-types" }, +] +attrs = [ + { name = "attrs" }, +] +brotli = [ + { name = "brotli" }, +] +cli = [ + { name = "jsbeautifier" }, + { name = "uvicorn", extra = ["standard"] }, + { name = "uvloop", marker = "sys_platform != 'win32'" }, +] +cryptography = [ + { name = "cryptography" }, +] +full = [ + { name = "advanced-alchemy" }, + { name = "annotated-types" }, + { name = "attrs" }, + { name = "brotli" }, + { name = "cryptography" }, + { name = "email-validator" }, + { name = "fast-query-parsers" }, + { name = "jinja2" }, + { name = "jsbeautifier" }, + { name = "mako" }, + { name = "minijinja" }, + { name = "opentelemetry-instrumentation-asgi" }, + { name = "piccolo" }, + { name = "picologging" }, + { name = "prometheus-client" }, + { name = "pydantic" }, + { name = "pydantic-extra-types" }, + { name = "pyjwt" }, + { name = "redis", extra = ["hiredis"] }, + { name = "structlog" }, + { name = "uvicorn", extra = ["standard"] }, + { name = "uvloop", marker = "sys_platform != 'win32'" }, + { name = "valkey", extra = ["libvalkey"] }, +] +jinja = [ + { name = "jinja2" }, +] +jwt = [ + { name = "cryptography" }, + { name = "pyjwt" }, +] +mako = [ + { name = "mako" }, +] +minijinja = [ + { name = "minijinja" }, +] +opentelemetry = [ + { name = "opentelemetry-instrumentation-asgi" }, +] +piccolo = [ + { name = "piccolo" }, +] +picologging = [ + { name = "picologging" }, +] +prometheus = [ + { name = "prometheus-client" }, +] +pydantic = [ + { name = "email-validator" }, + { name = "pydantic" }, + { name = "pydantic-extra-types" }, +] +redis = [ + { name = "redis", extra = ["hiredis"] }, +] +sqlalchemy = [ + { name = "advanced-alchemy" }, +] +standard = [ + { name = "fast-query-parsers" }, + { name = "jinja2" }, + { name = "jsbeautifier" }, + { name = "uvicorn", extra = ["standard"] }, + { name = "uvloop", marker = "sys_platform != 'win32'" }, +] +structlog = [ + { name = "structlog" }, +] +valkey = [ + { name = "valkey", extra = ["libvalkey"] }, +] + +[package.dev-dependencies] +dev = [ + { name = "aiosqlite" }, + { name = "asyncpg" }, + { name = "beanie" }, + { name = "beautifulsoup4" }, + { name = "daphne" }, + { name = "fsspec" }, + { name = "greenlet" }, + { name = "httpx-sse" }, + { name = "hypercorn" }, + { name = "hypothesis" }, + { name = "litestar", extra = ["full"] }, + { name = "opentelemetry-sdk" }, + { name = "psutil" }, + { name = "psycopg", extra = ["binary", "pool"] }, + { name = "psycopg2-binary" }, + { name = "python-dotenv" }, + { name = "starlette" }, + { name = "trio" }, +] +docs = [ + { name = "auto-pytabs", extra = ["sphinx"] }, + { name = "litestar-sphinx-theme" }, + { name = "sphinx" }, + { name = "sphinx-autobuild" }, + { name = "sphinx-click" }, + { name = "sphinx-copybutton" }, + { name = "sphinx-design" }, + { name = "sphinx-paramlinks" }, + { name = "sphinx-toolbox" }, + { name = "sphinxcontrib-mermaid" }, +] +linting = [ + { name = "asyncpg-stubs" }, + { name = "codecov-cli" }, + { name = "mypy" }, + { name = "pre-commit" }, + { name = "pyright" }, + { name = "ruff" }, + { name = "slotscheck" }, + { name = "types-beautifulsoup4" }, + { name = "types-psutil" }, + { name = "types-pyyaml" }, + { name = "types-redis" }, +] +test = [ + { name = "covdefaults" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-lazy-fixtures" }, + { name = "pytest-mock" }, + { name = "pytest-rerunfailures" }, + { name = "pytest-timeout" }, + { name = "pytest-xdist" }, + { name = "time-machine" }, +] + +[package.metadata] +requires-dist = [ + { name = "advanced-alchemy", marker = "extra == 'sqlalchemy'", specifier = ">=0.2.2" }, + { name = "annotated-types", marker = "extra == 'annotated-types'" }, + { name = "anyio", specifier = ">=3" }, + { name = "attrs", marker = "extra == 'attrs'" }, + { name = "brotli", marker = "extra == 'brotli'" }, + { name = "click" }, + { name = "cryptography", marker = "extra == 'cryptography'" }, + { name = "cryptography", marker = "extra == 'jwt'" }, + { name = "email-validator", marker = "extra == 'pydantic'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "fast-query-parsers", marker = "extra == 'standard'", specifier = ">=1.0.2" }, + { name = "httpx", specifier = ">=0.22" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "importlib-resources", marker = "python_full_version < '3.9'", specifier = ">=5.12.0" }, + { name = "jinja2", marker = "extra == 'jinja'", specifier = ">=3.1.2" }, + { name = "jinja2", marker = "extra == 'standard'" }, + { name = "jsbeautifier", marker = "extra == 'cli'" }, + { name = "jsbeautifier", marker = "extra == 'standard'" }, + { name = "litestar", extras = ["annotated-types", "attrs", "brotli", "cli", "cryptography", "jinja", "jwt", "mako", "minijinja", "opentelemetry", "piccolo", "picologging", "prometheus", "pydantic", "redis", "sqlalchemy", "standard", "structlog", "valkey"], marker = "extra == 'full'" }, + { name = "litestar-htmx", specifier = ">=0.4.0" }, + { name = "mako", marker = "extra == 'mako'", specifier = ">=1.2.4" }, + { name = "minijinja", marker = "extra == 'minijinja'", specifier = ">=1.0.0" }, + { name = "msgspec", specifier = ">=0.18.2" }, + { name = "multidict", specifier = ">=6.0.2" }, + { name = "multipart", specifier = ">=1.2.0" }, + { name = "opentelemetry-instrumentation-asgi", marker = "extra == 'opentelemetry'" }, + { name = "piccolo", marker = "extra == 'piccolo'" }, + { name = "picologging", marker = "extra == 'picologging'" }, + { name = "polyfactory", specifier = ">=2.6.3" }, + { name = "prometheus-client", marker = "extra == 'prometheus'" }, + { name = "pydantic", marker = "extra == 'pydantic'" }, + { name = "pydantic-extra-types", marker = "extra == 'pydantic'" }, + { name = "pyjwt", marker = "extra == 'jwt'", specifier = ">=2.9.0" }, + { name = "pyyaml" }, + { name = "redis", extras = ["hiredis"], marker = "extra == 'redis'", specifier = ">=4.4.4" }, + { name = "rich", specifier = ">=13.0.0" }, + { name = "rich-click" }, + { name = "structlog", marker = "extra == 'structlog'" }, + { name = "typing-extensions" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'cli'" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'standard'" }, + { name = "uvloop", marker = "sys_platform != 'win32' and extra == 'cli'", specifier = ">=0.18.0" }, + { name = "uvloop", marker = "sys_platform != 'win32' and extra == 'standard'", specifier = ">=0.18.0" }, + { name = "valkey", extras = ["libvalkey"], marker = "extra == 'valkey'", specifier = ">=6.0.2" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "aiosqlite" }, + { name = "asyncpg", specifier = ">=0.29.0" }, + { name = "beanie", specifier = ">=1.21.0" }, + { name = "beautifulsoup4" }, + { name = "daphne", specifier = ">=4.0.0" }, + { name = "fsspec" }, + { name = "greenlet" }, + { name = "httpx-sse" }, + { name = "hypercorn", specifier = ">=0.16.0" }, + { name = "hypothesis" }, + { name = "litestar", extras = ["full"] }, + { name = "opentelemetry-sdk" }, + { name = "psutil", specifier = ">=5.9.8" }, + { name = "psycopg", extras = ["pool", "binary"], specifier = ">=3.1.10,<3.2" }, + { name = "psycopg2-binary" }, + { name = "python-dotenv" }, + { name = "starlette" }, + { name = "trio" }, +] +docs = [ + { name = "auto-pytabs", extras = ["sphinx"], specifier = ">=0.5.0" }, + { name = "litestar-sphinx-theme", git = "https://github.com/litestar-org/litestar-sphinx-theme.git" }, + { name = "sphinx", specifier = ">=7.1.2" }, + { name = "sphinx-autobuild", specifier = ">=2021.3.14" }, + { name = "sphinx-click", specifier = ">=4.4.0" }, + { name = "sphinx-copybutton", specifier = ">=0.5.2" }, + { name = "sphinx-design", specifier = ">=0.5.0" }, + { name = "sphinx-paramlinks", specifier = ">=0.6.0" }, + { name = "sphinx-toolbox", specifier = ">=3.5.0" }, + { name = "sphinxcontrib-mermaid", specifier = ">=0.9.2" }, +] +linting = [ + { name = "asyncpg-stubs" }, + { name = "codecov-cli" }, + { name = "mypy" }, + { name = "pre-commit" }, + { name = "pyright", specifier = "==1.1.344" }, + { name = "ruff", specifier = ">=0.2.1" }, + { name = "slotscheck" }, + { name = "types-beautifulsoup4" }, + { name = "types-psutil" }, + { name = "types-pyyaml" }, + { name = "types-redis" }, +] +test = [ + { name = "covdefaults" }, + { name = "pytest", specifier = "<8.0.0" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-lazy-fixtures" }, + { name = "pytest-mock" }, + { name = "pytest-rerunfailures" }, + { name = "pytest-timeout" }, + { name = "pytest-xdist" }, + { name = "time-machine" }, +] + +[[package]] +name = "litestar-htmx" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/bc/68dda03c35e5067a09a4e57f9b5af958d0718da00ae741483d8d95a8fb5a/litestar_htmx-0.4.0.tar.gz", hash = "sha256:b5f53aa25b665d673fe2e9d835ff711f7fe28fa8f57b87b61e4f6317b983632e", size = 101616 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/ea/e423f6dd967ae32157e2abc74bf4cd714adf94536ca9926358d4f59da175/litestar_htmx-0.4.0-py3-none-any.whl", hash = "sha256:81e784b91a5a5ca6061d86271e026de7d785d90a4367c6b9c8f0c724b26986c4", size = 9784 }, +] + +[[package]] +name = "litestar-sphinx-theme" +version = "0.2.0" +source = { git = "https://github.com/litestar-org/litestar-sphinx-theme.git#76b1d0e4c8afff1ad135b1917fe09cf6c1cc6c9b" } +dependencies = [ + { name = "pydata-sphinx-theme" }, + { name = "sphinx-design" }, +] + +[[package]] +name = "livereload" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/fa/049e6a4aa02c5fe21d053bafddb5c03a551f8c667c0a7399c3ef4aa42d36/livereload-2.7.0.tar.gz", hash = "sha256:f4ba199ef93248902841e298670eebfe1aa9e148e19b343bc57dbf1b74de0513", size = 22143 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/51/7a862d416ed2b4dc90ea272e96a439a430bb4ca74ebcccfcc8dab4bac7e3/livereload-2.7.0-py3-none-any.whl", hash = "sha256:19bee55aff51d5ade6ede0dc709189a0f904d3b906d3ea71641ed548acff3246", size = 22530 }, +] + +[[package]] +name = "mako" +version = "1.3.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/0b/29bc5a230948bf209d3ed3165006d257e547c02c3c2a96f6286320dfe8dc/mako-1.3.6.tar.gz", hash = "sha256:9ec3a1583713479fae654f83ed9fa8c9a4c16b7bb0daba0e6bbebff50c0d983d", size = 390206 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/22/bc14c6f02e6dccaafb3eba95764c8f096714260c2aa5f76f654fd16a23dd/Mako-1.3.6-py3-none-any.whl", hash = "sha256:a91198468092a2f1a0de86ca92690fb0cfc43ca90ee17e15d93662b4c04b241a", size = 78557 }, +] + +[[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 = "2.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/54/ad5eb37bf9d51800010a74e4665425831a9db4e7c4e0fde4352e391e808e/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", size = 18206 }, + { url = "https://files.pythonhosted.org/packages/6a/4a/a4d49415e600bacae038c67f9fecc1d5433b9d3c71a4de6f33537b89654c/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", size = 14079 }, + { url = "https://files.pythonhosted.org/packages/0a/7b/85681ae3c33c385b10ac0f8dd025c30af83c78cec1c37a6aa3b55e67f5ec/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", size = 26620 }, + { url = "https://files.pythonhosted.org/packages/7c/52/2b1b570f6b8b803cef5ac28fdf78c0da318916c7d2fe9402a84d591b394c/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", size = 25818 }, + { url = "https://files.pythonhosted.org/packages/29/fe/a36ba8c7ca55621620b2d7c585313efd10729e63ef81e4e61f52330da781/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", size = 25493 }, + { url = "https://files.pythonhosted.org/packages/60/ae/9c60231cdfda003434e8bd27282b1f4e197ad5a710c14bee8bea8a9ca4f0/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", size = 30630 }, + { url = "https://files.pythonhosted.org/packages/65/dc/1510be4d179869f5dafe071aecb3f1f41b45d37c02329dfba01ff59e5ac5/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", size = 29745 }, + { url = "https://files.pythonhosted.org/packages/30/39/8d845dd7d0b0613d86e0ef89549bfb5f61ed781f59af45fc96496e897f3a/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", size = 30021 }, + { url = "https://files.pythonhosted.org/packages/c7/5c/356a6f62e4f3c5fbf2602b4771376af22a3b16efa74eb8716fb4e328e01e/MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", size = 16659 }, + { url = "https://files.pythonhosted.org/packages/69/48/acbf292615c65f0604a0c6fc402ce6d8c991276e16c80c46a8f758fbd30c/MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", size = 17213 }, + { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219 }, + { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098 }, + { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014 }, + { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220 }, + { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756 }, + { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988 }, + { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718 }, + { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317 }, + { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670 }, + { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224 }, + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 }, + { url = "https://files.pythonhosted.org/packages/f8/ff/2c942a82c35a49df5de3a630ce0a8456ac2969691b230e530ac12314364c/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", size = 18192 }, + { url = "https://files.pythonhosted.org/packages/4f/14/6f294b9c4f969d0c801a4615e221c1e084722ea6114ab2114189c5b8cbe0/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", size = 14072 }, + { url = "https://files.pythonhosted.org/packages/81/d4/fd74714ed30a1dedd0b82427c02fa4deec64f173831ec716da11c51a50aa/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", size = 26928 }, + { url = "https://files.pythonhosted.org/packages/c7/bd/50319665ce81bb10e90d1cf76f9e1aa269ea6f7fa30ab4521f14d122a3df/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", size = 26106 }, + { url = "https://files.pythonhosted.org/packages/4c/6f/f2b0f675635b05f6afd5ea03c094557bdb8622fa8e673387444fe8d8e787/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68", size = 25781 }, + { url = "https://files.pythonhosted.org/packages/51/e0/393467cf899b34a9d3678e78961c2c8cdf49fb902a959ba54ece01273fb1/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", size = 30518 }, + { url = "https://files.pythonhosted.org/packages/f6/02/5437e2ad33047290dafced9df741d9efc3e716b75583bbd73a9984f1b6f7/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", size = 29669 }, + { url = "https://files.pythonhosted.org/packages/0e/7d/968284145ffd9d726183ed6237c77938c021abacde4e073020f920e060b2/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", size = 29933 }, + { url = "https://files.pythonhosted.org/packages/bf/f3/ecb00fc8ab02b7beae8699f34db9357ae49d9f21d4d3de6f305f34fa949e/MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", size = 16656 }, + { url = "https://files.pythonhosted.org/packages/92/21/357205f03514a49b293e214ac39de01fadd0970a6e05e4bf1ddd0ffd0881/MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", size = 17206 }, + { url = "https://files.pythonhosted.org/packages/0f/31/780bb297db036ba7b7bbede5e1d7f1e14d704ad4beb3ce53fb495d22bc62/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", size = 18193 }, + { url = "https://files.pythonhosted.org/packages/6c/77/d77701bbef72892affe060cdacb7a2ed7fd68dae3b477a8642f15ad3b132/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", size = 14073 }, + { url = "https://files.pythonhosted.org/packages/d9/a7/1e558b4f78454c8a3a0199292d96159eb4d091f983bc35ef258314fe7269/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", size = 26486 }, + { url = "https://files.pythonhosted.org/packages/5f/5a/360da85076688755ea0cceb92472923086993e86b5613bbae9fbc14136b0/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", size = 25685 }, + { url = "https://files.pythonhosted.org/packages/6a/18/ae5a258e3401f9b8312f92b028c54d7026a97ec3ab20bfaddbdfa7d8cce8/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", size = 25338 }, + { url = "https://files.pythonhosted.org/packages/0b/cc/48206bd61c5b9d0129f4d75243b156929b04c94c09041321456fd06a876d/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", size = 30439 }, + { url = "https://files.pythonhosted.org/packages/d1/06/a41c112ab9ffdeeb5f77bc3e331fdadf97fa65e52e44ba31880f4e7f983c/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", size = 29531 }, + { url = "https://files.pythonhosted.org/packages/02/8c/ab9a463301a50dab04d5472e998acbd4080597abc048166ded5c7aa768c8/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", size = 29823 }, + { url = "https://files.pythonhosted.org/packages/bc/29/9bc18da763496b055d8e98ce476c8e718dcfd78157e17f555ce6dd7d0895/MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", size = 16658 }, + { url = "https://files.pythonhosted.org/packages/f6/f8/4da07de16f10551ca1f640c92b5f316f9394088b183c6a57183df6de5ae4/MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", size = 17211 }, +] + +[[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 = "minijinja" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/0e/7b76e4a45d5665047bb288a3e5f2520f25a691be58a1662e5a0ca80ae5b5/minijinja-2.5.0.tar.gz", hash = "sha256:ca97a8d8053aa0111f855d6706883dbfdbda600bd2dd0e152c03077a42c003e3", size = 218425 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/d7/d5a36cc6e293a91fe8183155c8c1db26b8ca1f34f8126ae0e76cf6639e42/minijinja-2.5.0-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:888d88ab7328de337581656b220409aaa85c992d3ec25e50095196fb4f0752ae", size = 1693920 }, + { url = "https://files.pythonhosted.org/packages/29/d4/2e8a5cfb701a779e69b5c6e917a96ba70d6fc7f5c358bd645c67cea55716/minijinja-2.5.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de15fc9ee8c6e0cd22799460af499520055139a8db3636e1c7136278215fe095", size = 856027 }, + { url = "https://files.pythonhosted.org/packages/66/3f/22a11b8ba1f6c514ab50c64d2cbeccc20cf1bc43e1ab01361566e8365b31/minijinja-2.5.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:516e84a4f03b81002fd0eea966341b8c12fd9ebc4b222dc6ebc50a2b7535df13", size = 849556 }, + { url = "https://files.pythonhosted.org/packages/84/3f/6ad712c2943f44781a72d6927063e9c8c4540555d9a5fd92154cf9de8145/minijinja-2.5.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369d6ed56b571feb25f84b87eec0fb286b1931bb472eed1f148ca69777c596d3", size = 915568 }, + { url = "https://files.pythonhosted.org/packages/e5/8e/a008fa6a19e3d3d3e6de731acaa5c258d2bc4b60e3023c6b662f020d20ff/minijinja-2.5.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:025c0232bfbb92ff53bd474cfd230b687480fd6b4c4e9b4f5b354d91a62dc144", size = 990999 }, + { url = "https://files.pythonhosted.org/packages/86/b8/208121205af6cc20f802a29df4312d75f332a7501c13ed73790e9fc3df82/minijinja-2.5.0-cp38-abi3-win32.whl", hash = "sha256:d393ca982b5189c2a8f42b82612170885b48f500e4afac6dc127e980b286b798", size = 820098 }, + { url = "https://files.pythonhosted.org/packages/92/8b/67c5fc89ec9eec5872e222f28671c36963c635d1d22ad93ecd9ee766d279/minijinja-2.5.0-cp38-abi3-win_amd64.whl", hash = "sha256:1ddd1941d0fbf84dbce103f65a3e04eba7d958d08e399f6d3c63bce2fbc1ae0c", size = 868506 }, +] + +[[package]] +name = "more-itertools" +version = "10.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/78/65922308c4248e0eb08ebcbe67c95d48615cc6f27854b6f2e57143e9178f/more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6", size = 121020 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/7e/3a64597054a70f7c86eb0a7d4fc315b8c1ab932f64883a297bdffeb5f967/more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", size = 60952 }, +] + +[[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 = "msgpack" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/d0/7555686ae7ff5731205df1012ede15dd9d927f6227ea151e901c7406af4f/msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e", size = 167260 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/f9/a892a6038c861fa849b11a2bb0502c07bc698ab6ea53359e5771397d883b/msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd", size = 150428 }, + { url = "https://files.pythonhosted.org/packages/df/7a/d174cc6a3b6bb85556e6a046d3193294a92f9a8e583cdbd46dc8a1d7e7f4/msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d", size = 84131 }, + { url = "https://files.pythonhosted.org/packages/08/52/bf4fbf72f897a23a56b822997a72c16de07d8d56d7bf273242f884055682/msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5", size = 81215 }, + { url = "https://files.pythonhosted.org/packages/02/95/dc0044b439b518236aaf012da4677c1b8183ce388411ad1b1e63c32d8979/msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5", size = 371229 }, + { url = "https://files.pythonhosted.org/packages/ff/75/09081792db60470bef19d9c2be89f024d366b1e1973c197bb59e6aabc647/msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e", size = 378034 }, + { url = "https://files.pythonhosted.org/packages/32/d3/c152e0c55fead87dd948d4b29879b0f14feeeec92ef1fd2ec21b107c3f49/msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b", size = 363070 }, + { url = "https://files.pythonhosted.org/packages/d9/2c/82e73506dd55f9e43ac8aa007c9dd088c6f0de2aa19e8f7330e6a65879fc/msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f", size = 359863 }, + { url = "https://files.pythonhosted.org/packages/cb/a0/3d093b248837094220e1edc9ec4337de3443b1cfeeb6e0896af8ccc4cc7a/msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68", size = 368166 }, + { url = "https://files.pythonhosted.org/packages/e4/13/7646f14f06838b406cf5a6ddbb7e8dc78b4996d891ab3b93c33d1ccc8678/msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b", size = 370105 }, + { url = "https://files.pythonhosted.org/packages/67/fa/dbbd2443e4578e165192dabbc6a22c0812cda2649261b1264ff515f19f15/msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044", size = 68513 }, + { url = "https://files.pythonhosted.org/packages/24/ce/c2c8fbf0ded750cb63cbcbb61bc1f2dfd69e16dca30a8af8ba80ec182dcd/msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f", size = 74687 }, + { url = "https://files.pythonhosted.org/packages/b7/5e/a4c7154ba65d93be91f2f1e55f90e76c5f91ccadc7efc4341e6f04c8647f/msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7", size = 150803 }, + { url = "https://files.pythonhosted.org/packages/60/c2/687684164698f1d51c41778c838d854965dd284a4b9d3a44beba9265c931/msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa", size = 84343 }, + { url = "https://files.pythonhosted.org/packages/42/ae/d3adea9bb4a1342763556078b5765e666f8fdf242e00f3f6657380920972/msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701", size = 81408 }, + { url = "https://files.pythonhosted.org/packages/dc/17/6313325a6ff40ce9c3207293aee3ba50104aed6c2c1559d20d09e5c1ff54/msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6", size = 396096 }, + { url = "https://files.pythonhosted.org/packages/a8/a1/ad7b84b91ab5a324e707f4c9761633e357820b011a01e34ce658c1dda7cc/msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59", size = 403671 }, + { url = "https://files.pythonhosted.org/packages/bb/0b/fd5b7c0b308bbf1831df0ca04ec76fe2f5bf6319833646b0a4bd5e9dc76d/msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0", size = 387414 }, + { url = "https://files.pythonhosted.org/packages/f0/03/ff8233b7c6e9929a1f5da3c7860eccd847e2523ca2de0d8ef4878d354cfa/msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e", size = 383759 }, + { url = "https://files.pythonhosted.org/packages/1f/1b/eb82e1fed5a16dddd9bc75f0854b6e2fe86c0259c4353666d7fab37d39f4/msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6", size = 394405 }, + { url = "https://files.pythonhosted.org/packages/90/2e/962c6004e373d54ecf33d695fb1402f99b51832631e37c49273cc564ffc5/msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5", size = 396041 }, + { url = "https://files.pythonhosted.org/packages/f8/20/6e03342f629474414860c48aeffcc2f7f50ddaf351d95f20c3f1c67399a8/msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88", size = 68538 }, + { url = "https://files.pythonhosted.org/packages/aa/c4/5a582fc9a87991a3e6f6800e9bb2f3c82972912235eb9539954f3e9997c7/msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788", size = 74871 }, + { url = "https://files.pythonhosted.org/packages/e1/d6/716b7ca1dbde63290d2973d22bbef1b5032ca634c3ff4384a958ec3f093a/msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d", size = 152421 }, + { url = "https://files.pythonhosted.org/packages/70/da/5312b067f6773429cec2f8f08b021c06af416bba340c912c2ec778539ed6/msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2", size = 85277 }, + { url = "https://files.pythonhosted.org/packages/28/51/da7f3ae4462e8bb98af0d5bdf2707f1b8c65a0d4f496e46b6afb06cbc286/msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420", size = 82222 }, + { url = "https://files.pythonhosted.org/packages/33/af/dc95c4b2a49cff17ce47611ca9ba218198806cad7796c0b01d1e332c86bb/msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2", size = 392971 }, + { url = "https://files.pythonhosted.org/packages/f1/54/65af8de681fa8255402c80eda2a501ba467921d5a7a028c9c22a2c2eedb5/msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39", size = 401403 }, + { url = "https://files.pythonhosted.org/packages/97/8c/e333690777bd33919ab7024269dc3c41c76ef5137b211d776fbb404bfead/msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f", size = 385356 }, + { url = "https://files.pythonhosted.org/packages/57/52/406795ba478dc1c890559dd4e89280fa86506608a28ccf3a72fbf45df9f5/msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247", size = 383028 }, + { url = "https://files.pythonhosted.org/packages/e7/69/053b6549bf90a3acadcd8232eae03e2fefc87f066a5b9fbb37e2e608859f/msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c", size = 391100 }, + { url = "https://files.pythonhosted.org/packages/23/f0/d4101d4da054f04274995ddc4086c2715d9b93111eb9ed49686c0f7ccc8a/msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b", size = 394254 }, + { url = "https://files.pythonhosted.org/packages/1c/12/cf07458f35d0d775ff3a2dc5559fa2e1fcd06c46f1ef510e594ebefdca01/msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b", size = 69085 }, + { url = "https://files.pythonhosted.org/packages/73/80/2708a4641f7d553a63bc934a3eb7214806b5b39d200133ca7f7afb0a53e8/msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f", size = 75347 }, + { url = "https://files.pythonhosted.org/packages/c8/b0/380f5f639543a4ac413e969109978feb1f3c66e931068f91ab6ab0f8be00/msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf", size = 151142 }, + { url = "https://files.pythonhosted.org/packages/c8/ee/be57e9702400a6cb2606883d55b05784fada898dfc7fd12608ab1fdb054e/msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330", size = 84523 }, + { url = "https://files.pythonhosted.org/packages/7e/3a/2919f63acca3c119565449681ad08a2f84b2171ddfcff1dba6959db2cceb/msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734", size = 81556 }, + { url = "https://files.pythonhosted.org/packages/7c/43/a11113d9e5c1498c145a8925768ea2d5fce7cbab15c99cda655aa09947ed/msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e", size = 392105 }, + { url = "https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca", size = 399979 }, + { url = "https://files.pythonhosted.org/packages/82/8c/cf64ae518c7b8efc763ca1f1348a96f0e37150061e777a8ea5430b413a74/msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915", size = 383816 }, + { url = "https://files.pythonhosted.org/packages/69/86/a847ef7a0f5ef3fa94ae20f52a4cacf596a4e4a010197fbcc27744eb9a83/msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d", size = 380973 }, + { url = "https://files.pythonhosted.org/packages/aa/90/c74cf6e1126faa93185d3b830ee97246ecc4fe12cf9d2d31318ee4246994/msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434", size = 387435 }, + { url = "https://files.pythonhosted.org/packages/7a/40/631c238f1f338eb09f4acb0f34ab5862c4e9d7eda11c1b685471a4c5ea37/msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c", size = 399082 }, + { url = "https://files.pythonhosted.org/packages/e9/1b/fa8a952be252a1555ed39f97c06778e3aeb9123aa4cccc0fd2acd0b4e315/msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc", size = 69037 }, + { url = "https://files.pythonhosted.org/packages/b6/bc/8bd826dd03e022153bfa1766dcdec4976d6c818865ed54223d71f07862b3/msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f", size = 75140 }, + { url = "https://files.pythonhosted.org/packages/77/68/6ddc40189295de4363af0597ecafb822ca7636ed1e91626f294cc8bc0d91/msgpack-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec", size = 375795 }, + { url = "https://files.pythonhosted.org/packages/55/f6/d4859a158a915be52eecd52dee9761ab3a5d84c834a1d13ffc198e068a48/msgpack-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96", size = 381539 }, + { url = "https://files.pythonhosted.org/packages/98/6c/3b89221b0f6b2fd92572bd752545fc96ca4e494b76e2a02be8da56451909/msgpack-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870", size = 369353 }, + { url = "https://files.pythonhosted.org/packages/ed/a1/16bd86502f1572a14c6ccfa057306be7f94ea3081ffec652308036cefbd2/msgpack-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7", size = 364560 }, + { url = "https://files.pythonhosted.org/packages/46/72/0454fa773fc4977ca70ae45471e38b1ab0cd831bef1990e9283d8683fe18/msgpack-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb", size = 374203 }, + { url = "https://files.pythonhosted.org/packages/fd/2f/885932948ec2f51509691684842f5870f960d908373744070400ac56e2d0/msgpack-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f", size = 375978 }, + { url = "https://files.pythonhosted.org/packages/37/60/1f79ed762cb2af7ab17bf8f6d7270e022aa26cff06facaf48a82b2c13473/msgpack-1.1.0-cp38-cp38-win32.whl", hash = "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b", size = 68763 }, + { url = "https://files.pythonhosted.org/packages/a4/b7/1517b4d65caf3394c0e5f4e557dda8eaaed2ad00b4517b7d4c7c2bc86f77/msgpack-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb", size = 74910 }, + { url = "https://files.pythonhosted.org/packages/f7/3b/544a5c5886042b80e1f4847a4757af3430f60d106d8d43bb7be72c9e9650/msgpack-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1", size = 150713 }, + { url = "https://files.pythonhosted.org/packages/93/af/d63f25bcccd3d6f06fd518ba4a321f34a4370c67b579ca5c70b4a37721b4/msgpack-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48", size = 84277 }, + { url = "https://files.pythonhosted.org/packages/92/9b/5c0dfb0009b9f96328664fecb9f8e4e9c8a1ae919e6d53986c1b813cb493/msgpack-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c", size = 81357 }, + { url = "https://files.pythonhosted.org/packages/d1/7c/3a9ee6ec9fc3e47681ad39b4d344ee04ff20a776b594fba92d88d8b68356/msgpack-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468", size = 371256 }, + { url = "https://files.pythonhosted.org/packages/f7/0a/8a213cecea7b731c540f25212ba5f9a818f358237ac51a44d448bd753690/msgpack-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74", size = 377868 }, + { url = "https://files.pythonhosted.org/packages/1b/94/a82b0db0981e9586ed5af77d6cfb343da05d7437dceaae3b35d346498110/msgpack-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846", size = 363370 }, + { url = "https://files.pythonhosted.org/packages/93/fc/6c7f0dcc1c913e14861e16eaf494c07fc1dde454ec726ff8cebcf348ae53/msgpack-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346", size = 358970 }, + { url = "https://files.pythonhosted.org/packages/1f/c6/e4a04c0089deace870dabcdef5c9f12798f958e2e81d5012501edaff342f/msgpack-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b", size = 366358 }, + { url = "https://files.pythonhosted.org/packages/b6/54/7d8317dac590cf16b3e08e3fb74d2081e5af44eb396f0effa13f17777f30/msgpack-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8", size = 370336 }, + { url = "https://files.pythonhosted.org/packages/dc/6f/a5a1f43b6566831e9630e5bc5d86034a8884386297302be128402555dde1/msgpack-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd", size = 68683 }, + { url = "https://files.pythonhosted.org/packages/5f/e8/2162621e18dbc36e2bc8492fd0e97b3975f5d89fe0472ae6d5f7fbdd8cf7/msgpack-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325", size = 74787 }, +] + +[[package]] +name = "msgspec" +version = "0.18.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/fb/42b1865063fddb14dbcbb6e74e0a366ecf1ba371c4948664dde0b0e10f95/msgspec-0.18.6.tar.gz", hash = "sha256:a59fc3b4fcdb972d09138cb516dbde600c99d07c38fd9372a6ef500d2d031b4e", size = 216757 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/54/34c2b70e0d42d876c04f6436c80777d786f25c7536830db5e4ec1aef8788/msgspec-0.18.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77f30b0234eceeff0f651119b9821ce80949b4d667ad38f3bfed0d0ebf9d6d8f", size = 202537 }, + { url = "https://files.pythonhosted.org/packages/d4/b8/d00d7d03bba8b4eb0bbfdeb6c047163877b2916995f837113d273fd3b774/msgspec-0.18.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a76b60e501b3932782a9da039bd1cd552b7d8dec54ce38332b87136c64852dd", size = 192246 }, + { url = "https://files.pythonhosted.org/packages/98/07/40bcd501d0f4e76694ca04a11689f3e06d9ef7a31d74e493a2cc34cd9198/msgspec-0.18.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06acbd6edf175bee0e36295d6b0302c6de3aaf61246b46f9549ca0041a9d7177", size = 208523 }, + { url = "https://files.pythonhosted.org/packages/23/1f/10f2bf07f8fcdc3b0c7bf1bfefdd28bd0353df9290c84e4b3ad8e93e0115/msgspec-0.18.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40a4df891676d9c28a67c2cc39947c33de516335680d1316a89e8f7218660410", size = 210276 }, + { url = "https://files.pythonhosted.org/packages/c7/e4/4bb5bcd89a74bbb246a21687dd62923c43007e28ad17db24ff58653456cb/msgspec-0.18.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a6896f4cd5b4b7d688018805520769a8446df911eb93b421c6c68155cdf9dd5a", size = 214659 }, + { url = "https://files.pythonhosted.org/packages/32/f1/57187427a5a3379cb74aaae753314f9dcde14c259552ec0cb44bcf18db49/msgspec-0.18.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3ac4dd63fd5309dd42a8c8c36c1563531069152be7819518be0a9d03be9788e4", size = 216585 }, + { url = "https://files.pythonhosted.org/packages/7d/d1/94919c9b837fc9a0e9dfc1b598a50298bd194146e7bc7d3f42f18826e9f6/msgspec-0.18.6-cp310-cp310-win_amd64.whl", hash = "sha256:fda4c357145cf0b760000c4ad597e19b53adf01382b711f281720a10a0fe72b7", size = 185677 }, + { url = "https://files.pythonhosted.org/packages/15/20/278def3822dec807be1e2a734ba9547500ff06667be9dda00ab5d277d605/msgspec-0.18.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e77e56ffe2701e83a96e35770c6adb655ffc074d530018d1b584a8e635b4f36f", size = 200058 }, + { url = "https://files.pythonhosted.org/packages/25/8c/75bfafb040934dd3eb46234a2bd4d8fcc7b646f77440866f954b60e0886b/msgspec-0.18.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5351afb216b743df4b6b147691523697ff3a2fc5f3d54f771e91219f5c23aaa", size = 189108 }, + { url = "https://files.pythonhosted.org/packages/0d/e6/5dd960a7678cbaf90dc910611a0e700775ee341876f029c3c987122afe84/msgspec-0.18.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3232fabacef86fe8323cecbe99abbc5c02f7698e3f5f2e248e3480b66a3596b", size = 208138 }, + { url = "https://files.pythonhosted.org/packages/6a/73/1b2f991dc26899d2f999c938cbc82c858b3cb7e3ccaad317b32760dbe1da/msgspec-0.18.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b524df6ea9998bbc99ea6ee4d0276a101bcc1aa8d14887bb823914d9f60d07", size = 209538 }, + { url = "https://files.pythonhosted.org/packages/29/d4/2fb2d40b3bde566fd14bf02bf503eea20a912a02cdf7ff100629906c9094/msgspec-0.18.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:37f67c1d81272131895bb20d388dd8d341390acd0e192a55ab02d4d6468b434c", size = 213571 }, + { url = "https://files.pythonhosted.org/packages/59/5a/c2aeeefd78946713047637f0c422c0b8b31182eb9bbed0068e906cc8aca0/msgspec-0.18.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0feb7a03d971c1c0353de1a8fe30bb6579c2dc5ccf29b5f7c7ab01172010492", size = 215785 }, + { url = "https://files.pythonhosted.org/packages/51/c6/0a8ae23c91ba1e6d58ddb089bba4ce8dad5815411b4a2bb40a5f15d2ab73/msgspec-0.18.6-cp311-cp311-win_amd64.whl", hash = "sha256:41cf758d3f40428c235c0f27bc6f322d43063bc32da7b9643e3f805c21ed57b4", size = 185877 }, + { url = "https://files.pythonhosted.org/packages/1d/b5/c8fbf1db814eb29eda402952374b594b2559419ba7ec6d0997a9e5687530/msgspec-0.18.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d86f5071fe33e19500920333c11e2267a31942d18fed4d9de5bc2fbab267d28c", size = 202109 }, + { url = "https://files.pythonhosted.org/packages/d7/9a/235d2dbab078a0b8e6f338205dc59be0b027ce000554ee6a9c41b19339e5/msgspec-0.18.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce13981bfa06f5eb126a3a5a38b1976bddb49a36e4f46d8e6edecf33ccf11df1", size = 190281 }, + { url = "https://files.pythonhosted.org/packages/0e/f2/f864ed36a8a62c26b57c3e08d212bd8f3d12a3ca3ef64600be5452aa3c82/msgspec-0.18.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97dec6932ad5e3ee1e3c14718638ba333befc45e0661caa57033cd4cc489466", size = 210305 }, + { url = "https://files.pythonhosted.org/packages/73/16/dfef780ced7d690dd5497846ed242ef3e27e319d59d1ddaae816a4f2c15e/msgspec-0.18.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad237100393f637b297926cae1868b0d500f764ccd2f0623a380e2bcfb2809ca", size = 212510 }, + { url = "https://files.pythonhosted.org/packages/c1/90/f5b3a788c4b3d92190e3345d1afa3dd107d5f16b8194e1f61b72582ee9bd/msgspec-0.18.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db1d8626748fa5d29bbd15da58b2d73af25b10aa98abf85aab8028119188ed57", size = 214844 }, + { url = "https://files.pythonhosted.org/packages/ce/0b/d4cc1b09f8dfcc6cc4cc9739c13a86e093fe70257b941ea9feb15df22996/msgspec-0.18.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d70cb3d00d9f4de14d0b31d38dfe60c88ae16f3182988246a9861259c6722af6", size = 217113 }, + { url = "https://files.pythonhosted.org/packages/3f/76/30d8f152299f65c85c46a2cbeaf95ad1d18516b5ce730acdaef696d4cfe6/msgspec-0.18.6-cp312-cp312-win_amd64.whl", hash = "sha256:1003c20bfe9c6114cc16ea5db9c5466e49fae3d7f5e2e59cb70693190ad34da0", size = 187184 }, + { url = "https://files.pythonhosted.org/packages/5b/2b/262847e614393f265f00b8096d8f71871b27cb71f68f1250a9eac93cb1bc/msgspec-0.18.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f7d9faed6dfff654a9ca7d9b0068456517f63dbc3aa704a527f493b9200b210a", size = 201291 }, + { url = "https://files.pythonhosted.org/packages/86/6f/1da53a2ba5f312c3dca9e5f38912732e77f996a22945c8d62df7617c4733/msgspec-0.18.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9da21f804c1a1471f26d32b5d9bc0480450ea77fbb8d9db431463ab64aaac2cf", size = 191604 }, + { url = "https://files.pythonhosted.org/packages/f0/77/00e1e55607de1092dded768eae746cfdfd6f5aca4ad52b9bb11c3e3b1153/msgspec-0.18.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46eb2f6b22b0e61c137e65795b97dc515860bf6ec761d8fb65fdb62aa094ba61", size = 210060 }, + { url = "https://files.pythonhosted.org/packages/21/e0/1dff019ae22b7d47782d6f1180760828bc96fde368aea983d8e5d872833a/msgspec-0.18.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8355b55c80ac3e04885d72db515817d9fbb0def3bab936bba104e99ad22cf46", size = 212378 }, + { url = "https://files.pythonhosted.org/packages/85/98/da3ad36c242fdf0e6cd9d63e5d47ca53577f23c180ef040f4b3aefb5b88e/msgspec-0.18.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9080eb12b8f59e177bd1eb5c21e24dd2ba2fa88a1dbc9a98e05ad7779b54c681", size = 215541 }, + { url = "https://files.pythonhosted.org/packages/13/cd/29b0de4e0e4a517fff7161fba034df19c45a5a0ef63b728d0e74dba4911d/msgspec-0.18.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cc001cf39becf8d2dcd3f413a4797c55009b3a3cdbf78a8bf5a7ca8fdb76032c", size = 218414 }, + { url = "https://files.pythonhosted.org/packages/1e/b1/1a92bf0dd6354316c9c3a0e6d1123873bb6f21efdb497980e71e843d2f85/msgspec-0.18.6-cp38-cp38-win_amd64.whl", hash = "sha256:fac5834e14ac4da1fca373753e0c4ec9c8069d1fe5f534fa5208453b6065d5be", size = 187715 }, + { url = "https://files.pythonhosted.org/packages/cc/01/54e711813b04a668cbc6467e20ea747aec1aaf2c9afd83ed470d774d22d0/msgspec-0.18.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:974d3520fcc6b824a6dedbdf2b411df31a73e6e7414301abac62e6b8d03791b4", size = 202455 }, + { url = "https://files.pythonhosted.org/packages/dd/b6/2a78cdd1ef872ad96c509fc4d732ffd86903861c9b4e0a47c85d0b37b0e3/msgspec-0.18.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fd62e5818731a66aaa8e9b0a1e5543dc979a46278da01e85c3c9a1a4f047ef7e", size = 192001 }, + { url = "https://files.pythonhosted.org/packages/87/fc/1e06294be19595fc72e99957bf191a8a51be88487e280841ac5925069537/msgspec-0.18.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7481355a1adcf1f08dedd9311193c674ffb8bf7b79314b4314752b89a2cf7f1c", size = 208372 }, + { url = "https://files.pythonhosted.org/packages/b7/ee/9967075f4ea0ca3e841e1b98f0f65a6033c464e3542fe594e2e6dad10029/msgspec-0.18.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6aa85198f8f154cf35d6f979998f6dadd3dc46a8a8c714632f53f5d65b315c07", size = 210257 }, + { url = "https://files.pythonhosted.org/packages/70/03/9a16fac8e3de1b1aa30e22db8a38710cbacdb1f25c54dd2fcc0c0fb10585/msgspec-0.18.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e24539b25c85c8f0597274f11061c102ad6b0c56af053373ba4629772b407be", size = 214445 }, + { url = "https://files.pythonhosted.org/packages/67/15/4b8e28bfd836cd0dbf7ac8feb52dc440d9ed028b798090b931aa6fac9636/msgspec-0.18.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c61ee4d3be03ea9cd089f7c8e36158786cd06e51fbb62529276452bbf2d52ece", size = 216412 }, + { url = "https://files.pythonhosted.org/packages/cd/b2/283d010db6836db2fe059f7ee3c13823927229975ffbe1edcbeded85a556/msgspec-0.18.6-cp39-cp39-win_amd64.whl", hash = "sha256:b5c390b0b0b7da879520d4ae26044d74aeee5144f83087eb7842ba59c02bc090", size = 185801 }, +] + +[[package]] +name = "multidict" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +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/3e/6a/af41f3aaf5f00fd86cc7d470a2f5b25299b0c84691163b8757f4a1a205f2/multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392", size = 48597 }, + { url = "https://files.pythonhosted.org/packages/d9/d6/3d4082760ed11b05734f8bf32a0615b99e7d9d2b3730ad698a4d7377c00a/multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a", size = 29338 }, + { url = "https://files.pythonhosted.org/packages/9d/7f/5d1ce7f47d44393d429922910afbe88fcd29ee3069babbb47507a4c3a7ea/multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2", size = 29562 }, + { url = "https://files.pythonhosted.org/packages/ce/ec/c425257671af9308a9b626e2e21f7f43841616e4551de94eb3c92aca75b2/multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc", size = 130980 }, + { url = "https://files.pythonhosted.org/packages/d8/d7/d4220ad2633a89b314593e9b85b5bc9287a7c563c7f9108a4a68d9da5374/multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478", size = 136694 }, + { url = "https://files.pythonhosted.org/packages/a1/2a/13e554db5830c8d40185a2e22aa8325516a5de9634c3fb2caf3886a829b3/multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4", size = 131616 }, + { url = "https://files.pythonhosted.org/packages/2e/a9/83692e37d8152f104333132105b67100aabfb2e96a87f6bed67f566035a7/multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d", size = 129664 }, + { url = "https://files.pythonhosted.org/packages/cc/1c/1718cd518fb9da7e8890d9d1611c1af0ea5e60f68ff415d026e38401ed36/multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6", size = 121855 }, + { url = "https://files.pythonhosted.org/packages/2b/92/f6ed67514b0e3894198f0eb42dcde22f0851ea35f4561a1e4acf36c7b1be/multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2", size = 127928 }, + { url = "https://files.pythonhosted.org/packages/f7/30/c66954115a4dc4dc3c84e02c8ae11bb35a43d79ef93122c3c3a40c4d459b/multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd", size = 122793 }, + { url = "https://files.pythonhosted.org/packages/62/c9/d386d01b43871e8e1631eb7b3695f6af071b7ae1ab716caf371100f0eb24/multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/69/ff/f70cb0a2f7a358acf48e32139ce3a150ff18c961ee9c714cc8c0dc7e3584/multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492", size = 127872 }, + { url = "https://files.pythonhosted.org/packages/89/5b/abea7db3ba4cd07752a9b560f9275a11787cd13f86849b5d99c1ceea921d/multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd", size = 126161 }, + { url = "https://files.pythonhosted.org/packages/22/03/acc77a4667cca4462ee974fc39990803e58fa573d5a923d6e82b7ef6da7e/multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167", size = 26338 }, + { url = "https://files.pythonhosted.org/packages/90/bf/3d0c1cc9c8163abc24625fae89c0ade1ede9bccb6eceb79edf8cff3cca46/multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef", size = 28736 }, + { url = "https://files.pythonhosted.org/packages/e7/c9/9e153a6572b38ac5ff4434113af38acf8d5e9957897cdb1f513b3d6614ed/multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c", size = 48550 }, + { url = "https://files.pythonhosted.org/packages/76/f5/79565ddb629eba6c7f704f09a09df085c8dc04643b12506f10f718cee37a/multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1", size = 29298 }, + { url = "https://files.pythonhosted.org/packages/60/1b/9851878b704bc98e641a3e0bce49382ae9e05743dac6d97748feb5b7baba/multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c", size = 29641 }, + { url = "https://files.pythonhosted.org/packages/89/87/d451d45aab9e422cb0fb2f7720c31a4c1d3012c740483c37f642eba568fb/multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c", size = 126202 }, + { url = "https://files.pythonhosted.org/packages/fa/b4/27cbe9f3e2e469359887653f2e45470272eef7295139916cc21107c6b48c/multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f", size = 133925 }, + { url = "https://files.pythonhosted.org/packages/4d/a3/afc841899face8adfd004235ce759a37619f6ec99eafd959650c5ce4df57/multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875", size = 129039 }, + { url = "https://files.pythonhosted.org/packages/5e/41/0d0fb18c1ad574f807196f5f3d99164edf9de3e169a58c6dc2d6ed5742b9/multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255", size = 124072 }, + { url = "https://files.pythonhosted.org/packages/00/22/defd7a2e71a44e6e5b9a5428f972e5b572e7fe28e404dfa6519bbf057c93/multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30", size = 116532 }, + { url = "https://files.pythonhosted.org/packages/91/25/f7545102def0b1d456ab6449388eed2dfd822debba1d65af60194904a23a/multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057", size = 128173 }, + { url = "https://files.pythonhosted.org/packages/45/79/3dbe8d35fc99f5ea610813a72ab55f426cb9cf482f860fa8496e5409be11/multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657", size = 122654 }, + { url = "https://files.pythonhosted.org/packages/97/cb/209e735eeab96e1b160825b5d0b36c56d3862abff828fc43999bb957dcad/multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28", size = 133197 }, + { url = "https://files.pythonhosted.org/packages/e4/3a/a13808a7ada62808afccea67837a79d00ad6581440015ef00f726d064c2d/multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972", size = 129754 }, + { url = "https://files.pythonhosted.org/packages/77/dd/8540e139eafb240079242da8f8ffdf9d3f4b4ad1aac5a786cd4050923783/multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43", size = 126402 }, + { url = "https://files.pythonhosted.org/packages/86/99/e82e1a275d8b1ea16d3a251474262258dbbe41c05cce0c01bceda1fc8ea5/multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada", size = 26421 }, + { url = "https://files.pythonhosted.org/packages/86/1c/9fa630272355af7e4446a2c7550c259f11ee422ab2d30ff90a0a71cf3d9e/multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a", size = 28791 }, + { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, +] + +[[package]] +name = "multipart" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/91/6c93b6a95e6a99ef929a99d019fbf5b5f7fd3368389a0b1ec7ce0a23565b/multipart-1.2.1.tar.gz", hash = "sha256:829b909b67bc1ad1c6d4488fcdc6391c2847842b08323addf5200db88dbe9480", size = 36507 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/d1/3598d1e73385baaab427392856f915487db7aa10abadd436f8f2d3e3b0f9/multipart-1.2.1-py3-none-any.whl", hash = "sha256:c03dc203bc2e67f6b46a599467ae0d87cf71d7530504b2c1ff4a9ea21d8b8c8c", size = 13730 }, +] + +[[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'" }, + { 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/5e/2a/13e9ad339131c0fba5c70584f639005a47088f5eed77081a3d00479df0ca/mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a", size = 10955147 }, + { url = "https://files.pythonhosted.org/packages/94/39/02929067dc16b72d78109195cfed349ac4ec85f3d52517ac62b9a5263685/mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb", size = 10138373 }, + { url = "https://files.pythonhosted.org/packages/4a/cc/066709bb01734e3dbbd1375749f8789bf9693f8b842344fc0cf52109694f/mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b", size = 12543621 }, + { url = "https://files.pythonhosted.org/packages/f5/a2/124df839025348c7b9877d0ce134832a9249968e3ab36bb826bab0e9a1cf/mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74", size = 13050348 }, + { url = "https://files.pythonhosted.org/packages/45/86/cc94b1e7f7e756a63043cf425c24fb7470013ee1c032180282db75b1b335/mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6", size = 9615311 }, + { url = "https://files.pythonhosted.org/packages/5f/d4/b33ddd40dad230efb317898a2d1c267c04edba73bc5086bf77edeb410fb2/mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc", size = 11013906 }, + { url = "https://files.pythonhosted.org/packages/f4/e6/f414bca465b44d01cd5f4a82761e15044bedd1bf8025c5af3cc64518fac5/mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732", size = 10180657 }, + { url = "https://files.pythonhosted.org/packages/38/e9/fc3865e417722f98d58409770be01afb961e2c1f99930659ff4ae7ca8b7e/mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc", size = 12586394 }, + { url = "https://files.pythonhosted.org/packages/2e/35/f4d8b6d2cb0b3dad63e96caf159419dda023f45a358c6c9ac582ccaee354/mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d", size = 13103591 }, + { url = "https://files.pythonhosted.org/packages/22/1d/80594aef135f921dd52e142fa0acd19df197690bd0cde42cea7b88cf5aa2/mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24", size = 9634690 }, + { 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 = "natsort" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/a9/a0c57aee75f77794adaf35322f8b6404cbd0f89ad45c87197a937764b7d0/natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581", size = 76575 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c", size = 38268 }, +] + +[[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 = "opentelemetry-api" +version = "1.28.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "importlib-metadata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/34/e4e9245c868c6490a46ffedf6bd5b0f512bbc0a848b19e3a51f6bbad648c/opentelemetry_api-1.28.2.tar.gz", hash = "sha256:ecdc70c7139f17f9b0cf3742d57d7020e3e8315d6cffcdf1a12a905d45b19cc0", size = 62796 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/58/b17393cdfc149e14ee84c662abf921993dcce8058628359ef1f49e2abb97/opentelemetry_api-1.28.2-py3-none-any.whl", hash = "sha256:6fcec89e265beb258fe6b1acaaa3c8c705a934bd977b9f534a2b7c0d2d4275a6", size = 64302 }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.49b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/1f/9fa51f6f64f4d179f4e3370eb042176ff7717682428552f5e1f4c5efcc09/opentelemetry_instrumentation-0.49b2.tar.gz", hash = "sha256:8cf00cc8d9d479e4b72adb9bd267ec544308c602b7188598db5a687e77b298e2", size = 26480 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e3/ad23372525653b0221212d5e2a71bd97aae64cc35f90cbf0c70de57dfa4e/opentelemetry_instrumentation-0.49b2-py3-none-any.whl", hash = "sha256:f6d782b0ef9fef4a4c745298651c65f5c532c34cd4c40d230ab5b9f3b3b4d151", size = 30693 }, +] + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.49b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/42/079079bd7c0423bfab987a6457e34468b6ddccf501d3c91d2795c200d65d/opentelemetry_instrumentation_asgi-0.49b2.tar.gz", hash = "sha256:2af5faf062878330714efe700127b837038c4d9d3b70b451ab2424d5076d6c1c", size = 24106 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/82/06a56e786de3ea0ef4703ed313d9d8395fb4bc9ae740cc71415178ae8bff/opentelemetry_instrumentation_asgi-0.49b2-py3-none-any.whl", hash = "sha256:c8ede13ed781402458a800411cb7ec16a25386dc21de8e5b9a568b386a1dc5f4", size = 16305 }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.28.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/f4/840a5af4efe48d7fb4c456ad60fd624673e871a60d6494f7ff8a934755d4/opentelemetry_sdk-1.28.2.tar.gz", hash = "sha256:5fed24c5497e10df30282456fe2910f83377797511de07d14cec0d3e0a1a3110", size = 157272 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/8b/4f2b418496c08016d4384f9b1c4725a8af7faafa248d624be4bb95993ce1/opentelemetry_sdk-1.28.2-py3-none-any.whl", hash = "sha256:93336c129556f1e3ccd21442b94d3521759541521861b2214c499571b85cb71b", size = 118757 }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.49b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "opentelemetry-api" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0a/e3b93f94aa3223c6fd8e743502a1fefd4fb3a753d8f501ce2a418f7c0bd4/opentelemetry_semantic_conventions-0.49b2.tar.gz", hash = "sha256:44e32ce6a5bb8d7c0c617f84b9dc1c8deda1045a07dc16a688cc7cbeab679997", size = 95213 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/be/6661c8f76708bb3ba38c90be8fa8d7ffe17ccbc5cbbc229334f5535f6448/opentelemetry_semantic_conventions-0.49b2-py3-none-any.whl", hash = "sha256:51e7e1d0daa958782b6c2a8ed05e5f0e7dd0716fc327ac058777b8659649ee54", size = 159199 }, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.49b2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/28/ac5b1a0fd210ecb6c86c5e04256ba09c8308eb41e116097b9e2714d4b8dd/opentelemetry_util_http-0.49b2.tar.gz", hash = "sha256:5958c7009f79146bbe98b0fdb23d9d7bf1ea9cd154a1c199029b1a89e0557199", size = 7861 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/22/9128f10d1c2868ee42df7e10937d00f154a69bee87c416ca9b20a6af6c54/opentelemetry_util_http-0.49b2-py3-none-any.whl", hash = "sha256:e325d6511c6bee7b43170eb0c93261a210ec57e20ab1d7a99838515ef6d2bf58", size = 6941 }, +] + +[[package]] +name = "outcome" +version = "1.3.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692 }, +] + +[[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 = "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 = "piccolo" +version = "1.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "black" }, + { name = "colorama" }, + { name = "inflection" }, + { name = "jinja2" }, + { name = "pydantic", extra = ["email"] }, + { name = "targ" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/16/6661f9e716ec31d6d3f2057890dc20a00166d0befd436db546edc5830b8e/piccolo-1.21.0.tar.gz", hash = "sha256:0e639b0aa3a43d8a7644b396770acf1bf473dc125de6ce565cc5998a5395f5d5", size = 276901 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/66/8e743fea2940f4ce8b17834e64a8ff09a8eda38d91eae15208953dcbd4a6/piccolo-1.21.0-py3-none-any.whl", hash = "sha256:763e033547dcdb5ef602dc562fc75296101d78e0d3e1289135d25820afb3b118", size = 392851 }, +] + +[[package]] +name = "picologging" +version = "0.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/6d/3fd3423e0391cf7bfb78d6d7510da77068aeb73a0c6db22a6a0078413cab/picologging-0.9.3.tar.gz", hash = "sha256:6921f86ea0875ac85e252188627e9f04b872895327a7028e06c825ddb888a825", size = 103751 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/13/ef1eb916f920a04ceebf2419ed3a3a7ec86afe88d0ab535b1692489ed788/picologging-0.9.3-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:3d3219765be2430c4e434ff66a90147bd29db56cd24be00c5999d9827b08d479", size = 163223 }, + { url = "https://files.pythonhosted.org/packages/cc/59/89fdceb1267e62e27b61e0b06b20bc76fea17b375a2a0d9ab8bd4d635d07/picologging-0.9.3-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:1c4755c06c37b53e70ebdc9cd2b39c835221a12c5ef149cfaad9b6073b41ee89", size = 98747 }, + { url = "https://files.pythonhosted.org/packages/3f/eb/651438c3733250bf2f437a18268a8ea8dcdab8e4b795a00b4fb7bd21efd5/picologging-0.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b582222a768f2bc28ca4a34aa41a008c6aa3430cf1f128b2440e93f691d0d714", size = 165551 }, + { url = "https://files.pythonhosted.org/packages/3c/40/6d830b59d01d2ab8fb22fc8430d734a3176992cd66c52e1eca7012af3f8d/picologging-0.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f833de82e5fd9a981f64ebfadc3f4d12fe75ef31897e796ba04f6e28455e42c9", size = 177158 }, + { url = "https://files.pythonhosted.org/packages/55/86/ba8c330808709ca0d34a89efa49b487577b4dfc3f03cb6fd7e1960f1d01e/picologging-0.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b21e46794ef29fda0033a2bcfc38ec7a5fea78a6353a5f74f7a6374c0c0d3d15", size = 165298 }, + { url = "https://files.pythonhosted.org/packages/59/39/ba0d22a86e49058f89a1a2fa250b2e51404b22f038a843f3fcc3d697bd24/picologging-0.9.3-cp310-cp310-win32.whl", hash = "sha256:3ef12744c17fb670e5028315019589fe21527d1546463ef9baf9a454dcd3d7c4", size = 78095 }, + { url = "https://files.pythonhosted.org/packages/97/0d/9ecf95e091c06363041f4822faa75cc137a77e7c4d029243bd5f032f6a8e/picologging-0.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:09f27425175ae23cca0fb49bb55622073589cb57f73bdd2c7f72f4cf05419c3e", size = 86671 }, + { url = "https://files.pythonhosted.org/packages/6a/bf/af16546887682e76979a5920fdf170b715c485b0c0a37954115837dad046/picologging-0.9.3-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:fbd88e3dde71d72ac72dfe6a23a52e13fcdf2c9cf033e13a8f2718746fed942b", size = 163401 }, + { url = "https://files.pythonhosted.org/packages/eb/88/97907480df34edcc8382a82bdcb7b2090d55b2de4b64e270e5c732447da0/picologging-0.9.3-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:542628ccdc7a1eb321e5a7533803577a943c7d84e62456b681037f7bbc8abe26", size = 98808 }, + { url = "https://files.pythonhosted.org/packages/5e/a4/1c3acd317d54ad1cadfc729f63eb6205bc90027e5ea3f6c366e9bc2ae17f/picologging-0.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78ed29eea9f28d28edb39fa81b448fd27bdf510d4e4c79fe64e08742c782a965", size = 165660 }, + { url = "https://files.pythonhosted.org/packages/97/f3/67e19ea6595ad31ebd8bd398f3fe1899709bc7f439fce1694a154e2d7e20/picologging-0.9.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a38eb07ef48218712ce9f860b4a27e5086a89a89af7c5bcc1eaed6fb1e3eafd", size = 177284 }, + { url = "https://files.pythonhosted.org/packages/da/55/4c67d07472870e5d81f5fd1b38fe0b0ab9f08ad140ae4a6ae3d2aa029832/picologging-0.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3495969c6a8e1e2fa8d2bc89b8478bce10716bb6617f50822667909e79478cb4", size = 165449 }, + { url = "https://files.pythonhosted.org/packages/76/05/b0f47219c5230943e1d03bc92bbd74af94ef7bf471e425163296f5b0f05e/picologging-0.9.3-cp311-cp311-win32.whl", hash = "sha256:c97f0ab43ee32924b33cadd176c5bfe404e85e2bc6ae5e4c713c6d4cb4c7e6f3", size = 78141 }, + { url = "https://files.pythonhosted.org/packages/1c/38/e1e54b0ddc798173a209b932e939f3f37d2bb4d8cc24c528745a32a08035/picologging-0.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:f8d448e063b8a2cbe3c76d2d7ca2beb523779a31a8b14ee0a452ec1247ce56b8", size = 86727 }, + { url = "https://files.pythonhosted.org/packages/65/4a/cfd4d86d23ed1535e6728a13e56a1374105430dad894bb7395e78c0cd974/picologging-0.9.3-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:70e6957df044af10ff35d293b8ebad3f04bbfa99f88bcf4fd2a8248d1cd46320", size = 164148 }, + { url = "https://files.pythonhosted.org/packages/c6/71/7adec10b32be86f54666006e75ee7c941d7823fd4b89289660d4261e8c6d/picologging-0.9.3-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:32d18fb7c089afe9c2a8cb1493021417c872f807d74f3fd6da85e43226664ccd", size = 99348 }, + { url = "https://files.pythonhosted.org/packages/02/d3/06225fd4e5d09e35aedd82cf2c608c8a7cbe6da0b977c166af1895f6f370/picologging-0.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:025a6262280374413142648cf5be7944136c54a5cc84b0ff5b2416928d7aec96", size = 166198 }, + { url = "https://files.pythonhosted.org/packages/c5/69/84a6cb4a9f2f427ba3e207c6a640db7d5f2557ae56cd10a64cdfdf9541ea/picologging-0.9.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:feaf33a2fd8a9431f3f4de9693569952ebce6e4c92cabe642327130a1d378b6c", size = 178075 }, + { url = "https://files.pythonhosted.org/packages/96/b9/b7bc99e4193688d1316189e3b8c9788105361f2d9b00fd64ef7911a3a7b9/picologging-0.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06d315458ff92a2df6ce07540b390212d179e3ef2d08b48c356df72be8d0ce2d", size = 166276 }, + { url = "https://files.pythonhosted.org/packages/9b/6e/21abb1f3efdb4797b792d5afcb215f38971eff412c7d4e6ee5d85d86bcb9/picologging-0.9.3-cp312-cp312-win32.whl", hash = "sha256:502a17cbf7303499e1edcb01b3503caeac204aa5d5f888d4b5ecdb113f0c25ee", size = 78613 }, + { url = "https://files.pythonhosted.org/packages/34/29/1f3aa01d4b2f97583d7c98259f49cdb425e4980518a7c8348d552dd6a30b/picologging-0.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:16f111cdf7a210eb07bd06932c9a8ff06dc830e213c8a0efbb769e586d1f3efb", size = 87018 }, + { url = "https://files.pythonhosted.org/packages/3c/42/a3e939b9d4439f1b8277a070a45d110d8e9254c509c163a9ce5a161aa569/picologging-0.9.3-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:6672feda1d95e81694b447c27ce51d0beec86f779ed6803e2b4a522086ac7765", size = 162516 }, + { url = "https://files.pythonhosted.org/packages/62/c2/0e1971c9a756ac09a08abbdcd281b87641dc2ee37ec97a10ff682433930e/picologging-0.9.3-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:94f71cf9a868047db5414f8358c7df51f3c1913a8f1e892f79ad334e63c85860", size = 98312 }, + { url = "https://files.pythonhosted.org/packages/09/d8/3603d409f8230acd4b03ae2060beb6c3b877524ec56df75cc4802f3a5e76/picologging-0.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:936a23a8b98fac51a0bfc3ec23dd2c156fc6b8839a522287ca295f59a5c6dfef", size = 165440 }, + { url = "https://files.pythonhosted.org/packages/60/28/5f857c479576e370b9f3165deacd2fd57f99b44f7b764e29c22406aaca82/picologging-0.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce7aad1ccc61d65bafcfda7429d902bce53a706cd8b1ade48a4eb80900f0654e", size = 176970 }, + { url = "https://files.pythonhosted.org/packages/a4/c7/7730a79530ca5f053cb46ca56396a5e20a463f479c83e5ff30ad941d5c33/picologging-0.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251d69cf4f9d8e7f9cfb113a58d37d493555ee6380af5e4e11599c4f4b60f735", size = 165161 }, + { url = "https://files.pythonhosted.org/packages/38/0a/a4c43b40625adeac827f94ab1d10f46a685fccdc77a92c0767c552dc0aae/picologging-0.9.3-cp38-cp38-win32.whl", hash = "sha256:f66744aa21d4741ba5e6ecc21babaa3b2cb4dc6e944a02cd5d428ebf24992751", size = 77847 }, + { url = "https://files.pythonhosted.org/packages/ea/2b/6c1a0bd18e013af827c23443b8108aa18ab0f04e8635c9bb22e030f6ddac/picologging-0.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:ccc190c6eb8e75fd6ce3e7a34f923731ad5a8c58e26e5a83cbc922174cc79e3b", size = 86440 }, + { url = "https://files.pythonhosted.org/packages/22/6d/3fde8f7cac72f76365b962b8a9178d56e0806fb040761a6ed4cd4157b253/picologging-0.9.3-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:b4c7f4bf9a5fa291e0f8f8b669f6ea35943ccf2a962903472591ae377de58825", size = 163221 }, + { url = "https://files.pythonhosted.org/packages/cb/8c/c1054b46f18e81ba2f145b557b28aef0c09510375dae3ff6ec4e98262f6f/picologging-0.9.3-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:ea5663f5907910405edf75d438c05b24b66fd23cb997d7d28dfaabdea9b78211", size = 98745 }, + { url = "https://files.pythonhosted.org/packages/29/87/760b848042b1087fa87e5b5a48cd77bf943716965270332bd92aba68b70f/picologging-0.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a629db4cbbb96e66d01d34c93d197c04cb71838830c6652033ee8d2ebe76d01f", size = 165538 }, + { url = "https://files.pythonhosted.org/packages/45/d4/234dddc7102630273bd10b6bc8d0e4e046841edc390491ae9d306052c2b2/picologging-0.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:759f21dda1bf9ee52c7fa91c1faf47aa3585f4f604d5d38de02f3bcd32d7369f", size = 177133 }, + { url = "https://files.pythonhosted.org/packages/9f/4d/a243d32fd5613a948a2f3ba3a1307fe5942fe96173e09809856a071ff890/picologging-0.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d4bb95e708ad973217cc2cbb6e058f777ae2539a5cf131824c66fd701adfe71", size = 165272 }, + { url = "https://files.pythonhosted.org/packages/11/2c/c8da2fd086e2118c70094648e9f62014ac676b28821069ef011e09f3d89a/picologging-0.9.3-cp39-cp39-win32.whl", hash = "sha256:35e11311c71678813112e03649fa8dacad2f4ee5c3a424e515876df96c66b89b", size = 78146 }, + { url = "https://files.pythonhosted.org/packages/e5/ac/0a0d965301cb78c60b25ebf963e07773d74e7cc9be9edbc9c6ce902ccf80/picologging-0.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:e1ab2768f4c178df653bd6cc94ca31d9b94c6d4a90012598a185e80ea66f1a8f", size = 86760 }, +] + +[[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 = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/b3/4ae08d21eb097162f5aad37f4585f8069a86402ed7f5362cc9ae097f9572/pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32", size = 177079 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/75/526915fedf462e05eeb1c75ceaf7e3f9cde7b5ce6f62740fe5f7f19a0050/pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660", size = 203698 }, +] + +[[package]] +name = "priority" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/3c/eb7c35f4dcede96fca1842dac5f4f5d15511aa4b52f3a961219e68ae9204/priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0", size = 24792 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/5f/82c8074f7e84978129347c2c6ec8b6c59f3584ff1a20bc3c940a3e061790/priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa", size = 8946 }, +] + +[[package]] +name = "prometheus-client" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/54/a369868ed7a7f1ea5163030f4fc07d85d22d7a1d270560dab675188fb612/prometheus_client-0.21.0.tar.gz", hash = "sha256:96c83c606b71ff2b0a433c98889d275f51ffec6c5e267de37c7a2b5c9aa9233e", size = 78634 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/2d/46ed6436849c2c88228c3111865f44311cff784b4aabcdef4ea2545dbc3d/prometheus_client-0.21.0-py3-none-any.whl", hash = "sha256:4fa6b4dd0ac16d58bb587c04b1caae65b8c5043e85f778f42f5f632f6af2e166", size = 54686 }, +] + +[[package]] +name = "psutil" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/10/2a30b13c61e7cf937f4adf90710776b7918ed0a9c434e2c38224732af310/psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a", size = 508565 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/9e/8be43078a171381953cfee33c07c0d628594b5dbfc5157847b85022c2c1b/psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688", size = 247762 }, + { url = "https://files.pythonhosted.org/packages/1d/cb/313e80644ea407f04f6602a9e23096540d9dc1878755f3952ea8d3d104be/psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e", size = 248777 }, + { url = "https://files.pythonhosted.org/packages/65/8e/bcbe2025c587b5d703369b6a75b65d41d1367553da6e3f788aff91eaf5bd/psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38", size = 284259 }, + { url = "https://files.pythonhosted.org/packages/58/4d/8245e6f76a93c98aab285a43ea71ff1b171bcd90c9d238bf81f7021fb233/psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b", size = 287255 }, + { url = "https://files.pythonhosted.org/packages/27/c2/d034856ac47e3b3cdfa9720d0e113902e615f4190d5d1bdb8df4b2015fb2/psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a", size = 288804 }, + { url = "https://files.pythonhosted.org/packages/ea/55/5389ed243c878725feffc0d6a3bc5ef6764312b6fc7c081faaa2cfa7ef37/psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e", size = 250386 }, + { url = "https://files.pythonhosted.org/packages/11/91/87fa6f060e649b1e1a7b19a4f5869709fbf750b7c8c262ee776ec32f3028/psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be", size = 254228 }, +] + +[[package]] +name = "psycopg" +version = "3.1.20" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-zoneinfo", marker = "python_full_version < '3.9'" }, + { name = "typing-extensions" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/6d/0939210f3ba089b360cf0d3741494719152567bc81303cca2c0f1e67c78a/psycopg-3.1.20.tar.gz", hash = "sha256:32f5862ab79f238496236f97fe374a7ab55b4b4bb839a74802026544735f9a07", size = 147567 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/e9/126bbfd5dded758bb109526c5f5f2c2538fe293b15b6fa208db7078c72c4/psycopg-3.1.20-py3-none-any.whl", hash = "sha256:898a29f49ac9c903d554f5a6cdc44a8fc564325557c18f82e51f39c1f4fc2aeb", size = 179473 }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] +pool = [ + { name = "psycopg-pool" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.1.20" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/cc/18faf6fe8fceaa67222f273657e6cd6183d4e43f387f7741da00b98b8111/psycopg_binary-3.1.20-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:8dadeddb9d2dced49f2371f222db1d78b0a1c0f515c6e9c9e65c8f958c288ce1", size = 3344434 }, + { url = "https://files.pythonhosted.org/packages/a6/51/f952b53f267047da31e9d5724d98eb179574398d13b044668f468bca0f6f/psycopg_binary-3.1.20-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:67f285eaf706712d1ac46f4a7fc27226ee6184f411e45aff4044284ac34fe3a3", size = 3474729 }, + { url = "https://files.pythonhosted.org/packages/96/50/f86d517dc07b100b6d11535cbf01dd0cf12b6b2caabb8996644d8e21a75c/psycopg_binary-3.1.20-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da1b831f0a33e69bf79c4f39587167720c58c046d46ad86232f12c3e17e7c865", size = 4436412 }, + { url = "https://files.pythonhosted.org/packages/dd/e2/95a68198caa3b12ab3b7cde8c769fedf8d7952b9117c0affbd82c41c9cee/psycopg_binary-3.1.20-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c067284df02ea7bcede5f89cc1ed76511ceaf7e560e0f79528125f1a3ef38832", size = 4235923 }, + { url = "https://files.pythonhosted.org/packages/ce/cd/163f69306eb00341fdeac2ecd8ea8cbc9193a3548517f53f9222f5ecbf39/psycopg_binary-3.1.20-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8dbff9808ba07fba4afa0a6c823ab411f1cf9f1e27ea684bd307ed268f61a39", size = 4482436 }, + { url = "https://files.pythonhosted.org/packages/56/8d/f1991468805a711f72011b2b7df3c6d165671a3dd53a87a45d646cdf36cc/psycopg_binary-3.1.20-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808828fc485f23082f974811cad8aa75120a6dde248453c4fba60e8780bf1841", size = 4179768 }, + { url = "https://files.pythonhosted.org/packages/ac/91/ad39a6b31640960f23cb4dd8d81dc32c4c246637a5288a018bd46da838ff/psycopg_binary-3.1.20-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:eb8479dd184b2e6bbf8aae52ca946efff0d852b2ead386c26fa6de8c92257a9b", size = 3103511 }, + { url = "https://files.pythonhosted.org/packages/3e/6d/b3e34ddfe1adaea0b89fc20b435f61b7bb840aad67f50c0e547d2b64dd0c/psycopg_binary-3.1.20-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:78b5932f0f6f97e143272fea16753ecd9a00cb65db2c60ac3710bea6e739e09d", size = 3082174 }, + { url = "https://files.pythonhosted.org/packages/66/46/35a2fc272fbec7607546db7d19a2ef6f7d8c8a5ed9f0624e35e4f1618be9/psycopg_binary-3.1.20-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1623073d3f6449223ec4843cf4e36d05258567d93284f9a9b97618a87b2ae4", size = 3184566 }, + { url = "https://files.pythonhosted.org/packages/cd/31/5a1b61fa674cc5ae9c1163db537c6097761f55f9bb1224a68519d5b4a4d6/psycopg_binary-3.1.20-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c26863471abba88396281649df34dd29e70f37d695af73ef98a4a9038bbff674", size = 3222041 }, + { url = "https://files.pythonhosted.org/packages/00/e5/c5af8a6094acd80ac074ff2cc3fd9eb32909b6c679f41408a5b43b5ecd05/psycopg_binary-3.1.20-cp310-cp310-win_amd64.whl", hash = "sha256:bfc5955e3035f141a567ccc608ba65d01b97f9179ba8061f4b7ce80fe0edb327", size = 2894494 }, + { url = "https://files.pythonhosted.org/packages/f8/1c/45e5f240765e80076b08c3ed02c5dfeb5e97d549769b81f8382485d70a15/psycopg_binary-3.1.20-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:802989350fcbc783732bfef660afb34439a62727642a05e8bb9acf7d68993627", size = 3350503 }, + { url = "https://files.pythonhosted.org/packages/52/b8/acf96d388692d0bbf2346286f8b175778bc24046aca9181f50d9df9f4714/psycopg_binary-3.1.20-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:01b0e39128715fc37fed6cdc50ab58278eacb75709af503eb607654030975f09", size = 3480091 }, + { url = "https://files.pythonhosted.org/packages/41/d4/20604282ff08823d0e90cf092738ea21b339f56a172d8583565b272fc4be/psycopg_binary-3.1.20-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77af1086bedfa0729465565c636de3519079ba523d7b7ee6e8b9486beb1ee905", size = 4434555 }, + { url = "https://files.pythonhosted.org/packages/73/e0/3917b766508bb749e08225492d45ba7463b559de1c8a41d3f8f3cf0927cb/psycopg_binary-3.1.20-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9b9562395d441e225f354e8c6303ee6993a93aaeb0dbb5b94368f3249ab2388", size = 4231402 }, + { url = "https://files.pythonhosted.org/packages/b4/9b/251435896f7459beda355ef3e3919b6b20d067582cd6838ba248d3cff188/psycopg_binary-3.1.20-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e814d69e5447a93e7b98117ec95a8ce606d3742092fd120960551ed67c376fea", size = 4484218 }, + { url = "https://files.pythonhosted.org/packages/a1/12/b2057f9bb8b5f408139266a5b48bfd7578340296d7314d964b9f09e5b18f/psycopg_binary-3.1.20-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf1c2061600235ae9b11d7ad357cab89ac583a76bdb0199f7a29ac947939c20", size = 4176668 }, + { url = "https://files.pythonhosted.org/packages/80/9c/a62fe4167427a06e69882d274ba90903507afc89caf6bcc3671790a20875/psycopg_binary-3.1.20-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:50f1d807b4167f973a6f67bca39bf656b737f7426be158a1dc9cb0000d020744", size = 3102502 }, + { url = "https://files.pythonhosted.org/packages/98/83/bceca23dd830d4069949e70dec9feb03c114cc551b104f0e2b48b1e598c6/psycopg_binary-3.1.20-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4cf6ec1490232a5b208dae94a8269dc739e6762684c8658a0f3570402db934ae", size = 3080005 }, + { url = "https://files.pythonhosted.org/packages/fc/83/bab7c8495e0eb11bf710663afb2849c2d3c91a2bf61b2bd597941f57f80b/psycopg_binary-3.1.20-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:309c09ec50a9c5c8492c2922ee666df1e30a08b08a9b63083d0daa414eccd09c", size = 3182315 }, + { url = "https://files.pythonhosted.org/packages/ca/9b/bd4970faed24ae4a850ee8c6ebd621e98fd86e2962e13038603a726e2504/psycopg_binary-3.1.20-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e2c33a01799f93ef8c11a023df66280e39ca3c3249a2581adb2a0e5e80801088", size = 3222552 }, + { url = "https://files.pythonhosted.org/packages/5d/0b/7ab0744f282df53968f5066d5fd8bf3f994f90bf2a8003ab40278818d0f2/psycopg_binary-3.1.20-cp311-cp311-win_amd64.whl", hash = "sha256:2c67532057fda72579b02d9d61e9cc8975982844bd5c3c9dc7f84ce8bcac859c", size = 2899115 }, + { url = "https://files.pythonhosted.org/packages/94/12/6e909d3a20f7bfa6915c1fdf64ab47bb9ca44b837adb468841aad51bab6c/psycopg_binary-3.1.20-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:ef08de60f1b8503a6f6b6f5bee612de36373c09bc0e3f84409fab09e1ff72107", size = 3326944 }, + { url = "https://files.pythonhosted.org/packages/e1/4e/dc425f5c8c102045486f2fa39c3cb379b073557d6bd2cf5d06de81036d7c/psycopg_binary-3.1.20-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a4847fa31c8d3a6dd3536cf1e130dfcc454ed26be471ef274e4358bf7f709cda", size = 3475444 }, + { url = "https://files.pythonhosted.org/packages/cd/cd/6484cbdb82dc29bfe43ae8c401a0be309402c304d1aaabcccf1e21908663/psycopg_binary-3.1.20-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b72e9c8c79dcc30e34e996079cfe0374b7c7233d2b5f6f25a0bc8872fe2babef", size = 4412872 }, + { url = "https://files.pythonhosted.org/packages/25/d3/d403dc61f9d8b56683a6a1db47ab156807d2e1c442b044fba5763e786893/psycopg_binary-3.1.20-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836246f3c486ef7edfce6cf6cc760173e244826ebecd54c1b63c91d4cc0341f7", size = 4216654 }, + { url = "https://files.pythonhosted.org/packages/d3/ff/389198638ad10ec0e80fcc97b5c8092987214d9ac529b1224bf0f7e221da/psycopg_binary-3.1.20-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:015f70b17539ec0ecfb0f87bcaface0c7fa1289b6e7e2313dc7cdfdc513e3235", size = 4451310 }, + { url = "https://files.pythonhosted.org/packages/84/94/9ae70af00caf9ce98f857a883ff64c5d236dfea5b7b4b8528d28e80515aa/psycopg_binary-3.1.20-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f52498dc7b41fee74e971823ede4519e3a9597d416f7a2044dbe4b98cc61ff35", size = 4153667 }, + { url = "https://files.pythonhosted.org/packages/b8/57/b8a34174803683ef0f3f2fe18304f7048d31bab431f21cf511598b894ed7/psycopg_binary-3.1.20-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:92b61bae0ac881580faa1c89bf2167db7041cb01cc0bd686244f9c20a010036a", size = 3081906 }, + { url = "https://files.pythonhosted.org/packages/bf/e7/5df8c4794f13004787cd7ddfe456eec90f49d1b99f1a10947f7ba2a67487/psycopg_binary-3.1.20-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3532b8677666aadb64a4e31f6e97fe4ab71b862ab100d337faf497198339fd4d", size = 3061376 }, + { url = "https://files.pythonhosted.org/packages/8e/c6/ec4abb814f54af4b659896ce10386be0c538dad8111b3daeaf672b4daa03/psycopg_binary-3.1.20-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f7df27f50a7db84c28e58be3df41f39618161096c3379ad68bc665a454c53e93", size = 3150174 }, + { url = "https://files.pythonhosted.org/packages/0c/50/7b4382e5f5d256ac720ee0bd6470c7aa7d28f78570bd44d5e0b1c29eeb96/psycopg_binary-3.1.20-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12b33c511f0be79d5a68231a10972ef9c68d954d30d176679472057ecc22891a", size = 3198871 }, + { url = "https://files.pythonhosted.org/packages/76/2f/eda1b86c01d2803ac05714b94283af1e5012437dcc63dfe0679cc4d445ad/psycopg_binary-3.1.20-cp312-cp312-win_amd64.whl", hash = "sha256:6f3c0b05fc3cbd4d99aaacf5c7afa13b086df5777b9fefb78d31bf81fc70bd04", size = 2884414 }, + { url = "https://files.pythonhosted.org/packages/d3/67/919392d6bd182a33f92d8db02d459d4a3153a30bf137d8d7900c035064e9/psycopg_binary-3.1.20-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:f1c78e40ba9a808b6f870f94efc3cfbf479169bf6c4f46c2b1e258a4b035b2ba", size = 3345000 }, + { url = "https://files.pythonhosted.org/packages/1e/b2/59ad867ebef0fa93fc89e5d243689f494f4309ee1f64736cd5cb232c7ea3/psycopg_binary-3.1.20-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44306e4b1acef590dc063d63317dc0ac34fce89756723efd22bd770c1a04850c", size = 4437360 }, + { url = "https://files.pythonhosted.org/packages/b4/d7/535edefbe0838aaba77ddd13ddb3c21e80c23e4721c33b4aa0e7bae8af39/psycopg_binary-3.1.20-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8c72fe67722ab78c7f4466c79539247306cde260367a4ac42e6302c26a7d6d2", size = 4237652 }, + { url = "https://files.pythonhosted.org/packages/11/c1/851ecbc3d261bb142c6775fd36aed8588f35baaf192ce1b89c07bc4fa702/psycopg_binary-3.1.20-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e1f9fa0a6404c7e405bed4f3237e4b4e9292d711deff0d870dcf66f87f0aad7", size = 4487045 }, + { url = "https://files.pythonhosted.org/packages/06/c7/8c8c89b1d7d37d47e31016316e0eb0ea3ec7ba084fb87bbd8ea648ae99e3/psycopg_binary-3.1.20-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1851c4ed763969a613246024d37153357308eeb78889dcd6d739b7240dacd4e", size = 4181015 }, + { url = "https://files.pythonhosted.org/packages/6f/d9/7259f5dc2b9e53c00914342f6b3a772d5460fd22a9f6f9ce2e61e0b2bc8f/psycopg_binary-3.1.20-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4fc1a6bc9cf8c23d87f3c3f79517b0ee15789f183ef84d077d68c5e1fad4677a", size = 3106010 }, + { url = "https://files.pythonhosted.org/packages/8a/16/3125dc69e4affe1aeee8be551f983c9b4935151d36d442415dc7acc2fb2e/psycopg_binary-3.1.20-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:b0d3ee6ae9760545cc78d03eaa858898cc40a59ca4cc2047f198cac2d1a000cc", size = 3083839 }, + { url = "https://files.pythonhosted.org/packages/12/48/344c13597330b31d25423906cbaf85147938e1dfb78f98de14cd912b8b18/psycopg_binary-3.1.20-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:8285827339f6221861c05c3a292e2464e114c1d0f93ef03c5756c16f3a755520", size = 3189184 }, + { url = "https://files.pythonhosted.org/packages/3b/17/c96487d0d70a7bf9aa972d9d68b1fafae6f478c21a907e04ea2f1ccf1d49/psycopg_binary-3.1.20-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e1656794434574d01955f2ceaaef88b4f5edd2099205c383680fa7d8ec18496b", size = 3227429 }, + { url = "https://files.pythonhosted.org/packages/7d/a2/ef22ee10fcebb0eac95917d795fd8f42c1903be172d79c41eaee6234e4a4/psycopg_binary-3.1.20-cp38-cp38-win_amd64.whl", hash = "sha256:0f5313ccad37d3f3d87fc8615feeb85b6f99975a338d135d641f2d0921a393dc", size = 2902065 }, + { url = "https://files.pythonhosted.org/packages/8a/72/fdb0a4eef4c17b2918eb43994944a9a2c5c849195021a7323372ec96d394/psycopg_binary-3.1.20-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:6902c01cf483dd60565833b04ea6a2ef4151cf9fdb88d461f914b49379470675", size = 3345720 }, + { url = "https://files.pythonhosted.org/packages/16/d2/1f95b18afaad6a4bc153c16d0a71cd73ffcc7190a53f82b8cd50bfffdbd2/psycopg_binary-3.1.20-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:381e096a0c7f8bcb00ca5121f335d9a2298c3a12d4d6043a4b07d9efa1816606", size = 4437674 }, + { url = "https://files.pythonhosted.org/packages/4e/6e/aa8c620e2235a0172d783abfd15c0c9b724a28d26e679aef3532a2a02d43/psycopg_binary-3.1.20-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ecd8cb66716ff5be7b1ca9c318c4a843807819a245f7c87e0aadd0d0283bc36", size = 4238118 }, + { url = "https://files.pythonhosted.org/packages/07/84/bc4b3fe82779c121549650e1e8ef0831c957b82936a8a9b3b8aa7478049c/psycopg_binary-3.1.20-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7462bddd3ffd9875b6344a10c379bba820b93a7c4ca962d2e5e9673a0cf46cf5", size = 4483525 }, + { url = "https://files.pythonhosted.org/packages/40/dc/11977825f32a1ac60ee8fd3c3f9c7323ce39a3b4f4140361718248f56626/psycopg_binary-3.1.20-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43e16e3d76021db831c95d2ade55de0d059b1f17732ba818265c6fcb3b662cb1", size = 4180183 }, + { url = "https://files.pythonhosted.org/packages/8c/3b/eed601fb8bf227576cf0bee3c300a9fa1f8964902ccf2b6f1d4e7789239a/psycopg_binary-3.1.20-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:75779e3b9d86576491653e78122a0bdedb791fdf65fe1d5caa5d002560912425", size = 3105224 }, + { url = "https://files.pythonhosted.org/packages/50/e8/a60fdfd76dc28b611003ac0224461031c09d259ce2920e70ea8efe599772/psycopg_binary-3.1.20-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4b9487593e511c5a6b7a0165e5bddf57efcc4d40173f2ac52e51659637840094", size = 3083096 }, + { url = "https://files.pythonhosted.org/packages/b7/c0/11297017a58b27bc6df1f9c6e74409512c23cc055b9dcdb58e4deefb20c7/psycopg_binary-3.1.20-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b03f6f7e512c6a8e37b10814bcb53dcd2ca0c02512a661b3aefffd7b6009e412", size = 3184296 }, + { url = "https://files.pythonhosted.org/packages/42/ff/2f06d75396431a459d0fa8debdb66a7f04070e51b04eae0046e76966ebb0/psycopg_binary-3.1.20-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8a92fc898af4080e3cf562c02e3a8a9cb897dc70976c91e05fd064bff76928ea", size = 3223204 }, + { url = "https://files.pythonhosted.org/packages/01/10/01ec61e06f6bb2305ab66f05f0501431ce5edbb61700e1cd5fa73cf05884/psycopg_binary-3.1.20-cp39-cp39-win_amd64.whl", hash = "sha256:47dd369cb4b263d29aed12ee23b37c03e58bfe656843692d109896c258c554b0", size = 2896197 }, +] + +[[package]] +name = "psycopg-pool" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/71/01d4e589dc5fd1f21368b7d2df183ed0e5bbc160ce291d745142b229797b/psycopg_pool-3.2.4.tar.gz", hash = "sha256:61774b5bbf23e8d22bedc7504707135aaf744679f8ef9b3fe29942920746a6ed", size = 29749 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/28/2b56ac94c236ee033c7b291bcaa6a83089d0cc0fe7830c35f6521177c199/psycopg_pool-3.2.4-py3-none-any.whl", hash = "sha256:f6a22cff0f21f06d72fb2f5cb48c618946777c49385358e0c88d062c59cbd224", size = 38240 }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/81/331257dbf2801cdb82105306042f7a1637cc752f65f2bb688188e0de5f0b/psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f", size = 3043397 }, + { url = "https://files.pythonhosted.org/packages/e7/9a/7f4f2f031010bbfe6a02b4a15c01e12eb6b9b7b358ab33229f28baadbfc1/psycopg2_binary-2.9.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906", size = 3274806 }, + { url = "https://files.pythonhosted.org/packages/e5/57/8ddd4b374fa811a0b0a0f49b6abad1cde9cb34df73ea3348cc283fcd70b4/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92", size = 2851361 }, + { url = "https://files.pythonhosted.org/packages/f9/66/d1e52c20d283f1f3a8e7e5c1e06851d432f123ef57b13043b4f9b21ffa1f/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007", size = 3080836 }, + { url = "https://files.pythonhosted.org/packages/a0/cb/592d44a9546aba78f8a1249021fe7c59d3afb8a0ba51434d6610cc3462b6/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0", size = 3264552 }, + { url = "https://files.pythonhosted.org/packages/64/33/c8548560b94b7617f203d7236d6cdf36fe1a5a3645600ada6efd79da946f/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4", size = 3019789 }, + { url = "https://files.pythonhosted.org/packages/b0/0e/c2da0db5bea88a3be52307f88b75eec72c4de62814cbe9ee600c29c06334/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1", size = 2871776 }, + { url = "https://files.pythonhosted.org/packages/15/d7/774afa1eadb787ddf41aab52d4c62785563e29949613c958955031408ae6/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5", size = 2820959 }, + { url = "https://files.pythonhosted.org/packages/5e/ed/440dc3f5991a8c6172a1cde44850ead0e483a375277a1aef7cfcec00af07/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5", size = 2919329 }, + { url = "https://files.pythonhosted.org/packages/03/be/2cc8f4282898306732d2ae7b7378ae14e8df3c1231b53579efa056aae887/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53", size = 2957659 }, + { url = "https://files.pythonhosted.org/packages/d0/12/fb8e4f485d98c570e00dad5800e9a2349cfe0f71a767c856857160d343a5/psycopg2_binary-2.9.10-cp310-cp310-win32.whl", hash = "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b", size = 1024605 }, + { url = "https://files.pythonhosted.org/packages/22/4f/217cd2471ecf45d82905dd09085e049af8de6cfdc008b6663c3226dc1c98/psycopg2_binary-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1", size = 1163817 }, + { url = "https://files.pythonhosted.org/packages/9c/8f/9feb01291d0d7a0a4c6a6bab24094135c2b59c6a81943752f632c75896d6/psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff", size = 3043397 }, + { url = "https://files.pythonhosted.org/packages/15/30/346e4683532011561cd9c8dfeac6a8153dd96452fee0b12666058ab7893c/psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c", size = 3274806 }, + { url = "https://files.pythonhosted.org/packages/66/6e/4efebe76f76aee7ec99166b6c023ff8abdc4e183f7b70913d7c047701b79/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c", size = 2851370 }, + { url = "https://files.pythonhosted.org/packages/7f/fd/ff83313f86b50f7ca089b161b8e0a22bb3c319974096093cd50680433fdb/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb", size = 3080780 }, + { url = "https://files.pythonhosted.org/packages/e6/c4/bfadd202dcda8333a7ccafdc51c541dbdfce7c2c7cda89fa2374455d795f/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341", size = 3264583 }, + { url = "https://files.pythonhosted.org/packages/5d/f1/09f45ac25e704ac954862581f9f9ae21303cc5ded3d0b775532b407f0e90/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a", size = 3019831 }, + { url = "https://files.pythonhosted.org/packages/9e/2e/9beaea078095cc558f215e38f647c7114987d9febfc25cb2beed7c3582a5/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b", size = 2871822 }, + { url = "https://files.pythonhosted.org/packages/01/9e/ef93c5d93f3dc9fc92786ffab39e323b9aed066ba59fdc34cf85e2722271/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7", size = 2820975 }, + { url = "https://files.pythonhosted.org/packages/a5/f0/049e9631e3268fe4c5a387f6fc27e267ebe199acf1bc1bc9cbde4bd6916c/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e", size = 2919320 }, + { url = "https://files.pythonhosted.org/packages/dc/9a/bcb8773b88e45fb5a5ea8339e2104d82c863a3b8558fbb2aadfe66df86b3/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68", size = 2957617 }, + { url = "https://files.pythonhosted.org/packages/e2/6b/144336a9bf08a67d217b3af3246abb1d027095dab726f0687f01f43e8c03/psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392", size = 1024618 }, + { url = "https://files.pythonhosted.org/packages/61/69/3b3d7bd583c6d3cbe5100802efa5beacaacc86e37b653fc708bf3d6853b8/psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4", size = 1163816 }, + { url = "https://files.pythonhosted.org/packages/49/7d/465cc9795cf76f6d329efdafca74693714556ea3891813701ac1fee87545/psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", size = 3044771 }, + { url = "https://files.pythonhosted.org/packages/8b/31/6d225b7b641a1a2148e3ed65e1aa74fc86ba3fee850545e27be9e1de893d/psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", size = 3275336 }, + { url = "https://files.pythonhosted.org/packages/30/b7/a68c2b4bff1cbb1728e3ec864b2d92327c77ad52edcd27922535a8366f68/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", size = 2851637 }, + { url = "https://files.pythonhosted.org/packages/0b/b1/cfedc0e0e6f9ad61f8657fd173b2f831ce261c02a08c0b09c652b127d813/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", size = 3082097 }, + { url = "https://files.pythonhosted.org/packages/18/ed/0a8e4153c9b769f59c02fb5e7914f20f0b2483a19dae7bf2db54b743d0d0/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", size = 3264776 }, + { url = "https://files.pythonhosted.org/packages/10/db/d09da68c6a0cdab41566b74e0a6068a425f077169bed0946559b7348ebe9/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", size = 3020968 }, + { url = "https://files.pythonhosted.org/packages/94/28/4d6f8c255f0dfffb410db2b3f9ac5218d959a66c715c34cac31081e19b95/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", size = 2872334 }, + { url = "https://files.pythonhosted.org/packages/05/f7/20d7bf796593c4fea95e12119d6cc384ff1f6141a24fbb7df5a668d29d29/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", size = 2822722 }, + { url = "https://files.pythonhosted.org/packages/4d/e4/0c407ae919ef626dbdb32835a03b6737013c3cc7240169843965cada2bdf/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", size = 2920132 }, + { url = "https://files.pythonhosted.org/packages/2d/70/aa69c9f69cf09a01da224909ff6ce8b68faeef476f00f7ec377e8f03be70/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", size = 2959312 }, + { url = "https://files.pythonhosted.org/packages/d3/bd/213e59854fafe87ba47814bf413ace0dcee33a89c8c8c814faca6bc7cf3c/psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", size = 1025191 }, + { url = "https://files.pythonhosted.org/packages/92/29/06261ea000e2dc1e22907dbbc483a1093665509ea586b29b8986a0e56733/psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", size = 1164031 }, + { url = "https://files.pythonhosted.org/packages/3e/30/d41d3ba765609c0763505d565c4d12d8f3c79793f0d0f044ff5a28bf395b/psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", size = 3044699 }, + { url = "https://files.pythonhosted.org/packages/35/44/257ddadec7ef04536ba71af6bc6a75ec05c5343004a7ec93006bee66c0bc/psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", size = 3275245 }, + { url = "https://files.pythonhosted.org/packages/1b/11/48ea1cd11de67f9efd7262085588790a95d9dfcd9b8a687d46caf7305c1a/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", size = 2851631 }, + { url = "https://files.pythonhosted.org/packages/62/e0/62ce5ee650e6c86719d621a761fe4bc846ab9eff8c1f12b1ed5741bf1c9b/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", size = 3082140 }, + { url = "https://files.pythonhosted.org/packages/27/ce/63f946c098611f7be234c0dd7cb1ad68b0b5744d34f68062bb3c5aa510c8/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", size = 3264762 }, + { url = "https://files.pythonhosted.org/packages/43/25/c603cd81402e69edf7daa59b1602bd41eb9859e2824b8c0855d748366ac9/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", size = 3020967 }, + { url = "https://files.pythonhosted.org/packages/5f/d6/8708d8c6fca531057fa170cdde8df870e8b6a9b136e82b361c65e42b841e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", size = 2872326 }, + { url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712 }, + { url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155 }, + { url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356 }, + { url = "https://files.pythonhosted.org/packages/03/a7/7aa45bea9c790da0ec4765902d714ee7c43b73ccff34916261090849b715/psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4", size = 3043405 }, + { url = "https://files.pythonhosted.org/packages/0e/ea/e0197035d74cc1065e94f2ebf7cdd9fa4aa00bb06b1850091568345441cd/psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8", size = 2851210 }, + { url = "https://files.pythonhosted.org/packages/23/bf/9be0b2dd105299860e6b001ad7519e36208944609c8382d5aa2dfc58294c/psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864", size = 3080972 }, + { url = "https://files.pythonhosted.org/packages/04/19/bd5324737573f1278d65a2abb907332b31b42622421232c42909c8802378/psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5", size = 3264718 }, + { url = "https://files.pythonhosted.org/packages/23/ac/e39fa755f7c99aed7a2ff5f0550519248aa8f9d39c2b0705dfc3b4f13a27/psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa", size = 3019807 }, + { url = "https://files.pythonhosted.org/packages/93/0d/4be488917130cde91431d859fce2b004417bce96a5fbb854d813ab9c2bde/psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92", size = 2871932 }, + { url = "https://files.pythonhosted.org/packages/6f/db/45ca7735a461ea2669ee579afa9e23af9d0f9453ca34c357dd648625ed39/psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44", size = 2820988 }, + { url = "https://files.pythonhosted.org/packages/90/2b/1123431e34df437768fd0d1fbb2ddde36bf44d8b3288cf1512ff66306bc3/psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863", size = 2919351 }, + { url = "https://files.pythonhosted.org/packages/a0/9d/d4ef15458a9b879ea3bdde77c93b16ea49762cc281f44cfd8850bb537050/psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3", size = 2957589 }, + { url = "https://files.pythonhosted.org/packages/a2/bc/e77648009b6e61af327c607543f65fdf25bcfb4100f5a6f3bdb62ddac03c/psycopg2_binary-2.9.10-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b", size = 3043437 }, + { url = "https://files.pythonhosted.org/packages/e0/e8/5a12211a1f5b959f3e3ccd342eace60c1f26422f53e06d687821dc268780/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc", size = 2851340 }, + { url = "https://files.pythonhosted.org/packages/47/ed/5932b0458a7fc61237b653df050513c8d18a6f4083cc7f90dcef967f7bce/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697", size = 3080905 }, + { url = "https://files.pythonhosted.org/packages/71/df/8047d85c3d23864aca4613c3be1ea0fe61dbe4e050a89ac189f9dce4403e/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481", size = 3264640 }, + { url = "https://files.pythonhosted.org/packages/f3/de/6157e4ef242920e8f2749f7708d5cc8815414bdd4a27a91996e7cd5c80df/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648", size = 3019812 }, + { url = "https://files.pythonhosted.org/packages/25/f9/0fc49efd2d4d6db3a8d0a3f5749b33a0d3fdd872cad49fbf5bfce1c50027/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d", size = 2871933 }, + { url = "https://files.pythonhosted.org/packages/57/bc/2ed1bd182219065692ed458d218d311b0b220b20662d25d913bc4e8d3549/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30", size = 2820990 }, + { url = "https://files.pythonhosted.org/packages/71/2a/43f77a9b8ee0b10e2de784d97ddc099d9fe0d9eec462a006e4d2cc74756d/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c", size = 2919352 }, + { url = "https://files.pythonhosted.org/packages/57/86/d2943df70469e6afab3b5b8e1367fccc61891f46de436b24ddee6f2c8404/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287", size = 2957614 }, + { url = "https://files.pythonhosted.org/packages/85/21/195d69371330983aa16139e60ba855d0a18164c9295f3a3696be41bbcd54/psycopg2_binary-2.9.10-cp39-cp39-win32.whl", hash = "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8", size = 1025341 }, + { url = "https://files.pythonhosted.org/packages/ad/53/73196ebc19d6fbfc22427b982fbc98698b7b9c361e5e7707e3a3247cf06d/psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5", size = 1163958 }, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/67/6afbf0d507f73c32d21084a79946bfcfca5fbc62a72057e9c23797a737c9/pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c", size = 310028 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/89/bc88a6711935ba795a679ea6ebee07e128050d6382eaa35a0a47c8032bdc/pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd", size = 181537 }, +] + +[[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.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/86/a03390cb12cf64e2a8df07c267f3eb8d5035e0f9a04bb20fb79403d2a00e/pydantic-2.10.2.tar.gz", hash = "sha256:2bc2d7f17232e0841cbba4641e65ba1eb6fafb3a08de3a091ff3ce14a197c4fa", size = 785401 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/74/da832196702d0c56eb86b75bfa346db9238617e29b0b7ee3b8b4eccfe654/pydantic-2.10.2-py3-none-any.whl", hash = "sha256:cfb96e45951117c3024e6b67b25cdc33a3cb7b2fa62e239f7af1378358a1d99e", size = 456364 }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[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/97/bb/c62074a65a32ed279bef44862e89fabb5ab1a81df8a9d383bddb4f49a1e0/pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62", size = 1901535 }, + { url = "https://files.pythonhosted.org/packages/9b/59/e224c93f95ffd4f5d37f1d148c569eda8ae23446ab8daf3a211ac0533e08/pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab", size = 1781287 }, + { url = "https://files.pythonhosted.org/packages/11/e2/33629134e577543b9335c5ca9bbfd2348f5023fda956737777a7a3b86788/pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864", size = 1834575 }, + { url = "https://files.pythonhosted.org/packages/fe/16/82e0849b3c6deb0330c07f1a8d55708d003ec8b1fd38ac84c7a830e25252/pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067", size = 1857948 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/cdee588a7440bc58b6351e8b8dc2432e38b1144b5ae6625bfbdfb7fa76d9/pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd", size = 2041138 }, + { url = "https://files.pythonhosted.org/packages/1d/0e/73e0d1dff37a29c31e5b3e8587d228ced736cc7af9f81f6d7d06aa47576c/pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5", size = 2783820 }, + { url = "https://files.pythonhosted.org/packages/9a/b1/f164d05be347b99b91327ea9dd1118562951d2c86e1ea943ef73636b0810/pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78", size = 2138035 }, + { url = "https://files.pythonhosted.org/packages/72/44/cf1f20d3036d7e1545eafde0af4f3172075573a407a3a20313115c8990ff/pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f", size = 1991778 }, + { url = "https://files.pythonhosted.org/packages/5d/4c/486d8ddd595892e7d791f26dfd3e51bd8abea478eb7747fe2bbe890a2177/pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36", size = 1996644 }, + { url = "https://files.pythonhosted.org/packages/33/2a/9a1cd4c8aca242816be431583a3250797f2932fad32d35ad5aefcea179bc/pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a", size = 2091778 }, + { url = "https://files.pythonhosted.org/packages/8f/61/03576dac806c49e76a714c23f501420b0aeee80f97b995fc4b28fe63a010/pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b", size = 2146020 }, + { url = "https://files.pythonhosted.org/packages/72/82/e236d762052d24949aabad3952bc2c8635a470d6f3cbdd69498692afa679/pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618", size = 1819443 }, + { url = "https://files.pythonhosted.org/packages/6e/89/26816cad528ca5d4af9be33aa91507504c4576100e53b371b5bc6d3c797b/pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4", size = 1979478 }, + { url = "https://files.pythonhosted.org/packages/bc/6a/d741ce0c7da75ce9b394636a406aace00ad992ae417935ef2ad2e67fb970/pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967", size = 1898376 }, + { url = "https://files.pythonhosted.org/packages/bd/68/6ba18e30f10c7051bc55f1dffeadbee51454b381c91846104892a6d3b9cd/pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60", size = 1777246 }, + { url = "https://files.pythonhosted.org/packages/36/b8/6f1b7c5f068c00dfe179b8762bc1d32c75c0e9f62c9372174b1b64a74aa8/pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854", size = 1832148 }, + { url = "https://files.pythonhosted.org/packages/d9/83/83ff64d599847f080a93df119e856e3bd93063cced04b9a27eb66d863831/pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9", size = 1856371 }, + { url = "https://files.pythonhosted.org/packages/72/e9/974e6c73f59627c446833ecc306cadd199edab40abcfa093372a5a5c0156/pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd", size = 2038686 }, + { url = "https://files.pythonhosted.org/packages/5e/bb/5e912d02dcf29aebb2da35e5a1a26088c39ffc0b1ea81242ee9db6f1f730/pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be", size = 2785725 }, + { url = "https://files.pythonhosted.org/packages/85/d7/936846087424c882d89c853711687230cd60179a67c79c34c99b64f92625/pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e", size = 2135177 }, + { url = "https://files.pythonhosted.org/packages/82/72/5a386e5ce8d3e933c3f283e61357474181c39383f38afffc15a6152fa1c5/pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792", size = 1989877 }, + { url = "https://files.pythonhosted.org/packages/ce/5c/b1c417a5fd67ce132d78d16a6ba7629dc7f188dbd4f7c30ef58111ee5147/pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01", size = 1996006 }, + { url = "https://files.pythonhosted.org/packages/dd/04/4e18f2c42b29929882f30e4c09a3a039555158995a4ac730a73585198a66/pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9", size = 2091441 }, + { url = "https://files.pythonhosted.org/packages/06/84/5a332345b7efb5ab361f916eaf7316ef010e72417e8c7dd3d34462ee9840/pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131", size = 2144471 }, + { url = "https://files.pythonhosted.org/packages/54/58/23caa58c35d36627156789c0fb562264c12cfdb451c75eb275535188a96f/pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3", size = 1816563 }, + { url = "https://files.pythonhosted.org/packages/f7/9c/e83f08adc8e222b43c7f11d98b27eba08f21bcb259bcbf74743ce903c49c/pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c", size = 1983137 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/85/3e/f6f75ba36678fee11dd07a7729e9ed172ecf31e3f50a5d636e9605eee2af/pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f", size = 1894250 }, + { url = "https://files.pythonhosted.org/packages/d3/2d/a40578918e2eb5b4ee0d206a4fb6c4040c2bf14e28d29fba9bd7e7659d16/pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31", size = 1772035 }, + { url = "https://files.pythonhosted.org/packages/7f/ee/0377e9f4ca5a47e8885f670a65c0a647ddf9ce98d50bf7547cf8e1ee5771/pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3", size = 1827025 }, + { url = "https://files.pythonhosted.org/packages/fe/0b/a24d9ef762d05bebdfafd6d5d176b990728fa9ec8ea7b6040d6fb5f3caaa/pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154", size = 1980927 }, + { url = "https://files.pythonhosted.org/packages/00/bd/deadc1722eb7dfdf787a3bbcd32eabbdcc36931fd48671a850e1b9f2cd77/pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd", size = 1980918 }, + { url = "https://files.pythonhosted.org/packages/f0/05/5d09d0b0e92053d538927308ea1d35cb25ab543d9c3e2eb2d7653bc73690/pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a", size = 1989990 }, + { url = "https://files.pythonhosted.org/packages/5b/7e/f7191346d1c3ac66049f618ee331359f8552a8b68a2daf916003c30b6dc8/pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97", size = 2079871 }, + { url = "https://files.pythonhosted.org/packages/f3/65/2caf4f7ad65413a137d43cb9578c54d1abd3224be786ad840263c1bf9e0f/pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2", size = 2133569 }, + { url = "https://files.pythonhosted.org/packages/fd/ab/718d9a1c41bb8d3e0e04d15b68b8afc135f8fcf552705b62f226225065c7/pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840", size = 2002035 }, +] + +[[package]] +name = "pydantic-extra-types" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/92/8542f406466d11bf348b795d498906034f9bb9016f09e906ff7fee6444be/pydantic_extra_types-2.10.0.tar.gz", hash = "sha256:552c47dd18fe1d00cfed75d9981162a2f3203cf7e77e55a3d3e70936f59587b9", size = 44559 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/41/0b0cc8b59c31a04bdfde2ae71fccbb13c11fadafc8bd41a2af3e76db7e44/pydantic_extra_types-2.10.0-py3-none-any.whl", hash = "sha256:b19943914e6286548254f5079d1da094e9c0583ee91a8e611e9df24bfd07dbcd", size = 34185 }, +] + +[[package]] +name = "pydata-sphinx-theme" +version = "0.14.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accessible-pygments" }, + { name = "babel" }, + { name = "beautifulsoup4" }, + { name = "docutils" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "sphinx" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/47/1bc31c4bc8b395cd37d8ceaf720abe10cf64c857fb9ce55856a6dd958484/pydata_sphinx_theme-0.14.4.tar.gz", hash = "sha256:f5d7a2cb7a98e35b9b49d3b02cec373ad28958c2ed5c9b1ffe6aff6c56e9de5b", size = 2410500 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/bf/3f8dc653e3015fa0656587e101013754d9bf926f395cbe0892f7e87158dd/pydata_sphinx_theme-0.14.4-py3-none-any.whl", hash = "sha256:ac15201f4c2e2e7042b0cad8b30251433c1f92be762ddcefdb4ae68811d918d9", size = 4682140 }, +] + +[[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 = "pyjwt" +version = "2.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/68/ce067f09fca4abeca8771fe667d89cc347d1e99da3e093112ac329c6020e/pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c", size = 78825 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/84/0fdf9b18ba31d69877bd39c9cd6052b47f3761e9910c15de788e519f079f/PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850", size = 22344 }, +] + +[[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 }, + { url = "https://files.pythonhosted.org/packages/bd/b0/3b07394be7a9282981f3ec6e9918f8528d9dcff7dea523cd86a03cbddc76/pymongo-4.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c3f28afd783be3cebef1235a45340589169d7774cd9909ba0249e2f851ff511d", size = 726089 }, + { url = "https://files.pythonhosted.org/packages/0a/34/7054e272a48a11a8ae376b1ab3f61370d50b448eae520cb2036da39d490c/pymongo-4.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7a0b2e7fedc5911cd44590b5fd8e3714029f378f37f3c0c2043f67150b588d4a", size = 726398 }, + { url = "https://files.pythonhosted.org/packages/a3/6f/3b6b28d0202b942d0a1cc6217dbdd36c8a24cad036c58b06449672d31acf/pymongo-4.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5af264b9a973859123e3129d131d7246f57659304400e3e6b35ed6eaf099854d", size = 927238 }, + { url = "https://files.pythonhosted.org/packages/b3/52/57294161b7f42553228ef72742231869a635e550d7e7a344055d0a30e254/pymongo-4.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65c6b2e2a6db38f49433021dda0802ad081118224b2264500ef03a2d82ae26a7", size = 943454 }, + { url = "https://files.pythonhosted.org/packages/bc/61/900db838a8e993a912d8058c7396a3ece5ccfab3d6c063e3dbf174b94c93/pymongo-4.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:410ea165f2f819118eed764c5faa35fa71aeff5ce8b5046af99ed158a5661e9e", size = 936839 }, + { url = "https://files.pythonhosted.org/packages/46/db/bd9d4d8ed19de90c07b53d1405ad8a3f479d1df7f18bfe1e7a37a5933f2f/pymongo-4.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c3c71337d4c923f719cb56253af9244e90353a2454088ee4f184bfb0dd446a4", size = 928431 }, + { url = "https://files.pythonhosted.org/packages/32/c6/fe5e207fce0c1e7a8ef342dc5b38b32172e1ed9f2660ef1297687be0b2b0/pymongo-4.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:77528a2b928fe3f1f655cefa195e6718ab1ccd1a456aba486d76318e526a7fac", size = 917943 }, + { url = "https://files.pythonhosted.org/packages/76/c0/40b3915f09211693df897ce8fe555974729abb07427109f6d53c5070878b/pymongo-4.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fdbd558d90b55d7c39c096a79f8a725f1f02b658211924ab98dbc03ecad01095", size = 919094 }, + { url = "https://files.pythonhosted.org/packages/3a/a6/7ebd35e409b05a61864c938e8d73ee8fbf46b2facf0578086c07df74ab9a/pymongo-4.9.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e3ff4201ea707f57bf381f61df0e9cd6e896627a59f98a5d1c4a1bd14a2544cb", size = 926651 }, + { url = "https://files.pythonhosted.org/packages/92/cf/f727361f21ffa412573e98f8e55585945c7dab91aa3dbddcfa5f85177ec6/pymongo-4.9.2-cp38-cp38-win32.whl", hash = "sha256:ae227bba43e2e6fc8c3440a70b3b8f9ab2b0eb0906d0d2cf814dd9490c572e2a", size = 720644 }, + { url = "https://files.pythonhosted.org/packages/d6/43/78a57401e276f29d1468c4623113f92bd01b019de2c8315d7313325e5d37/pymongo-4.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:a92c96886048d3ebae62dbcfc775c7f2b965270160e3cb6aab4e06750e030b05", size = 725630 }, + { url = "https://files.pythonhosted.org/packages/e2/0c/8101588ad2da1f023c77597aba612176051f731bf417e557275edc4102b9/pymongo-4.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e54e2c6f1dec45c57a587b4c13c16666d5f7c031a642ae177140d1e0551a947e", size = 779913 }, + { url = "https://files.pythonhosted.org/packages/66/93/2d237514aad615b94a617cc7092cdee959d99a883d156c25df2a550b6310/pymongo-4.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a49d9292f22a0395c0fd2822a06e385910f1f902c3a9feafc1d0bfc27cd2df6b", size = 780185 }, + { url = "https://files.pythonhosted.org/packages/6d/c1/4300586e96af2652fe3592c1eaa70574989b4a21f2704938a857083cdcda/pymongo-4.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80a1ee9b72eebd96619ebe0beb718a5bcf2a70f464edf315f97b9315ed6854a9", size = 1165465 }, + { url = "https://files.pythonhosted.org/packages/04/ea/3f577b203ecad8bd59f5aef0822e7cbfd4c76fe6e05c83a4c87c405c603a/pymongo-4.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea9c47f86a322280381e9ddba7491e664ea80bf75df247ea2346faf7626e4e4c", size = 1198286 }, + { url = "https://files.pythonhosted.org/packages/84/af/29248c6eaeb1055d14c8d457624b963bcd9e30be8421dd4fc2ed2e8989fd/pymongo-4.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf963104dfd7235bebc44cef40b4b12c6638bb03b3a828cb495498e286b6edd0", size = 1183784 }, + { url = "https://files.pythonhosted.org/packages/56/06/ddff399f79d410efd832e6673c06d94ab7901c9698734bbb512d4d630272/pymongo-4.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f13330bdf4a57ef70bdd6282721547ec464f773203be47bac1efc4abd74a9190", size = 1168068 }, + { url = "https://files.pythonhosted.org/packages/cd/f0/b057d94a627f2a6468707a342ea6c788d60c729cf8d4a698333a13b70c8b/pymongo-4.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fb10d7069f1e7d7d6a458b1c5e9d1454be6eca2d9885bec25c1202e22c88d2a", size = 1147611 }, + { url = "https://files.pythonhosted.org/packages/9f/f7/22ef54abf9dd083fe64153c26575bf3aec966ba50ec76b5e00e2a2ee75b3/pymongo-4.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cd832de5df92caa68ee66c872708951d7e0c1f7b289b74189f2ccf1832c56dda", size = 1132542 }, + { url = "https://files.pythonhosted.org/packages/d7/59/059fa4d81fb4f8a1cf6f04dd03a9f78119d56fd05fd656761e89b6c8d4cb/pymongo-4.9.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3f55efe0f77198c055800e605268bfd77a3f0223d1a80b55b771d0c350bc3ade", size = 1165854 }, + { url = "https://files.pythonhosted.org/packages/fd/61/67a7ed51f0ce8eedc25c917dc582478caa9606d39e508441742bbcd8f674/pymongo-4.9.2-cp39-cp39-win32.whl", hash = "sha256:f2f43e5d6e739aa78c7053bdf351453c0e53d7667a3cac73255c2169631e052a", size = 765568 }, + { url = "https://files.pythonhosted.org/packages/e3/cf/8c7a0b3d4d44ecf62bd0590d4d6a7d4e268b77de7f01f7dd362576f667d1/pymongo-4.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:31c35d3dac5a1b0f65b3da2a19dc7fb88271c86329c75cfea775d5381ade6c06", size = 775323 }, +] + +[[package]] +name = "pyopenssl" +version = "24.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/d4/1067b82c4fc674d6f6e9e8d26b3dff978da46d351ca3bac171544693e085/pyopenssl-24.3.0.tar.gz", hash = "sha256:49f7a019577d834746bc55c5fce6ecbcec0f2b4ec5ce1cf43a9a173b8138bb36", size = 178944 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/22/40f9162e943f86f0fc927ebc648078be87def360d9d8db346619fb97df2b/pyOpenSSL-24.3.0-py3-none-any.whl", hash = "sha256:e474f5a473cd7f92221cc04976e48f4d11502804657a08a989fb3be5514c904a", size = 56111 }, +] + +[[package]] +name = "pyright" +version = "1.1.344" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/9e/e8e54e9dd3f1e63e8573d9a3830be06e904629bc15a90ca052532b2656f4/pyright-1.1.344.tar.gz", hash = "sha256:ab7c962f00dd8141a5a0192c1060fb34b92d1f9047ad70dda45229938051922b", size = 17486 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/37/a118d8cc0381363c1a7f511a9f640c60df13329d35b4ee42c79dd5c87736/pyright-1.1.344-py3-none-any.whl", hash = "sha256:ab7117a911ce25fcd317f42272579f9ae53a6abc8b8a15f6aa069a11281953ee", size = 18225 }, +] + +[[package]] +name = "pytest" +version = "7.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287 }, +] + +[[package]] +name = "pytest-asyncio" +version = "0.23.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/b4/0b378b7bf26a8ae161c3890c0b48a91a04106c5713ce81b4b080ea2f4f18/pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3", size = 46920 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/82/62e2d63639ecb0fbe8a7ee59ef0bc69a4669ec50f6d3459f74ad4e4189a2/pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2", size = 17663 }, +] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990 }, +] + +[[package]] +name = "pytest-lazy-fixtures" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/15/a33df3d8d0f44bde7382c9201a06fa277d8442d783ab46874b616aae9239/pytest_lazy_fixtures-1.1.1.tar.gz", hash = "sha256:0c561f0d29eea5b55cf29b9264a3241999ffdb74c6b6e8c4ccc0bd2c934d01ed", size = 6978 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/3a/9354c3765bc1459fc18f6d5cf62709b3a606de55bd02d0483ceefd7fd98f/pytest_lazy_fixtures-1.1.1-py3-none-any.whl", hash = "sha256:a4b396a361faf56c6305535fd0175ce82902ca7cf668c4d812a25ed2bcde8183", size = 6928 }, +] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, +] + +[[package]] +name = "pytest-rerunfailures" +version = "14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/a4/6de45fe850759e94aa9a55cda807c76245af1941047294df26c851dfb4a9/pytest-rerunfailures-14.0.tar.gz", hash = "sha256:4a400bcbcd3c7a4ad151ab8afac123d90eca3abe27f98725dc4d9702887d2e92", size = 21350 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/e7/e75bd157331aecc190f5f8950d7ea3d2cf56c3c57fb44da70e60b221133f/pytest_rerunfailures-14.0-py3-none-any.whl", hash = "sha256:4197bdd2eaeffdbf50b5ea6e7236f47ff0e44d1def8dae08e409f536d84e7b32", size = 12709 }, +] + +[[package]] +name = "pytest-timeout" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/0d/04719abc7a4bdb3a7a1f968f24b0f5253d698c9cc94975330e9d3145befb/pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9", size = 17697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/27/14af9ef8321f5edc7527e47def2a21d8118c6f329a9342cc61387a0c0599/pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e", size = 14148 }, +] + +[[package]] +name = "pytest-xdist" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/c4/3c310a19bc1f1e9ef50075582652673ef2bfc8cd62afef9585683821902f/pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d", size = 84060 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/82/1d96bf03ee4c0fdc3c0cbe61470070e659ca78dc0086fb88b66c185e2449/pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7", size = 46108 }, +] + +[[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 = "pytz" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, +] + +[[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 }, + { url = "https://files.pythonhosted.org/packages/74/d9/323a59d506f12f498c2097488d80d16f4cf965cee1791eab58b56b19f47a/PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", size = 183218 }, + { url = "https://files.pythonhosted.org/packages/74/cc/20c34d00f04d785f2028737e2e2a8254e1425102e730fee1d6396f832577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", size = 728067 }, + { url = "https://files.pythonhosted.org/packages/20/52/551c69ca1501d21c0de51ddafa8c23a0191ef296ff098e98358f69080577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", size = 757812 }, + { url = "https://files.pythonhosted.org/packages/fd/7f/2c3697bba5d4aa5cc2afe81826d73dfae5f049458e44732c7a0938baa673/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", size = 746531 }, + { url = "https://files.pythonhosted.org/packages/8c/ab/6226d3df99900e580091bb44258fde77a8433511a86883bd4681ea19a858/PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", size = 800820 }, + { url = "https://files.pythonhosted.org/packages/a0/99/a9eb0f3e710c06c5d922026f6736e920d431812ace24aae38228d0d64b04/PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", size = 145514 }, + { url = "https://files.pythonhosted.org/packages/75/8a/ee831ad5fafa4431099aa4e078d4c8efd43cd5e48fbc774641d233b683a9/PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", size = 162702 }, + { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 }, + { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 }, + { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 }, + { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 }, + { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 }, + { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 }, + { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 }, + { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 }, + { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 }, +] + +[[package]] +name = "redis" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/17/2f4a87ffa4cd93714cf52edfa3ea94589e9de65f71e9f99cbcfa84347a53/redis-5.2.0.tar.gz", hash = "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0", size = 4607878 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/f5/ffa560ecc4bafbf25f7961c3d6f50d627a90186352e27e7d0ba5b1f6d87d/redis-5.2.0-py3-none-any.whl", hash = "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897", size = 261428 }, +] + +[package.optional-dependencies] +hiredis = [ + { name = "hiredis" }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +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'" }, +] +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.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/f4/e48dc2850662526a26fb0961aacb0162c6feab934312b109b748ae4efee2/rich_click-1.8.4.tar.gz", hash = "sha256:0f49471f04439269d0e66a6f43120f52d11d594869a2a0be600cfb12eb0616b9", size = 38247 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/f3/72f93d8494ee641bde76bfe1208cf4abc44c6f9448673762f6077bc162d6/rich_click-1.8.4-py3-none-any.whl", hash = "sha256:2d2841b3cebe610d5682baa1194beaf78ab00c4fa31931533261b5eba2ee80b7", size = 35071 }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.18.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ruamel-yaml-clib", marker = "python_full_version < '3.13' and platform_python_implementation == 'CPython'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/81/4dfc17eb6ebb1aac314a3eb863c1325b907863a1b8b1382cdffcb6ac0ed9/ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b", size = 143362 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/67/8ece580cc363331d9a53055130f86b096bf16e38156e33b1d3014fffda6b/ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636", size = 117761 }, +] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/ab/bab9eb1566cd16f060b54055dd39cf6a34bfa0240c53a7218c43e974295b/ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512", size = 213824 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/01/37ac131614f71b98e9b148b2d7790662dcee92217d2fb4bac1aa377def33/ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d", size = 148236 }, + { url = "https://files.pythonhosted.org/packages/61/ee/4874c9fc96010fce85abefdcbe770650c5324288e988d7a48b527a423815/ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462", size = 133996 }, + { url = "https://files.pythonhosted.org/packages/d3/62/c60b034d9a008bbd566eeecf53a5a4c73d191c8de261290db6761802b72d/ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412", size = 526680 }, + { url = "https://files.pythonhosted.org/packages/90/8c/6cdb44f548b29eb6328b9e7e175696336bc856de2ff82e5776f860f03822/ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f", size = 605853 }, + { url = "https://files.pythonhosted.org/packages/88/30/fc45b45d5eaf2ff36cffd215a2f85e9b90ac04e70b97fd4097017abfb567/ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334", size = 655206 }, + { url = "https://files.pythonhosted.org/packages/af/dc/133547f90f744a0c827bac5411d84d4e81da640deb3af1459e38c5f3b6a0/ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d", size = 689649 }, + { url = "https://files.pythonhosted.org/packages/23/1d/589139191b187a3c750ae8d983c42fd799246d5f0dd84451a0575c9bdbe9/ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d", size = 100044 }, + { url = "https://files.pythonhosted.org/packages/4f/5b/744df20285a75ac4c606452ce9a0fcc42087d122f42294518ded1017697c/ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31", size = 117825 }, + { url = "https://files.pythonhosted.org/packages/b1/15/971b385c098e8d0d170893f5ba558452bb7b776a0c90658b8f4dd0e3382b/ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069", size = 148870 }, + { url = "https://files.pythonhosted.org/packages/01/b0/4ddef56e9f703d7909febc3a421d709a3482cda25826816ec595b73e3847/ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248", size = 134475 }, + { url = "https://files.pythonhosted.org/packages/a4/f7/22d6b620ed895a05d40802d8281eff924dc6190f682d933d4efff60db3b5/ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b", size = 544020 }, + { url = "https://files.pythonhosted.org/packages/7c/e4/0d19d65e340f93df1c47f323d95fa4b256bb28320290f5fddef90837853a/ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe", size = 642643 }, + { url = "https://files.pythonhosted.org/packages/c9/ff/f781eb5e2ae011e586d5426e2086a011cf1e0f59704a6cad1387975c5a62/ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899", size = 695832 }, + { url = "https://files.pythonhosted.org/packages/e3/41/f62e67ac651358b8f0d60cfb12ab2daf99b1b69eeaa188d0cec809d943a6/ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9", size = 730923 }, + { url = "https://files.pythonhosted.org/packages/9f/f0/19ab8acbf983cd1b37f47d27ceb8b10a738d60d36316a54bad57e0d73fbb/ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7", size = 99999 }, + { url = "https://files.pythonhosted.org/packages/ec/54/d8a795997921d87224c65d44499ca595a833093fb215b133f920c1062956/ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb", size = 118008 }, + { url = "https://files.pythonhosted.org/packages/7a/a2/eb5e9d088cb9d15c24d956944c09dca0a89108ad6e2e913c099ef36e3f0d/ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1", size = 144636 }, + { url = "https://files.pythonhosted.org/packages/66/98/8de4f22bbfd9135deb3422e96d450c4bc0a57d38c25976119307d2efe0aa/ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2", size = 135684 }, + { url = "https://files.pythonhosted.org/packages/30/d3/5fe978cd01a61c12efd24d65fa68c6f28f28c8073a06cf11db3a854390ca/ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92", size = 734571 }, + { url = "https://files.pythonhosted.org/packages/55/b3/e2531a050758b717c969cbf76c103b75d8a01e11af931b94ba656117fbe9/ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62", size = 643946 }, + { url = "https://files.pythonhosted.org/packages/0d/aa/06db7ca0995b513538402e11280282c615b5ae5f09eb820460d35fb69715/ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9", size = 692169 }, + { url = "https://files.pythonhosted.org/packages/27/38/4cf4d482b84ecdf51efae6635cc5483a83cf5ca9d9c13e205a750e251696/ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d", size = 740325 }, + { url = "https://files.pythonhosted.org/packages/6f/67/c62c6eea53a4feb042727a3d6c18f50dc99683c2b199c06bd2a9e3db8e22/ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa", size = 98639 }, + { url = "https://files.pythonhosted.org/packages/10/d2/52a3d810d0b5b3720725c0504a27b3fced7b6f310fe928f7019d79387bc1/ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b", size = 115305 }, + { url = "https://files.pythonhosted.org/packages/18/52/8dc27bbd9ef1d4695975b8dc132c27c431d0186037ad3c731a6dd1c154b9/ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615", size = 146177 }, + { url = "https://files.pythonhosted.org/packages/08/4c/5770b8f318fe404a455141a7a33a5568c27a1f944724e82354c8f3554db2/ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337", size = 133289 }, + { url = "https://files.pythonhosted.org/packages/5a/45/644d839c09c0717c2d7f26b705560ad74b3056085b3bc7f9c2ac2081317b/ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1", size = 641518 }, + { url = "https://files.pythonhosted.org/packages/22/fa/b2a8fd49c92693e9b9b6b11eef4c2a8aedaca2b521ab3e020aa4778efc23/ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91", size = 596029 }, + { url = "https://files.pythonhosted.org/packages/5c/f0/702e56e12497da7960ed8a6972e5edc50545757c40f1a86a41a5217da7e9/ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28", size = 724558 }, + { url = "https://files.pythonhosted.org/packages/87/a6/efb1add3bac06c25aa4c8ff8c6d3e5e91c539f6600832dd63ff98e2b44cc/ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d", size = 767665 }, + { url = "https://files.pythonhosted.org/packages/1d/fe/a638c3ad6e74f4b15c8c1aa7de61a0cfe58c629d48ea59cf07dce5eaee1e/ruamel.yaml.clib-0.2.8-cp38-cp38-win32.whl", hash = "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe", size = 100578 }, + { url = "https://files.pythonhosted.org/packages/24/ce/6f587283caaff93d0b9cac2f244fcda686897e83401bb1aa91803db7bf94/ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312", size = 118511 }, + { url = "https://files.pythonhosted.org/packages/56/a9/e3be88fcebe04016c57207260f2b07c5ecacab86e9f585d10daaa2a4074f/ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001", size = 148719 }, + { url = "https://files.pythonhosted.org/packages/b2/ed/f221e60a4cdc7996aae23643da44b12ef33f457c2a52d590236a6950ac8e/ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf", size = 134394 }, + { url = "https://files.pythonhosted.org/packages/57/e4/f572d7e2502854f15291dfa94eebdc687e04db387559f026995c7697af34/ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c", size = 608071 }, + { url = "https://files.pythonhosted.org/packages/7c/b2/389b345a60131593028b0263fddaa580edb4081697a3f3aa1f168f67519f/ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5", size = 562085 }, + { url = "https://files.pythonhosted.org/packages/8d/c0/fd7196ca7a1c3867e7068ad1c4ff9230291af3f8adab2f9c2c202ecaf9cb/ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b", size = 658185 }, + { url = "https://files.pythonhosted.org/packages/54/61/c18d378caadac66fa97da5d28758c751730dac7510b6a8b8b096da3fff9a/ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880", size = 692222 }, + { url = "https://files.pythonhosted.org/packages/68/4c/f55fbf8510d087449b21b4cde4c05726c8dda5f9b25a8fad06d0c4319249/ruamel.yaml.clib-0.2.8-cp39-cp39-win32.whl", hash = "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5", size = 100563 }, + { url = "https://files.pythonhosted.org/packages/74/82/e9bb3a3a2268987b7bc472c5c26b420757e04db0d0408e6626d07e388e4c/ruamel.yaml.clib-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15", size = 118400 }, +] + +[[package]] +name = "ruff" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/d0/8ff5b189d125f4260f2255d143bf2fa413b69c2610c405ace7a0a8ec81ec/ruff-0.8.1.tar.gz", hash = "sha256:3583db9a6450364ed5ca3f3b4225958b24f78178908d5c4bc0f46251ccca898f", size = 3313222 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/d6/1a6314e568db88acdbb5121ed53e2c52cebf3720d3437a76f82f923bf171/ruff-0.8.1-py3-none-linux_armv6l.whl", hash = "sha256:fae0805bd514066f20309f6742f6ee7904a773eb9e6c17c45d6b1600ca65c9b5", size = 10532605 }, + { url = "https://files.pythonhosted.org/packages/89/a8/a957a8812e31facffb6a26a30be0b5b4af000a6e30c7d43a22a5232a3398/ruff-0.8.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8a4f7385c2285c30f34b200ca5511fcc865f17578383db154e098150ce0a087", size = 10278243 }, + { url = "https://files.pythonhosted.org/packages/a8/23/9db40fa19c453fabf94f7a35c61c58f20e8200b4734a20839515a19da790/ruff-0.8.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd054486da0c53e41e0086e1730eb77d1f698154f910e0cd9e0d64274979a209", size = 9917739 }, + { url = "https://files.pythonhosted.org/packages/e2/a0/6ee2d949835d5701d832fc5acd05c0bfdad5e89cfdd074a171411f5ccad5/ruff-0.8.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2029b8c22da147c50ae577e621a5bfbc5d1fed75d86af53643d7a7aee1d23871", size = 10779153 }, + { url = "https://files.pythonhosted.org/packages/7a/25/9c11dca9404ef1eb24833f780146236131a3c7941de394bc356912ef1041/ruff-0.8.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2666520828dee7dfc7e47ee4ea0d928f40de72056d929a7c5292d95071d881d1", size = 10304387 }, + { url = "https://files.pythonhosted.org/packages/c8/b9/84c323780db1b06feae603a707d82dbbd85955c8c917738571c65d7d5aff/ruff-0.8.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:333c57013ef8c97a53892aa56042831c372e0bb1785ab7026187b7abd0135ad5", size = 11360351 }, + { url = "https://files.pythonhosted.org/packages/6b/e1/9d4bbb2ace7aad14ded20e4674a48cda5b902aed7a1b14e6b028067060c4/ruff-0.8.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:288326162804f34088ac007139488dcb43de590a5ccfec3166396530b58fb89d", size = 12022879 }, + { url = "https://files.pythonhosted.org/packages/75/28/752ff6120c0e7f9981bc4bc275d540c7f36db1379ba9db9142f69c88db21/ruff-0.8.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b12c39b9448632284561cbf4191aa1b005882acbc81900ffa9f9f471c8ff7e26", size = 11610354 }, + { url = "https://files.pythonhosted.org/packages/ba/8c/967b61c2cc8ebd1df877607fbe462bc1e1220b4a30ae3352648aec8c24bd/ruff-0.8.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:364e6674450cbac8e998f7b30639040c99d81dfb5bbc6dfad69bc7a8f916b3d1", size = 12813976 }, + { url = "https://files.pythonhosted.org/packages/7f/29/e059f945d6bd2d90213387b8c360187f2fefc989ddcee6bbf3c241329b92/ruff-0.8.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b22346f845fec132aa39cd29acb94451d030c10874408dbf776af3aaeb53284c", size = 11154564 }, + { url = "https://files.pythonhosted.org/packages/55/47/cbd05e5a62f3fb4c072bc65c1e8fd709924cad1c7ec60a1000d1e4ee8307/ruff-0.8.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b2f2f7a7e7648a2bfe6ead4e0a16745db956da0e3a231ad443d2a66a105c04fa", size = 10760604 }, + { url = "https://files.pythonhosted.org/packages/bb/ee/4c3981c47147c72647a198a94202633130cfda0fc95cd863a553b6f65c6a/ruff-0.8.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:adf314fc458374c25c5c4a4a9270c3e8a6a807b1bec018cfa2813d6546215540", size = 10391071 }, + { url = "https://files.pythonhosted.org/packages/6b/e6/083eb61300214590b188616a8ac6ae1ef5730a0974240fb4bec9c17de78b/ruff-0.8.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a885d68342a231b5ba4d30b8c6e1b1ee3a65cf37e3d29b3c74069cdf1ee1e3c9", size = 10896657 }, + { url = "https://files.pythonhosted.org/packages/77/bd/aacdb8285d10f1b943dbeb818968efca35459afc29f66ae3bd4596fbf954/ruff-0.8.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d2c16e3508c8cc73e96aa5127d0df8913d2290098f776416a4b157657bee44c5", size = 11228362 }, + { url = "https://files.pythonhosted.org/packages/39/72/fcb7ad41947f38b4eaa702aca0a361af0e9c2bf671d7fd964480670c297e/ruff-0.8.1-py3-none-win32.whl", hash = "sha256:93335cd7c0eaedb44882d75a7acb7df4b77cd7cd0d2255c93b28791716e81790", size = 8803476 }, + { url = "https://files.pythonhosted.org/packages/e4/ea/cae9aeb0f4822c44651c8407baacdb2e5b4dcd7b31a84e1c5df33aa2cc20/ruff-0.8.1-py3-none-win_amd64.whl", hash = "sha256:2954cdbe8dfd8ab359d4a30cd971b589d335a44d444b6ca2cb3d1da21b75e4b6", size = 9614463 }, + { url = "https://files.pythonhosted.org/packages/eb/76/fbb4bd23dfb48fa7758d35b744413b650a9fd2ddd93bca77e30376864414/ruff-0.8.1-py3-none-win_arm64.whl", hash = "sha256:55873cc1a473e5ac129d15eccb3c008c096b94809d693fc7053f588b67822737", size = 8959621 }, +] + +[[package]] +name = "service-identity" +version = "24.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cryptography" }, + { name = "pyasn1" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/a5/dfc752b979067947261dbbf2543470c58efe735c3c1301dd870ef27830ee/service_identity-24.2.0.tar.gz", hash = "sha256:b8683ba13f0d39c6cd5d625d2c5f65421d6d707b013b375c355751557cbe8e09", size = 39245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/2c/ca6dd598b384bc1ce581e24aaae0f2bed4ccac57749d5c3befbb5e742081/service_identity-24.2.0-py3-none-any.whl", hash = "sha256:6b047fbd8a84fd0bb0d55ebce4031e400562b9196e1e0d3e0fe2b8a59f6d4a85", size = 11364 }, +] + +[[package]] +name = "setuptools" +version = "75.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/22/a438e0caa4576f8c383fa4d35f1cc01655a46c75be358960d815bfbb12bd/setuptools-75.3.0.tar.gz", hash = "sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686", size = 1351577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/12/282ee9bce8b58130cb762fbc9beabd531549952cac11fc56add11dcb7ea0/setuptools-75.3.0-py3-none-any.whl", hash = "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd", size = 1251070 }, +] + +[[package]] +name = "six" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, +] + +[[package]] +name = "slotscheck" +version = "0.16.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/d6/d94545d83dd2fd8d515c76461875d23ecf80b998d8c34963a2e850ef80ca/slotscheck-0.16.5.tar.gz", hash = "sha256:6cae3e73808121cf63c1bc638c3b5ae7e10f651323ad3cf38790ce005b77e221", size = 17073 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/49/826b5c7df3a908c79588f7bff560c93a1bd79b04034698433803f4d17400/slotscheck-0.16.5-py3-none-any.whl", hash = "sha256:b202def7a1d4559575a6a1926aabe461bf780c1584275eff2d3ee4465c52d8c6", size = 20273 }, +] + +[[package]] +name = "smart-open" +version = "7.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/d8/1481294b2d110b805c0f5d23ef34158b7d5d4283633c0d34c69ea89bb76b/smart_open-7.0.5.tar.gz", hash = "sha256:d3672003b1dbc85e2013e4983b88eb9a5ccfd389b0d4e5015f39a9ee5620ec18", size = 71693 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/bc/706838af28a542458bffe74a5d0772ca7f207b5495cd9fccfce61ef71f2a/smart_open-7.0.5-py3-none-any.whl", hash = "sha256:8523ed805c12dff3eaa50e9c903a6cb0ae78800626631c5fe7ea073439847b89", size = 61387 }, +] + +[[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 = "snowballstemmer" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 }, +] + +[[package]] +name = "soupsieve" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 }, +] + +[[package]] +name = "sphinx" +version = "7.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/01/688bdf9282241dca09fe6e3a1110eda399fa9b10d0672db609e37c2e7a39/sphinx-7.1.2.tar.gz", hash = "sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f", size = 6828258 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/17/325cf6a257d84751a48ae90752b3d8fe0be8f9535b6253add61c49d0d9bc/sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe", size = 3169543 }, +] + +[[package]] +name = "sphinx-autobuild" +version = "2021.3.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "livereload" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/a5/2ed1b81e398bc14533743be41bf0ceaa49d671675f131c4d9ce74897c9c1/sphinx-autobuild-2021.3.14.tar.gz", hash = "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05", size = 206402 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/7d/8fb7557b6c9298d2bcda57f4d070de443c6355dfb475582378e2aa16a02c/sphinx_autobuild-2021.3.14-py3-none-any.whl", hash = "sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac", size = 9881 }, +] + +[[package]] +name = "sphinx-autodoc-typehints" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/f0/b750f1ea593df9ba152e99929807530604d06fae887e5a38ae1e0a31358a/sphinx_autodoc_typehints-2.0.1.tar.gz", hash = "sha256:60ed1e3b2c970acc0aa6e877be42d48029a9faec7378a17838716cacd8c10b12", size = 38816 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/95/5baffb0ef1b8fd72d0a5a3ab531e82c5e810df3530c8f61857c69026b7ac/sphinx_autodoc_typehints-2.0.1-py3-none-any.whl", hash = "sha256:f73ae89b43a799e587e39266672c1075b2ef783aeb382d3ebed77c38a3fc0149", size = 19533 }, +] + +[[package]] +name = "sphinx-click" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "docutils" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/0a/5b1e8d0579dbb4ca8114e456ca4a68020bfe8e15c7001f3856be4929ab83/sphinx_click-6.0.0.tar.gz", hash = "sha256:f5d664321dc0c6622ff019f1e1c84e58ce0cecfddeb510e004cf60c2a3ab465b", size = 29574 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/d7/8621c4726ad3f788a1db4c0c409044b16edc563f5c9542807b3724037555/sphinx_click-6.0.0-py3-none-any.whl", hash = "sha256:1e0a3c83bcb7c55497751b19d07ebe56b5d7b85eb76dd399cf9061b497adc317", size = 9922 }, +] + +[[package]] +name = "sphinx-copybutton" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/2b/a964715e7f5295f77509e59309959f4125122d648f86b4fe7d70ca1d882c/sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd", size = 23039 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343 }, +] + +[[package]] +name = "sphinx-design" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/d0/62a7cee178d30f7217c4badea17eeca020801c0053773098d9ff65636a60/sphinx_design-0.5.0.tar.gz", hash = "sha256:e8e513acea6f92d15c6de3b34e954458f245b8e761b45b63950f65373352ab00", size = 2152330 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/52/a1e9d72ecf56047df714a3dd0840a5148e4e83c100e8e0046bcea60a1054/sphinx_design-0.5.0-py3-none-any.whl", hash = "sha256:1af1267b4cea2eedd6724614f19dcc88fe2e15aff65d06b2f6252cee9c4f4c1e", size = 2173865 }, +] + +[[package]] +name = "sphinx-jinja2-compat" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "standard-imghdr", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/df/27282da6f8c549f765beca9de1a5fc56f9651ed87711a5cac1e914137753/sphinx_jinja2_compat-0.3.0.tar.gz", hash = "sha256:f3c1590b275f42e7a654e081db5e3e5fb97f515608422bde94015ddf795dfe7c", size = 4998 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/42/2fd09d672eaaa937d6893d8b747d07943f97a6e5e30653aee6ebd339b704/sphinx_jinja2_compat-0.3.0-py3-none-any.whl", hash = "sha256:b1e4006d8e1ea31013fa9946d1b075b0c8d2a42c6e3425e63542c1e9f8be9084", size = 7883 }, +] + +[[package]] +name = "sphinx-paramlinks" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/21/62d3a58ff7bd02bbb9245a63d1f0d2e0455522a11a78951d16088569fca8/sphinx-paramlinks-0.6.0.tar.gz", hash = "sha256:746a0816860aa3fff5d8d746efcbec4deead421f152687411db1d613d29f915e", size = 12363 } + +[[package]] +name = "sphinx-prompt" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "pygments" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/00/c601097bfa180c0622bef4b3a8cbb91c10281b4fec2fa1cac835fa74001c/sphinx_prompt-1.7.0.tar.gz", hash = "sha256:f95c0b44d73621fc0b493f84b0c2866eb8741140ef0260c20a0f7578457f44ad", size = 4989 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/94/89f33756b82dbe9453cd05176c43a43e7f2337816f48c8a234396f027493/sphinx_prompt-1.7.0-py3-none-any.whl", hash = "sha256:7ee415d07f90f7ce1577a2c4c7f2560694af008926a69b4c940f20737621b089", size = 5224 }, +] + +[[package]] +name = "sphinx-tabs" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "pygments" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/32/ab475e252dc2b704e82a91141fa404cdd8901a5cf34958fd22afacebfccd/sphinx-tabs-3.4.5.tar.gz", hash = "sha256:ba9d0c1e3e37aaadd4b5678449eb08176770e0fc227e769b6ce747df3ceea531", size = 16070 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/9f/4ac7dbb9f23a2ff5a10903a4f9e9f43e0ff051f63a313e989c962526e305/sphinx_tabs-3.4.5-py3-none-any.whl", hash = "sha256:92cc9473e2ecf1828ca3f6617d0efc0aa8acb06b08c56ba29d1413f2f0f6cf09", size = 9904 }, +] + +[[package]] +name = "sphinx-toolbox" +version = "3.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apeye" }, + { name = "autodocsumm" }, + { name = "beautifulsoup4" }, + { name = "cachecontrol", extra = ["filecache"] }, + { name = "dict2css" }, + { name = "docutils" }, + { name = "domdf-python-tools" }, + { name = "filelock" }, + { name = "html5lib" }, + { name = "ruamel-yaml" }, + { name = "sphinx" }, + { name = "sphinx-autodoc-typehints" }, + { name = "sphinx-jinja2-compat" }, + { name = "sphinx-prompt" }, + { name = "sphinx-tabs" }, + { name = "tabulate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/80/f837e85c8c216cdeef9b60393e4b00c9092a1e3d734106e0021abbf5930c/sphinx_toolbox-3.8.1.tar.gz", hash = "sha256:a4b39a6ea24fc8f10e24f052199bda17837a0bf4c54163a56f521552395f5e1a", size = 111977 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/d6/2a28ee4cbc158ae65afb2cfcb6895ef54d972ce1e167f8a63c135b14b080/sphinx_toolbox-3.8.1-py3-none-any.whl", hash = "sha256:53d8e77dd79e807d9ef18590c4b2960a5aa3c147415054b04c31a91afed8b88b", size = 194621 }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/32/df/45e827f4d7e7fcc84e853bcef1d836effd762d63ccb86f43ede4e98b478c/sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e", size = 24766 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/c1/5e2cafbd03105ce50d8500f9b4e8a6e8d02e22d0475b574c3b3e9451a15f/sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228", size = 120601 }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/33/dc28393f16385f722c893cb55539c641c9aaec8d1bc1c15b69ce0ac2dbb3/sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4", size = 17398 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/09/5de5ed43a521387f18bdf5f5af31d099605c992fd25372b2b9b825ce48ee/sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", size = 84690 }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/47/64cff68ea3aa450c373301e5bebfbb9fce0a3e70aca245fcadd4af06cd75/sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff", size = 27967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/ee/a1f5e39046cbb5f8bc8fba87d1ddf1c6643fbc9194e58d26e606de4b9074/sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903", size = 99833 }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071 }, +] + +[[package]] +name = "sphinxcontrib-mermaid" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/69/bf039237ad260073e8c02f820b3e00dc34f3a2de20aff7861e6b19d2f8c5/sphinxcontrib_mermaid-1.0.0.tar.gz", hash = "sha256:2e8ab67d3e1e2816663f9347d026a8dee4a858acdd4ad32dd1c808893db88146", size = 15153 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/c8/784b9ac6ea08aa594c1a4becbd0dbe77186785362e31fd633b8c6ae0197a/sphinxcontrib_mermaid-1.0.0-py3-none-any.whl", hash = "sha256:60b72710ea02087f212028feb09711225fbc2e343a10d34822fe787510e1caa3", size = 9597 }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/8e/c4846e59f38a5f2b4a0e3b27af38f2fcf904d4bfd82095bf92de0b114ebd/sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", size = 21658 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/14/05f9206cf4e9cfca1afb5fd224c7cd434dcc3a433d6d9e4e0264d29c6cdb/sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6", size = 90609 }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/72/835d6fadb9e5d02304cf39b18f93d227cd93abd3c41ebf58e6853eeb1455/sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952", size = 21019 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/77/5464ec50dd0f1c1037e3c93249b040c8fc8078fdda97530eeb02424b6eea/sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd", size = 94021 }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.36" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'WIN32') or (python_full_version < '3.13' and platform_machine == 'aarch64') or (python_full_version < '3.13' and platform_machine == 'amd64') or (python_full_version < '3.13' and platform_machine == 'ppc64le') or (python_full_version < '3.13' and platform_machine == 'win32') or (python_full_version < '3.13' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/65/9cbc9c4c3287bed2499e05033e207473504dc4df999ce49385fb1f8b058a/sqlalchemy-2.0.36.tar.gz", hash = "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5", size = 9574485 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/72/14ab694b8b3f0e35ef5beb74a8fea2811aa791ba1611c44dc90cdf46af17/SQLAlchemy-2.0.36-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59b8f3adb3971929a3e660337f5dacc5942c2cdb760afcabb2614ffbda9f9f72", size = 2092604 }, + { url = "https://files.pythonhosted.org/packages/1e/59/333fcbca58b79f5b8b61853d6137530198823392151fa8fd9425f367519e/SQLAlchemy-2.0.36-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37350015056a553e442ff672c2d20e6f4b6d0b2495691fa239d8aa18bb3bc908", size = 2083796 }, + { url = "https://files.pythonhosted.org/packages/6c/a0/ec3c188d2b0c1bc742262e76408d44104598d7247c23f5b06bb97ee21bfa/SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8318f4776c85abc3f40ab185e388bee7a6ea99e7fa3a30686580b209eaa35c08", size = 3066165 }, + { url = "https://files.pythonhosted.org/packages/07/15/68ef91de5b8b7f80fb2d2b3b31ed42180c6227fe0a701aed9d01d34f98ec/SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c245b1fbade9c35e5bd3b64270ab49ce990369018289ecfde3f9c318411aaa07", size = 3074428 }, + { url = "https://files.pythonhosted.org/packages/e2/4c/9dfea5e63b87325eef6d9cdaac913459aa6a157a05a05ea6ff20004aee8e/SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:69f93723edbca7342624d09f6704e7126b152eaed3cdbb634cb657a54332a3c5", size = 3030477 }, + { url = "https://files.pythonhosted.org/packages/16/a5/fcfde8e74ea5f683b24add22463bfc21e431d4a5531c8a5b55bc6fbea164/SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f9511d8dd4a6e9271d07d150fb2f81874a3c8c95e11ff9af3a2dfc35fe42ee44", size = 3055942 }, + { url = "https://files.pythonhosted.org/packages/3c/ee/c22c415a771d791ae99146d72ffdb20e43625acd24835ea7fc157436d59f/SQLAlchemy-2.0.36-cp310-cp310-win32.whl", hash = "sha256:c3f3631693003d8e585d4200730616b78fafd5a01ef8b698f6967da5c605b3fa", size = 2064960 }, + { url = "https://files.pythonhosted.org/packages/aa/af/ad9c25cadc79bd851bdb9d82b68af9bdb91ff05f56d0da2f8a654825974f/SQLAlchemy-2.0.36-cp310-cp310-win_amd64.whl", hash = "sha256:a86bfab2ef46d63300c0f06936bd6e6c0105faa11d509083ba8f2f9d237fb5b5", size = 2089078 }, + { url = "https://files.pythonhosted.org/packages/00/4e/5a67963fd7cbc1beb8bd2152e907419f4c940ef04600b10151a751fe9e06/SQLAlchemy-2.0.36-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fd3a55deef00f689ce931d4d1b23fa9f04c880a48ee97af488fd215cf24e2a6c", size = 2093782 }, + { url = "https://files.pythonhosted.org/packages/b3/24/30e33b6389ebb5a17df2a4243b091bc709fb3dfc9a48c8d72f8e037c943d/SQLAlchemy-2.0.36-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f5e9cd989b45b73bd359f693b935364f7e1f79486e29015813c338450aa5a71", size = 2084180 }, + { url = "https://files.pythonhosted.org/packages/10/1e/70e9ed2143a27065246be40f78637ad5160ea0f5fd32f8cab819a31ff54d/SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ddd9db6e59c44875211bc4c7953a9f6638b937b0a88ae6d09eb46cced54eff", size = 3202469 }, + { url = "https://files.pythonhosted.org/packages/b4/5f/95e0ed74093ac3c0db6acfa944d4d8ac6284ef5e1136b878a327ea1f975a/SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2519f3a5d0517fc159afab1015e54bb81b4406c278749779be57a569d8d1bb0d", size = 3202464 }, + { url = "https://files.pythonhosted.org/packages/91/95/2cf9b85a6bc2ee660e40594dffe04e777e7b8617fd0c6d77a0f782ea96c9/SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59b1ee96617135f6e1d6f275bbe988f419c5178016f3d41d3c0abb0c819f75bb", size = 3139508 }, + { url = "https://files.pythonhosted.org/packages/92/ea/f0c01bc646456e4345c0fb5a3ddef457326285c2dc60435b0eb96b61bf31/SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:39769a115f730d683b0eb7b694db9789267bcd027326cccc3125e862eb03bfd8", size = 3159837 }, + { url = "https://files.pythonhosted.org/packages/a6/93/c8edbf153ee38fe529773240877bf1332ed95328aceef6254288f446994e/SQLAlchemy-2.0.36-cp311-cp311-win32.whl", hash = "sha256:66bffbad8d6271bb1cc2f9a4ea4f86f80fe5e2e3e501a5ae2a3dc6a76e604e6f", size = 2064529 }, + { url = "https://files.pythonhosted.org/packages/b1/03/d12b7c1d36fd80150c1d52e121614cf9377dac99e5497af8d8f5b2a8db64/SQLAlchemy-2.0.36-cp311-cp311-win_amd64.whl", hash = "sha256:23623166bfefe1487d81b698c423f8678e80df8b54614c2bf4b4cfcd7c711959", size = 2089874 }, + { url = "https://files.pythonhosted.org/packages/b8/bf/005dc47f0e57556e14512d5542f3f183b94fde46e15ff1588ec58ca89555/SQLAlchemy-2.0.36-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4", size = 2092378 }, + { url = "https://files.pythonhosted.org/packages/94/65/f109d5720779a08e6e324ec89a744f5f92c48bd8005edc814bf72fbb24e5/SQLAlchemy-2.0.36-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855", size = 2082778 }, + { url = "https://files.pythonhosted.org/packages/60/f6/d9aa8c49c44f9b8c9b9dada1f12fa78df3d4c42aa2de437164b83ee1123c/SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53", size = 3232191 }, + { url = "https://files.pythonhosted.org/packages/8a/ab/81d4514527c068670cb1d7ab62a81a185df53a7c379bd2a5636e83d09ede/SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a", size = 3243044 }, + { url = "https://files.pythonhosted.org/packages/35/b4/f87c014ecf5167dc669199cafdb20a7358ff4b1d49ce3622cc48571f811c/SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686", size = 3178511 }, + { url = "https://files.pythonhosted.org/packages/ea/09/badfc9293bc3ccba6ede05e5f2b44a760aa47d84da1fc5a326e963e3d4d9/SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588", size = 3205147 }, + { url = "https://files.pythonhosted.org/packages/c8/60/70e681de02a13c4b27979b7b78da3058c49bacc9858c89ba672e030f03f2/SQLAlchemy-2.0.36-cp312-cp312-win32.whl", hash = "sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e", size = 2062709 }, + { url = "https://files.pythonhosted.org/packages/b7/ed/f6cd9395e41bfe47dd253d74d2dfc3cab34980d4e20c8878cb1117306085/SQLAlchemy-2.0.36-cp312-cp312-win_amd64.whl", hash = "sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5", size = 2088433 }, + { url = "https://files.pythonhosted.org/packages/78/5c/236398ae3678b3237726819b484f15f5c038a9549da01703a771f05a00d6/SQLAlchemy-2.0.36-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef", size = 2087651 }, + { url = "https://files.pythonhosted.org/packages/a8/14/55c47420c0d23fb67a35af8be4719199b81c59f3084c28d131a7767b0b0b/SQLAlchemy-2.0.36-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8", size = 2078132 }, + { url = "https://files.pythonhosted.org/packages/3d/97/1e843b36abff8c4a7aa2e37f9bea364f90d021754c2de94d792c2d91405b/SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b", size = 3164559 }, + { url = "https://files.pythonhosted.org/packages/7b/c5/07f18a897b997f6d6b234fab2bf31dccf66d5d16a79fe329aefc95cd7461/SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2", size = 3177897 }, + { url = "https://files.pythonhosted.org/packages/b3/cd/e16f3cbefd82b5c40b33732da634ec67a5f33b587744c7ab41699789d492/SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf", size = 3111289 }, + { url = "https://files.pythonhosted.org/packages/15/85/5b8a3b0bc29c9928aa62b5c91fcc8335f57c1de0a6343873b5f372e3672b/SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c", size = 3139491 }, + { url = "https://files.pythonhosted.org/packages/a1/95/81babb6089938680dfe2cd3f88cd3fd39cccd1543b7cb603b21ad881bff1/SQLAlchemy-2.0.36-cp313-cp313-win32.whl", hash = "sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436", size = 2060439 }, + { url = "https://files.pythonhosted.org/packages/c1/ce/5f7428df55660d6879d0522adc73a3364970b5ef33ec17fa125c5dbcac1d/SQLAlchemy-2.0.36-cp313-cp313-win_amd64.whl", hash = "sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88", size = 2084574 }, + { url = "https://files.pythonhosted.org/packages/db/da/443679a0b9e0a009f00d1542595c8a4d582ece1809704e703c4843f18768/SQLAlchemy-2.0.36-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3d6718667da04294d7df1670d70eeddd414f313738d20a6f1d1f379e3139a545", size = 2097524 }, + { url = "https://files.pythonhosted.org/packages/60/88/08249dc5651d976b64c257250ade16ec02444257b44e404e258f2862c201/SQLAlchemy-2.0.36-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:72c28b84b174ce8af8504ca28ae9347d317f9dba3999e5981a3cd441f3712e24", size = 2088207 }, + { url = "https://files.pythonhosted.org/packages/19/41/feb0216cced91211c9dd045f08fa020a3bd2e188110217d24b6b2a90b6a2/SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b11d0cfdd2b095e7b0686cf5fabeb9c67fae5b06d265d8180715b8cfa86522e3", size = 3090690 }, + { url = "https://files.pythonhosted.org/packages/80/fe/0055147b71de2007e716ddc686438aefb390b03fd2e382ff4a8588b78b58/SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e32092c47011d113dc01ab3e1d3ce9f006a47223b18422c5c0d150af13a00687", size = 3097567 }, + { url = "https://files.pythonhosted.org/packages/4e/55/52eaef72f071b89e2e965decc264d16b6031a39365a6593067053ba8bb97/SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6a440293d802d3011028e14e4226da1434b373cbaf4a4bbb63f845761a708346", size = 3044686 }, + { url = "https://files.pythonhosted.org/packages/2d/c8/6fb6179ad2eb9baab61132786488fa10e5b588ef2b008f57f560ae7f39d1/SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c54a1e53a0c308a8e8a7dffb59097bff7facda27c70c286f005327f21b2bd6b1", size = 3067814 }, + { url = "https://files.pythonhosted.org/packages/52/84/be7227a06c24418f131a877159eb962c62447883069390c868d1a782f01d/SQLAlchemy-2.0.36-cp38-cp38-win32.whl", hash = "sha256:1e0d612a17581b6616ff03c8e3d5eff7452f34655c901f75d62bd86449d9750e", size = 2067569 }, + { url = "https://files.pythonhosted.org/packages/df/4f/9c70c53a5bd6de5957db78578cb0491732da636032566d3e8748659513cb/SQLAlchemy-2.0.36-cp38-cp38-win_amd64.whl", hash = "sha256:8958b10490125124463095bbdadda5aa22ec799f91958e410438ad6c97a7b793", size = 2092329 }, + { url = "https://files.pythonhosted.org/packages/43/10/c1c865afeb50270677942cda17ed78b55b0a0068e426d22284a625d7341f/SQLAlchemy-2.0.36-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc022184d3e5cacc9579e41805a681187650e170eb2fd70e28b86192a479dcaa", size = 2095474 }, + { url = "https://files.pythonhosted.org/packages/25/cb/78d7663ad1c82ca8b5cbc7532b8e3c9f80a53f1bdaafd8f5314525700a01/SQLAlchemy-2.0.36-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b817d41d692bf286abc181f8af476c4fbef3fd05e798777492618378448ee689", size = 2086708 }, + { url = "https://files.pythonhosted.org/packages/5c/5b/f9b5cf759865b0dd8b20579b3d920ed87b6160fce75e2b7ed697ddbf0008/SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e46a888b54be23d03a89be510f24a7652fe6ff660787b96cd0e57a4ebcb46d", size = 3080607 }, + { url = "https://files.pythonhosted.org/packages/18/f6/afaef83a3fbeff40b9289508b985c5630c0e8303d08106a0117447c680d9/SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4ae3005ed83f5967f961fd091f2f8c5329161f69ce8480aa8168b2d7fe37f06", size = 3088410 }, + { url = "https://files.pythonhosted.org/packages/62/60/ec2b8c14b3c15b4a915ae821b455823fbafa6f38c4011b27c0a76f94928a/SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03e08af7a5f9386a43919eda9de33ffda16b44eb11f3b313e6822243770e9763", size = 3047623 }, + { url = "https://files.pythonhosted.org/packages/40/a2/9f748bdaf769eceb780c438b3dd7a37b8b8cbc6573e2a3748b0d5c2e9d80/SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3dbb986bad3ed5ceaf090200eba750b5245150bd97d3e67343a3cfed06feecf7", size = 3074096 }, + { url = "https://files.pythonhosted.org/packages/01/f7/290d7193c81d1ff0f751bd9430f3762bee0f53efd0273aba7ba18eb10520/SQLAlchemy-2.0.36-cp39-cp39-win32.whl", hash = "sha256:9fe53b404f24789b5ea9003fc25b9a3988feddebd7e7b369c8fac27ad6f52f28", size = 2067304 }, + { url = "https://files.pythonhosted.org/packages/6f/a0/dc1a808d6ac466b190ca570f7ce52a1761308279eab4a09367ccf2cd6bd7/SQLAlchemy-2.0.36-cp39-cp39-win_amd64.whl", hash = "sha256:af148a33ff0349f53512a049c6406923e4e02bf2f26c5fb285f143faf4f0e46a", size = 2091520 }, + { url = "https://files.pythonhosted.org/packages/b8/49/21633706dd6feb14cd3f7935fc00b60870ea057686035e1a99ae6d9d9d53/SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e", size = 1883787 }, +] + +[[package]] +name = "standard-imghdr" +version = "3.10.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/d2/2eb5521072c9598886035c65c023f39f7384bcb73eed70794f469e34efac/standard_imghdr-3.10.14.tar.gz", hash = "sha256:2598fe2e7c540dbda34b233295e10957ab8dc8ac6f3bd9eaa8d38be167232e52", size = 5474 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/d0/9852f70eb01f814843530c053542b72d30e9fbf74da7abb0107e71938389/standard_imghdr-3.10.14-py3-none-any.whl", hash = "sha256:cdf6883163349624dee9a81d2853a20260337c4cd41c04e99c082e01833a08e2", size = 5598 }, +] + +[[package]] +name = "starlette" +version = "0.41.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +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 = "structlog" +version = "24.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/a3/e811a94ac3853826805253c906faa99219b79951c7d58605e89c79e65768/structlog-24.4.0.tar.gz", hash = "sha256:b27bfecede327a6d2da5fbc96bd859f114ecc398a6389d664f62085ee7ae6fc4", size = 1348634 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/65/813fc133609ebcb1299be6a42e5aea99d6344afb35ccb43f67e7daaa3b92/structlog-24.4.0-py3-none-any.whl", hash = "sha256:597f61e80a91cc0749a9fd2a098ed76715a1c8a01f73e336b746504d1aad7610", size = 67180 }, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252 }, +] + +[[package]] +name = "targ" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "docstring-parser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/8c/8ab5d5391e36e27a67c6921f952a7c18d0b0ec19f4f8a88f0a64da81552d/targ-0.4.0.tar.gz", hash = "sha256:dcdb57945bffe5bc59570d2e41bb1adc6280c5460332c5daf300729bc88d1aba", size = 9494 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/1e/47625c6b3615035d3ef11da1edd6726e245610ed1cbb08d60acc939bd1ce/targ-0.4.0-py3-none-any.whl", hash = "sha256:5237524323661ffa899158d668468b5c94bb84e2d988bd216981932844da63eb", size = 7204 }, +] + +[[package]] +name = "taskgroup" +version = "0.0.0a4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/40/02753c40fa30dfdde7567c1daeefbf957dcf8c99e6534a80afb438adf07e/taskgroup-0.0.0a4.tar.gz", hash = "sha256:eb08902d221e27661950f2a0320ddf3f939f579279996f81fe30779bca3a159c", size = 8553 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e5/0f8dac3d9e6314f60a725cdc9ec01f45591ddd720e59f6a4ff8bdcdf82cd/taskgroup-0.0.0a4-py2.py3-none-any.whl", hash = "sha256:5c1bd0e4c06114e7a4128583ab75c987597d5378a33948a3b74c662b90f61277", size = 9109 }, +] + +[[package]] +name = "time-machine" +version = "2.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/82d358c4d53555f031c2343d1c235b56b9f3b0a60ac3adc555778fe87506/time_machine-2.15.0.tar.gz", hash = "sha256:ebd2e63baa117ded04b978813fcd1279d3fc6be2149c9cac75c716b6f1db774c", size = 25067 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/47/35413db37da55865fdbf60649bcb948cc2559f420ef4e91e77e4e24c71b8/time_machine-2.15.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:892d016789b59950989b2db188dcd46cf16d34e8daf2343e33b679b0c5fd1001", size = 20779 }, + { url = "https://files.pythonhosted.org/packages/e0/c3/fda6d2336737d0331eb55357db1dc916af14c4fda77c69ad8b3733b003c4/time_machine-2.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4428bdae507996aa3fdeb4727bca09e26306fa64a502e7335207252684516cbf", size = 17040 }, + { url = "https://files.pythonhosted.org/packages/36/e1/71200f24d668e5183e875a08ba5e557b6107c1b7d57fa6d54ac24ad10234/time_machine-2.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0302568338c8bd333ed0698231dbb781b70ead1a5579b4ac734b9bf88313229f", size = 34811 }, + { url = "https://files.pythonhosted.org/packages/9e/2f/4b9289ea07978ad5c3469c872c7eeadf5f5b3a1dcebe2fb274c2486fc220/time_machine-2.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18fc4740073e67071472c48355775ec6d1b93af5c675524b7de2474e0dcd8741", size = 32820 }, + { url = "https://files.pythonhosted.org/packages/5f/9e/9f838c91d2248d716281af60dfea4131438c6ad6d7405ebc6e47f8c25c3b/time_machine-2.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:768d33b484a35da93731cc99bdc926b539240a78673216cdc6306833d9072350", size = 34635 }, + { url = "https://files.pythonhosted.org/packages/f3/10/1048b5ba6de55779563f005de5fbfb764727bf9678ad7701cea480b3816c/time_machine-2.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:73a8c8160d2a170dadcad5b82fb5ee53236a19cec0996651cf4d21da0a2574d5", size = 34326 }, + { url = "https://files.pythonhosted.org/packages/13/82/6b4df8e5abf754b0ccceeb59fa32486d28c65f67d4ada37ff8b1e9f52006/time_machine-2.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:09fd839a321a92aa8183206c383b9725eaf4e0a28a70e4cb87db292b352eeefb", size = 32639 }, + { url = "https://files.pythonhosted.org/packages/cf/07/95e380c46136252401d97f613782a10061b3c11b61edaeb78e83aedc1a88/time_machine-2.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:838a6d117739f1ae6ecc45ec630fa694f41a85c0d07b1f3b1db2a6cc52c1808b", size = 34021 }, + { url = "https://files.pythonhosted.org/packages/b6/0c/6595fa82bd70bc7e8065bfc6534e51a27c18978f7c158d6392c979cace2c/time_machine-2.15.0-cp310-cp310-win32.whl", hash = "sha256:d24d2ec74923b49bce7618e3e7762baa6be74e624d9829d5632321de102bf386", size = 19413 }, + { url = "https://files.pythonhosted.org/packages/2f/3d/cb3c1cecfeb4b6423302ee1b2863617390500f67526f0fc1fb5641db05f6/time_machine-2.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:95c8e7036cf442480d0bf6f5fde371e1eb6dbbf5391d7bdb8db73bd8a732b538", size = 20280 }, + { url = "https://files.pythonhosted.org/packages/22/aa/96aaac88738369fba43d5cb076bb09290b1a2cbd84210bcc0a9a519c7970/time_machine-2.15.0-cp310-cp310-win_arm64.whl", hash = "sha256:660810cd27a8a94cb5e845e8f28a95e70b01ff0c45466d394c4a0cba5a0ae279", size = 18392 }, + { url = "https://files.pythonhosted.org/packages/ce/54/829ab196c3306eb4cee95e3c8e7d004e15877b36479de5d2ecc72fc1d3d4/time_machine-2.15.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:674097dd54a0bbd555e7927092c74428c4c07268ad52bca38cfccc3214707e50", size = 20448 }, + { url = "https://files.pythonhosted.org/packages/e1/48/a06f8c7db768db501a60210a48f3d37b7b3d65ca85aa8dc08147eb204b4a/time_machine-2.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4e83fd6112808d1d14d1a57397c6fa3bd71bb2f3b8800036e12366e3680819b9", size = 16897 }, + { url = "https://files.pythonhosted.org/packages/e7/f8/73265927e3da54a417536dc3d8c9aad806b62b8133099a7ee12661aba1a3/time_machine-2.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b095a1de40ca1afaeae8df3f45e26b645094a1912e6e6871e725fcf06ecdb74a", size = 32789 }, + { url = "https://files.pythonhosted.org/packages/8a/a4/bcf8ad40a4c6973a67aba5df7ed704dc34256835fb074cfb4d4caa0f93a5/time_machine-2.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4601fe7a6b74c6fd9207e614d9db2a20dd4befd4d314677a0feac13a67189707", size = 30911 }, + { url = "https://files.pythonhosted.org/packages/13/87/a6de1b187f5468781e0e722c877323625227151cc8ffff363a7391b01149/time_machine-2.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:245ef73f9927b7d4909d554a6a0284dbc5dee9730adea599e430b37c9e9fa203", size = 32644 }, + { url = "https://files.pythonhosted.org/packages/33/1f/7378d5a032467891a1005e546532223b97c53440c6359b1133696a5cb1ef/time_machine-2.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:704abc7f3403584cca9c01c5809812e0bd70632ea4251389fae4f45e11aad94f", size = 32472 }, + { url = "https://files.pythonhosted.org/packages/68/14/2fab61ad2c9a831925bce3d5e341fa2108ba062c2de0c190569e1ee6a1c9/time_machine-2.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6425001e50a0c82108caed438233066cea04d42a8fc9c49bfcf081a5b96e5b4e", size = 30882 }, + { url = "https://files.pythonhosted.org/packages/8a/01/f5146b9956b548072000a87f3eb8dbb2591ada1516a5d889aa12b373948f/time_machine-2.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5d4073b754f90b19f28d036ec5143d3fca3a75e4d4241d78790a6178b00bb373", size = 32176 }, + { url = "https://files.pythonhosted.org/packages/ca/09/8a8488e6d3faf3cb68d078f27ca94aa3ba1bc08d5f804265c590208a70f5/time_machine-2.15.0-cp311-cp311-win32.whl", hash = "sha256:8817b0f7d7830215261b18db83c9c3ef1da6bb64da5c292d7c70b9a46e5a6745", size = 19305 }, + { url = "https://files.pythonhosted.org/packages/75/33/d8411b197a08eedb3ce086022cdf4faf1f15738607a2d943fd5286f57fdd/time_machine-2.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:ddad27a62df2ea47b7b483009fbfcf167a71d702cbd8e2eefd9ddc1c93146658", size = 20210 }, + { url = "https://files.pythonhosted.org/packages/8c/f5/e9b5d7be612403e570a42af5c2823506877e726f77f2d6ff272d72d0aed3/time_machine-2.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f021aa2dbd8fbfe54d3fa2258518129108b7496922b3bcff2cf5991078eec67", size = 18278 }, + { url = "https://files.pythonhosted.org/packages/49/47/46bf332f4ecd7f35e197131b9c23daa39423cf71b814e36e9d5df3cf2380/time_machine-2.15.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a22f47c34ee1fcf7d93a8c5c93135499aac879d9d5d8f820bd28571a30fdabcd", size = 20436 }, + { url = "https://files.pythonhosted.org/packages/f1/36/9990f16868ffdefe6b5aecfdfbcb11718230e414ca61a887fbee884f70e5/time_machine-2.15.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b684f8ecdeacd6baabc17b15ac1b054ca62029193e6c5367ef00b3516671de80", size = 16926 }, + { url = "https://files.pythonhosted.org/packages/ca/2d/007955a0899cd079a400bc204c03edc76274de2471d94ca235ff587a6eba/time_machine-2.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f7add997684bc6141e1c80f6ba0c38ffe316ba277a4074e61b1b7b4f5a172bf", size = 17157 }, + { url = "https://files.pythonhosted.org/packages/7a/e2/66d26450f9bfd1b019abdefbf0c62e760efc8992c7bf88d6c18f7ea6b94a/time_machine-2.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31af56399bf7c9ef76a3f7b6d9471dffa8f06ee373c194a374b69523f9061de9", size = 34005 }, + { url = "https://files.pythonhosted.org/packages/46/2c/dc2c42200aee6b47a55274d984736f7507ecfbfd0345114ec511ec444bef/time_machine-2.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5b94cba3edfc54bcb3ab5be616a2f50fa48be438e5af970824efdf882d1bc31", size = 31926 }, + { url = "https://files.pythonhosted.org/packages/87/59/10d8faecbd233b0da831eb9973c3e650c83fb3d443edb607b6b26c9688ac/time_machine-2.15.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3862dda89bdb05f9d521b08fdcb24b19a7dd9f559ae324f4301ba7a07b6eea64", size = 33685 }, + { url = "https://files.pythonhosted.org/packages/2e/a4/702ad9e328cbc7b3f1833dee4886d0994e52bc2d9640effa64bccc7740fa/time_machine-2.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e1790481a6b9ce38888f22ce30710244067898c3ac4805a0e061e381f3db3506", size = 33609 }, + { url = "https://files.pythonhosted.org/packages/a0/9d/6009d28ad395a45b5bb91af31616494b4e61d6d9df252d0e5933cd3aa8f1/time_machine-2.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a731c03bc00552ee6cc685a59616d36003124e7e04c6ddf65c2c47f1c3d85480", size = 31737 }, + { url = "https://files.pythonhosted.org/packages/36/22/b55df08cf48d46af93ee2f4310dd88c8519bc5f98afd24af57a81a5d5272/time_machine-2.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e6776840aea3ff5ab6924b50117957da62db51b109b3b491c0d5817a804b1a8e", size = 33253 }, + { url = "https://files.pythonhosted.org/packages/52/d7/bb5e92f0b0268cd13baad874d82b0e964a847cf52740464abeec48dc1642/time_machine-2.15.0-cp312-cp312-win32.whl", hash = "sha256:9479530e3fce65f6149058071fa4df8150025f15b43b103445f619842981a87c", size = 19369 }, + { url = "https://files.pythonhosted.org/packages/f4/33/276537ba292fc7ee67e6aef7566b2a6b313dbc4d479e5e80eed43094ef48/time_machine-2.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:b5f3ab4185c1f72010846ca9fccb08349e23a2b52982a18d9870e848ce9f1c86", size = 20219 }, + { url = "https://files.pythonhosted.org/packages/e0/0e/e8b75032248f59a2bc5c125d3a41242b1e577caa07585c42b22373d6466d/time_machine-2.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:c0473dfa8f17c6a9a250b2bd6a5b62af3aa7d22518f701649115f1085d5e35ab", size = 18297 }, + { url = "https://files.pythonhosted.org/packages/7c/a1/ebe212530628aa29a86a771ca77cb2d1ead667382cfa89a3fb849e3f0108/time_machine-2.15.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f50f10058b884d45cd8a50423bf561b1f9f9df7058abeb8b318700c8bcf4bb54", size = 20492 }, + { url = "https://files.pythonhosted.org/packages/29/0d/2a19951729e50d8809e161e533585c0be5ae39c0cf40140877353847b9b5/time_machine-2.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:df6f618b98f0848fd8d07039541e10f23db679d8283f8719e870a98e1ef8e639", size = 16961 }, + { url = "https://files.pythonhosted.org/packages/85/eb/33cf2173758b128f55c880c492e17b70f6c325e7bee879f9b0171cfe02a0/time_machine-2.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52468a0784544eba708c0ae6bc5e8c5dcfd685495a60f7f74028662c984bd9cd", size = 34051 }, + { url = "https://files.pythonhosted.org/packages/e1/23/da9a7935a7be952ab6163caf976b6bad049f6e117f3a396ecc381b077cb6/time_machine-2.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c08800c28160f4d32ca510128b4e201a43c813e7a2dd53178fa79ebe050eba13", size = 31966 }, + { url = "https://files.pythonhosted.org/packages/0b/0d/a8e3cbd91ffa98b0fa50b6e29d03151f37aa04cca4dd658e33cdf2b4731e/time_machine-2.15.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65d395211736d9844537a530287a7c64b9fda1d353e899a0e1723986a0859154", size = 33727 }, + { url = "https://files.pythonhosted.org/packages/07/53/c084031980706517cfbae9f462e455d61c7cbf9b66a8a83bcc5b79d00836/time_machine-2.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b177d334a35bf2ce103bfe4e0e416e4ee824dd33386ea73fa7491c17cc61897", size = 33690 }, + { url = "https://files.pythonhosted.org/packages/a0/30/5c87e8709ba00c893faf8a9bddf06abf317fdc6103fe78bdf99c53ab444f/time_machine-2.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9a6a9342fae113b12aab42c790880c549d9ba695b8deff27ee08096eedd67569", size = 31809 }, + { url = "https://files.pythonhosted.org/packages/04/7b/92ac7c556cd123bf8b23dbae3cf4a273c276110b87d0c4b5600c2cec8e70/time_machine-2.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bcbb25029ee8756f10c6473cea5ef21707a1d9a8752cdf29fad3a5f34aa4a313", size = 33325 }, + { url = "https://files.pythonhosted.org/packages/e9/71/36c74bab3d4e4385d31610b367da1535a36d17358df058e0920a7510e17c/time_machine-2.15.0-cp313-cp313-win32.whl", hash = "sha256:29b988b1f09f2a083b12b6b054787b799ae91ee15bb0e9de3e48f880e4d68674", size = 19397 }, + { url = "https://files.pythonhosted.org/packages/a4/69/ea5976c43a673894f2fa85a05b28a610b474472f393e59722a6946f7070b/time_machine-2.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:d828721dcbcb94b904a6b25df67c2513ecd24cd9e36694f38b9f0fa71c7c6103", size = 20242 }, + { url = "https://files.pythonhosted.org/packages/cc/c8/26367d0b8dfaf7445576fe0051bff61b8f5be752e7bf3e8807ed7fa3a343/time_machine-2.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:008bd668d933b1a029c81805bcdc0132390c2545b103cf8e6709e3adbc37989d", size = 18337 }, + { url = "https://files.pythonhosted.org/packages/97/54/eeac8568cad4f3eb255cc78f1fa2c36147afd3fcba770283bf2b2a188b33/time_machine-2.15.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e99689f6c6b9ca6e2fc7a75d140e38c5a7985dab61fe1f4e506268f7e9844e05", size = 20674 }, + { url = "https://files.pythonhosted.org/packages/5c/82/488341de4c03c0856aaf5db74f2a8fe18dcc7657401334c54c4aa6cb0fc6/time_machine-2.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:671e88a6209a1cf415dc0f8c67d2b2d3b55b436cc63801a518f9800ebd752959", size = 16990 }, + { url = "https://files.pythonhosted.org/packages/9c/cc/0ca559e71be4eb05917d02364f4d356351b31dd0d6ff3c4c86fa4de0a03e/time_machine-2.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b2d28daf4cabc698aafb12135525d87dc1f2f893cbd29a8a6fe0d8d36d1342c", size = 35501 }, + { url = "https://files.pythonhosted.org/packages/92/a0/14905a5feecc6d2e87ebe6dd2b044358422836ed173071cdc1245aa5ec88/time_machine-2.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cd9f057457d12604be18b623bcd5ae7d0b917ad66cb510ee1135d5f123666e2", size = 33430 }, + { url = "https://files.pythonhosted.org/packages/19/a4/282b65b4d835dfd7b863777cc4206bec375285bda884dc22bd1264716f6a/time_machine-2.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97dc6793e512a62ba9eab250134a2e67372c16ae9948e73d27c2ef355356e2e1", size = 35366 }, + { url = "https://files.pythonhosted.org/packages/93/8e/f7db3f641f1ff86b98594c9cf8d71c8d292cc2bde06c1369ce4745494cc5/time_machine-2.15.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0630a32e9ebcf2fac3704365b31e271fef6eabd6fedfa404cd8dbd244f7fc84d", size = 34373 }, + { url = "https://files.pythonhosted.org/packages/3d/9f/8c8ac57ccb29e692e0940e58515a9afb844d2d11b7f057a0fe153bfe4877/time_machine-2.15.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:617c9a92d8d8f60d5ef39e76596620503752a09f834a218e5b83be352fdd6c91", size = 32667 }, + { url = "https://files.pythonhosted.org/packages/ec/e7/f0c6f9507b0bbfdec54d256b6efc9417ae1a01ce6320c2a42235b807cf86/time_machine-2.15.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3f7eadd820e792de33a9ec91f8178a2b9088e4e8b9a166953419ddc4ec5f7cfe", size = 34070 }, + { url = "https://files.pythonhosted.org/packages/20/82/ac2d8343db8dade1372457d7a5694f069882d9eac110ddce2643ef0501aa/time_machine-2.15.0-cp38-cp38-win32.whl", hash = "sha256:b7b647684eb2e1fd1e5e6b101249d5fe9d6117c117b5e336ad8dd75af48d2d1f", size = 19396 }, + { url = "https://files.pythonhosted.org/packages/3a/12/ac7bb1e932536fce359e021a62c2a5a30c4e470293d6f8b2fb47077562dc/time_machine-2.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b48abd7745caec1a78a16a048966cde14ff6ccb04d471a7201532648d3f77d14", size = 20266 }, + { url = "https://files.pythonhosted.org/packages/ec/bf/d9689e1fa669e575c3ed57bf4f9205a9b5fbe703dc7ef89ba5ce9aa39a38/time_machine-2.15.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8c2b1c91b437133c672e374857eccb1dd2c2d9f8477ae3b35138382d5ef19846", size = 20775 }, + { url = "https://files.pythonhosted.org/packages/d8/4b/4314a7882b470c52cd527601107b1163e19d37fb1eb31eea0f8d73d0b178/time_machine-2.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:79bf1ef6850182e09d86e61fa31717da56014a3b2234afb025fca1f2a43ac07b", size = 17037 }, + { url = "https://files.pythonhosted.org/packages/f1/b8/adf2f8b8e10f6f5e498b0cddd103f6520144af53fb27b5a01eca50812a92/time_machine-2.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:658ea8477fa020f08435fb7277635eb0b50cd5206b9d4cbe10e9a5466b01f855", size = 34511 }, + { url = "https://files.pythonhosted.org/packages/e3/86/fda41a9e8115fd377f2d4d15c91a414f75cb8f2cd7f8bde974855a0f381f/time_machine-2.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c947135750d20f35acac290c34f1acf5771fc166a3fbc0e3816a97c756aaa5f5", size = 32533 }, + { url = "https://files.pythonhosted.org/packages/cb/7e/1e2e69fee659f00715f12392cabea1920245504862eab2caac6e3f30de5b/time_machine-2.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dee3a0dd1866988c49a5d00564404db9bcdf49ca92f9c4e8b6c99609d64e698", size = 34348 }, + { url = "https://files.pythonhosted.org/packages/41/da/8db2df73ebe9f23af25b05f1720b108a145805a8c83d5ff8248e2d3cbcfa/time_machine-2.15.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c596920d6017702a36e3a43fd8110a84e87d6229f30b84bd5640cbae9b5145da", size = 34081 }, + { url = "https://files.pythonhosted.org/packages/a7/c7/9202404f8885257c09c98d3e5186b989b6b482a2983dc24c81bd0333e668/time_machine-2.15.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:014589d0edd4aa14f8d63985745565e8cbbe48461d6c004a96000b47f6b44e78", size = 32357 }, + { url = "https://files.pythonhosted.org/packages/4d/79/482a69c31259c3c2efcd9e73ea4a0a4d315103836c1667875612288bca28/time_machine-2.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5ff655716cd13a242eef8cf5d368074e8b396ff86508a5933e7cff4f2b3eb3c2", size = 33742 }, + { url = "https://files.pythonhosted.org/packages/f0/39/89725d12a3552bb9113528d8f9aa7188e1660b377b74e7d72e8ab5eeff06/time_machine-2.15.0-cp39-cp39-win32.whl", hash = "sha256:1168eebd7af7e6e3e2fd378c16ca917b97dd81c89a1f1f9e1daa985c81699d90", size = 19410 }, + { url = "https://files.pythonhosted.org/packages/89/0c/50e86c4a7b72d2bdc658492b13e804f933814f86f34c4350758d1ab87586/time_machine-2.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:c344eb09fcfbf71e5b5847d4f188fec98e1c3a976125ef571eac5f1c39e7a5e5", size = 20281 }, + { url = "https://files.pythonhosted.org/packages/f6/4d/f8ad3b0c50a268a9ea766c9533866bba6a7717a5324c84e356abb7347fa4/time_machine-2.15.0-cp39-cp39-win_arm64.whl", hash = "sha256:899f1a856b3bebb82b6cbc3c0014834b583b83f246b28e462a031ec1b766130b", size = 18387 }, +] + +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 }, +] + +[[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 = "tornado" +version = "6.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/45/a0daf161f7d6f36c3ea5fc0c2de619746cc3dd4c76402e9db545bd920f63/tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b", size = 501135 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/7e/71f604d8cea1b58f82ba3590290b66da1e72d840aeb37e0d5f7291bd30db/tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1", size = 436299 }, + { url = "https://files.pythonhosted.org/packages/96/44/87543a3b99016d0bf54fdaab30d24bf0af2e848f1d13d34a3a5380aabe16/tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803", size = 434253 }, + { url = "https://files.pythonhosted.org/packages/cb/fb/fdf679b4ce51bcb7210801ef4f11fdac96e9885daa402861751353beea6e/tornado-6.4.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec", size = 437602 }, + { url = "https://files.pythonhosted.org/packages/4f/3b/e31aeffffc22b475a64dbeb273026a21b5b566f74dee48742817626c47dc/tornado-6.4.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946", size = 436972 }, + { url = "https://files.pythonhosted.org/packages/22/55/b78a464de78051a30599ceb6983b01d8f732e6f69bf37b4ed07f642ac0fc/tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf", size = 437173 }, + { url = "https://files.pythonhosted.org/packages/79/5e/be4fb0d1684eb822c9a62fb18a3e44a06188f78aa466b2ad991d2ee31104/tornado-6.4.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634", size = 437892 }, + { url = "https://files.pythonhosted.org/packages/f5/33/4f91fdd94ea36e1d796147003b490fe60a0215ac5737b6f9c65e160d4fe0/tornado-6.4.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73", size = 437334 }, + { url = "https://files.pythonhosted.org/packages/2b/ae/c1b22d4524b0e10da2f29a176fb2890386f7bd1f63aacf186444873a88a0/tornado-6.4.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c", size = 437261 }, + { url = "https://files.pythonhosted.org/packages/b5/25/36dbd49ab6d179bcfc4c6c093a51795a4f3bed380543a8242ac3517a1751/tornado-6.4.2-cp38-abi3-win32.whl", hash = "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482", size = 438463 }, + { url = "https://files.pythonhosted.org/packages/61/cc/58b1adeb1bb46228442081e746fcdbc4540905c87e8add7c277540934edb/tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38", size = 438907 }, +] + +[[package]] +name = "tree-sitter" +version = "0.21.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/9e/b7cb190aa08e4ea387f2b1531da03efb4b8b033426753c0b97e3698645f6/tree-sitter-0.21.3.tar.gz", hash = "sha256:b5de3028921522365aa864d95b3c41926e0ba6a85ee5bd000e10dc49b0766988", size = 155688 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/b3/5507348eee41af3abf537607779c87de9141fc41af5aa547ff5c1a87fcf6/tree_sitter-0.21.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:351f302b6615230c9dac9829f0ba20a94362cd658206ca9a7b2d58d73373dfb0", size = 133430 }, + { url = "https://files.pythonhosted.org/packages/a5/24/05a76f662445ebdebd00e12bc4c9ebf974a559bb7f4863dc1def775add39/tree_sitter-0.21.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:766e79ae1e61271e7fdfecf35b6401ad9b47fc07a0965ad78e7f97fddfdf47a6", size = 126087 }, + { url = "https://files.pythonhosted.org/packages/64/74/2c9dc1acb4c43b9c15765ab0fb4babb76e49e8198889dfc730bc1e906a82/tree_sitter-0.21.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c4d3d4d4b44857e87de55302af7f2d051c912c466ef20e8f18158e64df3542a", size = 485385 }, + { url = "https://files.pythonhosted.org/packages/06/a3/9f65bcb73947bf4d16e816fa547e1edfe411a530ff3370c1ae10c5bc21d8/tree_sitter-0.21.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84eedb06615461b9e2847be7c47b9c5f2195d7d66d31b33c0a227eff4e0a0199", size = 496711 }, + { url = "https://files.pythonhosted.org/packages/88/68/c916a2669ff92f8e66d519d5df20d36e0eac0dd178a77169ae8c8a7c3e08/tree_sitter-0.21.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9d33ea425df8c3d6436926fe2991429d59c335431bf4e3c71e77c17eb508be5a", size = 481965 }, + { url = "https://files.pythonhosted.org/packages/42/6e/b8bf5f75fc6485a429e2b6f71aaed2666dba483b875fbd76a7b007f85073/tree_sitter-0.21.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fae1ee0ff6d85e2fd5cd8ceb9fe4af4012220ee1e4cbe813305a316caf7a6f63", size = 492782 }, + { url = "https://files.pythonhosted.org/packages/16/bf/c6ee8d9287846912f427e1d8f1f7266e612a1d6ba161517c793e83f620cf/tree_sitter-0.21.3-cp310-cp310-win_amd64.whl", hash = "sha256:bb41be86a987391f9970571aebe005ccd10222f39c25efd15826583c761a37e5", size = 109732 }, + { url = "https://files.pythonhosted.org/packages/63/b5/72657d5874d7f0a722c0288f04e5e2bc33d7715b13a858885b6593047dce/tree_sitter-0.21.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54b22c3c2aab3e3639a4b255d9df8455da2921d050c4829b6a5663b057f10db5", size = 133429 }, + { url = "https://files.pythonhosted.org/packages/d3/64/c5d397efbb6d0dbed4254cd2ca389ed186a2e1e7e32661059f6eeaaf6424/tree_sitter-0.21.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab6e88c1e2d5e84ff0f9e5cd83f21b8e5074ad292a2cf19df3ba31d94fbcecd4", size = 126088 }, + { url = "https://files.pythonhosted.org/packages/ba/88/941669acc140f94e6c6196d6d8676ac4cd57c3b3fbc1ee61bb11c1b2da71/tree_sitter-0.21.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3fd34ed4cd5db445bc448361b5da46a2a781c648328dc5879d768f16a46771", size = 487879 }, + { url = "https://files.pythonhosted.org/packages/29/4e/798154f2846d620bf9fa3bc244e056d4858f2108f834656bf9f1219d4f30/tree_sitter-0.21.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fabc7182f6083269ce3cfcad202fe01516aa80df64573b390af6cd853e8444a1", size = 498776 }, + { url = "https://files.pythonhosted.org/packages/6e/d1/05ea77487bc7a3946d0e80fb6c5cb61515953f5e7a4f6804b98e113ed4b0/tree_sitter-0.21.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f874c3f7d2a2faf5c91982dc7d88ff2a8f183a21fe475c29bee3009773b0558", size = 483348 }, + { url = "https://files.pythonhosted.org/packages/42/fa/bf938e7c6afbc368d503deeda060891c3dba57e2d1166e4b884271f55616/tree_sitter-0.21.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ee61ee3b7a4eedf9d8f1635c68ba4a6fa8c46929601fc48a907c6cfef0cfbcb2", size = 493757 }, + { url = "https://files.pythonhosted.org/packages/1d/a7/98da36a6eab22f5729989c9e0137b1b04cbe368d1e024fccd72c0b00719b/tree_sitter-0.21.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b7256c723642de1c05fbb776b27742204a2382e337af22f4d9e279d77df7aa2", size = 109735 }, + { url = "https://files.pythonhosted.org/packages/81/e1/cceb06eae617a6bf5eeeefa9813d9fd57d89b50f526ce02486a336bcd2a9/tree_sitter-0.21.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:669b3e5a52cb1e37d60c7b16cc2221c76520445bb4f12dd17fd7220217f5abf3", size = 133640 }, + { url = "https://files.pythonhosted.org/packages/f6/ce/ac14e5cbb0f30b7bd338122491ee2b8e6c0408cfe26741cbd66fa9b53d35/tree_sitter-0.21.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2aa2a5099a9f667730ff26d57533cc893d766667f4d8a9877e76a9e74f48f0d3", size = 125954 }, + { url = "https://files.pythonhosted.org/packages/c2/df/76dbf830126e566c48db0d1bf2bef3f9d8cac938302a9b0f762ded8206c2/tree_sitter-0.21.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a3e06ae2a517cf6f1abb682974f76fa760298e6d5a3ecf2cf140c70f898adf0", size = 490092 }, + { url = "https://files.pythonhosted.org/packages/ec/87/0c3593552cb0d09ab6271d37fc0e6a9476919d2a975661d709d4b3289fc7/tree_sitter-0.21.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af992dfe08b4fefcfcdb40548d0d26d5d2e0a0f2d833487372f3728cd0772b48", size = 502155 }, + { url = "https://files.pythonhosted.org/packages/05/92/b2cb22cf52c18fcc95662897f380cf230c443dfc9196b872aad5948b7bb3/tree_sitter-0.21.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c7cbab1dd9765138505c4a55e2aa857575bac4f1f8a8b0457744a4fefa1288e6", size = 486020 }, + { url = "https://files.pythonhosted.org/packages/4a/ea/69b543538a46d763f3e787234d1617b718ab90f32ffa676ca856f1d9540e/tree_sitter-0.21.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1e66aeb457d1529370fcb0997ae5584c6879e0e662f1b11b2f295ea57e22f54", size = 496348 }, + { url = "https://files.pythonhosted.org/packages/eb/4f/df4ea84476443021707b537217c32147ccccbc3e10c17b216a969991e1b3/tree_sitter-0.21.3-cp312-cp312-win_amd64.whl", hash = "sha256:013c750252dc3bd0e069d82e9658de35ed50eecf31c6586d0de7f942546824c5", size = 109771 }, + { url = "https://files.pythonhosted.org/packages/a9/08/3a2faaca576eca4ba9d25ff54f5e3c9fe7bdde7b748208dad7c033106f77/tree_sitter-0.21.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4986a8cb4acebd168474ec2e5db440e59c7888819b3449a43ce8b17ed0331b07", size = 133680 }, + { url = "https://files.pythonhosted.org/packages/d4/9c/0465da56c8ca82a366d08f62be21df61f86116fd63dd70fb9bb94c67b21c/tree_sitter-0.21.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6e217fee2e7be7dbce4496caa3d1c466977d7e81277b677f954d3c90e3272ec2", size = 126286 }, + { url = "https://files.pythonhosted.org/packages/81/aa/28c6e2ee7506b6627ba5a35dcfa9042043a32d14cecb5bc7d8fa6f0a1441/tree_sitter-0.21.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f32a88afff4f2bc0f20632b0a2aa35fa9ae7d518f083409eca253518e0950929", size = 493104 }, + { url = "https://files.pythonhosted.org/packages/ec/19/5e964315acf83dc84ddf7d1abadda3d7950a743247cb331c6dea0664fd9f/tree_sitter-0.21.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3652ac9e47cdddf213c5d5d6854194469097e62f7181c0a9aa8435449a163a9", size = 503895 }, + { url = "https://files.pythonhosted.org/packages/55/fe/bb1ef3feb6e11ff7feefe423fbb833ede7b42f20dd7742cfa7b163d6127a/tree_sitter-0.21.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:60b4df3298ff467bc01e2c0f6c2fb43aca088038202304bf8e41edd9fa348f45", size = 490926 }, + { url = "https://files.pythonhosted.org/packages/b8/05/dffdba16b6d2295c24e51a812481fe27907213415b306f99b84c1b518506/tree_sitter-0.21.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:00e4d0c99dff595398ef5e88a1b1ddd53adb13233fb677c1fd8e497fb2361629", size = 499248 }, + { url = "https://files.pythonhosted.org/packages/cf/67/8a83076e07e57f809bf308973aac5c21f8a2e5757ddd3429a14986a76405/tree_sitter-0.21.3-cp38-cp38-win_amd64.whl", hash = "sha256:50c91353a26946e4dd6779837ecaf8aa123aafa2d3209f261ab5280daf0962f5", size = 110055 }, + { url = "https://files.pythonhosted.org/packages/29/ea/2f848cf720dce4835388f30dca377bee9054461c51cdd0340c82e2d2fe32/tree_sitter-0.21.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b17b8648b296ccc21a88d72ca054b809ee82d4b14483e419474e7216240ea278", size = 133664 }, + { url = "https://files.pythonhosted.org/packages/9e/33/db09a0b0f7ca96bbf53e2a5c024bc1f0a138babb2f4472b4954627778d79/tree_sitter-0.21.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f2f057fd01d3a95cbce6794c6e9f6db3d376cb3bb14e5b0528d77f0ec21d6478", size = 126419 }, + { url = "https://files.pythonhosted.org/packages/4f/22/e01759e2303dc9db719f9985d6e980ddd24ac2a9136d5d06ee7c7e13f25a/tree_sitter-0.21.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:839759de30230ffd60687edbb119b31521d5ac016749358e5285816798bb804a", size = 487811 }, + { url = "https://files.pythonhosted.org/packages/0a/5d/afef7068b7eb3f5b50b4c0ae0fff43511f37263876dbb487158b22f89bfb/tree_sitter-0.21.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df40aa29cb7e323898194246df7a03b9676955a0ac1f6bce06bc4903a70b5f7", size = 498473 }, + { url = "https://files.pythonhosted.org/packages/44/f1/18534c57d8e0bbaae6fe0d00d7e4ae3394b0be0b2b94b6aba370cdbd3be6/tree_sitter-0.21.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1d9be27dde007b569fa78ff9af5fe40d2532c998add9997a9729e348bb78fa59", size = 484418 }, + { url = "https://files.pythonhosted.org/packages/b0/57/f2b16d902e808ccf99740da2cf199043e12783692fe631e857259fbe279f/tree_sitter-0.21.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c4ac87735e6f98fe085244c7c020f0177d13d4c117db72ba041faa980d25d69d", size = 494421 }, + { url = "https://files.pythonhosted.org/packages/44/cc/ceef3adeeb998578eaf199632b64e34a6cb2f8186e3856c9796d4671a84e/tree_sitter-0.21.3-cp39-cp39-win_amd64.whl", hash = "sha256:fbbd137f7d9a5309fb4cb82e2c3250ba101b0dd08a8abdce815661e6cf2cbc19", size = 110032 }, +] + +[[package]] +name = "trio" +version = "0.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "outcome" }, + { name = "sniffio" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/d1/a83dee5be404da7afe5a71783a33b8907bacb935a6dc8c69ab785e4a3eed/trio-0.27.0.tar.gz", hash = "sha256:1dcc95ab1726b2da054afea8fd761af74bad79bd52381b84eae408e983c76831", size = 568064 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/83/ec3196c360afffbc5b342ead48d1eb7393dd74fa70bca75d33905a86f211/trio-0.27.0-py3-none-any.whl", hash = "sha256:68eabbcf8f457d925df62da780eff15ff5dc68fd6b367e2dde59f7aaf2a0b884", size = 481734 }, +] + +[[package]] +name = "twisted" +version = "24.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "automat" }, + { name = "constantly" }, + { name = "hyperlink" }, + { name = "incremental" }, + { name = "typing-extensions" }, + { name = "zope-interface" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/0f/2d0b0dcd52a849db64ff63619aead94ae1091fe4d4d7e100371efe513585/twisted-24.10.0.tar.gz", hash = "sha256:02951299672595fea0f70fa2d5f7b5e3d56836157eda68859a6ad6492d36756e", size = 3525999 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/7c/f80f6853d702782edb357190c42c3973f13c547a5f68ab1b17e6415061b8/twisted-24.10.0-py3-none-any.whl", hash = "sha256:67aa7c8aa94387385302acf44ade12967c747858c8bcce0f11d38077a11c5326", size = 3188753 }, +] + +[package.optional-dependencies] +tls = [ + { name = "idna" }, + { name = "pyopenssl" }, + { name = "service-identity" }, +] + +[[package]] +name = "txaio" +version = "23.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/91/bc9fd5aa84703f874dea27313b11fde505d343f3ef3ad702bddbe20bfd6e/txaio-23.1.1.tar.gz", hash = "sha256:f9a9216e976e5e3246dfd112ad7ad55ca915606b60b84a757ac769bd404ff704", size = 53704 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/6c/a53cc9a97c2da76d9cd83c03f377468599a28f2d4ad9fc71c3b99640e71e/txaio-23.1.1-py2.py3-none-any.whl", hash = "sha256:aaea42f8aad50e0ecfb976130ada140797e9dcb85fad2cf72b0f37f8cefcb490", size = 30512 }, +] + +[[package]] +name = "types-beautifulsoup4" +version = "4.12.0.20241020" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-html5lib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/ae/5a7571c649cdd9f3c07d16790467a4fe1191f12a3ad7eecd1097cb8b1d9f/types-beautifulsoup4-4.12.0.20241020.tar.gz", hash = "sha256:158370d08d0cd448bd11b132a50ff5279237a5d4b5837beba074de152a513059", size = 11682 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/43/0f96cdf27d7da7dea729af3476b7be997205765209651a42a4e1895bab72/types_beautifulsoup4-4.12.0.20241020-py3-none-any.whl", hash = "sha256:c95e66ce15a4f5f0835f7fbc5cd886321ae8294f977c495424eaf4225307fd30", size = 12170 }, +] + +[[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-html5lib" +version = "1.1.11.20241018" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/9d/f6fbcc8246f5e46845b4f989c4e17e6fb3ce572f7065b185e515bf8a3be7/types-html5lib-1.1.11.20241018.tar.gz", hash = "sha256:98042555ff78d9e3a51c77c918b1041acbb7eb6c405408d8a9e150ff5beccafa", size = 11370 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/7c/f862b1dc31268ef10fe95b43dcdf216ba21a592fafa2d124445cd6b92e93/types_html5lib-1.1.11.20241018-py3-none-any.whl", hash = "sha256:3f1e064d9ed2c289001ae6392c84c93833abb0816165c6ff0abfc304a779f403", size = 17292 }, +] + +[[package]] +name = "types-psutil" +version = "6.1.0.20241102" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/03/015b10b717747922457b54ecd3e2ac3b174d87667b74108f66ccf1c75636/types-psutil-6.1.0.20241102.tar.gz", hash = "sha256:8cbe086b9c29f5c0aa55c4422498c07a8e506f096205761dba088905198551dc", size = 15447 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/93/b84f9e33febb4745206882c24434d67dc04115ac04e61132c337c797a65f/types_psutil-6.1.0.20241102-py3-none-any.whl", hash = "sha256:61f836f8ba48f28f0d290d3bcd902f9130ce5057a1676e6ecbefb6141e2743f4", size = 18653 }, +] + +[[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-pyyaml" +version = "6.0.12.20240917" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/7d/a95df0a11f95c8f48d7683f03e4aed1a2c0fc73e9de15cca4d38034bea1a/types-PyYAML-6.0.12.20240917.tar.gz", hash = "sha256:d1405a86f9576682234ef83bcb4e6fff7c9305c8b1fbad5e0bcd4f7dbdc9c587", size = 12381 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/2c/c1d81d680997d24b0542aa336f0a65bd7835e5224b7670f33a7d617da379/types_PyYAML-6.0.12.20240917-py3-none-any.whl", hash = "sha256:392b267f1c0fe6022952462bf5d6523f31e37f6cea49b14cee7ad634b6301570", size = 15264 }, +] + +[[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 = "2.2.3" +source = { registry = "https://pypi.org/simple" } +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 = "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'" }, +] +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 }, + { url = "https://files.pythonhosted.org/packages/b5/7b/85a2c8231eac451ef9caecba8715295820c9f94fb51c4f5b2e39c79a5c11/uvloop-0.21.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414", size = 1433814 }, + { url = "https://files.pythonhosted.org/packages/78/c9/10272e791562be6cfc4ee127883087de6443fede8f010b019ca0fdf841c1/uvloop-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206", size = 797954 }, + { url = "https://files.pythonhosted.org/packages/62/23/29da7a6d3fba8dfe375ea48a8c3a3e5562b770d24008d79a7a6e0150d7c1/uvloop-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe", size = 4302867 }, + { url = "https://files.pythonhosted.org/packages/9a/46/72fb3fbb457cd68632542ecc7fa191a17dac501f70b7f3786a18912bbe0e/uvloop-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79", size = 4303228 }, + { url = "https://files.pythonhosted.org/packages/1a/80/3f57f2458460501b709aec7c7e7f303b81b38ca35f786b41bf402b3349e8/uvloop-0.21.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a", size = 4163776 }, + { url = "https://files.pythonhosted.org/packages/4f/9f/07c88dd3e76171e7808ff63719af12ee8bb6ea56fe40ea274da606ae5ade/uvloop-0.21.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc", size = 4292873 }, + { url = "https://files.pythonhosted.org/packages/3c/a4/646a9d0edff7cde25fc1734695d3dfcee0501140dd0e723e4df3f0a50acb/uvloop-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b", size = 1439646 }, + { url = "https://files.pythonhosted.org/packages/01/2e/e128c66106af9728f86ebfeeb52af27ecd3cb09336f3e2f3e06053707a15/uvloop-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2", size = 800931 }, + { url = "https://files.pythonhosted.org/packages/2d/1a/9fbc2b1543d0df11f7aed1632f64bdf5ecc4053cf98cdc9edb91a65494f9/uvloop-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0", size = 3829660 }, + { url = "https://files.pythonhosted.org/packages/b8/c0/392e235e4100ae3b95b5c6dac77f82b529d2760942b1e7e0981e5d8e895d/uvloop-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75", size = 3827185 }, + { url = "https://files.pythonhosted.org/packages/e1/24/a5da6aba58f99aed5255eca87d58d1760853e8302d390820cc29058408e3/uvloop-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd", size = 3705833 }, + { url = "https://files.pythonhosted.org/packages/1a/5c/6ba221bb60f1e6474474102e17e38612ec7a06dc320e22b687ab563d877f/uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff", size = 3804696 }, +] + +[[package]] +name = "valkey" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/f7/b552b7a67017e6233cd8a3b783ce8c4b548e29df98daedd7fb4c4c2cc8f8/valkey-6.0.2.tar.gz", hash = "sha256:dc2e91512b82d1da0b91ab0cdbd8c97c0c0250281728cb32f9398760df9caeae", size = 4602149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/cb/b1eac0fe9cbdbba0a5cf189f5778fe54ba7d7c9f26c2f62ca8d759b38f52/valkey-6.0.2-py3-none-any.whl", hash = "sha256:dbbdd65439ee0dc5689502c54f1899504cc7268e85cb7fe8935f062178ff5805", size = 260101 }, +] + +[package.optional-dependencies] +libvalkey = [ + { name = "libvalkey" }, +] + +[[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 = "watchfiles" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/27/2ba23c8cc85796e2d41976439b08d52f691655fdb9401362099502d1f0cf/watchfiles-0.24.0.tar.gz", hash = "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1", size = 37870 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/a1/631c12626378b9f1538664aa221feb5c60dfafbd7f60b451f8d0bdbcdedd/watchfiles-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:083dc77dbdeef09fa44bb0f4d1df571d2e12d8a8f985dccde71ac3ac9ac067a0", size = 375096 }, + { url = "https://files.pythonhosted.org/packages/f7/5c/f27c979c8a10aaa2822286c1bffdce3db731cd1aa4224b9f86623e94bbfe/watchfiles-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e94e98c7cb94cfa6e071d401ea3342767f28eb5a06a58fafdc0d2a4974f4f35c", size = 367425 }, + { url = "https://files.pythonhosted.org/packages/74/0d/1889e5649885484d29f6c792ef274454d0a26b20d6ed5fdba5409335ccb6/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82ae557a8c037c42a6ef26c494d0631cacca040934b101d001100ed93d43f361", size = 437705 }, + { url = "https://files.pythonhosted.org/packages/85/8a/01d9a22e839f0d1d547af11b1fcac6ba6f889513f1b2e6f221d9d60d9585/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:acbfa31e315a8f14fe33e3542cbcafc55703b8f5dcbb7c1eecd30f141df50db3", size = 433636 }, + { url = "https://files.pythonhosted.org/packages/62/32/a93db78d340c7ef86cde469deb20e36c6b2a873edee81f610e94bbba4e06/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b74fdffce9dfcf2dc296dec8743e5b0332d15df19ae464f0e249aa871fc1c571", size = 451069 }, + { url = "https://files.pythonhosted.org/packages/99/c2/e9e2754fae3c2721c9a7736f92dab73723f1968ed72535fff29e70776008/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:449f43f49c8ddca87c6b3980c9284cab6bd1f5c9d9a2b00012adaaccd5e7decd", size = 469306 }, + { url = "https://files.pythonhosted.org/packages/4c/45/f317d9e3affb06c3c27c478de99f7110143e87f0f001f0f72e18d0e1ddce/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4abf4ad269856618f82dee296ac66b0cd1d71450fc3c98532d93798e73399b7a", size = 476187 }, + { url = "https://files.pythonhosted.org/packages/ac/d3/f1f37248abe0114916921e638f71c7d21fe77e3f2f61750e8057d0b68ef2/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f895d785eb6164678ff4bb5cc60c5996b3ee6df3edb28dcdeba86a13ea0465e", size = 425743 }, + { url = "https://files.pythonhosted.org/packages/2b/e8/c7037ea38d838fd81a59cd25761f106ee3ef2cfd3261787bee0c68908171/watchfiles-0.24.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7ae3e208b31be8ce7f4c2c0034f33406dd24fbce3467f77223d10cd86778471c", size = 612327 }, + { url = "https://files.pythonhosted.org/packages/a0/c5/0e6e228aafe01a7995fbfd2a4edb221bb11a2744803b65a5663fb85e5063/watchfiles-0.24.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2efec17819b0046dde35d13fb8ac7a3ad877af41ae4640f4109d9154ed30a188", size = 595096 }, + { url = "https://files.pythonhosted.org/packages/63/d5/4780e8bf3de3b4b46e7428a29654f7dc041cad6b19fd86d083e4b6f64bbe/watchfiles-0.24.0-cp310-none-win32.whl", hash = "sha256:6bdcfa3cd6fdbdd1a068a52820f46a815401cbc2cb187dd006cb076675e7b735", size = 264149 }, + { url = "https://files.pythonhosted.org/packages/fe/1b/5148898ba55fc9c111a2a4a5fb67ad3fa7eb2b3d7f0618241ed88749313d/watchfiles-0.24.0-cp310-none-win_amd64.whl", hash = "sha256:54ca90a9ae6597ae6dc00e7ed0a040ef723f84ec517d3e7ce13e63e4bc82fa04", size = 277542 }, + { url = "https://files.pythonhosted.org/packages/85/02/366ae902cd81ca5befcd1854b5c7477b378f68861597cef854bd6dc69fbe/watchfiles-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428", size = 375579 }, + { url = "https://files.pythonhosted.org/packages/bc/67/d8c9d256791fe312fea118a8a051411337c948101a24586e2df237507976/watchfiles-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c", size = 367726 }, + { url = "https://files.pythonhosted.org/packages/b1/dc/a8427b21ef46386adf824a9fec4be9d16a475b850616cfd98cf09a97a2ef/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43", size = 437735 }, + { url = "https://files.pythonhosted.org/packages/3a/21/0b20bef581a9fbfef290a822c8be645432ceb05fb0741bf3c032e0d90d9a/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327", size = 433644 }, + { url = "https://files.pythonhosted.org/packages/1c/e8/d5e5f71cc443c85a72e70b24269a30e529227986096abe091040d6358ea9/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5", size = 450928 }, + { url = "https://files.pythonhosted.org/packages/61/ee/bf17f5a370c2fcff49e1fec987a6a43fd798d8427ea754ce45b38f9e117a/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61", size = 469072 }, + { url = "https://files.pythonhosted.org/packages/a3/34/03b66d425986de3fc6077e74a74c78da298f8cb598887f664a4485e55543/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15", size = 475517 }, + { url = "https://files.pythonhosted.org/packages/70/eb/82f089c4f44b3171ad87a1b433abb4696f18eb67292909630d886e073abe/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823", size = 425480 }, + { url = "https://files.pythonhosted.org/packages/53/20/20509c8f5291e14e8a13104b1808cd7cf5c44acd5feaecb427a49d387774/watchfiles-0.24.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab", size = 612322 }, + { url = "https://files.pythonhosted.org/packages/df/2b/5f65014a8cecc0a120f5587722068a975a692cadbe9fe4ea56b3d8e43f14/watchfiles-0.24.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec", size = 595094 }, + { url = "https://files.pythonhosted.org/packages/18/98/006d8043a82c0a09d282d669c88e587b3a05cabdd7f4900e402250a249ac/watchfiles-0.24.0-cp311-none-win32.whl", hash = "sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d", size = 264191 }, + { url = "https://files.pythonhosted.org/packages/8a/8b/badd9247d6ec25f5f634a9b3d0d92e39c045824ec7e8afcedca8ee52c1e2/watchfiles-0.24.0-cp311-none-win_amd64.whl", hash = "sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c", size = 277527 }, + { url = "https://files.pythonhosted.org/packages/af/19/35c957c84ee69d904299a38bae3614f7cede45f07f174f6d5a2f4dbd6033/watchfiles-0.24.0-cp311-none-win_arm64.whl", hash = "sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633", size = 266253 }, + { url = "https://files.pythonhosted.org/packages/35/82/92a7bb6dc82d183e304a5f84ae5437b59ee72d48cee805a9adda2488b237/watchfiles-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a", size = 374137 }, + { url = "https://files.pythonhosted.org/packages/87/91/49e9a497ddaf4da5e3802d51ed67ff33024597c28f652b8ab1e7c0f5718b/watchfiles-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370", size = 367733 }, + { url = "https://files.pythonhosted.org/packages/0d/d8/90eb950ab4998effea2df4cf3a705dc594f6bc501c5a353073aa990be965/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6", size = 437322 }, + { url = "https://files.pythonhosted.org/packages/6c/a2/300b22e7bc2a222dd91fce121cefa7b49aa0d26a627b2777e7bdfcf1110b/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b", size = 433409 }, + { url = "https://files.pythonhosted.org/packages/99/44/27d7708a43538ed6c26708bcccdde757da8b7efb93f4871d4cc39cffa1cc/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e", size = 452142 }, + { url = "https://files.pythonhosted.org/packages/b0/ec/c4e04f755be003129a2c5f3520d2c47026f00da5ecb9ef1e4f9449637571/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea", size = 469414 }, + { url = "https://files.pythonhosted.org/packages/c5/4e/cdd7de3e7ac6432b0abf282ec4c1a1a2ec62dfe423cf269b86861667752d/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f", size = 472962 }, + { url = "https://files.pythonhosted.org/packages/27/69/e1da9d34da7fc59db358424f5d89a56aaafe09f6961b64e36457a80a7194/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234", size = 425705 }, + { url = "https://files.pythonhosted.org/packages/e8/c1/24d0f7357be89be4a43e0a656259676ea3d7a074901f47022f32e2957798/watchfiles-0.24.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef", size = 612851 }, + { url = "https://files.pythonhosted.org/packages/c7/af/175ba9b268dec56f821639c9893b506c69fd999fe6a2e2c51de420eb2f01/watchfiles-0.24.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968", size = 594868 }, + { url = "https://files.pythonhosted.org/packages/44/81/1f701323a9f70805bc81c74c990137123344a80ea23ab9504a99492907f8/watchfiles-0.24.0-cp312-none-win32.whl", hash = "sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444", size = 264109 }, + { url = "https://files.pythonhosted.org/packages/b4/0b/32cde5bc2ebd9f351be326837c61bdeb05ad652b793f25c91cac0b48a60b/watchfiles-0.24.0-cp312-none-win_amd64.whl", hash = "sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896", size = 277055 }, + { url = "https://files.pythonhosted.org/packages/4b/81/daade76ce33d21dbec7a15afd7479de8db786e5f7b7d249263b4ea174e08/watchfiles-0.24.0-cp312-none-win_arm64.whl", hash = "sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418", size = 266169 }, + { url = "https://files.pythonhosted.org/packages/30/dc/6e9f5447ae14f645532468a84323a942996d74d5e817837a5c8ce9d16c69/watchfiles-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48", size = 373764 }, + { url = "https://files.pythonhosted.org/packages/79/c0/c3a9929c372816c7fc87d8149bd722608ea58dc0986d3ef7564c79ad7112/watchfiles-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90", size = 367873 }, + { url = "https://files.pythonhosted.org/packages/2e/11/ff9a4445a7cfc1c98caf99042df38964af12eed47d496dd5d0d90417349f/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94", size = 438381 }, + { url = "https://files.pythonhosted.org/packages/48/a3/763ba18c98211d7bb6c0f417b2d7946d346cdc359d585cc28a17b48e964b/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e", size = 432809 }, + { url = "https://files.pythonhosted.org/packages/30/4c/616c111b9d40eea2547489abaf4ffc84511e86888a166d3a4522c2ba44b5/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827", size = 451801 }, + { url = "https://files.pythonhosted.org/packages/b6/be/d7da83307863a422abbfeb12903a76e43200c90ebe5d6afd6a59d158edea/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df", size = 468886 }, + { url = "https://files.pythonhosted.org/packages/1d/d3/3dfe131ee59d5e90b932cf56aba5c996309d94dafe3d02d204364c23461c/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab", size = 472973 }, + { url = "https://files.pythonhosted.org/packages/42/6c/279288cc5653a289290d183b60a6d80e05f439d5bfdfaf2d113738d0f932/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f", size = 425282 }, + { url = "https://files.pythonhosted.org/packages/d6/d7/58afe5e85217e845edf26d8780c2d2d2ae77675eeb8d1b8b8121d799ce52/watchfiles-0.24.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b", size = 612540 }, + { url = "https://files.pythonhosted.org/packages/6d/d5/b96eeb9fe3fda137200dd2f31553670cbc731b1e13164fd69b49870b76ec/watchfiles-0.24.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18", size = 593625 }, + { url = "https://files.pythonhosted.org/packages/c1/e5/c326fe52ee0054107267608d8cea275e80be4455b6079491dfd9da29f46f/watchfiles-0.24.0-cp313-none-win32.whl", hash = "sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07", size = 263899 }, + { url = "https://files.pythonhosted.org/packages/a6/8b/8a7755c5e7221bb35fe4af2dc44db9174f90ebf0344fd5e9b1e8b42d381e/watchfiles-0.24.0-cp313-none-win_amd64.whl", hash = "sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366", size = 276622 }, + { url = "https://files.pythonhosted.org/packages/17/1c/c0b5f4347011b60e2dbde671a5050944f3aaf0eb2ffc0fb5c7adf2516229/watchfiles-0.24.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ee82c98bed9d97cd2f53bdb035e619309a098ea53ce525833e26b93f673bc318", size = 375982 }, + { url = "https://files.pythonhosted.org/packages/c5/b2/d417b982be5ace395f1aad32cd8e0dcf194e431dfbfeee88941b6da6735a/watchfiles-0.24.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fd92bbaa2ecdb7864b7600dcdb6f2f1db6e0346ed425fbd01085be04c63f0b05", size = 369757 }, + { url = "https://files.pythonhosted.org/packages/68/27/f3a1147af79085da95a879d7e6f953380da17a90b50d990749ae287550ca/watchfiles-0.24.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f83df90191d67af5a831da3a33dd7628b02a95450e168785586ed51e6d28943c", size = 439397 }, + { url = "https://files.pythonhosted.org/packages/31/de/4a677766880efee555cc56a4c6bf6993a7748901243cd2511419acc37a6c/watchfiles-0.24.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fca9433a45f18b7c779d2bae7beeec4f740d28b788b117a48368d95a3233ed83", size = 433878 }, + { url = "https://files.pythonhosted.org/packages/f4/7f/30fbf661dea01cf3d5ab4962ee4b52854b9fbbd7fe4918bc60c212bb5b60/watchfiles-0.24.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b995bfa6bf01a9e09b884077a6d37070464b529d8682d7691c2d3b540d357a0c", size = 451495 }, + { url = "https://files.pythonhosted.org/packages/c8/65/cda4b9ed13087d57f78ed386c4bdc2369c114dd871a32fa6e2419f6e81b8/watchfiles-0.24.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed9aba6e01ff6f2e8285e5aa4154e2970068fe0fc0998c4380d0e6278222269b", size = 470115 }, + { url = "https://files.pythonhosted.org/packages/4c/72/9b2ba3bb3a7233fb3d21900cd3f9005cfaa53884f496239541ec885b9861/watchfiles-0.24.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5171ef898299c657685306d8e1478a45e9303ddcd8ac5fed5bd52ad4ae0b69b", size = 476814 }, + { url = "https://files.pythonhosted.org/packages/3d/78/90a881916a4a3bafd5e82202d51406444d3720cfc03b326427a8e455d89d/watchfiles-0.24.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4933a508d2f78099162da473841c652ad0de892719043d3f07cc83b33dfd9d91", size = 426747 }, + { url = "https://files.pythonhosted.org/packages/56/52/5a2c6e0694013a53f596d4a66642de48dc1a53a635ad61bc6504abf5c87c/watchfiles-0.24.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95cf3b95ea665ab03f5a54765fa41abf0529dbaf372c3b83d91ad2cfa695779b", size = 613135 }, + { url = "https://files.pythonhosted.org/packages/fc/dc/8f177e6074e756d961d5dcb5ef65528b02ab09028d0380db4579a30af78f/watchfiles-0.24.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:01def80eb62bd5db99a798d5e1f5f940ca0a05986dcfae21d833af7a46f7ee22", size = 596318 }, + { url = "https://files.pythonhosted.org/packages/e6/16/d89e06188ed6672dc69eeef7af2ea158328bd59d45032a94daaaed2a6516/watchfiles-0.24.0-cp38-none-win32.whl", hash = "sha256:4d28cea3c976499475f5b7a2fec6b3a36208656963c1a856d328aeae056fc5c1", size = 264384 }, + { url = "https://files.pythonhosted.org/packages/e6/cc/2f2f27fc403193bedaaae5485cd04fd31064f5cdec885162bd0e390be68a/watchfiles-0.24.0-cp38-none-win_amd64.whl", hash = "sha256:21ab23fdc1208086d99ad3f69c231ba265628014d4aed31d4e8746bd59e88cd1", size = 277740 }, + { url = "https://files.pythonhosted.org/packages/93/90/15b3b1cc19799c217e4369ca15dbf9ba1d0f821d61b27173a54800002478/watchfiles-0.24.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b665caeeda58625c3946ad7308fbd88a086ee51ccb706307e5b1fa91556ac886", size = 375920 }, + { url = "https://files.pythonhosted.org/packages/74/e2/ec7766a5b20ba5e37397ef8d32ff39a90daf7e4677410efe077c386338b6/watchfiles-0.24.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5c51749f3e4e269231510da426ce4a44beb98db2dce9097225c338f815b05d4f", size = 369382 }, + { url = "https://files.pythonhosted.org/packages/a2/82/915b3a3295292f860181dc9c7d922834ac7265c8915052d396d40ccf4617/watchfiles-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b2509f08761f29a0fdad35f7e1638b8ab1adfa2666d41b794090361fb8b855", size = 438556 }, + { url = "https://files.pythonhosted.org/packages/9b/76/750eab8e7baecedca05e712b9571ac5eb240e494e8d4c78b359da2939619/watchfiles-0.24.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a60e2bf9dc6afe7f743e7c9b149d1fdd6dbf35153c78fe3a14ae1a9aee3d98b", size = 433677 }, + { url = "https://files.pythonhosted.org/packages/1a/69/7c55142bafcdad9fec58433b7c1f162b59c48f0a3732a9441252147b13c7/watchfiles-0.24.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7d9b87c4c55e3ea8881dfcbf6d61ea6775fffed1fedffaa60bd047d3c08c430", size = 451845 }, + { url = "https://files.pythonhosted.org/packages/d7/a6/124b0043a8936adf96fffa1d73217b205cc254b4a5a313b0a6ea33a44b7b/watchfiles-0.24.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78470906a6be5199524641f538bd2c56bb809cd4bf29a566a75051610bc982c3", size = 469931 }, + { url = "https://files.pythonhosted.org/packages/7a/14/29ffa6c7a695fb46a7ff835a5524976dbc07577215647fb35a61ea099c75/watchfiles-0.24.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07cdef0c84c03375f4e24642ef8d8178e533596b229d32d2bbd69e5128ede02a", size = 476577 }, + { url = "https://files.pythonhosted.org/packages/9d/8b/fea47dd852c644bd933108877293d0a1c56953c584e8b971530a9042c153/watchfiles-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d337193bbf3e45171c8025e291530fb7548a93c45253897cd764a6a71c937ed9", size = 426432 }, + { url = "https://files.pythonhosted.org/packages/0e/11/cfa073f1d9fa18867c2b4220ba445044fd48101ac481f8cbfea1c208ea88/watchfiles-0.24.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ec39698c45b11d9694a1b635a70946a5bad066b593af863460a8e600f0dff1ca", size = 613173 }, + { url = "https://files.pythonhosted.org/packages/77/44/05c8959304f96fbcd68b6c131c59df7bd3d7f0c2a7410324b7f63b1f9fe6/watchfiles-0.24.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e28d91ef48eab0afb939fa446d8ebe77e2f7593f5f463fd2bb2b14132f95b6e", size = 596184 }, + { url = "https://files.pythonhosted.org/packages/9b/85/033ecdb5eccb77770d6f24f9fa055067ffa962313a1383645afc711a3cd8/watchfiles-0.24.0-cp39-none-win32.whl", hash = "sha256:7138eff8baa883aeaa074359daabb8b6c1e73ffe69d5accdc907d62e50b1c0da", size = 264345 }, + { url = "https://files.pythonhosted.org/packages/16/6e/5ded97365346eceaf7fa32d4e2d16f4f97b11d648026b2903c2528c544f8/watchfiles-0.24.0-cp39-none-win_amd64.whl", hash = "sha256:b3ef2c69c655db63deb96b3c3e587084612f9b1fa983df5e0c3379d41307467f", size = 277760 }, + { url = "https://files.pythonhosted.org/packages/df/94/1ad200e937ec91b2a9d6b39ae1cf9c2b1a9cc88d5ceb43aa5c6962eb3c11/watchfiles-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:632676574429bee8c26be8af52af20e0c718cc7f5f67f3fb658c71928ccd4f7f", size = 376986 }, + { url = "https://files.pythonhosted.org/packages/ee/fd/d9e020d687ccf90fe95efc513fbb39a8049cf5a3ff51f53c59fcf4c47a5d/watchfiles-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a2a9891723a735d3e2540651184be6fd5b96880c08ffe1a98bae5017e65b544b", size = 369445 }, + { url = "https://files.pythonhosted.org/packages/43/cb/c0279b35053555d10ef03559c5aebfcb0c703d9c70a7b4e532df74b9b0e8/watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7fa2bc0efef3e209a8199fd111b8969fe9db9c711acc46636686331eda7dd4", size = 439383 }, + { url = "https://files.pythonhosted.org/packages/8b/c4/08b3c2cda45db5169148a981c2100c744a4a222fa7ae7644937c0c002069/watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01550ccf1d0aed6ea375ef259706af76ad009ef5b0203a3a4cce0f6024f9b68a", size = 426804 }, + { url = "https://files.pythonhosted.org/packages/02/81/9c9a1e6a83d3c320d2f89eca2b71854ae727eca8ab298ad5da00e36c69e2/watchfiles-0.24.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:96619302d4374de5e2345b2b622dc481257a99431277662c30f606f3e22f42be", size = 377076 }, + { url = "https://files.pythonhosted.org/packages/f1/05/9ef4158f15c417a420d907a6eb887c81953565d0268262195766a844a6d9/watchfiles-0.24.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:85d5f0c7771dcc7a26c7a27145059b6bb0ce06e4e751ed76cdf123d7039b60b5", size = 371717 }, + { url = "https://files.pythonhosted.org/packages/81/1b/8d036da7a9e4d490385b6979804229fb7ac9b591e4d84f159edf2b3f7cc2/watchfiles-0.24.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951088d12d339690a92cef2ec5d3cfd957692834c72ffd570ea76a6790222777", size = 440973 }, + { url = "https://files.pythonhosted.org/packages/fb/fc/885015d4a17ada85508e406c10d638808e7bfbb5622a2e342c868ede18c0/watchfiles-0.24.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fb58bcaa343fedc6a9e91f90195b20ccb3135447dc9e4e2570c3a39565853e", size = 428343 }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774 }, +] + +[[package]] +name = "websockets" +version = "13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/73/9223dbc7be3dcaf2a7bbf756c351ec8da04b1fa573edaf545b95f6b0c7fd/websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878", size = 158549 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/94/d15dbfc6a5eb636dbc754303fba18208f2e88cf97e733e1d64fb9cb5c89e/websockets-13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee", size = 157815 }, + { url = "https://files.pythonhosted.org/packages/30/02/c04af33f4663945a26f5e8cf561eb140c35452b50af47a83c3fbcfe62ae1/websockets-13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7", size = 155466 }, + { url = "https://files.pythonhosted.org/packages/35/e8/719f08d12303ea643655e52d9e9851b2dadbb1991d4926d9ce8862efa2f5/websockets-13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f779498eeec470295a2b1a5d97aa1bc9814ecd25e1eb637bd9d1c73a327387f6", size = 155716 }, + { url = "https://files.pythonhosted.org/packages/91/e1/14963ae0252a8925f7434065d25dcd4701d5e281a0b4b460a3b5963d2594/websockets-13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676df3fe46956fbb0437d8800cd5f2b6d41143b6e7e842e60554398432cf29b", size = 164806 }, + { url = "https://files.pythonhosted.org/packages/ec/fa/ab28441bae5e682a0f7ddf3d03440c0c352f930da419301f4a717f675ef3/websockets-13.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7affedeb43a70351bb811dadf49493c9cfd1ed94c9c70095fd177e9cc1541fa", size = 163810 }, + { url = "https://files.pythonhosted.org/packages/44/77/dea187bd9d16d4b91566a2832be31f99a40d0f5bfa55eeb638eb2c3bc33d/websockets-13.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1971e62d2caa443e57588e1d82d15f663b29ff9dfe7446d9964a4b6f12c1e700", size = 164125 }, + { url = "https://files.pythonhosted.org/packages/cf/d9/3af14544e83f1437eb684b399e6ba0fa769438e869bf5d83d74bc197fae8/websockets-13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5f2e75431f8dc4a47f31565a6e1355fb4f2ecaa99d6b89737527ea917066e26c", size = 164532 }, + { url = "https://files.pythonhosted.org/packages/1c/8a/6d332eabe7d59dfefe4b8ba6f46c8c5fabb15b71c8a8bc3d2b65de19a7b6/websockets-13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58cf7e75dbf7e566088b07e36ea2e3e2bd5676e22216e4cad108d4df4a7402a0", size = 163948 }, + { url = "https://files.pythonhosted.org/packages/1a/91/a0aeadbaf3017467a1ee03f8fb67accdae233fe2d5ad4b038c0a84e357b0/websockets-13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c90d6dec6be2c7d03378a574de87af9b1efea77d0c52a8301dd831ece938452f", size = 163898 }, + { url = "https://files.pythonhosted.org/packages/71/31/a90fb47c63e0ae605be914b0b969d7c6e6ffe2038cd744798e4b3fbce53b/websockets-13.1-cp310-cp310-win32.whl", hash = "sha256:730f42125ccb14602f455155084f978bd9e8e57e89b569b4d7f0f0c17a448ffe", size = 158706 }, + { url = "https://files.pythonhosted.org/packages/93/ca/9540a9ba80da04dc7f36d790c30cae4252589dbd52ccdc92e75b0be22437/websockets-13.1-cp310-cp310-win_amd64.whl", hash = "sha256:5993260f483d05a9737073be197371940c01b257cc45ae3f1d5d7adb371b266a", size = 159141 }, + { url = "https://files.pythonhosted.org/packages/b2/f0/cf0b8a30d86b49e267ac84addbebbc7a48a6e7bb7c19db80f62411452311/websockets-13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:61fc0dfcda609cda0fc9fe7977694c0c59cf9d749fbb17f4e9483929e3c48a19", size = 157813 }, + { url = "https://files.pythonhosted.org/packages/bf/e7/22285852502e33071a8cf0ac814f8988480ec6db4754e067b8b9d0e92498/websockets-13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ceec59f59d092c5007e815def4ebb80c2de330e9588e101cf8bd94c143ec78a5", size = 155469 }, + { url = "https://files.pythonhosted.org/packages/68/d4/c8c7c1e5b40ee03c5cc235955b0fb1ec90e7e37685a5f69229ad4708dcde/websockets-13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1dca61c6db1166c48b95198c0b7d9c990b30c756fc2923cc66f68d17dc558fd", size = 155717 }, + { url = "https://files.pythonhosted.org/packages/c9/e4/c50999b9b848b1332b07c7fd8886179ac395cb766fda62725d1539e7bc6c/websockets-13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308e20f22c2c77f3f39caca508e765f8725020b84aa963474e18c59accbf4c02", size = 165379 }, + { url = "https://files.pythonhosted.org/packages/bc/49/4a4ad8c072f18fd79ab127650e47b160571aacfc30b110ee305ba25fffc9/websockets-13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d516c325e6540e8a57b94abefc3459d7dab8ce52ac75c96cad5549e187e3a7", size = 164376 }, + { url = "https://files.pythonhosted.org/packages/af/9b/8c06d425a1d5a74fd764dd793edd02be18cf6fc3b1ccd1f29244ba132dc0/websockets-13.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c6e35319b46b99e168eb98472d6c7d8634ee37750d7693656dc766395df096", size = 164753 }, + { url = "https://files.pythonhosted.org/packages/d5/5b/0acb5815095ff800b579ffc38b13ab1b915b317915023748812d24e0c1ac/websockets-13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f9fee94ebafbc3117c30be1844ed01a3b177bb6e39088bc6b2fa1dc15572084", size = 165051 }, + { url = "https://files.pythonhosted.org/packages/30/93/c3891c20114eacb1af09dedfcc620c65c397f4fd80a7009cd12d9457f7f5/websockets-13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7c1e90228c2f5cdde263253fa5db63e6653f1c00e7ec64108065a0b9713fa1b3", size = 164489 }, + { url = "https://files.pythonhosted.org/packages/28/09/af9e19885539759efa2e2cd29b8b3f9eecef7ecefea40d46612f12138b36/websockets-13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6548f29b0e401eea2b967b2fdc1c7c7b5ebb3eeb470ed23a54cd45ef078a0db9", size = 164438 }, + { url = "https://files.pythonhosted.org/packages/b6/08/6f38b8e625b3d93de731f1d248cc1493327f16cb45b9645b3e791782cff0/websockets-13.1-cp311-cp311-win32.whl", hash = "sha256:c11d4d16e133f6df8916cc5b7e3e96ee4c44c936717d684a94f48f82edb7c92f", size = 158710 }, + { url = "https://files.pythonhosted.org/packages/fb/39/ec8832ecb9bb04a8d318149005ed8cee0ba4e0205835da99e0aa497a091f/websockets-13.1-cp311-cp311-win_amd64.whl", hash = "sha256:d04f13a1d75cb2b8382bdc16ae6fa58c97337253826dfe136195b7f89f661557", size = 159137 }, + { url = "https://files.pythonhosted.org/packages/df/46/c426282f543b3c0296cf964aa5a7bb17e984f58dde23460c3d39b3148fcf/websockets-13.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc", size = 157821 }, + { url = "https://files.pythonhosted.org/packages/aa/85/22529867010baac258da7c45848f9415e6cf37fef00a43856627806ffd04/websockets-13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49", size = 155480 }, + { url = "https://files.pythonhosted.org/packages/29/2c/bdb339bfbde0119a6e84af43ebf6275278698a2241c2719afc0d8b0bdbf2/websockets-13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd", size = 155715 }, + { url = "https://files.pythonhosted.org/packages/9f/d0/8612029ea04c5c22bf7af2fd3d63876c4eaeef9b97e86c11972a43aa0e6c/websockets-13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1b54689e38d1279a51d11e3467dd2f3a50f5f2e879012ce8f2d6943f00e83f0", size = 165647 }, + { url = "https://files.pythonhosted.org/packages/56/04/1681ed516fa19ca9083f26d3f3a302257e0911ba75009533ed60fbb7b8d1/websockets-13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf1781ef73c073e6b0f90af841aaf98501f975d306bbf6221683dd594ccc52b6", size = 164592 }, + { url = "https://files.pythonhosted.org/packages/38/6f/a96417a49c0ed132bb6087e8e39a37db851c70974f5c724a4b2a70066996/websockets-13.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d23b88b9388ed85c6faf0e74d8dec4f4d3baf3ecf20a65a47b836d56260d4b9", size = 165012 }, + { url = "https://files.pythonhosted.org/packages/40/8b/fccf294919a1b37d190e86042e1a907b8f66cff2b61e9befdbce03783e25/websockets-13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3c78383585f47ccb0fcf186dcb8a43f5438bd7d8f47d69e0b56f71bf431a0a68", size = 165311 }, + { url = "https://files.pythonhosted.org/packages/c1/61/f8615cf7ce5fe538476ab6b4defff52beb7262ff8a73d5ef386322d9761d/websockets-13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d6d300f8ec35c24025ceb9b9019ae9040c1ab2f01cddc2bcc0b518af31c75c14", size = 164692 }, + { url = "https://files.pythonhosted.org/packages/5c/f1/a29dd6046d3a722d26f182b783a7997d25298873a14028c4760347974ea3/websockets-13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9dcaf8b0cc72a392760bb8755922c03e17a5a54e08cca58e8b74f6902b433cf", size = 164686 }, + { url = "https://files.pythonhosted.org/packages/0f/99/ab1cdb282f7e595391226f03f9b498f52109d25a2ba03832e21614967dfa/websockets-13.1-cp312-cp312-win32.whl", hash = "sha256:2f85cf4f2a1ba8f602298a853cec8526c2ca42a9a4b947ec236eaedb8f2dc80c", size = 158712 }, + { url = "https://files.pythonhosted.org/packages/46/93/e19160db48b5581feac8468330aa11b7292880a94a37d7030478596cc14e/websockets-13.1-cp312-cp312-win_amd64.whl", hash = "sha256:38377f8b0cdeee97c552d20cf1865695fcd56aba155ad1b4ca8779a5b6ef4ac3", size = 159145 }, + { url = "https://files.pythonhosted.org/packages/51/20/2b99ca918e1cbd33c53db2cace5f0c0cd8296fc77558e1908799c712e1cd/websockets-13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6", size = 157828 }, + { url = "https://files.pythonhosted.org/packages/b8/47/0932a71d3d9c0e9483174f60713c84cee58d62839a143f21a2bcdbd2d205/websockets-13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708", size = 155487 }, + { url = "https://files.pythonhosted.org/packages/a9/60/f1711eb59ac7a6c5e98e5637fef5302f45b6f76a2c9d64fd83bbb341377a/websockets-13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418", size = 155721 }, + { url = "https://files.pythonhosted.org/packages/6a/e6/ba9a8db7f9d9b0e5f829cf626ff32677f39824968317223605a6b419d445/websockets-13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a", size = 165609 }, + { url = "https://files.pythonhosted.org/packages/c1/22/4ec80f1b9c27a0aebd84ccd857252eda8418ab9681eb571b37ca4c5e1305/websockets-13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f", size = 164556 }, + { url = "https://files.pythonhosted.org/packages/27/ac/35f423cb6bb15600438db80755609d27eda36d4c0b3c9d745ea12766c45e/websockets-13.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5", size = 164993 }, + { url = "https://files.pythonhosted.org/packages/31/4e/98db4fd267f8be9e52e86b6ee4e9aa7c42b83452ea0ea0672f176224b977/websockets-13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135", size = 165360 }, + { url = "https://files.pythonhosted.org/packages/3f/15/3f0de7cda70ffc94b7e7024544072bc5b26e2c1eb36545291abb755d8cdb/websockets-13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2", size = 164745 }, + { url = "https://files.pythonhosted.org/packages/a1/6e/66b6b756aebbd680b934c8bdbb6dcb9ce45aad72cde5f8a7208dbb00dd36/websockets-13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6", size = 164732 }, + { url = "https://files.pythonhosted.org/packages/35/c6/12e3aab52c11aeb289e3dbbc05929e7a9d90d7a9173958477d3ef4f8ce2d/websockets-13.1-cp313-cp313-win32.whl", hash = "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d", size = 158709 }, + { url = "https://files.pythonhosted.org/packages/41/d8/63d6194aae711d7263df4498200c690a9c39fb437ede10f3e157a6343e0d/websockets-13.1-cp313-cp313-win_amd64.whl", hash = "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2", size = 159144 }, + { url = "https://files.pythonhosted.org/packages/83/69/59872420e5bce60db166d6fba39ee24c719d339fb0ae48cb2ce580129882/websockets-13.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c7934fd0e920e70468e676fe7f1b7261c1efa0d6c037c6722278ca0228ad9d0d", size = 157811 }, + { url = "https://files.pythonhosted.org/packages/bb/f7/0610032e0d3981758fdd6ee7c68cc02ebf668a762c5178d3d91748228849/websockets-13.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:149e622dc48c10ccc3d2760e5f36753db9cacf3ad7bc7bbbfd7d9c819e286f23", size = 155471 }, + { url = "https://files.pythonhosted.org/packages/55/2f/c43173a72ea395263a427a36d25bce2675f41c809424466a13c61a9a2d61/websockets-13.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a569eb1b05d72f9bce2ebd28a1ce2054311b66677fcd46cf36204ad23acead8c", size = 155713 }, + { url = "https://files.pythonhosted.org/packages/92/7e/8fa930c6426a56c47910792717787640329e4a0e37cdfda20cf89da67126/websockets-13.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95df24ca1e1bd93bbca51d94dd049a984609687cb2fb08a7f2c56ac84e9816ea", size = 164995 }, + { url = "https://files.pythonhosted.org/packages/27/29/50ed4c68a3f606565a2db4b13948ae7b6f6c53aa9f8f258d92be6698d276/websockets-13.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8dbb1bf0c0a4ae8b40bdc9be7f644e2f3fb4e8a9aca7145bfa510d4a374eeb7", size = 164057 }, + { url = "https://files.pythonhosted.org/packages/3c/0e/60da63b1c53c47f389f79312b3356cb305600ffad1274d7ec473128d4e6b/websockets-13.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:035233b7531fb92a76beefcbf479504db8c72eb3bff41da55aecce3a0f729e54", size = 164340 }, + { url = "https://files.pythonhosted.org/packages/20/ef/d87c5fc0aa7fafad1d584b6459ddfe062edf0d0dd64800a02e67e5de048b/websockets-13.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:e4450fc83a3df53dec45922b576e91e94f5578d06436871dce3a6be38e40f5db", size = 164222 }, + { url = "https://files.pythonhosted.org/packages/f2/c4/7916e1f6b5252d3dcb9121b67d7fdbb2d9bf5067a6d8c88885ba27a9e69c/websockets-13.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:463e1c6ec853202dd3657f156123d6b4dad0c546ea2e2e38be2b3f7c5b8e7295", size = 163647 }, + { url = "https://files.pythonhosted.org/packages/de/df/2ebebb807f10993c35c10cbd3628a7944b66bd5fb6632a561f8666f3a68e/websockets-13.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6d6855bbe70119872c05107e38fbc7f96b1d8cb047d95c2c50869a46c65a8e96", size = 163590 }, + { url = "https://files.pythonhosted.org/packages/b5/82/d48911f56bb993c11099a1ff1d4041d9d1481d50271100e8ee62bc28f365/websockets-13.1-cp38-cp38-win32.whl", hash = "sha256:204e5107f43095012b00f1451374693267adbb832d29966a01ecc4ce1db26faf", size = 158701 }, + { url = "https://files.pythonhosted.org/packages/8b/b3/945aacb21fc89ad150403cbaa974c9e846f098f16d9f39a3dd6094f9beb1/websockets-13.1-cp38-cp38-win_amd64.whl", hash = "sha256:485307243237328c022bc908b90e4457d0daa8b5cf4b3723fd3c4a8012fce4c6", size = 159146 }, + { url = "https://files.pythonhosted.org/packages/61/26/5f7a7fb03efedb4f90ed61968338bfe7c389863b0ceda239b94ae61c5ae4/websockets-13.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9b37c184f8b976f0c0a231a5f3d6efe10807d41ccbe4488df8c74174805eea7d", size = 157810 }, + { url = "https://files.pythonhosted.org/packages/0e/d4/9b4814a07dffaa7a79d71b4944d10836f9adbd527a113f6675734ef3abed/websockets-13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:163e7277e1a0bd9fb3c8842a71661ad19c6aa7bb3d6678dc7f89b17fbcc4aeb7", size = 155467 }, + { url = "https://files.pythonhosted.org/packages/1a/1a/2abdc7ce3b56429ae39d6bfb48d8c791f5a26bbcb6f44aabcf71ffc3fda2/websockets-13.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b889dbd1342820cc210ba44307cf75ae5f2f96226c0038094455a96e64fb07a", size = 155714 }, + { url = "https://files.pythonhosted.org/packages/2a/98/189d7cf232753a719b2726ec55e7922522632248d5d830adf078e3f612be/websockets-13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:586a356928692c1fed0eca68b4d1c2cbbd1ca2acf2ac7e7ebd3b9052582deefa", size = 164587 }, + { url = "https://files.pythonhosted.org/packages/a5/2b/fb77cedf3f9f55ef8605238c801eef6b9a5269b01a396875a86896aea3a6/websockets-13.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7bd6abf1e070a6b72bfeb71049d6ad286852e285f146682bf30d0296f5fbadfa", size = 163588 }, + { url = "https://files.pythonhosted.org/packages/a3/b7/070481b83d2d5ac0f19233d9f364294e224e6478b0762f07fa7f060e0619/websockets-13.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2aad13a200e5934f5a6767492fb07151e1de1d6079c003ab31e1823733ae79", size = 163894 }, + { url = "https://files.pythonhosted.org/packages/eb/be/d6e1cff7d441cfe5eafaacc5935463e5f14c8b1c0d39cb8afde82709b55a/websockets-13.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:df01aea34b6e9e33572c35cd16bae5a47785e7d5c8cb2b54b2acdb9678315a17", size = 164315 }, + { url = "https://files.pythonhosted.org/packages/8b/5e/ffa234473e46ab2d3f9fd9858163d5db3ecea1439e4cb52966d78906424b/websockets-13.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e54affdeb21026329fb0744ad187cf812f7d3c2aa702a5edb562b325191fcab6", size = 163714 }, + { url = "https://files.pythonhosted.org/packages/cc/92/cea9eb9d381ca57065a5eb4ec2ce7a291bd96c85ce742915c3c9ffc1069f/websockets-13.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9ef8aa8bdbac47f4968a5d66462a2a0935d044bf35c0e5a8af152d58516dbeb5", size = 163673 }, + { url = "https://files.pythonhosted.org/packages/a4/f1/279104fff239bfd04c12b1e58afea227d72fd1acf431e3eed3f6ac2c96d2/websockets-13.1-cp39-cp39-win32.whl", hash = "sha256:deeb929efe52bed518f6eb2ddc00cc496366a14c726005726ad62c2dd9017a3c", size = 158702 }, + { url = "https://files.pythonhosted.org/packages/25/0b/b87370ff141375c41f7dd67941728e4b3682ebb45882591516c792a2ebee/websockets-13.1-cp39-cp39-win_amd64.whl", hash = "sha256:7c65ffa900e7cc958cd088b9a9157a8141c991f8c53d11087e6fb7277a03f81d", size = 159146 }, + { url = "https://files.pythonhosted.org/packages/2d/75/6da22cb3ad5b8c606963f9a5f9f88656256fecc29d420b4b2bf9e0c7d56f/websockets-13.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5dd6da9bec02735931fccec99d97c29f47cc61f644264eb995ad6c0c27667238", size = 155499 }, + { url = "https://files.pythonhosted.org/packages/c0/ba/22833d58629088fcb2ccccedfae725ac0bbcd713319629e97125b52ac681/websockets-13.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:2510c09d8e8df777177ee3d40cd35450dc169a81e747455cc4197e63f7e7bfe5", size = 155737 }, + { url = "https://files.pythonhosted.org/packages/95/54/61684fe22bdb831e9e1843d972adadf359cf04ab8613285282baea6a24bb/websockets-13.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1c3cf67185543730888b20682fb186fc8d0fa6f07ccc3ef4390831ab4b388d9", size = 157095 }, + { url = "https://files.pythonhosted.org/packages/fc/f5/6652fb82440813822022a9301a30afde85e5ff3fb2aebb77f34aabe2b4e8/websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcc03c8b72267e97b49149e4863d57c2d77f13fae12066622dc78fe322490fe6", size = 156701 }, + { url = "https://files.pythonhosted.org/packages/67/33/ae82a7b860fa8a08aba68818bdf7ff61f04598aa5ab96df4cd5a3e418ca4/websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:004280a140f220c812e65f36944a9ca92d766b6cc4560be652a0a3883a79ed8a", size = 156654 }, + { url = "https://files.pythonhosted.org/packages/63/0b/a1b528d36934f833e20f6da1032b995bf093d55cb416b9f2266f229fb237/websockets-13.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e2620453c075abeb0daa949a292e19f56de518988e079c36478bacf9546ced23", size = 159192 }, + { url = "https://files.pythonhosted.org/packages/5e/a1/5ae6d0ef2e61e2b77b3b4678949a634756544186620a728799acdf5c3482/websockets-13.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9156c45750b37337f7b0b00e6248991a047be4aa44554c9886fe6bdd605aab3b", size = 155433 }, + { url = "https://files.pythonhosted.org/packages/0d/2f/addd33f85600d210a445f817ff0d79d2b4d0eb6f3c95b9f35531ebf8f57c/websockets-13.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:80c421e07973a89fbdd93e6f2003c17d20b69010458d3a8e37fb47874bd67d51", size = 155733 }, + { url = "https://files.pythonhosted.org/packages/74/0b/f8ec74ac3b14a983289a1b42dc2c518a0e2030b486d0549d4f51ca11e7c9/websockets-13.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82d0ba76371769d6a4e56f7e83bb8e81846d17a6190971e38b5de108bde9b0d7", size = 157093 }, + { url = "https://files.pythonhosted.org/packages/ad/4c/aa5cc2f718ee4d797411202f332c8281f04c42d15f55b02f7713320f7a03/websockets-13.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9875a0143f07d74dc5e1ded1c4581f0d9f7ab86c78994e2ed9e95050073c94d", size = 156701 }, + { url = "https://files.pythonhosted.org/packages/1f/4b/7c5b2d0d0f0f1a54f27c60107cf1f201bee1f88c5508f87408b470d09a9c/websockets-13.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11e38ad8922c7961447f35c7b17bffa15de4d17c70abd07bfbe12d6faa3e027", size = 156648 }, + { url = "https://files.pythonhosted.org/packages/f3/63/35f3fb073884a9fd1ce5413b2dcdf0d9198b03dac6274197111259cbde06/websockets-13.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4059f790b6ae8768471cddb65d3c4fe4792b0ab48e154c9f0a04cefaabcd5978", size = 159188 }, + { url = "https://files.pythonhosted.org/packages/59/fd/e4bf9a7159dba6a16c59ae9e670e3e8ad9dcb6791bc0599eb86de32d50a9/websockets-13.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:25c35bf84bf7c7369d247f0b8cfa157f989862c49104c5cf85cb5436a641d93e", size = 155499 }, + { url = "https://files.pythonhosted.org/packages/74/42/d48ede93cfe0c343f3b552af08efc60778d234989227b16882eed1b8b189/websockets-13.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:83f91d8a9bb404b8c2c41a707ac7f7f75b9442a0a876df295de27251a856ad09", size = 155731 }, + { url = "https://files.pythonhosted.org/packages/f6/f2/2ef6bff1c90a43b80622a17c0852b48c09d3954ab169266ad7b15e17cdcb/websockets-13.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a43cfdcddd07f4ca2b1afb459824dd3c6d53a51410636a2c7fc97b9a8cf4842", size = 157093 }, + { url = "https://files.pythonhosted.org/packages/d1/14/6f20bbaeeb350f155edf599aad949c554216f90e5d4ae7373d1f2e5931fb/websockets-13.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48a2ef1381632a2f0cb4efeff34efa97901c9fbc118e01951ad7cfc10601a9bb", size = 156701 }, + { url = "https://files.pythonhosted.org/packages/c7/86/38279dfefecd035e22b79c38722d4f87c4b6196f1556b7a631d0a3095ca7/websockets-13.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459bf774c754c35dbb487360b12c5727adab887f1622b8aed5755880a21c4a20", size = 156649 }, + { url = "https://files.pythonhosted.org/packages/f6/c5/12c6859a2eaa8c53f59a647617a27f1835a226cd7106c601067c53251d98/websockets-13.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:95858ca14a9f6fa8413d29e0a585b31b278388aa775b8a81fa24830123874678", size = 159187 }, + { url = "https://files.pythonhosted.org/packages/56/27/96a5cd2626d11c8280656c6c71d8ab50fe006490ef9971ccd154e0c42cd2/websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f", size = 152134 }, +] + +[[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/71/da/1c12502da116b379e511c39d95cdc8f886ace2f3478217cde9494d38ca58/wrapt-1.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:69c40d4655e078ede067a7095544bcec5a963566e17503e75a3a3e0fe2803b13", size = 38712 }, + { url = "https://files.pythonhosted.org/packages/8a/b0/66f3e53c77257a505aaf7ef6d1b75ff7c8bb6a9da3d96f6aaa5810cd2f33/wrapt-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f495b6754358979379f84534f8dd7a43ff8cff2558dcdea4a148a6e713a758f", size = 86199 }, + { url = "https://files.pythonhosted.org/packages/08/4e/313f99f271557cc85b6ba086fb9a0d785d0373f237f30d0b4a4d14c7daed/wrapt-1.17.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baa7ef4e0886a6f482e00d1d5bcd37c201b383f1d314643dfb0367169f94f04c", size = 78073 }, + { url = "https://files.pythonhosted.org/packages/e1/62/5b50c324082081337c2b38daf4bae1de66e87eb126c754b0fa153b3525af/wrapt-1.17.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fc931382e56627ec4acb01e09ce66e5c03c384ca52606111cee50d931a342d", size = 85555 }, + { url = "https://files.pythonhosted.org/packages/eb/d2/31bb2c9362d84153d7597a471b22250783bf86be1a01c1acaba3bf7a0e01/wrapt-1.17.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8f8909cdb9f1b237786c09a810e24ee5e15ef17019f7cecb207ce205b9b5fcce", size = 83892 }, + { url = "https://files.pythonhosted.org/packages/eb/54/f43889a2c787f2b8ac989461c0d2011f0ff69811ebf9b84796cc671aed63/wrapt-1.17.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad47b095f0bdc5585bced35bd088cbfe4177236c7df9984b3cc46b391cc60627", size = 76869 }, + { url = "https://files.pythonhosted.org/packages/aa/37/0fbed8e67bd10b6f8835047abb6f42b8870689af45d7ae581946f1685468/wrapt-1.17.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:948a9bd0fb2c5120457b07e59c8d7210cbc8703243225dbd78f4dfc13c8d2d1f", size = 83564 }, + { url = "https://files.pythonhosted.org/packages/ef/3c/40db3a234871eda0a7eb48001d025474ed9fde85fd992eefda154ebc4632/wrapt-1.17.0-cp38-cp38-win32.whl", hash = "sha256:5ae271862b2142f4bc687bdbfcc942e2473a89999a54231aa1c2c676e28f29ea", size = 36407 }, + { url = "https://files.pythonhosted.org/packages/67/71/b9ce92b7820e9bd8e2c727d806a2e4e8c9d2a3e839ffadde2d0e44d84c0b/wrapt-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:f335579a1b485c834849e9075191c9898e0731af45705c2ebf70e0cd5d58beed", size = 38706 }, + { url = "https://files.pythonhosted.org/packages/89/03/518069f0708573c02cbba3a3e452be3642dc7d984d0a03a47e0850e2fb05/wrapt-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d751300b94e35b6016d4b1e7d0e7bbc3b5e1751e2405ef908316c2a9024008a1", size = 38765 }, + { url = "https://files.pythonhosted.org/packages/60/01/12dd81522f8c1c953e98e2cbf356ff44fbb06ef0f7523cd622ac06ad7f03/wrapt-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7264cbb4a18dc4acfd73b63e4bcfec9c9802614572025bdd44d0721983fc1d9c", size = 83012 }, + { url = "https://files.pythonhosted.org/packages/c4/2d/9853fe0009271b2841f839eb0e707c6b4307d169375f26c58812ecf4fd71/wrapt-1.17.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33539c6f5b96cf0b1105a0ff4cf5db9332e773bb521cc804a90e58dc49b10578", size = 74759 }, + { url = "https://files.pythonhosted.org/packages/94/5c/03c911442b01b50e364572581430e12f82c3f5ea74d302907c1449d7ba36/wrapt-1.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c30970bdee1cad6a8da2044febd824ef6dc4cc0b19e39af3085c763fdec7de33", size = 82540 }, + { url = "https://files.pythonhosted.org/packages/52/e0/ef637448514295a6b3a01cf1dff417e081e7b8cf1eb712839962459af1f6/wrapt-1.17.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bc7f729a72b16ee21795a943f85c6244971724819819a41ddbaeb691b2dd85ad", size = 81461 }, + { url = "https://files.pythonhosted.org/packages/7f/44/8b7d417c3aae3a35ccfe361375ee3e452901c91062e5462e1aeef98255e8/wrapt-1.17.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6ff02a91c4fc9b6a94e1c9c20f62ea06a7e375f42fe57587f004d1078ac86ca9", size = 74380 }, + { url = "https://files.pythonhosted.org/packages/af/a9/e65406a9c3a99162055efcb6bf5e0261924381228c0a7608066805da03df/wrapt-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dfb7cff84e72e7bf975b06b4989477873dcf160b2fd89959c629535df53d4e0", size = 81057 }, + { url = "https://files.pythonhosted.org/packages/55/0c/111d42fb658a2f9ed7024cd5e57c08521d61646a256a3946db7d500c1551/wrapt-1.17.0-cp39-cp39-win32.whl", hash = "sha256:2399408ac33ffd5b200480ee858baa58d77dd30e0dd0cab6a8a9547135f30a88", size = 36415 }, + { url = "https://files.pythonhosted.org/packages/00/33/e7b14a7c06cedfaae064f34e95c95350de7cc10187ac173743e30a956b30/wrapt-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f763a29ee6a20c529496a20a7bcb16a73de27f5da6a843249c7047daf135977", size = 38742 }, + { url = "https://files.pythonhosted.org/packages/4b/d9/a8ba5e9507a9af1917285d118388c5eb7a81834873f45df213a6fe923774/wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371", size = 23592 }, +] + +[[package]] +name = "wsproto" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226 }, +] + +[[package]] +name = "zipp" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", size = 24199 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", size = 9200 }, +] + +[[package]] +name = "zope-interface" +version = "7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/93/9210e7606be57a2dfc6277ac97dcc864fd8d39f142ca194fdc186d596fda/zope.interface-7.2.tar.gz", hash = "sha256:8b49f1a3d1ee4cdaf5b32d2e738362c7f5e40ac8b46dd7d1a65e82a4872728fe", size = 252960 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/71/e6177f390e8daa7e75378505c5ab974e0bf59c1d3b19155638c7afbf4b2d/zope.interface-7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce290e62229964715f1011c3dbeab7a4a1e4971fd6f31324c4519464473ef9f2", size = 208243 }, + { url = "https://files.pythonhosted.org/packages/52/db/7e5f4226bef540f6d55acfd95cd105782bc6ee044d9b5587ce2c95558a5e/zope.interface-7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05b910a5afe03256b58ab2ba6288960a2892dfeef01336dc4be6f1b9ed02ab0a", size = 208759 }, + { url = "https://files.pythonhosted.org/packages/28/ea/fdd9813c1eafd333ad92464d57a4e3a82b37ae57c19497bcffa42df673e4/zope.interface-7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550f1c6588ecc368c9ce13c44a49b8d6b6f3ca7588873c679bd8fd88a1b557b6", size = 254922 }, + { url = "https://files.pythonhosted.org/packages/3b/d3/0000a4d497ef9fbf4f66bb6828b8d0a235e690d57c333be877bec763722f/zope.interface-7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ef9e2f865721553c6f22a9ff97da0f0216c074bd02b25cf0d3af60ea4d6931d", size = 249367 }, + { url = "https://files.pythonhosted.org/packages/3e/e5/0b359e99084f033d413419eff23ee9c2bd33bca2ca9f4e83d11856f22d10/zope.interface-7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27f926f0dcb058211a3bb3e0e501c69759613b17a553788b2caeb991bed3b61d", size = 254488 }, + { url = "https://files.pythonhosted.org/packages/7b/90/12d50b95f40e3b2fc0ba7f7782104093b9fd62806b13b98ef4e580f2ca61/zope.interface-7.2-cp310-cp310-win_amd64.whl", hash = "sha256:144964649eba4c5e4410bb0ee290d338e78f179cdbfd15813de1a664e7649b3b", size = 211947 }, + { url = "https://files.pythonhosted.org/packages/98/7d/2e8daf0abea7798d16a58f2f3a2bf7588872eee54ac119f99393fdd47b65/zope.interface-7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1909f52a00c8c3dcab6c4fad5d13de2285a4b3c7be063b239b8dc15ddfb73bd2", size = 208776 }, + { url = "https://files.pythonhosted.org/packages/a0/2a/0c03c7170fe61d0d371e4c7ea5b62b8cb79b095b3d630ca16719bf8b7b18/zope.interface-7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:80ecf2451596f19fd607bb09953f426588fc1e79e93f5968ecf3367550396b22", size = 209296 }, + { url = "https://files.pythonhosted.org/packages/49/b4/451f19448772b4a1159519033a5f72672221e623b0a1bd2b896b653943d8/zope.interface-7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:033b3923b63474800b04cba480b70f6e6243a62208071fc148354f3f89cc01b7", size = 260997 }, + { url = "https://files.pythonhosted.org/packages/65/94/5aa4461c10718062c8f8711161faf3249d6d3679c24a0b81dd6fc8ba1dd3/zope.interface-7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a102424e28c6b47c67923a1f337ede4a4c2bba3965b01cf707978a801fc7442c", size = 255038 }, + { url = "https://files.pythonhosted.org/packages/9f/aa/1a28c02815fe1ca282b54f6705b9ddba20328fabdc37b8cf73fc06b172f0/zope.interface-7.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25e6a61dcb184453bb00eafa733169ab6d903e46f5c2ace4ad275386f9ab327a", size = 259806 }, + { url = "https://files.pythonhosted.org/packages/a7/2c/82028f121d27c7e68632347fe04f4a6e0466e77bb36e104c8b074f3d7d7b/zope.interface-7.2-cp311-cp311-win_amd64.whl", hash = "sha256:3f6771d1647b1fc543d37640b45c06b34832a943c80d1db214a37c31161a93f1", size = 212305 }, + { url = "https://files.pythonhosted.org/packages/68/0b/c7516bc3bad144c2496f355e35bd699443b82e9437aa02d9867653203b4a/zope.interface-7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:086ee2f51eaef1e4a52bd7d3111a0404081dadae87f84c0ad4ce2649d4f708b7", size = 208959 }, + { url = "https://files.pythonhosted.org/packages/a2/e9/1463036df1f78ff8c45a02642a7bf6931ae4a38a4acd6a8e07c128e387a7/zope.interface-7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21328fcc9d5b80768bf051faa35ab98fb979080c18e6f84ab3f27ce703bce465", size = 209357 }, + { url = "https://files.pythonhosted.org/packages/07/a8/106ca4c2add440728e382f1b16c7d886563602487bdd90004788d45eb310/zope.interface-7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6dd02ec01f4468da0f234da9d9c8545c5412fef80bc590cc51d8dd084138a89", size = 264235 }, + { url = "https://files.pythonhosted.org/packages/fc/ca/57286866285f4b8a4634c12ca1957c24bdac06eae28fd4a3a578e30cf906/zope.interface-7.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e7da17f53e25d1a3bde5da4601e026adc9e8071f9f6f936d0fe3fe84ace6d54", size = 259253 }, + { url = "https://files.pythonhosted.org/packages/96/08/2103587ebc989b455cf05e858e7fbdfeedfc3373358320e9c513428290b1/zope.interface-7.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cab15ff4832580aa440dc9790b8a6128abd0b88b7ee4dd56abacbc52f212209d", size = 264702 }, + { url = "https://files.pythonhosted.org/packages/5f/c7/3c67562e03b3752ba4ab6b23355f15a58ac2d023a6ef763caaca430f91f2/zope.interface-7.2-cp312-cp312-win_amd64.whl", hash = "sha256:29caad142a2355ce7cfea48725aa8bcf0067e2b5cc63fcf5cd9f97ad12d6afb5", size = 212466 }, + { url = "https://files.pythonhosted.org/packages/c6/3b/e309d731712c1a1866d61b5356a069dd44e5b01e394b6cb49848fa2efbff/zope.interface-7.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:3e0350b51e88658d5ad126c6a57502b19d5f559f6cb0a628e3dc90442b53dd98", size = 208961 }, + { url = "https://files.pythonhosted.org/packages/49/65/78e7cebca6be07c8fc4032bfbb123e500d60efdf7b86727bb8a071992108/zope.interface-7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15398c000c094b8855d7d74f4fdc9e73aa02d4d0d5c775acdef98cdb1119768d", size = 209356 }, + { url = "https://files.pythonhosted.org/packages/11/b1/627384b745310d082d29e3695db5f5a9188186676912c14b61a78bbc6afe/zope.interface-7.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:802176a9f99bd8cc276dcd3b8512808716492f6f557c11196d42e26c01a69a4c", size = 264196 }, + { url = "https://files.pythonhosted.org/packages/b8/f6/54548df6dc73e30ac6c8a7ff1da73ac9007ba38f866397091d5a82237bd3/zope.interface-7.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb23f58a446a7f09db85eda09521a498e109f137b85fb278edb2e34841055398", size = 259237 }, + { url = "https://files.pythonhosted.org/packages/b6/66/ac05b741c2129fdf668b85631d2268421c5cd1a9ff99be1674371139d665/zope.interface-7.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a71a5b541078d0ebe373a81a3b7e71432c61d12e660f1d67896ca62d9628045b", size = 264696 }, + { url = "https://files.pythonhosted.org/packages/0a/2f/1bccc6f4cc882662162a1158cda1a7f616add2ffe322b28c99cb031b4ffc/zope.interface-7.2-cp313-cp313-win_amd64.whl", hash = "sha256:4893395d5dd2ba655c38ceb13014fd65667740f09fa5bb01caa1e6284e48c0cd", size = 212472 }, + { url = "https://files.pythonhosted.org/packages/e6/0f/4e296d4b36ceb5464b671443ac4084d586d47698610025c4731ff2d30eae/zope.interface-7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d3a8ffec2a50d8ec470143ea3d15c0c52d73df882eef92de7537e8ce13475e8a", size = 208349 }, + { url = "https://files.pythonhosted.org/packages/f5/8c/49548aaa4f691615d703b5bee88ea67f68eac8f94c9fb6f1b2f4ae631354/zope.interface-7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:31d06db13a30303c08d61d5fb32154be51dfcbdb8438d2374ae27b4e069aac40", size = 208806 }, + { url = "https://files.pythonhosted.org/packages/fe/f9/5a66f98f8c21d644d94f95b9484564f76e175034de3a57a45ba72238ce10/zope.interface-7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e204937f67b28d2dca73ca936d3039a144a081fc47a07598d44854ea2a106239", size = 257897 }, + { url = "https://files.pythonhosted.org/packages/9e/dd/5505c6fa2dd3a6b76176c07bc85ad0c24f218a3e7c929272384a5eb5f18a/zope.interface-7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:224b7b0314f919e751f2bca17d15aad00ddbb1eadf1cb0190fa8175edb7ede62", size = 252095 }, + { url = "https://files.pythonhosted.org/packages/8a/cd/f1e8303b81151cd6ba6e229db5fe601f9867a7b29f24a95eeba255970099/zope.interface-7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf95683cde5bc7d0e12d8e7588a3eb754d7c4fa714548adcd96bdf90169f021", size = 257367 }, + { url = "https://files.pythonhosted.org/packages/7c/a3/c4e9d23ca87d3f922e38ba4f4edcbf6114c10ff9ee0b7b4d5023b0f1f3f5/zope.interface-7.2-cp38-cp38-win_amd64.whl", hash = "sha256:7dc5016e0133c1a1ec212fc87a4f7e7e562054549a99c73c8896fa3a9e80cbc7", size = 211957 }, + { url = "https://files.pythonhosted.org/packages/8c/2c/1f49dc8b4843c4f0848d8e43191aed312bad946a1563d1bf9e46cf2816ee/zope.interface-7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bd449c306ba006c65799ea7912adbbfed071089461a19091a228998b82b1fdb", size = 208349 }, + { url = "https://files.pythonhosted.org/packages/ed/7d/83ddbfc8424c69579a90fc8edc2b797223da2a8083a94d8dfa0e374c5ed4/zope.interface-7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a19a6cc9c6ce4b1e7e3d319a473cf0ee989cbbe2b39201d7c19e214d2dfb80c7", size = 208799 }, + { url = "https://files.pythonhosted.org/packages/36/22/b1abd91854c1be03f5542fe092e6a745096d2eca7704d69432e119100583/zope.interface-7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72cd1790b48c16db85d51fbbd12d20949d7339ad84fd971427cf00d990c1f137", size = 254267 }, + { url = "https://files.pythonhosted.org/packages/2a/dd/fcd313ee216ad0739ae00e6126bc22a0af62a74f76a9ca668d16cd276222/zope.interface-7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52e446f9955195440e787596dccd1411f543743c359eeb26e9b2c02b077b0519", size = 248614 }, + { url = "https://files.pythonhosted.org/packages/88/d4/4ba1569b856870527cec4bf22b91fe704b81a3c1a451b2ccf234e9e0666f/zope.interface-7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ad9913fd858274db8dd867012ebe544ef18d218f6f7d1e3c3e6d98000f14b75", size = 253800 }, + { url = "https://files.pythonhosted.org/packages/69/da/c9cfb384c18bd3a26d9fc6a9b5f32ccea49ae09444f097eaa5ca9814aff9/zope.interface-7.2-cp39-cp39-win_amd64.whl", hash = "sha256:1090c60116b3da3bfdd0c03406e2f14a1ff53e5771aebe33fec1edc0a350175d", size = 211980 }, +]