Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add uvicorn and web sockets for native Django 3 #2506

Merged
merged 17 commits into from
Apr 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Features
* Optimized development and production settings
* Registration via django-allauth_
* Comes with custom user model ready to go
* Optional basic ASGI setup for Websockets
* Optional custom static build using Gulp and livereload
* Send emails via Anymail_ (using Mailgun_ by default or Amazon SES if AWS is selected cloud provider, but switchable)
* Media storage using Amazon S3 or Google Cloud Storage
Expand Down
1 change: 1 addition & 0 deletions cookiecutter.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"SparkPost",
"Other SMTP"
],
"use_async": "n",
"use_drf": "n",
"custom_bootstrap_compilation": "n",
"use_compressor": "n",
Expand Down
6 changes: 5 additions & 1 deletion docs/developing-locally.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,14 @@ First things first.

$ python manage.py migrate

#. See the application being served through Django development server: ::
#. If you're running synchronously, see the application being served through Django development server: ::

$ python manage.py runserver 0.0.0.0:8000

or if you're running asynchronously: ::

$ gunicorn config.asgi --bind 0.0.0.0:8000 -k uvicorn.workers.UvicornWorker --reload

.. _PostgreSQL: https://www.postgresql.org/download/
.. _Redis: https://redis.io/download
.. _createdb: https://www.postgresql.org/docs/current/static/app-createdb.html
Expand Down
3 changes: 3 additions & 0 deletions docs/project-generation-options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ mail_service:
8. SparkPost_
9. `Other SMTP`_

use_async:
Indicates whether the project should use web sockets with Uvicorn + Gunicorn.

use_drf:
Indicates whether the project should be configured to use `Django Rest Framework`_.

Expand Down
12 changes: 12 additions & 0 deletions hooks/post_gen_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ def remove_celery_files():
os.remove(file_name)


def remove_async_files():
file_names = [
os.path.join("config", "asgi.py"),
os.path.join("config", "websocket.py"),
]
for file_name in file_names:
os.remove(file_name)


def remove_dottravisyml_file():
os.remove(".travis.yml")

Expand Down Expand Up @@ -372,6 +381,9 @@ def main():
if "{{ cookiecutter.use_drf }}".lower() == "n":
remove_drf_starter_files()

if "{{ cookiecutter.use_async }}".lower() == "n":
remove_async_files()

print(SUCCESS + "Project initialized, keep up the good work!" + TERMINATOR)


Expand Down
2 changes: 2 additions & 0 deletions tests/test_cookiecutter_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ def context():
{"cloud_provider": "GCP", "mail_service": "SparkPost"},
{"cloud_provider": "GCP", "mail_service": "Other SMTP"},
# Note: cloud_providers GCP and None with mail_service Amazon SES is not supported
{"use_async": "y"},
{"use_async": "n"},
{"use_drf": "y"},
{"use_drf": "n"},
{"js_task_runner": "None"},
Expand Down
4 changes: 4 additions & 0 deletions {{cookiecutter.project_slug}}/Procfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
release: python manage.py migrate
{% if cookiecutter.use_async == "y" -%}
web: gunicorn config.asgi:application -k uvicorn.workers.UvicornWorker
{%- else %}
web: gunicorn config.wsgi:application
{%- endif %}
{% if cookiecutter.use_celery == "y" -%}
worker: celery worker --app=config.celery_app --loglevel=info
beat: celery beat --app=config.celery_app --loglevel=info
Expand Down
4 changes: 4 additions & 0 deletions {{cookiecutter.project_slug}}/compose/local/django/start
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ set -o nounset


python manage.py migrate
{%- if cookiecutter.use_async == 'y' %}
/usr/local/bin/gunicorn config.asgi --bind 0.0.0.0:8000 --chdir=/app -k uvicorn.workers.UvicornWorker --reload
{%- else %}
python manage.py runserver_plus 0.0.0.0:8000
{% endif %}
4 changes: 4 additions & 0 deletions {{cookiecutter.project_slug}}/compose/production/django/start
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,8 @@ if compress_enabled; then
python /app/manage.py compress
fi
{%- endif %}
{% if cookiecutter.use_async == 'y' %}
/usr/local/bin/gunicorn config.asgi --bind 0.0.0.0:5000 --chdir=/app -k uvicorn.workers.UvicornWorker
{% else %}
/usr/local/bin/gunicorn config.wsgi --bind 0.0.0.0:5000 --chdir=/app
{%- endif %}
Andrew-Chen-Wang marked this conversation as resolved.
Show resolved Hide resolved
40 changes: 40 additions & 0 deletions {{cookiecutter.project_slug}}/config/asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""
ASGI config for {{ cookiecutter.project_name }} project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/dev/howto/deployment/asgi/

"""
import os
import sys
from pathlib import Path

from django.core.asgi import get_asgi_application

# This allows easy placement of apps within the interior
# {{ cookiecutter.project_slug }} directory.
app_path = Path(__file__).parents[1].resolve()
sys.path.append(str(app_path / "{{ cookiecutter.project_slug }}"))

# If DJANGO_SETTINGS_MODULE is unset, default to the local settings
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")

# This application object is used by any ASGI server configured to use this file.
django_application = get_asgi_application()
# Apply ASGI middleware here.
# from helloworld.asgi import HelloWorldApplication
# application = HelloWorldApplication(application)
Andrew-Chen-Wang marked this conversation as resolved.
Show resolved Hide resolved

# Import websocket application here, so apps from django_application are loaded first
from config.websocket import websocket_application # noqa isort:skip


async def application(scope, receive, send):
if scope["type"] == "http":
await django_application(scope, receive, send)
elif scope["type"] == "websocket":
await websocket_application(scope, receive, send)
else:
raise NotImplementedError(f"Unknown scope type {scope['type']}")
10 changes: 9 additions & 1 deletion {{cookiecutter.project_slug}}/config/urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
{%- if cookiecutter.use_async == 'y' %}
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
{%- endif %}
from django.urls import include, path
from django.views import defaults as default_views
from django.views.generic import TemplateView
Expand All @@ -20,7 +23,12 @@
path("accounts/", include("allauth.urls")),
# Your stuff: custom urls includes go here
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
{% if cookiecutter.use_drf == 'y' -%}
{%- if cookiecutter.use_async == 'y' %}
if settings.DEBUG:
# Static file serving when using Gunicorn + Uvicorn for local web socket development
urlpatterns += staticfiles_urlpatterns()
{%- endif %}
{% if cookiecutter.use_drf == 'y' %}
# API URLS
urlpatterns += [
# API base url
Expand Down
13 changes: 13 additions & 0 deletions {{cookiecutter.project_slug}}/config/websocket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
async def websocket_application(scope, receive, send):
browniebroke marked this conversation as resolved.
Show resolved Hide resolved
while True:
event = await receive()

if event["type"] == "websocket.connect":
await send({"type": "websocket.accept"})

if event["type"] == "websocket.disconnect":
break

if event["type"] == "websocket.receive":
if event["text"] == "ping":
await send({"type": "websocket.send", "text": "pong!"})
17 changes: 17 additions & 0 deletions {{cookiecutter.project_slug}}/gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,18 @@ function imgCompression() {
.pipe(dest(paths.images))
}

{% if cookiecutter.use_async == 'y' -%}
// Run django server
function asyncRunServer() {
var cmd = spawn('gunicorn', [
'config.asgi', '-k', 'uvicorn.workers.UvicornWorker', '--reload'
], {stdio: 'inherit'}
)
cmd.on('close', function(code) {
console.log('gunicorn exited with code ' + code)
})
}
{%- else %}
// Run django server
function runServer(cb) {
var cmd = spawn('python', ['manage.py', 'runserver'], {stdio: 'inherit'})
Expand All @@ -118,6 +130,7 @@ function runServer(cb) {
cb(code)
})
}
{%- endif %}

// Browser sync server for live reload
function initBrowserSync() {
Expand Down Expand Up @@ -166,8 +179,12 @@ const generateAssets = parallel(
// Set up dev environment
const dev = parallel(
{%- if cookiecutter.use_docker == 'n' %}
{%- if cookiecutter.use_async == 'y' %}
asyncRunServer,
{%- else %}
runServer,
{%- endif %}
{%- endif %}
initBrowserSync,
watchPaths
)
Expand Down
4 changes: 4 additions & 0 deletions {{cookiecutter.project_slug}}/requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ django-celery-beat==2.0.0 # https://github.com/celery/django-celery-beat
flower==0.9.4 # https://github.com/mher/flower
{%- endif %}
{%- endif %}
{%- if cookiecutter.use_async == 'y' %}
uvicorn==0.11.3 # https://github.com/encode/uvicorn
gunicorn==20.0.4 # https://github.com/benoitc/gunicorn
{%- endif %}

# Django
# ------------------------------------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions {{cookiecutter.project_slug}}/requirements/production.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

-r ./base.txt

{%- if cookiecutter.use_async == 'n' %}
gunicorn==20.0.4 # https://github.com/benoitc/gunicorn
{%- endif %}
psycopg2==2.8.5 --no-binary psycopg2 # https://github.com/psycopg/psycopg2
{%- if cookiecutter.use_whitenoise == 'n' %}
Collectfast==2.1.0 # https://github.com/antonagestam/collectfast
Expand Down