Faire du virtualhosting avec zope façon wsgi

Posté le 25/07/2008 sous zope3 wsgi

J'en avais ras le bol que Zope nécessite des url complètement tordues pour faire du virtual hosting. Ça m'empêchais entre autre d'utiliser Paste#urlmap pour dispatcher certaines url sur d'autres applis que Zope.

Du coup, j'ai tenté un truc tout con: plutôt que d'utiliser les RewriteRule d'Apache, récrire le PATH_INFO en englobant l'application Zope dans une autre. Et ça marche. Fiesta !

Voilà donc à quoi ça ressemble. J'utilise zopeproject. J'ai donc modifier le machin qui créer l'application Zope. A savoir le fichier startup.py comme ceci:

def application_factory(global_conf, conf='zope.conf', vhost='www.gawel.org'):
    vhost = '/++vh++http:%s:80/++' % vhost
    zopeapp = zope.app.wsgi.getWSGIApplication(zope_conf)
    def zopewrapper(environ, start_response):
        environ['PATH_INFO'] = vhost + environ['PATH_INFO']
        return zopeapp(environ, start_response)
    return zopewrapper

Et hop, ça roule. L'avantage, en plus d'avoir une url propre en entrée, c'est que vu que je développe aussi derrière Apache, j'ai juste eu à changer mon fichier debug.ini pour prendre en compte mon virtual host de développement.

En fait j'ai fais un peu mieux que tout ça, car comme dit au début, le but était d'utiliser Paste#urlmap. La source de la bidouille en question est ici.

Aller, pendant que j'y suis, j'en chiais aussi pas mal pour déterminer vers quel backend rediriger les requêtes dans varnish. Tester des ++ dans l'url, ça lui plaisait pas du tout. Vu que j'utilise Apache devant (surtout pour subversion), j'ai trouvé le truc. Il suffit d'activer le module headers:

# a2enmod headers

Puis rajouter un truc du genre dans votre virtualhost Apache:

RequestHeader set VARNISH_BACKEND gawel_org

Vous l'aurez compris, ceci ajoute un header à la requête. Ensuite, dans varnish, on test ce header:

if (req.http.VARNISH_BACKEND ~ "gawel_org") {
    set req.backend = gawel_org;
}

Et le tour est joué. Il faut bien sur que toutes les requêtes entrantes aient ce header. Pour moi ce n'est pas un problème vu que tout passe par apache.

nose doctest plugin sucks

Posté le 17/07/2008 sous python pylons

En ce moment je bosse sur une application en Pylons. J'adore ce petit framework, mais y a un truc que je pouvais pas encadrer, c'est de faire des tests avec des TestCase. Je préfère de loin les doctests.

Me voilà donc partit à la recherche de docs pour pouvoir écrire mes tests comme j'aime les écrire. Pylons utilise nose comme framework de test. Je découvre alors avec joie que nose fournit un plugin pour parcourir les doctests. Chouet !

Le problème, c'est que ce plugin est carrément rudimentaire. En gros, il choppe vos doctest et les initialise ultra basiquement. Comprendre: impossible de passer des options telles que optionflag, setUp ou tearDown. En bref, ça pu. Comment je fais pour initialiser mon framework Pylons pour mes tests moi ? Hein ?

J'ai finalement trouvé une solution en surclassant la classe doctest.DocFileCase afin de faire ce que je veux. Voici le code en question. Il suffit de le placer dans le fichier tests/functional/test_docs.py de votre application Pylons:

# -*- coding: utf-8 -*-
import os
import doctest
import mypylonsapp
from mypylonsapp.tests import *

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

dirname = os.path.join(os.path.dirname(mypylonsapp.__file__), 'docs')


def build_testcase(filename):
    name = os.path.splitext(filename)[0]
    path = os.path.join(dirname, filename)

    class Dummy(doctest.DocFileCase, TestController):
        def __init__(self, *args, **kwargs):
            # init pylons stuff
            TestController.__init__(self, *args, **kwargs)

            # get tests from file
            parser = doctest.DocTestParser()
            doc = open(self.path).read()
            test = parser.get_doctest(doc, globals(), name, self.path, 0)

            # init doc test case
            doctest.DocFileCase.__init__(self, test, optionflags=optionflags)

        def setUp(self):
            """init pylons stuff and make app available in doctest
            """
            TestController.setUp(self)
            test = self._dt_test
            test.globs['app'] = self.app

        def tearDown(self):
            """cleaning
            """
            TestController.tearDown(self)
            test = self._dt_test
            test.globs.clear()

    # generate a new class for the file
    return ("Test%s" % name.title(),
            type('Test%sClass' % name.title(), (Dummy,), dict(path=path)))

for filename in os.listdir(dirname):
    if filename == '.svn':
        continue
    name, klass = build_testcase(filename)
    exec "%s =  klass" % name

# clean namespace to avoid test duplication
del build_testcase, filename, name, klass

Vous admirerez la ruse qui est de générer une nouvelle classe pour chaque fichier trouvé dans le répertoire contenant les doctests.

On peut ensuite créer un fichier texte dans docs/ et y écrire des tests du genre:

>>> response = app.get(url_for(controller='main', action="index"))
>>> print response
Response: 200
...

Ce qui est tout de même vachement plus convi qu'un test classique.

Packager ses scripts Python

Posté le 08/07/2008 sous python unix

Bon nombre de gens utilise python pour faire de petits scripts. Le problème c'est que pour les distribuer, ensuite, c'est pas le top.

Heureusement il y a distutils !!

distutils est un paquet inclus dans les distributions python permettant de créer des paquet python.

Le principe est simple. On englobe un module python dans un paquet contenant un fichier setup.py

Le plus simple est d'utiliser paste pour créer son paquet. Renseignez bien les information demandées. Elles seront visible si vous décidez de distribuer votre paquet par la suite. Donc:

$ easy_install -U Paste
$ paster create monscript
$ cd monscript
$ ls
monscript/ monscript.egg-info/ setup.cfg setup.py

Ceci nous créer un répertoire monscript contenant un setup.py et un sous répertoire destiné à recevoir le code python.

Nous devons maintenant créer un point d'entrée pour notre script. Pour cela, nous allons modifier monscript/__init__.py pour qu'il ressemble à ça:

def main():
    print 'Yeah !'

Ensuite, en modifiant le fichier setup.py, nous pouvons associer ce point d'entrée à un véritable script qui sera installé à l'installation du paquet. Modifiez la section entry_points du setup.py pour qu'il ressemble à quelque chose du du genre:

entry_points="""
# -*- Entry points: -*-
[console_scripts]
mon_super_script = monscript:main
""",

Voilà, le tour est joué. Alors, pourquoi tout cela pour un simple script ? C'est simple. Vous pouvez maintenant aisément le distribuer.

Voici les principales commandes qui vous serons utiles:

  • créer un tarball:

    $ python setup.py sdist
    
  • créer un egg:

    $ python setup.py bdist_egg
    
  • rendre le paquet disponible sur pypi:

    $ python setup.py sdist bdist_egg register upload
    

Un utilisateur lambda pourra ensuite l'installer simplement:

  • via le tarball:

    $ wget http://exemple.com/monscript-0.1.tar.gz
    $ tar monscript-0.1.tar.gz
    $ cd monscript
    $ python setup.py install
    
  • via pypi:

    $ easy_install -U monscript
    

Moralité, distutils rends la vie plus facile.

Django, le wsgi et paste: status

Posté le 15/06/2008 sous django python afpy

Cette après-midi, j'ai retrouvé mon collègue de geekerie, Olivier. Pendant que lui tentait de faire apprendre à rêver à son cher Dr Gumby, je me suis atteler à faire fonctionner django avec paste. Pas une mince affaire à priori. J'avais vu une tentative pour faire un paste.django qui semblait évoluer péniblement. Mais bon, je suis plein de courage.

J'ai finalement été assez surpris. Déjà, on trouve dans le trunk de django un WSGIHandler. C'est la fête ! Il ne reste plus qu'as l'associer à paste. Bon, je me lance.

La première chose que j'ai faites a été de transformer l'application en egg. Je comprends pas que ce ne soit pas fait de fait dans la template django... Soit, un simple setup.py et mon application peut-être intégrée dans mon buidlout.

Ensuite, première tentative pour utiliser ce précieux handler: créer une usine qui renvois le WSGIHandler tel quel. Échec. Forcément, django attends son fameux module de settings. Soit, avec un peu de chance, en le plaçant dans l'environ utilisé par l'usine, on vas y arriver. Re-échec.

Pourquoi ? Tout bêtement parce que django vas chercher cette valeur dans os.environ. Et là, c'est le drame. Enfin non, c'est à moitié le drame. En effet, en initialisant correctement la variable qui vas bien, on arrive finalement à utiliser ce handler.

Second problème, les url générées par l'application sont toutes erronées. En effet, un bug fait que django ne tiens pas compte de la variable SCRIPT_NAME. C'est bien dommage, mais un bon vieux hack permet de fixer le problème relativement facilement. Il suffit d'initialiser le PATH_INFO en le précédent du SCRIPT_NAME et le tour est joué.

Voici le résultat:

# -*- coding: utf-8 -*-
import os
from paste.deploy.config import ConfigMiddleware
from django.core.handlers.wsgi import WSGIHandler

def factory(global_config, **local_config):
  os.environ['DJANGO_SETTINGS_MODULE'] = 'jobsafpyorg.settings'
  conf = global_config.copy()
  conf.update(**local_config)
  app = ConfigMiddleware(WSGIHandler(), conf)
  def django_app(environ, start_response):
    environ['PATH_INFO'] = environ['SCRIPT_NAME'] + environ['PATH_INFO']
    return app(environ, start_response)
  return django_app

On peut ensuite se servir de cette usine comme point d'entrée et l'utiliser dans un fichier de configuration paste. Notez que le ConfigMiddleware est indipensable. Le handler de django semble renvoyer des choses peu ordinaire de type class, parfois. Le middleware permet de corriger tout cela.

Problème, si on veut plusieurs instance django dans un seul environnement wsgi, ça devient problématique. Comment initialiser plusieurs configuration avec une seul clé du dictionnaire environ... Je me le demande.

Cela dit, c'est un problème auquel je vais être confronter car j'aimerais fair cohabiter les deux applications django qui ont été faites pout l'AFPy. Donc, la suite au prochaine épisode.

Il y a de la vie après PyCON !!!

Posté le 07/06/2008 sous afpy pycon python

Il y a peu, c'était Pycon FR, la récompense annuelle des efforts fournit dans cette belle association qu'est l'AFPy. Grande réussite de mon point de vue. La richesse et la diversité des conférences s'améliore, le public est plus nombreux. Python a de beaux jours devant lui.

J'y ai fait une conférence sur le WSGI. Ça à au moin eu le mérite de me faire réaliser que j'étais un bien piètre orateur. Probablement que l'AFPYro de la veille n'as rien fait pour arranger les choses, hin hin. En tout cas, j'espère que ça suscitera quelque vocations.

Je suis personnellement convaincu de l'intérêt de cette norme. Et j'ai maintenant un exemple concret à fournir. En effet, je bosse depuis plusieurs mois sur la nouvelle interface de gestion des membres de l'AFPy. Une petite application en Pylons qui permet d'administrer les utilisateurs de notre annuaire ldap et les inscription dans le Zope et les listes de diffusions. Elle est ici, pour ceux que ça intéresse. J'avais aussi fait une petite application compatible WSGI pour pouvoir afficher les photos avec un tag AFPy que les gens posent sur flickr. On avait aussi besoin d'un nouveau WIKI dans le cadre de la refonte du site. Le but était donc de brancher tout ce petit monde dans le site actuel. Heureusement, il y a findus^WWSGI.

Le paquet wsgi.afpy.org était né. Ça donne un petit fichier de configuration sympa qui dessers ces trois applications via un urlmap. Bien sur, le tout est géré avec zc.buildout, ce qui permet d'avoir un environnement python avec les paquets qui vont bien et de faire de l'administration ldap dans un shell python. Un exemple:

>>> from afpy.core import ldap
>>> user = ldap.getUser('gawel')
>>> user
<User dn:uid=gawel,ou=members,dc=afpy,dc=org>
>>> user.email
'gawel@afpy.org'
>>> ldap.getMembersOf('bureau')
[u'tarek', u'ogrisel', u'gwen', u'gawel', u'jpcw2002', u'ccomb']

C'est quand même super fun !!!

django monte en puissance

Posté le 09/04/2008 sous django pycon afpy

Je crois que django a de l'avenir. Google viens de sortir son app engine qui s'appuie en grande partie sur django et le WSGI (ça me fait penser qu'il faudrait que je prépare ma conf pour pycon un jour). C'est plutôt très bon signe pour django et pour python plus généralement, d'ailleurs. Google se lance dans le mass-hosting et c'est en python que ça se fait. Cet hébergement gratuit grand publique est-il le début de la vrai vulgarisation de python ? On ne peut que l'espérer ! Jusqu'à présent, objectis était le seul hébergeur gratuit (au monde ?) a proposer une solution python gratuite. Cela manquait vraiment.

Suite à cette annonce, j'ai eu aussi l'occasion de découvrir playlive.fm. Un site musico-social 2.0 basé sur django. Le résultat est joli (j'ai eu une invite, héhé) et j'ai été agréablement surpris de voir que Hocus-Pocus (je suis super fan, ils tuent. naoned représente !) est déjà présent sur la plate-forme.

svn: le futur du CMS !!!

Posté le 01/04/2008 sous zope3

Vous êtes comme moi je suis sûr. Editer des document dans des éditeur WYSIWYG, ça vous saoul au plus haut point. Personnellement, je préfère de loin un bon éditeur et du reStructuredText.

La solution je l'avais depuis longtemps. Stocker les billets de mon blog dans un svn et les publier avec un framework quelconque. Ca fait un moment maintenant que j'utilise Zope3 pour ça, mais le code était assez dégueulasse. Je me suis donc dit que si je mettais tout ça un peu au propre, cela pourrait servir à d'autres. Je me suis donc lancé dans un énorme week-end de geeking. le résultat est gp.svnfolder, un package Zope3 permettant de publier un répertoire svn contenant des documents en reStructuredText.

Voyons plutôt. Il nous faut une repository valide:

>>> import os
>>> from os.path import split
>>> curdir = split(split(os.getcwd())[0])[0]
>>> repos = os.path.join(curdir, 'tests', 'rstfolder')

Avec ça, on peut instancier un dossier svn:

>>> from gp.svnfolder.folder import SVNFolder
>>> folder = SVNFolder()
>>> folder.__name__ = 'blog'
>>> folder.path = u'file://%s' % repos

Et en voir le contenu:

>>> sorted(folder.keys())
[u'doc.rst', u'doc2.rst']

Et accéder aux fichiers:

>>> file = folder['doc.rst']
>>> print file.data
Document 1
==========
<BLANKLINE>

Ensuite il suffit d'utiliser les vues fournies pour rendre ces fichiers en html.

Voila. Je penses qu'avec un peu de motivation je pourrais facilement le rendre compatible avec Mercurial et Bazaar mais ça suffit à faire mon bonheur :)

Monter un environnement python avec zc.buildout

Posté le 20/03/2008 sous python buildout

zc.buildout est un utilitaire créer par Jim Fulton, le papa de Zope. L'étendue des capacités de zc.buildout est énorme et mériterais un livre entier.

Cependant, voici un petit exemple pour se monter un environnement de paquets/binaire python avec un simple petit fichier de configuration.

Tout d'abord, nous allons nous créer un virtualenv afin de ne vraiment pas polluer notre distribution:

gawel@Stacy:~$ virtualenv --no-site-packages py
New python executable in py/bin/python
Installing setuptools.............done.

Si vous n'avez pas virtualenv, installez setuptools et virtualenv sur votre système. D'après moi, ce sont les deux seuls packages qui mérite d'être installé globalement, pour tout le système.

On va ensuite installer zc.buildout dans notre environnement:

gawel@Stacy:~$ cd py
gawel@Stacy:~/py$ ./bin/easy_install zc.buildout
Searching for zc.buildout
...
Finished processing dependencies for zc.buildout

Parfait, maintenant, initialisons l'environnement de zc.buildout:

gawel@Stacy:~/py$ ./bin/buildout init
Creating '/Users/gawel/py/buildout.cfg'.
Creating directory '/Users/gawel/py/parts'.
Creating directory '/Users/gawel/py/develop-eggs'.
Generated script '/Users/gawel/py/bin/buildout'.

Puis on édite le fichier buildout.cfg pour qu'il ressemble à ça:

gawel@Stacy:~/py$ cat buildout.cfg
[buildout]
parts = eggs

[eggs]
recipe = zc.recipe.egg
eggs=
    ipython
    i18ndude
    ZopeSkel
    IngeniSkel
    iw.releaser

Vous pouvez mettre absolument n'importe quel egg dans l'option eggs. La seule contrainte est que ceux-ci se trouvent sur pypi.

On lance la construction du buildout:

gawel@Stacy:~/py$ ./bin/buildout
Installing eggs.
...
Generated script '/Users/gawel/py/bin/ipython'.
Generated script '/Users/gawel/py/bin/pycolor'.
Generated script '/Users/gawel/py/bin/i18ndude'.
Generated script '/Users/gawel/py/bin/project_deploy'.
Generated script '/Users/gawel/py/bin/project_release'.
Generated script '/Users/gawel/py/bin/project_diff'.

J'ai maintenant tout mes binaires dans le dossier bin/ de l'environnement:

gawel@Stacy:~/py$ ls bin/
activate          easy_install-2.4* project_deploy*   pycolor*
buildout*         i18ndude*         project_diff*     python*
easy_install*     ipython*          project_release*  python2.4@

Le must, c'est que je peux importer chacun de ses paquets dans mon ipython:

gawel@Stacy:~/py$ ./bin/ipython
In [1]: import i18ndude

In [2]: dir i18ndude
------> dir(i18ndude)
Out[2]: ['__builtins__', '__doc__', '__file__', '__name__', '__path__']

Vous pouvez bien sur ajouter des eggs dans votre buildout.cfg puis relancer ./bin/buildout pour qu'il soit prit en compte.

Ceci est une simple introduction à zc.buildout. On peut faire bien mieux, je sais. Mais ça deviendrais compliquer de tout expliquer dans un simple post :)

Simplement, Je trouve ça tellement pratique pour tester rapidement un package que je voulais en faire bénéficier tous ceux qui ne connaissent même pas l'existence de zc.buildout.

Traduire un package Plone 3

Posté le 15/02/2008 sous zope plone3

Je n'ai pas creusé plus que ça, mais apparemment il y a de beaux conflits entre zope.i18n et PlacelessTranslationService. Du coup, tant que l'on veut ajouter des traductions à un domaine non existant, se servir de zope.i18n ne pose pas de problème. Par contre, si l'on désire ajouter des traductions au domaine plone par exemple, ça ce complique.

Le seul moyen que j'ai pu trouver après quelques heures d'introspection est d'enregistrer un .po qui se trouve hors du répertoire locales (si vous le mettez dedans, ça casse tout) lors de l'initialisation du paquet. Cela donne un bout de code horrible qui me ferais presque honte... Voyez plutôt le contenu de mon __init__.py:

def initialize(context):
    """Initializer called when used as a Zope 2 product."""
    context._ProductContext__app.Control_Panel.TranslationService._load_i18n_dir(os.path.dirname(__file__)+'/i18n')

Alors, oui, c'est dégueulasse, mais ça marche.

Utiliser zope.sendmail dans Zope 2

Posté le 11/02/2008 sous zope

Depuis que les sources de Zope3 sont incluse dans Zope2, il est vraiment dommage de ne pas utiliser au maximum ces possibilités.

Entre autre, il existe un package assez peu connu zope.sendmail qui permet d'envoyer des mails de façon asynchrone. Les mails sont stockés dans une mailbox puis envoyés par un thread séparé de Zope.

L'inconvénient de ce package est que tout est configurable, mais uniquement via du zcml. Le TODO indique que, oui, ça serait bien qu'un jour cela soit dans le zope.conf, mais en attendant...

Finalement, depuis qu'on a tendance à utiliser buildout (parce que quand même, ça arrache) il était super simple de créer une petite recette qui installe un bout de zcml dans le répertoire qui vas bien.

Le résultat est iw.recipe.sendmail. Simple, et efficace.

Un autre petit paquet, iw.mailhost, permet de patcher les produits MailHost et SecureMailHost afin d'utiliser zope.sendmail en lieu et place de smtplib. Il suffit de le placer dans un répertoire inclus dans le sys.path pour qu'il soit importé par le zcml généré par iw.recipe.sendmail

A noter que ceci a été éprouvé sous OSX, Linux et Windows. Enjoy !