Repoze Notes

Tue, 18 Dec 2007

Adding repoze.tm to a Pylons Application

After sprinting with the PyATL folks in Atlanta last week, Chris and I decided to dive into the "share Zope stuff with Python web developers" story a little more. We wanted to show that various bits of Zopeish middleware might be useful in the context of the other WSGI-enabled web frameworks.

I worked through the How to write a basic blog with Pylons tutorial, using the sqlite backend for simplicity. Once done, I looked at the code in the application which did manual / explicit transaction handline in the controller method for the blog add form POST:


    def blog_add_process(self):
        # Create a new Blog object and populate it.
        newpost = model.Blog()
        newpost.date = datetime.datetime.now()
        newpost.content = request.params['content']
        newpost.author = request.params['author']
        newpost.subject = request.params['subject']
        # I didn't set ID because it will get an autoincrement value.
        
        # Attach the object to the session.
        model.Session.save(newpost)

        # Commit the transaction.
        # (This sends the SQL INSERT command due to autoflushing.)
        model.Session.commit()
        redirect_to("/blog")

Using the transaction Framework in the Application

I decided to knock together a simple "data manager" for this application, following Chris' Transactions in WSGI tutorial. The class implements the IDataManager API, using the ORM session to do the real work:

class DataManager(object):

    transaction_manager = None

    def __init__(self, post):
        self.post = post

    def commit(self, transaction):
        """ See IDataManager.
        """
        model.Session.save(self.post)

    def abort(self, transaction):
        """ See IDataManager.
        """
        model.Session.rollback()

    def tpc_begin(self, transaction):
        """ See IDataManager.
        """

    def tpc_vote(self, transaction):
        """ See IDataManager.
        """

    def tpc_finish(self, transaction):
        """ See IDataManager.
        """
        model.Session.commit()

    def tpc_abort(self, transaction):
        """ See IDataManager.
        """
        model.Session.rollback()

    def sortKey(self):
        """ See IDataManager.
        """
        return 'myblog-sql'

I then modified the controller method such that it registers an instance of the DataManager class with the transaction. Note the addition of pseudo-validation logic, which triggers an exception in order to demonstrate the "auto-rollback" feature of repoze.tm:


import transaction
...
    def blog_add_process(self):
        # Create a new Blog object and populate it.
        newpost = model.Blog()
        newpost.date = datetime.datetime.now()
        newpost.content = request.params['content']
        newpost.author = request.params['author']
        newpost.subject = request.params['subject']

        # Register with the global two-phase transaction manager
        dm = DataManager(newpost)
        transaction.get().join(dm)

        # Trigger an error on "invalid" data, to trigger the abort.
        if newpost.subject.startswith('Abort'):
            raise ValueError('Invalid data')

        redirect_to("/blog")

Configuring the Application

First, I needed to install repoze.tm and its dependencies:


  $ ../bin/easy_install -i http://dist.repoze.org/simple repoze.tm

Then, I needed to wire the repoze.tm middleware into the PasteDeploy configuration. In order to add middleware, I renamed the [app:main] section::


[app:myblog]
use = egg:MyBlog
# Let pipeline handle errors
full_stack = false
cache_dir = %(here)s/data
beaker.session.key = myblog
beaker.session.secret = somesecret
sqlalchemy.url = sqlite:///%(here)s/db.sqlite
sqlalchemy.convert_unicode = true

Note that I turned off the full_stack option, because I want errors to propagate out to the middleware, so that it can abort the transaction.

I then defined a [pipeline:main] section, adding both the transaction middleware and some error handling::


[pipeline:main]
pipeline =
           egg:Paste#cgitb
           egg:Paste#httpexceptions
           egg:repoze.tm#tm
           myblog

At this point, the application works as desired:

  • "Normal" posts get added to the table
  • "Invalid" posts (those whose subject starts with "Abort"), are blocked.

posted at: 17:36 | permanent link to this entry