diff --git a/cookiecutter.json b/cookiecutter.json index cfa1b346..62155ad0 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -44,6 +44,7 @@ "SparkPost", "Other SMTP" ], + "use_async": "n", "use_drf": "n", "custom_bootstrap_compilation": "n", "use_compressor": "n", diff --git a/docs/project-generation-options.rst b/docs/project-generation-options.rst index 8abeda9b..1c2fc314 100644 --- a/docs/project-generation-options.rst +++ b/docs/project-generation-options.rst @@ -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`_. diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 5cc8c32f..2e2c19e1 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -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") @@ -367,6 +376,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) diff --git a/tests/test_cookiecutter_generation.py b/tests/test_cookiecutter_generation.py index 2c7e6dc4..eee56115 100755 --- a/tests/test_cookiecutter_generation.py +++ b/tests/test_cookiecutter_generation.py @@ -73,6 +73,8 @@ SUPPORTED_COMBINATIONS = [ {"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"}, diff --git a/{{cookiecutter.project_slug}}/compose/local/django/start b/{{cookiecutter.project_slug}}/compose/local/django/start index f076ee51..bc1293de 100644 --- a/{{cookiecutter.project_slug}}/compose/local/django/start +++ b/{{cookiecutter.project_slug}}/compose/local/django/start @@ -6,4 +6,8 @@ set -o nounset python manage.py migrate +{%- if cookiecutter.use_async %} +/usr/local/bin/uvicorn config.asgi --bind 0.0.0.0:5000 --chdir=/app +{%- else %} python manage.py runserver_plus 0.0.0.0:8000 +{% endif %} diff --git a/{{cookiecutter.project_slug}}/compose/production/django/start b/{{cookiecutter.project_slug}}/compose/production/django/start index 4985aeed..dd06949a 100644 --- a/{{cookiecutter.project_slug}}/compose/production/django/start +++ b/{{cookiecutter.project_slug}}/compose/production/django/start @@ -27,4 +27,8 @@ if compress_enabled; then python /app/manage.py compress fi {%- endif %} +{% if cookiecutter.use_async %} +/usr/local/bin/uvicorn config.asgi --bind 0.0.0.0:5000 --chdir=/app +{% else %} /usr/local/bin/gunicorn config.wsgi --bind 0.0.0.0:5000 --chdir=/app +{%- endif %} diff --git a/{{cookiecutter.project_slug}}/config/asgi.py b/{{cookiecutter.project_slug}}/config/asgi.py new file mode 100644 index 00000000..81f1c76a --- /dev/null +++ b/{{cookiecutter.project_slug}}/config/asgi.py @@ -0,0 +1,41 @@ +""" +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 +from .websocket import websocket_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 }}")) +# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks +# if running multiple sites in the same mod_wsgi process. To fix this, use +# mod_wsgi daemon mode with each site in its own daemon process, or use +# os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.production" +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production") + +# This application object is used by any ASGI server configured to use this +# file. This includes Django's development server, if the ASGI_APPLICATION +# setting points here. +django_application = get_asgi_application() +# Apply ASGI middleware here. +# from helloworld.asgi import HelloWorldApplication +# application = HelloWorldApplication(application) + + +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']}") diff --git a/{{cookiecutter.project_slug}}/config/websocket.py b/{{cookiecutter.project_slug}}/config/websocket.py new file mode 100644 index 00000000..9c84a528 --- /dev/null +++ b/{{cookiecutter.project_slug}}/config/websocket.py @@ -0,0 +1,18 @@ +async def websocket_application(scope, receive, send): + 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!' + }) diff --git a/{{cookiecutter.project_slug}}/requirements/base.txt b/{{cookiecutter.project_slug}}/requirements/base.txt index b41be3b0..ec861f35 100644 --- a/{{cookiecutter.project_slug}}/requirements/base.txt +++ b/{{cookiecutter.project_slug}}/requirements/base.txt @@ -16,6 +16,9 @@ django-celery-beat==2.0.0 # https://github.com/celery/django-celery-beat flower==0.9.3 # https://github.com/mher/flower {%- endif %} {%- endif %} +{%- if cookiecutter.use_async %} +uvicorn==0.11.3 # https://github.com/encode/uvicorn +{%- endif %} # Django # ------------------------------------------------------------------------------