"""URL path splitting and delegation support.
This module provides support for selecting a request handler based on an arc
of the path component of the URL. A very flexible basic implementation is
provided along with several more specific implementations for common uses.
Additionally, there is support for delegating based on the request method and
in particular for the most common case of sending GET requests to one
handler and POST requests to another handler.
"""
import cgi
import datetime
import wsgiref.util
from . import parameter, status
[docs]class DelegateHandler (object):
"""Common superclass for WSGI handlers which delegate to other handlers.
This class is callable, and implements the WSGI interface.
Subclasses must define get_handler (environ) which determines the handler
to which this handler will delegate handling of the request, and also
updates environ appropriately.
"""
def __call__ (self, environ, start_response):
# Determine handler, update environ appropriately
handler = self.get_handler (environ)
# Pass the request along; if handler is None, report a 404
if handler is None:
raise status.HTTPNotFound ()
else:
return handler (environ, start_response)
[docs]def always (handler):
"""Simplest get_arc_handler choice - choose a constant handler.
:param handler: the WSGI handler to which to delegate all requests.
This function applied to a handler is typically passed to the
get_arc_handler of delegate_value and is intended primarily for use with
handlers resulting from that function.
The result of this function simply returns the specified handler,
regardless of the URL path arc or the context.
"""
return lambda arc_object, **params: handler
def _default_convert_arc (arc, **params):
"""Default arc conversion function.
The default is to keep unchanged the decoded bytes from the URL.
"""
return arc
[docs]def make_int_from_arc (width=1):
"""Convert an integer expressed canonically as a string into an int.
This essentially wraps int(), converting ValueError (parsing errors) into
None. Additionally, the input must be left-0-padded to the specified
width, and if the input is wider than the specified width, it must not
begin with a '0'. The combined effect of these rules ensures that each
non-negative integer has exactly one acceptable representation. Anything
that has the wrong number of leading zeroes or which does not parse as an
integer will return None.
"""
# Allowing **params allows direct use as convert_arc for DelegatePathArc
def int_from_arc (arc, **params):
try:
result = int (arc)
except ValueError:
return None
if len (arc) < width:
return None
if len (arc) > width and arc[0] == '0':
return None
return result
return int_from_arc
int_from_arc = make_int_from_arc (1)
def _default_save_arc (arc_object, environ):
"""Default routine to save arc information in the request.
By default we just ignore the arc information.
"""
pass
_default_get_arc_handler = always (None)
"""Default arc handler.
By default, report a 404 on all child nodes.
"""
[docs]def file_dir_redirect (environ, start_response):
"""WSGI handler to redirect file URL to corresponding directory URL.
This implements the standard web server behaviour of redirecting a URL
without an ending '/' that corresponds to a directory to the same URL
with '/' appended.
"""
raise status.HTTPFound ('%s%s/' %
(environ['SCRIPT_NAME'], environ['PATH_INFO']))
[docs]class DelegatePathArc (DelegateHandler):
"""Class for WSGI handlers which delegate based on a URL path arc.
:param dir_handler: The handler for requests with a slash '/' at the end.
:param file_handler: The handler for requests without a slash '/' at the
end.
:param get_arc_handler(arc_object,\*\*params): get the handler for a path
with an arc that translates to arc_object.
:param convert_arc(arc,\*\*params): convert the raw arc value into an
object.
:param save_arc(arc_object,environ): modify environ to store the
arc_object in the appropriate way.
"""
def __init__ (self, dir_handler, file_handler,
convert_arc=None, save_arc=None, get_arc_handler=None):
self.__dir_handler = dir_handler
self.__file_handler = file_handler
self.__convert_arc = convert_arc or _default_convert_arc
self.__save_arc = save_arc or _default_save_arc
self.__get_arc_handler = get_arc_handler or _default_get_arc_handler
@property
def dir_handler (self):
return self.__dir_handler
@property
def file_handler (self):
return self.__file_handler
@property
def convert_arc (self):
return self.__convert_arc
@property
def save_arc (self):
return self.__save_arc
@property
def get_arc_handler (self):
return self.__get_arc_handler
[docs] def get_handler (self, environ):
"""Get the handler which will be used to handle the specified request.
The environ is the environ WSGI parameter. It is updated as when
actually handling the request.
"""
arc = wsgiref.util.shift_path_info (environ)
if arc is None:
return self.file_handler
elif arc == '':
return self.dir_handler
else:
# Really should un-%-encode arc here
# note: don't convert to Unicode here, just to raw bytes
arc_object = self.convert_arc (arc,
**parameter.get_params (environ))
if arc_object is None:
return None
self.save_arc (arc_object, environ)
return self.get_arc_handler (arc_object,
**parameter.get_params (environ))
[docs]def delegate_file_only (handler):
"""Delegate only file requests to the provided handler.
All file requests with no path information are delegated to the provided
handler. All other requests (directory URL or if there are additional
URL path arcs) result in a 404 error.
"""
return DelegatePathArc (None, handler)
[docs]def delegate_action (dir_handler, handler_dict, file_handler=file_dir_redirect):
"""Delegate to a handler for a specific action based on the arc value.
:param dir_handler: the handler to use when no (remaining) URL path arcs
are present and the URL is a directory URL (ending in '/').
:param handler_dict: a dictionary from URL path arc values to handlers.
:param file_handler: the handler to use when no (remaining) URL path arcs
are present and the URL is a file URL (not ending in '/').
Looks up the arc value in the handler_dict and delegates to the
corresponding handler, or serve a 404 if not found.
"""
def get_arc_handler (arc, **params):
result = handler_dict.get (arc)
return result
return DelegatePathArc (dir_handler, file_handler,
get_arc_handler=get_arc_handler)
[docs]def delegate_value (name, dir_handler, get_arc_handler,
file_handler=file_dir_redirect, convert_arc=None):
"""Delegate to an inner handler, storing the arc in the parameters.
:param name: the name under which the converted arc value should be stored
in the context.
:param dir_handler: the handler to use when no (remaining) URL path arcs
are present and the URL is a directory URL (ending in '/').
:param get_arc_handler: a function that returns the handler to use when
there are remaining URL path arcs.
:param file_handler: the handler to use when no (remaining) URL path arcs
are present and the URL is a file URL (not ending in '/').
:param convert_arc: a function which converts the next URL path arc into
a value which gets stored in the context.
The arc value is converted into a parameter value and stored in the handler
parameters under the provided name. Usually get_arc_handler should just
be "always (h)" where h is the appropriate handler. The flexibility
allowed by making this parameter a function rather than simply being the
required handler allows for completely different handlers to be used
depending on the URL path arc value. In practice this capability has
tended not to be used with delegate_value.
"""
def save_arc (arc_object, environ):
parameter.set_param (environ, name, arc_object)
return DelegatePathArc (dir_handler, file_handler,
convert_arc, save_arc, get_arc_handler)
# Want dates, not just year/month/day numbers
make_year_from_arc = make_int_from_arc (4)
make_month_from_arc = make_int_from_arc (2)
make_day_from_arc = make_int_from_arc (2)
[docs]def date_delegate (index_handler, year_handler=None, month_handler=None,
day_handler=None, prefix=None):
"""Parse dates out of URL structure and delegate to year/month/day handlers.
:param index_handler: the handler for the root /.
:param year_handler: the handler for /YYYY/.
:param month_handler: the handler for /YYYY/MM/.
:param day_handler: the handler for /YYYY/MM/DD/.
:param prefix: prefix to use for storing dates in the context.
Constructs WSGI handlers which analyze a date included in the URL.
Requests for the root of the portion of the application handled by this
handler are delegated to index_handler. Requests containing just a year
number are delegated to year_handler. Requests containing just a year
and month are delegated to month_handler. Requests containing a full
date are delegated to day_handler.
The structure can be truncated by leaving some of the later parameters
as None. To delegate only based on year, leave month_handler and
day_handler as None; to delegate only to months leave day_handler as None.
"""
if prefix is None:
prefix = ''
else:
prefix = prefix + '_'
year_name = prefix + 'year'
month_name = prefix + 'month'
day_name = prefix + 'day'
def convert_day (arc, **params):
day = make_day_from_arc (arc)
if day is None:
return None
try:
return params[month_name].replace (day=day)
except ValueError:
return None
if day_handler is not None:
month_handler = delegate_value (day_name, month_handler,
always (day_handler), convert_arc=convert_day)
def convert_month (arc, **params):
month = make_month_from_arc (arc)
if month is None:
return None
try:
return params[year_name].replace (month=month)
except ValueError:
return None
if month_handler is not None:
year_handler = delegate_value (month_name, year_handler,
always (month_handler), convert_arc=convert_month)
def convert_year (arc, **params):
year = make_year_from_arc (arc)
if year is None:
return None
return datetime.date (year, 1, 1)
if year_handler is not None:
index_handler = delegate_value (year_name, index_handler,
always (year_handler), convert_arc=convert_year)
return index_handler
[docs]class DelegateMethod (DelegateHandler):
"""Delegate to an inner handler based on the HTTP method.
Takes a dictionary mapping HTTP methods to handlers. If the requested
method is not in the dictionary, automatically raises a 405 Method
Not Allowed HTTP Error.
"""
def __init__ (self, handler_dict):
self.__handler_dict = handler_dict
@property
def handler_dict (self):
return self.__handler_dict
[docs] def get_handler (self, environ):
request_method = environ['REQUEST_METHOD']
if request_method in self.handler_dict:
return self.handler_dict[request_method]
else:
return status.HTTPMethodNotAllowed.handler (list(self.handler_dict.keys ()))
[docs]def delegate_get_post (get_handler=None, post_handler=None):
"""Convenience procedure for most common use of DelegateMethod.
:param get_handler: the handler to use for HTTP GET requests.
:param post_handler: the handler to use for HTTP POST requests.
It is frequently convenient to use this as a function decorator, in which
case it has the effect of making the decorated handler function into a
GET-only handler.
"""
handler_dict = {}
if get_handler is not None:
handler_dict['GET'] = get_handler
if post_handler is not None:
handler_dict['POST'] = post_handler
return DelegateMethod (handler_dict)