.. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
.. % Copyright 2001 by Object Craft P/L, Melbourne, Australia.
.. % LICENCE - see LICENCE file distributed with this software for details.
.. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
.. _app-guide:
******************************
Guide to Building Applications
******************************
Roughly speaking, every page or document sent from a web server to a browser is
the result of the same processing sequence. For our purposes a document is one
page in an application.
#. The browser connects to the server and requests a page.
#. The server decodes the browser request and processes it. This can cause all
manner of subsidiary application processing to occur.
#. The server sends the result of processing back to the browser as the next
page in the application.
The essentially stateless nature of the web presents problems for the
application developer who wishes to retain state between different pages in
their application. From the point of view of the user, the state of the
application at the server is represented by the page that they see in their
browser window(s). When they enter values and press submit buttons on their
page they (quite reasonably) expect the application on the web server to process
their request and return the next page in the application.
The point of view of the web application developer is very different. At the
server side the application must be able to receive a request from one browser,
process it and send the next application page back to that browser. Before
seeing the next request from the same browser, another browser may request a
different page from the application. The web application even has to deal with
multiple browsers simultaneously accessing different pages in the application.
All of this while maintaining the illusion for the end user that they are
running their own copy of the application.
Even when you only have a single machine running Apache, there are multiple
Apache processes on that machine that are receiving and processing browser
requests. This means that there is no guarantee that the same process that
received the last request from a particular browser will receive the next
request from that browser. If your application requires some state to be
retained between pages, there has to be a way for that state to migrate between
different Apache processes. If you are using more than one machine to process
requests, then you need some way for the application state to move between
machines.
There are essentially three approaches to solving the state propagation problem.
#. Deploy a stateless application. Only the most trivial applications do not
require state to be retained across browser requests.
#. Get the browser to store the application state. This can be done by
embedding information in URL's and hidden fields in forms. When the browser
requests the next page the application restores state by extracting it from the
hidden field values sent back by the browser. Browser cookies can also be used
to store some types of application state.
#. Get the browser to store a session identifier in either URL's, hidden fields,
or a cookie. When the browser requests the next page the application uses the
browser supplied session identifier to locate the state in a server-side session
database of some description.
Assuming that your application is non-trivial, the second approach (state held
by the browser) is the easiest to implement.
If you are building a financial application, you will probably feel more
comfortable with the third approach. This has the advantage of hiding
implementation details of your application from prying eyes.
All three approaches are supported (in one way or another) by Albatross.
In all Albatross applications there are two important objects that determine how
browser requests are processed; the application object, and the execution
context object. The application is a potentially long lived object that
provides generic services for multiple browser requests. A new execution
context is created to process each browser request.
One of the things that the application and execution context do is cooperate to
provide session functionality. The execution context is the object where
session data is created and manipulated, but the application is usually
responsible to loading and saving the session. This allows the application to
maintain a long lived connection with a server if necessary.
.. _app-model:
Albatross Application Model
===========================
In the Albatross world view explained in the previous section, all web
applications follow the same processing sequence to handle browser requests.
When processing a browser request to generate a response the processing (in most
cases) flows according to figure :ref:`fig-dataflow`.
.. _fig-dataflow:
.. figure:: .build/figures/dataflow.*
:align: center
Request Processing Dataflow
The processing steps are:
#. Capture the browser request in a :class:`Request` object.
#. Pass the :class:`Request` object to the :meth:`run` method of the application
object.
#. Application locates the Python code for processing the browser request.
#. Page processing code runs one or more Albatross templates.
#. Templates contain either standard Albatross tags or application defined
extension tags.
#. As tags are converted to HTML a stream of content fragments is sent to the
execution context.
#. When the execution context content is flushed all of the fragments are joined
together.
#. Joined flushed content is sent to the :class:`Request` object
:meth:`write_content` method.
#. Application response is returned to the browser.
In the Albatross code the processing is driven by the :meth:`run` method of the
:class:`Application` class in the :mod:`app` module.
It is instructive to look at the *exact* code from Albatross that implements the
processing sequence.
.. code-block:: python
ctx = self.create_context()
ctx.set_request(req)
self.load_session(ctx)
self.load_page(ctx)
if self.validate_request(ctx):
self.merge_request(ctx)
self.process_request(ctx)
self.display_response(ctx)
self.save_session(ctx)
ctx.flush_content()
The code is contained within a ``try``/``except`` block to allow the application
to trap and handle exceptions.
The :class:`Application` class assumes very little about the implementation of
each of these steps. The detail of the processing stages is defined by a
collection of mixin classes. A combination of the :class:`Application` class
and a selection of the mixin classes is used to construct your application class
and execution context classes. There are a number of prefabricated applications
and execution contexts, see chapter :ref:`pack-overview`.
This mix and match approach to building the application and execution context
classes provides a great deal of flexibility. Albatross is an application
toolkit, not a deployment platform. You are encouraged to look at the code and
develop your own mixin classes to suit your deployment requirements. One of the
primary goals of Albatross is to keep the toolkit line count low. This reduces
the amount of time you need to spend before you can make your own custom
extensions to the toolkit.
In the previous chapter we talked about the importance of separating the
presentation layer of the application from the implementation as shown in figure
:ref:`fig-presimp`. Albatross HTML templates provide a fairly powerful tool for
achieving that separation.
.. _fig-presimp:
.. figure:: .build/figures/twolayer.*
:align: center
Separation of Presentation/Implementation
The presentation layer consists of a collection of template files that contain
the logic required to display data from the objects contained in the
implementation layer. In Albatross, the line between the two layers is the
execution context.
To make objects available to the presentation layer the application places
references to those objects into the local or global namespace of the execution
context. The local namespace is populated in application code like this:
.. code-block:: python
ctx.locals.mbox = Mbox(ctx.locals.username, ctx.locals.passwd)
ctx.locals.msg = ctx.locals.mbox[int(ctx.locals.msgnum) - 1]
To execute Python expressions inside the template files, the execution context
uses the following Python code from the :class:`NamespaceMixin` class:
.. code-block:: python
def eval_expr(self, expr):
self.locals.__ctx__ = self
try:
return eval(expr, self.__globals, self.locals.__dict__)
finally:
del self.locals.__ctx__
Whenever application code calls the :meth:`run_template` or
:meth:`run_template_once` methods of the :class:`NamespaceMixin` class Albatross
sets the global namespace (via :meth:`set_globals`) for expression evaluation
(in :attr:`self.__globals`) to the globals of the function that called
:meth:`run_template` or :meth:`run_template_once`.
Not many applications are output only, most accept browser input. The Albatross
application object merges the browser request into the execution context in the
:meth:`merge_request` method. Referring back to the application processing
sequence also note that the application object displays the result of processing
the browser request via the execution context.
With this in mind figure :ref:`fig-presimp` becomes figure
:ref:`fig-presimpexec`.
.. _fig-presimpexec:
.. figure:: .build/figures/twolayerctx.*
:align: center
Presentation/Implementation and Execution Context
The only thing missing is the application glue that processes the browser
requests, places application objects into the execution context, and directs the
execution of template files.
The application model built into Albatross is intended to facilitate the use of
a model-view-controller like approach (see figure :ref:`fig-mvc`) to
constructing your application. There are many excellent descriptions of the
model-view-controller design pattern which can be found by searching for "model
view controller" on ``_.
.. _fig-mvc:
.. figure:: .build/figures/mvc.*
:align: center
Albatross model-view-controller
The *user* invokes application functions via the *controller* through the
*view*. The *controller* contains logic to direct the application functionality
contained within the *model*. All of the real application functionality is in
the *model*, not the *controller*. Changes to the application *model* are then
propagated to the *view* via the *controller*.
In Albatross terms, the implementation layer is the *model* and the presentation
layer is the *view*. The application glue plays the role of the *controller*.
By divorcing all application logic from the *view* and *controller* you are able
to construct unit test suites for your application functionality using the
Python :mod:`unittest` module.
Albatross uses an approach inspired by the traditional model-view-controller
design pattern. So now we can draw the final version of the diagram which shows
how Albatross applications process browser requests in figure
:ref:`fig-appmodel`.
.. _fig-appmodel:
.. figure:: .build/figures/albmvc.*
:align: center
Albatross Application Model
As you can see the execution context is central to all of the Albatross
processing. It is worth revisiting the application processing sequence set out
in :ref:`app-model` at the start of this section to see how the application
draws all of the elements together.
During step four (page processing) the Albatross application object will call on
your application code to process the browser request.
There are a number of different ways in that your application code can be
"attached" to the Albatross application object. The :class:`PageObjectMixin`
application mixin class requires that you implement application functionality in
"page objects" and define methods that can be called by the toolkit. As an
example, here is the page object for the ``'login'`` page of the ``popview``
sample application.
.. code-block:: python
class LoginPage:
name = 'login'
def page_process(self, ctx):
if ctx.req_equals('login'):
if ctx.locals.username and ctx.locals.passwd:
try:
ctx.open_mbox()
ctx.add_session_vars('username', 'passwd')
except poplib.error_proto:
return
ctx.set_page('list')
def page_display(self, ctx):
ctx.run_template('login.html')
When the toolkit needs to process the browser request it calls the
:meth:`page_process` method of the current page object. As you can see, the
code determines which request was made by the browser, instantiates the
application objects required to service the browser request, then directs the
context to move to a new page via the :meth:`set_page` method.
When Albatross is ready to display the browser response it calls the
:meth:`page_display` method of the current page object. In the code above,
:meth:`set_page` is only called if the mailbox is opened successfully. This
means that a failed login will result in the login page being displayed again.
Note that when you change pages the object that generates the HTML will be a
different object to that that processed the browser request.
To let you get a foothold on the toolkit application functionality we will work
through another variant of the ``form`` application.
.. _tug-form4:
Using Albatross Input Tags (Again)
==================================
In the previous chapter we demonstrated the use of Albatross input tags to
transfer values from the execution context into HTML ```` tags, and from
the browser request back into the execution context. In this section we present
the same process using an Albatross application object.
The sample program from this section is supplied in the ``samples/form4``
directory and can be installed in your web server ``cgi-bin`` directory by
running the following commands.
.. code-block:: sh
cd samples/form4
python install.py
The ``form.html`` template file used by the application follows.
.. literalinclude:: ../samples/form4/form.html
:language: albatross
The most important new features in the template file are the use of the
```` tag, and the ``list`` attribute in the ```` tag.
Most execution contexts created by application objects inherit from the
:class:`NameRecorderMixin`. The :class:`NameRecorderMixin` records the name,
type and multiple value disposition of each input tag in a form in a
cryptographically signed hidden field named ``__albform__``. This mechanism
prevents clients from being able to merge arbitrary data into the local
namespace, as well as providing additional information to make the merging
process more reliable. The recording process requires that all ````
tags be enclosed by an ```` tag.
When the resulting form is submitted, the contents of the ``__albform__`` field
controls merging of form fields into ``ctx.locals``. Only fields tracked by
``__albform__`` will be merged. Consequently, if submission occurs via a GET
request without an ``__albform__`` field (for example, as a result of the user
following an ````), the application must explicitly request relevent
fields be merged via the ``merge_vars(...)`` method. [#]_
Any input field with the ``list`` attribute will always receive a list value
from a POST browser request regardless of how many values (including none) were
sent by the browser. An exception will be raised if you specify multiple input
tags with the same name in a form and do not include the ``list`` attribute.
The input tag types ``radio``, ``image``, and ``submit`` can only have a single
value, even if multiple inputs of the same name appear in a form, and the
``list`` attribute should not be specified on these.
The ``form.py`` program is show below.
.. literalinclude:: ../samples/form4/form.py
:language: python
You can run the program by pointing your browser at
``_.
Notice that the browser request is automatically merged into the local namespace
and then extracted by the template when generating the HTML response.
The program uses the :class:`SimpleApp` application class. :class:`SimpleApp`
uses an object to define each page served by the application. Each of the page
objects must be registered with the application via the :meth:`register_page`
method.
When the application enters a new page :class:`SimpleApp` calls the
:meth:`page_enter` method of the page object to allow the application to
initialise execution context values. In the above program the
:meth:`page_enter` method initialises all values used by the HTML form to
``None``, initialises the variable *num* to ``0`` and places it into the
session.
As shown in the application processing sequence in the :ref:`app-model` section,
the first step in handling a browser request is to create an execution context.
The :class:`SimpleApp` class uses instances of the :class:`SimpleAppContext`
class which inherits from :class:`HiddenFieldSessionMixin`. The
:class:`HiddenFieldSessionMixin` class stores session data in a hidden field
named ``__albstate__`` at the end of each form.
When an Albatross application needs to display the result of processing a
request it calls the :meth:`page_display` method of the current page. In the
above program this method increments *num* and then runs the ``form.html``
template. It is important to note that any changes to session values after
executing a template will be lost as the session state is saved in the HTML
produced by the template.
It is worth explaining again that the program does not perform any request
merging --- this is all done automatically by the Albatross application and
execution context objects.
.. _app-popview1:
The Popview Application
=======================
In this section we will develop a simple application that allows a user to log
onto a POP server to view the contents of their mailbox. Python provides the
:mod:`poplib` module which provides a nice interface to the POP3 protocol.
The complete sample program is contained in the ``samples/popview1`` directory.
Use the ``install.py`` script to install the sample.
.. code-block:: sh
cd samples/popview1
python install.py
First of all let's create the *model* components of the application. This
consists of some classes to simplify access to a user mailbox. Create a module
called ``popviewlib.py`` and start with the :class:`Mbox` class.
.. code-block:: python
import string
import poplib
pophost = 'pop'
class Mbox:
def __init__(self, name, passwd):
self.mbox = poplib.POP3(pophost)
self.mbox.user(name)
self.mbox.pass_(passwd)
def __getitem__(self, i):
try:
return Msg(self.mbox, i + 1)
except poplib.error_proto:
raise IndexError
The important feature of our :class:`Mbox` class is that it implements the
Python sequence protocol to retrieve messages. This allows us to iterate over
the mailbox using the ```` tag in templates. When there is an attempt to
retrieve a message number which does not exist in the mailbox, a
:mod:`poplib.error_proto` exception will be raised. We transform this exception
into an :exc:`IndexError` exception to signal the end of the sequence.
The sequence protocol is just one of the truly excellent Python features which
allows your application objects to become first class citizens.
Next we need to implement a :class:`Msg` class to access the header and body of
each message.
.. code-block:: python
class Msg:
def __init__(self, mbox, msgnum):
self.mbox = mbox
self.msgnum = msgnum
self.read_headers()
def read_headers(self):
res = self.mbox.top(self.msgnum, 0)
hdrs = Headers()
hdr = None
for line in res[1]:
if line and line[0] in string.whitespace:
hdr = hdr + '\n' + line
else:
hdrs.append(hdr)
hdr = line
hdrs.append(hdr)
self.hdrs = hdrs
return hdrs
def read_body(self):
res = self.mbox.retr(self.msgnum)
lines = res[1]
for i in range(len(lines)):
if not lines[i]:
break
self.body = string.join(lines[i:], '\n')
return self.body
Note that we retrieve the message headers in the constructor to ensure that the
creation of the :class:`Msg` object will fail if there is no such message in the
mailbox. The :class:`Mbox` class uses the exception raised by the :mod:`poplib`
module to detect when a non-existent message is referenced.
The :mod:`poplib` module returns the message headers as a flat list of text
lines. It is up to the :mod:`poplib` user to process those lines and impose a
higher level structure upon them. The :meth:`read_headers` method attaches
header continuation lines to the corresponding header line before passing each
complete header to a :class:`Headers` object.
The :meth:`read_body` method retrieves the message body lines from the POP
server and combines them into a single string.
We are going to need to display message headers by name so our :class:`Headers`
class implements the dictionary protocol.
.. code-block:: python
class Headers:
def __init__(self):
self.hdrs = {}
def append(self, header):
if not header:
return
parts = string.split(header, ': ', 1)
name = string.capitalize(parts[0])
if len(parts) > 1:
value = parts[1]
else:
value = ''
curr = self.hdrs.get(name)
if not curr:
self.hdrs[name] = value
return
if type(curr) is type(''):
curr = self.hdrs[name] = [curr]
curr.append(value)
def __getitem__(self, name):
return self.hdrs.get(string.capitalize(name), '')
Instead of raising a :exc:`KeyError` for undefined headers, the
:meth:`__getitem__` method returns the empty string. This allows us to test the
presence of headers in template files without being exposed to exception
handling.
Lets take all of these classes for a spin in the Python interpreter.
.. code-block:: pycon
>>> import popviewlib
>>> mbox = popviewlib.Mbox('djc', '***')
>>> msg = mbox[0]
>>> msg.hdrs['From']
'Owen Taylor '
>>> print msg.read_body()
Daniel Egger writes:
> Am 05 Aug 2001 12:00:15 -0400 schrieb Alex Larsson:
>
[snip]
Next we will create the application components (the *controller*) in
``popview.py``. Albatross has a prepackaged application object which you can
use for small applications; the :class:`SimpleApp` class.
In anything but the most trivial applications it is probably a good idea to draw
a site map like figure :ref:`fig-popview-sitemap` to help visualise the
application.
Our application will contain three pages; login, message list, and message
detail.
.. _fig-popview-sitemap:
.. figure:: .build/figures/pagemap.*
:align: center
Popview Site Map
The :class:`SimpleApp` class inherits from the :class:`PageObjectMixin` class
which requires that the application define an object for each page in the
application.
Albatross stores the current application page identifier in the local namespace
of the execution context as :attr:`__page__`. The value is automatically
created and placed into the session.
When the current page changes, the Albatross application object calls the
:meth:`page_enter` function/method of the new page object. The
:meth:`page_process` method is called to process a browser request, and finally
:meth:`page_display` is called to generate the application response.
The :class:`SimpleApp` class creates :class:`SimpleAppContext` execution context
objects. By subclassing :class:`SimpleApp` and overriding
:meth:`create_context` we can define our own execution context class.
The prologue of the application imports the required application and execution
context classes from Albatross and the :class:`Request` class from the
deployment module.
.. code-block:: python
#!/usr/bin/python
from albatross import SimpleApp, SimpleAppContext
from albatross.cgiapp import Request
import poplib
import popviewlib
Next in the file is the class for the login page. Note that we only implement
glue (or *controller*) logic in the page objects. Each time a new page is
served we will need to open the mail box and retrieve the relevant data. That
means that the username and password will need to be stored in some type of
session data storage.
.. code-block:: python
class LoginPage:
name = 'login'
def page_process(self, ctx):
if ctx.req_equals('login'):
if ctx.locals.username and ctx.locals.passwd:
try:
ctx.open_mbox()
ctx.add_session_vars('username', 'passwd')
except poplib.error_proto:
return
ctx.set_page('list')
def page_display(self, ctx):
ctx.run_template('login.html')
The :meth:`req_equals` method of the execution context looks inside the browser
request for a field with the specified name. It returns a TRUE value if such a
field exists and it has a value not equal to ``None``. The test above will
detect when a user presses the submit button named ``'login'`` on the
``login.html`` page.
Next, here is the page object for displaying the list of messages in the
mailbox.
.. code-block:: python
class ListPage:
name = 'list'
def page_process(self, ctx):
if ctx.req_equals('detail'):
ctx.set_page('detail')
def page_display(self, ctx):
ctx.open_mbox()
ctx.run_template('list.html')
The "detail" page displays the message detail.
.. code-block:: python
class DetailPage:
name = 'detail'
def page_process(self, ctx):
if ctx.req_equals('list'):
ctx.set_page('list')
def page_display(self, ctx):
ctx.open_mbox()
ctx.read_msg()
ctx.run_template('detail.html')
And finally we define the application class and instantiate the application
object. Note that we have subclassed :class:`SimpleApp` to create our own
application class. This allows us to implement our own application level
functionality as required.
.. code-block:: python
class AppContext(SimpleAppContext):
def open_mbox(self):
if hasattr(self.locals, 'mbox'):
return
self.locals.mbox = popviewlib.Mbox(self.locals.username, self.locals.passwd)
def read_msg(self):
if hasattr(self.locals, 'msg'):
return
self.locals.msg = self.locals.mbox[int(self.locals.msgnum) - 1]
self.locals.msg.read_body()
class App(SimpleApp):
def __init__(self):
SimpleApp.__init__(self,
base_url='popview.py',
template_path='.',
start_page='login',
secret='-=-secret-=-')
for page_class in (LoginPage, ListPage, DetailPage):
self.register_page(page_class.name, page_class())
def create_context(self):
return AppContext(self)
app = App()
if __name__ == '__main__':
app.run(Request())
The *base_url* argument to the application object constructor will be placed
into the ``action`` attribute of all forms produced by the ```` tag.
It will also form the left hand side of all hrefs produced by the ````
tag. The *template_path* argument is a relative path to the directory that
contains the application template files. The *start_page* argument is the name
of the application start page. When a browser starts a new session with the
application it will be served the application start page.
We have also created our own execution context to provide some application
functionality as execution context methods.
With the *model* and *controller* components in place we can now move onto the
template files that comprise the *view* components of the application.
First let's look at the ``login.html`` page.
.. literalinclude:: ../samples/popview1/login.html
:language: albatross
When you look at the HTML produced by the application you will notice two extra
```` tags have been generated at the bottom of the form. They are
displayed below (reformatted to fit on the page).
.. _alb-hidden:
.. code-block:: html
If we fire up the Python interpreter we can have a look at what these fields
contain.
.. code-block:: pycon
>>> import base64,zlib,cPickle
>>> s = "eJzTDJeu3P90rZC6dde04xUhHL\n" + \
... "WFjBqhHKXFqUV5ibmphUzeDKFsBYnFxeUphcxANmtOfnpmXiGLN0OpHgB7UBOp\n"
>>> cPickle.loads(zlib.decompress(base64.decodestring(s))[16:])
{'username': 0, 'passwd': 0, 'login': 0}
>>> s = "eJzT2sr5Jezh942TUrMty6q1j\n" + \
... "WsLGUM54uMLEtNT4+MLmUJZc/LTM/MKmYv1AH8XEAY=\n"
>>> cPickle.loads(zlib.decompress(base64.decodestring(s))[16:])
{'__page__': 'login'}
The first string contains a dictionary that defines the name and type of the
input fields that were present in the form. This is placed into the form by the
:class:`NameRecorderMixin` class which is subclassed by
:class:`SimpleAppContext`. If you look at the definition of
:class:`SimpleAppContext` you will notice the following definition at the start
of the class.
.. code-block:: python
NORMAL = 0
MULTI = 1
MULTISINGLE = 2
FILE = 3
The value ``0`` for each field in the dictionary above corresponds to field type
``NORMAL``.
When merging the browser request into the execution context the dictionary of
field names is used to assign the value ``None`` to any ``NORMAL`` or ``FILE``
fields, or ``[]`` to any ``LIST`` fields that were left empty by the user. This
is useful because it lets us write application code which can ignore the fact
that fields left empty by the user will not be sent by the browser when the form
is submitted.
The second string contains all of the session values for the application. In
the application start page the only session variable that exists is the
:attr:`__page__` variable. The :class:`HiddenFieldSessionMixin` places this
field in the form when the template is executed and pulls the field value back
out of the browser request into the execution context when the session is
loaded.
The first 20 bytes of the decompressed string is an HMAC-SHA1 cryptographic
signature. This was generated by combining the application secret (passed as
the *secret* argument to the application object) with the pickle string with a
cryptographic hash. When the field is sent back to the application the signing
process is repeated. The pickle is only loaded if the sign sent by the browser
and the regenerated signature are the same.
Since the ``popview`` application has been provided for example purposes you
will probably forgive the usage of the :class:`HiddenFieldSessionMixin` class to
propagate session state. In a real application that placed usernames and
passwords in the session you would probably do something to protect these values
from prying eyes.
Now let's look at the ``list.html`` template file.
Note that the use of the ```` tag causes the HTML output to be
streamed to the browser. Use of this tag can give your application a much more
responsive feel when generating pages that involve lengthy processing.
A slightly obscure feature of the page is the use of a separate form surrounding
the ```` field used to select each message. An
unfortunate limitation of the HTML ```` tag is that you
cannot associate a value with the field because the browser returns the
coordinates where the user pressed the mouse inside the image. In order to
associate the message number with the image button we place the message number
in a separate hidden ```` field and group the two fields using a form.
You have to be careful creating a large number of forms on the page because each
of these forms will also contain the ``__albform__`` and ``__albstate__`` hidden
fields. If you have a lot of data in your session the size of the
``__albstate__`` field will cause the size of the generated HTML to explode.
.. literalinclude:: ../samples/popview1/list.html
:language: albatross
Finally, here is the message detail page ``detail.html``.
.. literalinclude:: ../samples/popview1/detail.html
:language: albatross
.. _app-popview2:
Adding Pagination Support to Popview
====================================
If the previous section we constructed a simple application for viewing the
contents of a mailbox via the :mod:`poplib` module. One problem with the
application is that there is no limit to the number of messages that will be
displayed. In this section we will build pagination support into the message
list page.
We will modify the application to display 15 messages on each page and will add
buttons to navigate to the next and previous pages. The ``pagesize`` attribute
of the ```` tag provides automatic pagination support for displaying
sequences. The only extra code that we need to add is a ``__len__`` method to
the :class:`Mbox` class which returns the number of messages in the mailbox.
This ``__len__`` method is needed to allow the template file to test whether or
not to display a next page control.
The complete sample program is contained in the ``samples/popview2`` directory.
Use the ``install.py`` script to install the sample.
.. code-block:: sh
cd samples/popview2
python install.py
Add a ``__len__`` method to the :class:`Mbox` class in ``popviewlib.py``.
.. code-block:: python
class Mbox:
def __init__(self, name, passwd):
self.mbox = poplib.POP3(pophost)
self.mbox.user(name)
self.mbox.pass_(passwd)
def __getitem__(self, i):
try:
return Msg(self.mbox, i + 1)
except poplib.error_proto:
raise IndexError
def __len__(self):
len, size = self.mbox.stat()
return len
Now we modify the template file ``list.html``.
.. literalinclude:: ../samples/popview2/list.html
:language: albatross
The ```` tag just below the ```` tag contains two new attributes;
``pagesize`` and ``prepare``.
The ``pagesize`` turns on pagination for the ```` :class:`ListIterator`
object and defines the size of each page. In order to remember the current top
of page between pages, the tag places the iterator into the session. When
saving the iterator, only the top of page and pagesize are retained.
The ``prepare`` attribute instructs the ```` tag to perform all tasks
except actually display the content of the sequence. This allows us to place
pagination controls before the actual display of the list.
.. _app-popview3:
Adding Server-Side Session Support to Popview
=============================================
So far we have been saving all application state at the browser inside hidden
fields. Sometimes it is preferable to retain state at the server side.
Albatross includes support for server-side sessions in the
:class:`SessionServerContextMixin` and :class:`SessionServerAppMixin` classes.
In this section we will modify the ``popview.py`` program to use server-side
sessions.
The :class:`SessionServerAppMixin` class uses a socket to communicate with the
session server (see :ref:`app-ses-daemon`). You will need to start this
program before using the new ``popview`` application.
In previous versions of the program we were careful to place all user response
into forms. This allowed Albatross to transparently attach the session state to
hidden fields inside the form. When using server-side sessions Albatross does
not need to save any application state at the browser so we are free to use URL
style user inputs. To illustrate the point, we will replace all of the form
inputs with URL user inputs.
The complete sample program is contained in the ``samples/popview3`` directory.
Use the ``install.py`` script to install the sample.
.. code-block:: sh
cd samples/popview3
python install.py
The new ``list.html`` template file follows.
.. literalinclude:: ../samples/popview3/list.html
:language: albatross
Next the new ``detail.html`` template file.
.. literalinclude:: ../samples/popview3/detail.html
:language: albatross
One of the more difficult tasks for developing stateful web applications is
dealing with browser requests submitted from old pages in the browser history.
When all application state is stored in hidden fields in the HTML, requests from
old pages do not usually cause problems. This is because the old application
state is provided in the same request.
When application state is maintained at the server, requests from old pages can
cause all sorts of problems. The current application state at the server
represents the result of a sequence of browser requests. If the user submits a
request from an old page in the browser history then the fields and values in
the request will probably not be relevant to the current application state
Making sure all application requests are uniquely named provides some protection
against the application processing a request from another page which just
happened the share the same request name. It is not a complete defense as you
may receive a request from an old version of the same page. A request from an
old version of a page is likely to make reference to values which no longer
exist in the server session.
Some online banking applications attempt to avoid this problem by opening
browser windows that do not have history navigation controls. A user who uses
keyboard accelerators for history navigation will not be hindered by the lack of
navigation buttons.
The ``popview`` application does not modify any of the data it uses so there is
little scope for submissions from old pages to cause errors.
By changing the base class for the application object we can gain support for
server side sessions. Albatross includes a simple session server and supporting
mixin classes.
The new application prologue looks like this:
.. code-block:: python
#!/usr/bin/python
from albatross import SimpleSessionApp, SessionAppContext
from albatross.cgiapp import Request
import popviewlib
The execution context now inherits from :class:`SessionAppContext`:
.. code-block:: python
class AppContext(SessionAppContext):
def open_mbox(self):
if hasattr(self.locals, 'mbox'):
return
self.locals.mbox = popviewlib.Mbox(self.locals.username, self.locals.passwd)
def read_msg(self):
if hasattr(self.locals, 'msg'):
return
self.locals.msg = self.locals.mbox[int(self.locals.msgnum) - 1]
self.locals.msg.read_body()
And the new application class looks like this:
.. code-block:: python
class App(SimpleSessionApp):
def __init__(self):
SimpleSessionApp.__init__(self,
base_url='popview.py',
template_path='.',
start_page='login',
secret='-=-secret-=-',
session_appid='popview3')
for page_class in (LoginPage, ListPage, DetailPage):
self.register_page(page_class.name, page_class())
def create_context(self):
return AppContext(self)
The *session_appid* argument to the constructor is used to uniquely identify the
application at the server so that multiple applications can be accessed from the
same browser without the session from one application modifying the session from
another.
Apart from changes to load the new template files, we also need to change the
:class:`ListPage` class because we changed the method of selecting messages from
the message list.
.. code-block:: python
class ListPage:
name = 'list'
def page_process(self, ctx):
if ctx.req_equals('msgnum'):
ctx.set_page('detail')
def page_display(self, ctx):
ctx.open_mbox()
ctx.run_template('list.html')
.. _app-popview4:
Building Applications with Page Modules
=======================================
Implementing an application as a monolithic program is fine for small
applications. As the application grows the startup time becomes an issue, as
does maintenance. Albatross provides a set of classes that allow you to
implement each page in a separate Python module. In this section we will
convert the ``popview`` application to this type of application.
Converting a monolithic application to a page module application is usually
fairly simple. First we must turn each page object into a page module. When we
used page objects, the class that implemented each page was identified by the
:attr:`name` class member. With page modules the name of the module identifies
the page that it processes.
The complete sample program is contained in the ``samples/popview4`` directory.
Use the ``install.py`` script to install the sample.
.. code-block:: sh
cd samples/popview4
python install.py
The :class:`LoginPage` class becomes ``login.py``.
.. literalinclude:: ../samples/popview4/login.py
:language: python
The :class:`ListPage` class becomes ``list.py``.
.. literalinclude:: ../samples/popview4/list.py
:language: python
And the :class:`DetailPage` class becomes ``detail.py``.
.. literalinclude:: ../samples/popview4/detail.py
:language: python
When using page modules we do not need to register each page module. When
Albatross needs to locate the code for a page it simply imports the module. So
the entire ``popview.py`` program now looks like this:
.. literalinclude:: ../samples/popview4/popview.py
:language: python
.. _app-random:
Random Access Applications
==========================
In the popview application the server is in complete control of the sequence of
pages that are served to the browser. In some applications you want the user to
be able to bookmark individual pages for later retrieval in any desired
sequence. Albatross provides application classes built with the
:class:`RandomPageModuleMixin` class for this very purpose.
The ``random`` sample is provided to demonstrate the use of the
:class:`RandomModularSessionApp` class. Use the ``install.py`` script to
install the sample.
.. code-block:: python
cd samples/random
python install.py
The complete mainline of the ``randompage.py`` sample is shown below.
.. literalinclude:: ../samples/random/randompage.py
:language: python
When processing the browser request the application determines which page to
serve to the browser by inspecting the URL in the browser request. The page
identifier is taken from the part of the URL which follows the *base_url*
argument to the constructor. If the page identifier is empty then the
application serves the page identified by the *start_page* argument to the
constructor.
If you point your browser at
``_ you will
notice that the server has redirected your browser to the start page.
The sample program defines two pages which demonstrate two different ways to
direct user navigation through the application.
The ``tree.html`` page template uses a form to capture user input.
.. literalinclude:: ../samples/random/pages/tree.html
:language: albatross
During conversion to HTML the ```` tag automatically places the name of
the current page into the ``action`` attribute. This makes the browser send the
response back to the same page module (``tree.py``).
.. literalinclude:: ../samples/random/pages/tree.py
:language: python
When the application receives the ``paginate`` request it uses the
:meth:`redirect` method to direct the browser to a new page.
The ``paginate.html`` page template uses a URLs to capture user input.
.. literalinclude:: ../samples/random/pages/paginate.html
:language: albatross
During conversion to HTML the ```` tag automatically translates the
``href="tree"`` attribute into a URL which requests the ``tree`` page from the
application. Since the browser is doing all of the work, the ``paginate.py``
module which handles the ``paginate`` page is very simple.
.. literalinclude:: ../samples/random/pages/paginate.py
:language: python
.. _app-ses-server:
The Albatross Session Server
============================
The Albatross Session Server works in concert with the execution context and
application mixin classes in the :mod:`albatross.session` module to provide
server-side session recording. The application and the session server
communicate via TCP sockets, by default on port 34343.
More than one Albatross application can share a single session server process,
allowing applications to be deployed over multiple web server hosts while still
sharing state.
.. _app-ses-daemon:
Session Server Daemon
---------------------
The Session Server is started via the Albatross command line ``albatross
session-server``.
On POSIX systems (Linux, Solaris, Mac OS X), the server will attempt to run in
the background and become a daemon. This can be overridden via the
``--foreground`` option. Daemon mode has three sub-commands - ``start``,
``stop`` and ``status``. Daemon mode writes a file recording the process ID of
daemon. The location of this file is controlled with the ``--pidfile=FILE``
option. Daemon mode is not available on non-POSIX platforms such as Windows.
.. code-block:: sh
$ albatross session-server start
server pid 20653
.. code-block:: sh
$ albatross session-server status
running, pid is 20653
.. code-block:: sh
$ albatross session-server stop
stopping 20653 . done
Note that the daemon does not need to run as ``root``, provided it listens on a
port above 1024, and can write to it's pid file (and optional log file). If
possible, you should run it under a user ID not shared by any other processes
(and not ``nobody``). You should also ensure that only authorised clients can
connect to your session server, as the protocol provides no authentication or
authorisation mechanisms.
By default, the daemon operates on port 34343. This can be changed with the
``--port=PORT`` option.
.. code-block:: none
Usage: albatross session-server [options]...
Start or stop an albatross session server
is one of:
start start a new daemon
stop kill the current daemon
status request the daemon's status
Options:
-h, --help show this help message and exit
-p PORT, --port=PORT Listen on PORT (default: 34343)
-l LOGFILE, --logfile=LOGFILE, --log=LOGFILE
Write log to LOGFILE (default: none)
-D, --debug Generate additional debugging logs
-k PIDFILE, --pidfile=PIDFILE, --pid-file=PIDFILE
Record server pid in PIDFILE (default: /var/run/al-
session-server.pid)
--daemon, --bg Run in the background
--foreground, --fg Run in the foreground
.. _app-ses-server-start:
Starting the Session Server on boot
-----------------------------------
This depends on the specific startup system used by your platform and, if not
done correctly, can prevent your system booting.
System V rc.d
^^^^^^^^^^^^^
.. code-block:: sh
#!/bin/sh
#
# SysV-style init.d script for the albatross session daemon
#
### BEGIN INIT INFO
# Provides: al-session-server
# Required-Start: $remote_fs
# Required-Stop: $remote_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Albatross session daemon
# Description: Albatross session daemon
### END INIT INFO
DAEMON="/usr/bin/albatross session-server"
USER=albatross
PIDFILE=/var/run/al-session-server.pid
test -x ${DAEMON} || exit 0
umask 077
case "$1" in
start)
echo "Starting albatross session daemon"
su ${USER} -c "${DAEMON} --pidfile ${PIDFILE} start"
;;
stop)
echo "Stopping albatross session daemon"
su ${USER} -c "${DAEMON} --pidfile ${PIDFILE} stop"
;;
*)
echo "Usage: $* {start|stop}"
exit 1
;;
esac
exit 0
Upstart
^^^^^^^
This gets placed in ``/etc/init/al-session-server``, start as root with
``initctl start al-session-server``.
.. code-block:: none
description "Albatross Session Server"
author "Albatross "
start on local-filesystems
stop on runlevel [!2345]
respawn
exec /usr/bin/albatross session-server --foreground
OS X launchd
^^^^^^^^^^^^
Copy the following to ``/Launch/LaunchDaemons/al-session-server.plist`` and run
``launchctl load /Library/LaunchDaemons/al-session-server.plist``.
.. code-block:: xml
Labelau.com.obect-craft.session-serverUserNamealbatrossRunAtLoadProgram/usr/local/bin/albatrossProgramArgumentsalbatrosssession-server--foreground
Solaris 10 SMF
^^^^^^^^^^^^^^
Save the following as ``al-session-server.xml`` and install with ``svccfg
import al-session-server.xml``, then start the server with ``svcadm enable
al-session-server``. You can examine the state of the server using ``svcs -x
al-session-server``.
.. code-block:: xml
Albatross session daemon
systemd
^^^^^^^
To be written - tips gratefully received.
Windows
^^^^^^^
To be written - tips gratefully received.
.. _app-ses-simple-server:
Sample Simple Session Server
----------------------------
The ``albatross.simpleserver`` module is a simple session server that records
sessions in memory. It can be used as the basis for a more sophisticated
session server, or imported and instantiated directly.
Internally the server uses a select loop to allow connections from multiple
applications simultaneously.
Application constructor arguments which are relevant to the session server are:
* *session_appid*
This is used to identify the application with the session server. It is also
used as the session id in the cookie sent to the browser.
* *session_server* = ``'localhost'``
If you decide to run the session server on a different machine to the
application you must pass the host name of the session server in this
argument.
* *server_port* = ``34343``
If you decide to run the session server on a different port you must pass the
port number in this argument.
* *session_age* = ``1800``
This argument defines the amount of time in seconds for which idle sessions
will kept in the server.
.. _app-ses-protocol:
Server Protocol
---------------
You can see the session server in action by using telnet.
.. code-block:: none
djc@rat:~$ telnet localhost 34343
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
new myapp 3600
OK 38b80b3f546c8cfa
put myapp 38b80b3f546c8cfa
OK - send data now, terminate with blank line
here is my session data
it is on multiple lines
OK
get myapp 38b80b3f546c8cfa
OK - session follows
here is my session data
it is on multiple lines
del myapp 38b80b3f546c8cfa
OK
get myapp 38b80b3f546c8cfa
ERROR no such session
quit
Connection closed by foreign host.
djc@rat:~$
All dialogue is line based using a CRLF end of line sequence. Session ids are
generated by the server and each application has its own set of session ids.
The application mixin class in the :mod:`albatross.session` module uses the
*session_appid* argument to the constructor as the application id with the
session server. Note that this application id is also used in the cookie sent
to the browser.
If a command was successful the server response line will start with the text
``'OK'`` otherwise it will start with ``'ERROR'``.
.. _app-proto-new:
Create New Session
^^^^^^^^^^^^^^^^^^
To create a new session for the *appid* application which will be deleted if
left idle for more than *age* seconds the application sends a line of the form::
"new " appid " " age CRLF
Successful response will be a line of the form::
"OK " sesid CRLF
.. _app-proto-put:
Save Session
^^^^^^^^^^^^
To save data into an existing session the application sends a line of the form::
"put " appid " " sesid CRLF
If the session exists in the server it will respond with the following line::
"OK - send data now, terminate with blank line" CRLF
The program then sends a sequence of text lines terminated by a single blank
line. The server then responds with::
"OK" CRLF
.. _app-proto-get:
Retrieve Session
^^^^^^^^^^^^^^^^
To retrieve data for an existing session the application sends a line of the
form::
"get " appid " " sesid CRLF
If the session exists in the server it will respond with the following line::
"OK - session follows" CRLF
The session data saved previously will then be sent terminated by a single blank
line.
.. _app-proto-del:
Delete Session
^^^^^^^^^^^^^^
To delete an existing session the application sends a line of the form::
"del " appid " " sesid CRLF
If the session exists it will be deleted and the server will respond with the
following line::
"OK" CRLF
.. _app-proto-quit:
Quit
^^^^
To disconnect from the server the application sends a line of the form::
"quit" CRLF
The server will then close the connection.
.. _app-deployoptions:
Application Deployment Options
==============================
In all of the previous sections you will note that all of the programs used the
:class:`Request` class from the :mod:`albatross.cgiapp` module to deploy the
application as a CGI script.
The choice of :class:`Request` class determines how you wish to deploy your
application. Albatross supplies a number of pre-built Request implementations
suited to various deployment methods. You should import the Request method from
the appropriate module:
+--------------------------------+----------------------------+
| Deployment Method | Request Module |
+================================+============================+
| CGI | :mod:`albatross.cgiapp` |
+--------------------------------+----------------------------+
| mod_python | :mod:`albatross.apacheapp` |
+--------------------------------+----------------------------+
| FastCGI_python | :mod:`albatross.fcgiapp` |
+--------------------------------+----------------------------+
| Stand-alone Python HTTP server | :mod:`albatross.httpdapp` |
+--------------------------------+----------------------------+
By placing all deployment dependencies in a :class:`Request` class you are able
to change deployment method with only minimal changes to your application
mainline code. You could, for instance, carry out your initial development as a
stand-alone Python HTTP server, where debugging is easier, and final deployment
as FastCGI.
You could also develop your own :class:`Request` class to deploy an Albatross
application in other ways, such as using the Medusa web server
(``_), or to provide a
:class:`Request` class which for performing unit tests on your application.
The chapter on mod_python contains an example where the popview application is
changed from CGI to ``mod_python``.
.. _app-deploycgi:
``CGI`` Deployment
------------------
The :mod:`albatross.cgiapp` module contains a :class:`Request` class to allow
you to deploy your application using ``CGI``.
CGI is the simplest and most common application deployment scheme. The
application is started afresh by your web server to service each client request,
and is passed client input via the command line and stdin, and returns it's
output via stdout.
An example of a CGI application:
.. code-block:: python
#!/usr/bin/python
from albatross.cgiapp import Request
class Application(...):
...
app = Application()
if __name__ == '__main__':
app.run(Request())
.. _app-deploymodpython:
``mod_python`` Deployment
-------------------------
The :mod:`albatross.apacheapp` module contains a :class:`Request` class to allow
you to deploy your application using ``mod_python`` [#]_.
In the following example, we change the popview application from CGI to
``mod_python``. The complete sample program is contained in the
``samples/popview5`` directory. Use the ``install.py`` script to install the
sample.
.. code-block:: sh
cd samples/popview5
python install.py
The new ``popview.py`` mainline follows.
.. literalinclude:: ../samples/popview5/popview.py
:language: python
The :func:`handler` function is called by ``mod_python`` when a browser request
is received that must be handled by the program.
You also need to create a ``.htaccess`` file to tell Apache to run the
application using ``mod_python``. :
.. code-block:: apacheconf
DirectoryIndex popview.py
SetHandler python-program
PythonHandler popview
Assuming you install the popview sample below the ``/var/www`` directory you
will need configure Apache settings for the ``/var/www/alsamp`` directory:
.. code-block:: apacheconf
AllowOverride FileInfo Indexes
Order allow,deny
Allow from all
.. _app-deployfastcgi:
``FastCGI`` Deployment
----------------------
The :mod:`albatross.fcgiapp` module contains a :class:`Request` class to allow
you to deploy your application using ``FastCGI`` [#]_.
Applications deployed via CGI often perform poorly under load, because the
application is started afresh to service each client request, and the start-up
time can account for a significant proportion of request service time.
``FastCGI`` attempts to address this by turning the application into a
persistent server that can handle many client requests.
Unlike ``mod_python``, where applications run within the web server, ``FastCGI``
applications communicate with the web server via a platform-independent socket
protocol. This improves security and the resilience of the web service.
To deploy your application via ``FastCGI`` and Apache, you need to configure
Apache to load ``mod_fastcgi.so``, configure it to start your script as a
``FastCGI`` server, and use the ``albatross.fcgiapp`` :class:`Request` class in
your application. As an example of Apache configuration:
.. code-block:: apacheconf
LoadModule fastcgi_module /usr/lib/apache/1.3/mod_fastcgi.so
AddHandler fastcgi-script py
Options +ExecCGI
And the application main-line:
.. code-block:: python
#!/usr/bin/python
from albatross.fcgiapp import Request, running
class Application(...):
...
app = Application()
if __name__ == '__main__':
while running():
app.run(Request())
.. _app-deploypyhttpd:
Stand-alone Python HTTP Server Deployment
-----------------------------------------
The standard Python libraries provide a pure-Python HTTP server in the
``BaseHTTPServer`` module. Code contributed by Matt Goodall allows you to deploy
your Albatross applications as stand-alone scripts using this module to service
HTTP requests.
Unlike the other ``Request`` classes, applications deployed via the stand-alone
Python HTTP server do not require you to instantiate the ``Request`` class
directly. Instead, the ``albatross serve`` script imports your application
(specifically, the script that instantiates the application object), and starts
the HTTP server.
Currently, applications deployed via the stand-alone http server are single
threaded, meaning that other requests are blocked while the current request is
being serviced. In many cases this is not a problem as the requests are handled
quickly, but you should consider other deployment options for production use.
The stand-alone http server makes it particularly easy to develop Albatross
applications, and is a great way to debug applications without the
complications that a general purpose web server necessarily entail.
Most of the Albatross samples can be run under the stand-alone server:
.. code-block:: sh
$ cd samples/tree2
$ albatross serve --port=8080 tree.app /alsamp/images ../images/
.. _app-exceptions:
Albatross Exceptions
====================
.. exception:: AlbatrossError
An abstract base class all Albatross exceptions inherit from.
.. exception:: UserError
Raised on abnormal input from the user. All current use of this exception is
through the :exc:`SecurityError` subclass.
.. exception:: ApplicationError
Raised on invalid Albatross use by the application, such as attempting to set a
response header after the headers have been sent to the client. Template errors
are also instances of this exception.
.. exception:: InternalError
Raised if Albatross detects an internal error (bug).
.. exception:: ServerError
Raised on difficulties communicating with the session server or errors reading
server-side session files.
.. exception:: SecurityError
A subclass of :exc:`UserError`, this exception is raised when Albatross detects
potentially hostile client activity.
.. exception:: TemplateLoadError
A subclass of :exc:`ApplicationError`, this exception is raised if a template
cannot be loaded.
.. exception:: SessionExpired
A subclass of :exc:`UserError`, this exception is raised when a client attempts
to submit form results against a session that has expired.
.. rubric:: Footnotes
.. [#] Prior to Albatross version 1.36, in the absence of an ``__albform__`` field, all
request fields would be merged.
.. [#] For more information on ``mod_python``, including installation instructions, see
``_.
.. [#] For more information on ``FastCGI``, including installation instructions, see
``_.