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

ASGI cookbook recipe #198

Merged
merged 3 commits into from
May 20, 2018
Merged
Show file tree
Hide file tree
Changes from 2 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 CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
191 changes: 191 additions & 0 deletions docs/deployment/asgi.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
ASGI (Asynchronous Server Gateway Interface)
++++++++++++++++++++++++++++++++++++++++++++


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove 1 extra blank line.

Note, it's OK to have two blank lines preceding a heading because it helps visually separate the narrative from the subsequent heading. Next it's good to have the heading visually "connect" with its narrative content.

This chapter contains information about using ASGI with
Pyramid. You can read more about the specification here: https://github.com/django/asgiref/blob/master/specs/asgi.rst.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you consider publishing to Read the Docs? GitHub's rendering of reStructuredText is suboptimal. I can assist with PRs to your repo, if you like, but you'd need to configure GitHub and RTD under your own account.


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, `uvicorn` or `daphne`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reStructuredText uses double-ticks for inline literals, single-ticks for italics/emphasis. If literals are your intent, then change them throughout, for example:

``asgiref``

However, when first mentioned in a narrative, it is a good idea to include links to references. Suggest for the first instance of each turning these into links, with subsequent mentions as inline literals.

`asgiref <https://pypi.org/project/asgiref/>`_

This is two sentences. "responses. This"

Finally:

 server, including `uvicorn <http://www.uvicorn.org/>`_ or `daphne <https://pypi.org/project/daphne/>`_.

Note, choosing appropriate URLs for a project depends on context. Here the context is docs. Unfortunately there are no ReadTheDocs URLs for these projects, so I picked the one that appears to best represent the project, with PyPI as the default. If you have better URLs, please use them instead.


The example contains a class that extends the wrapper to enable routing ASGI consumers.


Simple WSGI -> ASGI WebSocket application
-----------------------------------------


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove 1 extra blank line.

.. 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 = """<!DOCTYPE html>
<html>
<head>
<title>ASGI WebSocket</title>
</head>
<body>
<h1>ASGI WebSocket Demo</h1>
<form action="" onsubmit="sendMessage(event)">
<input type="text" id="messageText" autocomplete="off"/>
<button>Send</button>
</form>
<ul id='messages'>
</ul>
<script>
var ws = new WebSocket("ws://127.0.0.1:8000/ws");
ws.onmessage = function(event) {
var messages = document.getElementById('messages')
var message = document.createElement('li')
var content = document.createTextNode(event.data)
message.appendChild(content)
messages.appendChild(message)
};
function sendMessage(event) {
var input = document.getElementById("messageText")
ws.send(input.value)
input.value = ''
event.preventDefault()
}
</script>
</body>
</html>
"""

# 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 is `nginx` and `supervisor`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`nginx <https://nginx.org/>`_ and `supervisor <http://supervisord.org/>`_.


.. code-block:: bash

# nginx.conf

upstream app {
server unix:/tmp/uvicorn.sock;
}

server {

listen 80;
server_name <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 </path-to-static>;
}
}


.. code-block:: bash

# supervisor-app.conf

[program:asgiapp]
directory=/path/to/app/
command=</path-to-virtualenv>bin/uvicorn app:app --bind unix:/tmp/uvicorn.sock --workers 2 --access-logfile /tmp/uvicorn-access.log --error-logfile /tmp/uvicorn-error.log
user=<app-user>
autostart=true
autorestart=true
redirect_stderr=True
1 change: 1 addition & 0 deletions docs/deployment/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ Deployment
gae
heroku
expresscloud
asgi