a blog by Marius Gedminas

How to use Zope 3 generations

When you're developing a Zope 3 based application, and you want to make old object databases continue to work as you change object attributes, you should consider Zope 3 database generations.

The DatabaseGenerations proposal in the Zope 3 wiki describes the problem and possible solutions. README.txt for the zope.app.generations package tells you about the internals. The zope.app.zopeappgenerations package is a real-world example of generations. This mini-article strives to be a HOWTO.

This is a DRAFT version. I plan to show it to Zope 3 gurus, get feedback, and fix all bugs. After that, I will remove this notice.

Short summary

Here's how it works (slightly simplified): you tell Zope 3 the name of your application, and the current generation number. Zope 3 stores this number in the ZODB. Later, when you change your application, you will change the current generation number and write a function that can upgrade existing data. On every startup Zope 3 will check the current generation number with the number stored in the ZODB for your application, and, if necessary, call your upgrade functions.

Zeroth version

So, you have a Zope 3 based application that is being used. You have worked long and hard on new shining features, but before you release a newer version, you want to make sure existing users can upgrade without losing any data. You decide to use generations. How do you start?

Well, you should have declared that the old version of your application used generation 0 long ago. However all is not lost, you can rectify that omission without too much trouble. Add the following bit of ZCML into all Zope instances that run the old version of your application:

<utility
    name="myapp"
    provides="zope.app.generations.interfaces.ISchemaManager"
    factory="zope.app.generations.generations.SchemaManager"
    />

Now restart that Zope 3 and go to http://localhost:8080/++etc++process/@@generations.html. Make sure your application is shown there. Congratulations -- you have just told your Zope that the old version of your application used database generation 0. This is necessary if you want to write an upgrade script to your new application version, which will be generation 1.

First version

Create a new package, e.g. myapp.generations. This package will contain the current version number and database upgrade scripts. Create a __init__.py with the following contents:

from zope.app.generations.generations import SchemaManager

schemaManager = SchemaManager(
    minimum_generation=1,
    generation=1,
    package_name='myapp.generations')

Create a evolve1.py with the following contents:

from zope.app.zopeappgenerations import getRootFolder

def evolve(context):
    """Upgrade myapp database from generation 0 to generation 1.

    (describe database schema changes here)
    """
    root_folder = getRootFolder(context)
    # write code to perform database upgrades here
    # e.g.
    from zope.app.generations.utility import findObjectsProviding
    for myobj in findObjectsProviding(root_folder, IMyAppObject):
        myobj.newattribute = 42
    from zope.app.generations.utility import findObjectsMatching
    for myobj in findObjectsMatching(root_folder,
                                     lambda ob: isinstance(ob, MyCppClass)):
        myobj.newattribute = 42

Create a configure.zcml file with the following content:

<configure xmlns="http://namespaces.zope.org/zope">

  <utility
      name="myapp"
      provides="zope.app.generations.interfaces.ISchemaManager"
      component=".schemaManager"
      />

</configure>

Test the script with an old Data.fs file.

Now when you upgrade your application, Zope 3 will automatically run your evolve function once.

Further versions

Increment generation and minimum_generation in __init__.py, and write a new evolveNUM.py module as soon as you make a change to the database schema. Update the evolve function as you continue evelopment, but freeze it when you make a new release.

More on PyBlosxom

I want to store my blog in Subversion. Problem: Subversion does not preserve mtimes of files. Solution: a plugin like pyfilenamemtime, but different (I do not want to rename existing blog entries). Since I want to import my existing blog entries without renaming them, I wrote a plugin that reads timestamps from a separate text file. This should suffice, in addition to adding

[miscellany]
### Set use-commit-times to make checkout/update/switch/revert
### put last-committed timestamps on every file touched.
use-commit-times = yes

to ~/.subversion/config.

Now I want to have an RSS or Atom feed that validates. Answer: rss2renderer.

I'd like to have browsable archives for entries that do not fit on the front page (and then make the front page contain fewer entries). There are plugins in the PyBlosxom plugin registry that look promising: pyarchives, wbgpager.

More wishes: I want to be able to post blog entries with images. I'd like to be able to be able to add new blog entries with gnome-blogger and via a web form. And I want to have drafts that I can look at until I become satisfied and publish.

Stay tuned, if I figure out how to accomplish what I want, I'll blog about it.

IRC logs

So I've got supybot running and logging to a text file (one file per day). My next goal was to produce nice, aesthetically pleasing IRC logs in HTML.

First I defeated the urge to write my own IRC log to HTML converter from scratch. Then I started googling and was surprised how hard it was to find anything. I went through the whole IRC section in freshmeat.net. I finally decided that irclog2html.pl by Jeff Waugh was the closest thing to my ideal, with only two deficiencies:

  1. It's written in Perl, so customization is difficult.
  2. It produces sloppy HTML4 output in ISO-8859-1, while I wanted XHTML/CSS in UTF-8.

The obvious next step was to port irclog2html.pl to Python, refactor it so that customizations (e.g. adding a new output style) are straightforward, and then improve it. I also wrote a test script that runs both irclog2html.pl and irclog2html.py and compares the output. Unfortunately, I did not have any log files to test, so there may be some remaining bugs or just differences.

That's how I spent the night from 1 AM to 9:30 AM. At the end I have irclog2html.py that has a couple of new output styles (xhtml and xhtmltable), some bug fixes, understands ISO 8601 timestamps (YYYY-MM-DDTHH:MM:SS, such as found in irc logs produced by supybot's ChannelLogger), and can produce navigation links (prev, next, index) if you specify them on the command line. You can see the end result here:

SchoolTool IRC logs

I also wrote a second script, logs2html.py. It finds all log files in a directory, compares their mtimes to mtimes of corresponding html files, and runs irclog2html.py for logs that have changed. It also produces an index page and passes the necessary command line options to irclog2html.py to create navigational links. This script now from cron runs every five minutes.

And here's the stylesheed used by both scripts: irclog.css.

Today I wrote an ugly hacky script to split XChat log files into daily IRC log files suitable as input for logs2html.py, so that I could import past IRC conversations. I'm not publishing it because it's very ugly.

Update: irclog2html now has a web page. You can find the latest version there.