├── .dockerignore ├── .github └── workflows │ └── lint-and-test.yml ├── .gitignore ├── LICENCE ├── README.md ├── app └── main.py ├── docker ├── Dockerfile ├── docker-entrypoint.sh └── gunicorn_conf.py ├── poetry.lock ├── pyproject.toml ├── scripts ├── build.sh ├── lint.sh └── test.sh └── tests ├── __init__.py ├── conftest.py └── test_main.py /.dockerignore: -------------------------------------------------------------------------------- 1 | 2 | .git 3 | .github 4 | .dockerignore 5 | .gitignore 6 | 7 | .idea 8 | .vscode 9 | 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | *.so 14 | htmlcov/ 15 | .coverage 16 | .coverage.* 17 | .pytest_cache/ 18 | .venv 19 | venv 20 | 21 | .DS_Store 22 | .AppleDouble 23 | .LSOverride 24 | ._* 25 | 26 | .vscode 27 | .idea 28 | -------------------------------------------------------------------------------- /.github/workflows/lint-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - name: Build 13 | run: docker build --tag poetry-project --file docker/Dockerfile . 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | 4 | # Python 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | *.so 9 | htmlcov/ 10 | .coverage 11 | .coverage.* 12 | .pytest_cache/ 13 | .venv 14 | venv 15 | 16 | .DS_Store 17 | .AppleDouble 18 | .LSOverride 19 | ._* 20 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Michael Oliver 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Poetry managed Python FastAPI application with Docker multi-stage builds 2 | 3 | ### This repo serves as a minimal reference on setting up docker multi-stage builds with poetry 4 | 5 | 6 | ### Requirements 7 | 8 | - [Docker >= 17.05](https://www.python.org/downloads/release/python-381/) 9 | - [Python >= 3.7](https://www.python.org/downloads/release/python-381/) 10 | - [Poetry](https://github.com/python-poetry/poetry) 11 | 12 | 13 | --- 14 | **NOTE** - Run all commands from the project root 15 | 16 | 17 | ## Local development 18 | 19 | --- 20 | ## Poetry 21 | 22 | 23 | Create the virtual environment and install dependencies with: 24 | 25 | poetry install 26 | 27 | See the [poetry docs](https://python-poetry.org/docs/) for information on how to add/update dependencies. 28 | 29 | Run commands inside the virtual environment with: 30 | 31 | poetry run 32 | 33 | Spawn a shell inside the virtual environment with 34 | 35 | poetry shell 36 | 37 | Start a development server locally 38 | 39 | poetry run uvicorn app.main:app --reload --host localhost --port 8000 40 | 41 | API will be available at [localhost:8000/](http://localhost:8000/) 42 | 43 | Swagger docs at [localhost:8000/docs](http://localhost:8000/docs) 44 | 45 | To run testing/linting locally you would execute lint/test in the [scripts directory](/scripts). 46 | 47 | 48 | --- 49 | 50 | ## Docker 51 | 52 | 53 | Build images with: 54 | 55 | docker build --tag poetry-project --file docker/Dockerfile . 56 | 57 | The Dockerfile uses multi-stage builds to run lint and test stages before building the production stage. If linting or testing fails the build will fail. 58 | 59 | You can stop the build at specific stages with the `--target` option: 60 | 61 | docker build --name poetry-project --file docker/Dockerfile . --target 62 | 63 | 64 | For example we wanted to stop at the **test** stage: 65 | 66 | docker build --tag poetry-project --file docker/Dockerfile --target test . 67 | 68 | We could then get a shell inside the container with: 69 | 70 | docker run -it poetry-project:latest bash 71 | 72 | If you do not specify a target the resulting image will be the last image defined which in our case is the 'production' image. 73 | 74 | 75 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Minimal FastAPI application taken directly from the tutorial. 3 | https://fastapi.tiangolo.com/ 4 | """ 5 | 6 | from fastapi import FastAPI 7 | from pydantic import BaseModel 8 | 9 | app = FastAPI() 10 | 11 | 12 | class Item(BaseModel): 13 | name: str 14 | price: float 15 | is_offer: bool = None 16 | 17 | 18 | @app.get("/") 19 | def read_root(): 20 | return {"Hello": "World"} 21 | 22 | 23 | @app.get("/items/{item_id}") 24 | def read_item(item_id: int, q: str = None): 25 | return {"item_id": item_id, "q": q} 26 | 27 | 28 | @app.put("/items/{item_id}") 29 | def update_item(item_id: int, item: Item): 30 | return {"item_name": item.name, "item_id": item_id} 31 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | # Uses multi-stage builds requiring Docker 17.05 or higher 3 | # See https://docs.docker.com/develop/develop-images/multistage-build/ 4 | 5 | # Creating a python base with shared environment variables 6 | FROM python:3.8.1-slim as python-base 7 | ENV PYTHONUNBUFFERED=1 \ 8 | PYTHONDONTWRITEBYTECODE=1 \ 9 | PIP_NO_CACHE_DIR=off \ 10 | PIP_DISABLE_PIP_VERSION_CHECK=on \ 11 | PIP_DEFAULT_TIMEOUT=100 \ 12 | POETRY_HOME="/opt/poetry" \ 13 | POETRY_VIRTUALENVS_IN_PROJECT=true \ 14 | POETRY_NO_INTERACTION=1 \ 15 | PYSETUP_PATH="/opt/pysetup" \ 16 | VENV_PATH="/opt/pysetup/.venv" 17 | 18 | ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH" 19 | 20 | 21 | # builder-base is used to build dependencies 22 | FROM python-base as builder-base 23 | RUN apt-get update \ 24 | && apt-get install --no-install-recommends -y \ 25 | curl \ 26 | build-essential 27 | 28 | # Install Poetry - respects $POETRY_VERSION & $POETRY_HOME 29 | ENV POETRY_VERSION=1.0.5 30 | RUN curl -sSL https://install.python-poetry.org | python3 - 31 | 32 | # We copy our Python requirements here to cache them 33 | # and install only runtime deps using poetry 34 | WORKDIR $PYSETUP_PATH 35 | COPY ./poetry.lock ./pyproject.toml ./ 36 | RUN poetry install --no-dev # respects 37 | 38 | 39 | # 'development' stage installs all dev deps and can be used to develop code. 40 | # For example using docker-compose to mount local volume under /app 41 | FROM python-base as development 42 | ENV FASTAPI_ENV=development 43 | 44 | # Copying poetry and venv into image 45 | COPY --from=builder-base $POETRY_HOME $POETRY_HOME 46 | COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH 47 | 48 | # Copying in our entrypoint 49 | COPY ./docker/docker-entrypoint.sh /docker-entrypoint.sh 50 | RUN chmod +x /docker-entrypoint.sh 51 | 52 | # venv already has runtime deps installed we get a quicker install 53 | WORKDIR $PYSETUP_PATH 54 | RUN poetry install 55 | 56 | WORKDIR /app 57 | COPY . . 58 | 59 | EXPOSE 8000 60 | ENTRYPOINT /docker-entrypoint.sh $0 $@ 61 | CMD ["poetry", "run", "uvicorn", "app.main:app", "--host=0.0.0.0", "--port=8000", "--reload"] 62 | 63 | 64 | # 'lint' stage runs black and isort 65 | # running in check mode means build will fail if any linting errors occur 66 | FROM development AS lint 67 | RUN black --config ./pyproject.toml --check app tests 68 | RUN isort --settings-path ./pyproject.toml --recursive --check-only 69 | CMD ["tail", "-f", "/dev/null"] 70 | 71 | 72 | # 'test' stage runs our unit tests with pytest and 73 | # coverage. Build will fail if test coverage is under 95% 74 | FROM development AS test 75 | RUN coverage run --rcfile ./pyproject.toml -m pytest ./tests 76 | RUN coverage report --fail-under 95 77 | 78 | 79 | # 'production' stage uses the clean 'python-base' stage and copyies 80 | # in only our runtime deps that were installed in the 'builder-base' 81 | FROM python-base as production 82 | ENV FASTAPI_ENV=production 83 | 84 | COPY --from=builder-base $VENV_PATH $VENV_PATH 85 | COPY ./docker/gunicorn_conf.py /gunicorn_conf.py 86 | 87 | COPY ./docker/docker-entrypoint.sh /docker-entrypoint.sh 88 | RUN chmod +x /docker-entrypoint.sh 89 | 90 | COPY ./app /app 91 | WORKDIR /app 92 | 93 | ENTRYPOINT /docker-entrypoint.sh $0 $@ 94 | CMD [ "gunicorn", "--worker-class uvicorn.workers.UvicornWorker", "--config /gunicorn_conf.py", "main:app"] 95 | -------------------------------------------------------------------------------- /docker/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # activate our virtual environment here 6 | . /opt/pysetup/.venv/bin/activate 7 | 8 | # You can put other setup logic here 9 | 10 | # Evaluating passed command: 11 | exec "$@" 12 | -------------------------------------------------------------------------------- /docker/gunicorn_conf.py: -------------------------------------------------------------------------------- 1 | # From: https://github.com/tiangolo/uvicorn-gunicorn-docker/blob/315f04413114e938ff37a410b5979126facc90af/python3.7/gunicorn_conf.py 2 | 3 | import json 4 | import multiprocessing 5 | import os 6 | 7 | workers_per_core_str = os.getenv("WORKERS_PER_CORE", "1") 8 | web_concurrency_str = os.getenv("WEB_CONCURRENCY", None) 9 | host = os.getenv("HOST", "0.0.0.0") 10 | port = os.getenv("PORT", "8000") 11 | bind_env = os.getenv("BIND", None) 12 | use_loglevel = os.getenv("LOG_LEVEL", "info") 13 | if bind_env: 14 | use_bind = bind_env 15 | else: 16 | use_bind = f"{host}:{port}" 17 | 18 | cores = multiprocessing.cpu_count() 19 | workers_per_core = float(workers_per_core_str) 20 | default_web_concurrency = workers_per_core * cores 21 | if web_concurrency_str: 22 | web_concurrency = int(web_concurrency_str) 23 | assert web_concurrency > 0 24 | else: 25 | web_concurrency = max(int(default_web_concurrency), 2) 26 | 27 | # Gunicorn config variables 28 | loglevel = use_loglevel 29 | workers = web_concurrency 30 | bind = use_bind 31 | keepalive = 120 32 | errorlog = "-" 33 | 34 | # For debugging and testing 35 | log_data = { 36 | "loglevel": loglevel, 37 | "workers": workers, 38 | "bind": bind, 39 | # Additional, non-gunicorn variables 40 | "workers_per_core": workers_per_core, 41 | "host": host, 42 | "port": port, 43 | } 44 | print(json.dumps(log_data)) 45 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "dev" 3 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 4 | name = "appdirs" 5 | optional = false 6 | python-versions = "*" 7 | version = "1.4.3" 8 | 9 | [[package]] 10 | category = "dev" 11 | description = "Atomic file writes." 12 | marker = "sys_platform == \"win32\"" 13 | name = "atomicwrites" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 16 | version = "1.3.0" 17 | 18 | [[package]] 19 | category = "dev" 20 | description = "Classes Without Boilerplate" 21 | name = "attrs" 22 | optional = false 23 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 24 | version = "19.3.0" 25 | 26 | [package.extras] 27 | azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] 28 | dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] 29 | docs = ["sphinx", "zope.interface"] 30 | tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 31 | 32 | [[package]] 33 | category = "dev" 34 | description = "The uncompromising code formatter." 35 | name = "black" 36 | optional = false 37 | python-versions = ">=3.6" 38 | version = "19.10b0" 39 | 40 | [package.dependencies] 41 | appdirs = "*" 42 | attrs = ">=18.1.0" 43 | click = ">=6.5" 44 | pathspec = ">=0.6,<1" 45 | regex = "*" 46 | toml = ">=0.9.4" 47 | typed-ast = ">=1.4.0" 48 | 49 | [package.extras] 50 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 51 | 52 | [[package]] 53 | category = "dev" 54 | description = "Python package for providing Mozilla's CA Bundle." 55 | name = "certifi" 56 | optional = false 57 | python-versions = "*" 58 | version = "2019.11.28" 59 | 60 | [[package]] 61 | category = "dev" 62 | description = "Universal encoding detector for Python 2 and 3" 63 | name = "chardet" 64 | optional = false 65 | python-versions = "*" 66 | version = "3.0.4" 67 | 68 | [[package]] 69 | category = "main" 70 | description = "Composable command line interface toolkit" 71 | name = "click" 72 | optional = false 73 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 74 | version = "7.0" 75 | 76 | [[package]] 77 | category = "dev" 78 | description = "Cross-platform colored terminal text." 79 | marker = "sys_platform == \"win32\"" 80 | name = "colorama" 81 | optional = false 82 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 83 | version = "0.4.3" 84 | 85 | [[package]] 86 | category = "dev" 87 | description = "Code coverage measurement for Python" 88 | name = "coverage" 89 | optional = false 90 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 91 | version = "5.0.3" 92 | 93 | [package.dependencies] 94 | [package.dependencies.toml] 95 | optional = true 96 | version = "*" 97 | 98 | [package.extras] 99 | toml = ["toml"] 100 | 101 | [[package]] 102 | category = "main" 103 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 104 | name = "fastapi" 105 | optional = false 106 | python-versions = ">=3.6" 107 | version = "0.52.0" 108 | 109 | [package.dependencies] 110 | pydantic = ">=0.32.2,<2.0.0" 111 | starlette = "0.13.2" 112 | 113 | [package.extras] 114 | all = ["requests", "aiofiles", "jinja2", "python-multipart", "itsdangerous", "pyyaml", "graphene", "ujson", "email-validator", "uvicorn", "async-exit-stack", "async-generator"] 115 | dev = ["pyjwt", "passlib", "autoflake", "flake8", "uvicorn", "graphene"] 116 | doc = ["mkdocs", "mkdocs-material", "markdown-include"] 117 | test = ["pytest (>=4.0.0)", "pytest-cov", "mypy", "black", "isort", "requests", "email-validator", "sqlalchemy", "peewee", "databases", "orjson", "async-exit-stack", "async-generator", "python-multipart", "aiofiles", "ujson", "flask"] 118 | 119 | [[package]] 120 | category = "main" 121 | description = "WSGI HTTP Server for UNIX" 122 | name = "gunicorn" 123 | optional = false 124 | python-versions = ">=3.4" 125 | version = "20.0.4" 126 | 127 | [package.dependencies] 128 | setuptools = ">=3.0" 129 | 130 | [package.extras] 131 | eventlet = ["eventlet (>=0.9.7)"] 132 | gevent = ["gevent (>=0.13)"] 133 | setproctitle = ["setproctitle"] 134 | tornado = ["tornado (>=0.2)"] 135 | 136 | [[package]] 137 | category = "main" 138 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 139 | name = "h11" 140 | optional = false 141 | python-versions = "*" 142 | version = "0.9.0" 143 | 144 | [[package]] 145 | category = "main" 146 | description = "A collection of framework independent HTTP protocol utils." 147 | marker = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"" 148 | name = "httptools" 149 | optional = false 150 | python-versions = "*" 151 | version = "0.1.1" 152 | 153 | [package.extras] 154 | test = ["Cython (0.29.14)"] 155 | 156 | [[package]] 157 | category = "dev" 158 | description = "Internationalized Domain Names in Applications (IDNA)" 159 | name = "idna" 160 | optional = false 161 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 162 | version = "2.9" 163 | 164 | [[package]] 165 | category = "dev" 166 | description = "A Python utility / library to sort Python imports." 167 | name = "isort" 168 | optional = false 169 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 170 | version = "4.3.21" 171 | 172 | [package.dependencies] 173 | [package.dependencies.toml] 174 | optional = true 175 | version = "*" 176 | 177 | [package.extras] 178 | pipfile = ["pipreqs", "requirementslib"] 179 | pyproject = ["toml"] 180 | requirements = ["pipreqs", "pip-api"] 181 | xdg_home = ["appdirs (>=1.4.0)"] 182 | 183 | [[package]] 184 | category = "dev" 185 | description = "More routines for operating on iterables, beyond itertools" 186 | name = "more-itertools" 187 | optional = false 188 | python-versions = ">=3.5" 189 | version = "8.2.0" 190 | 191 | [[package]] 192 | category = "dev" 193 | description = "Core utilities for Python packages" 194 | name = "packaging" 195 | optional = false 196 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 197 | version = "20.1" 198 | 199 | [package.dependencies] 200 | pyparsing = ">=2.0.2" 201 | six = "*" 202 | 203 | [[package]] 204 | category = "dev" 205 | description = "Utility library for gitignore style pattern matching of file paths." 206 | name = "pathspec" 207 | optional = false 208 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 209 | version = "0.7.0" 210 | 211 | [[package]] 212 | category = "dev" 213 | description = "plugin and hook calling mechanisms for python" 214 | name = "pluggy" 215 | optional = false 216 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 217 | version = "0.13.1" 218 | 219 | [package.extras] 220 | dev = ["pre-commit", "tox"] 221 | 222 | [[package]] 223 | category = "dev" 224 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 225 | name = "py" 226 | optional = false 227 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 228 | version = "1.8.1" 229 | 230 | [[package]] 231 | category = "main" 232 | description = "Data validation and settings management using python 3.6 type hinting" 233 | name = "pydantic" 234 | optional = false 235 | python-versions = ">=3.6" 236 | version = "1.4" 237 | 238 | [package.extras] 239 | dotenv = ["python-dotenv (>=0.10.4)"] 240 | email = ["email-validator (>=1.0.3)"] 241 | typing_extensions = ["typing-extensions (>=3.7.2)"] 242 | 243 | [[package]] 244 | category = "dev" 245 | description = "Python parsing module" 246 | name = "pyparsing" 247 | optional = false 248 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 249 | version = "2.4.6" 250 | 251 | [[package]] 252 | category = "dev" 253 | description = "pytest: simple powerful testing with Python" 254 | name = "pytest" 255 | optional = false 256 | python-versions = ">=3.5" 257 | version = "5.3.5" 258 | 259 | [package.dependencies] 260 | atomicwrites = ">=1.0" 261 | attrs = ">=17.4.0" 262 | colorama = "*" 263 | more-itertools = ">=4.0.0" 264 | packaging = "*" 265 | pluggy = ">=0.12,<1.0" 266 | py = ">=1.5.0" 267 | wcwidth = "*" 268 | 269 | [package.extras] 270 | checkqa-mypy = ["mypy (v0.761)"] 271 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 272 | 273 | [[package]] 274 | category = "dev" 275 | description = "Alternative regular expression module, to replace re." 276 | name = "regex" 277 | optional = false 278 | python-versions = "*" 279 | version = "2020.2.20" 280 | 281 | [[package]] 282 | category = "dev" 283 | description = "Python HTTP for Humans." 284 | name = "requests" 285 | optional = false 286 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 287 | version = "2.23.0" 288 | 289 | [package.dependencies] 290 | certifi = ">=2017.4.17" 291 | chardet = ">=3.0.2,<4" 292 | idna = ">=2.5,<3" 293 | urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" 294 | 295 | [package.extras] 296 | security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] 297 | socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] 298 | 299 | [[package]] 300 | category = "dev" 301 | description = "Python 2 and 3 compatibility utilities" 302 | name = "six" 303 | optional = false 304 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 305 | version = "1.14.0" 306 | 307 | [[package]] 308 | category = "main" 309 | description = "The little ASGI library that shines." 310 | name = "starlette" 311 | optional = false 312 | python-versions = ">=3.6" 313 | version = "0.13.2" 314 | 315 | [package.extras] 316 | full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "ujson"] 317 | 318 | [[package]] 319 | category = "dev" 320 | description = "Python Library for Tom's Obvious, Minimal Language" 321 | name = "toml" 322 | optional = false 323 | python-versions = "*" 324 | version = "0.10.0" 325 | 326 | [[package]] 327 | category = "dev" 328 | description = "a fork of Python 2 and 3 ast modules with type comment support" 329 | name = "typed-ast" 330 | optional = false 331 | python-versions = "*" 332 | version = "1.4.1" 333 | 334 | [[package]] 335 | category = "dev" 336 | description = "HTTP library with thread-safe connection pooling, file post, and more." 337 | name = "urllib3" 338 | optional = false 339 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 340 | version = "1.25.8" 341 | 342 | [package.extras] 343 | brotli = ["brotlipy (>=0.6.0)"] 344 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 345 | socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] 346 | 347 | [[package]] 348 | category = "main" 349 | description = "The lightning-fast ASGI server." 350 | name = "uvicorn" 351 | optional = false 352 | python-versions = "*" 353 | version = "0.11.3" 354 | 355 | [package.dependencies] 356 | click = ">=7.0.0,<8.0.0" 357 | h11 = ">=0.8,<0.10" 358 | httptools = ">=0.1.0,<0.2.0" 359 | uvloop = ">=0.14.0" 360 | websockets = ">=8.0.0,<9.0.0" 361 | 362 | [[package]] 363 | category = "main" 364 | description = "Fast implementation of asyncio event loop on top of libuv" 365 | marker = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"" 366 | name = "uvloop" 367 | optional = false 368 | python-versions = "*" 369 | version = "0.14.0" 370 | 371 | [[package]] 372 | category = "dev" 373 | description = "Measures number of Terminal column cells of wide-character codes" 374 | name = "wcwidth" 375 | optional = false 376 | python-versions = "*" 377 | version = "0.1.8" 378 | 379 | [[package]] 380 | category = "main" 381 | description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" 382 | name = "websockets" 383 | optional = false 384 | python-versions = ">=3.6.1" 385 | version = "8.1" 386 | 387 | [metadata] 388 | content-hash = "17865a8620ea8834a5f8498c54bc0a2ec881ab6744a20b1551935a7c701f1fed" 389 | python-versions = "3.8.1" 390 | 391 | [metadata.files] 392 | appdirs = [ 393 | {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"}, 394 | {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"}, 395 | ] 396 | atomicwrites = [ 397 | {file = "atomicwrites-1.3.0-py2.py3-none-any.whl", hash = "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4"}, 398 | {file = "atomicwrites-1.3.0.tar.gz", hash = "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"}, 399 | ] 400 | attrs = [ 401 | {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, 402 | {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, 403 | ] 404 | black = [ 405 | {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, 406 | {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, 407 | ] 408 | certifi = [ 409 | {file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"}, 410 | {file = "certifi-2019.11.28.tar.gz", hash = "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"}, 411 | ] 412 | chardet = [ 413 | {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, 414 | {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, 415 | ] 416 | click = [ 417 | {file = "Click-7.0-py2.py3-none-any.whl", hash = "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13"}, 418 | {file = "Click-7.0.tar.gz", hash = "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"}, 419 | ] 420 | colorama = [ 421 | {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, 422 | {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, 423 | ] 424 | coverage = [ 425 | {file = "coverage-5.0.3-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:cc1109f54a14d940b8512ee9f1c3975c181bbb200306c6d8b87d93376538782f"}, 426 | {file = "coverage-5.0.3-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:be18f4ae5a9e46edae3f329de2191747966a34a3d93046dbdf897319923923bc"}, 427 | {file = "coverage-5.0.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:3230d1003eec018ad4a472d254991e34241e0bbd513e97a29727c7c2f637bd2a"}, 428 | {file = "coverage-5.0.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:e69215621707119c6baf99bda014a45b999d37602cb7043d943c76a59b05bf52"}, 429 | {file = "coverage-5.0.3-cp27-cp27m-win32.whl", hash = "sha256:1daa3eceed220f9fdb80d5ff950dd95112cd27f70d004c7918ca6dfc6c47054c"}, 430 | {file = "coverage-5.0.3-cp27-cp27m-win_amd64.whl", hash = "sha256:51bc7710b13a2ae0c726f69756cf7ffd4362f4ac36546e243136187cfcc8aa73"}, 431 | {file = "coverage-5.0.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9bea19ac2f08672636350f203db89382121c9c2ade85d945953ef3c8cf9d2a68"}, 432 | {file = "coverage-5.0.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:5012d3b8d5a500834783689a5d2292fe06ec75dc86ee1ccdad04b6f5bf231691"}, 433 | {file = "coverage-5.0.3-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:d513cc3db248e566e07a0da99c230aca3556d9b09ed02f420664e2da97eac301"}, 434 | {file = "coverage-5.0.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:3dbb72eaeea5763676a1a1efd9b427a048c97c39ed92e13336e726117d0b72bf"}, 435 | {file = "coverage-5.0.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:15cf13a6896048d6d947bf7d222f36e4809ab926894beb748fc9caa14605d9c3"}, 436 | {file = "coverage-5.0.3-cp35-cp35m-win32.whl", hash = "sha256:fca1669d464f0c9831fd10be2eef6b86f5ebd76c724d1e0706ebdff86bb4adf0"}, 437 | {file = "coverage-5.0.3-cp35-cp35m-win_amd64.whl", hash = "sha256:1e44a022500d944d42f94df76727ba3fc0a5c0b672c358b61067abb88caee7a0"}, 438 | {file = "coverage-5.0.3-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:b26aaf69713e5674efbde4d728fb7124e429c9466aeaf5f4a7e9e699b12c9fe2"}, 439 | {file = "coverage-5.0.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:722e4557c8039aad9592c6a4213db75da08c2cd9945320220634f637251c3894"}, 440 | {file = "coverage-5.0.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:7afad9835e7a651d3551eab18cbc0fdb888f0a6136169fbef0662d9cdc9987cf"}, 441 | {file = "coverage-5.0.3-cp36-cp36m-win32.whl", hash = "sha256:25dbf1110d70bab68a74b4b9d74f30e99b177cde3388e07cc7272f2168bd1477"}, 442 | {file = "coverage-5.0.3-cp36-cp36m-win_amd64.whl", hash = "sha256:c312e57847db2526bc92b9bfa78266bfbaabac3fdcd751df4d062cd4c23e46dc"}, 443 | {file = "coverage-5.0.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:a8b8ac7876bc3598e43e2603f772d2353d9931709345ad6c1149009fd1bc81b8"}, 444 | {file = "coverage-5.0.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:527b4f316e6bf7755082a783726da20671a0cc388b786a64417780b90565b987"}, 445 | {file = "coverage-5.0.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d649dc0bcace6fcdb446ae02b98798a856593b19b637c1b9af8edadf2b150bea"}, 446 | {file = "coverage-5.0.3-cp37-cp37m-win32.whl", hash = "sha256:cd60f507c125ac0ad83f05803063bed27e50fa903b9c2cfee3f8a6867ca600fc"}, 447 | {file = "coverage-5.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c60097190fe9dc2b329a0eb03393e2e0829156a589bd732e70794c0dd804258e"}, 448 | {file = "coverage-5.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:d7008a6796095a79544f4da1ee49418901961c97ca9e9d44904205ff7d6aa8cb"}, 449 | {file = "coverage-5.0.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ea9525e0fef2de9208250d6c5aeeee0138921057cd67fcef90fbed49c4d62d37"}, 450 | {file = "coverage-5.0.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c62a2143e1313944bf4a5ab34fd3b4be15367a02e9478b0ce800cb510e3bbb9d"}, 451 | {file = "coverage-5.0.3-cp38-cp38m-win32.whl", hash = "sha256:b0840b45187699affd4c6588286d429cd79a99d509fe3de0f209594669bb0954"}, 452 | {file = "coverage-5.0.3-cp38-cp38m-win_amd64.whl", hash = "sha256:76e2057e8ffba5472fd28a3a010431fd9e928885ff480cb278877c6e9943cc2e"}, 453 | {file = "coverage-5.0.3-cp39-cp39m-win32.whl", hash = "sha256:b63dd43f455ba878e5e9f80ba4f748c0a2156dde6e0e6e690310e24d6e8caf40"}, 454 | {file = "coverage-5.0.3-cp39-cp39m-win_amd64.whl", hash = "sha256:da93027835164b8223e8e5af2cf902a4c80ed93cb0909417234f4a9df3bcd9af"}, 455 | {file = "coverage-5.0.3.tar.gz", hash = "sha256:77afca04240c40450c331fa796b3eab6f1e15c5ecf8bf2b8bee9706cd5452fef"}, 456 | ] 457 | fastapi = [ 458 | {file = "fastapi-0.52.0-py3-none-any.whl", hash = "sha256:532648b4e16dd33673d71dc0b35dff1b4d20c709d04078010e258b9f3a79771a"}, 459 | {file = "fastapi-0.52.0.tar.gz", hash = "sha256:721b11d8ffde52c669f52741b6d9d761fe2e98778586f4cfd6f5e47254ba5016"}, 460 | ] 461 | gunicorn = [ 462 | {file = "gunicorn-20.0.4-py2.py3-none-any.whl", hash = "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"}, 463 | {file = "gunicorn-20.0.4.tar.gz", hash = "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626"}, 464 | ] 465 | h11 = [ 466 | {file = "h11-0.9.0-py2.py3-none-any.whl", hash = "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"}, 467 | {file = "h11-0.9.0.tar.gz", hash = "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1"}, 468 | ] 469 | httptools = [ 470 | {file = "httptools-0.1.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce"}, 471 | {file = "httptools-0.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4"}, 472 | {file = "httptools-0.1.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6"}, 473 | {file = "httptools-0.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c"}, 474 | {file = "httptools-0.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a"}, 475 | {file = "httptools-0.1.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f"}, 476 | {file = "httptools-0.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2"}, 477 | {file = "httptools-0.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009"}, 478 | {file = "httptools-0.1.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437"}, 479 | {file = "httptools-0.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d"}, 480 | {file = "httptools-0.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be"}, 481 | {file = "httptools-0.1.1.tar.gz", hash = "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce"}, 482 | ] 483 | idna = [ 484 | {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, 485 | {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, 486 | ] 487 | isort = [ 488 | {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, 489 | {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, 490 | ] 491 | more-itertools = [ 492 | {file = "more-itertools-8.2.0.tar.gz", hash = "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"}, 493 | {file = "more_itertools-8.2.0-py3-none-any.whl", hash = "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c"}, 494 | ] 495 | packaging = [ 496 | {file = "packaging-20.1-py2.py3-none-any.whl", hash = "sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73"}, 497 | {file = "packaging-20.1.tar.gz", hash = "sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334"}, 498 | ] 499 | pathspec = [ 500 | {file = "pathspec-0.7.0-py2.py3-none-any.whl", hash = "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424"}, 501 | {file = "pathspec-0.7.0.tar.gz", hash = "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96"}, 502 | ] 503 | pluggy = [ 504 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 505 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 506 | ] 507 | py = [ 508 | {file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"}, 509 | {file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"}, 510 | ] 511 | pydantic = [ 512 | {file = "pydantic-1.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:07911aab70f3bc52bb845ce1748569c5e70478ac977e106a150dd9d0465ebf04"}, 513 | {file = "pydantic-1.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:012c422859bac2e03ab3151ea6624fecf0e249486be7eb8c6ee69c91740c6752"}, 514 | {file = "pydantic-1.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:61d22d36808087d3184ed6ac0d91dd71c533b66addb02e4a9930e1e30833202f"}, 515 | {file = "pydantic-1.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f863456d3d4bf817f2e5248553dee3974c5dc796f48e6ddb599383570f4215ac"}, 516 | {file = "pydantic-1.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:bbbed364376f4a0aebb9ea452ff7968b306499a9e74f4db69b28ff2cd4043a11"}, 517 | {file = "pydantic-1.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e27559cedbd7f59d2375bfd6eea29a330ea1a5b0589c34d6b4e0d7bec6027bbf"}, 518 | {file = "pydantic-1.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:50e4e948892a6815649ad5a9a9379ad1e5f090f17842ac206535dfaed75c6f2f"}, 519 | {file = "pydantic-1.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:8848b4eb458469739126e4c1a202d723dd092e087f8dbe3104371335f87ba5df"}, 520 | {file = "pydantic-1.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:831a0265a9e3933b3d0f04d1a81bba543bafbe4119c183ff2771871db70524ab"}, 521 | {file = "pydantic-1.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:47b8db7024ba3d46c3d4768535e1cf87b6c8cf92ccd81e76f4e1cb8ee47688b3"}, 522 | {file = "pydantic-1.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:51f11c8bbf794a68086540da099aae4a9107447c7a9d63151edbb7d50110cf21"}, 523 | {file = "pydantic-1.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:6100d7862371115c40be55cc4b8d766a74b1d0dbaf99dbfe72bb4bac0faf89ed"}, 524 | {file = "pydantic-1.4-py36.py37.py38-none-any.whl", hash = "sha256:72184c1421103cca128300120f8f1185fb42a9ea73a1c9845b1c53db8c026a7d"}, 525 | {file = "pydantic-1.4.tar.gz", hash = "sha256:f17ec336e64d4583311249fb179528e9a2c27c8a2eaf590ec6ec2c6dece7cb3f"}, 526 | ] 527 | pyparsing = [ 528 | {file = "pyparsing-2.4.6-py2.py3-none-any.whl", hash = "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"}, 529 | {file = "pyparsing-2.4.6.tar.gz", hash = "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f"}, 530 | ] 531 | pytest = [ 532 | {file = "pytest-5.3.5-py3-none-any.whl", hash = "sha256:ff615c761e25eb25df19edddc0b970302d2a9091fbce0e7213298d85fb61fef6"}, 533 | {file = "pytest-5.3.5.tar.gz", hash = "sha256:0d5fe9189a148acc3c3eb2ac8e1ac0742cb7618c084f3d228baaec0c254b318d"}, 534 | ] 535 | regex = [ 536 | {file = "regex-2020.2.20-cp27-cp27m-win32.whl", hash = "sha256:99272d6b6a68c7ae4391908fc15f6b8c9a6c345a46b632d7fdb7ef6c883a2bbb"}, 537 | {file = "regex-2020.2.20-cp27-cp27m-win_amd64.whl", hash = "sha256:974535648f31c2b712a6b2595969f8ab370834080e00ab24e5dbb9d19b8bfb74"}, 538 | {file = "regex-2020.2.20-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5de40649d4f88a15c9489ed37f88f053c15400257eeb18425ac7ed0a4e119400"}, 539 | {file = "regex-2020.2.20-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:82469a0c1330a4beb3d42568f82dffa32226ced006e0b063719468dcd40ffdf0"}, 540 | {file = "regex-2020.2.20-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d58a4fa7910102500722defbde6e2816b0372a4fcc85c7e239323767c74f5cbc"}, 541 | {file = "regex-2020.2.20-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f1ac2dc65105a53c1c2d72b1d3e98c2464a133b4067a51a3d2477b28449709a0"}, 542 | {file = "regex-2020.2.20-cp36-cp36m-win32.whl", hash = "sha256:8c2b7fa4d72781577ac45ab658da44c7518e6d96e2a50d04ecb0fd8f28b21d69"}, 543 | {file = "regex-2020.2.20-cp36-cp36m-win_amd64.whl", hash = "sha256:269f0c5ff23639316b29f31df199f401e4cb87529eafff0c76828071635d417b"}, 544 | {file = "regex-2020.2.20-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:bed7986547ce54d230fd8721aba6fd19459cdc6d315497b98686d0416efaff4e"}, 545 | {file = "regex-2020.2.20-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:046e83a8b160aff37e7034139a336b660b01dbfe58706f9d73f5cdc6b3460242"}, 546 | {file = "regex-2020.2.20-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:b33ebcd0222c1d77e61dbcd04a9fd139359bded86803063d3d2d197b796c63ce"}, 547 | {file = "regex-2020.2.20-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bba52d72e16a554d1894a0cc74041da50eea99a8483e591a9edf1025a66843ab"}, 548 | {file = "regex-2020.2.20-cp37-cp37m-win32.whl", hash = "sha256:01b2d70cbaed11f72e57c1cfbaca71b02e3b98f739ce33f5f26f71859ad90431"}, 549 | {file = "regex-2020.2.20-cp37-cp37m-win_amd64.whl", hash = "sha256:113309e819634f499d0006f6200700c8209a2a8bf6bd1bdc863a4d9d6776a5d1"}, 550 | {file = "regex-2020.2.20-cp38-cp38-manylinux1_i686.whl", hash = "sha256:25f4ce26b68425b80a233ce7b6218743c71cf7297dbe02feab1d711a2bf90045"}, 551 | {file = "regex-2020.2.20-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9b64a4cc825ec4df262050c17e18f60252cdd94742b4ba1286bcfe481f1c0f26"}, 552 | {file = "regex-2020.2.20-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:9ff16d994309b26a1cdf666a6309c1ef51ad4f72f99d3392bcd7b7139577a1f2"}, 553 | {file = "regex-2020.2.20-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:c7f58a0e0e13fb44623b65b01052dae8e820ed9b8b654bb6296bc9c41f571b70"}, 554 | {file = "regex-2020.2.20-cp38-cp38-win32.whl", hash = "sha256:200539b5124bc4721247a823a47d116a7a23e62cc6695744e3eb5454a8888e6d"}, 555 | {file = "regex-2020.2.20-cp38-cp38-win_amd64.whl", hash = "sha256:7f78f963e62a61e294adb6ff5db901b629ef78cb2a1cfce3cf4eeba80c1c67aa"}, 556 | {file = "regex-2020.2.20.tar.gz", hash = "sha256:9e9624440d754733eddbcd4614378c18713d2d9d0dc647cf9c72f64e39671be5"}, 557 | ] 558 | requests = [ 559 | {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"}, 560 | {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"}, 561 | ] 562 | six = [ 563 | {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, 564 | {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, 565 | ] 566 | starlette = [ 567 | {file = "starlette-0.13.2-py3-none-any.whl", hash = "sha256:6169ee78ded501095d1dda7b141a1dc9f9934d37ad23196e180150ace2c6449b"}, 568 | {file = "starlette-0.13.2.tar.gz", hash = "sha256:a9bb130fa7aa736eda8a814b6ceb85ccf7a209ed53843d0d61e246b380afa10f"}, 569 | ] 570 | toml = [ 571 | {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, 572 | {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, 573 | {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, 574 | ] 575 | typed-ast = [ 576 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, 577 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, 578 | {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, 579 | {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, 580 | {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, 581 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, 582 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, 583 | {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, 584 | {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, 585 | {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, 586 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, 587 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, 588 | {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, 589 | {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, 590 | {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, 591 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, 592 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, 593 | {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, 594 | {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, 595 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, 596 | {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, 597 | ] 598 | urllib3 = [ 599 | {file = "urllib3-1.25.8-py2.py3-none-any.whl", hash = "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc"}, 600 | {file = "urllib3-1.25.8.tar.gz", hash = "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"}, 601 | ] 602 | uvicorn = [ 603 | {file = "uvicorn-0.11.3-py3-none-any.whl", hash = "sha256:0f58170165c4495f563d8224b2f415a0829af0412baa034d6f777904613087fd"}, 604 | {file = "uvicorn-0.11.3.tar.gz", hash = "sha256:6fdaf8e53bf1b2ddf0fe9ed06079b5348d7d1d87b3365fe2549e6de0d49e631c"}, 605 | ] 606 | uvloop = [ 607 | {file = "uvloop-0.14.0-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd"}, 608 | {file = "uvloop-0.14.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726"}, 609 | {file = "uvloop-0.14.0-cp36-cp36m-macosx_10_11_x86_64.whl", hash = "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7"}, 610 | {file = "uvloop-0.14.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"}, 611 | {file = "uvloop-0.14.0-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891"}, 612 | {file = "uvloop-0.14.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95"}, 613 | {file = "uvloop-0.14.0-cp38-cp38-macosx_10_11_x86_64.whl", hash = "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5"}, 614 | {file = "uvloop-0.14.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09"}, 615 | {file = "uvloop-0.14.0.tar.gz", hash = "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e"}, 616 | ] 617 | wcwidth = [ 618 | {file = "wcwidth-0.1.8-py2.py3-none-any.whl", hash = "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603"}, 619 | {file = "wcwidth-0.1.8.tar.gz", hash = "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"}, 620 | ] 621 | websockets = [ 622 | {file = "websockets-8.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c"}, 623 | {file = "websockets-8.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170"}, 624 | {file = "websockets-8.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8"}, 625 | {file = "websockets-8.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb"}, 626 | {file = "websockets-8.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5"}, 627 | {file = "websockets-8.1-cp36-cp36m-win32.whl", hash = "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a"}, 628 | {file = "websockets-8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5"}, 629 | {file = "websockets-8.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989"}, 630 | {file = "websockets-8.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d"}, 631 | {file = "websockets-8.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779"}, 632 | {file = "websockets-8.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8"}, 633 | {file = "websockets-8.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422"}, 634 | {file = "websockets-8.1-cp37-cp37m-win32.whl", hash = "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc"}, 635 | {file = "websockets-8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308"}, 636 | {file = "websockets-8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092"}, 637 | {file = "websockets-8.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485"}, 638 | {file = "websockets-8.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1"}, 639 | {file = "websockets-8.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55"}, 640 | {file = "websockets-8.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824"}, 641 | {file = "websockets-8.1-cp38-cp38-win32.whl", hash = "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36"}, 642 | {file = "websockets-8.1-cp38-cp38-win_amd64.whl", hash = "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"}, 643 | {file = "websockets-8.1.tar.gz", hash = "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f"}, 644 | ] 645 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry>=0.12"] 3 | build-backend = "poetry.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "backend" 7 | version = "0.1.0" 8 | description = "Minimal Poetry example with Docker Multi-Stage builds" 9 | authors = ["Michael Oliver "] 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.7" 13 | 14 | fastapi = "0.52.0" 15 | gunicorn = "^20.0.4" 16 | uvicorn = "^0.11.3" 17 | 18 | 19 | [tool.poetry.dev-dependencies] 20 | pytest = "5.3.5" 21 | coverage = { version = "5.0.3", extras = ["toml"]} 22 | black = "^19.10b0" 23 | isort = { version = "^4.3.21", extras = ["pyproject"]} 24 | requests = "^2.23.0" 25 | 26 | 27 | [tool.black] 28 | # https://github.com/psf/black 29 | line-length = 88 30 | target_version = ['py38'] 31 | exclude = ''' 32 | ( 33 | /( 34 | \.git 35 | | \.mypy_cache 36 | | \.pytest_cache 37 | | htmlcov 38 | | venv 39 | | .venv 40 | )/ 41 | ) 42 | ''' 43 | 44 | [tool.isort] 45 | # https://github.com/timothycrosley/isort 46 | # https://github.com/timothycrosley/isort/wiki/isort-Settings 47 | line_length = 88 48 | indent = ' ' 49 | multi_line_output = 3 50 | include_trailing_comma = true 51 | force_grid_wrap = 0 52 | 53 | 54 | [tool.coverage] 55 | # https://github.com/nedbat/coveragepy 56 | [tool.coverage.run] 57 | 58 | source = ["app"] 59 | branch = true 60 | omit = [''] 61 | 62 | [tool.coverage.report] 63 | exclude_lines = [ 64 | "# noqa", 65 | "raise NotImplementedError" 66 | ] 67 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | CURRENT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) 6 | BASE_DIR="$(dirname "$CURRENT_DIR")" 7 | 8 | cd $BASE_DIR 9 | 10 | docker build --tag poetry-project --file docker/Dockerfile . 11 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | CURRENT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) 6 | BASE_DIR="$(dirname "$CURRENT_DIR")" 7 | 8 | black --config "${BASE_DIR}/pyproject.toml" "${BASE_DIR}" 9 | isort --recursive --settings-path "${BASE_DIR}/pyproject.toml" "${BASE_DIR}" 10 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | CURRENT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) 6 | BASE_DIR="$(dirname "$CURRENT_DIR")" 7 | 8 | coverage run --rcfile "${BASE_DIR}/pyproject.toml" -m pytest "${BASE_DIR}/tests" "$*" 9 | coverage html --rcfile "${BASE_DIR}/pyproject.toml" 10 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeloliverx/python-poetry-docker-example/1d90e6b7bd8ff75f9771429b484544986b440440/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from starlette.testclient import TestClient 5 | 6 | from app.main import app 7 | 8 | 9 | @pytest.fixture(scope="function") 10 | def testclient(): 11 | 12 | with TestClient(app) as client: 13 | # Application 'startup' handlers are called on entering the block. 14 | yield client 15 | # Application 'shutdown' handlers are called on exiting the block. 16 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | from starlette.testclient import TestClient 2 | 3 | 4 | def test_root_endpoint(testclient: TestClient): 5 | r = testclient.get("/") 6 | assert r.status_code == 200 7 | 8 | 9 | def test_read_item(testclient: TestClient): 10 | 11 | r = testclient.get("/items/1", params={"q": "query"}) 12 | assert r.status_code == 200, r.text 13 | assert r.json()["item_id"] == 1 14 | 15 | 16 | def test_update_item(testclient: TestClient): 17 | data = {"name": "New Item", "price": "0.38", "is_offer": True} 18 | r = testclient.put("/items/1", json=data) 19 | assert r.status_code == 200, r.text 20 | assert r.json()["item_name"] == data["name"] 21 | --------------------------------------------------------------------------------