-
Notifications
You must be signed in to change notification settings - Fork 124
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
ASGI cookbook recipe #198
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
ASGI (Asynchronous Server Gateway Interface) | ||
++++++++++++++++++++++++++++++++++++++++++++ | ||
|
||
|
||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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 | ||
----------------------------------------- | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
.. 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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,3 +16,4 @@ Deployment | |
gae | ||
heroku | ||
expresscloud | ||
asgi |
There was a problem hiding this comment.
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.