WSGI, the first steps

Building web apps in python is quite a lot of fun, and so I decided to write a little micro-framework from scratch. Today we will only think about interface with the webserver, using WSGI.
WSGI is the de-facto standard to interface a web application with a server (also known as “gateway”). You may read the PEP 3333 to learn more about it, but here I want to go straight to building a little wsgi app.
WSGI applications have an onion architecture. The application is in the middle, the webserver is outside, and a certain number of middlewares inbetween can gives your application extra super-powers.
Get your environment ready
To build our application, we’ll use a virtual environment to isolate our experiments.
Let’s install some packages. Depending on your system, python-pip may be not
available and python-setuptools will replace it well (you’ll need to use
easy_install instead of pip to install python packages, but this is outside
the scope of this blog post).
$ sudo apt-get install python-virtualenv python python-pip build-essential libevent-dev python-dev
$ mkdir wsgi-test
$ cd wsgi-test
$ virtualenv --no-site-packages wsgi-test.env
$ . wsgi-test.env/bin/activate
$ pip install webob weberror gevent
This article has been written using python 2.6, but should be ok for python>=2.5. It probably needs some adjustements for python>=3.0
First application
A WSGI application is basically a python callable which takes some parameters
and returns an iterator on the response body parts. The input parameters are an
environment dict, and a start_response callable which you’ll call to start
sending the headers.
Let’s write the first app.
def wsgi_app(environ, start_response):
start_response('200 OK', [])
return ['Hello, little world.']
if __name__ == '__main__':
from gevent import wsgi
wsgi.WSGIServer(('', 2222), wsgi_app, spawn=None).serve_forever()
This app is a bit minimalist, but it works. You can run the file and check http://localhost:2222/.
We’re using gevent to embed a webserver in our application because it’s fast and easy. Almost every webserver have WSGI support, either builtin or using extensions, so you can switch to whatever you like. Using Apache for example, you can run your application using mod_wsgi. Configuring a web server is out of this article’s scope.
Getting programmer friendly
Of course, it’s always annoying to talk raw WSGI. You will forget some headers,
behave badly, kill people around you … But we have installed a tool that will
transform the environ dict into a nicely formated python object.
Let me introduce my good friend webob.
from webob import ( Request as HttpRequest,
Response as HttpResponse )
def wsgi_app(environ, start_response):
# Create request from WSGI environment.
request = HttpRequest(environ=environ)
# A real app would look at the request before giving blind answers.
# Create a response
response = HttpResponse('Hello, better world.')
# Communicate
start_response(response.status, response.headerlist)
return response.body
if __name__ == '__main__':
from gevent import wsgi
wsgi.WSGIServer(('', 2222), wsgi_app, spawn=None).serve_forever()
This is a bit better. But hey, we can then separate the WSGI stuff, and the request-to-response process.
from webob import ( Request as HttpRequest,
Response as HttpResponse )
def web_app(request):
return HttpResponse('Hello, nice world at %s.' % request.path_info)
def wsgi_app(environ, start_response):
# Create the request
request = HttpRequest(environ=environ)
# Build a response from this request
response = web_app(request)
# Communicate with gateway
start_response(response.status, response.headerlist)
return response.body
if __name__ == '__main__':
from gevent import wsgi
wsgi.WSGIServer(('', 2222), wsgi_app, spawn=None).serve_forever()
Pythonize it
The good news is that the webob library includes a decorator that is able to
turn any request-taking/response-returning python callable in a WSGI-capable
callable.
The last version of this program, which does nothing more than the last one, follows.
from webob import Response as HttpResponse
from webob.dec import wsgify
@wsgify
def app(request):
return HttpResponse('Hello, short world at %s.' % request.path_info)
if __name__ == '__main__':
from gevent import wsgi
wsgi.WSGIServer(('', 2222), app, spawn=None).serve_forever()
This is all for today, but there is a lot more to come so stay tuned.