From 4437b6c71c5847f35ce1aeb756caee3520b88a9c Mon Sep 17 00:00:00 2001 From: jordan Date: Sun, 20 May 2018 15:42:53 +1000 Subject: [PATCH] ASGI cookbook recipe (#198) * ASGI doc * Removing unused import from code example * Updating formatting, links, splitting example --- CONTRIBUTORS.txt | 1 + docs/deployment/asgi.rst | 219 ++++++++++++++++++++++++++++++++++++++ docs/deployment/index.rst | 1 + 3 files changed, 221 insertions(+) create mode 100644 docs/deployment/asgi.rst diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 70ebdd7..8830b7c 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -119,3 +119,4 @@ Dan Clark, 03/11/2017 Benjamin Petersen, 03/20/2017 Volker Diels-Grabsch, 06/08/2017 Jeremy Davis, 07/09/2017 +Jordan Eremieff, 05/17/2018 \ No newline at end of file diff --git a/docs/deployment/asgi.rst b/docs/deployment/asgi.rst new file mode 100644 index 0000000..521f72d --- /dev/null +++ b/docs/deployment/asgi.rst @@ -0,0 +1,219 @@ +ASGI (Asynchronous Server Gateway Interface) +++++++++++++++++++++++++++++++++++++++++++++ + +This chapter contains information about using ASGI with +Pyramid. You can read more about the specification here: https://asgi.readthedocs.io/en/latest/index.html. + +The example app below uses the WSGI to ASGI wrapper from the `asgiref `_ library to transform normal WSGI requests into ASGI responses - this allows the application to be run with an ASGI server, such as `uvicorn `_ or `daphne `_. + + +WSGI -> ASGI application +------------------------ + +This example uses the wrapper provided by ``asgiref`` to convert a WSGI application to ASGI, this allows it to be run by an ASGI server. + +Please note that not all extended features of WSGI may be supported (such as file handles for incoming POST bodies). + +.. code-block:: python + + # app.py + + from asgiref.wsgi import WsgiToAsgi + from pyramid.config import Configurator + from pyramid.response import Response + + def hello_world(request): + return Response("Hello") + + # Configure a normal WSGI app then wrap it with WSGI -> ASGI class + + with Configurator() as config: + config.add_route("hello", "/") + config.add_view(hello_world, route_name="hello") + wsgi_app = config.make_wsgi_app() + + + app = WsgiToAsgi(wsgi_app) + + +Extended WSGI -> ASGI WebSocket application +------------------------------------------- + +The example extends the ``asgiref`` wrapper to enable routing ASGI consumers alongside the converted WSGI application. This is just one potential solution for routing ASGI consumers. + +.. code-block:: python + + # app.py + + from asgiref.wsgi import WsgiToAsgi + + from pyramid.config import Configurator + from pyramid.response import Response + + + class ExtendedWsgiToAsgi(WsgiToAsgi): + + """Extends the WsgiToAsgi wrapper to include an ASGI consumer protocol router""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.protocol_router = {"http": {}, "websocket": {}} + + def __call__(self, scope, **kwargs): + protocol = scope["type"] + path = scope["path"] + try: + consumer = self.protocol_router[protocol][path] + except KeyError: + consumer = None + if consumer is not None: + return consumer(scope) + return super().__call__(scope, **kwargs) + + def route(self, rule, *args, **kwargs): + try: + protocol = kwargs["protocol"] + except KeyError: + raise Exception("You must define a protocol type for an ASGI handler") + + def _route(func): + self.protocol_router[protocol][rule] = func + + return _route + + + HTML_BODY = """ + + + ASGI WebSocket + + +

ASGI WebSocket Demo

+
+ + +
+
    +
+ + + + """ + + # Define normal WSGI views + + + def hello_world(request): + return Response(HTML_BODY) + + + # Configure a normal WSGI app then wrap it with WSGI -> ASGI class + + + with Configurator() as config: + config.add_route("hello", "/") + config.add_view(hello_world, route_name="hello") + wsgi_app = config.make_wsgi_app() + + + app = ExtendedWsgiToAsgi(wsgi_app) + + + # Define ASGI consumers + + + @app.route("/ws", protocol="websocket") + def hello_websocket(scope): + + async def asgi_instance(receive, send): + while True: + message = await receive() + if message["type"] == "websocket.connect": + await send({"type": "websocket.accept"}) + if message["type"] == "websocket.receive": + text = message.get("text") + if text: + await send({"type": "websocket.send", "text": text}) + else: + await send({"type": "websocket.send", "bytes": message.get("bytes")}) + + return asgi_instance + + +Running & Deploying +------------------- + +The application can be run using an ASGI server: + +.. code-block:: bash + + $ uvicorn app:app + +or + +.. code-block:: bash + + $ daphne app:app + + +There are several potential deployment options, one example would be to use `nginx `_ and `supervisor `_. Below are example configuration files that run the application using ``uvicorn``, however ``daphne`` may be used as well. + +Example Nginx configuration +=========================== + +.. code-block:: bash + + upstream app { + server unix:/tmp/uvicorn.sock; + } + + server { + + listen 80; + server_name ; + + location / { + proxy_pass http://app; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_redirect off; + } + + location /static { + root ; + } + } + +Example Supervisor configuration +================================ + +.. code-block:: bash + + [program:asgiapp] + directory=/path/to/app/ + command=bin/uvicorn app:app --bind unix:/tmp/uvicorn.sock --workers 2 --access-logfile /tmp/uvicorn-access.log --error-logfile /tmp/uvicorn-error.log + user= + autostart=true + autorestart=true + redirect_stderr=True diff --git a/docs/deployment/index.rst b/docs/deployment/index.rst index 8293725..eb3208d 100644 --- a/docs/deployment/index.rst +++ b/docs/deployment/index.rst @@ -16,3 +16,4 @@ Deployment gae heroku expresscloud + asgi