├── .env-example ├── .github ├── dependabot.yml └── workflows │ └── pipeline.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── build ├── .gitignore ├── Dockerfile ├── __init__.py ├── constants.py ├── publish.py └── utils.py ├── examples ├── fast_api_multistage_build │ ├── .dockerignore │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── app │ │ ├── __init__.py │ │ └── main.py │ ├── poetry.lock │ ├── pyproject.toml │ └── tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ └── test_api │ │ ├── __init__.py │ │ └── test_api_endpoints.py ├── fast_api_multistage_build_with_json_logging │ ├── .dockerignore │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── app │ │ ├── __init__.py │ │ └── main.py │ ├── application_server │ │ └── logging_configuration_file.yaml │ ├── poetry.lock │ ├── pyproject.toml │ └── tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ └── test_api │ │ ├── __init__.py │ │ └── test_api_endpoints.py └── fast_api_singlestage_build │ ├── .dockerignore │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── app │ ├── __init__.py │ └── main.py │ ├── poetry.lock │ ├── pyproject.toml │ └── tests │ ├── __init__.py │ ├── conftest.py │ └── test_api │ ├── __init__.py │ └── test_api_endpoints.py ├── poetry.lock ├── pyproject.toml └── tests ├── __init__.py ├── build_image ├── __init__.py ├── conftest.py ├── test_build_version.py ├── test_default_configuration.py └── test_json_logging_configuration.py ├── conftest.py ├── constants.py ├── publish_image ├── __init__.py ├── conftest.py ├── test_cli.py ├── test_cli_and_env.py └── test_env.py ├── registry_container.py └── utils.py /.env-example: -------------------------------------------------------------------------------- 1 | PYTHON_VERSION=3.12.0 2 | OS_VARIANT=slim-bookworm 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /.github/workflows/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: Pipeline 2 | 3 | on: push 4 | 5 | jobs: 6 | code-quality: 7 | runs-on: ubuntu-20.04 8 | steps: 9 | - name: Checkout Repository 10 | uses: actions/checkout@v4 11 | - name: Setup Python 12 | uses: actions/setup-python@v4 13 | with: 14 | python-version: 3.11 15 | - name: Install Poetry 16 | uses: snok/install-poetry@v1 17 | with: 18 | version: 1.7.1 19 | virtualenvs-in-project: true 20 | - name: Load cached venv 21 | id: cached-poetry-dependencies 22 | uses: actions/cache@v3 23 | with: 24 | path: .venv 25 | key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} 26 | - name: Install dependencies 27 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 28 | run: | 29 | poetry install --no-interaction --no-root 30 | - name: Run pre-commit hooks 31 | run: | 32 | poetry run pre-commit run -a 33 | 34 | run-build-image-tests: 35 | needs: code-quality 36 | runs-on: ubuntu-20.04 37 | strategy: 38 | fail-fast: false 39 | matrix: 40 | python_version: ["3.10.14", "3.11.8", "3.12.2"] 41 | os_variant: ["bookworm", "slim-bookworm"] 42 | steps: 43 | - name: Checkout Repository 44 | uses: actions/checkout@v4 45 | - name: Expose GitHub Runtime 46 | uses: crazy-max/ghaction-github-runtime@v3 47 | - name: Setup Python 48 | uses: actions/setup-python@v4 49 | with: 50 | python-version: 3.11 51 | - name: Install Poetry 52 | uses: snok/install-poetry@v1 53 | with: 54 | version: 1.7.1 55 | virtualenvs-in-project: true 56 | - name: Load cached venv 57 | id: cached-poetry-dependencies 58 | uses: actions/cache@v3 59 | with: 60 | path: .venv 61 | key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} 62 | - name: Install dependencies 63 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 64 | run: | 65 | poetry install --no-interaction --no-root 66 | - name: Run tests for image builds with pytest 67 | env: 68 | PYTHON_VERSION: ${{ matrix.python_version }} 69 | OS_VARIANT: ${{ matrix.os_variant }} 70 | POETRY_VERSION: ${{ matrix.poetry_version }} 71 | run: | 72 | poetry run pytest tests/build_image --cov --cov-report=xml:build_image_coverage_report.xml 73 | - name: Upload coverage report to artifactory 74 | uses: actions/upload-artifact@v3 75 | with: 76 | name: build-image-coverage-report-${{ matrix.python_version }}-${{ matrix.os_variant }}-${{ matrix.poetry_version }} 77 | path: build_image_coverage_report.xml 78 | if-no-files-found: error 79 | retention-days: 1 80 | 81 | run-publish-image-tests: 82 | needs: code-quality 83 | runs-on: ubuntu-20.04 84 | strategy: 85 | fail-fast: false 86 | matrix: 87 | python_version: ["3.10.14", "3.11.8", "3.12.2"] 88 | os_variant: ["bookworm", "slim-bookworm"] 89 | steps: 90 | - name: Checkout Repository 91 | uses: actions/checkout@v4 92 | - name: Expose GitHub Runtime 93 | uses: crazy-max/ghaction-github-runtime@v3 94 | - name: Setup Python 95 | uses: actions/setup-python@v4 96 | with: 97 | python-version: 3.11 98 | - name: Install Poetry 99 | uses: snok/install-poetry@v1 100 | with: 101 | version: 1.7.1 102 | virtualenvs-in-project: true 103 | - name: Load cached venv 104 | id: cached-poetry-dependencies 105 | uses: actions/cache@v3 106 | with: 107 | path: .venv 108 | key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} 109 | - name: Install dependencies 110 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 111 | run: | 112 | poetry install --no-interaction --no-root 113 | - name: Run tests for image publishing with pytest 114 | env: 115 | PYTHON_VERSION: ${{ matrix.python_version }} 116 | OS_VARIANT: ${{ matrix.os_variant }} 117 | POETRY_VERSION: ${{ matrix.poetry_version }} 118 | run: | 119 | poetry run pytest tests/publish_image --cov --cov-report=xml:publish_image_coverage_report.xml 120 | - name: Upload coverage report to artifactory 121 | uses: actions/upload-artifact@v3 122 | with: 123 | name: publish-image-coverage-report-${{ matrix.python_version }}-${{ matrix.os_variant }}-${{ matrix.poetry_version }} 124 | path: publish_image_coverage_report.xml 125 | if-no-files-found: error 126 | retention-days: 1 127 | 128 | upload-test-coverage-reports: 129 | needs: 130 | - run-build-image-tests 131 | - run-publish-image-tests 132 | runs-on: ubuntu-20.04 133 | steps: 134 | - name: Checkout Repository 135 | uses: actions/checkout@v4 136 | - name: Download coverage reports from artifactory 137 | uses: actions/download-artifact@v3 138 | - name: Compile the relevant reports 139 | run: | 140 | find . -name "*.xml" -exec cp {} . \; 141 | - name: Upload coverage to Codecov 142 | uses: codecov/codecov-action@v3 143 | with: 144 | files: ./build_image_coverage_report.xml,./publish_image_coverage_report.xml 145 | fail_ci_if_error: true 146 | token: ${{ secrets.CODECOV_TOKEN }} 147 | 148 | publish-all-images: 149 | needs: 150 | - upload-test-coverage-reports 151 | if: startsWith(github.ref, 'refs/tags/') 152 | runs-on: ubuntu-20.04 153 | strategy: 154 | fail-fast: false 155 | matrix: 156 | python_version: ["3.10.14", "3.11.8", "3.12.2"] 157 | os_variant: ["bookworm", "slim-bookworm"] 158 | steps: 159 | - name: Checkout Repository 160 | uses: actions/checkout@v4 161 | - name: Expose GitHub Runtime 162 | uses: crazy-max/ghaction-github-runtime@v3 163 | - name: Get Git Commit Tag Name 164 | uses: olegtarasov/get-tag@v2.1 165 | - name: Setup Python 166 | uses: actions/setup-python@v4 167 | with: 168 | python-version: 3.11 169 | - name: Install Poetry 170 | uses: snok/install-poetry@v1 171 | with: 172 | version: 1.7.1 173 | virtualenvs-in-project: true 174 | - name: Load cached venv 175 | id: cached-poetry-dependencies 176 | uses: actions/cache@v3 177 | with: 178 | path: .venv 179 | key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} 180 | - name: Install dependencies 181 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 182 | run: | 183 | poetry install --no-interaction --no-root 184 | - name: Publish Image to Docker Hub 185 | env: 186 | DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} 187 | DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} 188 | PYTHON_VERSION: ${{ matrix.python_version }} 189 | OS_VARIANT: ${{ matrix.os_variant }} 190 | POETRY_VERSION: ${{ matrix.poetry_version }} 191 | run: | 192 | source .venv/bin/activate 193 | python -m build.publish 194 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .venv 3 | .env 4 | .idea 5 | .vscode 6 | .pytest_cache 7 | .coverage 8 | __pycache__ 9 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.4.0 4 | hooks: 5 | - id: check-ast 6 | - id: check-merge-conflict 7 | - id: detect-private-key 8 | - id: check-added-large-files 9 | - repo: https://github.com/astral-sh/ruff-pre-commit 10 | rev: v0.3.4 11 | hooks: 12 | - id: ruff 13 | args: ["--fix", "--config", "pyproject.toml", "."] 14 | - id: ruff-format 15 | args: ["--config", "pyproject.toml", "."] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Max Pfeiffer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/) 2 | [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) 3 | [![codecov](https://codecov.io/gh/max-pfeiffer/uvicorn-poetry/branch/main/graph/badge.svg?token=WQI2SJJLZN)](https://codecov.io/gh/max-pfeiffer/uvicorn-poetry) 4 | ![pipeline workflow](https://github.com/max-pfeiffer/uvicorn-poetry/actions/workflows/pipeline.yml/badge.svg) 5 | ![Docker Image Size (latest semver)](https://img.shields.io/docker/image-size/pfeiffermax/uvicorn-poetry?sort=semver) 6 | ![Docker Pulls](https://img.shields.io/docker/pulls/pfeiffermax/uvicorn-poetry) 7 | # uvicorn-poetry - Docker image for FastAPI 8 | This Docker image provides a platform to run Python applications with [Uvicorn](https://github.com/encode/uvicorn) on [Kubernetes](https://kubernetes.io/) container orchestration system. 9 | It provides [Poetry](https://python-poetry.org/) for managing dependencies and setting up a virtual environment in the container. 10 | 11 | This image aims to follow the best practices for a production grade container image for hosting Python web applications based 12 | on micro frameworks like [FastAPI](https://fastapi.tiangolo.com/). 13 | Therefore, source and documentation contain a lot of references to documentation of dependencies used in this project, so users 14 | of this image can follow up on that. 15 | 16 | Any feedback is highly appreciated and will be considered. 17 | 18 | **Docker Hub:** [pfeiffermax/uvicorn-poetry](https://hub.docker.com/r/pfeiffermax/uvicorn-poetry) 19 | 20 | **GitHub Repository:** [https://github.com/max-pfeiffer/uvicorn-poetry](https://github.com/max-pfeiffer/uvicorn-poetry) 21 | 22 | ## Docker Image Features 23 | 1. Poetry v1.7.1 is available as Python package dependency management tool 24 | 2. A virtual environment for the application and application server 25 | 3. The application is run with [Uvicorn](https://www.uvicorn.org) as application server 26 | 4. Python versions: 27 | 1. 3.10 28 | 2. 3.11 29 | 3. 3.12 30 | 5. Operating system variants: 31 | 1. [Debian Bookworm v12.1](https://www.debian.org/releases/bookworm/) 32 | 2. [Debian Bookworm slim v12.1](https://www.debian.org/releases/bookworm/) 33 | 6. Supported CPU architectures: 34 | 1. linux/amd64 35 | 2. linux/arm64/v8 36 | 37 | ## Usage 38 | The image provides a platform to run your Python application, so it does not provide an application itself. 39 | 40 | Please have a look at the [single stage](https://github.com/max-pfeiffer/uvicorn-poetry/tree/main/examples/fast_api_singlestage_build) and [multi stage](https://github.com/max-pfeiffer/uvicorn-poetry/tree/main/examples/fast_api_multistage_build) example to learn how to use the image. 41 | 42 | The [multi stage approach](https://github.com/max-pfeiffer/uvicorn-poetry/tree/main/examples/fast_api_multistage_build) 43 | is a bit more efficient with regard to build time. It caches the Python package dependencies in a separate build stage. 44 | 45 | You can also use the [uvicorn-poetry-fastapi-project-template](https://github.com/max-pfeiffer/uvicorn-poetry-fastapi-project-template) for your convenience (requires [Cookiecutter](https://github.com/cookiecutter/cookiecutter)). The generated project basically contains the Dockerfile of this image and production image is build upon the standard Python image which results in an even smaller image size eventually. 46 | 47 | Please be aware that your application needs an application layout without src folder which is proposed in 48 | [fastapi-realworld-example-app](https://github.com/nsidnev/fastapi-realworld-example-app). 49 | The application and test structure needs to be like that: 50 | ```bash 51 | ├── .dockerignore 52 | ├── Dockerfile 53 | ├── app 54 | │ ├── __init__.py 55 | │ └── main.py 56 | ├── poetry.lock 57 | ├── pyproject.toml 58 | └── tests 59 | ├── __init__.py 60 | ├── conftest.py 61 | └── test_api 62 | ├── __init__.py 63 | ├── test_items.py 64 | └── test_root.py 65 | ``` 66 | Please be aware that you need to provide a pyproject.toml file to specify your Python package dependencies for Poetry and configure 67 | dependencies like Pytest. Poetry dependencies must at least contain the following to work: 68 | * python = "^3.11" 69 | * uvicorn = "0.24.0" 70 | 71 | If your application uses FastAPI framework this needs to be added as well: 72 | * fastapi = "0.104.1" 73 | 74 | **IMPORTANT:** make sure you have a [.dockerignore file](https://github.com/max-pfeiffer/uvicorn-poetry/blob/main/examples/fast_api_multistage_build/.dockerignore) 75 | in your application root which excludes your local virtual environment in .venv! Otherwise you will have an issue activating that virtual 76 | environment when running the container. 77 | 78 | ## Configuration 79 | Configuration is done through command line options and arguments in the 80 | [Dockerfile](https://github.com/max-pfeiffer/uvicorn-poetry/blob/main/build/Dockerfile). 81 | For everything else Uvicorn uses its defaults. 82 | Since [Uvicorn v0.16.0](https://github.com/encode/uvicorn/releases/tag/0.16.0) you can configure Uvicorn via 83 | [environment variables](https://www.uvicorn.org/settings/) with the prefix `UVICORN_`. 84 | If you would like to do a deep dive on all the configuration options please see the 85 | [official Uvicorn documentation](https://www.uvicorn.org/settings/). 86 | 87 | ### Important changes since V3.0.0 88 | 1. Scripts for entrypoints are dropped and removed 89 | 2. Application is run with an unprivileged user 90 | 91 | ### Important change since V2.0.0 92 | These custom environment variables are not supported anymore: 93 | 1. `LOG_LEVEL` : The granularity of Error log outputs. 94 | 2. `LOG_CONFIG_FILE` : Logging configuration file. 95 | 3. `RELOAD` : Enable auto-reload. 96 | -------------------------------------------------------------------------------- /build/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /build/Dockerfile: -------------------------------------------------------------------------------- 1 | # The Poetry installation is provided through the base image. Please check out the 2 | # base image if you interested in the details. 3 | # Base image: https://hub.docker.com/r/pfeiffermax/python-poetry 4 | # Dockerfile: https://github.com/max-pfeiffer/python-poetry/blob/main/build/Dockerfile 5 | ARG BASE_IMAGE 6 | FROM ${BASE_IMAGE} 7 | ARG APPLICATION_SERVER_PORT 8 | 9 | LABEL maintainer="Max Pfeiffer " 10 | 11 | # https://docs.python.org/3/using/cmdline.html#envvar-PYTHONUNBUFFERED 12 | ENV PYTHONUNBUFFERED=1 \ 13 | # https://docs.python.org/3/using/cmdline.html#envvar-PYTHONDONTWRITEBYTECODE 14 | PYTHONDONTWRITEBYTECODE=1 \ 15 | PYTHONPATH=/application_root \ 16 | # https://python-poetry.org/docs/configuration/#virtualenvsin-project 17 | POETRY_VIRTUALENVS_IN_PROJECT=true \ 18 | POETRY_CACHE_DIR="/application_root/.cache" \ 19 | VIRTUAL_ENVIRONMENT_PATH="/application_root/.venv" \ 20 | APPLICATION_SERVER_PORT=$APPLICATION_SERVER_PORT 21 | 22 | # Adding the virtual environment to PATH in order to "activate" it. 23 | # https://docs.python.org/3/library/venv.html#how-venvs-work 24 | ENV PATH="$VIRTUAL_ENVIRONMENT_PATH/bin:$PATH" 25 | 26 | # Principle of least privilege: create a new user for running the application 27 | RUN groupadd -g 1001 python_application && \ 28 | useradd -r -u 1001 -g python_application python_application 29 | 30 | # Set the WORKDIR to the application root. 31 | # https://www.uvicorn.org/settings/#development 32 | # https://docs.docker.com/engine/reference/builder/#workdir 33 | WORKDIR ${PYTHONPATH} 34 | RUN chown python_application:python_application ${PYTHONPATH} 35 | 36 | # Create cache directory and set permissions because user 1001 has no home 37 | # and poetry cache directory. 38 | # https://python-poetry.org/docs/configuration/#cache-directory 39 | RUN mkdir ${POETRY_CACHE_DIR} && chown python_application:python_application ${POETRY_CACHE_DIR} 40 | 41 | # Document the exposed port 42 | # https://docs.docker.com/engine/reference/builder/#expose 43 | EXPOSE ${APPLICATION_SERVER_PORT} 44 | 45 | # Use the unpriveledged user to run the application 46 | USER 1001 47 | 48 | # Run the uvicorn application server. 49 | CMD exec uvicorn --workers 1 --host 0.0.0.0 --port $APPLICATION_SERVER_PORT app.main:app 50 | -------------------------------------------------------------------------------- /build/__init__.py: -------------------------------------------------------------------------------- 1 | """Image build.""" 2 | -------------------------------------------------------------------------------- /build/constants.py: -------------------------------------------------------------------------------- 1 | """Constants for image build.""" 2 | 3 | # As we are running the server with an unprivileged user, we need to use 4 | # a high port. 5 | APPLICATION_SERVER_PORT: str = "8000" 6 | 7 | PLATFORMS: list[str] = ["linux/amd64", "linux/arm64/v8"] 8 | -------------------------------------------------------------------------------- /build/publish.py: -------------------------------------------------------------------------------- 1 | """Image publishing.""" 2 | 3 | from os import getenv 4 | from pathlib import Path 5 | 6 | import click 7 | from python_on_whales import Builder, DockerClient 8 | 9 | from build.constants import APPLICATION_SERVER_PORT, PLATFORMS 10 | from build.utils import ( 11 | get_context, 12 | get_image_reference, 13 | get_python_poetry_image_reference, 14 | ) 15 | 16 | 17 | @click.command() 18 | @click.option( 19 | "--docker-hub-username", 20 | envvar="DOCKER_HUB_USERNAME", 21 | help="Docker Hub username", 22 | ) 23 | @click.option( 24 | "--docker-hub-password", 25 | envvar="DOCKER_HUB_PASSWORD", 26 | help="Docker Hub password", 27 | ) 28 | @click.option("--version-tag", envvar="GIT_TAG_NAME", required=True, help="Version tag") 29 | @click.option( 30 | "--python-version", 31 | envvar="PYTHON_VERSION", 32 | required=True, 33 | help="Python version", 34 | ) 35 | @click.option( 36 | "--os-variant", 37 | envvar="OS_VARIANT", 38 | required=True, 39 | help="Operating system variant", 40 | ) 41 | @click.option( 42 | "--registry", envvar="REGISTRY", default="docker.io", help="Docker registry" 43 | ) 44 | def main( 45 | docker_hub_username: str, 46 | docker_hub_password: str, 47 | version_tag: str, 48 | python_version: str, 49 | os_variant: str, 50 | registry: str, 51 | ) -> None: 52 | """Build Docker image. 53 | 54 | :param docker_hub_username: 55 | :param docker_hub_password: 56 | :param version_tag: 57 | :param python_version: 58 | :param os_variant: 59 | :param registry: 60 | :return: 61 | """ 62 | github_ref_name: str = getenv("GITHUB_REF_NAME") 63 | context: Path = get_context() 64 | image_reference: str = get_image_reference( 65 | registry, version_tag, python_version, os_variant 66 | ) 67 | cache_scope: str = f"{python_version}-{os_variant}" 68 | 69 | if github_ref_name: 70 | cache_to: str = f"type=gha,mode=max,scope={github_ref_name}-{cache_scope}" 71 | cache_from: str = f"type=gha,scope={github_ref_name}-{cache_scope}" 72 | else: 73 | cache_to = f"type=local,mode=max,dest=/tmp,scope={cache_scope}" 74 | cache_from = f"type=local,src=/tmp,scope={cache_scope}" 75 | 76 | docker_client: DockerClient = DockerClient() 77 | builder: Builder = docker_client.buildx.create( 78 | driver="docker-container", driver_options=dict(network="host") 79 | ) 80 | 81 | docker_client.login( 82 | server=registry, 83 | username=docker_hub_username, 84 | password=docker_hub_password, 85 | ) 86 | 87 | docker_client.buildx.build( 88 | context_path=context, 89 | build_args={ 90 | "BASE_IMAGE": get_python_poetry_image_reference(python_version, os_variant), 91 | "APPLICATION_SERVER_PORT": APPLICATION_SERVER_PORT, 92 | }, 93 | tags=image_reference, 94 | platforms=PLATFORMS, 95 | builder=builder, 96 | cache_to=cache_to, 97 | cache_from=cache_from, 98 | push=True, 99 | ) 100 | 101 | # Cleanup 102 | docker_client.buildx.stop(builder) 103 | docker_client.buildx.remove(builder) 104 | 105 | 106 | if __name__ == "__main__": 107 | # pylint: disable=no-value-for-parameter 108 | main() 109 | -------------------------------------------------------------------------------- /build/utils.py: -------------------------------------------------------------------------------- 1 | """Utilities for image publishing.""" 2 | 3 | from pathlib import Path 4 | 5 | 6 | def get_context() -> Path: 7 | """Return Docker build context. 8 | 9 | :return: 10 | """ 11 | return Path(__file__).parent.resolve() 12 | 13 | 14 | def get_image_reference( 15 | registry: str, 16 | image_version: str, 17 | python_version: str, 18 | os_variant: str, 19 | ) -> str: 20 | """Return image reference. 21 | 22 | :param registry: 23 | :param image_version: 24 | :param python_version: 25 | :param os_variant: 26 | :return: 27 | """ 28 | reference: str = ( 29 | f"{registry}/pfeiffermax/uvicorn-poetry:{image_version}" 30 | f"-python{python_version}-{os_variant}" 31 | ) 32 | return reference 33 | 34 | 35 | def get_python_poetry_image_reference( 36 | python_version: str, 37 | os_variant: str, 38 | ) -> str: 39 | """Return image reference for base image. 40 | 41 | :param python_version: 42 | :param os_variant: 43 | :return: 44 | """ 45 | reference: str = ( 46 | f"pfeiffermax/python-poetry:1.10.0-poetry1.8.2-python" 47 | f"{python_version}-{os_variant}" 48 | ) 49 | return reference 50 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build/.dockerignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .idea 3 | .pytest_cache 4 | .venv 5 | .coverage 6 | Dockerfile 7 | README.md 8 | test_coverage_reports/ 9 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build/.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | .pytest_cache 3 | __pycache__ 4 | .idea 5 | test_coverage_reports/* 6 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build/Dockerfile: -------------------------------------------------------------------------------- 1 | # Be aware that you need to specify these arguments before the first FROM 2 | # see: https://docs.docker.com/engine/reference/builder/#understand-how-arg-and-from-interact 3 | ARG BASE_IMAGE=pfeiffermax/uvicorn-poetry:3.2.0-python3.12.0-slim-bookworm 4 | FROM ${BASE_IMAGE} as dependencies-build-stage 5 | 6 | # install [tool.poetry.dependencies] 7 | # this will install virtual environment into /.venv because of POETRY_VIRTUALENVS_IN_PROJECT=true 8 | # see: https://python-poetry.org/docs/configuration/#virtualenvsin-project 9 | COPY --chown=python_application:python_application ./poetry.lock ./pyproject.toml /application_root/ 10 | RUN poetry install --no-interaction --no-root --without dev 11 | 12 | FROM ${BASE_IMAGE} as production-image 13 | 14 | # Copy virtual environment 15 | COPY --chown=python_application:python_application --from=dependencies-build-stage /application_root/.venv /application_root/.venv 16 | 17 | # Copy application files 18 | COPY --chown=python_application:python_application /app /application_root/app/ 19 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build/README.md: -------------------------------------------------------------------------------- 1 | # fast-api-multistage-build 2 | This is an example project to demonstrate the use of the uvicorn-poetry image. 3 | It is also used for testing that image. 4 | 5 | ## Build the image 6 | ```shell 7 | docker build --tag fast-api-multistage --target production-image . 8 | ``` 9 | Build the image with another base image variant: 10 | ```shell 11 | docker build --build-arg BASE_IMAGE=pfeiffermax/uvicorn-poetry:3.2.0-python3.10.13-bookworm --tag fast-api-multistage --target production-image . 12 | ``` 13 | 14 | ## Run the image 15 | ```shell 16 | docker run -it --rm fast-api-multistage 17 | ``` 18 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build/app/__init__.py: -------------------------------------------------------------------------------- 1 | """Application.""" 2 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build/app/main.py: -------------------------------------------------------------------------------- 1 | """Main module.""" 2 | 3 | from typing import Dict 4 | 5 | from fastapi import FastAPI 6 | 7 | app = FastAPI() 8 | 9 | HELLO_WORLD: str = "Hello World!" 10 | ITEMS: Dict[str, str] = {"1": "sausage", "2": "ham", "3": "tofu"} 11 | 12 | 13 | @app.get("/") 14 | def read_root(): 15 | """Root endpoint. 16 | 17 | :return: 18 | """ 19 | return HELLO_WORLD 20 | 21 | 22 | @app.get("/items/{item_id}") 23 | def read_item(item_id: str): 24 | """Items. 25 | 26 | :param item_id: 27 | :return: 28 | """ 29 | return ITEMS[item_id] 30 | 31 | 32 | def get_app(): 33 | """Return app. 34 | 35 | :return: 36 | """ 37 | return app 38 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fast_api_multistage_build" 3 | version = "1.0.0" 4 | description = "Example app for testing and demonstrating uvicorn-poetry docker image." 5 | authors = ["Max Pfeiffer "] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.10" 10 | uvicorn = {version = " 0.24.0.post1", extras = ["standard"]} 11 | fastapi = "0.104.1" 12 | json-log-formatter = "0.5.2" 13 | 14 | [tool.poetry.dev-dependencies] 15 | pytest = "7.4.3" 16 | pytest-cov = "4.1.0" 17 | coverage = "7.3.2" 18 | black = "23.11.0" 19 | requests = "2.31.0" 20 | 21 | # https://docs.pytest.org/en/latest/reference/customize.html 22 | [tool.pytest.ini_options] 23 | testpaths = [ 24 | "tests", 25 | ] 26 | 27 | # https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#configuration-via-a-file 28 | [tool.black] 29 | line-length = 80 30 | target-version = ['py39'] 31 | 32 | [build-system] 33 | requires = ["poetry-core>=1.0.0"] 34 | build-backend = "poetry.core.masonry.api" 35 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests.""" 2 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Test fixtures.""" 2 | 3 | import pytest 4 | from app.main import app 5 | from fastapi.testclient import TestClient 6 | 7 | 8 | @pytest.fixture(scope="module") 9 | def test_client() -> TestClient: 10 | """Fixture for providing test client. 11 | 12 | :return: 13 | """ 14 | return TestClient(app) 15 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build/tests/test_api/__init__.py: -------------------------------------------------------------------------------- 1 | """API tests.""" 2 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build/tests/test_api/test_api_endpoints.py: -------------------------------------------------------------------------------- 1 | """Tests for API endpoints.""" 2 | 3 | import random 4 | 5 | from app.main import HELLO_WORLD, ITEMS 6 | from requests import Response 7 | from starlette import status 8 | 9 | 10 | def test_root(test_client): 11 | """Test for root endpoint. 12 | 13 | :param test_client: 14 | :return: 15 | """ 16 | response: Response = test_client.get("/") 17 | 18 | assert response.status_code == status.HTTP_200_OK 19 | assert response.json() == HELLO_WORLD 20 | 21 | 22 | def test_items(test_client): 23 | """Test for items endpoint. 24 | 25 | :param test_client: 26 | :return: 27 | """ 28 | key, value = random.choice(list(ITEMS.items())) 29 | 30 | response: Response = test_client.get(f"/items/{key}") 31 | 32 | assert response.status_code == status.HTTP_200_OK 33 | assert response.json() == value 34 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build_with_json_logging/.dockerignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .idea 3 | .pytest_cache 4 | .venv 5 | .coverage 6 | Dockerfile 7 | README.md 8 | test_coverage_reports/ 9 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build_with_json_logging/.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | .pytest_cache 3 | __pycache__ 4 | .idea 5 | test_coverage_reports/* 6 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build_with_json_logging/Dockerfile: -------------------------------------------------------------------------------- 1 | # Be aware that you need to specify these arguments before the first FROM 2 | # see: https://docs.docker.com/engine/reference/builder/#understand-how-arg-and-from-interact 3 | ARG BASE_IMAGE=pfeiffermax/uvicorn-poetry:3.2.0-python3.12.0-slim-bookworm 4 | FROM ${BASE_IMAGE} as dependencies-build-stage 5 | 6 | # install [tool.poetry.dependencies] 7 | # this will install virtual environment into /.venv because of POETRY_VIRTUALENVS_IN_PROJECT=true 8 | # see: https://python-poetry.org/docs/configuration/#virtualenvsin-project 9 | COPY --chown=python_application:python_application ./poetry.lock ./pyproject.toml /application_root/ 10 | RUN poetry install --no-interaction --no-root --without dev 11 | 12 | FROM ${BASE_IMAGE} as production-image 13 | ENV UVICORN_LOG_CONFIG=/application_server/logging_configuration_file.yaml \ 14 | UVICORN_LOG_LEVEL=trace 15 | 16 | # Copy virtual environment 17 | COPY --chown=python_application:python_application --from=dependencies-build-stage /application_root/.venv /application_root/.venv 18 | 19 | # Adding log configuration for Uvicorn 20 | COPY --chown=python_application:python_application /application_server/logging_configuration_file.yaml /application_server/logging_configuration_file.yaml 21 | 22 | # Copy application files 23 | COPY --chown=python_application:python_application /app /application_root/app/ 24 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build_with_json_logging/README.md: -------------------------------------------------------------------------------- 1 | # fast-api-multistage-build-with-json-logging 2 | This is an example project to demonstrate the use of the uvicorn-poetry image. 3 | It is also used for testing that image. 4 | 5 | ## Custom log config 6 | Please be aware of [Uvicorn's default logging config](https://github.com/encode/uvicorn/blob/master/uvicorn/config.py). 7 | I took that basically as a template for the custom logging config which I provided as an example for demonstrating 8 | the use of this configuration option and the environment variable. I choose the JSON formatter for customisation because 9 | this is a common use case when you run your application on Kubernetes with a log aggregation system. 10 | Please be aware that there are more convenient options to achieve this like with the 11 | [json-logging-python package](https://github.com/bobbui/json-logging-python). 12 | 13 | ## Build the image 14 | ```shell 15 | docker build --tag fast-api-multistage --target production-image . 16 | ``` 17 | Build the image with another base image variant: 18 | ```shell 19 | docker build --build-arg BASE_IMAGE=pfeiffermax/uvicorn-poetry:3.2.0-python3.10.13-bookworm --tag fast-api-multistage --target production-image . 20 | ``` 21 | 22 | ## Run the image 23 | ```shell 24 | docker run -it --rm fast-api-multistage 25 | ``` 26 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build_with_json_logging/app/__init__.py: -------------------------------------------------------------------------------- 1 | """Application.""" 2 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build_with_json_logging/app/main.py: -------------------------------------------------------------------------------- 1 | """Main module.""" 2 | 3 | from typing import Dict 4 | 5 | from fastapi import FastAPI 6 | 7 | app = FastAPI() 8 | 9 | HELLO_WORLD: str = "Hello World!" 10 | ITEMS: Dict[str, str] = {"1": "sausage", "2": "ham", "3": "tofu"} 11 | 12 | 13 | @app.get("/") 14 | def read_root(): 15 | """Root endpoint. 16 | 17 | :return: 18 | """ 19 | return HELLO_WORLD 20 | 21 | 22 | @app.get("/items/{item_id}") 23 | def read_item(item_id: str): 24 | """Items endpoint. 25 | 26 | :param item_id: 27 | :return: 28 | """ 29 | return ITEMS[item_id] 30 | 31 | 32 | def get_app(): 33 | """Return application. 34 | 35 | :return: 36 | """ 37 | return app 38 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build_with_json_logging/application_server/logging_configuration_file.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | disable_existing_loggers: False 3 | formatters: 4 | json: 5 | (): "json_log_formatter.VerboseJSONFormatter" 6 | handlers: 7 | default: 8 | formatter: "json" 9 | class: "logging.StreamHandler" 10 | stream: "ext://sys.stderr" 11 | access: 12 | formatter: "json" 13 | class: "logging.StreamHandler" 14 | stream: "ext://sys.stdout" 15 | loggers: 16 | uvicorn: 17 | handlers: ["default"] 18 | level: "INFO" 19 | uvicorn.error: 20 | level: "INFO" 21 | uvicorn.access: 22 | handlers: ["access"] 23 | level: "INFO" 24 | propagate: False 25 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build_with_json_logging/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fast_api_multistage_build" 3 | version = "1.0.0" 4 | description = "Example app for testing and demonstrating uvicorn-poetry docker image." 5 | authors = ["Max Pfeiffer "] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.9" 10 | uvicorn = {version = " 0.24.0.post1", extras = ["standard"]} 11 | fastapi = "0.104.1" 12 | json-log-formatter = "0.5.2" 13 | 14 | [tool.poetry.dev-dependencies] 15 | pytest = "7.4.3" 16 | pytest-cov = "4.1.0" 17 | coverage = "7.3.2" 18 | black = "23.11.0" 19 | requests = "2.31.0" 20 | 21 | # https://docs.pytest.org/en/latest/reference/customize.html 22 | [tool.pytest.ini_options] 23 | testpaths = [ 24 | "tests", 25 | ] 26 | 27 | # https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#configuration-via-a-file 28 | [tool.black] 29 | line-length = 80 30 | target-version = ['py39'] 31 | 32 | [build-system] 33 | requires = ["poetry-core>=1.0.0"] 34 | build-backend = "poetry.core.masonry.api" 35 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build_with_json_logging/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests.""" 2 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build_with_json_logging/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Fixtures for tests.""" 2 | 3 | import pytest 4 | from app.main import app 5 | from fastapi.testclient import TestClient 6 | 7 | 8 | @pytest.fixture(scope="module") 9 | def test_client() -> TestClient: 10 | """Fixture for providing a test client. 11 | 12 | :return: 13 | """ 14 | return TestClient(app) 15 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build_with_json_logging/tests/test_api/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests.""" 2 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build_with_json_logging/tests/test_api/test_api_endpoints.py: -------------------------------------------------------------------------------- 1 | """Tests for API endpoints.""" 2 | 3 | import random 4 | 5 | from app.main import HELLO_WORLD, ITEMS 6 | from requests import Response 7 | from starlette import status 8 | 9 | 10 | def test_root(test_client) -> None: 11 | """Test for root endpoint. 12 | 13 | :param test_client: 14 | :return: 15 | """ 16 | response: Response = test_client.get("/") 17 | 18 | assert response.status_code == status.HTTP_200_OK 19 | assert response.json() == HELLO_WORLD 20 | 21 | 22 | def test_items(test_client): 23 | """Test for items endpoint. 24 | 25 | :param test_client: 26 | :return: 27 | """ 28 | key, value = random.choice(list(ITEMS.items())) 29 | 30 | response: Response = test_client.get(f"/items/{key}") 31 | 32 | assert response.status_code == status.HTTP_200_OK 33 | assert response.json() == value 34 | -------------------------------------------------------------------------------- /examples/fast_api_singlestage_build/.dockerignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .idea 3 | .pytest_cache 4 | .venv 5 | .coverage 6 | Dockerfile 7 | README.md 8 | test_coverage_reports/ 9 | -------------------------------------------------------------------------------- /examples/fast_api_singlestage_build/.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | .pytest_cache 3 | __pycache__ 4 | .idea 5 | test_coverage_reports/* 6 | -------------------------------------------------------------------------------- /examples/fast_api_singlestage_build/Dockerfile: -------------------------------------------------------------------------------- 1 | # Be aware that you need to specify these arguments before the first FROM 2 | # see: https://docs.docker.com/engine/reference/builder/#understand-how-arg-and-from-interact 3 | ARG BASE_IMAGE=pfeiffermax/uvicorn-poetry:3.2.0-python3.12.0-slim-bookworm 4 | FROM ${BASE_IMAGE} 5 | 6 | # install [tool.poetry.dependencies] 7 | # this will install virtual environment into /.venv because of POETRY_VIRTUALENVS_IN_PROJECT=true 8 | # see: https://python-poetry.org/docs/configuration/#virtualenvsin-project 9 | COPY --chown=python_application:python_application ./poetry.lock ./pyproject.toml /application_root/ 10 | RUN poetry install --no-interaction --no-root --without dev 11 | 12 | # Copy application files 13 | COPY --chown=python_application:python_application /app /application_root/app/ 14 | -------------------------------------------------------------------------------- /examples/fast_api_singlestage_build/README.md: -------------------------------------------------------------------------------- 1 | # fast-api-multistage-build 2 | This is an example project to demonstrate the use of the uvicorn-poetry image. 3 | It is also used for testing that image. 4 | 5 | ## Build the image 6 | ```shell 7 | docker build --tag fast-api-singlestage . 8 | ``` 9 | Build the image with another base image variant: 10 | ```shell 11 | docker build --build-arg BASE_IMAGE=pfeiffermax/uvicorn-poetry:3.2.0-python3.10.13-bookworm --tag fast-api-singlestage . 12 | ``` 13 | 14 | ## Run the image 15 | ```shell 16 | docker run -it --rm fast-api-singlestage 17 | ``` 18 | -------------------------------------------------------------------------------- /examples/fast_api_singlestage_build/app/__init__.py: -------------------------------------------------------------------------------- 1 | """Application.""" 2 | -------------------------------------------------------------------------------- /examples/fast_api_singlestage_build/app/main.py: -------------------------------------------------------------------------------- 1 | """Main module.""" 2 | 3 | from typing import Dict 4 | 5 | from fastapi import FastAPI 6 | 7 | app = FastAPI() 8 | 9 | HELLO_WORLD: str = "Hello World!" 10 | ITEMS: Dict[str, str] = {"1": "sausage", "2": "ham", "3": "tofu"} 11 | 12 | 13 | @app.get("/") 14 | def read_root(): 15 | """Root endpoint. 16 | 17 | :return: 18 | """ 19 | return HELLO_WORLD 20 | 21 | 22 | @app.get("/items/{item_id}") 23 | def read_item(item_id: str): 24 | """Items endpoint. 25 | 26 | :param item_id: 27 | :return: 28 | """ 29 | return ITEMS[item_id] 30 | 31 | 32 | def get_app(): 33 | """Return application. 34 | 35 | :return: 36 | """ 37 | return app 38 | -------------------------------------------------------------------------------- /examples/fast_api_singlestage_build/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fast_api_multistage_build" 3 | version = "1.0.0" 4 | description = "Example app for testing and demonstrating uvicorn-poetry docker image." 5 | authors = ["Max Pfeiffer "] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.10" 10 | uvicorn = {version = " 0.24.0.post1", extras = ["standard"]} 11 | fastapi = "0.104.1" 12 | json-log-formatter = "0.5.2" 13 | 14 | [tool.poetry.dev-dependencies] 15 | pytest = "7.4.3" 16 | pytest-cov = "4.1.0" 17 | coverage = "7.3.2" 18 | black = "23.11.0" 19 | requests = "2.31.0" 20 | 21 | # https://docs.pytest.org/en/latest/reference/customize.html 22 | [tool.pytest.ini_options] 23 | testpaths = [ 24 | "tests", 25 | ] 26 | 27 | # https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#configuration-via-a-file 28 | [tool.black] 29 | line-length = 80 30 | target-version = ['py39'] 31 | 32 | [build-system] 33 | requires = ["poetry-core>=1.0.0"] 34 | build-backend = "poetry.core.masonry.api" 35 | -------------------------------------------------------------------------------- /examples/fast_api_singlestage_build/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests.""" 2 | -------------------------------------------------------------------------------- /examples/fast_api_singlestage_build/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Fixtures for tests.""" 2 | 3 | import pytest 4 | from app.main import app 5 | from fastapi.testclient import TestClient 6 | 7 | 8 | @pytest.fixture(scope="module") 9 | def test_client() -> TestClient: 10 | """Fixture for providing a test client. 11 | 12 | :return: 13 | """ 14 | return TestClient(app) 15 | -------------------------------------------------------------------------------- /examples/fast_api_singlestage_build/tests/test_api/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for API endpoints.""" 2 | -------------------------------------------------------------------------------- /examples/fast_api_singlestage_build/tests/test_api/test_api_endpoints.py: -------------------------------------------------------------------------------- 1 | """Tests for API endpoints.""" 2 | 3 | import random 4 | 5 | from app.main import HELLO_WORLD, ITEMS 6 | from requests import Response 7 | from starlette import status 8 | 9 | 10 | def test_root(test_client) -> None: 11 | """Test root endpoint. 12 | 13 | :param test_client: 14 | :return: 15 | """ 16 | response: Response = test_client.get("/") 17 | 18 | assert response.status_code == status.HTTP_200_OK 19 | assert response.json() == HELLO_WORLD 20 | 21 | 22 | def test_items(test_client) -> None: 23 | """Test items endpoint. 24 | 25 | :param test_client: 26 | :return: 27 | """ 28 | key, value = random.choice(list(ITEMS.items())) 29 | 30 | response: Response = test_client.get(f"/items/{key}") 31 | 32 | assert response.status_code == status.HTTP_200_OK 33 | assert response.json() == value 34 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "annotated-types" 5 | version = "0.6.0" 6 | description = "Reusable constraint types to use with typing.Annotated" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, 11 | {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, 12 | ] 13 | 14 | [[package]] 15 | name = "bcrypt" 16 | version = "4.1.2" 17 | description = "Modern password hashing for your software and your servers" 18 | optional = false 19 | python-versions = ">=3.7" 20 | files = [ 21 | {file = "bcrypt-4.1.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:ac621c093edb28200728a9cca214d7e838529e557027ef0581685909acd28b5e"}, 22 | {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea505c97a5c465ab8c3ba75c0805a102ce526695cd6818c6de3b1a38f6f60da1"}, 23 | {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57fa9442758da926ed33a91644649d3e340a71e2d0a5a8de064fb621fd5a3326"}, 24 | {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb3bd3321517916696233b5e0c67fd7d6281f0ef48e66812db35fc963a422a1c"}, 25 | {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6cad43d8c63f34b26aef462b6f5e44fdcf9860b723d2453b5d391258c4c8e966"}, 26 | {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:44290ccc827d3a24604f2c8bcd00d0da349e336e6503656cb8192133e27335e2"}, 27 | {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:732b3920a08eacf12f93e6b04ea276c489f1c8fb49344f564cca2adb663b3e4c"}, 28 | {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1c28973decf4e0e69cee78c68e30a523be441972c826703bb93099868a8ff5b5"}, 29 | {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b8df79979c5bae07f1db22dcc49cc5bccf08a0380ca5c6f391cbb5790355c0b0"}, 30 | {file = "bcrypt-4.1.2-cp37-abi3-win32.whl", hash = "sha256:fbe188b878313d01b7718390f31528be4010fed1faa798c5a1d0469c9c48c369"}, 31 | {file = "bcrypt-4.1.2-cp37-abi3-win_amd64.whl", hash = "sha256:9800ae5bd5077b13725e2e3934aa3c9c37e49d3ea3d06318010aa40f54c63551"}, 32 | {file = "bcrypt-4.1.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:71b8be82bc46cedd61a9f4ccb6c1a493211d031415a34adde3669ee1b0afbb63"}, 33 | {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e3c6642077b0c8092580c819c1684161262b2e30c4f45deb000c38947bf483"}, 34 | {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:387e7e1af9a4dd636b9505a465032f2f5cb8e61ba1120e79a0e1cd0b512f3dfc"}, 35 | {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f70d9c61f9c4ca7d57f3bfe88a5ccf62546ffbadf3681bb1e268d9d2e41c91a7"}, 36 | {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2a298db2a8ab20056120b45e86c00a0a5eb50ec4075b6142db35f593b97cb3fb"}, 37 | {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ba55e40de38a24e2d78d34c2d36d6e864f93e0d79d0b6ce915e4335aa81d01b1"}, 38 | {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3566a88234e8de2ccae31968127b0ecccbb4cddb629da744165db72b58d88ca4"}, 39 | {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b90e216dc36864ae7132cb151ffe95155a37a14e0de3a8f64b49655dd959ff9c"}, 40 | {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:69057b9fc5093ea1ab00dd24ede891f3e5e65bee040395fb1e66ee196f9c9b4a"}, 41 | {file = "bcrypt-4.1.2-cp39-abi3-win32.whl", hash = "sha256:02d9ef8915f72dd6daaef40e0baeef8a017ce624369f09754baf32bb32dba25f"}, 42 | {file = "bcrypt-4.1.2-cp39-abi3-win_amd64.whl", hash = "sha256:be3ab1071662f6065899fe08428e45c16aa36e28bc42921c4901a191fda6ee42"}, 43 | {file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d75fc8cd0ba23f97bae88a6ec04e9e5351ff3c6ad06f38fe32ba50cbd0d11946"}, 44 | {file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:a97e07e83e3262599434816f631cc4c7ca2aa8e9c072c1b1a7fec2ae809a1d2d"}, 45 | {file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e51c42750b7585cee7892c2614be0d14107fad9581d1738d954a262556dd1aab"}, 46 | {file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba4e4cc26610581a6329b3937e02d319f5ad4b85b074846bf4fef8a8cf51e7bb"}, 47 | {file = "bcrypt-4.1.2.tar.gz", hash = "sha256:33313a1200a3ae90b75587ceac502b048b840fc69e7f7a0905b5f87fac7a1258"}, 48 | ] 49 | 50 | [package.extras] 51 | tests = ["pytest (>=3.2.1,!=3.3.0)"] 52 | typecheck = ["mypy"] 53 | 54 | [[package]] 55 | name = "certifi" 56 | version = "2024.2.2" 57 | description = "Python package for providing Mozilla's CA Bundle." 58 | optional = false 59 | python-versions = ">=3.6" 60 | files = [ 61 | {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, 62 | {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, 63 | ] 64 | 65 | [[package]] 66 | name = "cfgv" 67 | version = "3.4.0" 68 | description = "Validate configuration and produce human readable error messages." 69 | optional = false 70 | python-versions = ">=3.8" 71 | files = [ 72 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 73 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 74 | ] 75 | 76 | [[package]] 77 | name = "charset-normalizer" 78 | version = "3.3.2" 79 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 80 | optional = false 81 | python-versions = ">=3.7.0" 82 | files = [ 83 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 84 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 85 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 86 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 87 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 88 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 89 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 90 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 91 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 92 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 93 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 94 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 95 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 96 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 97 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 98 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 99 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 100 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 101 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 102 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 103 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 104 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 105 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 106 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 107 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 108 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 109 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 110 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 111 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 112 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 113 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 114 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 115 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 116 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 117 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 118 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 119 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 120 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 121 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 122 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 123 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 124 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 125 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 126 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 127 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 128 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 129 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 130 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 131 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 132 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 133 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 134 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 135 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 136 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 137 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 138 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 139 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 140 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 141 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 142 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 143 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 144 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 145 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 146 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 147 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 148 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 149 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 150 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 151 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 152 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 153 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 154 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 155 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 156 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 157 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 158 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 159 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 160 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 161 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 162 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 163 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 164 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 165 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 166 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 167 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 168 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 169 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 170 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 171 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 172 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 173 | ] 174 | 175 | [[package]] 176 | name = "click" 177 | version = "8.1.7" 178 | description = "Composable command line interface toolkit" 179 | optional = false 180 | python-versions = ">=3.7" 181 | files = [ 182 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 183 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 184 | ] 185 | 186 | [package.dependencies] 187 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 188 | 189 | [[package]] 190 | name = "colorama" 191 | version = "0.4.6" 192 | description = "Cross-platform colored terminal text." 193 | optional = false 194 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 195 | files = [ 196 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 197 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 198 | ] 199 | 200 | [[package]] 201 | name = "coverage" 202 | version = "7.4.4" 203 | description = "Code coverage measurement for Python" 204 | optional = false 205 | python-versions = ">=3.8" 206 | files = [ 207 | {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, 208 | {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, 209 | {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, 210 | {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, 211 | {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, 212 | {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, 213 | {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, 214 | {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, 215 | {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, 216 | {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, 217 | {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, 218 | {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, 219 | {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, 220 | {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, 221 | {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, 222 | {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, 223 | {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, 224 | {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, 225 | {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, 226 | {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, 227 | {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, 228 | {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, 229 | {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, 230 | {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, 231 | {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, 232 | {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, 233 | {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, 234 | {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, 235 | {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, 236 | {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, 237 | {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, 238 | {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, 239 | {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, 240 | {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, 241 | {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, 242 | {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, 243 | {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, 244 | {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, 245 | {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, 246 | {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, 247 | {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, 248 | {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, 249 | {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, 250 | {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, 251 | {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, 252 | {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, 253 | {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, 254 | {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, 255 | {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, 256 | {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, 257 | {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, 258 | {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, 259 | ] 260 | 261 | [package.dependencies] 262 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 263 | 264 | [package.extras] 265 | toml = ["tomli"] 266 | 267 | [[package]] 268 | name = "distlib" 269 | version = "0.3.8" 270 | description = "Distribution utilities" 271 | optional = false 272 | python-versions = "*" 273 | files = [ 274 | {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, 275 | {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, 276 | ] 277 | 278 | [[package]] 279 | name = "docker" 280 | version = "7.0.0" 281 | description = "A Python library for the Docker Engine API." 282 | optional = false 283 | python-versions = ">=3.8" 284 | files = [ 285 | {file = "docker-7.0.0-py3-none-any.whl", hash = "sha256:12ba681f2777a0ad28ffbcc846a69c31b4dfd9752b47eb425a274ee269c5e14b"}, 286 | {file = "docker-7.0.0.tar.gz", hash = "sha256:323736fb92cd9418fc5e7133bc953e11a9da04f4483f828b527db553f1e7e5a3"}, 287 | ] 288 | 289 | [package.dependencies] 290 | packaging = ">=14.0" 291 | pywin32 = {version = ">=304", markers = "sys_platform == \"win32\""} 292 | requests = ">=2.26.0" 293 | urllib3 = ">=1.26.0" 294 | 295 | [package.extras] 296 | ssh = ["paramiko (>=2.4.3)"] 297 | websockets = ["websocket-client (>=1.3.0)"] 298 | 299 | [[package]] 300 | name = "docker-image-py" 301 | version = "0.1.12" 302 | description = "Parse docker image as distribution does." 303 | optional = false 304 | python-versions = "*" 305 | files = [ 306 | {file = "docker-image-py-0.1.12.tar.gz", hash = "sha256:c0eebb6c25714b2a4f91a1462183e70252fa34fb189496d3c54711a64f12f96e"}, 307 | {file = "docker_image_py-0.1.12-py2-none-any.whl", hash = "sha256:44e18e8000aaaddbd2e02d40050dca850acd071c4780cbe2b3366cb5dc1a6d62"}, 308 | ] 309 | 310 | [package.dependencies] 311 | regex = ">=2019.4.14" 312 | 313 | [[package]] 314 | name = "filelock" 315 | version = "3.13.1" 316 | description = "A platform independent file lock." 317 | optional = false 318 | python-versions = ">=3.8" 319 | files = [ 320 | {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, 321 | {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, 322 | ] 323 | 324 | [package.extras] 325 | docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] 326 | testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] 327 | typing = ["typing-extensions (>=4.8)"] 328 | 329 | [[package]] 330 | name = "furl" 331 | version = "2.1.3" 332 | description = "URL manipulation made simple." 333 | optional = false 334 | python-versions = "*" 335 | files = [ 336 | {file = "furl-2.1.3-py2.py3-none-any.whl", hash = "sha256:9ab425062c4217f9802508e45feb4a83e54324273ac4b202f1850363309666c0"}, 337 | {file = "furl-2.1.3.tar.gz", hash = "sha256:5a6188fe2666c484a12159c18be97a1977a71d632ef5bb867ef15f54af39cc4e"}, 338 | ] 339 | 340 | [package.dependencies] 341 | orderedmultidict = ">=1.0.1" 342 | six = ">=1.8.0" 343 | 344 | [[package]] 345 | name = "identify" 346 | version = "2.5.35" 347 | description = "File identification library for Python" 348 | optional = false 349 | python-versions = ">=3.8" 350 | files = [ 351 | {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, 352 | {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, 353 | ] 354 | 355 | [package.extras] 356 | license = ["ukkonen"] 357 | 358 | [[package]] 359 | name = "idna" 360 | version = "3.6" 361 | description = "Internationalized Domain Names in Applications (IDNA)" 362 | optional = false 363 | python-versions = ">=3.5" 364 | files = [ 365 | {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, 366 | {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, 367 | ] 368 | 369 | [[package]] 370 | name = "iniconfig" 371 | version = "2.0.0" 372 | description = "brain-dead simple config-ini parsing" 373 | optional = false 374 | python-versions = ">=3.7" 375 | files = [ 376 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 377 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 378 | ] 379 | 380 | [[package]] 381 | name = "nodeenv" 382 | version = "1.8.0" 383 | description = "Node.js virtual environment builder" 384 | optional = false 385 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" 386 | files = [ 387 | {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, 388 | {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, 389 | ] 390 | 391 | [package.dependencies] 392 | setuptools = "*" 393 | 394 | [[package]] 395 | name = "orderedmultidict" 396 | version = "1.0.1" 397 | description = "Ordered Multivalue Dictionary" 398 | optional = false 399 | python-versions = "*" 400 | files = [ 401 | {file = "orderedmultidict-1.0.1-py2.py3-none-any.whl", hash = "sha256:43c839a17ee3cdd62234c47deca1a8508a3f2ca1d0678a3bf791c87cf84adbf3"}, 402 | {file = "orderedmultidict-1.0.1.tar.gz", hash = "sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad"}, 403 | ] 404 | 405 | [package.dependencies] 406 | six = ">=1.8.0" 407 | 408 | [[package]] 409 | name = "packaging" 410 | version = "24.0" 411 | description = "Core utilities for Python packages" 412 | optional = false 413 | python-versions = ">=3.7" 414 | files = [ 415 | {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, 416 | {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, 417 | ] 418 | 419 | [[package]] 420 | name = "platformdirs" 421 | version = "4.2.0" 422 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 423 | optional = false 424 | python-versions = ">=3.8" 425 | files = [ 426 | {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, 427 | {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, 428 | ] 429 | 430 | [package.extras] 431 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 432 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 433 | 434 | [[package]] 435 | name = "pluggy" 436 | version = "1.4.0" 437 | description = "plugin and hook calling mechanisms for python" 438 | optional = false 439 | python-versions = ">=3.8" 440 | files = [ 441 | {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, 442 | {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, 443 | ] 444 | 445 | [package.extras] 446 | dev = ["pre-commit", "tox"] 447 | testing = ["pytest", "pytest-benchmark"] 448 | 449 | [[package]] 450 | name = "pre-commit" 451 | version = "3.6.2" 452 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 453 | optional = false 454 | python-versions = ">=3.9" 455 | files = [ 456 | {file = "pre_commit-3.6.2-py2.py3-none-any.whl", hash = "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c"}, 457 | {file = "pre_commit-3.6.2.tar.gz", hash = "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"}, 458 | ] 459 | 460 | [package.dependencies] 461 | cfgv = ">=2.0.0" 462 | identify = ">=1.0.0" 463 | nodeenv = ">=0.11.1" 464 | pyyaml = ">=5.1" 465 | virtualenv = ">=20.10.0" 466 | 467 | [[package]] 468 | name = "pydantic" 469 | version = "2.6.4" 470 | description = "Data validation using Python type hints" 471 | optional = false 472 | python-versions = ">=3.8" 473 | files = [ 474 | {file = "pydantic-2.6.4-py3-none-any.whl", hash = "sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5"}, 475 | {file = "pydantic-2.6.4.tar.gz", hash = "sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6"}, 476 | ] 477 | 478 | [package.dependencies] 479 | annotated-types = ">=0.4.0" 480 | pydantic-core = "2.16.3" 481 | typing-extensions = ">=4.6.1" 482 | 483 | [package.extras] 484 | email = ["email-validator (>=2.0.0)"] 485 | 486 | [[package]] 487 | name = "pydantic-core" 488 | version = "2.16.3" 489 | description = "" 490 | optional = false 491 | python-versions = ">=3.8" 492 | files = [ 493 | {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, 494 | {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, 495 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, 496 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, 497 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, 498 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, 499 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, 500 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, 501 | {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, 502 | {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, 503 | {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, 504 | {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, 505 | {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, 506 | {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, 507 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, 508 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, 509 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, 510 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, 511 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, 512 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, 513 | {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, 514 | {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, 515 | {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, 516 | {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, 517 | {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, 518 | {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, 519 | {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, 520 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, 521 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, 522 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, 523 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, 524 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, 525 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, 526 | {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, 527 | {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, 528 | {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, 529 | {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, 530 | {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, 531 | {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, 532 | {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, 533 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, 534 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, 535 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, 536 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, 537 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, 538 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, 539 | {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, 540 | {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, 541 | {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, 542 | {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, 543 | {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, 544 | {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, 545 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, 546 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, 547 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, 548 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, 549 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, 550 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, 551 | {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, 552 | {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, 553 | {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, 554 | {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, 555 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, 556 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, 557 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, 558 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, 559 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, 560 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, 561 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, 562 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, 563 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, 564 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, 565 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, 566 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, 567 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, 568 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, 569 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, 570 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, 571 | {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, 572 | ] 573 | 574 | [package.dependencies] 575 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 576 | 577 | [[package]] 578 | name = "pytest" 579 | version = "8.1.1" 580 | description = "pytest: simple powerful testing with Python" 581 | optional = false 582 | python-versions = ">=3.8" 583 | files = [ 584 | {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, 585 | {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, 586 | ] 587 | 588 | [package.dependencies] 589 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 590 | iniconfig = "*" 591 | packaging = "*" 592 | pluggy = ">=1.4,<2.0" 593 | 594 | [package.extras] 595 | testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 596 | 597 | [[package]] 598 | name = "pytest-cov" 599 | version = "5.0.0" 600 | description = "Pytest plugin for measuring coverage." 601 | optional = false 602 | python-versions = ">=3.8" 603 | files = [ 604 | {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, 605 | {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, 606 | ] 607 | 608 | [package.dependencies] 609 | coverage = {version = ">=5.2.1", extras = ["toml"]} 610 | pytest = ">=4.6" 611 | 612 | [package.extras] 613 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] 614 | 615 | [[package]] 616 | name = "pytest-dotenv" 617 | version = "0.5.2" 618 | description = "A py.test plugin that parses environment files before running tests" 619 | optional = false 620 | python-versions = "*" 621 | files = [ 622 | {file = "pytest-dotenv-0.5.2.tar.gz", hash = "sha256:2dc6c3ac6d8764c71c6d2804e902d0ff810fa19692e95fe138aefc9b1aa73732"}, 623 | {file = "pytest_dotenv-0.5.2-py3-none-any.whl", hash = "sha256:40a2cece120a213898afaa5407673f6bd924b1fa7eafce6bda0e8abffe2f710f"}, 624 | ] 625 | 626 | [package.dependencies] 627 | pytest = ">=5.0.0" 628 | python-dotenv = ">=0.9.1" 629 | 630 | [[package]] 631 | name = "python-dotenv" 632 | version = "1.0.1" 633 | description = "Read key-value pairs from a .env file and set them as environment variables" 634 | optional = false 635 | python-versions = ">=3.8" 636 | files = [ 637 | {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, 638 | {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, 639 | ] 640 | 641 | [package.extras] 642 | cli = ["click (>=5.0)"] 643 | 644 | [[package]] 645 | name = "python-on-whales" 646 | version = "0.70.1" 647 | description = "A Docker client for Python, designed to be fun and intuitive!" 648 | optional = false 649 | python-versions = "<4,>=3.8" 650 | files = [ 651 | {file = "python-on-whales-0.70.1.tar.gz", hash = "sha256:1e7ac35cd16afaad8d23f01be860cb3ff906ee81816d032a327d4d07da1f9341"}, 652 | {file = "python_on_whales-0.70.1-py3-none-any.whl", hash = "sha256:3cecd833359d90fd564cadf7f5e3c88209f8baf998316e0731f3d375d17af2f2"}, 653 | ] 654 | 655 | [package.dependencies] 656 | pydantic = ">=1.9,<2.0.dev0 || >=2.1.dev0,<3" 657 | requests = "*" 658 | tqdm = "*" 659 | typer = ">=0.4.1" 660 | typing-extensions = "*" 661 | 662 | [package.extras] 663 | test = ["pytest"] 664 | 665 | [[package]] 666 | name = "pywin32" 667 | version = "306" 668 | description = "Python for Window Extensions" 669 | optional = false 670 | python-versions = "*" 671 | files = [ 672 | {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, 673 | {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, 674 | {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, 675 | {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, 676 | {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, 677 | {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, 678 | {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, 679 | {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, 680 | {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, 681 | {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, 682 | {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, 683 | {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, 684 | {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, 685 | {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, 686 | ] 687 | 688 | [[package]] 689 | name = "pyyaml" 690 | version = "6.0.1" 691 | description = "YAML parser and emitter for Python" 692 | optional = false 693 | python-versions = ">=3.6" 694 | files = [ 695 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 696 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 697 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 698 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 699 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 700 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 701 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 702 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 703 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 704 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 705 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 706 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 707 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 708 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 709 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 710 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 711 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 712 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 713 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, 714 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 715 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 716 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 717 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 718 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 719 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 720 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 721 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 722 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 723 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 724 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 725 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 726 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 727 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 728 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 729 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 730 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 731 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 732 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 733 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 734 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, 735 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 736 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 737 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 738 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 739 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 740 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 741 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 742 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 743 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 744 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 745 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 746 | ] 747 | 748 | [[package]] 749 | name = "regex" 750 | version = "2023.12.25" 751 | description = "Alternative regular expression module, to replace re." 752 | optional = false 753 | python-versions = ">=3.7" 754 | files = [ 755 | {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5"}, 756 | {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8"}, 757 | {file = "regex-2023.12.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586"}, 758 | {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c"}, 759 | {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400"}, 760 | {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e"}, 761 | {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4"}, 762 | {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5"}, 763 | {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd"}, 764 | {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704"}, 765 | {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1"}, 766 | {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392"}, 767 | {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423"}, 768 | {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f"}, 769 | {file = "regex-2023.12.25-cp310-cp310-win32.whl", hash = "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630"}, 770 | {file = "regex-2023.12.25-cp310-cp310-win_amd64.whl", hash = "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105"}, 771 | {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6"}, 772 | {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97"}, 773 | {file = "regex-2023.12.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887"}, 774 | {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb"}, 775 | {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c"}, 776 | {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b"}, 777 | {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa"}, 778 | {file = "regex-2023.12.25-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7"}, 779 | {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0"}, 780 | {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe"}, 781 | {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80"}, 782 | {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd"}, 783 | {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4"}, 784 | {file = "regex-2023.12.25-cp311-cp311-win32.whl", hash = "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87"}, 785 | {file = "regex-2023.12.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f"}, 786 | {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715"}, 787 | {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d"}, 788 | {file = "regex-2023.12.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a"}, 789 | {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a"}, 790 | {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5"}, 791 | {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060"}, 792 | {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3"}, 793 | {file = "regex-2023.12.25-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9"}, 794 | {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f"}, 795 | {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c"}, 796 | {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457"}, 797 | {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf"}, 798 | {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d"}, 799 | {file = "regex-2023.12.25-cp312-cp312-win32.whl", hash = "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5"}, 800 | {file = "regex-2023.12.25-cp312-cp312-win_amd64.whl", hash = "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232"}, 801 | {file = "regex-2023.12.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69"}, 802 | {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7"}, 803 | {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73"}, 804 | {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2"}, 805 | {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482"}, 806 | {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f"}, 807 | {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8"}, 808 | {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a"}, 809 | {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39"}, 810 | {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b"}, 811 | {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347"}, 812 | {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39"}, 813 | {file = "regex-2023.12.25-cp37-cp37m-win32.whl", hash = "sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c"}, 814 | {file = "regex-2023.12.25-cp37-cp37m-win_amd64.whl", hash = "sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445"}, 815 | {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53"}, 816 | {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64"}, 817 | {file = "regex-2023.12.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415"}, 818 | {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770"}, 819 | {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590"}, 820 | {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb"}, 821 | {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1"}, 822 | {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988"}, 823 | {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861"}, 824 | {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc"}, 825 | {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4"}, 826 | {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360"}, 827 | {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756"}, 828 | {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2"}, 829 | {file = "regex-2023.12.25-cp38-cp38-win32.whl", hash = "sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb"}, 830 | {file = "regex-2023.12.25-cp38-cp38-win_amd64.whl", hash = "sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697"}, 831 | {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31"}, 832 | {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7"}, 833 | {file = "regex-2023.12.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc"}, 834 | {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95"}, 835 | {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1"}, 836 | {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf"}, 837 | {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae"}, 838 | {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6"}, 839 | {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923"}, 840 | {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d"}, 841 | {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca"}, 842 | {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5"}, 843 | {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f"}, 844 | {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20"}, 845 | {file = "regex-2023.12.25-cp39-cp39-win32.whl", hash = "sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9"}, 846 | {file = "regex-2023.12.25-cp39-cp39-win_amd64.whl", hash = "sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91"}, 847 | {file = "regex-2023.12.25.tar.gz", hash = "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5"}, 848 | ] 849 | 850 | [[package]] 851 | name = "requests" 852 | version = "2.31.0" 853 | description = "Python HTTP for Humans." 854 | optional = false 855 | python-versions = ">=3.7" 856 | files = [ 857 | {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, 858 | {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, 859 | ] 860 | 861 | [package.dependencies] 862 | certifi = ">=2017.4.17" 863 | charset-normalizer = ">=2,<4" 864 | idna = ">=2.5,<4" 865 | urllib3 = ">=1.21.1,<3" 866 | 867 | [package.extras] 868 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 869 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 870 | 871 | [[package]] 872 | name = "semver" 873 | version = "3.0.2" 874 | description = "Python helper for Semantic Versioning (https://semver.org)" 875 | optional = false 876 | python-versions = ">=3.7" 877 | files = [ 878 | {file = "semver-3.0.2-py3-none-any.whl", hash = "sha256:b1ea4686fe70b981f85359eda33199d60c53964284e0cfb4977d243e37cf4bf4"}, 879 | {file = "semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc"}, 880 | ] 881 | 882 | [[package]] 883 | name = "setuptools" 884 | version = "69.2.0" 885 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 886 | optional = false 887 | python-versions = ">=3.8" 888 | files = [ 889 | {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, 890 | {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, 891 | ] 892 | 893 | [package.extras] 894 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 895 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 896 | testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 897 | 898 | [[package]] 899 | name = "six" 900 | version = "1.16.0" 901 | description = "Python 2 and 3 compatibility utilities" 902 | optional = false 903 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 904 | files = [ 905 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 906 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 907 | ] 908 | 909 | [[package]] 910 | name = "testcontainers" 911 | version = "4.1.1" 912 | description = "Python library for throwaway instances of anything that can run in a Docker container" 913 | optional = false 914 | python-versions = "<4.0,>=3.9" 915 | files = [ 916 | {file = "testcontainers-4.1.1-py3-none-any.whl", hash = "sha256:9105b1807cbd0a4e0bfe084c2159754554bc5264ff8173c32f88b0a91e6a5e7c"}, 917 | {file = "testcontainers-4.1.1.tar.gz", hash = "sha256:6c20bde6e040a0fea0ddfaa80492bcde105251b8a59a1325bb1a6fdffe0e62ad"}, 918 | ] 919 | 920 | [package.dependencies] 921 | docker = "*" 922 | urllib3 = "*" 923 | wrapt = "*" 924 | 925 | [package.extras] 926 | arangodb = ["python-arango (>=7.8,<8.0)"] 927 | azurite = ["azure-storage-blob (>=12.19,<13.0)"] 928 | clickhouse = ["clickhouse-driver"] 929 | google = ["google-cloud-pubsub (>=2)"] 930 | k3s = ["kubernetes", "pyyaml"] 931 | kafka = ["kafka-python"] 932 | keycloak = ["python-keycloak"] 933 | localstack = ["boto3"] 934 | minio = ["minio"] 935 | mongodb = ["pymongo"] 936 | mssql = ["pymssql", "sqlalchemy"] 937 | mysql = ["pymysql[rsa]", "sqlalchemy"] 938 | neo4j = ["neo4j"] 939 | opensearch = ["opensearch-py"] 940 | oracle = ["cx_Oracle", "sqlalchemy"] 941 | rabbitmq = ["pika"] 942 | redis = ["redis"] 943 | selenium = ["selenium"] 944 | 945 | [[package]] 946 | name = "tomli" 947 | version = "2.0.1" 948 | description = "A lil' TOML parser" 949 | optional = false 950 | python-versions = ">=3.7" 951 | files = [ 952 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 953 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 954 | ] 955 | 956 | [[package]] 957 | name = "tqdm" 958 | version = "4.66.2" 959 | description = "Fast, Extensible Progress Meter" 960 | optional = false 961 | python-versions = ">=3.7" 962 | files = [ 963 | {file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"}, 964 | {file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"}, 965 | ] 966 | 967 | [package.dependencies] 968 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 969 | 970 | [package.extras] 971 | dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] 972 | notebook = ["ipywidgets (>=6)"] 973 | slack = ["slack-sdk"] 974 | telegram = ["requests"] 975 | 976 | [[package]] 977 | name = "typer" 978 | version = "0.10.0" 979 | description = "Typer, build great CLIs. Easy to code. Based on Python type hints." 980 | optional = false 981 | python-versions = ">=3.6" 982 | files = [ 983 | {file = "typer-0.10.0-py3-none-any.whl", hash = "sha256:b8a587aa06d3c5422c09c2e9935eb80b4c9de8605fd5ab702b2f92d72246ca48"}, 984 | {file = "typer-0.10.0.tar.gz", hash = "sha256:597f974754520b091665f993f88abdd088bb81c56b3042225434ced0b50a788b"}, 985 | ] 986 | 987 | [package.dependencies] 988 | click = ">=7.1.1,<9.0.0" 989 | typing-extensions = ">=3.7.4.3" 990 | 991 | [package.extras] 992 | all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] 993 | dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] 994 | doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] 995 | test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.971)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] 996 | 997 | [[package]] 998 | name = "typing-extensions" 999 | version = "4.10.0" 1000 | description = "Backported and Experimental Type Hints for Python 3.8+" 1001 | optional = false 1002 | python-versions = ">=3.8" 1003 | files = [ 1004 | {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, 1005 | {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "urllib3" 1010 | version = "2.2.1" 1011 | description = "HTTP library with thread-safe connection pooling, file post, and more." 1012 | optional = false 1013 | python-versions = ">=3.8" 1014 | files = [ 1015 | {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, 1016 | {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, 1017 | ] 1018 | 1019 | [package.extras] 1020 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 1021 | h2 = ["h2 (>=4,<5)"] 1022 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 1023 | zstd = ["zstandard (>=0.18.0)"] 1024 | 1025 | [[package]] 1026 | name = "virtualenv" 1027 | version = "20.25.1" 1028 | description = "Virtual Python Environment builder" 1029 | optional = false 1030 | python-versions = ">=3.7" 1031 | files = [ 1032 | {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, 1033 | {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, 1034 | ] 1035 | 1036 | [package.dependencies] 1037 | distlib = ">=0.3.7,<1" 1038 | filelock = ">=3.12.2,<4" 1039 | platformdirs = ">=3.9.1,<5" 1040 | 1041 | [package.extras] 1042 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 1043 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 1044 | 1045 | [[package]] 1046 | name = "wrapt" 1047 | version = "1.16.0" 1048 | description = "Module for decorators, wrappers and monkey patching." 1049 | optional = false 1050 | python-versions = ">=3.6" 1051 | files = [ 1052 | {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, 1053 | {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, 1054 | {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, 1055 | {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, 1056 | {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"}, 1057 | {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, 1058 | {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, 1059 | {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, 1060 | {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, 1061 | {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, 1062 | {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, 1063 | {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, 1064 | {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, 1065 | {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, 1066 | {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"}, 1067 | {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, 1068 | {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, 1069 | {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, 1070 | {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, 1071 | {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, 1072 | {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, 1073 | {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, 1074 | {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, 1075 | {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, 1076 | {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"}, 1077 | {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, 1078 | {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, 1079 | {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, 1080 | {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, 1081 | {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, 1082 | {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, 1083 | {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, 1084 | {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, 1085 | {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, 1086 | {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, 1087 | {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, 1088 | {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, 1089 | {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, 1090 | {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, 1091 | {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, 1092 | {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, 1093 | {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, 1094 | {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, 1095 | {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, 1096 | {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, 1097 | {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, 1098 | {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, 1099 | {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, 1100 | {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, 1101 | {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, 1102 | {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, 1103 | {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, 1104 | {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"}, 1105 | {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, 1106 | {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, 1107 | {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, 1108 | {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, 1109 | {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, 1110 | {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, 1111 | {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, 1112 | {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, 1113 | {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, 1114 | {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"}, 1115 | {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, 1116 | {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, 1117 | {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, 1118 | {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, 1119 | {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, 1120 | {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, 1121 | {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, 1122 | ] 1123 | 1124 | [metadata] 1125 | lock-version = "2.0" 1126 | python-versions = "3.11.*" 1127 | content-hash = "21a21c72a4b4330f2cd03ce2fad8c09d3bf36f62c91c4529c9253c033b51c880" 1128 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "uvicorn-poetry" 3 | version = "3.3.0" 4 | description = "Docker image with Uvicorn ASGI server for running Python web applications on Kubernetes. Uses Poetry for managing dependencies and setting up a virtual environment." 5 | authors = ["Max Pfeiffer "] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = "3.11.*" 10 | click = "8.1.7" 11 | python-on-whales = "0.70.1" 12 | 13 | [tool.poetry.dev-dependencies] 14 | pytest = "8.1.1" 15 | pytest-cov = "5.0.0" 16 | pytest-dotenv = "0.5.2" 17 | coverage = "7.4.4" 18 | requests = "2.31.0" 19 | pre-commit = "3.6.2" 20 | semver = "3.0.2" 21 | testcontainers = "4.1.1" 22 | bcrypt = "4.1.2" 23 | docker-image-py = "0.1.12" 24 | furl = "2.1.3" 25 | 26 | [tool.ruff] 27 | exclude = [".venv"] 28 | 29 | [tool.ruff.lint] 30 | select = [ 31 | "F", # Pyflakes 32 | "E", # pycodestyle 33 | "W", # pycodestyle 34 | "I", # isort 35 | "D", # pydocstyle 36 | "UP", # pyupgrade 37 | "ASYNC", # flake8-async 38 | "RUF", # Ruff-specific rules 39 | ] 40 | 41 | [tool.ruff.lint.pydocstyle] 42 | convention = "pep257" 43 | 44 | # https://docs.pytest.org/en/latest/reference/customize.html 45 | [tool.pytest.ini_options] 46 | testpaths = [ 47 | "tests", 48 | ] 49 | 50 | # https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#configuration-via-a-file 51 | [tool.black] 52 | line-length = 80 53 | target-version = ["py39"] 54 | 55 | [tool.pylint.main] 56 | errors-only = true 57 | recursive = "y" 58 | ignore-paths = "^examples/.*$" 59 | 60 | [build-system] 61 | requires = ["poetry-core>=1.0.0"] 62 | build-backend = "poetry.core.masonry.api" 63 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests.""" 2 | -------------------------------------------------------------------------------- /tests/build_image/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for image build.""" 2 | -------------------------------------------------------------------------------- /tests/build_image/conftest.py: -------------------------------------------------------------------------------- 1 | """Test fixtures for image build.""" 2 | 3 | from os import getenv 4 | 5 | import pytest 6 | from python_on_whales import Builder, DockerClient 7 | 8 | from build.constants import APPLICATION_SERVER_PORT, PLATFORMS 9 | from build.utils import ( 10 | get_context, 11 | get_image_reference, 12 | get_python_poetry_image_reference, 13 | ) 14 | from tests.constants import REGISTRY_PASSWORD, REGISTRY_USERNAME 15 | from tests.registry_container import DockerRegistryContainer 16 | from tests.utils import ( 17 | get_fast_api_multistage_context, 18 | get_fast_api_multistage_image_reference, 19 | get_fast_api_multistage_with_json_logging_context, 20 | get_fast_api_multistage_with_json_logging_image_reference, 21 | get_fast_api_singlestage_context, 22 | get_fast_api_singlestage_image_reference, 23 | ) 24 | 25 | 26 | @pytest.fixture(scope="session") 27 | def cache_settings(python_version: str, os_variant: str) -> tuple: 28 | """Fixture for providing cache settings. 29 | 30 | :param python_version: 31 | :param os_variant: 32 | :return: 33 | """ 34 | github_ref_name: str = getenv("GITHUB_REF_NAME") 35 | cache_scope: str = f"{python_version}-{os_variant}" 36 | 37 | if github_ref_name: 38 | cache_to: str = f"type=gha,mode=max,scope={github_ref_name}-{cache_scope}" 39 | cache_from: str = f"type=gha,scope={github_ref_name}-{cache_scope}" 40 | else: 41 | cache_to = f"type=local,mode=max,dest=/tmp,scope={cache_scope}" 42 | cache_from = f"type=local,src=/tmp,scope={cache_scope}" 43 | 44 | return cache_to, cache_from 45 | 46 | 47 | @pytest.fixture(scope="package") 48 | def registry_container() -> DockerRegistryContainer: 49 | """Fixture for providing a running registry container. 50 | 51 | :return: 52 | """ 53 | registry_container = DockerRegistryContainer( 54 | username=REGISTRY_USERNAME, password=REGISTRY_PASSWORD 55 | ).with_bind_ports(5000, 5000) 56 | registry_container.start() 57 | yield registry_container 58 | registry_container.stop() 59 | 60 | 61 | @pytest.fixture(scope="package") 62 | def registry_login( 63 | docker_client: DockerClient, registry_container: DockerRegistryContainer 64 | ) -> None: 65 | """Fixture login into registry container. 66 | 67 | :param docker_client: 68 | :param registry_container: 69 | :return: 70 | """ 71 | docker_client.login( 72 | server=registry_container.get_registry(), 73 | username=REGISTRY_USERNAME, 74 | password=REGISTRY_PASSWORD, 75 | ) 76 | 77 | 78 | @pytest.fixture(scope="package") 79 | def base_image_reference( 80 | docker_client: DockerClient, 81 | pow_buildx_builder: Builder, 82 | image_version: str, 83 | registry_container: DockerRegistryContainer, 84 | python_version: str, 85 | os_variant: str, 86 | cache_settings: tuple, 87 | registry_login, 88 | ) -> str: 89 | """Fixture providing a base image build. 90 | 91 | :param docker_client: 92 | :param pow_buildx_builder: 93 | :param image_version: 94 | :param registry_container: 95 | :param python_version: 96 | :param os_variant: 97 | :param cache_settings: 98 | :param registry_login: 99 | :return: 100 | """ 101 | image_reference: str = get_image_reference( 102 | registry_container.get_registry(), 103 | image_version, 104 | python_version, 105 | os_variant, 106 | ) 107 | 108 | docker_client.buildx.build( 109 | context_path=get_context(), 110 | build_args={ 111 | "BASE_IMAGE": get_python_poetry_image_reference(python_version, os_variant), 112 | "APPLICATION_SERVER_PORT": APPLICATION_SERVER_PORT, 113 | }, 114 | tags=image_reference, 115 | platforms=PLATFORMS, 116 | builder=pow_buildx_builder, 117 | cache_to=cache_settings[0], 118 | cache_from=cache_settings[1], 119 | push=True, 120 | ) 121 | yield image_reference 122 | 123 | 124 | @pytest.fixture(scope="package") 125 | def fast_api_singlestage_image_reference( 126 | docker_client: DockerClient, 127 | pow_buildx_builder: Builder, 128 | registry_container: DockerRegistryContainer, 129 | image_version: str, 130 | python_version: str, 131 | os_variant: str, 132 | cache_settings: tuple, 133 | base_image_reference: str, 134 | registry_login, 135 | ) -> str: 136 | """Fixture providing a single stage image build for example application. 137 | 138 | :param docker_client: 139 | :param pow_buildx_builder: 140 | :param registry_container: 141 | :param image_version: 142 | :param python_version: 143 | :param os_variant: 144 | :param cache_settings: 145 | :param base_image_reference: 146 | :param registry_login: 147 | :return: 148 | """ 149 | image_reference: str = get_fast_api_singlestage_image_reference( 150 | registry_container.get_registry(), 151 | image_version, 152 | python_version, 153 | os_variant, 154 | ) 155 | 156 | docker_client.buildx.build( 157 | context_path=get_fast_api_singlestage_context(), 158 | build_args={ 159 | "BASE_IMAGE": base_image_reference, 160 | }, 161 | tags=image_reference, 162 | platforms=PLATFORMS, 163 | builder=pow_buildx_builder, 164 | cache_to=cache_settings[0], 165 | cache_from=cache_settings[1], 166 | push=True, 167 | ) 168 | yield image_reference 169 | 170 | 171 | @pytest.fixture(scope="package") 172 | def fast_api_multistage_image_reference( 173 | docker_client: DockerClient, 174 | pow_buildx_builder: Builder, 175 | registry_container: DockerRegistryContainer, 176 | image_version: str, 177 | python_version: str, 178 | os_variant: str, 179 | cache_settings: tuple, 180 | base_image_reference: str, 181 | registry_login, 182 | ) -> str: 183 | """Fixture providing a multi-stage image build for example application. 184 | 185 | :param docker_client: 186 | :param pow_buildx_builder: 187 | :param registry_container: 188 | :param image_version: 189 | :param python_version: 190 | :param os_variant: 191 | :param cache_settings: 192 | :param base_image_reference: 193 | :param registry_login: 194 | :return: 195 | """ 196 | image_reference: str = get_fast_api_multistage_image_reference( 197 | registry_container.get_registry(), 198 | image_version, 199 | python_version, 200 | os_variant, 201 | ) 202 | 203 | docker_client.buildx.build( 204 | context_path=get_fast_api_multistage_context(), 205 | target="production-image", 206 | build_args={ 207 | "BASE_IMAGE": base_image_reference, 208 | }, 209 | tags=image_reference, 210 | platforms=PLATFORMS, 211 | builder=pow_buildx_builder, 212 | cache_to=cache_settings[0], 213 | cache_from=cache_settings[1], 214 | push=True, 215 | ) 216 | yield image_reference 217 | 218 | 219 | @pytest.fixture(scope="package") 220 | def fast_api_multistage_with_json_logging_image_reference( 221 | docker_client: DockerClient, 222 | pow_buildx_builder: Builder, 223 | registry_container: DockerRegistryContainer, 224 | image_version: str, 225 | python_version: str, 226 | os_variant: str, 227 | cache_settings: tuple, 228 | base_image_reference: str, 229 | registry_login, 230 | ) -> str: 231 | """Fixture providing a multi-stage image build with JSON logging. 232 | 233 | :param docker_client: 234 | :param pow_buildx_builder: 235 | :param registry_container: 236 | :param image_version: 237 | :param python_version: 238 | :param os_variant: 239 | :param cache_settings: 240 | :param base_image_reference: 241 | :param registry_login: 242 | :return: 243 | """ 244 | image_reference: str = get_fast_api_multistage_with_json_logging_image_reference( 245 | registry_container.get_registry(), 246 | image_version, 247 | python_version, 248 | os_variant, 249 | ) 250 | 251 | docker_client.buildx.build( 252 | context_path=get_fast_api_multistage_with_json_logging_context(), 253 | target="production-image", 254 | build_args={ 255 | "BASE_IMAGE": base_image_reference, 256 | }, 257 | tags=image_reference, 258 | platforms=PLATFORMS, 259 | builder=pow_buildx_builder, 260 | cache_to=cache_settings[0], 261 | cache_from=cache_settings[1], 262 | push=True, 263 | ) 264 | yield image_reference 265 | -------------------------------------------------------------------------------- /tests/build_image/test_build_version.py: -------------------------------------------------------------------------------- 1 | """Tests checking the build version.""" 2 | 3 | from tests.utils import ImageTagComponents 4 | 5 | 6 | def test_build_version( 7 | base_image_reference: str, 8 | image_version: str, 9 | python_version: str, 10 | os_variant: str, 11 | ) -> None: 12 | """Test for checking the build version of base image. 13 | 14 | :param base_image_reference: 15 | :param image_version: 16 | :param python_version: 17 | :param os_variant: 18 | :return: 19 | """ 20 | components: ImageTagComponents = ImageTagComponents.create_from_reference( 21 | base_image_reference 22 | ) 23 | assert components.version == image_version 24 | assert components.python_version == python_version 25 | assert components.os_variant == os_variant 26 | 27 | 28 | def test_example_app_singlestage_build_version( 29 | fast_api_singlestage_image_reference: str, 30 | image_version: str, 31 | python_version: str, 32 | os_variant: str, 33 | ) -> None: 34 | """Test for checking the build version of single stage image. 35 | 36 | :param fast_api_singlestage_image_reference: 37 | :param image_version: 38 | :param python_version: 39 | :param os_variant: 40 | :return: 41 | """ 42 | components: ImageTagComponents = ImageTagComponents.create_from_reference( 43 | fast_api_singlestage_image_reference 44 | ) 45 | assert components.version == image_version 46 | assert components.python_version == python_version 47 | assert components.os_variant == os_variant 48 | 49 | 50 | def test_example_app_multistage__build_version( 51 | fast_api_multistage_image_reference: str, 52 | image_version: str, 53 | python_version: str, 54 | os_variant: str, 55 | ) -> None: 56 | """Test for checking the build version of multi-stage image. 57 | 58 | :param fast_api_multistage_image_reference: 59 | :param image_version: 60 | :param python_version: 61 | :param os_variant: 62 | :return: 63 | """ 64 | components: ImageTagComponents = ImageTagComponents.create_from_reference( 65 | fast_api_multistage_image_reference 66 | ) 67 | assert components.version == image_version 68 | assert components.python_version == python_version 69 | assert components.os_variant == os_variant 70 | 71 | 72 | def test_example_app_with_json_logging_build_version( 73 | fast_api_multistage_with_json_logging_image_reference: str, 74 | image_version: str, 75 | python_version: str, 76 | os_variant: str, 77 | ) -> None: 78 | """Test for checking the build version of multi-stage image with JSON logging. 79 | 80 | :param fast_api_multistage_with_json_logging_image_reference: 81 | :param image_version: 82 | :param python_version: 83 | :param os_variant: 84 | :return: 85 | """ 86 | components: ImageTagComponents = ImageTagComponents.create_from_reference( 87 | fast_api_multistage_with_json_logging_image_reference 88 | ) 89 | assert components.version == image_version 90 | assert components.python_version == python_version 91 | assert components.os_variant == os_variant 92 | -------------------------------------------------------------------------------- /tests/build_image/test_default_configuration.py: -------------------------------------------------------------------------------- 1 | """Tests for default configuration.""" 2 | 3 | import json 4 | from time import sleep 5 | 6 | import requests 7 | from python_on_whales import DockerClient 8 | 9 | from build.constants import APPLICATION_SERVER_PORT 10 | from tests.constants import ( 11 | DEFAULT_UVICORN_CONFIG, 12 | EXPOSED_CONTAINER_PORT, 13 | HELLO_WORLD, 14 | SLEEP_TIME, 15 | ) 16 | from tests.utils import UvicornPoetryContainerConfig 17 | 18 | 19 | def test_fast_api_singlestage_image( 20 | docker_client: DockerClient, 21 | fast_api_singlestage_image_reference: str, 22 | ) -> None: 23 | """Test default configuration for single stage image. 24 | 25 | :param docker_client: 26 | :param fast_api_singlestage_image_reference: 27 | :return: 28 | """ 29 | with docker_client.container.run( 30 | fast_api_singlestage_image_reference, 31 | detach=True, 32 | publish=[(EXPOSED_CONTAINER_PORT, APPLICATION_SERVER_PORT)], 33 | ) as container: 34 | # Wait for uvicorn to come up 35 | sleep(SLEEP_TIME) 36 | 37 | uvicorn_gunicorn_container_config: UvicornPoetryContainerConfig = ( 38 | UvicornPoetryContainerConfig(container.id) 39 | ) 40 | 41 | assert f"{APPLICATION_SERVER_PORT}/tcp" in container.config.exposed_ports.keys() 42 | 43 | response = requests.get(f"http://127.0.0.1:{EXPOSED_CONTAINER_PORT}") 44 | assert json.loads(response.text) == HELLO_WORLD 45 | 46 | config_data: dict[str, str] = ( 47 | uvicorn_gunicorn_container_config.get_uvicorn_conf() 48 | ) 49 | assert config_data["workers"] == DEFAULT_UVICORN_CONFIG["workers"] 50 | assert config_data["host"] == DEFAULT_UVICORN_CONFIG["host"] 51 | assert config_data["port"] == DEFAULT_UVICORN_CONFIG["port"] 52 | 53 | 54 | def test_fast_api_multistage_image( 55 | docker_client: DockerClient, 56 | fast_api_multistage_image_reference: str, 57 | ) -> None: 58 | """Test default configuration for multi-stage image. 59 | 60 | :param docker_client: 61 | :param fast_api_multistage_image_reference: 62 | :return: 63 | """ 64 | with docker_client.container.run( 65 | fast_api_multistage_image_reference, 66 | detach=True, 67 | publish=[(EXPOSED_CONTAINER_PORT, APPLICATION_SERVER_PORT)], 68 | ) as container: 69 | # Wait for uvicorn to come up 70 | sleep(SLEEP_TIME) 71 | 72 | uvicorn_gunicorn_container_config: UvicornPoetryContainerConfig = ( 73 | UvicornPoetryContainerConfig(container.id) 74 | ) 75 | 76 | assert f"{APPLICATION_SERVER_PORT}/tcp" in container.config.exposed_ports.keys() 77 | 78 | response = requests.get(f"http://127.0.0.1:{EXPOSED_CONTAINER_PORT}") 79 | assert json.loads(response.text) == HELLO_WORLD 80 | 81 | config_data: dict[str, str] = ( 82 | uvicorn_gunicorn_container_config.get_uvicorn_conf() 83 | ) 84 | assert config_data["workers"] == DEFAULT_UVICORN_CONFIG["workers"] 85 | assert config_data["host"] == DEFAULT_UVICORN_CONFIG["host"] 86 | assert config_data["port"] == DEFAULT_UVICORN_CONFIG["port"] 87 | -------------------------------------------------------------------------------- /tests/build_image/test_json_logging_configuration.py: -------------------------------------------------------------------------------- 1 | """Tests for JSON logging configuration.""" 2 | 3 | import json 4 | from time import sleep 5 | 6 | import requests 7 | from python_on_whales import DockerClient 8 | 9 | from build.constants import APPLICATION_SERVER_PORT 10 | from tests.constants import ( 11 | DEFAULT_UVICORN_CONFIG, 12 | EXPOSED_CONTAINER_PORT, 13 | HELLO_WORLD, 14 | SLEEP_TIME, 15 | ) 16 | from tests.utils import UvicornPoetryContainerConfig 17 | 18 | 19 | def test_fast_api_multistage_with_json_logging_image( 20 | docker_client: DockerClient, 21 | fast_api_multistage_with_json_logging_image_reference: str, 22 | ) -> None: 23 | """Test JSON logging configuration with multi-stage image. 24 | 25 | :param docker_client: 26 | :param fast_api_multistage_with_json_logging_image_reference: 27 | :return: 28 | """ 29 | with docker_client.container.run( 30 | fast_api_multistage_with_json_logging_image_reference, 31 | detach=True, 32 | publish=[(EXPOSED_CONTAINER_PORT, APPLICATION_SERVER_PORT)], 33 | ) as container: 34 | # Wait for uvicorn to come up 35 | sleep(SLEEP_TIME) 36 | 37 | uvicorn_gunicorn_container_config: UvicornPoetryContainerConfig = ( 38 | UvicornPoetryContainerConfig(container.id) 39 | ) 40 | 41 | assert f"{APPLICATION_SERVER_PORT}/tcp" in container.config.exposed_ports.keys() 42 | 43 | response = requests.get(f"http://127.0.0.1:{EXPOSED_CONTAINER_PORT}") 44 | assert json.loads(response.text) == HELLO_WORLD 45 | 46 | config_data: dict[str, str] = ( 47 | uvicorn_gunicorn_container_config.get_uvicorn_conf() 48 | ) 49 | assert config_data["workers"] == DEFAULT_UVICORN_CONFIG["workers"] 50 | assert config_data["host"] == DEFAULT_UVICORN_CONFIG["host"] 51 | assert config_data["port"] == DEFAULT_UVICORN_CONFIG["port"] 52 | 53 | logs: str = container.logs() 54 | lines: list[str] = logs.splitlines() 55 | log_statement: dict = json.loads(lines[1]) 56 | assert log_statement["levelname"] == "INFO" 57 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Test fixtures.""" 2 | 3 | from os import getenv 4 | from random import randrange 5 | 6 | import pytest 7 | from python_on_whales import Builder, DockerClient 8 | from semver import VersionInfo 9 | 10 | 11 | @pytest.fixture(scope="session") 12 | def docker_client() -> DockerClient: 13 | """Fixture provides a Python-on-Whales docker client. 14 | 15 | :return: 16 | """ 17 | return DockerClient(debug=True) 18 | 19 | 20 | @pytest.fixture(scope="session") 21 | def pow_buildx_builder(docker_client: DockerClient) -> Builder: 22 | """Fixture for providing a Python-on-Whales buildx builder. 23 | 24 | :param docker_client: 25 | :return: 26 | """ 27 | builder: Builder = docker_client.buildx.create( 28 | driver="docker-container", driver_options=dict(network="host") 29 | ) 30 | yield builder 31 | docker_client.buildx.stop(builder) 32 | docker_client.buildx.remove(builder) 33 | 34 | 35 | @pytest.fixture(scope="session") 36 | def image_version() -> str: 37 | """Fixture providing a fake image version. 38 | 39 | :return: 40 | """ 41 | version: VersionInfo = VersionInfo( 42 | major=randrange(100), minor=randrange(100), patch=randrange(100) 43 | ) 44 | version_string: str = str(version) 45 | return version_string 46 | 47 | 48 | @pytest.fixture(scope="session") 49 | def python_version() -> str: 50 | """Fixture provides the Python version set in .env file. 51 | 52 | :return: 53 | """ 54 | return getenv("PYTHON_VERSION") 55 | 56 | 57 | @pytest.fixture(scope="session") 58 | def os_variant() -> str: 59 | """Fixture provides the OS variant set in .env file. 60 | 61 | :return: 62 | """ 63 | return getenv("OS_VARIANT") 64 | -------------------------------------------------------------------------------- /tests/constants.py: -------------------------------------------------------------------------------- 1 | """Contants for tests.""" 2 | 3 | from build.constants import APPLICATION_SERVER_PORT 4 | 5 | SLEEP_TIME: float = 4.0 6 | HELLO_WORLD: str = "Hello World!" 7 | DEFAULT_UVICORN_CONFIG: dict[str, str] = { 8 | "workers": "1", 9 | "host": "0.0.0.0", 10 | "port": APPLICATION_SERVER_PORT, 11 | } 12 | DEVELOPMENT_UVICORN_CONFIG: dict[str, str] = { 13 | "workers": "1", 14 | "host": "0.0.0.0", 15 | "port": APPLICATION_SERVER_PORT, 16 | } 17 | JSON_LOGGING_CONFIG: dict[str, str] = { 18 | "workers": "1", 19 | "host": "0.0.0.0", 20 | "port": APPLICATION_SERVER_PORT, 21 | } 22 | EXPOSED_CONTAINER_PORT: str = "80" 23 | REGISTRY_USERNAME: str = "foo" 24 | REGISTRY_PASSWORD: str = "bar" 25 | -------------------------------------------------------------------------------- /tests/publish_image/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for image publishing.""" 2 | -------------------------------------------------------------------------------- /tests/publish_image/conftest.py: -------------------------------------------------------------------------------- 1 | """Test fixtures for image publishing.""" 2 | 3 | import pytest 4 | from click.testing import CliRunner 5 | 6 | 7 | @pytest.fixture(scope="package") 8 | def cli_runner() -> CliRunner: 9 | """Fixture providing a Click CLI runner. 10 | 11 | :return: 12 | """ 13 | runner = CliRunner() 14 | return runner 15 | -------------------------------------------------------------------------------- /tests/publish_image/test_cli.py: -------------------------------------------------------------------------------- 1 | """Tests for image publishing using CLI options.""" 2 | 3 | from click.testing import CliRunner, Result 4 | from python_on_whales import DockerException 5 | 6 | from build.publish import main 7 | from tests.constants import REGISTRY_PASSWORD, REGISTRY_USERNAME 8 | from tests.registry_container import DockerRegistryContainer 9 | 10 | 11 | def test_registry_with_credentials( 12 | cli_runner: CliRunner, 13 | image_version: str, 14 | python_version: str, 15 | os_variant: str, 16 | ): 17 | """Test for using a Docker registry with credentials. 18 | 19 | :param cli_runner: 20 | :param image_version: 21 | :param python_version: 22 | :param os_variant: 23 | :return: 24 | """ 25 | with DockerRegistryContainer( 26 | username=REGISTRY_USERNAME, password=REGISTRY_PASSWORD 27 | ).with_bind_ports(5000, 5000) as docker_registry: 28 | result: Result = cli_runner.invoke( 29 | main, 30 | args=[ 31 | "--docker-hub-username", 32 | REGISTRY_USERNAME, 33 | "--docker-hub-password", 34 | REGISTRY_PASSWORD, 35 | "--version-tag", 36 | image_version, 37 | "--python-version", 38 | python_version, 39 | "--os-variant", 40 | os_variant, 41 | "--registry", 42 | docker_registry.get_registry(), 43 | ], 44 | ) 45 | assert result.exit_code == 0 46 | 47 | 48 | def test_registry_with_wrong_credentials( 49 | cli_runner: CliRunner, 50 | image_version: str, 51 | python_version: str, 52 | os_variant: str, 53 | ): 54 | """Test for using a Docker registry with credentials. 55 | 56 | :param cli_runner: 57 | :param image_version: 58 | :param python_version: 59 | :param os_variant: 60 | :return: 61 | """ 62 | with DockerRegistryContainer( 63 | username=REGISTRY_USERNAME, password=REGISTRY_PASSWORD 64 | ).with_bind_ports(5000, 5000) as docker_registry: 65 | result: Result = cli_runner.invoke( 66 | main, 67 | args=[ 68 | "--docker-hub-username", 69 | "bang", 70 | "--docker-hub-password", 71 | "boom", 72 | "--version-tag", 73 | image_version, 74 | "--python-version", 75 | python_version, 76 | "--os-variant", 77 | os_variant, 78 | "--registry", 79 | docker_registry.get_registry(), 80 | ], 81 | ) 82 | assert result.exit_code == 1 83 | assert isinstance(result.exception, DockerException) 84 | -------------------------------------------------------------------------------- /tests/publish_image/test_cli_and_env.py: -------------------------------------------------------------------------------- 1 | """Tests for using CLI aptions and environment variables.""" 2 | 3 | from click.testing import CliRunner, Result 4 | 5 | from build.publish import main 6 | 7 | 8 | def test_missing_options_and_env(cli_runner: CliRunner) -> None: 9 | """Test if image publishing fails without providing anything. 10 | 11 | :param cli_runner: 12 | :return: 13 | """ 14 | result: Result = cli_runner.invoke(main) 15 | assert result.exit_code == 2 16 | -------------------------------------------------------------------------------- /tests/publish_image/test_env.py: -------------------------------------------------------------------------------- 1 | """Tests for image publishing using environment variables.""" 2 | 3 | from click.testing import CliRunner, Result 4 | from python_on_whales import DockerException 5 | 6 | from build.publish import main 7 | from tests.constants import REGISTRY_PASSWORD, REGISTRY_USERNAME 8 | from tests.registry_container import DockerRegistryContainer 9 | 10 | 11 | def test_registry_with_credentials( 12 | cli_runner: CliRunner, 13 | image_version: str, 14 | python_version: str, 15 | os_variant: str, 16 | ): 17 | """Test for using a Docker registry with credentials. 18 | 19 | :param cli_runner: 20 | :param image_version: 21 | :param python_version: 22 | :param os_variant: 23 | :return: 24 | """ 25 | with DockerRegistryContainer( 26 | username=REGISTRY_USERNAME, password=REGISTRY_PASSWORD 27 | ).with_bind_ports(5000, 5000) as docker_registry: 28 | result: Result = cli_runner.invoke( 29 | main, 30 | env={ 31 | "DOCKER_HUB_USERNAME": REGISTRY_USERNAME, 32 | "DOCKER_HUB_PASSWORD": REGISTRY_PASSWORD, 33 | "GIT_TAG_NAME": image_version, 34 | "PYTHON_VERSION": python_version, 35 | "OS_VARIANT": os_variant, 36 | "REGISTRY": docker_registry.get_registry(), 37 | }, 38 | ) 39 | assert result.exit_code == 0 40 | 41 | 42 | def test_registry_with_wrong_credentials( 43 | cli_runner: CliRunner, 44 | image_version: str, 45 | python_version: str, 46 | os_variant: str, 47 | ): 48 | """Test for using a Docker registry with credentials. 49 | 50 | :param cli_runner: 51 | :param image_version: 52 | :param python_version: 53 | :param os_variant: 54 | :return: 55 | """ 56 | with DockerRegistryContainer( 57 | username=REGISTRY_USERNAME, password=REGISTRY_PASSWORD 58 | ).with_bind_ports(5000, 5000) as docker_registry: 59 | result: Result = cli_runner.invoke( 60 | main, 61 | env={ 62 | "DOCKER_HUB_USERNAME": "boom", 63 | "DOCKER_HUB_PASSWORD": "bang", 64 | "GIT_TAG_NAME": image_version, 65 | "PYTHON_VERSION": python_version, 66 | "OS_VARIANT": os_variant, 67 | "REGISTRY": docker_registry.get_registry(), 68 | }, 69 | ) 70 | assert result.exit_code == 1 71 | assert isinstance(result.exception, DockerException) 72 | -------------------------------------------------------------------------------- /tests/registry_container.py: -------------------------------------------------------------------------------- 1 | """Docker registry test container.""" 2 | 3 | import time 4 | from io import BytesIO 5 | from tarfile import TarFile, TarInfo 6 | from typing import Optional 7 | 8 | import bcrypt 9 | from requests import Response, get 10 | from requests.auth import HTTPBasicAuth 11 | from requests.exceptions import ConnectionError, ReadTimeout 12 | from testcontainers.core.container import DockerContainer 13 | from testcontainers.core.waiting_utils import wait_container_is_ready 14 | 15 | 16 | class DockerRegistryContainer(DockerContainer): 17 | """A test container providing a Docker registry.""" 18 | 19 | # https://docs.docker.com/registry/ 20 | credentials_path: str = "/htpasswd/credentials.txt" 21 | 22 | def __init__( 23 | self, 24 | image: str = "registry:2", 25 | port: int = 5000, 26 | username: Optional[str] = None, 27 | password: Optional[str] = None, 28 | **kwargs, 29 | ) -> None: 30 | """Class initializer. 31 | 32 | :param image: 33 | :param port: 34 | :param username: 35 | :param password: 36 | :param kwargs: 37 | """ 38 | super().__init__(image=image, **kwargs) 39 | self.port: int = port 40 | self.username: Optional[str] = username 41 | self.password: Optional[str] = password 42 | self.with_exposed_ports(self.port) 43 | 44 | def _copy_credentials(self) -> None: 45 | """Create credentials and write them to the container. 46 | 47 | :return: 48 | """ 49 | hashed_password: str = bcrypt.hashpw( 50 | self.password.encode("utf-8"), 51 | bcrypt.gensalt(rounds=12, prefix=b"2a"), 52 | ).decode("utf-8") 53 | content = f"{self.username}:{hashed_password}".encode() 54 | 55 | with BytesIO() as tar_archive_object, TarFile( 56 | fileobj=tar_archive_object, mode="w" 57 | ) as tmp_tarfile: 58 | tarinfo: TarInfo = TarInfo(name=self.credentials_path) 59 | tarinfo.size = len(content) 60 | tarinfo.mtime = time.time() 61 | 62 | tmp_tarfile.addfile(tarinfo, BytesIO(content)) 63 | tar_archive_object.seek(0) 64 | self.get_wrapped_container().put_archive("/", tar_archive_object) 65 | 66 | @wait_container_is_ready(ConnectionError, ReadTimeout) 67 | def _readiness_probe(self) -> None: 68 | """Readiness probe for container. 69 | 70 | :return: 71 | """ 72 | url: str = f"http://{self.get_registry()}/v2" 73 | if self.username and self.password: 74 | response: Response = get( 75 | url, auth=HTTPBasicAuth(self.username, self.password), timeout=1 76 | ) 77 | else: 78 | response: Response = get(url, timeout=1) 79 | response.raise_for_status() 80 | 81 | def start(self): 82 | """Start container. 83 | 84 | :return: 85 | """ 86 | if self.username and self.password: 87 | self.with_env("REGISTRY_AUTH_HTPASSWD_REALM", "local-registry") 88 | self.with_env("REGISTRY_AUTH_HTPASSWD_PATH", self.credentials_path) 89 | super().start() 90 | self._copy_credentials() 91 | else: 92 | super().start() 93 | 94 | self._readiness_probe() 95 | return self 96 | 97 | def get_registry(self) -> str: 98 | """Return registry host and port. 99 | 100 | :return: 101 | """ 102 | host: str = self.get_container_host_ip() 103 | port: str = self.get_exposed_port(self.port) 104 | return f"{host}:{port}" 105 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | """Utilities for tests.""" 2 | 3 | from dataclasses import dataclass 4 | from pathlib import Path 5 | 6 | import docker 7 | from docker.models.containers import Container 8 | from docker_image import reference 9 | 10 | 11 | class UvicornPoetryContainerConfig: 12 | """Class for providing container configuration.""" 13 | 14 | def __init__(self, container_id: str): 15 | """Class initializer. 16 | 17 | :param container_id: 18 | """ 19 | self.container: Container = docker.from_env().containers.get(container_id) 20 | 21 | def get_uvicorn_processes(self) -> list[str]: 22 | """Return uvicorn processes. 23 | 24 | :return: 25 | """ 26 | top = self.container.top() 27 | process_commands: list[str] = [p[7] for p in top["Processes"]] 28 | uvicorn_processes: list[str] = [ 29 | p for p in process_commands if "/application_root/.venv/bin/uvicorn" in p 30 | ] 31 | return uvicorn_processes 32 | 33 | def get_uvicorn_conf(self) -> dict[str, any]: 34 | """Return uvicorn configuration. 35 | 36 | :return: 37 | """ 38 | uvicorn_config: dict[str, any] = {} 39 | uvicorn_processes = self.get_uvicorn_processes() 40 | first_process = uvicorn_processes[0] 41 | 42 | first_part: str 43 | partition: str 44 | last_part: str 45 | first_part, partition, last_part = first_process.partition( 46 | "/application_root/.venv/bin/uvicorn" 47 | ) 48 | 49 | uvicorn_arguments: list[str] = last_part.strip().split() 50 | app: str = uvicorn_arguments.pop() 51 | uvicorn_config["app"] = app 52 | 53 | for index, element in enumerate(uvicorn_arguments): 54 | option: str 55 | value: any 56 | if element.startswith("--"): 57 | option = element.lstrip("--") 58 | try: 59 | next_element = uvicorn_arguments[index + 1] 60 | if next_element.startswith("--"): 61 | # It is an option without value 62 | value = True 63 | else: 64 | # add the value for the current option 65 | value = next_element 66 | except IndexError: 67 | # It is an option without value at the end of options list 68 | value = True 69 | 70 | uvicorn_config[option] = value 71 | return uvicorn_config 72 | 73 | 74 | @dataclass 75 | class ImageTagComponents: 76 | """Class for parsing and providing image tag components.""" 77 | 78 | registry: str 79 | image_name: str 80 | tag: str 81 | version: str 82 | python_version: str 83 | os_variant: str 84 | 85 | @classmethod 86 | def create_from_reference(cls, tag: str): 87 | """Instantiate a class using an image tag. 88 | 89 | :param tag: 90 | :return: 91 | """ 92 | ref = reference.Reference.parse(tag) 93 | registry: str = ref.repository["domain"] 94 | image_name: str = ref.repository["path"] 95 | tag: str = ref["tag"] 96 | 97 | tag_parts: list[str] = tag.split("-") 98 | version: str = tag_parts[0] 99 | python_version: str = tag_parts[1].lstrip("python") 100 | os_variant: str = "-".join(tag_parts[2:]) 101 | return cls( 102 | registry=registry, 103 | image_name=image_name, 104 | tag=tag, 105 | version=version, 106 | python_version=python_version, 107 | os_variant=os_variant, 108 | ) 109 | 110 | 111 | def get_fast_api_singlestage_context() -> Path: 112 | """Return Docker build context for single stage example app. 113 | 114 | :return: 115 | """ 116 | context: Path = ( 117 | Path(__file__).parent.parent.resolve() 118 | / "examples" 119 | / "fast_api_singlestage_build" 120 | ) 121 | return context 122 | 123 | 124 | def get_fast_api_singlestage_image_reference( 125 | registry: str, 126 | image_version: str, 127 | python_version: str, 128 | os_variant: str, 129 | ) -> str: 130 | """Return image reference for single stage example app. 131 | 132 | :param registry: 133 | :param image_version: 134 | :param python_version: 135 | :param os_variant: 136 | :return: 137 | """ 138 | reference: str = ( 139 | f"{registry}/fast-api-singlestage-build:{image_version}" 140 | f"-python{python_version}-{os_variant}" 141 | ) 142 | return reference 143 | 144 | 145 | def get_fast_api_multistage_context() -> Path: 146 | """Return Docker build context for multi-stage example app. 147 | 148 | :return: 149 | """ 150 | context: Path = ( 151 | Path(__file__).parent.parent.resolve() 152 | / "examples" 153 | / "fast_api_multistage_build" 154 | ) 155 | return context 156 | 157 | 158 | def get_fast_api_multistage_image_reference( 159 | registry: str, 160 | image_version: str, 161 | python_version: str, 162 | os_variant: str, 163 | ) -> str: 164 | """Return image reference for multi-stage example app. 165 | 166 | :param registry: 167 | :param image_version: 168 | :param python_version: 169 | :param os_variant: 170 | :return: 171 | """ 172 | reference: str = ( 173 | f"{registry}/fast-api-multistage-build:{image_version}" 174 | f"-python{python_version}-{os_variant}" 175 | ) 176 | return reference 177 | 178 | 179 | def get_fast_api_multistage_with_json_logging_context() -> Path: 180 | """Return Docker build context for multi-stage example app with JSON logging. 181 | 182 | :return: 183 | """ 184 | context: Path = ( 185 | Path(__file__).parent.parent.resolve() 186 | / "examples" 187 | / "fast_api_multistage_build_with_json_logging" 188 | ) 189 | return context 190 | 191 | 192 | def get_fast_api_multistage_with_json_logging_image_reference( 193 | registry: str, 194 | image_version: str, 195 | python_version: str, 196 | os_variant: str, 197 | ) -> str: 198 | """Return image reference for multi-stage example app with JSON logging. 199 | 200 | :param registry: 201 | :param image_version: 202 | :param python_version: 203 | :param os_variant: 204 | :return: 205 | """ 206 | reference: str = ( 207 | f"{registry}/fast_api_multistage_build_with_json_logging:{image_version}" 208 | f"-python{python_version}-{os_variant}" 209 | ) 210 | return reference 211 | --------------------------------------------------------------------------------