Developing Web Applications with Quixote

PyCon 2004
March 24, 2004

A.M. Kuchling
www.amk.ca
amk @ amk.ca

Overview

Quixote is a Web development framework written in Python.

Some of Quixote's goals:

Related tools:

Sites/Applications using Quixote

Sites:

Applications:

MX: site for distributed semiconductor fabrication

MX: Quixote was originally written for implementing it

LWN: the highest-traffic Quixote site

LWN: demonstrates that QX can be pretty scalable

LWN: Survived a Slashdotting w/ 1GHz Pentium, 512Mb of RAM

Cartwheel: genomic sequence analyses

Solidus: a Slashdot clone being developed

Other apps: Jim Dukarm's app for analyzing oil tests

The Quixote Toolkit

How Quixote Works: The basic idea

Simple example

From quixote/demo/__init__.py:

# Every publicly accessible attribute has to be listed in _q_exports.
_q_exports = ["simple"]  

def simple (request):
    # This function returns a plain text document, not HTML.
    request.response.set_content_type("text/plain")
    return "This is the Python function 'quixote.demo.simple'.\n"

How Quixote Works: Special names

_q_index: If traversal ends up at an object that isn't callable, this name is checked for and called.

_q_lookup: if an attribute isn't found, this name is checked for and called with the attribute.

_q_access: at every step, this name is checked for and called to perform access checks.

_q_resolve: like a memoized version of _q_lookup (rarely used)

Names all begin with _q_.

_q_lookup: used for building URLs taken from a database

_q_access: can require that users be logged in, or check that logged-in users have a particular privilege

_q_resolve: used to avoid having to import everything on application startup

How Quixote Works: _q_lookup example

This example handles URLs such as /whatever/1/, .../2/, etc.

def _q_lookup (request, component):
    try:
        key = int(component)
    except ValueError: 
        raise TraversalError("URL component is not an integer")

    obj = ... database lookup (key) ...
    if obj is None:
        raise TraversalError("No such object.")

    # Traversal will continue with the ObjectUI instance
    return ObjectUI(obj)

How Quixote works: _q_access example

_q_access is always called before traversing any further.

This example requires that all users must be logged in.

from quixote.errors import AccessError, TraversalError

def _q_access (request):
    if request.session.user is None:
         raise AccessError("You must be logged in.")

    # exits quietly if nothing is wrong


def _q_index [html] (request):
    """Here is some security-critical material ..."""

How Quixote works: The HTTPRequest class

How Quixote works: The HTTPResponse class

How Quixote works: Enabling sessions

Instead of Publisher, use SessionPublisher:

from quixote.publisher import SessionPublisher
app = SessionPublisher('quixote.demo')

The request will then have a .session attribute containing a Session instance.

Two other classes:

SessionManager is a dictionary-like object responsible for storing sessions. Default is in-memory; possible to use ZODB/SQL/whatever.

The only interesting thing on Session is probably .user.

Running Quixote applications

Several options:

Running Quixote applications: CGI/FastCGI

demo.cgi:

#!/www/python/bin/python
# Example driver script for the Quixote demo: 
# publishes the quixote.demo package.

from quixote import Publisher

# Create a Publisher instance, giving it the root package name
app = Publisher('quixote.demo')

# Open the configured log files
app.setup_logs()

# Enter the publishing main loop
app.publish_cgi()

This will also handle FastCGI. CGI scripts will run through publish_cgi() once and exit; under FastCGI it will loop.

Running Quixote Applications: Stand-alone

Running a server on localhost is really easy:

import os, time 
from quixote.server import medusa_http 
 
if __name__ == '__main__': 
    s = medusa_http.Server('quixote.demo', port=8000) 
    s.run() 

Easy-to-write application: run a Quixote server locally and use Python's webbrowser.open() module to open a browser pointing at it.

PTL: Overview

PTL = Python Templating Language

example.ptl:

# To callers, templates behave like regular Python functions 
def cell [html] (content):
    '<td>'         # Literal expressions are appended to the output
    content        # Expressions are evaluated, too.
    '</td>'

def row [html] (L):
    # L: list of strings containing cell content
    '<tr>'
    for s in L:
        cell(s)
    '</tr>\n'

def loop (n):  # No [html], so this is a regular Python function
    output = ""
    for i in range(1, 10):
        output += row([str(i), i*'a', i*'b'])
    return output

PTL: Using templates

Templates live in .ptl files, which can be imported. To enable this:

import quixote ; quixote.enable_ptl()    # Enable import hook

Templates behave just like Python functions:

>>> import example
>>> example.cell('abc')
<htmltext '<td>abc</td>'>
>>> example.loop()
<htmltext '<tr><td>1</td><td>a</td><td>b</td>...</tr>\n'>

In .ptl files, methods can even be PTL files.

PTL: Comparison with other syntaxes

System Syntax
Apache SSI <!--#include virtual="/script/"-->
PHP <?php func()?>
ASP <% func() %>
ZPT <span tal:replace="content">...</span>
PTL def f [html] (): content

PTL's advantages:

PTL: Automatic escaping

def no_quote [plain] (arg):
    '<title>'
    arg              # Converted to string
    '</title>'

def quote [html] (arg):
    '<title>'
    arg              # Converted to string and HTML-escaped
    '</title>'

>>> no_quote('A history of the < symbol')
'<title>A history of the < symbol</title>'
>>> quote('A history of the < symbol')
<htmltext '<title>A history of the &lt; symbol</title>'>

By using '[html]' instead of '[plain]', string literals are compiled as htmltext instances. When combined with regular strings using a + b or '%s' % b, htmltext HTML-escapes the regular string.

We think PTL is really neat, but it's optional. Using alternative templating isn't hard.

Toolkit: Form processing

Quixote contains a set of classes for implementing forms. Example:

from quixote.form import Form

class UserForm (Form):
    def __init__ (self):
        Form.__init__(self)
        user = get_session().user
        self.add_widget("string", "name", title="Your name",
                        value=user.name)
        self.add_widget("password", "password1", title="Password",
                        value="")
        self.add_widget("password", "password2", 
                        title="Password, again", value="")
        self.add_widget("single_select", "vote",
                        title = "Vote on proposal",
                        allowed_values=[None] + range(4),
                        descriptions=['No vote', '+1', '+0', 
                                      '-0', '-1'],
                        hint = "Your vote on this proposal")
        self.add_widget("submit_button", "submit", 
                        value="Update information")

Widgets are provided for strings, ints, floats, single selections, multiple selections, weird ones such as OptionSelect. We've written ones for dates, materials, etc.

Toolkit: Form processing (cont'd)

                   
class UserForm (Form):
    ...
    def render [html] (self, request, action_url):
        standard.header("Edit Your User Information")
        Form.render(self, request, action_url)
        standard.footer()
        
    def process (self, request):
        values = Form.process(self, request)
        if not (values['password1'] == values['password2']):
            self.error['password1'] = 'The two passwords must match.'
        return values
   
    def action (self, request, submit, values):
        user = request.session.user
        user.name = values['name']
        if values['password1'] is not None:
            user.password = values['password1']
        return request.response.redirect(request.get_url(1))

render() draws -- we're just going to wrap our header/footer around the default rendering.

process() gets the values and performs error checks.

action() does the work of the form, and can assume the input data is all correct.

Toolkit: Serving Static Files

For Quixote-only apps, you often need to return static files such as PNGs, PDFs, etc.

from quixote.util import StaticFile, StaticDirectory

_q_exports = ['images', 'report_pdf']

report_pdf = StaticFile('/www/sites/qx/docroot/report.pdf',
                        mime_type='application/pdf' 
images = StaticDirectory('/www/sites/qx/docroot/images/')

The quixote.util module also contains helpers for XML-RPC, for streaming files back to the client, etc.

Omit the MIME type and the mimetypes module will be used to guess.

StaticDir defaults to security: it doesn't follow symlinks or allow listing the directory, though you can enable this.

Both can optionally cache things in memory.

SQL: Defining a class with SQLObject

It's possible to use SQL "by hand".

Easier to use SQLObject: an object/relational mapper written by Ian Bicking (www.sqlobject.org)

This example is from Solidus, a Slashcode clone I'm working on.

from SQLObject import *
class Story (SQLObject):
    section = ForeignKey('Section', default=None)
    topic = ForeignKey('Topic', default=None)
    user = ForeignKey('User')           # User who approved it
    submitter = ForeignKey('User')      # User who submitted it
    discussion = IntCol(default=None)

    title = StringCol(default=None)
    department = StringCol(default=None)
    intro = StringCol(default=None)

    timestamp = DateTimeCol(default=datetime.now)
    deleted = BoolCol(default=False)
    approved = BoolCol(default=False)

Using SQLObject: basic tasks

from SQLObject import *
connection = SQLiteConnection('/tmp/database.db')

# Creating the table
Story.createTable()

# Retrieve a particular story by ID
s = Story(id=1)

# Updating a story
user = User(id='amk')
s = Story()
s.submitter = user      # Immediately issues SQL 'update' command

Using SQLObject: basic tasks

# Performing a query
def get_recent_stories (N=10, topic=None, approved=True):
    """(int, Section, bool): [Story]
    Return the N most recent stories in the (optional)
    topic, with the requested approval status.
    """
    q = AND(Story.q.approved == approved,
            Story.q.deleted == False)
    
    if topic is not None:
        q = AND(q, Story.q.topicID == topic.id)

    stories = Story.select(q, orderBy="-timestamp", )
    stories = stories[:N]
    stories = list(stories)
    return stories

Design patterns: Good URL design matters

The canonical bad URL:

http://gandalf.example.com/cgi-bin/catalog.py
  ?item=9876543&display=complete&tag=nfse_lfde

A better set of URLs:

http://www.example.com/catalog/9876543/complete
                                   .../brief 
                                   .../features  

Quixote features such as _q_lookup make it easy to support sensible URLs.

Design patterns: Separate problem objects and UI classes

Don't mix the basic objects for your problem with the HTML for the user interface.

For each object, represent it by a class and put the user interface in a *UI class elsewhere.

Advantages:

Structure of an application: PyCon proposal submission

Directory organization:

qx/bin/                       # Various scripts 
qx/conference/__init__.py     # Marker
qx/conference/objects.py      # Basic objects: Proposal, Author, Review
qx/ui/conference/__init__.py
qx/ui/conference/email.ptl    # Text of e-mail messages
qx/ui/conference/standard.ptl # Header, footer, display_proposal()
qx/ui/conference/pages.ptl    # Login form, base CSS
qx/ui/conference/proposal.ptl # ProposalUI class

Scripts: creating conference committee, reassigning reviewers for a prop., listing accepted proposals.

standard.ptl: Display dates, proposal titles, etc. Handy for getting a uniform look.

Design patterns: common filenames

Naming conventions for common modules:

Questions, comments?

These slides: www.amk.ca/talks/quixote

Quixote home page: www.mems-exchange.org/software/quixote

Quixote resources: