"""
URL routing for Twisted Web resources.
A Python-based Domain Specific Language is used to specify and match routing
paths, string literal components are matched for structure while plain callable
components match segment values and are stored by name for use in the handler,
assuming all the components match; this makes it trivial to create new
functions to match path components.
`route` is used to match a URL exactly (the number of route components must
match the number of URL path segments) while `subroute` is used to match a URL
prefix (the specified route components must match the respective segments in
a URL path, additional segments are used in child resource location as normal.)
`Router` is an `IResource` that allows decorating methods as route or subroute
handlers.
"""
from collections import OrderedDict
from functools import partial
from itertools import izip_longest
from zope.interface import implementer
from txspinneret import query
from txspinneret.resource import (
NotFound, SpinneretResource, ISpinneretResource)
from txspinneret.util import contentEncoding
[docs]def Text(name, encoding=None):
"""
Match a route parameter.
`Any` is a synonym for `Text`.
:type name: `bytes`
:param name: Route parameter name.
:type encoding: `bytes`
:param encoding: Default encoding to assume if the ``Content-Type``
header is lacking one.
:return: ``callable`` suitable for use with `route` or `subroute`.
"""
def _match(request, value):
return name, query.Text(
value,
encoding=contentEncoding(request.requestHeaders, encoding))
return _match
Any = Text
[docs]def Integer(name, base=10, encoding=None):
"""
Match an integer route parameter.
:type name: `bytes`
:param name: Route parameter name.
:type base: `int`
:param base: Base to interpret the value in.
:type encoding: `bytes`
:param encoding: Default encoding to assume if the ``Content-Type``
header is lacking one.
:return: ``callable`` suitable for use with `route` or `subroute`.
"""
def _match(request, value):
return name, query.Integer(
value,
base=base,
encoding=contentEncoding(request.requestHeaders, encoding))
return _match
def _matchRoute(components, request, segments, partialMatching):
"""
Match a request path against our path components.
The path components are always matched relative to their parent is in the
resource hierarchy, in other words it is only possible to match URIs nested
more deeply than the parent resource.
:type components: ``iterable`` of `bytes` or `callable`
:param components: Iterable of path components, to match against the
request, either static strings or dynamic parameters. As a convenience,
a single `bytes` component containing ``/`` may be given instead of
manually separating the components. If no components are given the null
route is matched, this is the case where ``segments`` is empty.
:type segments: ``sequence`` of `bytes`
:param segments: Sequence of path segments, from the request, to match
against.
:type partialMatching: `bool`
:param partialMatching: Allow partial matching against the request path?
:rtype: 2-`tuple` of `dict` keyed on `bytes` and `list` of `bytes`
:return: Pair of parameter results, mapping parameter names to processed
values, and a list of the remaining request path segments. If there is
no route match the result will be ``None`` and the original request path
segments.
"""
if len(components) == 1 and isinstance(components[0], bytes):
components = components[0]
if components[0] == '/':
components = components[1:]
components = components.split('/')
results = OrderedDict()
NO_MATCH = None, segments
remaining = list(segments)
# Handle the null route.
if len(segments) == len(components) == 0:
return results, remaining
for us, them in izip_longest(components, segments):
if us is None:
if partialMatching:
# We've matched all of our components, there might be more
# segments for something else to process.
break
else:
return NO_MATCH
elif them is None:
# We've run out of path segments to match, so this route can't be
# the matching one.
return NO_MATCH
if callable(us):
name, match = us(request, them)
if match is None:
return NO_MATCH
results[name] = match
elif us != them:
return NO_MATCH
remaining.pop(0)
return results, remaining
[docs]def route(*components):
"""
Match a request path exactly.
The path components are always matched relative to their parent is in the
resource hierarchy, in other words it is only possible to match URIs nested
more deeply than the parent resource.
:type components: ``iterable`` of `bytes` or `callable`
:param components: Iterable of path components, to match against the
request, either static strings or dynamic parameters. As a convenience,
a single `bytes` component containing ``/`` may be given instead of
manually separating the components. If no components are given the null
route is matched, this is the case where ``segments`` is empty.
:rtype: 2-`tuple` of `dict` keyed on `bytes` and `list` of `bytes`
:return: Pair of parameter results, mapping parameter names to processed
values, and a list of the remaining request path segments. If there is
no route match the result will be ``None`` and the original request
path segments.
"""
return partial(_matchRoute, components, partialMatching=False)
[docs]def subroute(*components):
"""
Partially match a request path exactly.
The path components are always matched relative to their parent is in the
resource hierarchy, in other words it is only possible to match URIs nested
more deeply than the parent resource.
If there are more request path segments than components the match may still
be successful, the remaining path segments are returned in the second part
of the result.
:type components: ``iterable`` of `bytes` or `callable`
:param components: Iterable of path components, to match against the
request, either static strings or dynamic parameters. As a convenience,
a single `bytes` component containing ``/`` may be given instead of
manually separating the components. If no components are given the null
route is matched, this is the case where ``segments`` is empty.
:rtype: 2-`tuple` of `dict` keyed on `bytes` and `list` of `bytes`
:return: Pair of parameter results, mapping parameter names to processed
values, and a list of the remaining request path segments. If there is
no route match the result will be ``None`` and the original request
path segments.
"""
return partial(_matchRoute, components, partialMatching=True)
@implementer(ISpinneretResource)
class _RouterResource(object):
"""
Resource that provides URL routing to `IResource
<twisted:twisted.web.resource.IResource>`.
"""
def __init__(self, obj, routes):
"""
:param obj: Parent object containing the route handler.
:type routes: `list` of 3-`tuple` containing `bytes`, `callable`,
`callable`
:param routes: List of 3-tuple containing the route handler name, the
route handler function and the matcher function.
"""
self._obj = obj
self._routes = routes
def _matchRoute(self, request, segments):
"""
Find a route handler that matches the request path and invoke it.
"""
for name, meth, route in self._routes:
matches, remaining = route(request, segments)
if matches is not None:
return meth(self._obj, request, matches), remaining
return None, segments
def render(self, request):
# This only exists to handle the null route case, ie. there are no
# segments so this resource's render method is invoked.
result, segments = self._matchRoute(request, [])
if result is None:
result = NotFound()
return result.render(request)
# ISpinneretResource
def locateChild(self, request, segments):
return self._matchRoute(request, segments)
[docs]class Router(object):
"""
URL routing.
`Router` is designed to be used as a Python descriptor using `Router.route`
or `Router.subroute` to decorate route handlers, for example:
.. code-block:: python
class Users(object):
router = Router()
@router.route('name')
def name(self, request, params):
# ...
Route handlers can return any value supported by
`ISpinneretResource.locateChild`.
Calling `Router.resource` will produce an `IResource
<twisted:twisted.web.resource.IResource>`.
"""
def __init__(self):
self._routes = []
def _forObject(self, obj):
"""
Create a new `Router` instance, with it's own set of routes, for
``obj``.
"""
router = type(self)()
router._routes = list(self._routes)
router._self = obj
return router
def __get__(self, obj, type=None):
if obj is None:
return self
return self._forObject(obj)
def _addRoute(self, f, matcher):
"""
Add a route handler and matcher to the collection of possible routes.
"""
self._routes.append((f.func_name, f, matcher))
[docs] def resource(self):
"""
Create an `IResource <twisted:twisted.web.resource.IResource>` that
will perform URL routing.
"""
return SpinneretResource(_RouterResource(self._self, self._routes))
[docs] def route(self, *components):
"""
See `txspinneret.route.route`.
This decorator can be stacked with itself to specify multiple routes
with a single handler.
"""
def _factory(f):
self._addRoute(f, route(*components))
return f
return _factory
[docs] def subroute(self, *components):
"""
See `txspinneret.route.subroute`.
This decorator can be stacked with itself to specify multiple routes
with a single handler.
"""
def _factory(f):
self._addRoute(f, subroute(*components))
return f
return _factory
__all__ = ['Router', 'Any', 'Text', 'Integer', 'route', 'subroute']