Source code for findig.dispatcher

from functools import singledispatch
import warnings
import traceback

from werkzeug.exceptions import HTTPException
from werkzeug.routing import Rule
from werkzeug.wrappers import Response, BaseResponse

from findig.content import ErrorHandler, Formatter, Parser
from findig.context import ctx
from findig.resource import Resource, AbstractResource
from findig.utils import DataPipe

[docs]class Dispatcher: """ A :class:`Dispatcher` creates resources and routes requests to them. :param formatter: A function that converts resource data to a string string suitable for output. It returns a 2-tuple: *(mime_type, output)*. If not given, a generic :class:`findig.content.Formatter` is used. :param parser: A function that parses request input and returns a 2-tuple: *(mime_type, data)*. If not given, a generic :class:`findig.content.Parser`. :param error_handler: A function that converts an exception to a :class:`Response <werkzeug.wrappers.BaseResponse>`. If not given, a generic :class:`findig.content.ErrorHandler` is used. :param pre_processor: A function that is called on request data just after is is parsed. :param post_processor: A function that is called on resource data just before it is formatted. This class is fairly low-level and shouldn't be instantiated directly in application code. It does however serve as a base for :class:`findig.App`. """ #: A class that is used to construct responses after they're #: returned from formatters. response_class = Response def __init__(self, formatter=None, parser=None, error_handler=None, pre_processor=None, post_processor=None): self.route = singledispatch(self.route) self.route.register(str, self.route_decorator) if error_handler is None: error_handler = ErrorHandler() error_handler.register(BaseException, self._handle_exception) error_handler.register(HTTPException, self._handle_http_exception) if parser is None: parser = Parser() if formatter is None: formatter = Formatter() formatter.register('text/plain', str, default=True) self.formatter = formatter self.parser = parser self.error_handler = error_handler self.pre_processor = DataPipe() if pre_processor is None else pre_processor self.post_processor = DataPipe() if post_processor is None else post_processor self.resources = {} self.routes = [] self.endpoints = {} def _handle_exception(self, err): # TODO: log error traceback.print_exc() return Response("An internal application error has been logged.", status=500) def _handle_http_exception(self, http_err): response = http_err.get_response(ctx.request) headers = response.headers del headers['Content-Type'] del headers['Content-Length'] return Response(http_err.description, status=response.status, headers=response.headers)
[docs] def resource(self, wrapped=None, **args): """ resource(wrapped, **args) Create a :class:`findig.resource.Resource` instance. :param wrapped: A wrapped function for the resource. In most cases, this should be a function that takes named route arguments for the resource and returns a dictionary with the resource's data. The keyword arguments are passed on directly to the constructor for :class:`Resource`, with the exception that *name* will default to {module}.{name} of the wrapped function if not given. This method may also be used as a decorator factory:: @dispatcher.resource(name='my-very-special-resource') def my_resource(route, param): return {'id': 10, ... } In this case the decorated function will be replaced by a :class:`Resource` instance that wraps it. Any keyword arguments passed to the decorator factory will be handed over to the :class:`Resource` constructor. If no keyword arguments are required, then ``@resource`` may be used instead of ``@resource()``. .. note:: If this function is used as a decorator factory, then a keyword parameter for *wrapped* must not be used. """ def decorator(wrapped): args['wrapped'] = wrapped args.setdefault( 'name', "{0.__module__}.{0.__qualname__}".format(wrapped)) resource = Resource(**args) self.resources[resource.name] = resource return resource if wrapped is not None: return decorator(wrapped) else: return decorator
[docs] def route(self, resource, rulestr, **ruleargs): """ Add a route to a resource. Adding a URL route to a resource allows Findig to dispatch incoming requests to it. :param resource: The resource that the route will be created for. :type resource: :class:`Resource` or function :param rulestr: A URL rule, according to :ref:`werkzeug's specification <werkzeug:routing>`. :type rulestr: str See :py:class:`werkzeug.routing.Rule` for valid rule parameters. This method can also be used as a decorator factory to assign route to resources using declarative syntax:: @route("/index") @resource(name='index') def index_generator(): return ( ... ) """ if not isinstance(resource, AbstractResource): resource = self.resource(resource) self.routes.append((resource, rulestr, ruleargs)) return resource
def route_decorator(self, rulestr, **ruleargs): #See :meth:`route`. def decorator(resource): # Collect the rule resource = self.route(resource, rulestr, **ruleargs) # return the resource return resource return decorator
[docs] def build_rules(self): """ Return a generator for all of the url rules collected by the :class:`Dispatcher`. :rtype: Iterable of :class:`werkzeug.routing.Rule` .. note:: This method will 'freeze' resource names; do not change resource names after this function is invoked. """ self.endpoints.clear() # Refresh the resource dict so that up-to-date resource names # are used in dictionaries self.resources = dict((r.name, r) for r in self.resources.values()) # Build the URL rules for resource, string, args in self.routes: # Set up the callback endpoint args.setdefault('endpoint', resource.name) self.endpoints[args['endpoint']] = resource # And the supported methods supported_methods = resource.get_supported_methods() restricted_methods = set( map(str.upper, args.get('methods', supported_methods))) args['methods'] = supported_methods.intersection(restricted_methods) # warn about unsupported methods unsupported_methods = list(set(restricted_methods) - supported_methods) if unsupported_methods: warnings.warn( "Error building rule: {string}\n" "The following HTTP methods have been declared, but " "are not supported by the data model for {resource.name}: " "{unsupported_methods}.".format(**locals()) ) # Initialize the rule, and yield it yield Rule(string, **args)
def get_resource(self, rule): return self.endpoints[rule.endpoint]
[docs] def dispatch(self): """ Dispatch the current request to the appropriate resource, based on which resource the rule applies to. This function requires an active request context in order to work. """ # TODO: document request context variables. request = ctx.request url_values = ctx.url_values resource = ctx.resource ctx.response = response = {'headers': {}} # response arguments try: data = resource.handle_request(request, url_values) response = {k:v for k,v in response.items() if k in ('status', 'headers')} if isinstance(data, (self.response_class, BaseResponse)): return data elif data is not None: process = DataPipe( getattr(resource, 'post_processor', None), self.post_processor ) data = process(data) format = Formatter.compose( getattr(resource, 'formatter', Formatter()), self.formatter ) mime_type, data = format(data) response['mimetype'] = mime_type response['response'] = data return self.response_class(**response) except BaseException as err: return self.error_handler(err)
@property def unrouted_resources(self): """ A list of resources created by the dispatcher which have no routes to them. """ routed = set() for resource in self.endpoints.values(): if resource.name in self.resources: routed.add(resource.name) else: return list(map(self.resources.get, set(self.resources) - routed))