1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
|
.. _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).
|