BitWorking

Joe Gregorio's writings (archives), projects and status updates.

Up

WSGI Dispatcher

Dispatcher is WSGI middleware that dispatches incoming WSGI requests based on URI Templates and the request method and passes along the request to the selected WSGI application. Dispatcher conforms to the routing_args specification. You should read PEP333 if you don't know about WSGI.

Dispatcher maps an incoming HTTP request path against a series of patterns. Patterns may be given as plain text string that must match the request path exactly, they may be templates, or they may be specified as regular expressions. Groups of matching characters for templates and regular expressions are extracted from the URI for easy handling by your application. When a match is obtained then that matching WSGI application is given the request to handle.:

from wsgidispatcher import Dispatcher

def index(environ, start_response):
    start_response("200 Ok", [('content-type', 'text/html')])
    return ['<h1>"Hi there</h1>']

def hello(environ, start_response):
    response = "<h1>Hello %s</h1>" % environ['wsgiorg.routing_args'][1]['name']
    start_response("200 Ok", [('content-type', 'text/html')])
    return [response]

urls = Dispatcher()
urls.add('/index/', GET=index)
urls.add('/index/{name}', GET=hello)

from wsgiref.simple_server import make_server

server = make_server('', 8000, urls)
server.serve_forever()

If you save this off as dispatcher-example.py and run it from the command line:

$ python dispatcher-example.py

you can then visit this URI:

http://localhost:8000/index/Joe

And you will get a web page that prints 'Hello Joe'.

If you visit this URI:

http://localhost:8000/index/

You will get a web page that prints 'Hi there'.

Note that wsgiref is standard in Python 2.5.

Look at the function hello(), it uses the data stored in 'wsgiorg.routing_args' to determine the value of the '{id}' segment of the requested URI.

Some things to note:

  • Patterns are matched in the order in which they are added to the Dispatcher.
  • First match wins
  • You can define you own new ranges, or over-ride the built in ranges.
  • You can provide one application for every method, or you can provide a single application that will response to every method used.
  • If no matches are found a 404 message is generated.
  • You can provide your own custom 404 handler.
  • Does not use setuptools
  • No external dependencies

You specify an application to handle a request based on the HTTP method:

urls = Dispatcher()
urls.add('/index/', GET=index, POST=add_stuff)
urls.add('/index/{name}', GET=hello)

For applications that will handle all methods, you can either use _ANY_ for the method, or drop the method entirely:

urls = Dispatcher()
urls.add('/index/', does_it_all_app)
urls.add('/index/{name}', GET=hello)

You can also mix and match templates and regular expressions:

urls = Dispatcher()
urls.add('/index/', does_it_all_app)
urls.addregex('^/comments/(\d+)$', GET=comments)
urls.add('/index/{name}', GET=hello)

You can add an optional range qualifier to every template parameter that restricts the characters that consistitute a match. The range specifier follows a colon in the template name. Here are the ranges that are predefined:

Range Regular Expression
word w+
alpha [a-zA-Z]+
digits d+
alnum [a-zA-Z0-9]+
segment [^/]+
unreserved [a-zA-Zd-._~]+
any .+

Here is an example the uses ranges:

d = Dispatcher()
d.add("/a/b/{n:digits}", myapp)

You can add new range values by passing in a dictionary that maps the range name to a regular expression. Here is an example of a Dispatcher being constructed that recognizes real numbers in engineering format:

d = Dispatcher(ranges = {'real':'(\+|-)?[1-9]\.[0-9]*E(\+|-)?[0-9]+'})
d.add("/a/b/{n:real}", my_math_app)

Templates understand three special kinds of markup:

{name} Whatever matches this part of the path will be available to the application in the routing_args named parameters.
[] Any part of a path enclosed in brackets is optional
| The bar may only be present at the end of the template and signals that the path need not match the whole path.
  • Brackets may be nested
  • Brackets may contain template parameters

This is an exmaple template:

/service/[{collection:alpha}[/[{id:unreserved}/]]][;{noun}]

The template will match these paths:

/service;service_document
/service/entry/12/;media
/service/

But not these:

/service
/service/12/entry/;media
/other/

In addition, regular expressions may be used as templates. They are added via addregex():

d = Dispatcher()
d.addregex("/([^/]+)/(?P<fred>\d+)", self._app)
d({'PATH_INFO': '/fred/123abc', 'REQUEST_METHOD': 'GET'}, self._start_response)
self.assertEqual(self.environ['wsgiorg.routing_args'][0][0], 'fred')
self.assertEqual(self.environ['wsgiorg.routing_args'][1]['fred'], '123')

Note that the value of the unnamed groups is returned in the positional args and the named groups are returned via the named args.