11.30.07

Using ZopeSkel to raise Plone projects quality

Posted in plone, python, quality, zope tagged at 11:05 am by Tarek Ziadé

At Ingeniweb, we have started to define standards for our Plone projects using ZopeSkel. IngeniSkel is a thin layer on the top of ZopeSkel, that was:

  • injecting Archetype content within existing products
  • defining standard tests skeletons for all elements contained in products.
  • providing other templates like the recipe one, that creates a skeleton for zc.buildout recipes

The injection idea was previously proposed by Martin, and Mustapha started to implement it in a branch. Erik F. has added the archetype content injection yesterday.

It means you can now inject new archetype based content with the Paster directly with ZopeSkel:

$ paster create -t archetype my.package $ cd my.package  $ paster --help
usage: paster [paster_options] COMMAND [command_options]
options:
  …
Commands:
  create       Create the file layout for a Python distribution
  …
ZopeSkel local commands:
  addcontent   Adds plone content types to your project  $ paster addcontent –list
Available templates:
  contenttype:  A content type skeleton
  portlet:      A Plone 3 portlet
  view:         A browser view skeleton
  zcmlmeta:     A ZCML meta directive skeleton $ paster addcontent

This is great, now the only thing it misses so we can drop IngeniSkel in favor of this enhanced version of ZopeSkel is generating tests modules on all templates and local commands, and the same way everytime. I started such a work in another branch to backport what we have it and I will propose it.

Why ? Because having tests that are written the same way on all layers of a Plone project is important to:

  • automate some QA and documentation tasks
  • make sure a newcomer won’t get lost on how to startup a new piece of code, with the right test fixture. If he takes too much time to prepare the test fixture, he’ll probably drop the TDD approach…

Edit: I have merged the the recipe template into ZopeSkel trunk already, as I’ve been asked to

11.11.07

Controlling Cache headers with zope.testbrowser

Posted in plone, python, zope tagged at 10:26 am by Tarek Ziadé

On a Plone website, setting the cache headers with CacheFu or other tools is quite easy, but when you need to check on a given page which headers are inserted, it’s not always easy to know what will be called in the stack of rules you have set.

You can look at each page properties in you web browser to check this, but it is a pain and you won’t be able to make sure nothing is broken when you change, for example, your CacheFu settings.

So, having a dedicated functional test to control your website cache settings is a good way to avoid regressions. zope.testbrowser allows you to write this on a doctest, so it’s easy to gather all your cache headers control in one text file. To install it, you can use Easy Install:

$ easy_install zope.testbrowser

Here’s an example of such doctest (cache.txt):

=============
Testing cache
=============

    >>> from zope.testbrowser.browser import Browser

This method is used to print a page headers::

    >>> def headers(url, login=None, password=None):
    ...		b = Browser()
    ...		if login is not None and password is not None:
    ...			b.addHeader('Authorization',
    ...			     	    'Basic %s:%s' % (login, password))
    ...		b.open(url)
    ...		print b.headers

Let's try on the python front page::

    >>> headers('http://python.org')
    Date: ...
    Server: Apache/2.2.3 (Debian) DAV/2 SVN/1.4.2 mod_ssl/2.2.3 OpenSSL/0.9.8c
    Last-Modified: ...
    ETag: "60193-3fd1-b811ca00"
    Accept-Ranges: bytes
    Content-Length: 16337
    Connection: close
    content-type: text/html; charset=utf-8

The page returns an ETag header, which tells the browser if the page has changed.

Let's try Plone's one::

    >>> headers('http://plone.org')
    Server: nginx/0.5.26
    Date: ...
    Connection: close
    Content-Language: en
    X-Cache-Headers-Set-By: CachingPolicyManager: /plone.org/caching_policy_manager
    Expires: ...
    Vary: Accept-Encoding
    Last-Modified: Sun, 04 Dec 2005 12:13:31 GMT
    X-Cache-Rules-Applied: yes
    X-Caching-Rule-Id: frontpage
    Cache-Control: max-age=0, s-maxage=3600, must-revalidate
    X-Header-Set-Id: cache-in-proxy-1-hour
    Content-Length: 44947
    X-Varnish: 783893351 783878475
    Age: 3364
    Via: 1.1 varnish
    Content-Type: text/html;charset=utf-8
    imagetoolbar: no

It has more complex cache information (CacheFu) used by Varnish cache software.

Notice the use of Ellipsis (…) that replaces changing data like Dates. It can be called using a python module that can look like this:

import os
import unittest
import doctest

OPTIONFLAGS = (doctest.REPORT_ONLY_FIRST_FAILURE |
                        doctest.ELLIPSIS |
                        doctest.NORMALIZE_WHITESPACE)

def test_suite():
    return doctest.DocFileSuite(os.path.basename('cache.txt'),
                                         optionflags=OPTIONFLAGS)

if __name__ == '__main__':
    test_suite()

This is not specific to Plone, and can be used on any website. Added to the stack of tests, this helps a lot on making sure the cache settings are doing what they are supposed to.

11.01.07

Scheduling tasks in Windows with PyWin32

Posted in plone, python, windows, zope tagged at 12:23 pm by Tarek Ziadé

I am posting this small entry because it took me quite a time to compute all informations to programmatically add tasks in Windows using pywin32, so this post should be an helpfull example.

The process to schedule a task is as follows:

  • create an instance of the COM TaskScheduler interface
  • adding a task within the task scheduler
  • adding a trigger within the task

Here’s how it can look in Python, to create daily tasks:

import pythoncom, win32api
import time
from win32com.taskscheduler import taskscheduler

def create_daily_task(name, cmd, hour=None, minute=None):
    """creates a daily task"""
    cmd = cmd.split()
    ts = pythoncom.CoCreateInstance(taskscheduler.CLSID_CTaskScheduler,None,
                                    pythoncom.CLSCTX_INPROC_SERVER,
                                    taskscheduler.IID_ITaskScheduler)

    if '%s.job' % name not in ts.Enum():
        task = ts.NewWorkItem(name)

        task.SetApplicationName(cmd[0])
        task.SetParameters(’ ‘.join(cmd[1:]))
        task.SetPriority(taskscheduler.REALTIME_PRIORITY_CLASS)
        task.SetFlags(taskscheduler.TASK_FLAG_RUN_ONLY_IF_LOGGED_ON)
        task.SetAccountInformation(”, None)
        ts.AddWorkItem(name, task)
        run_time = time.localtime(time.time() + 300)
        tr_ind, tr = task.CreateTrigger()
        tt = tr.GetTrigger()
        tt.Flags = 0
        tt.BeginYear = int(time.strftime(’%Y’, run_time))
        tt.BeginMonth = int(time.strftime(’%m’, run_time))
        tt.BeginDay = int(time.strftime(’%d’, run_time))
        if minute is None:
            tt.StartMinute = int(time.strftime(’%M’, run_time))
        else:
            tt.StartMinute = minute
        if hour is None:
            tt.StartHour = int(time.strftime(’%H’, run_time))
        else:
            tt.StartHour = hour
        tt.TriggerType = int(taskscheduler.TASK_TIME_TRIGGER_DAILY)
        tr.SetTrigger(tt)
        pf = task.QueryInterface(pythoncom.IID_IPersistFile)
        pf.Save(None,1)
        task.Run()
    else:
        raise KeyError(”%s already exists” % name)

    task = ts.Activate(name)
    exit_code, startup_error_code = task.GetExitCode()
    return win32api.FormatMessage(startup_error_code)

You can see an example of usage here, in the iw.win32 package we have started to build, to gather all win32 specific Python things.