Entries tagged “zope3”

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

Zope 3 s'appelle dorénavant BlueBream !

written by ccomb, on Jan 27, 2010 8:12:00 PM.

BlueBream, the Zope Framework.

Un renommage qui aurait dû avoir lieu depuis très lontemps ! Pour ceux qui ont du mal à suivre (et ça se comprend), une petite explication de texte : la réécriture de Zope 2, démarrée il y a 8 ans, et qui a longtemps été appelée « Zope 3 », a consisté à créer une architecture modulaire et innovante, la « Zope Component Architecture » (ZCA), puis à découper proprement Zope et une multitude de paquets utilisant cette architecture. Cet ensemble de paquets, cohérents et compatibles entre eux a été nommé tout d'abord « KGS » (Known Good Set), puis renommé courant 2009 en « Zope Toolkit ». Le Zope Toolkit (ZTK) est l'ensemble des bibliothèques Zope, utilisées maintenant par tout l'écosystème Zope : Zope 2, Plone, Grok, etc... Plus de 900 paquets sont taggés « Zope 3 » dans PyPI !

Nous avons donc l'architecture (la ZCA) et l'ensemble des bibliothèques (le ZTK). Mais où est le serveur d'application dans tout ça ? Rappelons que l'idée originale était de réécrire Zope 2, donc il manque quelque chose. Est-ce Zope 3 ? Oui, mais Zope 3 ça ne veut plus rien dire : le concept Zope 3 a été éclaté en morceaux, et même le nom Zope 3 a fini par se noyer au milieu du reste. On avait presque oublié qu'on pouvait créer une application en pur Zope 3. Un coup de zopeproject et c'était parti !

C'était sans compter sur l'initiative de Baiju M., qui a pris le risque de renommer Zope 3 (le serveur d'application) en BlueBream, et de relancer la machine buzz et documentation ! La vidéo fait sourire, mais on ne peut rien lui reprocher, vu l'énergie qu'il déploie actuellement. Globalement son effort a été extrêmement bien accueilli et encouragé par la communauté. BlueBream, en soi, ne contient pas grand chose, c'est un simple template de projet du même genre que zopeproject, grokproject, django-admin.py startproject ou autres zf.sh create project. C'est donc une nouvelle identité, un nouveau logo et une nouvelle dynamique de contribution. Dans BlueBream, il n'y a absolument rien d'autre que les paquets du Zope Toolkit, c'est donc bien du Zope 3 pur. Il n'y a même pas de namespace bluebream et ce n'est pas à l'ordre du jour.

Voici quelques ressources utiles pour s'informer et suivre l'actualité BlueBream :

Petit tutoriel de démarrage

La documentation officielle explique ça très bien en anglais, mais je vous en fais une en français.

Création d'une application

Avant tout, assurez-vous d'avoir Python 2.5 ou 2.6 et les bons paquets de développement, sur Debian/Ubuntu ça ressemble à ça:

$ sudo aptitude install python-dev build-essential libxml2-dev libxslt1-dev

On commence par installer la dernière version de Distribute:

$ wget http://python-distribute.org/distribute_setup.py
$ sudo python distribute_setup.py

Maintenant on installe le paquet bluebream (0.1.9 à la date de cet article), qui est minuscule et n'a pour dépendances que Sphinx-PyPI-upload, PasteScript, PasteDeploy et Paste.:

$ sudo easy_install bluebream

C'est tout, maintenant on peut démarrer un nouveau projet BlueBream. Remarquez que la méthode est la même que pour un projet Pylons, BFG, Plone ou n'importe quoi pris en charge par un template Paste. Vous devez répondre au moins à la première question en donnant le nom de votre projet, ce qui aura pour effet de créer un dosser du même nom. Pour tout le reste, vous pouvez appuyer sur Entrée.

$ paster create -t bluebream

Maintenant on rentre dans le dossier, et on construit l'application grâce à Buildout (Je suppose que votre projet s'appelle monprojet)

$ cd monprojet
$ python bootstrap.py
$ ./bin/buildout

Avant de lancer buildout, un bon conseil est de configurer le dossier partagé pour les paquets Python, qui évitera de les re-télécharger dans chaque buildout ou projet : vous devriez avoir un fichier ~/.buildout/default.cfg contenant ça :

[buildout]
eggs-directory = /home/ccomb/buildout-eggs

Si tout s'est déroulé correctement, vous devriez avoir un dossier bin contenant : breampy, buildout, paster, test. (Installez la dernière version de Virtualenv, créez un environnement isolé avec virtualenv --no-site-packages --distribute monprojet, activez le avec source bin/activate et recommencez sans sudo à partir de l'installaton de bluebream.)

Vous pouvez maintenant démarrer votre application:

$ bin/paster serve deploy.ini

Puis accéder à la page par défaut sur http://127.0.0.1:8080/

Page d'accueil BlueBream

Vous pouvez aussi démarrer en utilisant debug.ini, ce qui active divers outils de debug, dont un middleware WSGI très utile, pour afficher le traceback dans le navigateur et même inspecter les variables de manière interactive.

Quelque chose de nouveau par rapport à Zope 3.4, il y a une commande shell qui permet de se retrouver dans l'environnement de l'application, et d'accéder directement à la racine de base de données pour l'inspecter (objet root). Cette commande shell est l'équivalent du debugzope ou zopectl debug des versions précédentes de Zope 2 ou Zope 3.:

$ bin/paster shell debug.ini
>>> root
<zope.site.folder.Folder object at 0x46319b0>

On voir que l'objet racine de la ZODB est maintenant pris en charge par un nouveau paquet zope.site. On a aussi accès à un objet debugger ou app (c'est le même), provenant de zope.app.debug, et permettant de simuler une requête ou d'avoir accès à l'objet DB. Par exemple si vous voulez vider l'historique de la ZODB (ce qu'on appelle faire un « pack »), il suffit de taper ça dans le shell de debug:

>>> app.db.pack()

Modifier la page d'accueil

Pour comprendre d'où vient la page d'accueil que vous voyez sur http://localhosts:8080 , il suffit de regarder dans le dossier src/monprojet/main/. Dans le fichier configure.zcml, une vue est configurée :

<browser:page
   for="zope.site.interfaces.IRootFolder"
   name="index"
   permission="zope.Public"
   class=".views.RootDefaultView"
   />

Cette déclaration signifie : « J'ai une vue disponible pour l'objet racine (celui fournissant l'interface IRootFolder). Cette vue est gérée par la classe RootDefaultView, la vue a pour nom « index », qui sera le nom visible dans l'URL, et cette vue est publique (permission zope.Public). »

La vue est une simple classe Python visible dans le fichier views.py, et on peut voir que la vue renvoie elle-même la page HTML, sans même utiliser de langage de templating :

class RootDefaultView(BrowserView):

    def __call__(self):
        return """\
<html><head><title>Welcome to BlueBream!</title></head><body>
<h1>Welcome to BlueBream!</h1>
<ul>
(...)

Dans cette page d'accueil, on peut déjà s'authentifier en cliquant sur login, avec le mot de passe admin/admin. On peut ensuite cliquer sur Add a sample application pour ajouter une Sample Application. Cette SampleApplication n'est rien d'autre qu'un conteneur qui sera ajouté à la racine de la ZODB. Elle est définie dans le fichier app.py:

class SampleApplication(BTreeContainer):

  implements(ISampleApplication)
  name = u""
  description = u""

Tout ceci fait penser un peu à l'interface d'accueil de Grok qui permet d'« ajouter une application ». Ça ne sert pas à grand chose, à part montrer un exemple simple de formulaire, et d'ajout de contenu hiérarchique dans la ZODB. Si vous créez votre propre application, vous pouvez supprimer ces classes d'exemples, redéfinir la vue pour l'objet racine, de préférence en utilisant le templating par défaut de Zope : le ZPT.

Pour remplacer cette page d'accueil, commencez par supprimer la méthode __call__ de la vue RootDefaultView, car cette méthode surcharge celle de la classe parente (BrowserView). De cette façon on bénéficie à nouveau de la méthode de publication par défaut de BrowserView, qui peut faire appel à un template configuré en ZCML.

Dans view.py :

class RootDefaultView(BrowserView):
    pass

Et dans configure.zcml :

<browser:page
   for="zope.site.interfaces.IRootFolder"
   name="index"
   permission="zope.Public"
   class=".views.RootDefaultView"
   template="accueil.pt"
   />

Ensuite évidemment il faut créer le template pour l'accueil (fichier accueil.pt) :

<html><body>

<h1>Accueil</h1>

<p>Bonjour, la classe de l'objet racine (le contexte de cette vue) est :
  <span tal:content="python: context.__class__.__name__" style="color: blue">
  nom de la classe
  </span>
</p>

<p>
  Le user-agent de mon navigateur est :<br/>
  <span tal:content="python: request['HTTP_USER_AGENT']" style="color: blue">
  contenu de la requête
  </span>
</p>

</body></html>
Accueil modifié

La vue étant un double adaptateur sur le contexte (qui dans ce cas est l'objet racine) et sur la requête. Ces deux objet sont donc disponibles dans la vue en Python via self.context et self.request. Ces deux objet sont même transmis par défaut au template via les variables context et request. De cette façon, on peut accéder à l'objet racine et connaître le nom de sa classe (voir la capture d'écran), et à l'objet requête ce qui permet de récupérer entre autres le User-Agent, ou d'autres informations liées à la requête, comme l'utilisateur authentifié.

C'est tout pour aujourd'hui.


test