"""
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 ['
"Hi there
']
def hello(environ, start_response):
response = "Hello %s
" % 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-Z\d\-\.\_\~]+ |
+-----------+--------------------+
|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\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.
.. _PEP333: http://www.python.org/dev/peps/pep-0333/
.. _routing_args: http://wsgi.org/wsgi/Specifications/routing_args
"""
__version__ = "0.1.0"
__author__ = "Joe Gregorio "
__license__ = """MIT
Copyright (c) 2007, Joe Gregorio
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE."""
__all__ = [
"DispatcherException",
"DuplicateArgumentError",
"InvalidArgumentError",
"InvalidTemplateError",
"Dispatcher",
"template2regex"
]
import re
NOMATCH = -1
template_splitter = re.compile("([\[\]\{\}])")
class DispatcherException(Exception): pass
class DuplicateArgumentError(DispatcherException): pass
class InvalidArgumentError(DispatcherException): pass
class InvalidTemplateError(DispatcherException): pass
DEFAULT_RANGES = {
'word': r'\w+',
'alpha': r'[a-zA-Z]+',
'digits': r'\d+',
'alnum': r'[a-zA-Z0-9]+',
'segment': r'[^/]+',
'unreserved': r'[a-zA-Z\d\-\.\_\~]+',
'any': r'.+'
}
# The conversion done in template2regex can be in one of two states, either
# handling a path, or it can be inside a {} template.
S_PATH = 0
S_TEMPLATE = 1
def template2regex(template, ranges=None):
"""
Converts a template, such as /{name}/ to
a regular expression, e.g. /(?P[^/]+)/
Ranges are given after a colon in a template name
to indicate a restriction on the characters that
can appear there. For example, in the template::
"/user/{id:alpha}"
The ``id`` must contain only characters from a-zA-Z.
Other characters there will cause the pattern not to match.
The ranges parameter is an optional dictionary that maps
range names to regular expressions. New range names
can be added, or old range names can be redefined
using this parameter.
Example:
>>> import wsgidispatcher
>>> wsgidispatcher.template2regex("{fred}")
'^(?P[^/]+)$'
This function is used internally by Dispatcher, but left
public for testing and in case anyone finds it useful.
"""
if ranges == None:
ranges = DEFAULT_RANGES
anchor = True
state = S_PATH
if len(template) and template[-1] == '|':
anchor = False
bracketdepth = 0
result = ['^']
name = ""
pattern = "[^/]+"
rangename = None
for c in template_splitter.split(template):
if state == S_PATH:
if len(c) > 1:
result.append(c)
elif c == '[':
result.append("(")
bracketdepth += 1
elif c == ']':
bracketdepth -= 1
if bracketdepth < 0:
raise InvalidTemplateError("Mismatched brackets in %s" % template)
result.append(")?")
elif c == '{':
name = ""
state = S_TEMPLATE
elif c == '}':
raise InvalidTemplateError("Mismatched braces in %s" % template)
elif c == '|':
pass
else:
result.append(c)
else:
if c == '}':
if rangename and rangename in ranges:
result.append("(?P<%s>%s)" % (name, ranges[rangename]))
else:
result.append("(?P<%s>%s)" % (name, pattern))
state = S_PATH
rangename = None
else:
name = c
if name.find(":") > -1:
name, rangename = name.split(":")
if bracketdepth != 0:
raise InvalidTemplateError("Mismatched brackets in %s" % template)
if state == S_TEMPLATE:
raise InvalidTemplateError("Mismatched braces in %s" % template)
if anchor:
result.append('$')
return "".join(result)
class TemplatePredicate(object):
"""The presence of [], |, or {} indicates
match is a template and not just a plain string match."""
def __init__(self, path, appdict, ranges):
self.path = path
self.appdict = appdict
self.ranges = ranges
# We lazy eval the paths, only parsing regex's and such until we are called on to make a match.
self.isparsed = False
# Either this is a template, or a pure string match
self.istemplate = False
def __call__(self, environ, start_response):
if not self.isparsed:
if self.path.find("{") > -1 or self.path.find("[") > -1 or (len(self.path) and self.path[-1] == '|'):
regex = template2regex(self.path, self.ranges)
try:
self.regex= re.compile(regex)
except:
raise Exception("Invalid Template")
self.istemplate = True
self.isparsed = True
else:
self.isparsed = True
request_path = environ.get('PATH_INFO', '')
method = environ.get('REQUEST_METHOD', 'GET')
if not self.istemplate:
if self.path == request_path:
if method in self.appdict or "_ANY_" in self.appdict:
environ['wsgiorg.routing_args'] = ([], {})
return self.appdict.get(method, self.appdict.get('_ANY_', None))(environ, start_response)
else:
script_name = environ.get('SCRIPT_NAME', '')
match = self.regex.match(request_path)
if match:
if method in self.appdict or "_ANY_" in self.appdict:
extra_request_path = request_path[match.end():]
pos, named = environ.get('wsgiorg.routing_args', ((), {}))
new_named = named.copy()
new_named.update(match.groupdict())
environ['wsgiorg.routing_args'] = (pos, new_named)
environ['SCRIPT_NAME'] = script_name + request_path[:match.end()]
environ['PATH_INFO'] = extra_request_path
return self.appdict.get(method, self.appdict.get('_ANY_', None))(environ, start_response)
return NOMATCH
class RegexPredicate(object):
def __init__(self, regex, appdict, ranges):
self.regexsrc = regex
self.isparsed = False
self.appdict = appdict
def __call__(self, environ, start_response):
if not self.isparsed:
self.regex = re.compile(self.regexsrc)
script_name = environ.get('SCRIPT_NAME', '')
request_path = environ.get('PATH_INFO', '')
method = environ.get('REQUEST_METHOD', 'GET')
match = self.regex.match(request_path)
if match:
extra_request_path = request_path[match.end():]
pos, named = environ.get('wsgiorg.routing_args', ((), {}))
new_named = named.copy()
new_named.update(match.groupdict())
new_pos = list(pos) + list(match.groups())
environ['wsgiorg.routing_args'] = (new_pos, new_named)
environ['SCRIPT_NAME'] = script_name + request_path[:match.end()]
environ['PATH_INFO'] = extra_request_path
environ['wsgiorg.routing_args']= (list(match.groups()), match.groupdict())
if method in self.appdict or "_ANY_" in self.appdict:
return self.appdict.get(method, self.appdict.get('_ANY_', None))(environ, start_response)
return NOMATCH
class Dispatcher(object):
def __init__(self, handle404 = None, ranges = None):
"""
handle404 - A WSGI application to be used when no match is found for a requested URI path.
ranges - A dictionary that maps new range names to regular expressions that match those characters.
Example::
import logging
def my404(environ, start_response):
logging.warning("404: %s" % environ.get('PATH_INFO', ''))
start_response("404 Not Found", [('Content-Type', "text/html")])
return ["File Not Found
"]
d = Dispatcher(self.my404, {'real':'(\+|-)?[1-9]\.[0-9]*E(\+|-)?[0-9]+'})
d.add("/arc/{degrees:real}/", view.display)
"""
self.matchers = []
if handle404 == None:
self.handle404 = self._404
else:
self.handle404 = handle404
self.ranges = DEFAULT_RANGES
if ranges:
self.ranges.update(ranges)
def _404(self, environ, start_response):
""" The default 404 response for Dispatcher. You can pass in your
own app to handle 404s to __init__."""
start_response("404 Not Found", [('Content-Type', "text/html")])
return ["File Not Found
"]
def __call__(self, environ, start_response):
"""An instance of a Dispatcher is a callable that is
a WSGI application. See the module level documentation
for an example."""
for predicate in self.matchers:
ret = predicate(environ, start_response)
if ret != NOMATCH:
return ret
return self.handle404(environ, start_response)
def _appmap(self, args, kwargs):
appmap = {}
if args and kwargs:
raise DuplicateArgumentError("You can not specify both an application and a map of methods")
if args:
if len(args) != 1:
raise InvalidArgumentError("You may only specify one WSGI app for each _ANY_ call.")
appmap['_ANY_'] = args[0]
else:
appmap.update(kwargs)
return appmap
def add(self, path, *args, **kwargs):
"""You can either add a WSGI application to handle all the matches,
or you can add applications based on the method.
Example::
d = Dispatcher()
d.add("/fred/", app)
d.add("/barney/", GET=app2, POST=app3)
d.add("/wilma/", _ANY_=app4)
A request with any method to either ``/fred/`` or ``/wilma/`` will
cause ``app`` or ``app4`` to called. A PUT to ``/barney/``
will result in a 404, while a POST will call ``app3``.
"""
appmap = self._appmap(args, kwargs)
self.matchers.append(TemplatePredicate(path, appmap, self.ranges))
def addregex(self, regex, *args, **kwargs):
"""Same exact operation as add, except that 'regex' is a regular
expression and not a template. Named groups appead in the
positional args and named groups in the regular expression
will appear in the named args.
Example::
def app(environ, start_response):
start_response("200 Ok", [('Content-Type', "text/html")])
return ["You requested: %s
" % environ['wsgiorg.routing_args'][0][0] ]
d = Dispatcher()
d.addregex("/([^/]+)/", app)
"""
appmap = self._appmap(args, kwargs)
self.matchers.append(RegexPredicate(regex, appmap, self.ranges))