├── .dockerignore ├── .gitignore ├── Readme.md ├── core ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── tasks.py ├── tests.py ├── urls.py └── views.py ├── docker ├── .sample-env ├── Dockerfile ├── docker-compose.yml ├── entrypoint.sh └── start.sh ├── dockerapp ├── __init__.py ├── asgi.py ├── celery_app.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py └── requirements.txt /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .vscode/ 3 | env/ 4 | htmlcov/ 5 | __pycache__/ 6 | 7 | .coverage 8 | .docker/Dockerfile 9 | requirements/test.txt 10 | .*ignore 11 | .env* 12 | db.sqlite3 13 | *.yml 14 | *.yaml 15 | *.md 16 | *.cfg 17 | celerybeat-schedule 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | */migrations/* 6 | !*/migrations/__init__.py 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | .static_storage/ 57 | .media/ 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # django staticfiles 105 | /static 106 | 107 | # mypy 108 | .mypy_cache/ 109 | 110 | .vscode/ 111 | node_modules/ 112 | celerybeat.pid 113 | 114 | .idea/ 115 | 116 | # DS Store files on mac 117 | .DS_Store 118 | 119 | # google credentials 120 | google_service_account.json 121 | credentials.json -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # DockerApp 2 | #### A test Django application with production-level docker setup. 3 | 4 | **Blog: https://bettersoftwarewithsid.com/the-near-perfect-dockerfile-for-django-applications** 5 | 6 | This project is to document production-level Docker setup. Please note that Django application is rudimentary and not fit for production. 7 | 8 | This project present docker setup of four component of a typical Django application i.e. web server, celery worker, celery beat and celery monitoring using flower. 9 | 10 | ## Docker Setup 11 | Detailed documentation of Docker setup can be found in above blog. 12 | It discusses in detail `Dockerfile`, `entrypoint.sh` and `start.sh` 13 | 14 | #### Useful Commands 15 | To be run from root of the project. 16 | 17 | Note: Before running commands, copy `.sample-env` into `.env` in the same level `dockerapp/docker/`. 18 | 1. Build docker image with tag 'development': 19 | `docker build -t dockerapp:development -f docker/Dockerfile .` 20 | 21 | 2. Start all containers including essential services(PostgreSQL and Redis): `docker-compose -f docker/docker-compose.yml up` 22 | 23 | 3. Stop all containers: `docker-compose -f docker/docker-compose.yml down` 24 | 25 | ## Django application details 26 | The Django application has a simple view `dockerapp/core/views.py` linked to url `/task-trigger/`, which picks a random integer between 1 and 100 and send to celery task(defined in `dockerapp/core/tasks.py`) which count next five integer at interval of one second. 27 | 28 | 29 | ## Development 30 | This is a work in progress, hence further enhancements in all aspects of Docker in this project is expected. Any advice regarding improvement of Docker aspect is welcome and encouraged. 31 | 32 | For detailed discussion please refer to corresponding blog https://bettersoftwarewithsid.com/the-near-perfect-dockerfile-for-django-applications. 33 | 34 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siddharthsahu/django-docker/aa1ff3ae348b41a5e8b008be5ef290e88cc54d95/core/__init__.py -------------------------------------------------------------------------------- /core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | name = 'core' 6 | -------------------------------------------------------------------------------- /core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siddharthsahu/django-docker/aa1ff3ae348b41a5e8b008be5ef290e88cc54d95/core/migrations/__init__.py -------------------------------------------------------------------------------- /core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /core/tasks.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from dockerapp.celery_app import app 4 | 5 | 6 | @app.task 7 | def sample_task(num1): 8 | for i in range(5): 9 | num1 += 1 10 | time.sleep(1) 11 | print(f"Num: {num1}") 12 | 13 | return num1 14 | -------------------------------------------------------------------------------- /core/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /core/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import sample_view 4 | 5 | 6 | urlpatterns = [ 7 | path('task-trigger/', sample_view) 8 | ] -------------------------------------------------------------------------------- /core/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse, HttpResponseServerError 2 | import datetime 3 | import random 4 | 5 | from .tasks import sample_task 6 | 7 | 8 | def sample_view(request): 9 | now = datetime.datetime.now() 10 | 11 | try: 12 | task = sample_task.delay(random.randint(1, 100)) 13 | html = f"Celery task with id {task.id} is triggered at {now}." 14 | return HttpResponse(html) 15 | 16 | except Exception as e: 17 | return HttpResponseServerError("Error encountered in triggering task") 18 | 19 | -------------------------------------------------------------------------------- /docker/.sample-env: -------------------------------------------------------------------------------- 1 | DJANGO_POSTGRES_HOST=postgres 2 | DJANGO_POSTGRES_PORT=5432 3 | DJANGO_POSTGRES_USER=postgres 4 | DJANGO_POSTGRES_PASSWORD=postgres 5 | DJANGO_POSTGRES_DATABASE=postgres 6 | CELERY_FLOWER_USER=flower 7 | CELERY_FLOWER_PASSWORD=flower 8 | CELERY_BROKER_URL=redis://redis:6379 9 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | ENV PYTHONDONTWRITEBYTECODE 1 5 | 6 | RUN apt-get update \ 7 | && apt-get install -y --no-install-recommends build-essential libpq-dev \ 8 | && rm -rf /var/lib/apt/lists/* 9 | 10 | COPY requirements.txt /tmp/requirements.txt 11 | RUN pip install --no-cache-dir -r /tmp/requirements.txt \ 12 | && rm -rf /tmp/requirements.txt \ 13 | && useradd -U app_user \ 14 | && install -d -m 0755 -o app_user -g app_user /app/static 15 | 16 | WORKDIR /app 17 | USER app_user:app_user 18 | COPY --chown=app_user:app_user . . 19 | RUN chmod +x docker/*.sh 20 | 21 | ENTRYPOINT [ "docker/entrypoint.sh" ] 22 | CMD [ "docker/start.sh", "server" ] 23 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | volumes: 4 | app_data: 5 | name: app_data 6 | app_broker: 7 | name: app_broker 8 | 9 | services: 10 | django: &django 11 | image: dockerapp:development 12 | command: /app/docker/start.sh server 13 | depends_on: 14 | - postgres 15 | env_file: 16 | - .env 17 | ports: 18 | - 8000:8000 19 | 20 | postgres: 21 | image: postgres:13.3-alpine 22 | volumes: 23 | - app_data:/var/lib/postgresql/data 24 | environment: 25 | POSTGRES_PASSWORD: postgres 26 | POSTGRES_USER: postgres 27 | POSTGRES_DB: postgres 28 | 29 | redis: 30 | image: redis:6.2.5-alpine 31 | command: redis-server --appendonly yes 32 | volumes: 33 | - app_broker:/var/lib/redis/data 34 | 35 | celery: 36 | <<: *django 37 | depends_on: 38 | - django 39 | - postgres 40 | - redis 41 | ports: 42 | - 8001:8000 43 | command: /app/docker/start.sh worker 44 | 45 | beat: 46 | <<: *django 47 | depends_on: 48 | - django 49 | - postgres 50 | - redis 51 | ports: 52 | - 8002:8000 53 | command: /app/docker/start.sh beat 54 | 55 | -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | postgres_ready() { 8 | python << END 9 | import sys 10 | 11 | from psycopg2 import connect 12 | from psycopg2.errors import OperationalError 13 | 14 | try: 15 | connect( 16 | dbname="${DJANGO_POSTGRES_DATABASE}", 17 | user="${DJANGO_POSTGRES_USER}", 18 | password="${DJANGO_POSTGRES_PASSWORD}", 19 | host="${DJANGO_POSTGRES_HOST}", 20 | port="${DJANGO_POSTGRES_PORT}", 21 | ) 22 | except OperationalError: 23 | sys.exit(-1) 24 | END 25 | } 26 | 27 | redis_ready() { 28 | python << END 29 | import sys 30 | 31 | from redis import Redis 32 | from redis import RedisError 33 | 34 | 35 | try: 36 | redis = Redis.from_url("${CELERY_BROKER_URL}", db=0) 37 | redis.ping() 38 | except RedisError: 39 | sys.exit(-1) 40 | END 41 | } 42 | 43 | until postgres_ready; do 44 | >&2 echo "Waiting for PostgreSQL to become available..." 45 | sleep 5 46 | done 47 | >&2 echo "PostgreSQL is available" 48 | 49 | until redis_ready; do 50 | >&2 echo "Waiting for Redis to become available..." 51 | sleep 5 52 | done 53 | >&2 echo "Redis is available" 54 | 55 | python3 manage.py collectstatic --noinput 56 | python3 manage.py makemigrations 57 | python3 manage.py migrate 58 | 59 | exec "$@" 60 | -------------------------------------------------------------------------------- /docker/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd /app 4 | 5 | if [ $# -eq 0 ]; then 6 | echo "Usage: start.sh [PROCESS_TYPE](server/beat/worker/flower)" 7 | exit 1 8 | fi 9 | 10 | PROCESS_TYPE=$1 11 | 12 | if [ "$PROCESS_TYPE" = "server" ]; then 13 | if [ "$DJANGO_DEBUG" = "true" ]; then 14 | gunicorn \ 15 | --reload \ 16 | --bind 0.0.0.0:8000 \ 17 | --workers 2 \ 18 | --worker-class eventlet \ 19 | --log-level DEBUG \ 20 | --access-logfile "-" \ 21 | --error-logfile "-" \ 22 | dockerapp.wsgi 23 | else 24 | gunicorn \ 25 | --bind 0.0.0.0:8000 \ 26 | --workers 2 \ 27 | --worker-class eventlet \ 28 | --log-level DEBUG \ 29 | --access-logfile "-" \ 30 | --error-logfile "-" \ 31 | dockerapp.wsgi 32 | fi 33 | elif [ "$PROCESS_TYPE" = "beat" ]; then 34 | celery \ 35 | --app dockerapp.celery_app \ 36 | beat \ 37 | --loglevel INFO \ 38 | --scheduler django_celery_beat.schedulers:DatabaseScheduler 39 | elif [ "$PROCESS_TYPE" = "flower" ]; then 40 | celery \ 41 | --app dockerapp.celery_app \ 42 | flower \ 43 | --basic_auth="${CELERY_FLOWER_USER}:${CELERY_FLOWER_PASSWORD}" \ 44 | --loglevel INFO 45 | elif [ "$PROCESS_TYPE" = "worker" ]; then 46 | celery \ 47 | --app dockerapp.celery_app \ 48 | worker \ 49 | --loglevel INFO 50 | fi 51 | -------------------------------------------------------------------------------- /dockerapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siddharthsahu/django-docker/aa1ff3ae348b41a5e8b008be5ef290e88cc54d95/dockerapp/__init__.py -------------------------------------------------------------------------------- /dockerapp/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for dockerapp project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dockerapp.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /dockerapp/celery_app.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | 3 | from celery import Celery 4 | from django.conf import settings 5 | 6 | environ.setdefault("DJANGO_SETTINGS_MODULE", "dockerapp.settings") 7 | 8 | 9 | app = Celery("docker_celery", broker=environ.get("CELERY_BROKER_URL")) 10 | 11 | app.config_from_object("django.conf:settings", namespace="CELERY") 12 | # Load task modules from all registered Django app configs. 13 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) 14 | -------------------------------------------------------------------------------- /dockerapp/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for dockerapp project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.1/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | import os 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve(strict=True).parent.parent 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '4p#&v5-odmv47r()n$wp%2$)znhj6^65t1t@qc*2bg4jz%=6r*' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'django_celery_beat', 41 | 'core' 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | 'django.middleware.security.SecurityMiddleware', 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.common.CommonMiddleware', 48 | 'django.middleware.csrf.CsrfViewMiddleware', 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ] 53 | 54 | ROOT_URLCONF = 'dockerapp.urls' 55 | 56 | TEMPLATES = [ 57 | { 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 59 | 'DIRS': [], 60 | 'APP_DIRS': True, 61 | 'OPTIONS': { 62 | 'context_processors': [ 63 | 'django.template.context_processors.debug', 64 | 'django.template.context_processors.request', 65 | 'django.contrib.auth.context_processors.auth', 66 | 'django.contrib.messages.context_processors.messages', 67 | ], 68 | }, 69 | }, 70 | ] 71 | 72 | WSGI_APPLICATION = 'dockerapp.wsgi.application' 73 | 74 | 75 | # Database 76 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 77 | 78 | DATABASES = { 79 | "default": { 80 | "ENGINE": "django.db.backends.postgresql_psycopg2", 81 | "HOST": os.environ.get("DJANGO_POSTGRES_HOST"), 82 | "PORT": os.environ.get("DJANGO_POSTGRES_PORT"), 83 | "USER": os.environ.get("DJANGO_POSTGRES_USER"), 84 | "PASSWORD": os.environ.get("DJANGO_POSTGRES_PASSWORD"), 85 | "NAME": os.environ.get("DJANGO_POSTGRES_DATABASE"), 86 | } 87 | } 88 | 89 | 90 | # Celery 91 | 92 | CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL") 93 | CELERY_RESULT_BACKEND = CELERY_BROKER_URL 94 | CELERY_ACCEPT_CONTENT = ["json"] 95 | CELERY_TASK_SERIALIZER = "json" 96 | CELERY_RESULT_SERIALIZER = "json" 97 | CELERY_TASK_TIME_LIMIT = 5 * 60 98 | CELERY_TASK_SOFT_TIME_LIMIT = 5 * 60 99 | CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" 100 | 101 | 102 | # Password validation 103 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 104 | 105 | AUTH_PASSWORD_VALIDATORS = [ 106 | { 107 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 108 | }, 109 | { 110 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 111 | }, 112 | { 113 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 114 | }, 115 | { 116 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 117 | }, 118 | ] 119 | 120 | 121 | # Internationalization 122 | # https://docs.djangoproject.com/en/3.1/topics/i18n/ 123 | 124 | LANGUAGE_CODE = 'en-us' 125 | 126 | TIME_ZONE = 'UTC' 127 | 128 | USE_I18N = True 129 | 130 | USE_L10N = True 131 | 132 | USE_TZ = True 133 | 134 | 135 | # Static files (CSS, JavaScript, Images) 136 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 137 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 138 | STATIC_URL = '/static/' 139 | -------------------------------------------------------------------------------- /dockerapp/urls.py: -------------------------------------------------------------------------------- 1 | """dockerapp URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | path('', include(("core.urls", "core"))), 22 | ] 23 | -------------------------------------------------------------------------------- /dockerapp/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for dockerapp project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dockerapp.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dockerapp.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | amqp==5.0.6 2 | asgiref==3.2.10 3 | billiard==3.6.4.0 4 | celery==5.1.2 5 | click==7.1.2 6 | click-didyoumean==0.0.3 7 | click-plugins==1.1.1 8 | click-repl==0.2.0 9 | Django==3.1 10 | django-celery-beat==2.2.1 11 | eventlet==0.30.2 12 | gunicorn==20.0.4 13 | kombu==5.1.0 14 | prompt-toolkit==3.0.20 15 | psycopg2==2.9.1 16 | pytz==2021.1 17 | redis==3.5.3 18 | six==1.16.0 19 | sqlparse==0.4.2 20 | vine==5.0.0 21 | wcwidth==0.2.5 22 | --------------------------------------------------------------------------------