Login

Repoze Blog

2007-12-18 00:00:00-05:00

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: 00:00 | permalink