├── .env-example ├── .github └── workflows │ └── pipeline.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── build ├── Dockerfile ├── __init__.py ├── constants.py ├── gunicorn_configuration.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_singlestage_build │ ├── .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 ├── poetry.lock ├── pyproject.toml └── tests ├── __init__.py ├── build_image ├── __init__.py ├── conftest.py ├── test_build_version.py └── test_default_configuration.py ├── conftest.py ├── constants.py ├── publish_image ├── __init__.py ├── conftest.py ├── test_cli.py ├── test_cli_and_env.py └── test_env.py └── utils.py /.env-example: -------------------------------------------------------------------------------- 1 | PYTHON_VERSION=3.12.2 2 | OS_VARIANT=slim-bookworm 3 | -------------------------------------------------------------------------------- /.github/workflows/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: Pipeline 2 | 3 | on: push 4 | 5 | jobs: 6 | code-quality: 7 | runs-on: ubuntu-22.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.8.2 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-22.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.8.2 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-22.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.8.2 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-22.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: upload-test-coverage-reports 150 | if: startsWith(github.ref, 'refs/tags/') 151 | runs-on: ubuntu-22.04 152 | strategy: 153 | fail-fast: false 154 | matrix: 155 | python_version: ["3.10.14", "3.11.8", "3.12.2"] 156 | os_variant: ["bookworm", "slim-bookworm"] 157 | steps: 158 | - name: Checkout Repository 159 | uses: actions/checkout@v4 160 | - name: Expose GitHub Runtime 161 | uses: crazy-max/ghaction-github-runtime@v3 162 | - name: Get Git Commit Tag Name 163 | uses: olegtarasov/get-tag@v2.1 164 | - name: Setup Python 165 | uses: actions/setup-python@v4 166 | with: 167 | python-version: 3.11 168 | - name: Install Poetry 169 | uses: snok/install-poetry@v1 170 | with: 171 | version: 1.8.2 172 | virtualenvs-in-project: true 173 | - name: Load cached venv 174 | id: cached-poetry-dependencies 175 | uses: actions/cache@v3 176 | with: 177 | path: .venv 178 | key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} 179 | - name: Install dependencies 180 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 181 | run: | 182 | poetry install --no-interaction --no-root 183 | - name: Publish Image to Docker Hub 184 | env: 185 | DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} 186 | DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} 187 | PYTHON_VERSION: ${{ matrix.python_version }} 188 | OS_VARIANT: ${{ matrix.os_variant }} 189 | POETRY_VERSION: ${{ matrix.poetry_version }} 190 | run: | 191 | source .venv/bin/activate 192 | python -m build.publish 193 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .venv 3 | .idea 4 | .vscode 5 | .pytest_cache 6 | __pycache__ 7 | -------------------------------------------------------------------------------- /.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/psf/black 10 | rev: 24.4.0 11 | hooks: 12 | - id: black 13 | -------------------------------------------------------------------------------- /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 | [![codecov](https://codecov.io/gh/max-pfeiffer/uvicorn-gunicorn-poetry/graph/badge.svg?token=ZRUKVNP3I5)](https://codecov.io/gh/max-pfeiffer/uvicorn-gunicorn-poetry) 2 | ![pipeline workflow](https://github.com/max-pfeiffer/uvicorn-gunicorn-poetry/actions/workflows/pipeline.yml/badge.svg) 3 | ![Docker Image Size (latest semver)](https://img.shields.io/docker/image-size/pfeiffermax/uvicorn-gunicorn-poetry?sort=semver) 4 | ![Docker Pulls](https://img.shields.io/docker/pulls/pfeiffermax/uvicorn-gunicorn-poetry) 5 | # uvicorn-gunicorn-poetry - Docker image for FastAPI 6 | This Docker image provides a platform to run Python applications with [Gunicorn](https://gunicorn.org) as process manager and 7 | [Uvicorn](https://www.uvicorn.org/) workers. 8 | It provides [Poetry](https://python-poetry.org/) for managing dependencies and setting up a virtual environment in the container. 9 | 10 | This image aims to follow the best practices for a production grade container image for hosting Python web applications based 11 | on micro frameworks like [FastAPI](https://fastapi.tiangolo.com/). 12 | Therefore, source and documentation contain a lot of references to documentation of dependencies used in this project, so users 13 | of this image can follow up on that. 14 | 15 | If you would like to run your Python application with Uvicorn on [Kubernetes](https://kubernetes.io/), please check out my other project which does not use 16 | Gunicorn as process manager: https://github.com/max-pfeiffer/uvicorn-poetry 17 | 18 | Any feedback is highly appreciated and will be considered. 19 | 20 | **Docker Hub:** [pfeiffermax/uvicorn-gunicorn-poetry](https://hub.docker.com/r/pfeiffermax/uvicorn-gunicorn-poetry) 21 | 22 | **GitHub Repository:** [https://github.com/max-pfeiffer/uvicorn-gunicorn-poetry](https://github.com/max-pfeiffer/uvicorn-gunicorn-poetry) 23 | 24 | ## Docker Image Features 25 | 1. Poetry v1.8.2 is available as Python package dependency management tool 26 | 2. A virtual environment for the application and application server 27 | 3. The application is run with [Gunicorn](https://gunicorn.org/) and Uvicorn workers 28 | 4. Python versions: 29 | 1. 3.10 30 | 2. 3.11 31 | 3. 3.12 32 | 5. Operating system variants: 33 | 1. [Debian Bookworm v12.1](https://www.debian.org/releases/bookworm/) 34 | 2. [Debian Bookworm slim v12.1](https://www.debian.org/releases/bookworm/) 35 | 6. Supported CPU architectures: 36 | 1. linux/amd64 37 | 2. linux/arm64/v8 38 | 39 | ## Usage 40 | It just provides a platform that you can use to build upon your own multistage builds. So it consequently does not contain an 41 | application itself. 42 | 43 | Please check out the [example application for multistage builds](https://github.com/max-pfeiffer/uvicorn-gunicorn-poetry/tree/master/examples/fast_api_multistage_build) 44 | on how to use that image and build containers efficiently. 45 | 46 | There is also another [sample app demonstrating a very simple single stage build](https://github.com/max-pfeiffer/uvicorn-gunicorn-poetry/tree/main/examples/fast_api_singlestage_build). 47 | 48 | Please be aware that your application needs an application layout without src folder which is proposed in 49 | [fastapi-realworld-example-app](https://github.com/nsidnev/fastapi-realworld-example-app). 50 | The application and test structure needs to be like that: 51 | ```bash 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.10" 69 | * gunicorn = "22.0.0" 70 | * uvicorn = "0.29.0" 71 | 72 | If your application uses FastAPI framework this needs to be added as well: 73 | * fastapi = "0.110.2" 74 | 75 | **IMPORTANT:** make sure you have a [.dockerignore file](https://github.com/max-pfeiffer/uvicorn-gunicorn-poetry/blob/master/examples/fast_api_multistage_build/.dockerignore) 76 | in your application root which excludes your local virtual environment in .venv! Otherwise, you will have an issue activating that virtual 77 | environment when running the container. 78 | 79 | ## Configuration 80 | Configuration is done through the following environment variables during docker build. 81 | For all the following configuration options please see always the 82 | [official Gunicorn documentation](https://docs.gunicorn.org/en/stable/settings.html) 83 | if you would like to do a deep dive. Following environment variables are supported: 84 | 85 | ### [Logging](https://docs.gunicorn.org/en/stable/settings.html#logging) 86 | `LOG_LEVEL` : The granularity of Error log outputs. Valid level names are: 87 | * debug 88 | * info 89 | * warning 90 | * error 91 | * critical 92 | 93 | **default:** `info` 94 | 95 | `ACCESS_LOG` : The Access log file to write to. 96 | 97 | **default:** `-` 98 | 99 | `ERROR_LOG` : The Error log file to write to. 100 | 101 | **default:** `-` 102 | 103 | ### [Worker processes](https://docs.gunicorn.org/en/stable/settings.html#worker-processes) 104 | `WORKERS` : The number of worker processes for handling requests. By default, this is set to one 105 | worker as this image is meant to be used on a production grade Kubernetes environment. There you 106 | have usually monitoring data exported to Prometheus which will not work properly with multiple workers. 107 | 108 | **default:** `1` 109 | 110 | `TIMEOUT` : Workers silent for more than this many seconds are killed and restarted. 111 | 112 | **default:** `30` 113 | 114 | `GRACEFUL_TIMEOUT` : Timeout for graceful workers restart. 115 | 116 | **default:** `30` 117 | 118 | `KEEP_ALIVE` : The number of seconds to wait for requests on a Keep-Alive connection. 119 | 120 | **default:** `2` 121 | 122 | ### [Server mechanics](https://docs.gunicorn.org/en/stable/settings.html?highlight=worker_tmp_dir#worker-tmp-dir) 123 | `WORKER_TMP_DIR` : A directory to use for the worker heartbeat temporary file. 124 | By default, this is set to /dev/shm to speed up the startup of workers by using a in memory file system 125 | 126 | **default:** `/dev/shm` 127 | 128 | ### [Server socket](https://docs.gunicorn.org/en/stable/settings.html?highlight=bind#bind) 129 | `BIND` : The socket to bind. 130 | 131 | **default:** `0.0.0.0:8000` 132 | -------------------------------------------------------------------------------- /build/Dockerfile: -------------------------------------------------------------------------------- 1 | # The Poetry installation is provided through the base image. Please check 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 | # Copy the application server configuration 42 | COPY --chown=python_application:python_application gunicorn_configuration.py ${PYTHONPATH} 43 | 44 | # Document the exposed port 45 | # https://docs.docker.com/engine/reference/builder/#expose 46 | EXPOSE ${APPLICATION_SERVER_PORT} 47 | 48 | # Activate entrypoint for running the Gunicorn application server 49 | CMD exec gunicorn -k uvicorn.workers.UvicornWorker -c $PYTHONPATH/gunicorn_configuration.py app.main:app 50 | -------------------------------------------------------------------------------- /build/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-pfeiffer/uvicorn-gunicorn-poetry/ad8ee32e791e695b91318e9ca5c947f0011d4751/build/__init__.py -------------------------------------------------------------------------------- /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/gunicorn_configuration.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | DEFAULT_GUNICORN_CONFIG = { 5 | "bind": "0.0.0.0:8000", 6 | "workers": 1, 7 | "timeout": 30, 8 | "graceful_timeout": 30, 9 | "keepalive": 2, 10 | "loglevel": "info", 11 | "accesslog": "-", 12 | "errorlog": "-", 13 | "worker_tmp_dir": "/dev/shm", 14 | } 15 | 16 | # Logging 17 | # https://docs.gunicorn.org/en/stable/settings.html#logging 18 | loglevel = os.getenv("LOG_LEVEL", DEFAULT_GUNICORN_CONFIG["loglevel"]) 19 | accesslog = os.getenv("ACCESS_LOG", DEFAULT_GUNICORN_CONFIG["accesslog"]) 20 | errorlog = os.getenv("ERROR_LOG", DEFAULT_GUNICORN_CONFIG["errorlog"]) 21 | 22 | # Worker processes 23 | # https://docs.gunicorn.org/en/stable/settings.html#worker-processes 24 | workers = int(os.getenv("WORKERS", DEFAULT_GUNICORN_CONFIG["workers"])) 25 | timeout = int(os.getenv("TIMEOUT", DEFAULT_GUNICORN_CONFIG["timeout"])) 26 | graceful_timeout = int( 27 | os.getenv("GRACEFUL_TIMEOUT", DEFAULT_GUNICORN_CONFIG["graceful_timeout"]) 28 | ) 29 | keepalive = int(os.getenv("KEEP_ALIVE", DEFAULT_GUNICORN_CONFIG["keepalive"])) 30 | 31 | # Server machanics 32 | # https://docs.gunicorn.org/en/stable/settings.html?highlight=worker_tmp_dir#worker-tmp-dir 33 | # This is set to /dev/shm to speed up the startup of workers by using a in memory file system 34 | worker_tmp_dir = str( 35 | os.getenv("WORKER_TMP_DIR", DEFAULT_GUNICORN_CONFIG["worker_tmp_dir"]) 36 | ) 37 | 38 | # Server socket 39 | # https://docs.gunicorn.org/en/stable/settings.html?highlight=bind#bind 40 | bind = os.getenv("BIND", DEFAULT_GUNICORN_CONFIG["bind"]) 41 | 42 | log_data = { 43 | "bind": bind, 44 | "workers": workers, 45 | "timeout": timeout, 46 | "graceful_timeout": graceful_timeout, 47 | "keepalive": keepalive, 48 | "loglevel": loglevel, 49 | "errorlog": errorlog, 50 | "accesslog": accesslog, 51 | "worker_tmp_dir": worker_tmp_dir, 52 | } 53 | print(json.dumps(log_data)) 54 | -------------------------------------------------------------------------------- /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( 29 | "--version-tag", envvar="GIT_TAG_NAME", required=True, help="Version tag" 30 | ) 31 | @click.option( 32 | "--python-version", 33 | envvar="PYTHON_VERSION", 34 | required=True, 35 | help="Python version", 36 | ) 37 | @click.option( 38 | "--os-variant", 39 | envvar="OS_VARIANT", 40 | required=True, 41 | help="Operating system variant", 42 | ) 43 | @click.option( 44 | "--registry", envvar="REGISTRY", default="docker.io", help="Docker registry" 45 | ) 46 | def main( 47 | docker_hub_username: str, 48 | docker_hub_password: str, 49 | version_tag: str, 50 | python_version: str, 51 | os_variant: str, 52 | registry: str, 53 | ) -> None: 54 | """Build Docker image. 55 | 56 | :param docker_hub_username: 57 | :param docker_hub_password: 58 | :param version_tag: 59 | :param python_version: 60 | :param os_variant: 61 | :param registry: 62 | :return: 63 | """ 64 | github_ref_name: str = getenv("GITHUB_REF_NAME") 65 | context: Path = get_context() 66 | image_reference: str = get_image_reference( 67 | registry, version_tag, python_version, os_variant 68 | ) 69 | cache_scope: str = f"{python_version}-{os_variant}" 70 | 71 | if github_ref_name: 72 | cache_to: str = ( 73 | f"type=gha,mode=max,scope={github_ref_name}-{cache_scope}" 74 | ) 75 | cache_from: str = f"type=gha,scope={github_ref_name}-{cache_scope}" 76 | else: 77 | cache_to = f"type=local,mode=max,dest=/tmp,scope={cache_scope}" 78 | cache_from = f"type=local,src=/tmp,scope={cache_scope}" 79 | 80 | docker_client: DockerClient = DockerClient() 81 | builder: Builder = docker_client.buildx.create( 82 | driver="docker-container", driver_options=dict(network="host") 83 | ) 84 | 85 | docker_client.login( 86 | server=registry, 87 | username=docker_hub_username, 88 | password=docker_hub_password, 89 | ) 90 | 91 | docker_client.buildx.build( 92 | context_path=context, 93 | build_args={ 94 | "BASE_IMAGE": get_python_poetry_image_reference( 95 | python_version, os_variant 96 | ), 97 | "APPLICATION_SERVER_PORT": APPLICATION_SERVER_PORT, 98 | }, 99 | tags=image_reference, 100 | platforms=PLATFORMS, 101 | builder=builder, 102 | cache_to=cache_to, 103 | cache_from=cache_from, 104 | push=True, 105 | ) 106 | 107 | # Cleanup 108 | docker_client.buildx.stop(builder) 109 | docker_client.buildx.remove(builder) 110 | 111 | 112 | if __name__ == "__main__": 113 | # pylint: disable=no-value-for-parameter 114 | main() 115 | -------------------------------------------------------------------------------- /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-gunicorn-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 | .idea 2 | .pytest_cache 3 | .venv 4 | .coverage 5 | test_coverage_reports/ 6 | Dockerfile 7 | README.md 8 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build/.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | .pytest_cache 3 | __pycache__ 4 | .idea 5 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build/Dockerfile: -------------------------------------------------------------------------------- 1 | # A multistage build with a dependency build stage has the following advantages: 2 | # dependencies are only build when they actually change. Code changes do not 3 | # trigger the dependency build stage (if dependencies have been built before 4 | # once, build cache) 5 | ARG BASE_IMAGE_NAME_AND_TAG=pfeiffermax/uvicorn-gunicorn-poetry:2.2.0-python3.12.2-slim-bookworm 6 | FROM ${BASE_IMAGE_NAME_AND_TAG} as dependencies-build-stage 7 | 8 | # install [tool.poetry.dependencies] 9 | # this will install virtual environment into /.venv because of POETRY_VIRTUALENVS_IN_PROJECT=true 10 | # see: https://python-poetry.org/docs/configuration/#virtualenvsin-project 11 | COPY --chown=python_application:python_application ./poetry.lock ./pyproject.toml /application_root/ 12 | RUN poetry install --no-interaction --no-root --without dev 13 | 14 | FROM ${BASE_IMAGE_NAME_AND_TAG} as production-image 15 | 16 | # Copy virtual environment 17 | COPY --chown=python_application:python_application --from=dependencies-build-stage /application_root/.venv /application_root/.venv 18 | 19 | # Copy application files 20 | COPY --chown=python_application:python_application /app /application_root/app/ 21 | -------------------------------------------------------------------------------- /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-gunicorn-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-gunicorn-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: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-pfeiffer/uvicorn-gunicorn-poetry/ad8ee32e791e695b91318e9ca5c947f0011d4751/examples/fast_api_multistage_build/app/__init__.py -------------------------------------------------------------------------------- /examples/fast_api_multistage_build/app/main.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from fastapi import FastAPI 4 | 5 | app = FastAPI() 6 | 7 | HELLO_WORLD: str = "Hello World!" 8 | ITEMS: Dict[str, str] = {"1": "sausage", "2": "ham", "3": "tofu"} 9 | 10 | 11 | @app.get("/") 12 | def read_root(): 13 | return HELLO_WORLD 14 | 15 | 16 | @app.get("/items/{item_id}") 17 | def read_item(item_id: str): 18 | return ITEMS[item_id] 19 | 20 | 21 | def get_app(): 22 | return app 23 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build/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 = "anyio" 16 | version = "4.3.0" 17 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 18 | optional = false 19 | python-versions = ">=3.8" 20 | files = [ 21 | {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, 22 | {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, 23 | ] 24 | 25 | [package.dependencies] 26 | exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} 27 | idna = ">=2.8" 28 | sniffio = ">=1.1" 29 | typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} 30 | 31 | [package.extras] 32 | doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 33 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] 34 | trio = ["trio (>=0.23)"] 35 | 36 | [[package]] 37 | name = "black" 38 | version = "24.4.0" 39 | description = "The uncompromising code formatter." 40 | optional = false 41 | python-versions = ">=3.8" 42 | files = [ 43 | {file = "black-24.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6ad001a9ddd9b8dfd1b434d566be39b1cd502802c8d38bbb1ba612afda2ef436"}, 44 | {file = "black-24.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3a3a092b8b756c643fe45f4624dbd5a389f770a4ac294cf4d0fce6af86addaf"}, 45 | {file = "black-24.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dae79397f367ac8d7adb6c779813328f6d690943f64b32983e896bcccd18cbad"}, 46 | {file = "black-24.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:71d998b73c957444fb7c52096c3843875f4b6b47a54972598741fe9a7f737fcb"}, 47 | {file = "black-24.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8e5537f456a22cf5cfcb2707803431d2feeb82ab3748ade280d6ccd0b40ed2e8"}, 48 | {file = "black-24.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64e60a7edd71fd542a10a9643bf369bfd2644de95ec71e86790b063aa02ff745"}, 49 | {file = "black-24.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cd5b4f76056cecce3e69b0d4c228326d2595f506797f40b9233424e2524c070"}, 50 | {file = "black-24.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:64578cf99b6b46a6301bc28bdb89f9d6f9b592b1c5837818a177c98525dbe397"}, 51 | {file = "black-24.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f95cece33329dc4aa3b0e1a771c41075812e46cf3d6e3f1dfe3d91ff09826ed2"}, 52 | {file = "black-24.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4396ca365a4310beef84d446ca5016f671b10f07abdba3e4e4304218d2c71d33"}, 53 | {file = "black-24.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d99dfdf37a2a00a6f7a8dcbd19edf361d056ee51093b2445de7ca09adac965"}, 54 | {file = "black-24.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:21f9407063ec71c5580b8ad975653c66508d6a9f57bd008bb8691d273705adcd"}, 55 | {file = "black-24.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:652e55bb722ca026299eb74e53880ee2315b181dfdd44dca98e43448620ddec1"}, 56 | {file = "black-24.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7f2966b9b2b3b7104fca9d75b2ee856fe3fdd7ed9e47c753a4bb1a675f2caab8"}, 57 | {file = "black-24.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bb9ca06e556a09f7f7177bc7cb604e5ed2d2df1e9119e4f7d2f1f7071c32e5d"}, 58 | {file = "black-24.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4e71cdebdc8efeb6deaf5f2deb28325f8614d48426bed118ecc2dcaefb9ebf3"}, 59 | {file = "black-24.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6644f97a7ef6f401a150cca551a1ff97e03c25d8519ee0bbc9b0058772882665"}, 60 | {file = "black-24.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:75a2d0b4f5eb81f7eebc31f788f9830a6ce10a68c91fbe0fade34fff7a2836e6"}, 61 | {file = "black-24.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb949f56a63c5e134dfdca12091e98ffb5fd446293ebae123d10fc1abad00b9e"}, 62 | {file = "black-24.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:7852b05d02b5b9a8c893ab95863ef8986e4dda29af80bbbda94d7aee1abf8702"}, 63 | {file = "black-24.4.0-py3-none-any.whl", hash = "sha256:74eb9b5420e26b42c00a3ff470dc0cd144b80a766128b1771d07643165e08d0e"}, 64 | {file = "black-24.4.0.tar.gz", hash = "sha256:f07b69fda20578367eaebbd670ff8fc653ab181e1ff95d84497f9fa20e7d0641"}, 65 | ] 66 | 67 | [package.dependencies] 68 | click = ">=8.0.0" 69 | mypy-extensions = ">=0.4.3" 70 | packaging = ">=22.0" 71 | pathspec = ">=0.9.0" 72 | platformdirs = ">=2" 73 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 74 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 75 | 76 | [package.extras] 77 | colorama = ["colorama (>=0.4.3)"] 78 | d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] 79 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 80 | uvloop = ["uvloop (>=0.15.2)"] 81 | 82 | [[package]] 83 | name = "certifi" 84 | version = "2024.2.2" 85 | description = "Python package for providing Mozilla's CA Bundle." 86 | optional = false 87 | python-versions = ">=3.6" 88 | files = [ 89 | {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, 90 | {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, 91 | ] 92 | 93 | [[package]] 94 | name = "charset-normalizer" 95 | version = "3.3.2" 96 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 97 | optional = false 98 | python-versions = ">=3.7.0" 99 | files = [ 100 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 101 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 102 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 103 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 104 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 105 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 106 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 107 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 108 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 109 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 110 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 111 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 112 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 113 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 114 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 115 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 116 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 117 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 118 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 119 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 120 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 121 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 122 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 123 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 124 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 125 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 126 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 127 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 128 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 129 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 130 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 131 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 132 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 133 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 134 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 135 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 136 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 137 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 138 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 139 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 140 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 141 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 142 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 143 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 144 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 145 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 146 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 147 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 148 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 149 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 150 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 151 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 152 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 153 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 154 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 155 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 156 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 157 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 158 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 159 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 160 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 161 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 162 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 163 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 164 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 165 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 166 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 167 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 168 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 169 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 170 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 171 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 172 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 173 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 174 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 175 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 176 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 177 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 178 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 179 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 180 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 181 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 182 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 183 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 184 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 185 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 186 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 187 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 188 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 189 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 190 | ] 191 | 192 | [[package]] 193 | name = "click" 194 | version = "8.1.7" 195 | description = "Composable command line interface toolkit" 196 | optional = false 197 | python-versions = ">=3.7" 198 | files = [ 199 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 200 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 201 | ] 202 | 203 | [package.dependencies] 204 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 205 | 206 | [[package]] 207 | name = "colorama" 208 | version = "0.4.6" 209 | description = "Cross-platform colored terminal text." 210 | optional = false 211 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 212 | files = [ 213 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 214 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 215 | ] 216 | 217 | [[package]] 218 | name = "coverage" 219 | version = "7.4.4" 220 | description = "Code coverage measurement for Python" 221 | optional = false 222 | python-versions = ">=3.8" 223 | files = [ 224 | {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, 225 | {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, 226 | {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, 227 | {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, 228 | {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"}, 229 | {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, 230 | {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, 231 | {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, 232 | {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, 233 | {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, 234 | {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, 235 | {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, 236 | {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, 237 | {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, 238 | {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"}, 239 | {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, 240 | {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, 241 | {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, 242 | {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, 243 | {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, 244 | {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, 245 | {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, 246 | {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, 247 | {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, 248 | {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"}, 249 | {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, 250 | {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, 251 | {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, 252 | {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, 253 | {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, 254 | {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, 255 | {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, 256 | {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, 257 | {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, 258 | {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"}, 259 | {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, 260 | {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, 261 | {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, 262 | {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, 263 | {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, 264 | {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, 265 | {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, 266 | {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, 267 | {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, 268 | {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"}, 269 | {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, 270 | {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, 271 | {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, 272 | {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, 273 | {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, 274 | {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, 275 | {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, 276 | ] 277 | 278 | [package.dependencies] 279 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 280 | 281 | [package.extras] 282 | toml = ["tomli"] 283 | 284 | [[package]] 285 | name = "exceptiongroup" 286 | version = "1.2.1" 287 | description = "Backport of PEP 654 (exception groups)" 288 | optional = false 289 | python-versions = ">=3.7" 290 | files = [ 291 | {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, 292 | {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, 293 | ] 294 | 295 | [package.extras] 296 | test = ["pytest (>=6)"] 297 | 298 | [[package]] 299 | name = "fastapi" 300 | version = "0.110.2" 301 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 302 | optional = false 303 | python-versions = ">=3.8" 304 | files = [ 305 | {file = "fastapi-0.110.2-py3-none-any.whl", hash = "sha256:239403f2c0a3dda07a9420f95157a7f014ddb2b770acdbc984f9bdf3ead7afdb"}, 306 | {file = "fastapi-0.110.2.tar.gz", hash = "sha256:b53d673652da3b65e8cd787ad214ec0fe303cad00d2b529b86ce7db13f17518d"}, 307 | ] 308 | 309 | [package.dependencies] 310 | pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" 311 | starlette = ">=0.37.2,<0.38.0" 312 | typing-extensions = ">=4.8.0" 313 | 314 | [package.extras] 315 | all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] 316 | 317 | [[package]] 318 | name = "gunicorn" 319 | version = "22.0.0" 320 | description = "WSGI HTTP Server for UNIX" 321 | optional = false 322 | python-versions = ">=3.7" 323 | files = [ 324 | {file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"}, 325 | {file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"}, 326 | ] 327 | 328 | [package.dependencies] 329 | packaging = "*" 330 | 331 | [package.extras] 332 | eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] 333 | gevent = ["gevent (>=1.4.0)"] 334 | setproctitle = ["setproctitle"] 335 | testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] 336 | tornado = ["tornado (>=0.2)"] 337 | 338 | [[package]] 339 | name = "h11" 340 | version = "0.14.0" 341 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 342 | optional = false 343 | python-versions = ">=3.7" 344 | files = [ 345 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 346 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 347 | ] 348 | 349 | [[package]] 350 | name = "idna" 351 | version = "3.7" 352 | description = "Internationalized Domain Names in Applications (IDNA)" 353 | optional = false 354 | python-versions = ">=3.5" 355 | files = [ 356 | {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, 357 | {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, 358 | ] 359 | 360 | [[package]] 361 | name = "iniconfig" 362 | version = "2.0.0" 363 | description = "brain-dead simple config-ini parsing" 364 | optional = false 365 | python-versions = ">=3.7" 366 | files = [ 367 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 368 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 369 | ] 370 | 371 | [[package]] 372 | name = "mypy-extensions" 373 | version = "1.0.0" 374 | description = "Type system extensions for programs checked with the mypy type checker." 375 | optional = false 376 | python-versions = ">=3.5" 377 | files = [ 378 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 379 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 380 | ] 381 | 382 | [[package]] 383 | name = "packaging" 384 | version = "24.0" 385 | description = "Core utilities for Python packages" 386 | optional = false 387 | python-versions = ">=3.7" 388 | files = [ 389 | {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, 390 | {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, 391 | ] 392 | 393 | [[package]] 394 | name = "pathspec" 395 | version = "0.12.1" 396 | description = "Utility library for gitignore style pattern matching of file paths." 397 | optional = false 398 | python-versions = ">=3.8" 399 | files = [ 400 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 401 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 402 | ] 403 | 404 | [[package]] 405 | name = "platformdirs" 406 | version = "4.2.0" 407 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 408 | optional = false 409 | python-versions = ">=3.8" 410 | files = [ 411 | {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, 412 | {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, 413 | ] 414 | 415 | [package.extras] 416 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 417 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 418 | 419 | [[package]] 420 | name = "pluggy" 421 | version = "1.4.0" 422 | description = "plugin and hook calling mechanisms for python" 423 | optional = false 424 | python-versions = ">=3.8" 425 | files = [ 426 | {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, 427 | {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, 428 | ] 429 | 430 | [package.extras] 431 | dev = ["pre-commit", "tox"] 432 | testing = ["pytest", "pytest-benchmark"] 433 | 434 | [[package]] 435 | name = "pydantic" 436 | version = "2.7.0" 437 | description = "Data validation using Python type hints" 438 | optional = false 439 | python-versions = ">=3.8" 440 | files = [ 441 | {file = "pydantic-2.7.0-py3-none-any.whl", hash = "sha256:9dee74a271705f14f9a1567671d144a851c675b072736f0a7b2608fd9e495352"}, 442 | {file = "pydantic-2.7.0.tar.gz", hash = "sha256:b5ecdd42262ca2462e2624793551e80911a1e989f462910bb81aef974b4bb383"}, 443 | ] 444 | 445 | [package.dependencies] 446 | annotated-types = ">=0.4.0" 447 | pydantic-core = "2.18.1" 448 | typing-extensions = ">=4.6.1" 449 | 450 | [package.extras] 451 | email = ["email-validator (>=2.0.0)"] 452 | 453 | [[package]] 454 | name = "pydantic-core" 455 | version = "2.18.1" 456 | description = "Core functionality for Pydantic validation and serialization" 457 | optional = false 458 | python-versions = ">=3.8" 459 | files = [ 460 | {file = "pydantic_core-2.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ee9cf33e7fe14243f5ca6977658eb7d1042caaa66847daacbd2117adb258b226"}, 461 | {file = "pydantic_core-2.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b7bbb97d82659ac8b37450c60ff2e9f97e4eb0f8a8a3645a5568b9334b08b50"}, 462 | {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df4249b579e75094f7e9bb4bd28231acf55e308bf686b952f43100a5a0be394c"}, 463 | {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d0491006a6ad20507aec2be72e7831a42efc93193d2402018007ff827dc62926"}, 464 | {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ae80f72bb7a3e397ab37b53a2b49c62cc5496412e71bc4f1277620a7ce3f52b"}, 465 | {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58aca931bef83217fca7a390e0486ae327c4af9c3e941adb75f8772f8eeb03a1"}, 466 | {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1be91ad664fc9245404a789d60cba1e91c26b1454ba136d2a1bf0c2ac0c0505a"}, 467 | {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:667880321e916a8920ef49f5d50e7983792cf59f3b6079f3c9dac2b88a311d17"}, 468 | {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f7054fdc556f5421f01e39cbb767d5ec5c1139ea98c3e5b350e02e62201740c7"}, 469 | {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:030e4f9516f9947f38179249778709a460a3adb516bf39b5eb9066fcfe43d0e6"}, 470 | {file = "pydantic_core-2.18.1-cp310-none-win32.whl", hash = "sha256:2e91711e36e229978d92642bfc3546333a9127ecebb3f2761372e096395fc649"}, 471 | {file = "pydantic_core-2.18.1-cp310-none-win_amd64.whl", hash = "sha256:9a29726f91c6cb390b3c2338f0df5cd3e216ad7a938762d11c994bb37552edb0"}, 472 | {file = "pydantic_core-2.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9ece8a49696669d483d206b4474c367852c44815fca23ac4e48b72b339807f80"}, 473 | {file = "pydantic_core-2.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a5d83efc109ceddb99abd2c1316298ced2adb4570410defe766851a804fcd5b"}, 474 | {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7973c381283783cd1043a8c8f61ea5ce7a3a58b0369f0ee0ee975eaf2f2a1b"}, 475 | {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54c7375c62190a7845091f521add19b0f026bcf6ae674bdb89f296972272e86d"}, 476 | {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd63cec4e26e790b70544ae5cc48d11b515b09e05fdd5eff12e3195f54b8a586"}, 477 | {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:561cf62c8a3498406495cfc49eee086ed2bb186d08bcc65812b75fda42c38294"}, 478 | {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68717c38a68e37af87c4da20e08f3e27d7e4212e99e96c3d875fbf3f4812abfc"}, 479 | {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d5728e93d28a3c63ee513d9ffbac9c5989de8c76e049dbcb5bfe4b923a9739d"}, 480 | {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f0f17814c505f07806e22b28856c59ac80cee7dd0fbb152aed273e116378f519"}, 481 | {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d816f44a51ba5175394bc6c7879ca0bd2be560b2c9e9f3411ef3a4cbe644c2e9"}, 482 | {file = "pydantic_core-2.18.1-cp311-none-win32.whl", hash = "sha256:09f03dfc0ef8c22622eaa8608caa4a1e189cfb83ce847045eca34f690895eccb"}, 483 | {file = "pydantic_core-2.18.1-cp311-none-win_amd64.whl", hash = "sha256:27f1009dc292f3b7ca77feb3571c537276b9aad5dd4efb471ac88a8bd09024e9"}, 484 | {file = "pydantic_core-2.18.1-cp311-none-win_arm64.whl", hash = "sha256:48dd883db92e92519201f2b01cafa881e5f7125666141a49ffba8b9facc072b0"}, 485 | {file = "pydantic_core-2.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b6b0e4912030c6f28bcb72b9ebe4989d6dc2eebcd2a9cdc35fefc38052dd4fe8"}, 486 | {file = "pydantic_core-2.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3202a429fe825b699c57892d4371c74cc3456d8d71b7f35d6028c96dfecad31"}, 487 | {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3982b0a32d0a88b3907e4b0dc36809fda477f0757c59a505d4e9b455f384b8b"}, 488 | {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25595ac311f20e5324d1941909b0d12933f1fd2171075fcff763e90f43e92a0d"}, 489 | {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14fe73881cf8e4cbdaded8ca0aa671635b597e42447fec7060d0868b52d074e6"}, 490 | {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca976884ce34070799e4dfc6fbd68cb1d181db1eefe4a3a94798ddfb34b8867f"}, 491 | {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684d840d2c9ec5de9cb397fcb3f36d5ebb6fa0d94734f9886032dd796c1ead06"}, 492 | {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:54764c083bbe0264f0f746cefcded6cb08fbbaaf1ad1d78fb8a4c30cff999a90"}, 493 | {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:201713f2f462e5c015b343e86e68bd8a530a4f76609b33d8f0ec65d2b921712a"}, 494 | {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd1a9edb9dd9d79fbeac1ea1f9a8dd527a6113b18d2e9bcc0d541d308dae639b"}, 495 | {file = "pydantic_core-2.18.1-cp312-none-win32.whl", hash = "sha256:d5e6b7155b8197b329dc787356cfd2684c9d6a6b1a197f6bbf45f5555a98d411"}, 496 | {file = "pydantic_core-2.18.1-cp312-none-win_amd64.whl", hash = "sha256:9376d83d686ec62e8b19c0ac3bf8d28d8a5981d0df290196fb6ef24d8a26f0d6"}, 497 | {file = "pydantic_core-2.18.1-cp312-none-win_arm64.whl", hash = "sha256:c562b49c96906b4029b5685075fe1ebd3b5cc2601dfa0b9e16c2c09d6cbce048"}, 498 | {file = "pydantic_core-2.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:3e352f0191d99fe617371096845070dee295444979efb8f27ad941227de6ad09"}, 499 | {file = "pydantic_core-2.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0295d52b012cbe0d3059b1dba99159c3be55e632aae1999ab74ae2bd86a33d7"}, 500 | {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56823a92075780582d1ffd4489a2e61d56fd3ebb4b40b713d63f96dd92d28144"}, 501 | {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd3f79e17b56741b5177bcc36307750d50ea0698df6aa82f69c7db32d968c1c2"}, 502 | {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38a5024de321d672a132b1834a66eeb7931959c59964b777e8f32dbe9523f6b1"}, 503 | {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ce426ee691319d4767748c8e0895cfc56593d725594e415f274059bcf3cb76"}, 504 | {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2adaeea59849ec0939af5c5d476935f2bab4b7f0335b0110f0f069a41024278e"}, 505 | {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9b6431559676a1079eac0f52d6d0721fb8e3c5ba43c37bc537c8c83724031feb"}, 506 | {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:85233abb44bc18d16e72dc05bf13848a36f363f83757541f1a97db2f8d58cfd9"}, 507 | {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:641a018af4fe48be57a2b3d7a1f0f5dbca07c1d00951d3d7463f0ac9dac66622"}, 508 | {file = "pydantic_core-2.18.1-cp38-none-win32.whl", hash = "sha256:63d7523cd95d2fde0d28dc42968ac731b5bb1e516cc56b93a50ab293f4daeaad"}, 509 | {file = "pydantic_core-2.18.1-cp38-none-win_amd64.whl", hash = "sha256:907a4d7720abfcb1c81619863efd47c8a85d26a257a2dbebdb87c3b847df0278"}, 510 | {file = "pydantic_core-2.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:aad17e462f42ddbef5984d70c40bfc4146c322a2da79715932cd8976317054de"}, 511 | {file = "pydantic_core-2.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:94b9769ba435b598b547c762184bcfc4783d0d4c7771b04a3b45775c3589ca44"}, 512 | {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80e0e57cc704a52fb1b48f16d5b2c8818da087dbee6f98d9bf19546930dc64b5"}, 513 | {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76b86e24039c35280ceee6dce7e62945eb93a5175d43689ba98360ab31eebc4a"}, 514 | {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a05db5013ec0ca4a32cc6433f53faa2a014ec364031408540ba858c2172bb0"}, 515 | {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:250ae39445cb5475e483a36b1061af1bc233de3e9ad0f4f76a71b66231b07f88"}, 516 | {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a32204489259786a923e02990249c65b0f17235073149d0033efcebe80095570"}, 517 | {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6395a4435fa26519fd96fdccb77e9d00ddae9dd6c742309bd0b5610609ad7fb2"}, 518 | {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2533ad2883f001efa72f3d0e733fb846710c3af6dcdd544fe5bf14fa5fe2d7db"}, 519 | {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b560b72ed4816aee52783c66854d96157fd8175631f01ef58e894cc57c84f0f6"}, 520 | {file = "pydantic_core-2.18.1-cp39-none-win32.whl", hash = "sha256:582cf2cead97c9e382a7f4d3b744cf0ef1a6e815e44d3aa81af3ad98762f5a9b"}, 521 | {file = "pydantic_core-2.18.1-cp39-none-win_amd64.whl", hash = "sha256:ca71d501629d1fa50ea7fa3b08ba884fe10cefc559f5c6c8dfe9036c16e8ae89"}, 522 | {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e178e5b66a06ec5bf51668ec0d4ac8cfb2bdcb553b2c207d58148340efd00143"}, 523 | {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:72722ce529a76a4637a60be18bd789d8fb871e84472490ed7ddff62d5fed620d"}, 524 | {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fe0c1ce5b129455e43f941f7a46f61f3d3861e571f2905d55cdbb8b5c6f5e2c"}, 525 | {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4284c621f06a72ce2cb55f74ea3150113d926a6eb78ab38340c08f770eb9b4d"}, 526 | {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0c3e718f4e064efde68092d9d974e39572c14e56726ecfaeebbe6544521f47"}, 527 | {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2027493cc44c23b598cfaf200936110433d9caa84e2c6cf487a83999638a96ac"}, 528 | {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:76909849d1a6bffa5a07742294f3fa1d357dc917cb1fe7b470afbc3a7579d539"}, 529 | {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ee7ccc7fb7e921d767f853b47814c3048c7de536663e82fbc37f5eb0d532224b"}, 530 | {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ee2794111c188548a4547eccc73a6a8527fe2af6cf25e1a4ebda2fd01cdd2e60"}, 531 | {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a139fe9f298dc097349fb4f28c8b81cc7a202dbfba66af0e14be5cfca4ef7ce5"}, 532 | {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d074b07a10c391fc5bbdcb37b2f16f20fcd9e51e10d01652ab298c0d07908ee2"}, 533 | {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c69567ddbac186e8c0aadc1f324a60a564cfe25e43ef2ce81bcc4b8c3abffbae"}, 534 | {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf1c7b78cddb5af00971ad5294a4583188bda1495b13760d9f03c9483bb6203"}, 535 | {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2684a94fdfd1b146ff10689c6e4e815f6a01141781c493b97342cdc5b06f4d5d"}, 536 | {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:73c1bc8a86a5c9e8721a088df234265317692d0b5cd9e86e975ce3bc3db62a59"}, 537 | {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e60defc3c15defb70bb38dd605ff7e0fae5f6c9c7cbfe0ad7868582cb7e844a6"}, 538 | {file = "pydantic_core-2.18.1.tar.gz", hash = "sha256:de9d3e8717560eb05e28739d1b35e4eac2e458553a52a301e51352a7ffc86a35"}, 539 | ] 540 | 541 | [package.dependencies] 542 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 543 | 544 | [[package]] 545 | name = "pytest" 546 | version = "8.1.1" 547 | description = "pytest: simple powerful testing with Python" 548 | optional = false 549 | python-versions = ">=3.8" 550 | files = [ 551 | {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, 552 | {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, 553 | ] 554 | 555 | [package.dependencies] 556 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 557 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 558 | iniconfig = "*" 559 | packaging = "*" 560 | pluggy = ">=1.4,<2.0" 561 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 562 | 563 | [package.extras] 564 | testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 565 | 566 | [[package]] 567 | name = "pytest-cov" 568 | version = "5.0.0" 569 | description = "Pytest plugin for measuring coverage." 570 | optional = false 571 | python-versions = ">=3.8" 572 | files = [ 573 | {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, 574 | {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, 575 | ] 576 | 577 | [package.dependencies] 578 | coverage = {version = ">=5.2.1", extras = ["toml"]} 579 | pytest = ">=4.6" 580 | 581 | [package.extras] 582 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] 583 | 584 | [[package]] 585 | name = "requests" 586 | version = "2.31.0" 587 | description = "Python HTTP for Humans." 588 | optional = false 589 | python-versions = ">=3.7" 590 | files = [ 591 | {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, 592 | {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, 593 | ] 594 | 595 | [package.dependencies] 596 | certifi = ">=2017.4.17" 597 | charset-normalizer = ">=2,<4" 598 | idna = ">=2.5,<4" 599 | urllib3 = ">=1.21.1,<3" 600 | 601 | [package.extras] 602 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 603 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 604 | 605 | [[package]] 606 | name = "sniffio" 607 | version = "1.3.1" 608 | description = "Sniff out which async library your code is running under" 609 | optional = false 610 | python-versions = ">=3.7" 611 | files = [ 612 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 613 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 614 | ] 615 | 616 | [[package]] 617 | name = "starlette" 618 | version = "0.37.2" 619 | description = "The little ASGI library that shines." 620 | optional = false 621 | python-versions = ">=3.8" 622 | files = [ 623 | {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"}, 624 | {file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"}, 625 | ] 626 | 627 | [package.dependencies] 628 | anyio = ">=3.4.0,<5" 629 | 630 | [package.extras] 631 | full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] 632 | 633 | [[package]] 634 | name = "tomli" 635 | version = "2.0.1" 636 | description = "A lil' TOML parser" 637 | optional = false 638 | python-versions = ">=3.7" 639 | files = [ 640 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 641 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 642 | ] 643 | 644 | [[package]] 645 | name = "typing-extensions" 646 | version = "4.11.0" 647 | description = "Backported and Experimental Type Hints for Python 3.8+" 648 | optional = false 649 | python-versions = ">=3.8" 650 | files = [ 651 | {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, 652 | {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, 653 | ] 654 | 655 | [[package]] 656 | name = "urllib3" 657 | version = "2.2.1" 658 | description = "HTTP library with thread-safe connection pooling, file post, and more." 659 | optional = false 660 | python-versions = ">=3.8" 661 | files = [ 662 | {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, 663 | {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, 664 | ] 665 | 666 | [package.extras] 667 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 668 | h2 = ["h2 (>=4,<5)"] 669 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 670 | zstd = ["zstandard (>=0.18.0)"] 671 | 672 | [[package]] 673 | name = "uvicorn" 674 | version = "0.29.0" 675 | description = "The lightning-fast ASGI server." 676 | optional = false 677 | python-versions = ">=3.8" 678 | files = [ 679 | {file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"}, 680 | {file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"}, 681 | ] 682 | 683 | [package.dependencies] 684 | click = ">=7.0" 685 | h11 = ">=0.8" 686 | typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} 687 | 688 | [package.extras] 689 | standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] 690 | 691 | [metadata] 692 | lock-version = "2.0" 693 | python-versions = "^3.10" 694 | content-hash = "c86a2f7d879c6f81a2d903473a16249faa390219980b4dcbe3b0c88c01f47ba9" 695 | -------------------------------------------------------------------------------- /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-gunicorn-poetry docker image." 5 | authors = ["Max Pfeiffer "] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.10" 10 | gunicorn = "22.0.0" 11 | uvicorn = "0.29.0" 12 | fastapi = "0.110.2" 13 | 14 | [tool.poetry.dev-dependencies] 15 | pytest = "8.1.1" 16 | pytest-cov = "5.0.0" 17 | coverage = "7.4.4" 18 | black = "24.4.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: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-pfeiffer/uvicorn-gunicorn-poetry/ad8ee32e791e695b91318e9ca5c947f0011d4751/examples/fast_api_multistage_build/tests/__init__.py -------------------------------------------------------------------------------- /examples/fast_api_multistage_build/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from starlette.testclient import TestClient 3 | 4 | from app.main import app 5 | 6 | 7 | @pytest.fixture(scope="module") 8 | def test_client() -> TestClient: 9 | return TestClient(app) 10 | -------------------------------------------------------------------------------- /examples/fast_api_multistage_build/tests/test_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-pfeiffer/uvicorn-gunicorn-poetry/ad8ee32e791e695b91318e9ca5c947f0011d4751/examples/fast_api_multistage_build/tests/test_api/__init__.py -------------------------------------------------------------------------------- /examples/fast_api_multistage_build/tests/test_api/test_api_endpoints.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from requests import Response 4 | from starlette import status 5 | 6 | from app.main import ITEMS, HELLO_WORLD 7 | 8 | 9 | def test_root(test_client): 10 | response: Response = test_client.get("/") 11 | 12 | assert response.status_code == status.HTTP_200_OK 13 | assert response.json() == HELLO_WORLD 14 | 15 | 16 | def test_items(test_client): 17 | key, value = random.choice(list(ITEMS.items())) 18 | 19 | response: Response = test_client.get(f"/items/{key}") 20 | 21 | assert response.status_code == status.HTTP_200_OK 22 | assert response.json() == value 23 | -------------------------------------------------------------------------------- /examples/fast_api_singlestage_build/.dockerignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .pytest_cache 3 | .venv 4 | .coverage 5 | test_coverage_reports/ 6 | Dockerfile 7 | README.md 8 | 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_NAME_AND_TAG=pfeiffermax/uvicorn-gunicorn-poetry:2.2.0-python3.12.2-slim-bookworm 4 | FROM ${BASE_IMAGE_NAME_AND_TAG} as development-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 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-gunicorn-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: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-pfeiffer/uvicorn-gunicorn-poetry/ad8ee32e791e695b91318e9ca5c947f0011d4751/examples/fast_api_singlestage_build/app/__init__.py -------------------------------------------------------------------------------- /examples/fast_api_singlestage_build/app/main.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from fastapi import FastAPI 4 | 5 | app = FastAPI() 6 | 7 | HELLO_WORLD: str = "Hello World!" 8 | ITEMS: Dict[str, str] = {"1": "sausage", "2": "ham", "3": "tofu"} 9 | 10 | 11 | @app.get("/") 12 | def read_root(): 13 | return HELLO_WORLD 14 | 15 | 16 | @app.get("/items/{item_id}") 17 | def read_item(item_id: str): 18 | return ITEMS[item_id] 19 | 20 | 21 | def get_app(): 22 | return app 23 | -------------------------------------------------------------------------------- /examples/fast_api_singlestage_build/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_singlestage_build/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 = "anyio" 16 | version = "4.3.0" 17 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 18 | optional = false 19 | python-versions = ">=3.8" 20 | files = [ 21 | {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, 22 | {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, 23 | ] 24 | 25 | [package.dependencies] 26 | exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} 27 | idna = ">=2.8" 28 | sniffio = ">=1.1" 29 | typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} 30 | 31 | [package.extras] 32 | doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 33 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] 34 | trio = ["trio (>=0.23)"] 35 | 36 | [[package]] 37 | name = "black" 38 | version = "24.4.0" 39 | description = "The uncompromising code formatter." 40 | optional = false 41 | python-versions = ">=3.8" 42 | files = [ 43 | {file = "black-24.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6ad001a9ddd9b8dfd1b434d566be39b1cd502802c8d38bbb1ba612afda2ef436"}, 44 | {file = "black-24.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3a3a092b8b756c643fe45f4624dbd5a389f770a4ac294cf4d0fce6af86addaf"}, 45 | {file = "black-24.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dae79397f367ac8d7adb6c779813328f6d690943f64b32983e896bcccd18cbad"}, 46 | {file = "black-24.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:71d998b73c957444fb7c52096c3843875f4b6b47a54972598741fe9a7f737fcb"}, 47 | {file = "black-24.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8e5537f456a22cf5cfcb2707803431d2feeb82ab3748ade280d6ccd0b40ed2e8"}, 48 | {file = "black-24.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64e60a7edd71fd542a10a9643bf369bfd2644de95ec71e86790b063aa02ff745"}, 49 | {file = "black-24.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cd5b4f76056cecce3e69b0d4c228326d2595f506797f40b9233424e2524c070"}, 50 | {file = "black-24.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:64578cf99b6b46a6301bc28bdb89f9d6f9b592b1c5837818a177c98525dbe397"}, 51 | {file = "black-24.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f95cece33329dc4aa3b0e1a771c41075812e46cf3d6e3f1dfe3d91ff09826ed2"}, 52 | {file = "black-24.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4396ca365a4310beef84d446ca5016f671b10f07abdba3e4e4304218d2c71d33"}, 53 | {file = "black-24.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d99dfdf37a2a00a6f7a8dcbd19edf361d056ee51093b2445de7ca09adac965"}, 54 | {file = "black-24.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:21f9407063ec71c5580b8ad975653c66508d6a9f57bd008bb8691d273705adcd"}, 55 | {file = "black-24.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:652e55bb722ca026299eb74e53880ee2315b181dfdd44dca98e43448620ddec1"}, 56 | {file = "black-24.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7f2966b9b2b3b7104fca9d75b2ee856fe3fdd7ed9e47c753a4bb1a675f2caab8"}, 57 | {file = "black-24.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bb9ca06e556a09f7f7177bc7cb604e5ed2d2df1e9119e4f7d2f1f7071c32e5d"}, 58 | {file = "black-24.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4e71cdebdc8efeb6deaf5f2deb28325f8614d48426bed118ecc2dcaefb9ebf3"}, 59 | {file = "black-24.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6644f97a7ef6f401a150cca551a1ff97e03c25d8519ee0bbc9b0058772882665"}, 60 | {file = "black-24.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:75a2d0b4f5eb81f7eebc31f788f9830a6ce10a68c91fbe0fade34fff7a2836e6"}, 61 | {file = "black-24.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb949f56a63c5e134dfdca12091e98ffb5fd446293ebae123d10fc1abad00b9e"}, 62 | {file = "black-24.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:7852b05d02b5b9a8c893ab95863ef8986e4dda29af80bbbda94d7aee1abf8702"}, 63 | {file = "black-24.4.0-py3-none-any.whl", hash = "sha256:74eb9b5420e26b42c00a3ff470dc0cd144b80a766128b1771d07643165e08d0e"}, 64 | {file = "black-24.4.0.tar.gz", hash = "sha256:f07b69fda20578367eaebbd670ff8fc653ab181e1ff95d84497f9fa20e7d0641"}, 65 | ] 66 | 67 | [package.dependencies] 68 | click = ">=8.0.0" 69 | mypy-extensions = ">=0.4.3" 70 | packaging = ">=22.0" 71 | pathspec = ">=0.9.0" 72 | platformdirs = ">=2" 73 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 74 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 75 | 76 | [package.extras] 77 | colorama = ["colorama (>=0.4.3)"] 78 | d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] 79 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 80 | uvloop = ["uvloop (>=0.15.2)"] 81 | 82 | [[package]] 83 | name = "certifi" 84 | version = "2024.2.2" 85 | description = "Python package for providing Mozilla's CA Bundle." 86 | optional = false 87 | python-versions = ">=3.6" 88 | files = [ 89 | {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, 90 | {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, 91 | ] 92 | 93 | [[package]] 94 | name = "charset-normalizer" 95 | version = "3.3.2" 96 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 97 | optional = false 98 | python-versions = ">=3.7.0" 99 | files = [ 100 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 101 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 102 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 103 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 104 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 105 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 106 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 107 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 108 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 109 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 110 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 111 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 112 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 113 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 114 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 115 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 116 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 117 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 118 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 119 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 120 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 121 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 122 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 123 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 124 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 125 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 126 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 127 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 128 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 129 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 130 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 131 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 132 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 133 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 134 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 135 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 136 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 137 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 138 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 139 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 140 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 141 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 142 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 143 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 144 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 145 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 146 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 147 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 148 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 149 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 150 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 151 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 152 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 153 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 154 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 155 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 156 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 157 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 158 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 159 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 160 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 161 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 162 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 163 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 164 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 165 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 166 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 167 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 168 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 169 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 170 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 171 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 172 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 173 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 174 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 175 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 176 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 177 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 178 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 179 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 180 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 181 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 182 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 183 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 184 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 185 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 186 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 187 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 188 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 189 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 190 | ] 191 | 192 | [[package]] 193 | name = "click" 194 | version = "8.1.7" 195 | description = "Composable command line interface toolkit" 196 | optional = false 197 | python-versions = ">=3.7" 198 | files = [ 199 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 200 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 201 | ] 202 | 203 | [package.dependencies] 204 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 205 | 206 | [[package]] 207 | name = "colorama" 208 | version = "0.4.6" 209 | description = "Cross-platform colored terminal text." 210 | optional = false 211 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 212 | files = [ 213 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 214 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 215 | ] 216 | 217 | [[package]] 218 | name = "coverage" 219 | version = "7.4.4" 220 | description = "Code coverage measurement for Python" 221 | optional = false 222 | python-versions = ">=3.8" 223 | files = [ 224 | {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, 225 | {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, 226 | {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, 227 | {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, 228 | {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"}, 229 | {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, 230 | {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, 231 | {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, 232 | {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, 233 | {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, 234 | {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, 235 | {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, 236 | {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, 237 | {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, 238 | {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"}, 239 | {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, 240 | {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, 241 | {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, 242 | {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, 243 | {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, 244 | {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, 245 | {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, 246 | {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, 247 | {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, 248 | {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"}, 249 | {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, 250 | {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, 251 | {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, 252 | {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, 253 | {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, 254 | {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, 255 | {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, 256 | {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, 257 | {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, 258 | {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"}, 259 | {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, 260 | {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, 261 | {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, 262 | {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, 263 | {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, 264 | {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, 265 | {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, 266 | {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, 267 | {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, 268 | {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"}, 269 | {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, 270 | {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, 271 | {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, 272 | {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, 273 | {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, 274 | {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, 275 | {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, 276 | ] 277 | 278 | [package.dependencies] 279 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 280 | 281 | [package.extras] 282 | toml = ["tomli"] 283 | 284 | [[package]] 285 | name = "exceptiongroup" 286 | version = "1.2.1" 287 | description = "Backport of PEP 654 (exception groups)" 288 | optional = false 289 | python-versions = ">=3.7" 290 | files = [ 291 | {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, 292 | {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, 293 | ] 294 | 295 | [package.extras] 296 | test = ["pytest (>=6)"] 297 | 298 | [[package]] 299 | name = "fastapi" 300 | version = "0.110.2" 301 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 302 | optional = false 303 | python-versions = ">=3.8" 304 | files = [ 305 | {file = "fastapi-0.110.2-py3-none-any.whl", hash = "sha256:239403f2c0a3dda07a9420f95157a7f014ddb2b770acdbc984f9bdf3ead7afdb"}, 306 | {file = "fastapi-0.110.2.tar.gz", hash = "sha256:b53d673652da3b65e8cd787ad214ec0fe303cad00d2b529b86ce7db13f17518d"}, 307 | ] 308 | 309 | [package.dependencies] 310 | pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" 311 | starlette = ">=0.37.2,<0.38.0" 312 | typing-extensions = ">=4.8.0" 313 | 314 | [package.extras] 315 | all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] 316 | 317 | [[package]] 318 | name = "gunicorn" 319 | version = "22.0.0" 320 | description = "WSGI HTTP Server for UNIX" 321 | optional = false 322 | python-versions = ">=3.7" 323 | files = [ 324 | {file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"}, 325 | {file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"}, 326 | ] 327 | 328 | [package.dependencies] 329 | packaging = "*" 330 | 331 | [package.extras] 332 | eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] 333 | gevent = ["gevent (>=1.4.0)"] 334 | setproctitle = ["setproctitle"] 335 | testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] 336 | tornado = ["tornado (>=0.2)"] 337 | 338 | [[package]] 339 | name = "h11" 340 | version = "0.14.0" 341 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 342 | optional = false 343 | python-versions = ">=3.7" 344 | files = [ 345 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 346 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 347 | ] 348 | 349 | [[package]] 350 | name = "idna" 351 | version = "3.7" 352 | description = "Internationalized Domain Names in Applications (IDNA)" 353 | optional = false 354 | python-versions = ">=3.5" 355 | files = [ 356 | {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, 357 | {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, 358 | ] 359 | 360 | [[package]] 361 | name = "iniconfig" 362 | version = "2.0.0" 363 | description = "brain-dead simple config-ini parsing" 364 | optional = false 365 | python-versions = ">=3.7" 366 | files = [ 367 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 368 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 369 | ] 370 | 371 | [[package]] 372 | name = "mypy-extensions" 373 | version = "1.0.0" 374 | description = "Type system extensions for programs checked with the mypy type checker." 375 | optional = false 376 | python-versions = ">=3.5" 377 | files = [ 378 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 379 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 380 | ] 381 | 382 | [[package]] 383 | name = "packaging" 384 | version = "24.0" 385 | description = "Core utilities for Python packages" 386 | optional = false 387 | python-versions = ">=3.7" 388 | files = [ 389 | {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, 390 | {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, 391 | ] 392 | 393 | [[package]] 394 | name = "pathspec" 395 | version = "0.12.1" 396 | description = "Utility library for gitignore style pattern matching of file paths." 397 | optional = false 398 | python-versions = ">=3.8" 399 | files = [ 400 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 401 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 402 | ] 403 | 404 | [[package]] 405 | name = "platformdirs" 406 | version = "4.2.0" 407 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 408 | optional = false 409 | python-versions = ">=3.8" 410 | files = [ 411 | {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, 412 | {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, 413 | ] 414 | 415 | [package.extras] 416 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 417 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 418 | 419 | [[package]] 420 | name = "pluggy" 421 | version = "1.4.0" 422 | description = "plugin and hook calling mechanisms for python" 423 | optional = false 424 | python-versions = ">=3.8" 425 | files = [ 426 | {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, 427 | {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, 428 | ] 429 | 430 | [package.extras] 431 | dev = ["pre-commit", "tox"] 432 | testing = ["pytest", "pytest-benchmark"] 433 | 434 | [[package]] 435 | name = "pydantic" 436 | version = "2.7.0" 437 | description = "Data validation using Python type hints" 438 | optional = false 439 | python-versions = ">=3.8" 440 | files = [ 441 | {file = "pydantic-2.7.0-py3-none-any.whl", hash = "sha256:9dee74a271705f14f9a1567671d144a851c675b072736f0a7b2608fd9e495352"}, 442 | {file = "pydantic-2.7.0.tar.gz", hash = "sha256:b5ecdd42262ca2462e2624793551e80911a1e989f462910bb81aef974b4bb383"}, 443 | ] 444 | 445 | [package.dependencies] 446 | annotated-types = ">=0.4.0" 447 | pydantic-core = "2.18.1" 448 | typing-extensions = ">=4.6.1" 449 | 450 | [package.extras] 451 | email = ["email-validator (>=2.0.0)"] 452 | 453 | [[package]] 454 | name = "pydantic-core" 455 | version = "2.18.1" 456 | description = "Core functionality for Pydantic validation and serialization" 457 | optional = false 458 | python-versions = ">=3.8" 459 | files = [ 460 | {file = "pydantic_core-2.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ee9cf33e7fe14243f5ca6977658eb7d1042caaa66847daacbd2117adb258b226"}, 461 | {file = "pydantic_core-2.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b7bbb97d82659ac8b37450c60ff2e9f97e4eb0f8a8a3645a5568b9334b08b50"}, 462 | {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df4249b579e75094f7e9bb4bd28231acf55e308bf686b952f43100a5a0be394c"}, 463 | {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d0491006a6ad20507aec2be72e7831a42efc93193d2402018007ff827dc62926"}, 464 | {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ae80f72bb7a3e397ab37b53a2b49c62cc5496412e71bc4f1277620a7ce3f52b"}, 465 | {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58aca931bef83217fca7a390e0486ae327c4af9c3e941adb75f8772f8eeb03a1"}, 466 | {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1be91ad664fc9245404a789d60cba1e91c26b1454ba136d2a1bf0c2ac0c0505a"}, 467 | {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:667880321e916a8920ef49f5d50e7983792cf59f3b6079f3c9dac2b88a311d17"}, 468 | {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f7054fdc556f5421f01e39cbb767d5ec5c1139ea98c3e5b350e02e62201740c7"}, 469 | {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:030e4f9516f9947f38179249778709a460a3adb516bf39b5eb9066fcfe43d0e6"}, 470 | {file = "pydantic_core-2.18.1-cp310-none-win32.whl", hash = "sha256:2e91711e36e229978d92642bfc3546333a9127ecebb3f2761372e096395fc649"}, 471 | {file = "pydantic_core-2.18.1-cp310-none-win_amd64.whl", hash = "sha256:9a29726f91c6cb390b3c2338f0df5cd3e216ad7a938762d11c994bb37552edb0"}, 472 | {file = "pydantic_core-2.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9ece8a49696669d483d206b4474c367852c44815fca23ac4e48b72b339807f80"}, 473 | {file = "pydantic_core-2.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a5d83efc109ceddb99abd2c1316298ced2adb4570410defe766851a804fcd5b"}, 474 | {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7973c381283783cd1043a8c8f61ea5ce7a3a58b0369f0ee0ee975eaf2f2a1b"}, 475 | {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54c7375c62190a7845091f521add19b0f026bcf6ae674bdb89f296972272e86d"}, 476 | {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd63cec4e26e790b70544ae5cc48d11b515b09e05fdd5eff12e3195f54b8a586"}, 477 | {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:561cf62c8a3498406495cfc49eee086ed2bb186d08bcc65812b75fda42c38294"}, 478 | {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68717c38a68e37af87c4da20e08f3e27d7e4212e99e96c3d875fbf3f4812abfc"}, 479 | {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d5728e93d28a3c63ee513d9ffbac9c5989de8c76e049dbcb5bfe4b923a9739d"}, 480 | {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f0f17814c505f07806e22b28856c59ac80cee7dd0fbb152aed273e116378f519"}, 481 | {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d816f44a51ba5175394bc6c7879ca0bd2be560b2c9e9f3411ef3a4cbe644c2e9"}, 482 | {file = "pydantic_core-2.18.1-cp311-none-win32.whl", hash = "sha256:09f03dfc0ef8c22622eaa8608caa4a1e189cfb83ce847045eca34f690895eccb"}, 483 | {file = "pydantic_core-2.18.1-cp311-none-win_amd64.whl", hash = "sha256:27f1009dc292f3b7ca77feb3571c537276b9aad5dd4efb471ac88a8bd09024e9"}, 484 | {file = "pydantic_core-2.18.1-cp311-none-win_arm64.whl", hash = "sha256:48dd883db92e92519201f2b01cafa881e5f7125666141a49ffba8b9facc072b0"}, 485 | {file = "pydantic_core-2.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b6b0e4912030c6f28bcb72b9ebe4989d6dc2eebcd2a9cdc35fefc38052dd4fe8"}, 486 | {file = "pydantic_core-2.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3202a429fe825b699c57892d4371c74cc3456d8d71b7f35d6028c96dfecad31"}, 487 | {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3982b0a32d0a88b3907e4b0dc36809fda477f0757c59a505d4e9b455f384b8b"}, 488 | {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25595ac311f20e5324d1941909b0d12933f1fd2171075fcff763e90f43e92a0d"}, 489 | {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14fe73881cf8e4cbdaded8ca0aa671635b597e42447fec7060d0868b52d074e6"}, 490 | {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca976884ce34070799e4dfc6fbd68cb1d181db1eefe4a3a94798ddfb34b8867f"}, 491 | {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684d840d2c9ec5de9cb397fcb3f36d5ebb6fa0d94734f9886032dd796c1ead06"}, 492 | {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:54764c083bbe0264f0f746cefcded6cb08fbbaaf1ad1d78fb8a4c30cff999a90"}, 493 | {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:201713f2f462e5c015b343e86e68bd8a530a4f76609b33d8f0ec65d2b921712a"}, 494 | {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd1a9edb9dd9d79fbeac1ea1f9a8dd527a6113b18d2e9bcc0d541d308dae639b"}, 495 | {file = "pydantic_core-2.18.1-cp312-none-win32.whl", hash = "sha256:d5e6b7155b8197b329dc787356cfd2684c9d6a6b1a197f6bbf45f5555a98d411"}, 496 | {file = "pydantic_core-2.18.1-cp312-none-win_amd64.whl", hash = "sha256:9376d83d686ec62e8b19c0ac3bf8d28d8a5981d0df290196fb6ef24d8a26f0d6"}, 497 | {file = "pydantic_core-2.18.1-cp312-none-win_arm64.whl", hash = "sha256:c562b49c96906b4029b5685075fe1ebd3b5cc2601dfa0b9e16c2c09d6cbce048"}, 498 | {file = "pydantic_core-2.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:3e352f0191d99fe617371096845070dee295444979efb8f27ad941227de6ad09"}, 499 | {file = "pydantic_core-2.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0295d52b012cbe0d3059b1dba99159c3be55e632aae1999ab74ae2bd86a33d7"}, 500 | {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56823a92075780582d1ffd4489a2e61d56fd3ebb4b40b713d63f96dd92d28144"}, 501 | {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd3f79e17b56741b5177bcc36307750d50ea0698df6aa82f69c7db32d968c1c2"}, 502 | {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38a5024de321d672a132b1834a66eeb7931959c59964b777e8f32dbe9523f6b1"}, 503 | {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ce426ee691319d4767748c8e0895cfc56593d725594e415f274059bcf3cb76"}, 504 | {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2adaeea59849ec0939af5c5d476935f2bab4b7f0335b0110f0f069a41024278e"}, 505 | {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9b6431559676a1079eac0f52d6d0721fb8e3c5ba43c37bc537c8c83724031feb"}, 506 | {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:85233abb44bc18d16e72dc05bf13848a36f363f83757541f1a97db2f8d58cfd9"}, 507 | {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:641a018af4fe48be57a2b3d7a1f0f5dbca07c1d00951d3d7463f0ac9dac66622"}, 508 | {file = "pydantic_core-2.18.1-cp38-none-win32.whl", hash = "sha256:63d7523cd95d2fde0d28dc42968ac731b5bb1e516cc56b93a50ab293f4daeaad"}, 509 | {file = "pydantic_core-2.18.1-cp38-none-win_amd64.whl", hash = "sha256:907a4d7720abfcb1c81619863efd47c8a85d26a257a2dbebdb87c3b847df0278"}, 510 | {file = "pydantic_core-2.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:aad17e462f42ddbef5984d70c40bfc4146c322a2da79715932cd8976317054de"}, 511 | {file = "pydantic_core-2.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:94b9769ba435b598b547c762184bcfc4783d0d4c7771b04a3b45775c3589ca44"}, 512 | {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80e0e57cc704a52fb1b48f16d5b2c8818da087dbee6f98d9bf19546930dc64b5"}, 513 | {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76b86e24039c35280ceee6dce7e62945eb93a5175d43689ba98360ab31eebc4a"}, 514 | {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a05db5013ec0ca4a32cc6433f53faa2a014ec364031408540ba858c2172bb0"}, 515 | {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:250ae39445cb5475e483a36b1061af1bc233de3e9ad0f4f76a71b66231b07f88"}, 516 | {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a32204489259786a923e02990249c65b0f17235073149d0033efcebe80095570"}, 517 | {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6395a4435fa26519fd96fdccb77e9d00ddae9dd6c742309bd0b5610609ad7fb2"}, 518 | {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2533ad2883f001efa72f3d0e733fb846710c3af6dcdd544fe5bf14fa5fe2d7db"}, 519 | {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b560b72ed4816aee52783c66854d96157fd8175631f01ef58e894cc57c84f0f6"}, 520 | {file = "pydantic_core-2.18.1-cp39-none-win32.whl", hash = "sha256:582cf2cead97c9e382a7f4d3b744cf0ef1a6e815e44d3aa81af3ad98762f5a9b"}, 521 | {file = "pydantic_core-2.18.1-cp39-none-win_amd64.whl", hash = "sha256:ca71d501629d1fa50ea7fa3b08ba884fe10cefc559f5c6c8dfe9036c16e8ae89"}, 522 | {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e178e5b66a06ec5bf51668ec0d4ac8cfb2bdcb553b2c207d58148340efd00143"}, 523 | {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:72722ce529a76a4637a60be18bd789d8fb871e84472490ed7ddff62d5fed620d"}, 524 | {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fe0c1ce5b129455e43f941f7a46f61f3d3861e571f2905d55cdbb8b5c6f5e2c"}, 525 | {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4284c621f06a72ce2cb55f74ea3150113d926a6eb78ab38340c08f770eb9b4d"}, 526 | {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0c3e718f4e064efde68092d9d974e39572c14e56726ecfaeebbe6544521f47"}, 527 | {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2027493cc44c23b598cfaf200936110433d9caa84e2c6cf487a83999638a96ac"}, 528 | {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:76909849d1a6bffa5a07742294f3fa1d357dc917cb1fe7b470afbc3a7579d539"}, 529 | {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ee7ccc7fb7e921d767f853b47814c3048c7de536663e82fbc37f5eb0d532224b"}, 530 | {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ee2794111c188548a4547eccc73a6a8527fe2af6cf25e1a4ebda2fd01cdd2e60"}, 531 | {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a139fe9f298dc097349fb4f28c8b81cc7a202dbfba66af0e14be5cfca4ef7ce5"}, 532 | {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d074b07a10c391fc5bbdcb37b2f16f20fcd9e51e10d01652ab298c0d07908ee2"}, 533 | {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c69567ddbac186e8c0aadc1f324a60a564cfe25e43ef2ce81bcc4b8c3abffbae"}, 534 | {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf1c7b78cddb5af00971ad5294a4583188bda1495b13760d9f03c9483bb6203"}, 535 | {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2684a94fdfd1b146ff10689c6e4e815f6a01141781c493b97342cdc5b06f4d5d"}, 536 | {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:73c1bc8a86a5c9e8721a088df234265317692d0b5cd9e86e975ce3bc3db62a59"}, 537 | {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e60defc3c15defb70bb38dd605ff7e0fae5f6c9c7cbfe0ad7868582cb7e844a6"}, 538 | {file = "pydantic_core-2.18.1.tar.gz", hash = "sha256:de9d3e8717560eb05e28739d1b35e4eac2e458553a52a301e51352a7ffc86a35"}, 539 | ] 540 | 541 | [package.dependencies] 542 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 543 | 544 | [[package]] 545 | name = "pytest" 546 | version = "8.1.1" 547 | description = "pytest: simple powerful testing with Python" 548 | optional = false 549 | python-versions = ">=3.8" 550 | files = [ 551 | {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, 552 | {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, 553 | ] 554 | 555 | [package.dependencies] 556 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 557 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 558 | iniconfig = "*" 559 | packaging = "*" 560 | pluggy = ">=1.4,<2.0" 561 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 562 | 563 | [package.extras] 564 | testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 565 | 566 | [[package]] 567 | name = "pytest-cov" 568 | version = "5.0.0" 569 | description = "Pytest plugin for measuring coverage." 570 | optional = false 571 | python-versions = ">=3.8" 572 | files = [ 573 | {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, 574 | {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, 575 | ] 576 | 577 | [package.dependencies] 578 | coverage = {version = ">=5.2.1", extras = ["toml"]} 579 | pytest = ">=4.6" 580 | 581 | [package.extras] 582 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] 583 | 584 | [[package]] 585 | name = "requests" 586 | version = "2.31.0" 587 | description = "Python HTTP for Humans." 588 | optional = false 589 | python-versions = ">=3.7" 590 | files = [ 591 | {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, 592 | {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, 593 | ] 594 | 595 | [package.dependencies] 596 | certifi = ">=2017.4.17" 597 | charset-normalizer = ">=2,<4" 598 | idna = ">=2.5,<4" 599 | urllib3 = ">=1.21.1,<3" 600 | 601 | [package.extras] 602 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 603 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 604 | 605 | [[package]] 606 | name = "sniffio" 607 | version = "1.3.1" 608 | description = "Sniff out which async library your code is running under" 609 | optional = false 610 | python-versions = ">=3.7" 611 | files = [ 612 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 613 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 614 | ] 615 | 616 | [[package]] 617 | name = "starlette" 618 | version = "0.37.2" 619 | description = "The little ASGI library that shines." 620 | optional = false 621 | python-versions = ">=3.8" 622 | files = [ 623 | {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"}, 624 | {file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"}, 625 | ] 626 | 627 | [package.dependencies] 628 | anyio = ">=3.4.0,<5" 629 | 630 | [package.extras] 631 | full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] 632 | 633 | [[package]] 634 | name = "tomli" 635 | version = "2.0.1" 636 | description = "A lil' TOML parser" 637 | optional = false 638 | python-versions = ">=3.7" 639 | files = [ 640 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 641 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 642 | ] 643 | 644 | [[package]] 645 | name = "typing-extensions" 646 | version = "4.11.0" 647 | description = "Backported and Experimental Type Hints for Python 3.8+" 648 | optional = false 649 | python-versions = ">=3.8" 650 | files = [ 651 | {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, 652 | {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, 653 | ] 654 | 655 | [[package]] 656 | name = "urllib3" 657 | version = "2.2.1" 658 | description = "HTTP library with thread-safe connection pooling, file post, and more." 659 | optional = false 660 | python-versions = ">=3.8" 661 | files = [ 662 | {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, 663 | {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, 664 | ] 665 | 666 | [package.extras] 667 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 668 | h2 = ["h2 (>=4,<5)"] 669 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 670 | zstd = ["zstandard (>=0.18.0)"] 671 | 672 | [[package]] 673 | name = "uvicorn" 674 | version = "0.29.0" 675 | description = "The lightning-fast ASGI server." 676 | optional = false 677 | python-versions = ">=3.8" 678 | files = [ 679 | {file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"}, 680 | {file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"}, 681 | ] 682 | 683 | [package.dependencies] 684 | click = ">=7.0" 685 | h11 = ">=0.8" 686 | typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} 687 | 688 | [package.extras] 689 | standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] 690 | 691 | [metadata] 692 | lock-version = "2.0" 693 | python-versions = "^3.10" 694 | content-hash = "c86a2f7d879c6f81a2d903473a16249faa390219980b4dcbe3b0c88c01f47ba9" 695 | -------------------------------------------------------------------------------- /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 | gunicorn = "22.0.0" 11 | uvicorn = "0.29.0" 12 | fastapi = "0.110.2" 13 | 14 | [tool.poetry.dev-dependencies] 15 | pytest = "8.1.1" 16 | pytest-cov = "5.0.0" 17 | coverage = "7.4.4" 18 | black = "24.4.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: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-pfeiffer/uvicorn-gunicorn-poetry/ad8ee32e791e695b91318e9ca5c947f0011d4751/examples/fast_api_singlestage_build/tests/__init__.py -------------------------------------------------------------------------------- /examples/fast_api_singlestage_build/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from fastapi.testclient import TestClient 3 | 4 | from app.main import app 5 | 6 | 7 | @pytest.fixture(scope="module") 8 | def test_client() -> TestClient: 9 | return TestClient(app) 10 | -------------------------------------------------------------------------------- /examples/fast_api_singlestage_build/tests/test_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-pfeiffer/uvicorn-gunicorn-poetry/ad8ee32e791e695b91318e9ca5c947f0011d4751/examples/fast_api_singlestage_build/tests/test_api/__init__.py -------------------------------------------------------------------------------- /examples/fast_api_singlestage_build/tests/test_api/test_api_endpoints.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from requests import Response 4 | from starlette import status 5 | 6 | from app.main import ITEMS, HELLO_WORLD 7 | 8 | 9 | def test_root(test_client): 10 | response: Response = test_client.get("/") 11 | 12 | assert response.status_code == status.HTTP_200_OK 13 | assert response.json() == HELLO_WORLD 14 | 15 | 16 | def test_items(test_client): 17 | key, value = random.choice(list(ITEMS.items())) 18 | 19 | response: Response = test_client.get(f"/items/{key}") 20 | 21 | assert response.status_code == status.HTTP_200_OK 22 | assert response.json() == value 23 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "uvicorn-gunicorn-poetry" 3 | version = "2.2.0" 4 | description = "Gunicorn with Uvicorn workers for running web applications. 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 | docker = "7.0.0" 12 | python-on-whales = "0.70.1" 13 | 14 | [tool.poetry.dev-dependencies] 15 | black = "24.4.0" 16 | bcrypt = "4.1.2" 17 | coverage = "7.4.4" 18 | docker-image-py = "0.1.12" 19 | pytest = "8.1.1" 20 | pytest-cov = "5.0.0" 21 | pytest-dotenv = "0.5.2" 22 | pylint = "2.16.2" 23 | pre-commit = "3.7.0" 24 | requests = "2.28.2" 25 | semver = "2.13.0" 26 | testcontainers = "4.4.0" 27 | 28 | 29 | # https://docs.pytest.org/en/latest/reference/customize.html 30 | [tool.pytest.ini_options] 31 | testpaths = [ 32 | "tests", 33 | ] 34 | 35 | # https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#configuration-via-a-file 36 | [tool.black] 37 | line-length = 80 38 | target-version = ['py39'] 39 | 40 | [tool.pylint.main] 41 | errors-only = true 42 | recursive = "y" 43 | 44 | [build-system] 45 | requires = ["poetry-core>=1.0.0"] 46 | build-backend = "poetry.core.masonry.api" 47 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-pfeiffer/uvicorn-gunicorn-poetry/ad8ee32e791e695b91318e9ca5c947f0011d4751/tests/__init__.py -------------------------------------------------------------------------------- /tests/build_image/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-pfeiffer/uvicorn-gunicorn-poetry/ad8ee32e791e695b91318e9ca5c947f0011d4751/tests/build_image/__init__.py -------------------------------------------------------------------------------- /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 | from testcontainers.registry import DockerRegistryContainer 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 | from tests.constants import REGISTRY_PASSWORD, REGISTRY_USERNAME 16 | from tests.utils import ( 17 | get_fast_api_multistage_context, 18 | get_fast_api_multistage_image_reference, 19 | get_fast_api_singlestage_context, 20 | get_fast_api_singlestage_image_reference, 21 | ) 22 | 23 | 24 | @pytest.fixture(scope="session") 25 | def cache_settings(python_version: str, os_variant: str) -> tuple: 26 | """Fixture for providing cache settings. 27 | 28 | :param python_version: 29 | :param os_variant: 30 | :return: 31 | """ 32 | github_ref_name: str = getenv("GITHUB_REF_NAME") 33 | cache_scope: str = f"{python_version}-{os_variant}" 34 | 35 | if github_ref_name: 36 | cache_to: str = ( 37 | f"type=gha,mode=max,scope={github_ref_name}-{cache_scope}" 38 | ) 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( 112 | python_version, os_variant 113 | ), 114 | "APPLICATION_SERVER_PORT": APPLICATION_SERVER_PORT, 115 | }, 116 | tags=image_reference, 117 | platforms=PLATFORMS, 118 | builder=pow_buildx_builder, 119 | cache_to=cache_settings[0], 120 | cache_from=cache_settings[1], 121 | push=True, 122 | ) 123 | yield image_reference 124 | 125 | 126 | @pytest.fixture(scope="package") 127 | def fast_api_singlestage_image_reference( 128 | docker_client: DockerClient, 129 | pow_buildx_builder: Builder, 130 | registry_container: DockerRegistryContainer, 131 | image_version: str, 132 | python_version: str, 133 | os_variant: str, 134 | cache_settings: tuple, 135 | base_image_reference: str, 136 | registry_login, 137 | ) -> str: 138 | """Fixture providing a single stage image build for example application. 139 | 140 | :param docker_client: 141 | :param pow_buildx_builder: 142 | :param registry_container: 143 | :param image_version: 144 | :param python_version: 145 | :param os_variant: 146 | :param cache_settings: 147 | :param base_image_reference: 148 | :param registry_login: 149 | :return: 150 | """ 151 | image_reference: str = get_fast_api_singlestage_image_reference( 152 | registry_container.get_registry(), 153 | image_version, 154 | python_version, 155 | os_variant, 156 | ) 157 | 158 | docker_client.buildx.build( 159 | context_path=get_fast_api_singlestage_context(), 160 | build_args={ 161 | "BASE_IMAGE_NAME_AND_TAG": base_image_reference, 162 | }, 163 | tags=image_reference, 164 | platforms=PLATFORMS, 165 | builder=pow_buildx_builder, 166 | cache_to=cache_settings[0], 167 | cache_from=cache_settings[1], 168 | push=True, 169 | ) 170 | yield image_reference 171 | 172 | 173 | @pytest.fixture(scope="package") 174 | def fast_api_multistage_image_reference( 175 | docker_client: DockerClient, 176 | pow_buildx_builder: Builder, 177 | registry_container: DockerRegistryContainer, 178 | image_version: str, 179 | python_version: str, 180 | os_variant: str, 181 | cache_settings: tuple, 182 | base_image_reference: str, 183 | registry_login, 184 | ) -> str: 185 | """Fixture providing a multi-stage image build for example application. 186 | 187 | :param docker_client: 188 | :param pow_buildx_builder: 189 | :param registry_container: 190 | :param image_version: 191 | :param python_version: 192 | :param os_variant: 193 | :param cache_settings: 194 | :param base_image_reference: 195 | :param registry_login: 196 | :return: 197 | """ 198 | image_reference: str = get_fast_api_multistage_image_reference( 199 | registry_container.get_registry(), 200 | image_version, 201 | python_version, 202 | os_variant, 203 | ) 204 | 205 | docker_client.buildx.build( 206 | context_path=get_fast_api_multistage_context(), 207 | target="production-image", 208 | build_args={ 209 | "BASE_IMAGE_NAME_AND_TAG": base_image_reference, 210 | }, 211 | tags=image_reference, 212 | platforms=PLATFORMS, 213 | builder=pow_buildx_builder, 214 | cache_to=cache_settings[0], 215 | cache_from=cache_settings[1], 216 | push=True, 217 | ) 218 | yield image_reference 219 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/build_image/test_default_configuration.py: -------------------------------------------------------------------------------- 1 | import json 2 | from time import sleep 3 | 4 | import requests 5 | from python_on_whales import DockerClient 6 | 7 | from build.constants import APPLICATION_SERVER_PORT 8 | from build.gunicorn_configuration import DEFAULT_GUNICORN_CONFIG 9 | from tests.constants import SLEEP_TIME, HELLO_WORLD, EXPOSED_CONTAINER_PORT 10 | from tests.utils import UvicornGunicornPoetryContainerConfig 11 | 12 | 13 | def verify_container_config( 14 | container_config: UvicornGunicornPoetryContainerConfig, 15 | ) -> None: 16 | response = requests.get(f"http://127.0.0.1:{EXPOSED_CONTAINER_PORT}") 17 | assert json.loads(response.text) == HELLO_WORLD 18 | 19 | config_data = container_config.get_config() 20 | assert config_data["bind"] == DEFAULT_GUNICORN_CONFIG["bind"] 21 | assert config_data["workers"] == DEFAULT_GUNICORN_CONFIG["workers"] 22 | assert config_data["timeout"] == DEFAULT_GUNICORN_CONFIG["timeout"] 23 | assert ( 24 | config_data["graceful_timeout"] 25 | == DEFAULT_GUNICORN_CONFIG["graceful_timeout"] 26 | ) 27 | assert config_data["keepalive"] == DEFAULT_GUNICORN_CONFIG["keepalive"] 28 | assert config_data["loglevel"] == DEFAULT_GUNICORN_CONFIG["loglevel"] 29 | assert config_data["accesslog"] == DEFAULT_GUNICORN_CONFIG["accesslog"] 30 | assert config_data["errorlog"] == DEFAULT_GUNICORN_CONFIG["errorlog"] 31 | assert ( 32 | config_data["worker_tmp_dir"] 33 | == DEFAULT_GUNICORN_CONFIG["worker_tmp_dir"] 34 | ) 35 | 36 | 37 | def test_fast_api_singlestage_image( 38 | docker_client: DockerClient, 39 | fast_api_singlestage_image_reference: str, 40 | ) -> None: 41 | """Test default configuration for single stage image. 42 | 43 | :param docker_client: 44 | :param fast_api_singlestage_image_reference: 45 | :return: 46 | """ 47 | with docker_client.container.run( 48 | fast_api_singlestage_image_reference, 49 | detach=True, 50 | publish=[(EXPOSED_CONTAINER_PORT, APPLICATION_SERVER_PORT)], 51 | ) as container: 52 | # Wait for uvicorn to come up 53 | sleep(SLEEP_TIME) 54 | 55 | uvicorn_gunicorn_container_config: ( 56 | UvicornGunicornPoetryContainerConfig 57 | ) = UvicornGunicornPoetryContainerConfig(container.id) 58 | 59 | assert ( 60 | f"{APPLICATION_SERVER_PORT}/tcp" 61 | in container.config.exposed_ports.keys() 62 | ) 63 | 64 | verify_container_config(uvicorn_gunicorn_container_config) 65 | 66 | 67 | def test_fast_api_multistage_image( 68 | docker_client: DockerClient, 69 | fast_api_multistage_image_reference: str, 70 | ) -> None: 71 | """Test default configuration for multi-stage image. 72 | 73 | :param docker_client: 74 | :param fast_api_multistage_image_reference: 75 | :return: 76 | """ 77 | with docker_client.container.run( 78 | fast_api_multistage_image_reference, 79 | detach=True, 80 | publish=[(EXPOSED_CONTAINER_PORT, APPLICATION_SERVER_PORT)], 81 | ) as container: 82 | # Wait for uvicorn to come up 83 | sleep(SLEEP_TIME) 84 | 85 | uvicorn_gunicorn_container_config: ( 86 | UvicornGunicornPoetryContainerConfig 87 | ) = UvicornGunicornPoetryContainerConfig(container.id) 88 | 89 | assert ( 90 | f"{APPLICATION_SERVER_PORT}/tcp" 91 | in container.config.exposed_ports.keys() 92 | ) 93 | 94 | verify_container_config(uvicorn_gunicorn_container_config) 95 | -------------------------------------------------------------------------------- /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 | SLEEP_TIME: float = 4.0 2 | HELLO_WORLD: str = "Hello World!" 3 | DEVELOPMENT_GUNICORN_CONFIG: dict[str, str] = { 4 | "bind": "0.0.0.0:80", 5 | "workers": 1, 6 | "timeout": 30, 7 | "graceful_timeout": 30, 8 | "keepalive": 2, 9 | "loglevel": "debug", 10 | "accesslog": "-", 11 | "errorlog": "-", 12 | "worker_tmp_dir": "/dev/shm", 13 | } 14 | EXPOSED_CONTAINER_PORT: str = "8000" 15 | REGISTRY_USERNAME: str = "foo" 16 | REGISTRY_PASSWORD: str = "bar" 17 | -------------------------------------------------------------------------------- /tests/publish_image/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-pfeiffer/uvicorn-gunicorn-poetry/ad8ee32e791e695b91318e9ca5c947f0011d4751/tests/publish_image/__init__.py -------------------------------------------------------------------------------- /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 | from testcontainers.registry import DockerRegistryContainer 6 | 7 | from build.publish import main 8 | from tests.constants import REGISTRY_PASSWORD, REGISTRY_USERNAME 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 | from testcontainers.registry import DockerRegistryContainer 6 | 7 | from build.publish import main 8 | from tests.constants import REGISTRY_PASSWORD, REGISTRY_USERNAME 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/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | from dataclasses import dataclass 3 | from pathlib import Path 4 | from typing import List, Dict, Any 5 | 6 | import docker 7 | from docker.models.containers import Container 8 | from docker_image import reference 9 | 10 | 11 | class UvicornGunicornPoetryContainerConfig: 12 | def __init__(self, container_id: str): 13 | self.container: Container = docker.from_env().containers.get( 14 | container_id 15 | ) 16 | 17 | def get_process_names(self) -> List[str]: 18 | top = self.container.top() 19 | process_commands = [p[7] for p in top["Processes"]] 20 | gunicorn_processes = [p for p in process_commands if "gunicorn" in p] 21 | return gunicorn_processes 22 | 23 | def get_gunicorn_conf_path(self) -> str: 24 | gunicorn_processes = self.get_process_names() 25 | first_process = gunicorn_processes[0] 26 | first_part, partition, last_part = first_process.partition("-c") 27 | gunicorn_conf = last_part.strip().split()[0] 28 | return gunicorn_conf 29 | 30 | def get_config(self) -> Dict[str, Any]: 31 | gunicorn_conf = self.get_gunicorn_conf_path() 32 | result = self.container.exec_run(f"python {gunicorn_conf}") 33 | return json.loads(result.output.decode()) 34 | 35 | 36 | @dataclass 37 | class ImageTagComponents: 38 | """Class for parsing and providing image tag components.""" 39 | 40 | registry: str 41 | image_name: str 42 | tag: str 43 | version: str 44 | python_version: str 45 | os_variant: str 46 | 47 | @classmethod 48 | def create_from_reference(cls, tag: str): 49 | """Instantiate a class using an image tag. 50 | 51 | :param tag: 52 | :return: 53 | """ 54 | ref = reference.Reference.parse(tag) 55 | registry: str = ref.repository["domain"] 56 | image_name: str = ref.repository["path"] 57 | tag: str = ref["tag"] 58 | 59 | tag_parts: list[str] = tag.split("-") 60 | version: str = tag_parts[0] 61 | python_version: str = tag_parts[1].lstrip("python") 62 | os_variant: str = "-".join(tag_parts[2:]) 63 | return cls( 64 | registry=registry, 65 | image_name=image_name, 66 | tag=tag, 67 | version=version, 68 | python_version=python_version, 69 | os_variant=os_variant, 70 | ) 71 | 72 | 73 | def get_fast_api_singlestage_context() -> Path: 74 | """Return Docker build context for single stage example app. 75 | 76 | :return: 77 | """ 78 | context: Path = ( 79 | Path(__file__).parent.parent.resolve() 80 | / "examples" 81 | / "fast_api_singlestage_build" 82 | ) 83 | return context 84 | 85 | 86 | def get_fast_api_singlestage_image_reference( 87 | registry: str, 88 | image_version: str, 89 | python_version: str, 90 | os_variant: str, 91 | ) -> str: 92 | """Return image reference for single stage example app. 93 | 94 | :param registry: 95 | :param image_version: 96 | :param python_version: 97 | :param os_variant: 98 | :return: 99 | """ 100 | reference: str = ( 101 | f"{registry}/fast-api-singlestage-build:{image_version}" 102 | f"-python{python_version}-{os_variant}" 103 | ) 104 | return reference 105 | 106 | 107 | def get_fast_api_multistage_context() -> Path: 108 | """Return Docker build context for multi-stage example app. 109 | 110 | :return: 111 | """ 112 | context: Path = ( 113 | Path(__file__).parent.parent.resolve() 114 | / "examples" 115 | / "fast_api_multistage_build" 116 | ) 117 | return context 118 | 119 | 120 | def get_fast_api_multistage_image_reference( 121 | registry: str, 122 | image_version: str, 123 | python_version: str, 124 | os_variant: str, 125 | ) -> str: 126 | """Return image reference for multi-stage example app. 127 | 128 | :param registry: 129 | :param image_version: 130 | :param python_version: 131 | :param os_variant: 132 | :return: 133 | """ 134 | reference: str = ( 135 | f"{registry}/fast-api-multistage-build:{image_version}" 136 | f"-python{python_version}-{os_variant}" 137 | ) 138 | return reference 139 | 140 | 141 | def get_fast_api_multistage_with_json_logging_context() -> Path: 142 | """Return Docker build context for multi-stage example app with JSON logging. 143 | 144 | :return: 145 | """ 146 | context: Path = ( 147 | Path(__file__).parent.parent.resolve() 148 | / "examples" 149 | / "fast_api_multistage_build_with_json_logging" 150 | ) 151 | return context 152 | 153 | 154 | def get_fast_api_multistage_with_json_logging_image_reference( 155 | registry: str, 156 | image_version: str, 157 | python_version: str, 158 | os_variant: str, 159 | ) -> str: 160 | """Return image reference for multi-stage example app with JSON logging. 161 | 162 | :param registry: 163 | :param image_version: 164 | :param python_version: 165 | :param os_variant: 166 | :return: 167 | """ 168 | reference: str = ( 169 | f"{registry}/fast_api_multistage_build_with_json_logging:{image_version}" 170 | f"-python{python_version}-{os_variant}" 171 | ) 172 | return reference 173 | --------------------------------------------------------------------------------