========== User Guide ========== :ref:`weblayer` is made up of a number of components. You can use them "out of the box", as shown by the :ref:`helloworld` example, or you can pick and choose from and override them, as introduced in the :ref:`Components` section. You can then take advantage of the :ref:`request handler api` when writing your web application. .. _helloworld: Hello World =========== `helloworld.py`_ shows how to start writing a web application using :ref:`weblayer`'s default configuration: .. literalinclude:: ../src/weblayer/examples/helloworld.py :lines: 7- Let's walk through it. First up, we import :py:class:`~weblayer.bootstrap.Bootstrapper`, :py:class:`~weblayer.request.RequestHandler` and :py:class:`~weblayer.wsgi.WSGIApplication`: .. literalinclude:: ../src/weblayer/examples/helloworld.py :lines: 7 Handling Requests ----------------- We then see ``Hello``, a simple request handler (aka "view") that subclasses the :py:class:`~weblayer.request.RequestHandler` class we imported: .. literalinclude:: ../src/weblayer/examples/helloworld.py :lines: 9-13 ``Hello`` defines a single method: ``get``, which will be called when ``Hello`` receives an `HTTP GET request`_. .. note:: By default, request handlers accept GET and HEAD requests (i.e.: they are theoretically read only). You can explicitly specify which methods of each request handler should be exposed using the ``__all__`` property. For example, to handle HEAD, GET, POST and DOFOO requests, you might write something like:: class Hello2(RequestHandler): """ I explicitly accept only HEAD, GET, POST and DOFOO requests. """ __all__ = ('head', 'get', 'post', 'dofoo') def get(self): form = u'
' return u'What is your name? %s' % form def post(self): return u'Hello %s!' % self.request.params.get('name') def dofoo(self): return u'I just did foo!' .. note:: In the above example, HEAD requests will be handled by the ``def get()`` method. This special case is carried through from the underlying `webob.Response`_ implementation (and is documented in :py:meth:`~weblayer.method.ExposedMethodSelector.select_method`). In most cases, the trick is simply to remember to include ``'head'`` in your ``__all__`` list of exposed methods wherever you expose ``'get'``. .. note:: In order to protect against XSRF attacks, POST Requests (that are not XMLHttpRequest requests) are validated to check for the presence and value of an ``_xsrf`` parameter. You can include this in your forms using the :py:attr:`~weblayer.request.RequestHandler.xsrf_input` (available as ``xrsf_input`` in your templates, e.g.::
${xsrf_input}
You can disable XSRF validation by setting ``check_xsrf`` to ``False`` in your application level :py:mod:`~weblayer.settings` and / or by overriding the ``check_xsrf`` :py:class:`~weblayer.request.RequestHandler` class attribute, e.g.:: class Hello3(RequestHandler): """ I don't validate POST requests against XSRF request forgery. """ check_xsrf = False Handlers are mapped to incoming requests using the incoming request path. This mapping takes the form of a list of tuples where the first item in the tuple is a `regular expression`_ and the second is a :py:class:`~weblayer.request.RequestHandler` class. In this case, we map ``Hello`` to all incoming requests: .. literalinclude:: ../src/weblayer/examples/helloworld.py :lines: 15 The `groups`_ in the `regular expression`_ (i.e.: the parts with parenthesis around them) that match the request path are passed to the appropriate method of the request handler as arguments. So, in this case, an `HTTP GET request`_ to ``/foo`` will yield one match group, ``'foo'`` which is passed into ``Hello.get`` as the positional argument ``world``, resulting in the response ``u'hello foo'``. You can see this for yourself by running:: weblayer-demo And then opening http://localhost:8080/foo in a web browser. .. note:: The pattern of using an explicit, ordered mapping of regular expressions to request handlers is used by many frameworks, including Google App Engine's `webapp`_ framework. Other common patterns include `routes`_ and `traversal`_, sometimes used in tandem with `declarative configuration`_ and / or `decorators`_. :ref:`weblayer` avoids declarative configuration by default. Decorators are not explicit and can introduce problems, as `explained here`_. Regular expressions are preferred over `routes`_ as they are both more powerful and an essential part of any Python developer's toolbox. It seems strange to invent another tool for the job when such a good one already exists. Finally, `traversal`_ implies there is an object graph to traverse, which is not always the case. You may, of course, disagree with this analysis and :ref:`override ` the :py:class:`~weblayer.interfaces.IPathRouter` implementation as you see fit. Bootstrapping ------------- Carrying on through the example, we next hardcode three configuration settings that are required by default: .. literalinclude:: ../src/weblayer/examples/helloworld.py :lines: 17-21 We then initialise a :py:class:`~weblayer.bootstrap.Bootstrapper` with these configuration settings and the url mapping we made earlier and use it to bootstrap a :py:class:`~weblayer.wsgi.WSGIApplication`: .. literalinclude:: ../src/weblayer/examples/helloworld.py :lines: 23-24 .. note:: The :py:class:`~weblayer.bootstrap.Bootstrapper` is similar to `repoze.bfg's Configurator`_ in that it allows for imperative configuration of components. .. note:: :py:mod:`~weblayer.settings` implements a pattern of optional explicit declaration of settings that is inspired by `tornado.options`_. Explicitly requiring settings allows the application to throw an error on initialisation, rather than further down the line (e.g.: when a request happens to come in). If you choose to, you can explicitly require your own settings by calling :py:func:`~weblayer.settings.require_setting` at module level. For example, the ``cookie_secret`` requirement is defined at the top of :py:mod:`weblayer.cookie` using: .. literalinclude:: ../src/weblayer/cookie.py :lines: 42 Because :py:func:`~weblayer.settings.require_setting` works in tandem with a `venusian scan`_ to prevent `duplicate import issues`_, to require your own settings, you must tell :ref:`weblayer` to scan the modules you've required them in. This can be done most simply by passing in a list of dotted names of modules or packages using the ``packages`` keyword argument to :py:meth:`Bootstrapper.__call__ `. For example, to require all settings declared using :py:func:`~weblayer.settings.require_setting` in modules in the ``my.webapp`` and ``some.dependency`` packages, use:: application = WSGIApplication( *bootstrapper(packages=['my.webapp', 'some.dependency',]) ) Serving ------- Finally, the remainder of the example takes care of serving the example application on http://localhost:8080: .. literalinclude:: ../src/weblayer/examples/helloworld.py :lines: 26- For more realistic setups, see the :ref:`Deployment` recipes. .. _components: Components ========== Architecture ------------ :ref:`weblayer` uses the `Zope Component Architecture`_ under the hood. Individual components are said to `implement`_ one of :py:mod:`weblayer.interfaces`, listed in ``weblayer.interfaces.__all__``: .. literalinclude:: ../src/weblayer/interfaces.py :lines: 12-25 For example, :py:class:`~weblayer.route.RegExpPathRouter`:: class RegExpPathRouter(object): """ Routes paths to request handlers using regexp patterns. """ implements(IPathRouter) # ``__init__`` method removed from this example for brevity def match(self, path): for regexp, handler_class in self._mapping: match = regexp.match(path) if match: return handler_class, match.groups(), {} return None, None, None Is one particular implementation of :py:class:`~weblayer.interfaces.IPathRouter`:: class IPathRouter(Interface): """ Maps incoming requests to request handlers using the request path. """ def match(path): """ Return ``handler, args, kwargs`` from ``path``. """ Default Implementations ----------------------- The default implementations are as follows: * :py:class:`~weblayer.interfaces.IAuthenticationManager` is implemented by :py:class:`~weblayer.auth.TrivialAuthenticationManager` * :py:class:`~weblayer.interfaces.IMethodSelector` is implemented by :py:class:`~weblayer.method.ExposedMethodSelector` * :py:class:`~weblayer.interfaces.IPathRouter` is implemented by :py:class:`~weblayer.route.RegExpPathRouter` * :py:class:`~weblayer.interfaces.IRequest` is implemented by :py:class:`~weblayer.base.Request` * :py:class:`~weblayer.interfaces.IRequestHandler` is implemented by :py:class:`~weblayer.request.RequestHandler` * :py:class:`~weblayer.interfaces.IResponse` is implemented by :py:class:`~weblayer.base.Response` * :py:class:`~weblayer.interfaces.IResponseNormaliser` is implemented by :py:class:`~weblayer.normalise.DefaultToJSONResponseNormaliser` * :py:class:`~weblayer.interfaces.ISecureCookieWrapper` is implemented by :py:class:`~weblayer.cookie.SignedSecureCookieWrapper` * :py:class:`~weblayer.interfaces.ISettings` is implemented by :py:class:`~weblayer.settings.RequirableSettings` * :py:class:`~weblayer.interfaces.IStaticURLGenerator` is implemented by :py:class:`~weblayer.static.MemoryCachedStaticURLGenerator` * :py:class:`~weblayer.interfaces.ITemplateRenderer` is implemented by :py:class:`~weblayer.template.MakoTemplateRenderer` * :py:class:`~weblayer.interfaces.IWSGIApplication` is implemented by :py:class:`~weblayer.wsgi.WSGIApplication` Workflow -------- Each application requires an :py:class:`~weblayer.interfaces.ISettings` implementation and an :py:class:`~weblayer.interfaces.IPathRouter`. These are passed in to your :py:class:`~weblayer.interfaces.IWSGIApplication` when it is initialised, most commonly using the :py:class:`~weblayer.bootstrap.Bootstrapper`. When HTTP requests come in to your application, :py:class:`~weblayer.interfaces.IWSGIApplication` uses the :py:class:`~weblayer.interfaces.IPathRouter` to map the incoming requests to an :py:class:`~weblayer.interfaces.IRequestHandler` that is instantiated with an :py:class:`~weblayer.interfaces.IRequest`, :py:class:`~weblayer.interfaces.IResponse` and the :py:class:`~weblayer.interfaces.ISettings`. The :py:class:`~weblayer.interfaces.IRequestHandler` then uses the :py:class:`~weblayer.interfaces.IMethodSelector` to select which of its methods (``def get()``, ``def post()`` etc.) to call to handle the request. The method is then called with ``*args`` and ``**kwargs`` derived from the incoming request path by the :py:class:`~weblayer.interfaces.IPathRouter`. When writing :py:class:`~weblayer.interfaces.IRequestHandler` code, you can take advantage of the :ref:`request handler api` to access your :py:class:`~weblayer.interfaces.IStaticURLGenerator` at ``self.static``, your :py:class:`~weblayer.interfaces.IAuthenticationManager` at ``self.auth`` and your :py:class:`~weblayer.interfaces.ISecureCookieWrapper` at ``self.cookies``. Your :py:class:`~weblayer.interfaces.ITemplateRenderer` is available through ``self.render()``. The return value of your handler method is passed to your :py:class:`~weblayer.interfaces.IResponseNormaliser`, which uses it to either replace or update the :py:class:`~weblayer.interfaces.IResponse` originally passed in to your :py:class:`~weblayer.interfaces.IRequestHandler` before the :py:class:`~weblayer.interfaces.IResponse` is called to provide a `WSGI`_ compliant response from your application. Overriding ---------- Alternative component implementations need to declare that they implement the appropriate interface and provide the attributes and methods that the interface specifies. For example, an alternative :py:class:`~weblayer.interfaces.IPathRouter` implementation needs to provide a ``match(path)`` method, e.g.:: class LazyPathRouter(object): """ Never even bothers trying. """ implements(IPathRouter) def match(self, path): return None, None, None The simplest way to then register this component is using the :py:class:`~weblayer.bootstrap.Bootstrapper` when bootstrapping the :py:class:`~weblayer.wsgi.WSGIApplication`. The `override/path_router.py`_ example shows how: .. literalinclude:: ../src/weblayer/examples/override/path_router.py :lines: 12- If you then run this, all requests will meet with a 404 response:: $ python src/weblayer/examples/override_path_router.py ... "GET / HTTP/1.1" 404 0 ... "GET /foo HTTP/1.1" 404 0 You can see two further examples at `override/authentication_manager.py`_ and `override/template_renderer.py`_ .. note:: Using the :py:class:`~weblayer.bootstrap.Bootstrapper` to register components is entirely optional. You can register components manually using (or even by monkey patching) the :py:mod:`weblayer.component` ``registry``. .. _`request handler api`: Request Handler API =================== :py:class:`~weblayer.request.RequestHandler` provides the following useful attributes and methods: * ``self.request`` is an :py:class:`~weblayer.interfaces.IRequest` instance encapsulating the incoming HTTP request * ``self.response`` is an :py:class:`~weblayer.interfaces.IResponse` instance you can choose to manipulate and return (or update indirectly through the :py:class:`~weblayer.interfaces.IResponseNormaliser`) * ``self.settings`` is an :py:class:`~weblayer.interfaces.ISettings` instance that provides dictionary-like access to your :py:mod:`~weblayer.settings` * ``self.auth`` is an :py:class:`~weblayer.interfaces.IAuthenticationManager` instance that provides :py:attr:`~weblayer.interfaces.IAuthenticationManager.is_authenticated` and :py:attr:`~weblayer.interfaces.IAuthenticationManager.current_user` properties * ``self.cookies`` is an :py:class:`~weblayer.interfaces.ISecureCookieWrapper` instance that provides methods to :py:meth:`~weblayer.interfaces.ISecureCookieWrapper.set` and :py:meth:`~weblayer.interfaces.ISecureCookieWrapper.get` secure cookies * ``self.static`` is an an :py:class:`~weblayer.interfaces.IStaticURLGenerator` instance that provides a :py:meth:`~weblayer.interfaces.IStaticURLGenerator.get_url` method to generate static URLs * ``self.xsrf_input`` is an html ```` element you can include in forms to protect against XSRF attacks * return ``self.error()`` to return an HTTP error * return ``self.redirect()`` to redirect the request * return ``self.render()`` to return a rendered template .. note:: When you use ``self.render()``, it passes through any keyword arguments you provide, along with ``self.request`` as ``request``, ``self.auth.current_user`` as ``current_user``, ``self.static.get_url()`` as ``get_static_url()`` and ``self.xsrf_input`` as ``xsrf_input`` to the template namespace (along with any built-ins your :py:class:`~weblayer.interfaces.ITemplateRenderer` implementation provides). .. _`helloworld.py`: http://github.com/thruflo/weblayer/tree/master/src/weblayer/examples/helloworld.py .. _`override/path_router.py`: http://github.com/thruflo/weblayer/tree/master/src/weblayer/examples/override/path_router.py .. _`override/authentication_manager.py`: http://github.com/thruflo/weblayer/tree/master/src/weblayer/examples/override/authentication_manager.py .. _`override/template_renderer.py`: http://github.com/thruflo/weblayer/tree/master/src/weblayer/examples/override/template_renderer.py .. _`webob.Response`: http://pythonpaste.org/webob/reference.html#id2 .. _`HTTP GET request`: http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3 .. _`regular expression`: http://docs.python.org/library/re.html .. _`groups`: http://docs.python.org/library/re.html#re.MatchObject.groups .. _`webapp`: http://code.google.com/appengine/docs/python/tools/webapp/ .. _`routes`: http://routes.groovie.org/ .. _`traversal`: http://docs.repoze.org/bfg/narr/traversal.html .. _`declarative configuration`: http://docs.repoze.org/bfg/1.2/narr/configuration.html .. _`decorators`: http://bottle.paws.de/docs/dev/tutorial.html#routing .. _`explained here`: http://docs.repoze.org/bfg/current/designdefense.html#application-programmers-don-t-control-the-module-scope-codepath-import-time-side-effects-are-evil .. _`duplicate import issues`: http://docs.repoze.org/bfg/current/designdefense.html#application-programmers-don-t-control-the-module-scope-codepath-import-time-side-effects-are-evil .. _`tornado.options`: https://github.com/facebook/tornado/blob/master/tornado/options.py .. _`venusian scan`: http://docs.repoze.org/venusian/ .. _`repoze.bfg's Configurator`: http://docs.repoze.org/bfg/narr/configuration.html .. _`deployment`: recipes#deployment .. _`implement`: http://pypi.python.org/pypi/zope.interface#declaring-implemented-interfaces .. _`wsgi`: http://en.wikipedia.org/wiki/Web_Server_Gateway_Interface .. _`zope component architecture`: http://pypi.python.org/pypi/zope.component