"""
This module provides classes for creating WSGI (:pep:`333`) applications.
For a simple example, see ``examples/wsgi.py``. For a more complete example,
see ``examples/todo``.
"""
from .router import Router
from .template import Template
from .context import Context
from .util import rename_args
[docs]class PathRouter(Router):
"""
Route by URL/path.
Utilizes the :class:`~potpy.template.Template` class to capture path
parameters, adding them to the :class:`~potpy.context.Context`. For
example, you might define a route with a path
template: ``/posts/{slug}`` -- which would match the path
``/posts/my-post``, adding ``{'slug': 'my-post'}`` to the context:
>>> from potpy.context import Context
>>> from pprint import pprint
>>> handler = lambda: None # just a bogus handler
>>> router = PathRouter(('/posts/{slug}', handler))
>>> ctx = Context(path_info='/posts/my-post')
>>> ctx.inject(router)
>>> pprint(dict(ctx))
{'path_info': '/posts/my-post', 'slug': 'my-post'}
Routes can also be named, allowing reverse path lookup and filling of path
parameters. See :meth:`reverse` for details.
"""
def __init__(self, *routes):
self._templates = {}
super(PathRouter, self).__init__(*routes)
[docs] def add(self, *args):
"""Add a path template and handler.
:param name: Optional. If specified, allows reverse path lookup with
:meth:`reverse`.
:param template: A string or :class:`~potpy.template.Template`
instance used to match paths against. Strings will be wrapped in a
Template instance.
:param handler: A callable or :class:`~potpy.router.Route` instance
which will handle calls for the given path. See
:meth:`potpy.router.Router.add` for details.
"""
if len(args) > 2:
name, template = args[:2]
args = args[2:]
else:
name = None
template = args[0]
args = args[1:]
if isinstance(template, tuple):
template, type_converters = template
template = Template(template, **type_converters)
elif not isinstance(template, Template):
template = Template(template)
if name:
self._templates[name] = template
super(PathRouter, self).add(template, *args)
[docs] def match(self, template, path_info):
"""Check for a path match.
:param template: A :class:`~potpy.template.Template` object to match
against.
:param path_info: The path to check for a match.
:returns: The template parameters extracted from the path, or ``None``
if the path does not match the template.
Example:
>>> from potpy.template import Template
>>> template = Template('/posts/{slug}')
>>> PathRouter().match(template, '/posts/my-post')
{'slug': 'my-post'}
"""
return template.match(path_info)
__call__ = rename_args(Router.__call__, (
'self', 'context', 'path_info'))
[docs] def reverse(self, *args, **kwargs):
"""Look up a path by name and fill in the provided parameters.
Example:
>>> handler = lambda: None # just a bogus handler
>>> router = PathRouter(('post', '/posts/{slug}', handler))
>>> router.reverse('post', slug='my-post')
'/posts/my-post'
"""
(name,) = args
return self._templates[name].fill(**kwargs)
[docs]class MethodRouter(Router):
"""
Route by request method.
>>> from potpy.context import Context
>>> handler1 = lambda request_method: (1, request_method.lower())
>>> handler2 = lambda request_method: (2, request_method.lower())
>>> router = MethodRouter(
... ('POST', handler1), # can specify a single method
... (('GET', 'HEAD'), handler2) # or a tuple of methods
... )
>>> Context(request_method='GET').inject(router)
(2, 'get')
>>> Context(request_method='POST').inject(router)
(1, 'post')
"""
[docs] class MethodNotAllowed(Router.NoRoute):
"""
Raised instead of :exc:`potpy.router.Router.NoRoute` when no handler
matches the given method.
Has an ``allowed_methods`` attribute which is a list of the methods
handled by this router.
"""
def __init__(self, allowed_methods, request_method):
self.allowed_methods = allowed_methods
self.request_method = request_method
def NoRoute(self, request_method):
allowed_methods = []
for methods, route in self.routes:
allowed_methods.extend(methods)
return self.MethodNotAllowed(allowed_methods, request_method)
[docs] def match(self, methods, request_method):
"""Check for a method match.
:param methods: A method or tuple of methods to match against.
:param request_method: The method to check for a match.
:returns: An empty :class:`dict` in the case of a match, or ``None``
if there is no matching handler for the given method.
Example:
>>> MethodRouter().match(('GET', 'HEAD'), 'HEAD')
{}
>>> MethodRouter().match('POST', 'DELETE')
"""
if isinstance(methods, basestring):
return {} if request_method == methods else None
return {} if request_method in methods else None
__call__ = rename_args(Router.__call__, (
'self', 'context', 'request_method'))
[docs]class App(object):
"""Wrap a potpy router in a WSGI application.
Use this with :class:`PathRouter` and :class:`MethodRouter` to implement a
full-featured HTTP request routing system. Return a WSGI app from the last
handler in the route, and it will be called with ``environ`` and
``start_response``.
If no route matches, a `404 Not Found` response will be generated. If
using a MethodRouter, and the request method doesn't match, a `405 Method
Not Allowed` response will be generated. Also responds to HTTP ``OPTIONS``
requests.
Calls the provided router with a context containing ``environ``,
``path_info``, and ``request_method`` fields, and any fields from the
optional ``default_context`` argument.
:param router: The router to call in response to WSGI requests.
:param default_context: Optional. A :class:`dict`-like mapping of extra
fields to add to the context for each request.
Example:
>>> def my_app(environ, start_response):
... start_response('200 OK', [('Content-type', 'text/plain')])
... return ['Hello, world!']
...
>>> def handler(request):
... # do something with the request
... return my_app
...
>>> class Request(object):
... def __init__(self, environ):
... pass # wrap environ in a custom request object
...
>>> app = App(
... PathRouter(('/hello', lambda: my_app)),
... {'request': Request} # add a Request object to context
... )
>>> app({
... 'PATH_INFO': '/hello',
... 'REQUEST_METHOD': 'GET',
... }, lambda status, headers: None) # bogus start_response
['Hello, world!']
"""
def __init__(self, router, default_context=None):
self.router = router
if default_context is None:
default_context = {}
self.default_context = default_context
def not_found(self, environ, start_response):
message = 'The requested resource could not be found.\r\n'
start_response('404 Not Found', [
('Content-type', 'text/plain'),
('Content-length', str(len(message)))
])
return [message]
def method_not_allowed(self, request_method, allowed_methods):
joined_methods = ', '.join(allowed_methods)
def method_not_allowed(environ, start_response):
if request_method == 'OPTIONS':
status = '200 OK'
message = (
'The requested resource supports the '
'following methods: %s.\r\n'
) % (joined_methods,)
else:
status = '405 Method Not Allowed'
message = (
'The requested resource does not support '
'the %s method. It does support: %s.\r\n'
) % (request_method, joined_methods)
start_response(status, [
('Content-type', 'text/plain'),
('Content-length', str(len(message))),
('Allow', joined_methods)
])
return [message]
return method_not_allowed
[docs] def __call__(self, environ, start_response):
"""Call the router as a WSGI app.
Constructs a :class:`~potpy.context.Context` object with ``environ``,
``path_info``, and ``request_method`` (extracted from the environ),
and any fields supplied in ``self.default_context``.
Calls the result of the router call as a WSGI app.
"""
context = Context(
self.default_context,
environ=environ,
path_info=environ['PATH_INFO'],
request_method=environ['REQUEST_METHOD']
)
try:
response = context.inject(self.router)
except MethodRouter.MethodNotAllowed, exc:
return self.method_not_allowed(
exc.request_method, exc.allowed_methods
)(environ, start_response)
except PathRouter.NoRoute:
return self.not_found(environ, start_response)
return response(environ, start_response)