""" 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))