A lightning-fast asyncio server for Python 3.
Uvicorn is intended to be the basis for providing Python 3 with a simple interface on which to build asyncio web frameworks. It provides the following:
- A lightning-fast asyncio server implementation, using uvloop and httptools.
- A minimal application interface, based on ASGI.
Requirements: Python 3.5.3+
Install using pip
:
$ pip install uvicorn
Create an application, in app.py
:
async def hello_world(message, channels):
content = b'Hello, world'
response = {
'status': 200,
'headers': [
[b'content-type', b'text/plain'],
],
'content': content
}
await channels['reply'].send(response)
Run the server:
$ uvicorn app:hello_world
Uvicorn introduces a messaging interface broadly based on ASGI...
The application should expose a coroutine callable which takes two arguments:
message
is an ASGI message. (But see below for ammendments.)channels
is a dictionary of<unicode string>:<channel interface>
.
The channel interface is an object with the following attributes:
.send(message)
- A coroutine for sending outbound messages. Optional..receive()
- A coroutine for receiving incoming messages. Optional..name
- A unicode string, uniquely identifying the channel. Optional.
Messages diverge from ASGI in the following ways:
- Messages additionally include a
channel
key, to allow for routing eg.'channel': 'http.request'
- Messages do not include channel names, such as
reply_channel
orbody_channel
, instead thechannels
dictionary presents the available channels.
You can stream the request body without blocking the asyncio task pool,
by receiving request body chunks from the body
channel.
async def read_body(message, channels):
"""
Read and return the entire body from an incoming ASGI message.
"""
body = message.get('body', b'')
if 'body' in channels:
while True:
message_chunk = await channels['body'].receive()
body += message_chunk['content']
if not message_chunk.get('more_content', False):
break
return body
async def echo_body(message, channels):
body = await read_body(message, channels)
response = {
'status': 200,
'headers': [
[b'content-type', b'text/plain'],
],
'content': body
}
await channels['reply'].send(response)
You can stream responses by sending response chunks to the
reply
channel:
async def stream_response(message, channels):
# Send the start of the response.
await channels['reply'].send({
'status': 200,
'headers': [
[b'content-type', b'text/plain'],
],
'content': b'',
'more_content': True
})
# Stream response content.
for chunk in [b'Hello', b', ', b'world']:
await channels['reply'].send({
'content': chunk,
'more_content': True
})
# End the response.
await channels['reply'].send({
'content': b'',
'more_content': False
})
Uvicorn supports websockets, using the same messaging interface described above, with ASGI WebSocket messages.
We'll start with an example that simply echos any incoming websocket messages back to the client.
async def echo(message, channels):
if message['channel'] == 'websocket.receive':
text = message['text']
await channels['reply'].send({
'text': text
})
Another example, this time demonstrating a websocket connection that sends back the current time to each connected client, roughly once per second.
import datetime
import asyncio
async def send_times(channel):
while True:
text = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
await channel.send({'text': text})
await asyncio.sleep(1)
async def tick(message, channels):
if message['channel'] == 'websocket.connect':
loop = asyncio.get_event_loop()
loop.create_task(send_times(channels['reply']))
Here's a more complete example that demonstrates a basic WebSocket chat server:
index.html:
<!DOCTYPE html>
<html>
<head>
<title>WebSocket demo</title>
</head>
<body>
<h1>WebSocket Chat</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.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>
app.py:
clients = set()
with open('index.html', 'rb') as file:
homepage = file.read()
async def chat_server(message, channels):
"""
ASGI-style 'Hello, world' application.
"""
if message['channel'] == 'websocket.connect':
clients.add(channels['reply'])
elif message['channel'] == 'websocket.receive':
for client in clients:
await client.send({'text': message['text']})
elif message['channel'] == 'websocket.disconnect':
clients.remove(channels['reply'])
elif message['channel'] == 'http.request':
await channels['reply'].send({
'status': 200,
'headers': [
[b'content-type', b'text/html'],
],
'content': homepage
})
Note that the example above will only work properly when running as a single process on a single machine, since the set of connected clients is stored in memory.
In order to build properly scalable WebSocket services you'll typically want some way of sending messages across a group of client connections, each of which may be connected to a different server instance...
Uvicorn includes broadcast functionality, using Redis Pub/Sub.
First, make sure to install the asyncio_redis
package:
$ pip install asyncio_redis
Broadcast functionality is not integrated directly into the server, but is included as application-level middleware. You can install the broadcast module by wrapping it around your existing application, like so:
from uvicorn.broadcast import BroadCastMiddleware
async def my_app(messages, channels):
...
app = BroadCastMiddleware(my_app, 'localhost', 6379)
This will make a groups
channel available, which accepts the following
messages:
await channels['groups'].send({
'group': <name>,
'add': <channel_name>
})
Add a channel to the given group.
await channels['groups'].send({
'group': <name>,
'discard': <channel_name>
})
Remove a channel from the given group.
await channels['groups'].send({
'group': <name>,
'send': <message>
})
Send a message to all channels in the given group.
Let's add broadcast functionality to our previous chat server example...
from uvicorn.broadcast import BroadcastMiddleware
with open('index.html', 'rb') as file:
homepage = file.read()
async def chat_server(message, channels):
"""
A WebSocket based chat server.
"""
if message['channel'] == 'websocket.connect':
await channels['groups'].send({
'group': 'chat',
'add': channels['reply'].name
})
elif message['channel'] == 'websocket.receive':
await channels['groups'].send({
'group': 'chat',
'send': {'text': message['text']}
})
elif message['channel'] == 'websocket.disconnect':
await channels['groups'].send({
'group': 'chat',
'discard': channels['reply'].name
})
elif message['channel'] == 'http.request':
await channels['reply'].send({
'status': 200,
'headers': [
[b'content-type', b'text/html'],
],
'content': homepage
})
chat_server = BroadcastMiddleware(chat_server)
We can now start up a connected group of chat server instances:
First, start a Redis server:
$ redis-server
Then start one or more Uvicorn instances:
$ uvicorn app:chat_server --bind 127.0.0.1:8000
$ uvicorn app:chat_server --bind 127.0.0.1:8001
$ uvicorn app:chat_server --bind 127.0.0.1:8002
You can now open multiple browser windows, each connected to a different server instance, and send chat messages between them.
Provides an ASGI-style interface for an existing WSGI application.
from uvicorn.utils import ASGIAdapter
def app(environ, start_response):
...
asgi = ASGIAdapter(app)
Provides a WSGI interface for an existing ASGI-style application.
Useful if you're writing an asyncio application, but want to provide a backwards-compatibility interface for WSGI.
from uvicorn.utils import WSGIAdapter
async def app(message, channels):
...
wsgi = WSGIAdapter(app)