mike watkins dot ca : January 21 2009 Archives

January 21 2009

QP and WSGI

QP doesn't use WSGI itself in the operation of its own built-in web and SCGI servers, but the framework does make it easy to drive QP applications via WSGI if you have a need to. Here's a quick how-to.

Exposing the QP application as a WSGI app is simple. Create a driver file (let's name it runwsgi.py) and place it in your Python path. Myself I prefer to put such files in my QP application's "site" directory.

# this is runwsgi.py
from qp.lib.site import Site
site = Site('songs')
application = site.get_publisher()

You can then run a WSGI server (such as Spawning) from the command line:

% spawn -t 0 -p 8000 qp.sites.songs.runwsgi.application

Or we can extend runwsgi.py to also provide a self-contained WSGI server:

#! /usr/bin/env python
# this is runwsgi.py
from qp.lib.site import Site
site = Site('songs')
application = site.get_publisher()

if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    host, port = site.get_http_address()
    httpd = make_server(host, port, application)
    hp = httpd.socket.getsockname()
    print("Serving HTTP on %s port %d..." % (hp[0], hp[1]))
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        print("\nStopping server(s)")
        if site.is_durus_running():
            site.stop_durus()

We can execute this directly; the application will use the same host and port defined in our application's configuration.

% ./runwsgi.py

Python 3 Compatibility

In keeping with my blog's objective of only publishing examples that are compatible with both Python 2.x and 3.x, I need to point out that the QP code presented certainly will run on Python 3.0 but the wsgi servers (spawning and the reference WSGI server) noted will not.

While QP and the rest of its stack is compatible with Python >= 2.4 (yes, that means Python 3.x too), the reference implementation of WSGI supplied with Python 3.0 is broken. As far as I know the only working WSGI server implementation for Python 3 at present is mod_wsgi, available for Apache here.

Resty applications in QP, Part II

In this installment we'll take a break from the implementation started in the post previous of a QP and Durus based RESTy song database application loosely following the meme which has arisen from Eric Florenzano's example bare-metal WSGI implementation of same. Today I want to point out some QP features which are useful and perhaps a little unusual.

Basics

While QP may not be a bare-metal framework, it also isn't an mega-framework encapsulating a ton of functionality. The designer's ethos might well be extracted from the last line in QP's README:

The abbreviation "qp" stands for "quantum placet", the Latin phrase meaning "as much as you please".

QP provides basic but solid building blocks for web applications out of the box, including via its partner package QPY high level protection against XSS (cross site scripting) attacks and a novel and very fast Python templating system. You'll also get a user authentication system including form, HTTP Basic and HTTP Digest authentication.

QP Hello World Prototype

QP application's typically minimally consist of a SitePublisher and SiteDirectory class. SitePublisher contains a configuration dictionary which can be used for both the framework and your applications. Since we are using the built-in Durus object database-backed User database for the Songs application, we'll employ a DurusPublisher and instruct the qp site management command line utility to start a web and Durus daemon:

class SitePublisher(DurusPublisher):

    configuration = dict(
        durus_address=('localhost', 7023),
        http_address=('', 8023),
        )

The "root directory" of a typical QP application is a class named SiteDirectory. A hello world application driven by the above publisher would look like this:

class SiteDirectory(Directory):

    def get_exports(self):
        # URI segment exported, method name, crumb, title
        yield ('', 'index', 'Home', 'Home page of the site')

    def index(self):
        return 'hello, world.'

That's it.

Starting a QP application using the built-in site management tool is simple:

% qp -u songs

QP includes all the facilities for starting and stopping web and other processes. It uses a multi-process rather than multi-thread model and spawns new worker processes (configurable) as needed. Multi-CPU / multi-core machines can eek out significant performance with nothing but the base QP servers; for a large site typically I'll put the QP application behind an Apache or lighttpd web daemon (using either a proxy or SCGI configuration), and serve the static content with those servers. QP can also play in the WSGI space - look for a quick how-to in the next post.

QPY Templates

Let us now demonstrate one of the cooler or unusual features of QP / QPY - Python centric templating. Whether this model works for you or not depends a lot on how you look at code and (x)HTML / text templates. Programming teams who are also responsible for the site layout may find refreshing the QPY approach of putting content-in-Python, as opposed to most templating packages which focus on enabling Python-like languages within text / HTML / XMl templates.

An example should make things clear. We'll write a method to say hello back to us n-times based on url parameters age and name, to respond to a query like:

curl http://localhost:8023/?age=3&name=Mary

Instead of using return to send a string back to the Publisher, we use a special notation on our method - :xml:

from qp.pub.common import get_request

class SiteDirectory(Directory):

    def index:xml(self):

        context = get_request().fields
        '''
        <html>
        <head>
            <title>Hello</title>
        </head>
        <body>
            <ol>
        '''
        for i in range(int(context.get('age', 1))):
            '<li>%(name)s, %(age)s years old</li>' % context
        '''
            </ol>
        </body>
        </html>
        '''

When index is called the string literal will be completed and passed back as a return value. Its a much cleaner looking approach than building up a string.

The method descriptor :xml is enabled by QPY. There are two such descriptors provided by QPY, :xml and :str. String literals within :xml templates are cast as a quote no more type, while variable data passed to the template (such as the context dictionary in the example) will be escaped or quoted for safety once and only once as it is included in the template. :str templates provide no automatic escaping and quoting.

The QPY templating system provides four major benefits:

  1. You already know the language because it is pure Python.
  2. Automatic protection from cross site scripting (XSS) attacks and other malicious value injection.
  3. Unicode sane and safe, like the rest of the QP stack.
  4. QPY is uncomplicated and very fast.

Speaking of Unicode, QP applications treat all text data internally as Unicode. The default character set is utf-8. Writing QP / QPY applications to run on either Python 2.x or 3.x requires no special Unicode gymnastics.

In addition, all parts of the QP stack - Durus, QPY and QP itself - are Python 3 compatible, today. QPY is packaged separately from QP to make it accessible for other templating package authors to employ features such as its quote no more class and Python extensions.

Authentication

QP's default authentication method is HTTP Digest; this can be changed by trivially overriding the ensure_signed_in() method of Publisher. For the SongDatabase API we won't alter the default since Form isn't workable for a RESTful API, and using Digest over Basic gives us the ability to offer at least authentication security without using HTTPS.

If we extended our hello, world prototype to provide some more functionality if you are logged on user, and even more if you are an administrator, it would look like this:

from qp.pub.common import get_publisher, get_user

class SiteDirectory(Directory):

    def get_exports(self):
        # URI segment exported, method name, crumb, title
        yield ('', 'index', 'Home', 'Home page of the site')
        get_publisher().ensure_signed_in()
        yield ('users', 'users', 'Users', 'Pithy title here')
        if get_user().is_admin():
            yield ('secrets', 'top_secret', 'Home', 'Secrets', 'Something intriguing')

    def index(self):
        return 'hello, world.'

    def users(self):
        return 'hello, real user %s' % get_user().id

    def top_secret(self):
        return 'the password is "orange". Sssh!'

In the foregoing example an unauthenticated user will not even be able to navigate to /users or /secrets. An authenticated user who isn't also a site administrator will not be able to navigate to /secrets.

One could put the authentication checks within an exported method instead, but the nice thing about using the get_exports mechanism is menu and crumb generation routines also use the data yielded, so putting auth checks there is a benefit if you prefer not to show your site users menus they don't currently have authorization to access.

Coming up next, a quick note on running QP as a WSGI application, and we'll get back to the Song application.