Tutorial¶
This is a basic tutorial for setting up a new project with Aiopyramid.
Install Aiopyramid and Initialize Project¶
It is highly recommended that you use a virtual environment for your project. The tutorial will assume that you are using virtualenvwrapper with a virtualenv created like so:
mkvirtualenv aiotutorial --python=/path/to/python3.4/interpreter
Once you have your tutorial environment active, install Aiopyramid
:
pip install aiopyramid
This will also install the Pyramid framework. Now create
a new project using the aio_websocket
scaffold.
pcreate -s aio_websocket aiotutorial
This will make an aiotutorial
directory with the following structure:
.
├── aiotutorial << Our Python package
│ ├── __init__.py << main file, contains the app constructor
│ ├── templates << directory for storing jinja templates
│ │ └── home.jinja2 << template for the example homepage, contains a websocket test
│ ├── tests.py << tests module, contains tests for each of our existing views
│ └── views.py << views module, contains view callables
├── CHANGES.rst << file for tracking changes to the library
├── development.ini << config file, contains project and server settings
├── MANIFEST.in << manifest file for distributing the project
├── README.rst << readme for bragging about the project
└── setup.py << Python module for distributing the package and managing dependencies
Let’s look at some of these files a little closer.
App Constructor¶
The aiotutorial/__init__.py
file contains the constructor for our app. It loads the logging
config from the development.ini
config file and sets up Python logging. This is necessary
because the logging configuration won’t be automatically detected when using Python3. Then, it
sets up two routes home
and echo
that we can tie into with our views. Finally,
the constructor scans the project for configuration decorators and builds the wsgi callable.
The app constructor is the place where we will connect Python libraries to our application and perform other configuration tasks.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import logging.config
from pyramid.config import Configurator
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
# support logging in python3
logging.config.fileConfig(
settings['logging.config'],
disable_existing_loggers=False
)
config = Configurator(settings=settings)
config.add_route('home', '/')
config.add_route('echo', '/echo')
config.scan()
return config.make_wsgi_app()
|
Note
Thinking Asynchronously
The app constructor is called once to setup the application, which means that it is a synchronous context. The app is constructed before any requests are served, so it is safe to call blocking code here.
Tests¶
The aiotutorial/tests.py
file is a Python module with unittests for each of our views.
Let’s look at the test case for the home page:
1 2 3 4 5 6 7 8 | class HomeTestCase(unittest.TestCase):
def test_home_view(self):
from .views import home
request = testing.DummyRequest()
info = asyncio.get_event_loop().run_until_complete(home(request))
self.assertEqual(info['title'], 'aiotutorial websocket test')
|
Since test runners for unittest expect tests, such as test_home_view
, to run synchronously
but our home view is a coroutine, we need to manually obtain an asyncio
event
loop and run our view. Line 6 obtains a dummy request from pyramid.testing
. We then pass
that request to our view and run it on line 7. Finally, line 8 makes assertions about the kind
of output we expect from our view.
Views¶
This is the brains of our application, the place where decisions about how to respond to a particular request are made, and as such this is the place where you will most often start chaining together coroutines to perform asynchronous tasks. Let’s look at each of the example views in turn:
1 2 3 4 5 6 | @view_config(route_name='home', renderer='aiotutorial:templates/home.jinja2')
@asyncio.coroutine
def home(request):
wait_time = float(request.params.get('sleep', 0.1))
yield from asyncio.sleep(wait_time)
return {'title': 'aiotutorial websocket test', 'wait_time': wait_time}
|
For those already familiar with Pyramid most of this view should require no explanation. The important parts for running asynchronously are lines 2 and 5.
The view_config()
decorator on line 1 ties this view to the ‘home’
route declared in the app constructor. It also assigns a renderer to the view that will
render the data returned into the template/home.jinja
template and return a response
to the user. Line 2 wraps the view in a coroutine which differentiates it from a generator
or native coroutine. Line 3 is the signature for the coroutine. Aiopyramid
view mappers
do not change the two default signarures for views, i.e. views that accept a request
and views that accept a context and a request. On line 4, we retrieve a sleep parameter,
from the request (the parameter can be either part of the querystring or the body). If
the request doesn’t include a sleep parameter, the view defaults to 0.1. We don’t need to
use yield from
because request.params.get
doesn’t return a coroutine or future.
The data for the request exists in memory so retrieving the parameter should be very fast.
Line 5 simulates performing some asynchronous task by suspending the coroutine and delegating to
another coroutine, asyncio.sleep()
, which uses events to wait for wait_time
seconds.
Using yield from
is very important, without it the coroutine would
continue without sleeping. Line 6 returns a Python dictionary that will be passed to the
jinja2 renderer.
The second view accepts a websocket connection:
1 2 3 4 5 6 7 8 | @view_config(route_name='echo', mapper=WebsocketMapper)
@asyncio.coroutine
def echo(ws):
while True:
message = yield from ws.recv()
if message is None:
break
yield from ws.send(message)
|
This view is tied to the ‘echo’ route from the app constructor. Note that we use a special view mapper
for websocket connections. The aiopyramid.websocket.config.WebsocketMapper
changes the signature
of the view to accept a single websocket connection instead of a request. The connection object has three methods
for communicating with the websocket recv()
, send()
, and close()
that
correspond to similar methods in the websockets library.
This websocket view will run echoing the data it recieves until the connection is closed. On line 5 we use
yield from
to wait until a message is received. If the message is None, then we know that the websocket
has closed and we break the loop to complete the echo coroutine. Otherwise, line 7 simply returns the same
message back to the websocket. Very simple. In both cases when we need to perform some io we use yield from
to suspend our coroutine and delegate to another.
This kind of explicit yielding is a nice advantage for readability in Python code. It shows us exactly where we are calling asynchronous code.
Development.ini¶
The development.ini
file contains the config for the project. Most of these settings could be specified in
the app constructor but it makes sense to separate out these values from procedural code. Here is an overview
of the two most important sections:
[app:main]
use = egg:aiotutorial
pyramid.includes =
aiopyramid
pyramid_jinja2
# for py3
logging.config = %(here)s/development.ini
The [app:main]
section contains the settings that will be passed to the app constructor as settings
.
This is where we include extensions for Pyramid such as Aiopyramid
and the jinja
templating library.
The [server:main]
configures the default server for the project, which in this case is gunicorn
:
[server:main]
use = egg:gunicorn#main
host = 0.0.0.0
port = 6543
worker_class = aiopyramid.gunicorn.worker.AsyncGunicornWorker
The port
setting here is the port that we will use to access the application, such as in a browser. The
worker_class
is set to the aiopyramid.gunicorn.worker.AsyncGunicornWorker
because we need to have
gunicorn
setup the Aiopyramid Architecture for us.
Setup¶
The setup.py
file makes the aiotutorial
package easy to distirbute, and it is also a good way, although
not the only good way, to manage dependencies for our project. Lines 18-21 list the Python packages that we need
for this project.
requires = [
'aiopyramid[gunicorn]',
'pyramid_jinja2',
]
Note about View Mappers¶
The default view mapper that Aiopyramid
sets up when it is included by the application tries to be as
robust as possible. It will inspect all of the views that we configure and try to guess whether or not
they are coroutines. If the view looks like a coroutine, in other words if it has
a yield from
in it, the framework will treat it as a coroutine, otherwise it will assume it is
legacy code and will run it in a separate thread to avoid blocking the event loop. This is very important.
When using Aiopyramid
view mappers, it is actually not necessary to explicitly decorate view callables
with asyncio.coroutine()
as in the examples because the mapper will wrap views that appear to be coroutines
for you. It is still good practice to explicitly wrap your views because it facilitates using them in places where a
view mapper may not be active, but if you are annoyed by the repetition, then you can skip writing @asyncio.coroutine
before
every view as long as you remember what is a coroutine.
Making Sure it Works¶
The last step in initializing the project is to install out dependencies and test out that the scaffold works as we expect:
python setup.py develop
You can also use setup.py
to run unittests:
python setup.py test
You should see the following at the end of the output:
test_home_view (aiotutorial.tests.HomeTestCase) ... ok
test_echo_view (aiotutorial.tests.WSTest) ... ok
----------------------------------------------------------------------
Ran 2 tests in 1.709s
OK
If you don’t like the test output from setup.py
, consider using a test runner like pytest.
Now try running the server and visiting the homepage:
gunicorn --paste development.ini
Open your browser to http://127.0.0.1:6543 to see the JavaScript test of the our echo websocket. You should see the following output:
aiotutorial websocket test
CONNECTED
SENT: Aiopyramid echo test.
RESPONSE: Aiopyramid echo test.
DISCONNECTED
This shows that the websocket is working. If you want to verify that the server is able to handle
multiple requests on a single thread, simply open a different browser (to avoid browser connection
limitations) and go to http://127.0.0.1:6543?sleep=10. The new browser should take roughly ten seconds
to load the page because our view is waiting for the value of sleep
. However, while that request is
ongoing, you can refresh your first browser and see that the server is still able to fulfill requests.
Congratulations! You have successfuly setup a highly configurable asynchronous server using Aiopyramid
!
Note
Extra Credit
If you really want to see the power of asynchronous programming in Python, obtain a copy of slowloris
and run it against your knew Aiopyramid
server and some non-asynchronous server. For example,
you could run a simple Django
application with gunicorn. You should see that the Aiopyramid
server
is still able to respond to requests whereas the Django
server is bogged down. You could also use a simple
PHP application using Apache to see this difference.