Entries tagged “bluebream”

Comprendre la « Component Architecture »

written by ccomb, on Oct 12, 2010 11:03:00 AM.

/static/python-250px.png

La Component Architecture, ou ZCA, historiquement issue de Zope 3 (qui s'appelle maintenant BlueBream), peut être utilisée pour n'importe quel projet Python, même en dehors d'une appli web. Elle trouve son intérêt lorsqu'on veut créer un programme hautement modulable, où les composants peuvent être rempacés facilement, ou même dynamiquement, sans toucher au code. Elle est aussi idéale pour créer un framework ou un CMS, et laisser la possibilité aux utilisateurs (qui sont donc, dans ce cas, développeurs ou intégrateurs), de choisir les composants ou de les surcharger par de la configuration.

Voici un petit exemple d'utilisation qui peut aider à saisir les principes.

Cet exemple est une sorte d'extrait du projet Virtual Care Team d'Etienne Saliez sur lequel je travaille de temps en temps, et donc le but est d'implémenter un dossier médical partagé entre une équipe de soin pluridisciplinaire, et de suivre l'évolution d'un problème. Après avoir fait une maquette en repoze.BFG (vct.demo), nous tentons de repartir de plus bas niveau avec le paquet vct.core, qui consiste pour l'instant en une api de stockage minimaliste, avec un plugin pour la ZODB, et un serveur XMLRPC permettant l'utilisation depuis n'importe quel langage. J'ai réutilisé ce projet pour un atelier interne de l'équipe Python à Alter Way.

Dossier médical

Le but de l'exercice est de créer un dossier médical permettant :

  • un calcul de statistiques
  • l'enregistrement dans une base de données arbitraire
  • la lecture depuis une des bases de données
  • une gestion des commentaires

On dispose d'une classe dossier médical :

>>> from dossiermedical import Dossier
>>> dossier = Dossier()

Le dossier médical possède deux attributs :

>>> dossier.patient = 'ccomb'
>>> dossier.exams = ['prise sang', 'radio du genou', 'coqueluche']

Les fonctionnalités du dossier médical sont décrites par son interface :

>>> from dossiermedical import IDossier
>>> IDossier.providedBy(dossier)
True

Voici l'implémentation du dossier, dans un fichier dossiermedical.py :

from zope.interface import Interface, Attribute, implements

class IDossier(Interface):
    patient = Attribute(u'nom du patient')
    exams = Attribute(u'liste des examens')


class Dossier(object):
    implements (IDossier)
    patient = None
    exams = None

Ajout de fonctionnalités

On souhaite ajouter deux fonctionnalités de statistiques au dossier médical : connaître sa taille et connaître la longueur moyenne des éléments. Pour ajouter des fonctionnalités, une première idée est d'ajouter des méthodes à notre Dossier (et donc aussi dans l'interface IDossier). Mais on souhaite plutôt séparer les fonctionnalités, et ne plus toucher à notre Dossier, qui est la fonctionnalité métier de base. Ceci permettra de changer le calcul des stats facilement sans toucher à l'implémentation du Dossier médical.

On décrit donc la nouvelle fonctionnalité dans une interface (qui possède deux attributs, num_exam et avg_exam) :

>>> from dossiermedical import IStat

Pour utiliser les fonctionnalités correspondant à cette interface sur le dossier médical, on utilise la notation suivante, qui signifie « donne moi la fonctionnalité IStat pour le dossier » :

>>> s = IStat(dossier)

Cette notation, qui ressemble à une instanciation de classe, permet en réalité de récupérer un adaptateur qui fournit IStat, et qui s'adapte au dossier. Pour que ça fonctionne il faut :

  • écrire l'implémentation de l'adaptateur
  • inscrire cette implémentation dans le registre de composants.

On pourra donc ensuite utiliser les méthodes de stats :

>>> s.num_exam()
3
>>> s.avg_exam()
11

Ci-dessous, voici l'implémentation de l'adaptateur, ainsi que son inscription dans le registre.

On utilise ici uniquement les paquets de la ZCA, à savoir zope.interface et zope.component. Le premier permet de définir des interfaces, tandis que le second offre le registre de composants et son API (inscription, récupération, etc.) :

from zope.component import adapts

class IStat(Interface):

    def num_exam():
        "return the number of exams"

    def avg_exam():
        "return the average lenght of the exams"


class Stat(object):
    implements(IStat)
    adapts(IDossier)

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

    def num_exam(self):
        return len(self.context.exams)

    def avg_exam(self):
        return len(''.join(self.context.exams))/self.num_exam()


# inscription dans le registre
from zope.component import getGlobalSiteManager
gsm = getGlobalSiteManager()
gsm.registerAdapter(Stat)

Ajout de plugins

On souhaite ajouter encore une fonctionnalité au dossier médical : pouvoir le stocker de différentes façons (sql, zodb, csv, ini). On commence donc par décrire la fonctionnalité dans une nouvelle interface IStockage, qui possède juste une méthode save(). :

>>> from dossiermedical import IStockage

Pour que le Dossier médical puisse fournir la fonctionnalité IStockage, on doit écrire un adaptateur fournissant IStockage, et qui s'adapte à IDossier. Comme on veut la fonctionnalité de IStockage aussi bien pour du SQL que du CSV ou du ZODB, on écrit plusieurs adaptateurs. Mais on ne peut pas inscrire dans le registre de composants plusieurs implémentations pour les même interfaces. En réalité on peut le faire, mais il faut inscrire chaque implémentataion sous un nom différent. Chaque adaptateur sera un adaptateur nommé, et le nom correspondra dans notre cas au type de base de données. Ici on ne peut pas utiliser la notation IStockage(dossier), car on veut indiquer quelle implémentation utiliser. Il faut utiliser getAdapter :

>>> from zope.component import getAdapter

On peut stocker une fois en CSV :

>>> stockage_csv = getAdapter(dossier, IStockage, "csv")
>>> stockage_csv.save()
ok, saved in CSV

Et une fois en SQL :

>>> stockage_sql = getAdapter(dossier, IStockage, "sql")
>>> stockage_sql.save()
ok, saved in SQL

Voici l'implémentation correspondante, et les inscriptions dans le registre :

class IStockage(Interface):
    def save():
        "allows to save a dossier"


class StockageCSV(object):
    implements(IStockage)
    adapts(IDossier)

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

    def save(self):
        print u"ok, saved in CSV"


class StockageSQL(object):
    implements(IStockage)
    adapts(IDossier)

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

    def save(self):
        print u"ok, saved in SQL"


# inscriptions dans le registre
gsm.registerAdapter(StockageCSV, name="csv")
gsm.registerAdapter(StockageSQL, name="sql")

Service pluggable

On souhaite maintenant récupérer notre dossier médical depuis une des sources au choix (sql, zodb, etc.). Ceci correspond encore à une nouvelle fonctionnalité, décrite par une interface ILoad. Le principe est le même que pour l'adaptateur nommé, sauf que cette fois on souhaite juste obtenir un composant qui fournit ILoad, mais qui ne s'adapte à rien. Ce type de composant s'appelle un utility :

>>> from zope.component import getUtility
>>> getUtility(IStockage, "csv").load()
ok, loaded

Voici le utility en question et son inscription dans le registre :

class ILoad(Interface):
    def load(self):
        """load the object"""

class Loader(object):
    implements(ILoad)

    def load(self):
        print u"ok, loaded"


# inscription dans le registre
gsm.registerUtility(Loader(), name="csv")

Ajout dynamique d'une fonctionnalité générique

On souhaite enfin développer une fonctionnalité générique, et réutilisable pour autre chose qu'un dossier médical : ajouter des commentaires au dossier médical. Pour obtenir une vraie généricité, on doit implémenter la fonctionnalité de commentaires sans jamais faire référence au Dossier médical.

Comment fait-on ?

La fonctionnalité de commentaire correspond à l'interface IComments. Pour utiliser cette fonctionnalité IComments sur un dossier médical, il nous faut un adaptateur fournissant IComments, et s'adaptant à IDossier. Mais comme on ne veut pas faire référence à IDossier dans notre gestion de commentaires, au lieu de créer un adaptateur s'adaptant à IDossier on crée un adapatateur s'adaptant à une interface vide, qu'on appelle interface marqueur : ICommentable. Les interfaces marqueurs sont souvent utilisées de cette façon : leur nom ressemble à un adjectif en « able », et elle indique une capacité à fournir une fonctionnalité : par exemple ICommentable, IIndexable, IStockable, etc. (C'est de cette façon que son conçues les « behaviors » dans Plone 4) :

>>> from comment import ICommentable, IComments

Comment notre dossier médical peut-il maintenant fournir la fonctionnalité de commentaire ? Il suffit d'indiquer qu'il est « commentable » en lui ajoutant dynamiquement l'interface ICommentable :

>>> from zope.interface import alsoProvides
>>> alsoProvides(dossier, ICommentable)

De cette façon, comme notre dossier médical fournit maintenant l'interface ICommentable, notre adaptateur de commentaire devient disponible :

>>> IComments(dossier).add_comment("un commentaire")

Voici la fonctionnalité de commentaire, qu'on place dans un module séparé comment.py :

from zope.interface import Interface, Attribute, implements
from zope.component import adapts


class ICommentable(Interface):
    "Add this marker interface to objects you want to be commentable"


class IComments(Interface):
    comments = Attribute("list of comments")

    def add_comment(comment):
        "add a comment"


class Comments(object):
    implements(IComments)
    adapts(ICommentable)

    def __init__(self, context):
        self.context = context
        if not hasattr(self.context, '_comments'):
            self.context._comments = []
        self.comments = self.context._comments

    def add_comment(self, comment):
        self.comments.append(comment)


# inscription dans le registre
from zope.component import getGlobalSiteManager
gsm = getGlobalSiteManager()
gsm.registerAdapter(Comments)

Conclusion

Cet exemple a couvert les notions suivantes de la ZCA :

  • adaptateur simple
  • adaptateur nommé
  • utility
  • interface marqueur et fonctionnalité générique

Parmi les notions non abordées, on trouve les adaptateurs multiples, qui sont exatement la même chose que les adaptateurs simples (sauf qu'ils s'adaptent à plusieurs objets), ou bien les subscribers (adaptateurs d'abonnement). Pour en savoir plus, consultez le Guide complet de la ZCA

Discover new versions of your packages or dependencies

written by ccomb, on Aug 6, 2010 1:23:00 AM.

Python

z3c.checkversions

As part of my work on the the Zope Toolkit 1.0 and BlueBream 1.0, I've released several versions of z3c.checkversions. This package is part of the z3c (Zope 3 Community) namespace, but it does not depend on any Zope package and has zero dependencies (except setuptools). It provides you with a single checkversions script which can be used with any project. This is particularly useful, for instance, on large Plone projects, with many packages. The more packages you use, the more bugs you find. You shouldn't loose time on bugs that are already fixed by newer versions of packages! In the Zope community, it's generally safe to upgrade to the latest minor versions of packages. If you're using package foo version 1.0.2, and discover that version 1.0.6 and 1.1.0 are available, you can upgrade immediately to 1.0.6 without risk.

As a summary, versions are commonly formed with 3 numbers : X.Y.Z:

  • a new minor Z version only provides bugfixes. No new features are allowed.
  • a new intermediate Y version should provide new (backward compatible) features
  • a new major X version is expected to cause breakages and is probably backward-incompatible.

Discovering new versions on the system Python

Install z3c.checkversions as an administrator. This is safe because there is no dependencies, and you can uninstall it easily with pip afterwards:

$ sudo pip install z3c.checkversions  # or sudo easy_install z3c.checkversions
$ checkversions
# Checking your installed distributions
pida=0.6.2
python-dateutil=1.5
genshi=0.6
buildbot=0.8.1
pyparsing=1.5.3
(...)

If you add the -v option, all parsed versions will be displayed, as well as the current installed versions:

$ checkversions -v
# Checking your installed distributions
twisted-words=10.0.0
pida=0.6.2 # was: 0.5.0
foolscap=0.5.1
distribute=0.6.14
twisted-names=10.0.0
bicyclerepair=0.9
python-dateutil=1.5 # was: 1.4.1
genshi=0.6 # was: 0.5.1
buildbot=0.8.1 # was: 0.7.12
pyparsing=1.5.3 # was: 1.5.2
(...)

If you only want the new intermediate versions, add -l 1. If you only want the new minor (bugfix) versions, add -l 2 instead. If you ever have some packages with 4 version digits, you can even use -l 3:

$ checkversions -v -l 2
# Checking your installed distributions
twisted-words=10.0.0
pida=0.5.1 # was: 0.5.0
foolscap=0.5.1
distribute=0.6.14
twisted-names=10.0.0
bicyclerepair=0.9
python-dateutil=1.5
genshi=0.6
buildbot=0.8.1
pyparsing=1.5.3
(...)

Now you can uninstall it:

$ sudo pip uninstall z3c.checkversions

Working with a virtualenv

Do the same, but install it in a virtualenv:

$ virtualenv --no-site-packages --distribute sandbox
$ sandbox/bin/pip install z3c.checkversions

Then the script is available only in the virtualenv:

$ sandbox/bin/checkversions -v
# Checking your installed distributions
pip=0.8 # was: 0.7.1
python=2.6
distribute=0.6.14 # was: 0.6.10
z3c.checkversions=0.4
wsgiref=0.1.2
(...)

Working with a buildout

If the [versions] section of your buildout is huge, finding new versions one by one can be painful, particularly if you want to find only new minor versions. In that case just install z3c.checkversions in your buildout. Don't forget to enable the [buildout] extra:

[some_part]
recipe = zc.recipe.egg
eggs = z3c.checkversions [buildout]

Then run your buildout again, and run the checkversions script by passing the buildout.cfg (or versions.cfg) file as an argument:

$ bin/checkversions -v -l 2  versions.cfg
# Checking buildout file versions.cfg
zope.app.error=3.5.2
distribute=0.6.14 # was: 0.6.10
zope.dottedname=3.4.6
Twisted=10.0.0
zope.hookable=3.4.1
z3c.viewtemplate=0.3.2
zope.app.pagetemplate=3.4.1
zope.decorator=3.4.0
jquery.javascript=1.0.0
zope.app.container=3.5.6 # was: 3.5.4
hachoir-parser=1.2.1
z3c.template=1.1.0
(...)

Blacklist and incremental options

While setting up the AFPY buildbots for the Zope Toolkit, I've been experimenting with a special buildbot that would be able to create a new Zope Toolkit version list automatically, by testing arbitrary new versions. For this purpose I've added two options to z3c.checkversions:

  • -1 or --incremental
  • -b or --blacklist

The first option allows to suggest only one new version:

$ checkversions -v -l 2 --incremental versions.cfg
# Checking buildout file versions.cfg
zope.app.error=3.5.2
distribute=0.6.14 # was: 0.6.10
zope.dottedname=3.4.6
Twisted=10.0.0
zope.hookable=3.4.1
z3c.viewtemplate=0.3.2
zope.app.pagetemplate=3.4.1
zope.decorator=3.4.0
jquery.javascript=1.0.0
zope.app.container=3.5.4
hachoir-parser=1.2.1
z3c.template=1.1.0
(...)

The second option allows to not suggest a version which is in a blacklist version file. Say we have the following blacklist.cfg file:

zope.app.container=3.5.6

Then the script will suggest all the highest new versions, except the one in the blacklist:

$ checkversions -v -l 2 -b blacklist.cfg versions.cfg
# Checking buildout file versions.cfg
zope.app.error=3.5.2
distribute=0.6.14 # was: 0.6.10
zope.dottedname=3.4.6
Twisted=10.0.0
zope.hookable=3.4.1
z3c.viewtemplate=0.3.2
zope.app.pagetemplate=3.4.1
zope.decorator=3.4.0
jquery.javascript=1.0.0
zope.app.container=3.5.5 # was: 3.5.4
hachoir-parser=1.2.1
z3c.template=1.1.0
(...)

Since zope.app.container=3.5.6 is part of the blacklist, the script suggests the version just below it: zope.app.container=3.5.5.

Automatic discovery of the Zope Toolkit version list

I'm using these two options in this special Zope Toolkit buildbot: http://buildbot.afpy.org:8018/waterfall

This buildbot automatically upgrades one package version (builder at the left), then run the tests. If tests fail, the package is added in the blacklist (builder at the right). In the next run, the buildbot will choose the version just below. If tests fail, it will downgrade again. If tests pass, it will remember the current versions and try to upgrade another package. When the buildbot finally reach a stable state, we automatically get all the latest working versions.

During the first cycles, the buildbot has given the following behaviour :

  • it started with ZTK 1.0a2
  • it upgraded (arbitrary) zope.dublincore 3.6.0 -> 3.6.3
  • tests failed
  • it added zope.dublincore 3.6.3 in the blacklist
  • it upgraded zope.dublincore 3.6.0 -> 3.6.2
  • tests failed
  • it added zope.dublincore 3.6.2 in the blacklist
  • it upgraded zope.dublincore 3.6.0 -> 3.6.1
  • tests failed
  • it added zope.dublincore 3.6.1 in the blacklist
  • so it keeps zope.dublincore 3.6.0 (until 3.6.4 is released)
  • it upgraded lxml 2.2.6 -> 2.2.7
  • tests passed
  • lxml 2.2.7 is kept for future builds
  • etc....

The cycle is not optimal, because zope.dublincore 3.6.3 maybe works well when upgraded with another package. So maybe I should try the opposite: upgrading packages bottom up, one by one, until the highest one is reached.


test