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.

  1. The browser connects to the server and requests a page.
  2. The server decodes the browser request and processes it. This can cause all manner of subsidiary application processing to occur.
  3. 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.

  1. Deploy a stateless application. Only the most trivial applications do not require state to be retained across browser requests.
  2. 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.
  3. 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.

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 Request Processing Dataflow.

.build/figures/dataflow.*

Request Processing Dataflow

The processing steps are:

  1. Capture the browser request in a Request object.
  2. Pass the Request object to the run() method of the application object.
  3. Application locates the Python code for processing the browser request.
  4. Page processing code runs one or more Albatross templates.
  5. Templates contain either standard Albatross tags or application defined extension tags.
  6. As tags are converted to HTML a stream of content fragments is sent to the execution context.
  7. When the execution context content is flushed all of the fragments are joined together.
  8. Joined flushed content is sent to the Request object write_content() method.
  9. Application response is returned to the browser.

In the Albatross code the processing is driven by the run() method of the Application class in the app module.

It is instructive to look at the exact code from Albatross that implements the processing sequence.

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 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 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 Prepackaged Application and Execution Context Classes.

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 Separation of Presentation/Implementation. Albatross HTML templates provide a fairly powerful tool for achieving that separation.

.build/figures/twolayer.*

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:

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 NamespaceMixin class:

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 run_template() or run_template_once() methods of the NamespaceMixin class Albatross sets the global namespace (via set_globals()) for expression evaluation (in self.__globals) to the globals of the function that called run_template() or 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 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 Separation of Presentation/Implementation becomes figure Presentation/Implementation and Execution Context.

.build/figures/twolayerctx.*

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 Albatross model-view-controller) 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 http://www.google.com/.

.build/figures/mvc.*

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 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 Albatross Application Model.

.build/figures/albmvc.*

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 Albatross Application 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 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.

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 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 set_page() method.

When Albatross is ready to display the browser response it calls the page_display() method of the current page object. In the code above, 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.

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 <input> 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.

cd samples/form4
python install.py

The form.html template file used by the application follows.

<html>
 <head>
  <title>Display Form Input</title>
 </head>
 <body>
  Input some values to the following form and press the submit button.
  <al-form method="post">
   Text field: <al-input name="text"><br>
   Singleton checkbox: <al-input type="checkbox" name="singleton"><br>
   Checkbox group:
   <al-input type="checkbox" name="group" list value="check1">
   <al-input type="checkbox" name="group" list value="check2"><br>
   Radio buttons:
   <al-input type="radio" name="radio" value="radio1">
   <al-input type="radio" name="radio" value="radio2"><br>
   Option menu:
   <al-select name="select">
    <al-option>option1</al-option>
    <al-option>option2</al-option>
    <al-option>option3</al-option>
   </al-select>
   <al-input type="submit" name="submit" value="submit">
  </al-form>
  number of requests: <al-value expr="num"><br>
  text: <al-value expr="text"><br>
  singleton: <al-value expr="singleton"><br>
  group: <al-value expr="group"><br>
  radio: <al-value expr="radio"><br>
  select: <al-value expr="select"><br>
 </body>
</html>

The most important new features in the template file are the use of the <al-form> tag, and the list attribute in the <al-input type="checkbox"> tag.

Most execution contexts created by application objects inherit from the NameRecorderMixin. The 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 <al-input> tags be enclosed by an <al-form> 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 <al-a>), the application must explicitly request relevent fields be merged via the merge_vars(...) method. [1]

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.

#!/usr/bin/python
from albatross import SimpleApp
from albatross.cgiapp import Request


class Form:

    def page_enter(self, ctx):
        ctx.locals.text = ctx.locals.singleton = ctx.locals.group = \
                          ctx.locals.radio = ctx.locals.select = None
        ctx.locals.num = 0
        ctx.add_session_vars('num')

    def page_display(self, ctx):
        ctx.locals.num += 1
        ctx.run_template('form.html')


app = SimpleApp(base_url='form.py',
                template_path='.',
                start_page='form',
                secret='-=-secret-=-')
app.register_page('form', Form())


if __name__ == '__main__':
    app.run(Request())

You can run the program by pointing your browser at http://www.object-craft.com.au/cgi-bin/alsamp/form4/form.py.

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 SimpleApp application 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 register_page() method.

When the application enters a new page SimpleApp calls the page_enter() method of the page object to allow the application to initialise execution context values. In the above program the 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 Albatross Application Model section, the first step in handling a browser request is to create an execution context. The SimpleApp class uses instances of the SimpleAppContext class which inherits from HiddenFieldSessionMixin. The 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 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.

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 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.

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 Mbox class.

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 Mbox class is that it implements the Python sequence protocol to retrieve messages. This allows us to iterate over the mailbox using the <al-for> tag in templates. When there is an attempt to retrieve a message number which does not exist in the mailbox, a poplib.error_proto exception will be raised. We transform this exception into an 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 Msg class to access the header and body of each message.

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 Msg object will fail if there is no such message in the mailbox. The Mbox class uses the exception raised by the poplib module to detect when a non-existent message is referenced.

The poplib module returns the message headers as a flat list of text lines. It is up to the poplib user to process those lines and impose a higher level structure upon them. The read_headers() method attaches header continuation lines to the corresponding header line before passing each complete header to a Headers object.

The 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 Headers class implements the dictionary protocol.

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 KeyError for undefined headers, the __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.

>>> import popviewlib
>>> mbox = popviewlib.Mbox('djc', '***')
>>> msg = mbox[0]
>>> msg.hdrs['From']
'Owen Taylor <otaylor@redhat.com>'
>>> print msg.read_body()

Daniel Egger <egger@suse.de> 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 SimpleApp class.

In anything but the most trivial applications it is probably a good idea to draw a site map like figure Popview Site Map to help visualise the application.

Our application will contain three pages; login, message list, and message detail.

.build/figures/pagemap.*

Popview Site Map

The SimpleApp class inherits from the 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 __page__. The value is automatically created and placed into the session.

When the current page changes, the Albatross application object calls the page_enter() function/method of the new page object. The page_process() method is called to process a browser request, and finally page_display() is called to generate the application response.

The SimpleApp class creates SimpleAppContext execution context objects. By subclassing SimpleApp and overriding 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 Request class from the deployment module.

#!/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.

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 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.

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.

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 SimpleApp to create our own application class. This allows us to implement our own application level functionality as required.

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 <al-form> tag. It will also form the left hand side of all hrefs produced by the <al-a> 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.

<html>
 <head>
  <title>Please log in</title>
 </head>
 <body>
  <h1>Please log in</h1>
  <al-form method="post">
   <table>
    <tr>
     <td>Username</td>
     <td><al-input name="username" size="10" maxlength="10"></td>
    </tr>
    <tr>
     <td>Password</td>
     <td><al-input type="password" name="passwd" size="10" maxlength="10"></td>
     <td><al-input type="submit" name="login" value="Log In"></td>
    </tr>
   </table>
  </al-form>
 </body>
</html>

When you look at the HTML produced by the application you will notice two extra <input> tags have been generated at the bottom of the form. They are displayed below (reformatted to fit on the page).

<input type="hidden" name="__albform__" value="eJzTDJeu3P90rZC6dde04xUhHL
WFjBqhHKXFqUV5ibmphUzeDKFsBYnFxeUphcxANmtOfnpmXiGLN0OpHgB7UBOp
">
<input type="hidden" name="__albstate__" value="eJzT2sr5Jezh942TUrMty6q1j
WsLGUM54uMLEtNT4+MLmUJZc/LTM/MKmYv1AH8XEAY=
">

If we fire up the Python interpreter we can have a look at what these fields contain.

>>> 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 NameRecorderMixin class which is subclassed by SimpleAppContext. If you look at the definition of SimpleAppContext you will notice the following definition at the start of the class.

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 __page__ variable. The 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 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 <al-flush> 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 <al-input type="image"> field used to select each message. An unfortunate limitation of the HTML <input type="image"> 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 <input> 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.

<html>
 <head>
  <title>Message List</title>
 </head>
 <body>
  <al-form method="post">
   <al-input type="submit" name="refresh" value="Refresh">
  </al-form>
  <hr noshade>
  <table>
   <tr align="left">
    <td><b>View</b></td>
    <td><b>To</b></td>
    <td><b>From</b></td>
    <td><b>Subject</b></td>
   </tr>
   <al-for iter="m" expr="mbox">
   <tr align="left" valign="top">
    <td>
     <al-form method="post">
      <al-input type="image" name="detail" src="/icons/generic.gif" border="0">
      <al-input type="hidden" name="msgnum" expr="m.value().msgnum">
     </al-form>
    </td>
    <td><al-value expr="m.value().hdrs['To']"></td>
    <td><al-value expr="m.value().hdrs['From']"></td>
    <td><al-value expr="m.value().hdrs['Subject']"></td>
   </tr>
   <al-if expr="(m.index() % 10) == 9"><al-flush></al-if>
   </al-for>
  </table>
 </body>
</html>

Finally, here is the message detail page detail.html.

<html>
 <head>
  <title>Message Detail</title>
 </head>
 <body>
  <al-form method="post">
   <al-input type="submit" name="list" value="Back">
  </al-form>
  <hr noshade>
  <table>
   <al-for iter="f" expr="('To', 'Cc', 'From', 'Subject')">
    <al-if expr="msg.hdrs[f.value()]">
     <tr align="left">
      <td><b><al-value expr="f.value()">:</b></td>
      <td><al-value expr="msg.hdrs[f.value()]"></td>
     </tr>
    </al-if>
   </al-for>
  </table>
  <hr noshade>
  <pre><al-value expr="msg.body"></pre>
 </body>
</html>

Adding Pagination Support to Popview

If the previous section we constructed a simple application for viewing the contents of a mailbox via the 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 <al-for> tag provides automatic pagination support for displaying sequences. The only extra code that we need to add is a __len__ method to the 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.

cd samples/popview2
python install.py

Add a __len__ method to the Mbox class in popviewlib.py.

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.

<html>
 <head>
  <title>Message List</title>
 </head>
 <body>
  <al-for iter="m" expr="mbox" pagesize="15" prepare/>
  <al-form method="post">
   <al-if expr="m.has_prevpage()">
    <al-input type="image" prevpage="m" src="/icons/left.gif" border="0">
   </al-if>
   <al-input type="submit" name="refresh" value="Refresh">
   <al-if expr="m.has_nextpage()">
    <al-input type="image" nextpage="m" src="/icons/right.gif" border="0">
   </al-if>
  </al-form>
  <hr noshade>
  <table>
   <tr align="left">
    <td></td>
    <td><b>View</b></td>
    <td><b>To</b></td>
    <td><b>From</b></td>
    <td><b>Subject</b></td>
   </tr>
   <al-for iter="m" continue>
   <tr align="left" valign="top">
    <td><al-value expr="m.value().msgnum"></td>
    <td>
     <al-form method="post">
      <al-input type="image" name="detail" src="/icons/generic.gif" border="0">
      <al-input type="hidden" name="msgnum" expr="m.value().msgnum">
     </al-form>
    </td>
    <td><al-value expr="m.value().hdrs['To']"></td>
    <td><al-value expr="m.value().hdrs['From']"></td>
    <td><al-value expr="m.value().hdrs['Subject']"></td>
   </tr>
   </al-for>
  </table>
  <hr noshade>
 </body>
</html>

The <al-for> tag just below the <body> tag contains two new attributes; pagesize and prepare.

The pagesize turns on pagination for the <al-for> 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 <al-for> 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.

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 SessionServerContextMixin and SessionServerAppMixin classes. In this section we will modify the popview.py program to use server-side sessions.

The SessionServerAppMixin class uses a socket to communicate with the session server (see Session Server 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.

cd samples/popview3
python install.py

The new list.html template file follows.

<html>
 <head>
  <title>Message List</title>
 </head>
 <body>
  <al-for iter="m" expr="mbox" pagesize="15" prepare/>
  <al-if expr="m.has_prevpage()"><al-a prevpage="m">Prev</al-a> | </al-if>
  <al-a href="refresh=1">Refresh</al-a>
  <al-if expr="m.has_nextpage()"> | <al-a nextpage="m">Next</al-a></al-if>
  <hr noshade>
  <table>
   <tr align="left">
    <td></td>
    <td><b>To</b></td>
    <td><b>From</b></td>
    <td><b>Subject</b></td>
   </tr>
   <al-for iter="m" continue>
   <tr align="left" valign="top">
    <td align="right">
     <al-a expr="'msgnum=%s' % m.value().msgnum">
      <al-value expr="m.value().msgnum"></al-a>
    </td>
    <td><al-value expr="m.value().hdrs['To']"></td>
    <td><al-value expr="m.value().hdrs['From']"></td>
    <td><al-value expr="m.value().hdrs['Subject']"></td>
   </tr>
   </al-for>
  </table>
  <hr noshade>
 </body>
</html>

Next the new detail.html template file.

<html>
 <head>
  <title>Message Detail</title>
 </head>
 <body>
  <al-a href="list=1">Back</al-a>
  <hr noshade>
  <table>
   <al-for iter="f" expr="('To', 'Cc', 'From', 'Subject')">
    <al-if expr="msg.hdrs[f.value()]">
     <tr align="left">
      <td><b><al-value expr="f.value()">:</b></td>
      <td><al-value expr="msg.hdrs[f.value()]"></td>
     </tr>
    </al-if>
   </al-for>
  </table>
  <hr noshade>
  <pre><al-value expr="msg.body"></pre>
 </body>
</html>

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:

#!/usr/bin/python
from albatross import SimpleSessionApp, SessionAppContext
from albatross.cgiapp import Request
import popviewlib

The execution context now inherits from SessionAppContext:

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:

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 ListPage class because we changed the method of selecting messages from the message list.

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')

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 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.

cd samples/popview4
python install.py

The LoginPage class becomes login.py.

import poplib


def page_process(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(ctx):
    ctx.run_template('login.html')

The ListPage class becomes list.py.

def page_process(ctx):
    if ctx.req_equals('msgnum'):
        ctx.set_page('detail')


def page_display(ctx):
    ctx.open_mbox()
    ctx.run_template('list.html')

And the DetailPage class becomes detail.py.

def page_process(ctx):
    if ctx.req_equals('list'):
        ctx.set_page('list')


def page_display(ctx):
    ctx.open_mbox()
    ctx.read_msg()
    ctx.run_template('detail.html')

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:

#!/usr/bin/python
from albatross import ModularSessionApp, SessionAppContext
from albatross.cgiapp import Request
import popviewlib


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()


class App(ModularSessionApp):

    def __init__(self):
        ModularSessionApp.__init__(self,
                                   base_url='popview.py',
                                   module_path='.',
                                   template_path='.',
                                   start_page='login',
                                   secret='-=-secret-=-',
                                   session_appid='popview4')

    def create_context(self):
        return AppContext(self)


app = App()


if __name__ == '__main__':
    app.run(Request())

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 RandomPageModuleMixin class for this very purpose.

The random sample is provided to demonstrate the use of the RandomModularSessionApp class. Use the install.py script to install the sample.

cd samples/random
python install.py

The complete mainline of the randompage.py sample is shown below.

#!/usr/bin/python
from albatross import RandomModularSessionApp
from albatross.cgiapp import Request


app = RandomModularSessionApp(base_url='randompage.py',
                              page_path='pages',
                              start_page='tree',
                              secret='-=-secret-=-',
                              session_appid='random')


if __name__ == '__main__':
    app.run(Request())

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 http://www.object-craft.com.au/cgi-bin/alsamp/random/randompage.py 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.

<html>
<head><title>Here is a Tree</title></head>
<body>
<al-lookup name="indent">
 <al-item expr="0">  </al-item>
 <al-item expr="1"> |</al-item>
 <al-item expr="2"> \</al-item>
</al-lookup>
<h1>Here is a Tree</h1>
<al-form method="post">
<pre>
<al-tree iter="n" expr="tree">
 <al-for iter="c" expr="range(n.depth())">
  <al-value expr="n.line(c.value())" lookup="indent">
 </al-for>
 -<al-input type="checkbox" alias="n.value().selected">
  <al-value expr="n.value().name" whitespace="newline">
</al-tree>
</pre>
<al-input type="submit" name="save" value="Save">
<al-input type="submit" name="paginate" value="To Paginate Page">
</al-form>
</body>
</html>

During conversion to HTML the <al-form> 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).

from utils import Node

def page_display(ctx):
    ctx.run_template('tree.html')


def page_process(ctx):
    if ctx.req_equals('paginate'):
        ctx.redirect('paginate')
    if not hasattr(ctx.locals, 'tree'):
        ctx.locals.tree \
            = Node('a', [Node('a', [Node('a'),
                                    Node('b')]),
                         Node('b', [Node('a', [Node('a', [Node('a'),
                                                          Node('b')])]),
                                    Node('b'),
                                    Node('c', [Node('a'),
                                               Node('b')])])])
        ctx.add_session_vars('tree')

When the application receives the paginate request it uses the redirect() method to direct the browser to a new page.

The paginate.html page template uses a URLs to capture user input.

<html>
 <head><title>Pagination Example</title></head>
 <body>
  <h1>This is a list with pagination...</h1>

  <al-for iter="i" expr="data" pagesize="10" prepare/>
  <al-if expr="i.has_prevpage()">
   <al-a prevpage="i">prev</al-a>
  </al-if>
  <al-for iter="i" pagesize="10">
   <al-if expr="i.count()">,</al-if> <al-value expr="i.value()">
  </al-for>
  <al-if expr="i.has_nextpage()" whitespace>
   <al-a nextpage="i">next</al-a>
  </al-if>
  <p>
  <al-a href="tree">To Tree Page</al-a>
 </body>
</html>

During conversion to HTML the <al-a> 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.

def page_display(ctx):
    ctx.locals.data = range(100)
    ctx.run_template('paginate.html')

The Albatross Session Server

The Albatross Session Server works in concert with the execution context and application mixin classes in the 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.

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.

$ albatross session-server start
server pid 20653
$ albatross session-server status
running, pid is 20653
$ 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.

Usage: albatross session-server [options]... <command>
        Start or stop an albatross session server

<command> 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

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

#!/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.

description "Albatross Session Server"
author      "Albatross <albatross@object-craft.com.au>"

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.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>Label</key>
        <string>au.com.obect-craft.session-server</string>
        <key>UserName</key>
        <string>albatross</string>
        <key>RunAtLoad</key>
        <true/>
        <key>Program</key>
        <string>/usr/local/bin/albatross</string>
        <key>ProgramArguments</key>
        <array>
                <string>albatross</string>
                <string>session-server</string>
                <string>--foreground</string>
        </array>
</dict>
</plist>

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.

<?xml version="1.0"?>
<!DOCTYPE service_bundle SYSTEM "/usr/share/lib/xml/dtd/service_bundle.dtd.1">
<service_bundle type="manifest" name="al-session-server">

    <service name="application/al-session-server"
             type="service"
             version="1">

        <create_default_instance enabled="false"/>
        <single_instance/>

        <!-- Wait for network interfaces to be initialized.  -->
        <dependency name="network"
                    grouping="require_all"
                    restart_on="none"
                    type="service">
            <service_fmri value="svc:/milestone/network:default"/>
        </dependency>

        <!-- Wait for all local filesystems to be mounted.  -->
        <dependency name="filesystem-local"
                    grouping="require_all"
                    restart_on="none"
                    type="service">
            <service_fmri value="svc:/system/filesystem/local:default"/>
        </dependency>

        <exec_method type="method"
                     name="start"
                     exec="/bin/albatross session-server start"
                     timeout_seconds="60"/>

        <exec_method type="method"
                     name="stop"
                     exec="/bin/albatross session-server stop"
                     timeout_seconds="60"/>

        <template>
            <common_name>
                <loctext xml:lang="C">
                        Albatross session daemon
                </loctext>
            </common_name>
            <documentation>
                <doc_link name="Albatross web site"
                   uri="http://www.object-craft.com.au/projects/albatross"/>
            </documentation>
        </template>

    </service>

</service_bundle>

systemd

To be written - tips gratefully received.

Windows

To be written - tips gratefully received.

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.

Server Protocol

You can see the session server in action by using telnet.

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 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'.

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

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

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.

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

Quit

To disconnect from the server the application sends a line of the form:

"quit" CRLF

The server will then close the connection.

Application Deployment Options

In all of the previous sections you will note that all of the programs used the Request class from the albatross.cgiapp module to deploy the application as a CGI script.

The choice of 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 albatross.cgiapp
mod_python albatross.apacheapp
FastCGI_python albatross.fcgiapp
Stand-alone Python HTTP server albatross.httpdapp

By placing all deployment dependencies in a 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 Request class to deploy an Albatross application in other ways, such as using the Medusa web server (http://www.amk.ca/python/code/medusa.html), or to provide a 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.

CGI Deployment

The albatross.cgiapp module contains a 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:

#!/usr/bin/python
from albatross.cgiapp import Request

class Application(...):
    ...

app = Application()
if __name__ == '__main__':
    app.run(Request())

mod_python Deployment

The albatross.apacheapp module contains a Request class to allow you to deploy your application using mod_python [2].

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.

cd samples/popview5
python install.py

The new popview.py mainline follows.

from albatross import ModularSessionApp, SessionAppContext
from albatross.apacheapp import Request
import popviewlib


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()


class App(ModularSessionApp):

    def __init__(self):
        ModularSessionApp.__init__(self,
                                   base_url='popview.py',
                                   module_path='-=-install_dir-=-',
                                   template_path='-=-install_dir-=-',
                                   start_page='login',
                                   secret='-=-secret-=-',
                                   session_appid='popview5')

    def create_context(self):
        return AppContext(self)


app = App()


def handler(req):
    return app.run(Request(req))

The 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. :

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:

<Directory /var/www/alsamp/>
    AllowOverride FileInfo Indexes
    Order allow,deny
    Allow from all
</Directory>

FastCGI Deployment

The albatross.fcgiapp module contains a Request class to allow you to deploy your application using FastCGI [3].

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 Request class in your application. As an example of Apache configuration:

LoadModule fastcgi_module /usr/lib/apache/1.3/mod_fastcgi.so

<IfModule mod_fastcgi.c>
    <Directory /usr/lib/cgi-bin/application/>
        AddHandler fastcgi-script py
        Options +ExecCGI
    </Directory>
</IfModule>

And the application main-line:

#!/usr/bin/python
from albatross.fcgiapp import Request, running

class Application(...):
    ...

app = Application()
if __name__ == '__main__':
    while running():
        app.run(Request())

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:

$ cd samples/tree2
$ albatross serve --port=8080 tree.app /alsamp/images ../images/

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 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 UserError, this exception is raised when Albatross detects potentially hostile client activity.

exception TemplateLoadError

A subclass of ApplicationError, this exception is raised if a template cannot be loaded.

exception SessionExpired

A subclass of UserError, this exception is raised when a client attempts to submit form results against a session that has expired.

Footnotes

[1]Prior to Albatross version 1.36, in the absence of an __albform__ field, all request fields would be merged.
[2]For more information on mod_python, including installation instructions, see http://www.modpython.org/.
[3]For more information on FastCGI, including installation instructions, see http://www.fastcgi.com/.