.. _hooks: Pecan Hooks =========== Although it is easy to use WSGI middleware with Pecan, it can be hard (sometimes impossible) to have access to Pecan's internals from within middleware. Pecan Hooks are a way to interact with the framework, without having to write separate middleware. Hooks allow you to execute code at key points throughout the life cycle of your request: * :func:`~pecan.hooks.PecanHook.on_route`: called before Pecan attempts to route a request to a controller * :func:`~pecan.hooks.PecanHook.before`: called after routing, but before controller code is run * :func:`~pecan.hooks.PecanHook.after`: called after controller code has been run * :func:`~pecan.hooks.PecanHook.on_error`: called when a request generates an exception Implementating a Pecan Hook --------------------------- In the below example, a simple hook will gather some information about the request and print it to ``stdout``. Your hook implementation needs to import :class:`~pecan.hooks.PecanHook` so it can be used as a base class. From there, you'll want to override the :func:`~pecan.hooks.PecanHook.on_route`, :func:`~pecan.hooks.PecanHook.before`, :func:`~pecan.hooks.PecanHook.after`, or :func:`~pecan.hooks.PecanHook.on_error` methods to define behavior. :: from pecan.hooks import PecanHook class SimpleHook(PecanHook): def before(self, state): print "\nabout to enter the controller..." def after(self, state): print "\nmethod: \t %s" % state.request.method print "\nresponse: \t %s" % state.response.status :func:`~pecan.hooks.PecanHook.on_route`, :func:`~pecan.hooks.PecanHook.before`, and :func:`~pecan.hooks.PecanHook.after` are each passed a shared state object which includes useful information, such as the request and response objects, and which controller was selected by Pecan's routing:: class SimpleHook(PecanHook): def on_route(self, state): print "\nabout to map the URL to a Python method (controller)..." assert state.controller is None # Routing hasn't occurred yet assert isinstance(state.request, webob.Request) assert isinstance(state.response, webob.Response) assert isinstance(state.hooks, list) # A list of hooks to apply def before(self, state): print "\nabout to enter the controller..." if state.request.path == '/': # # `state.controller` is a reference to the actual # `@pecan.expose()`-ed controller that will be routed to # and used to generate the response body # assert state.controller.__func__ is RootController.index.__func__ assert isinstance(state.arguments, inspect.Arguments) print state.arguments.args print state.arguments.varargs print state.arguments.keywords assert isinstance(state.request, webob.Request) assert isinstance(state.response, webob.Response) assert isinstance(state.hooks, list) :func:`~pecan.hooks.PecanHook.on_error` is passed a shared state object **and** the original exception. If an :func:`~pecan.hooks.PecanHook.on_error` handler returns a Response object, this response will be returned to the end user and no furthur :func:`~pecan.hooks.PecanHook.on_error` hooks will be executed:: class CustomErrorHook(PecanHook): def on_error(self, state, exc): if isinstance(exc, SomeExceptionType): return webob.Response('Custom Error!', status=500) .. _attaching_hooks: Attaching Hooks --------------- Hooks can be attached in a project-wide manner by specifying a list of hooks in your project's configuration file. :: app = { 'root' : '...' # ... 'hooks': lambda: [SimpleHook()] } Hooks can also be applied selectively to controllers and their sub-controllers using the :attr:`__hooks__` attribute on one or more controllers and subclassing :class:`~pecan.hooks.HookController`. :: from pecan import expose from pecan.hooks import HookController from my_hooks import SimpleHook class SimpleController(HookController): __hooks__ = [SimpleHook()] @expose('json') def index(self): print "DO SOMETHING!" return dict() Now that :class:`SimpleHook` is included, let's see what happens when we run the app and browse the application from our web browser. :: pecan serve config.py serving on 0.0.0.0:8080 view at http://127.0.0.1:8080 about to enter the controller... DO SOMETHING! method: GET response: 200 OK Hooks can be inherited from parent class or mixins. Just make sure to subclass from :class:`~pecan.hooks.HookController`. :: from pecan import expose from pecan.hooks import PecanHook, HookController class ParentHook(PecanHook): priority = 1 def before(self, state): print "\nabout to enter the parent controller..." class CommonHook(PecanHook): priority = 2 def before(self, state): print "\njust a common hook..." class SubHook(PecanHook): def before(self, state): print "\nabout to enter the subcontroller..." class SubMixin(object): __hooks__ = [SubHook()] # We'll use the same instance for both controllers, # to avoid double calls common = CommonHook() class SubController(HookController, SubMixin): __hooks__ = [common] @expose('json') def index(self): print "\nI AM THE SUB!" return dict() class RootController(HookController): __hooks__ = [common, ParentHook()] @expose('json') def index(self): print "\nI AM THE ROOT!" return dict() sub = SubController() Let's see what happens when we run the app. First loading the root controller: :: pecan serve config.py serving on 0.0.0.0:8080 view at http://127.0.0.1:8080 GET / HTTP/1.1" 200 about to enter the parent controller... just a common hook I AM THE ROOT! Then loading the sub controller: :: pecan serve config.py serving on 0.0.0.0:8080 view at http://127.0.0.1:8080 GET /sub HTTP/1.1" 200 about to enter the parent controller... just a common hook about to enter the subcontroller... I AM THE SUB! .. note:: Make sure to set proper priority values for nested hooks in order to get them executed in the desired order. .. warning:: Two hooks of the same type will be added/executed twice, if passed as different instances to a parent and a child controller. If passed as one instance variable - will be invoked once for both controllers. Hooks That Come with Pecan -------------------------- Pecan includes some hooks in its core. This section will describe their different uses, how to configure them, and examples of common scenarios. .. _requestviewerhook: RequestViewerHook ''''''''''''''''' This hook is useful for debugging purposes. It has access to every attribute the ``response`` object has plus a few others that are specific to the framework. There are two main ways that this hook can provide information about a request: #. Terminal or logging output (via an file-like stream like ``stdout``) #. Custom header keys in the actual response. By default, both outputs are enabled. .. seealso:: * :ref:`pecan_hooks` Configuring RequestViewerHook ............................. There are a few ways to get this hook properly configured and running. However, it is useful to know that no actual configuration is needed to have it up and running. By default it will output information about these items: * path : Displays the url that was used to generate this response * status : The response from the server (e.g. '200 OK') * method : The method for the request (e.g. 'GET', 'POST', 'PUT or 'DELETE') * controller : The actual controller method in Pecan responsible for the response * params : A list of tuples for the params passed in at request time * hooks : Any hooks that are used in the app will be listed here. The default configuration will show those values in the terminal via ``stdout`` and it will also add them to the response headers (in the form of ``X-Pecan-item_name``). This is how the terminal output might look for a `/favicon.ico` request:: path - /favicon.ico status - 404 Not Found method - GET controller - The resource could not be found. params - [] hooks - ['RequestViewerHook'] In the above case, the file was not found, and the information was printed to `stdout`. Additionally, the following headers would be present in the HTTP response:: X-Pecan-path /favicon.ico X-Pecan-status 404 Not Found X-Pecan-method GET X-Pecan-controller The resource could not be found. X-Pecan-params [] X-Pecan-hooks ['RequestViewerHook'] The configuration dictionary is flexible (none of the keys are required) and can hold two keys: ``items`` and ``blacklist``. This is how the hook would look if configured directly (shortened for brevity):: ... 'hooks': lambda: [ RequestViewerHook({'items':['path']}) ] Modifying Output Format ....................... The ``items`` list specify the information that the hook will return. Sometimes you will need a specific piece of information or a certain bunch of them according to the development need so the defaults will need to be changed and a list of items specified. .. note:: When specifying a list of items, this list overrides completely the defaults, so if a single item is listed, only that item will be returned by the hook. The hook has access to every single attribute the request object has and not only to the default ones that are displayed, so you can fine tune the information displayed. These is a list containing all the possible attributes the hook has access to (directly from `webob`): ====================== ========================== ====================== ========================== accept make_tempfile accept_charset max_forwards accept_encoding method accept_language params application_url path as_string path_info authorization path_info_peek blank path_info_pop body path_qs body_file path_url body_file_raw postvars body_file_seekable pragma cache_control query_string call_application queryvars charset range content_length referer content_type referrer cookies relative_url copy remote_addr copy_body remote_user copy_get remove_conditional_headers date request_body_tempfile_limit decode_param_names scheme environ script_name from_file server_name from_string server_port get_response str_GET headers str_POST host str_cookies host_url str_params http_version str_postvars if_match str_queryvars if_modified_since unicode_errors if_none_match upath_info if_range url if_unmodified_since urlargs is_body_readable urlvars is_body_seekable uscript_name is_xhr user_agent make_body_seekable ====================== ========================== And these are the specific ones from Pecan and the hook: * controller * hooks * params (params is actually available from `webob` but it is parsed by the hook for redability) Blacklisting Certain Paths .......................... Sometimes it's annoying to get information about *every* single request. To limit the output, pass the list of URL paths for which you do not want data as the ``blacklist``. The matching is done at the start of the URL path, so be careful when using this feature. For example, if you pass a configuration like this one:: { 'blacklist': ['/f'] } It would not show *any* url that starts with ``f``, effectively behaving like a globbing regular expression (but not quite as powerful). For any number of blocking you may need, just add as many items as wanted:: { 'blacklist' : ['/favicon.ico', '/javascript', '/images'] } Again, the ``blacklist`` key can be used along with the ``items`` key or not (it is not required).