Source code for uw.web.wsgi.delegate

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