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/.
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.