Nautilebleu

Dive into my universe

Posts tagged python

0 notes &

Testing a ØMQ async job with Python Mock

At Greenbureau, we are huge fans of ZeroMQ, a network library that eases building our queue management tool, called HGMQ.

Below is a typical job:

@hgmq.job('async_job')
def async_job(msg):
    # Do something usefull here
    actor = Actor.objects.get(name=msg["name"])
    msg = {
        "results": [fact.title for fact in actor.facts.all()]
    }
    hgmq.send_job('core/back', 'recv_async_job', msg)

The @hgmq.job decorator as the same role as the @task in Celery or the @job for rq.

As always, testing an async process is a hard job (^^). Not only being able to test the whole context, we can call the function in a blocking manner. But how to test what is done, as the function doesn’t return a value? If it saves some data to the DB, we can check how they are altered but what if the job is read only?

Hopefully, we can count on Python Mock library and especially the call helper.

So in a test, we are able to get the message sent via ØMQ:

from django.test import TestCase
from mock import Mock

from apps.corehgmq import hgmq  # Our hgmq instance

# Prevent to send job to the back during tests
hgmq.send_job = Mock(return_value=None)


class JobTestCase(TestCase):
    def test_async_job(self):
        msg = {
            'params': {
                'name': 'chuck'
            }
        }
        responses = [
            'Chuck Norris a déjà compté jusqu’à l’infini. Deux fois.',
            'La gravité, c'est la force qui fait tenir la Terre sous Chuck Norris',
            'L'avenir se demande parfois ce que Chuck Norris lui réserve.',
            'Dieu voulait créer l'Univers en 10 jours. Chuck Norris lui en a laissé 6.',
            'Chuck Norris est la raison pour laquelle Cthulhu se cache.',
            'Chuck Norris a lu le texte de la Constitution européenne.'
        ]
        async_job(msg)
        dest, job, response = hgmq.send_job.call_args[0]
        self.assertEquals(dest, 'core/back')
        self.assertEquals(job, 'recv_async_job')
        self.assertEquals(response["results"],
                          responses)

Note that the call helper has lots of other goodies, such as call_list(), which stores the sequences of call to the mocked object.

Filed under python zmq unittest async django

1 note &

Celery et rq, deux gestionnaires de files d’attentes

J’ai eu l’occasion de faire une présentation de Celery et rq au meetup django organisé par @revolunet et hébergé par @cyberdelia de 20 minutes. Merci à eux pour l’organisation et le cadre avec une vue imprenable !

Celery et rq sont deux gestionnaires de files d’attentes pour des tâches conçu en Python.

Le principe : dès qu’un traitement requiert trop de temps ou de ressources pour être traité par django durant une requête HTTP, on délègue ce traitement à un daemon qui va pouvoir les traiter de façon asynchrone.

Il faudra donc pouvoir alerter l’utilisateur (envoi d’un mail, pooling…)

Présentation de Celery

Initialement conçu comme une application django, il a évolué vers un projet autonome (mais qui peut bien évidemment toujours fonctionner de concert avec django.)

Celery est un gestionnaire distribué. Un broker va recevoir les ordres de tâches de la part du serveur sur lequel tourne django. Ce broker va distribuer les tâches.
Plusieurs backends sont supportés, RabbitMQ étant mis en avant comme celui offrant le meilleur support.

C’est un projet dynamique, qui a connu de nombreuses évolutions et dont l’API bouge encore pas mal (ex: suppression des décorateurs magiques dans la version 2.5.) Cependant, la politique de stabilité de l’API est très claire et les commandes sont dépréciées longtemps avant d’être retirées.

C’est un projet mature (les premiers commits remontent à 2007) et donc très (trop ?) riche.

De là est né rq.

Présentation de rq

rq se veut une alternative light à Celery, lorsqu’on a pas besoin toutes les fonctionnalités :

  • différents types de tâches — Abortable, Repeatable;
  • différents backends supportés;
  • webhooks pour les autres langages…

Utilisation de Celery

La documentation de Celery est complète, et la prise en main assez rapide.

Malgré tout, de nombreux points ne sont pas abordés (ou alors cachés dans des sections parfois peu explicites).

Par exemple, toutes les fonctionnalités ne sont pas disponibles avec tous les backends.

Il est facile de partir dans une configuration pour se rendre compte au bout de quelque temps que toutes les options dont on a besoin ne sont pas autorisées.

Le tuning du daemon pour de bonnes performances requiert du temps. Selon la doc elle-même, il n’y a pas de recette miracle et chaque projet requiert un peu de tâtonnement (par exemple, avec le même hardware, 3 workers pouvant gérer chacun 10 tâches apparaît plus stable qu’un seul worker avec 30 tâches en parallèles.)

Malgré tout, certaines fonctionnalités pourtant importantes telles que la priorisation des tâches ne sont pas disponibles. La solution : avoir plusieurs workers (un pour les tâches urgentes, un pour les autres) et envoyer les tâches à l’un ou l’autre.

Si vous utilisez celery avec django, vous avez une application, dj_celery qui vous permet de surveiller le status de vos tâches.
Pour fonctionner, cette application requiert d’activer deux autres outils de celery :

  • celerymon, qui va retourner l’état des files d’attentes,
  • celerycam, qui va prendre des instantannés à intervalles réguliers pour les enregistrer dans la base de données de votre projet django.

Comme on travaille en asynchrone, il est absolument indispensable de surveiller celery, car le suivi des erreurs encore plus problématiques que pour un site classique.

Il faut également surveiller le broker. Le broker le plus utilisé est RabbitMQ, pour lequel il existe un plugin d’administration.

Utilisation de rq

Beaucoup plus simple, la doc est beaucoup plus concise (mais il y a beaucoup moins à dire aussi.)

Le backend du broker est redis uniquement (aussi disponible avec Celery).

On ne peut pas de tester l’état d’une tâche, à part vérifier si on a un résultat ou non. Celery possède différents états (PENDING, STARTED, SUCCESS, FAILED) et on peut définir des états personnalisés au besoin.

rq ne permet pas de distribuer vers un channel un exchange spécifique pour router les tâches entre différents workers selon le type de tâches.

Plusieurs workers peuvent être lancés mais une tâche est exécutée à la fois (idem pour Celery.)

Python only : pas de webhooks.

Il existe une interface d’administration, mais pour Flask.

Ne fonctionne que sous Unix.

rq me semble avoir été conçu avec des idées très arrêtées sur l’environnement d’exécution, je pense notamment à des environnements de production du type Heroku où l’on ne pourra pas nécessairement avoir RabbitMQ qui est dépendant de erlang, tandis que redis fait désormais partie de l’offre standard.

Conclusion

La discussion qui a suivi a mis en avant les nombreux problèmes rencontrés avec Celery lorsqu’il est utilisé conjointement avec RabbitMQ, tandis que redis semble plus facile à vivre. C’est sans doute l’info à retenir.

Filed under celery rq python

0 notes &

Dynamically import modules with Python

Django admin is great but the dashboard is not so good. Peter Baumgartner from Lincoln Loop has presented at django con 2010 some interesting slides about it.

Particularly, it seems interesting to me to propose a way to customize the display by user or user permission. This leads me to find how to load dynamically a module.

It seems that you can easily import a module dinamically using __import__:

>>> p = __import__('os.path')
>>> p
<module 'os' from '/path/to/virtualenv/lib/python2.6/os.pyc'>

But wait a minute, I’ve imported os.path and I’ve got os? If you try:

>>> p.join('Users', 'goulwen')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'module' object has no attribute 'join'

you’ve got an error, but:

>>> p.path.join('Users', 'goulwen')
'Users/goulwen'

works.

If you need to import a submodule, you’ll need a little more work:

>>> o = __import__('os')
>>> p = getattr(o, 'path')
>>> p.join('Users', 'goulwen')
'Users/goulwen'

Once you know that, you can import dynamically and recursively as deeper as you need with a for loop:

>>> mods = ('os', 'path', 'join')
>>> m = __import__(mods[0])
>>> for item in mods[1:]:
...     m = getattr(m, item)
>>> m
<function join at 0x100288ed8>
>>> m('Users', 'goulwen')
'Users/goulwen'
>>> 

This is in fact how django is doing in the default dashboard.

Now it’s easy to create your own dashboard.

Filed under python

0 notes &

Quick introduction to Python generators

This morning, my boss asks me to republish a set of SCORM packages for a customer.

Until now, during the development phase, I was using the shell zip utility:

$ zip -ru9 ../<archive_name>.zip . -x @../exclude

where <archive_name> is the name of the folder and the exclude is a list of ignored files & folders.

But the perspective doing this at least 20 times (and probably more) a Monday morning was not very pleasant and very error prone.

So I decide to write a quick and dirty script that will do the export for me, i.e. browse the project directory to select some folders that match a glob pattern and add some of their files, excluding things like .svn, .DS_Store, Thumbs.db and so on.

A shell ninja would probably be able to do this with 3 LOC but I think the future when some of my admin scripts may gone wild on the desktop of end-users that use Windows. So Python appears to be a perfectly valid option and this was the occasion to add a new tool to my personal Swiss-Army knife: yield.

The first time I encounter yield was during my work on django_hg. Mercurial is doing an extensive use of it in his Wire Protocol, which is totally normal: when a repository is cloned, you don’t want your server to be completely hang because a crazy user has versionned a PPT file. yield lets Mercurial stream the files to the client without blocking the server.

At this time I was stuck by yield: how so much can be done by so little code? This afternoon, I find a quite nice definition for yield in an IBM article:

A generator “returns” with the new keyword yield, but “remembers” the exact point of execution where it returned.

This way, you can work with a lot of data without using lots of RAM. This is very useful when streaming a file, browsing a big recordset, etc. Generators let you work with a small amount of data within a large set.

A few months after finishing my work on django_hg, I have the chance to talk with Jonathan Schemoul, who is a contributor of TurboGears framework. He’s doing lots of functional programming with heavy datasets, which lead him to work everyday with generators. He explains me very well the concepts, but until now, I haven’t a reason to use it.

A typical SCORM package for this customer can contains 500+ files and weight 400Mb before export (this includes .svn folders, *.fla files, etc.) I don’t want those but I also don’t want to use all my RAM with a big list of all files that need to be included in the zip package: a generator fits exactly my needs.

So using a combination of os.walk, yield and zipfile I quickly get a script that do the job. Using it over 11 packages that weight no less than 3,74Gb and that contains ~ 4000 files (before export), I notice that I never use more than 4Mb of RAM. I suspect the zipfile module to heavily use yield when compressing files.

This is a very nice surprise as this script will be perfectly usable on a server, with Celery for example: a end user will add a message to the queue for getting all packages of a project, in the background Celery will launch my script to build them asynchronously.

As the need of working with very large datasets grows everyday, we need ways to do jobs without consuming enormous amounts of RAM. As far as I understand, this is the purpose of functional programming with languages such as erlang, scala… So I think that beginning to work with these concepts is very important as a developer, and I’m happy to have written this very little code.

Filed under python generators yield

0 notes &

Importer dans django depuis Tumblr

Je travaille depuis quelque temps à me (re)créer un site personnel, avec django. Mais je ne voudrais perdre les articles postés sur Tumblr.

Grâce à python tumblr [en], c’est très simple. J’ai choisi de faire une commande personnalisé, qui pourra être lancée depuis le shell de cette façon :

$ python manage.py import_from_tumblr

Pour cela il faut créer un dossier management dans le dossier de votre application, puis un dossier commands à l’intérieur de celui-ci. En plus des inévitables __init__.py, créer dans le dossier commands un fichier nommé import_from_tumblr.py.

Dans ce fichier, coller le code de ce lien sur Friendpaste [en]

Pour une fois la document sur le site est assez limitée, mais tout est très bien expliqué sur ce post de Brian Rosner[en].

Filed under django python tumblr management command

0 notes &

Authentification avec django_hg, partie 2

La dernière fois, nous avons vu comment servir django avec mod_wsgi.

Une fois Apache correctement configuré, effectuer l’authentification via django a été un jeu d’enfants.

Tout d’abord, rééditer la configuration du virtual host Apache pour ajouter cette ligne :

WSGIPassAuthorization On

On redémarre ensuite Apache.

Cette ligne informe Apache de transmettre les informations d’authentification au programme wsgi, en l’occurence django. Sinon, par défaut, Apache ne les transmets pas [en].

Ensuite, ajouter un decorator devant les vues que vous souhaitez protéger par l’authentification HTTP. Je me suis basé sur ce snippet [en] en l’adaptant au cas particulier de django_hg, c’est à dire que je souhaite une authentification HTTP uniquement si c’est un client Mercurial qui tente une commande clone, push ou pull.

Et voilà, le tour est joué

Pour détecter le cas de figure, je m’étais dans un premier temps basé sur le HTTP_USER_AGENT transmis par le client. Seulement je me suis dit que cette valeur risquait bien de changer, selon les clients ou avec le temps, même si Murky [en] par exemple, utilise le même.

Je suis donc en train de refactoriser cette partie en me basant uniquement sur la présence de l’argument cmd dans l’url et en vérifiant la validité. Pour cela j’ai mis en place un certain de tests unitaires, pour vérifier que je ne créais pas de régression. J’ai commité le résultat hier soir sur bitbucket [en].

Le dernier gros point complexe du programme est en passe d’être achevé. Je souhaite effectuer une dernière modification pour reproduire le comportement classique des projets open-source: clone et pull peuvent être anonymes mais pas le push, pour des raisons évidentes de sécurité et de traçabilité.

Honnêtement, au départ, je ne pensais être en mesure d’intégrer à ce point django et Mercurial, en tout cas, certainement pas aussi facilement.

La suite devrait être assez simple: ajouter les fonctionnalités manquante (diff, branches, etc.) et faire un outil de ticketing.

Filed under apache django django_hg python

0 notes &

Authentification avec django_hg

La semaine dernière [fr], j’ai réussi à utiliser django [en] pour répondre aux commandes de Mercurial.

L’étape suivante dans cette logique était donc de faire l’authentification grâce à django. Le code a été poussé sur bitbucket [en] hier soir. Je n’ai par contre pas eu le temps d’écrire la documentation, j’espère pouvoir faire cela dans la journée.

Si l’authentification est finalement contenue dans un decorator, j’ai fait pas mal d’administration pour mettre en place un environnement de développement. En effet, j’ai tout d’abord configuré Apache pour qu’il serve django. Cela n’a pas été sans mal, car la configuration de mod_wsgi m’a donné un peu de fil à retordre.

La documentation officielle de django [en] recommande d’utiliser mod_wsgi [en]. Je me suis donc tourné vers cette solution. Malheureusement, la version distribuée par MacPort [en] est vieille (version 1.1 alors que nous en sommes à la 2.5 en stable et que la 3.0 est en RC) et surtout elle est couplée à Python2.4, alors que django utilise Python2.5. J’ai donc dû en passer par la compilation.

La documentation de mod_wsgi est très bien faite et elle explique bien les problèmes que l’on peut rencontrer avec MacOSX [en], essentiellement dû au fait que MacOSX supporte 4 architectures (PPC7400 — 32 bits —, PPC64 — 64 bits —, i386 — 32 bits— et x86_64 — 64 bits) mais que tous les programmes ne sont pas forcément distribués avec les 4 codes. En l’occurence, Apache et Python ont été compilé en i386 uniquement… mais voilà, même si on lui indique le chemin de Python a utilisé, le compilateur va chercher à un moment donné celui installé par défaut. En l’occurence, il s’agit d’une installation via MacPython, effectuée sur mon précédent Mac, avec un code PPC7400. Il m’a fallu un peu de temps avant de m’en rendre compte et remplacer le fichier PPC par un fichier i386 et permettre à la compilation de se terminer.

Du coup, entre temps, j’ai installé mod_python pour pouvoir avancer un peu sur la configuration de django via Apache. Pas de remarque particulière, l’installation se fait sans difficulté via MacPort et la configuration ne m’a pas posé de problème particulier, y compris l’authentification HTTP via Apache, django proposant un backend [en] pour cela. Ceci étant, ce n’était qu’une solution très provisoire, car mod_python ne permet pas d’utiliser Mercurial via django. En effet, le principe de django_hg est d’utiliser la requête django pour construire une requête Mercurial qui est en fait une requête wsgi. Ca fonctionne avec le serveur de développement de django car celui-ci est également wsgi. Évidemment, ce n’est pas le cas lorsque django est servi via mod_python.

D’où mon retour sur mod_wsgi. Une fois la compilation réussie, la configuration m’a donné un peu de mal également. En fait, là aussi, la doc est très claire, encore faut-il penser à vérifier le chemin de django. En effet, si django n’est pas dans le path du dossier où est exécuté le script wsgi, il faut ajouter django à l’aide de sys.path.append("/chemin/vers/django/"). Cependant, le piège par rapport aux exemples de la doc, c’est qu’avec MacPort, django est installé dans opt/local/lib/python2.5/site-packages/django/bin/. Or si on inclue juste opt/local/lib/python2.5/site-packages/django/, on peut inclure django, accéder aux settings, mais le script crash après. Mais si l’on lit bien la documentation, il est bien précisé qu’il faut mettre le chemin où se situe django-admin.py. Voilà qui m’apprendra à lire les documentations au pied de la lettre !

Cela fait, le reste a été un jeu d’enfant, mais je réserve le détail à un prochain article.

Filed under django_hg django python apache athena

0 notes &

django_hg sait cloner

J’ai profité du pont pour travailler sur l’un des points qui me semblait le plus critique dans django_hg, la gestion par django des requêtes HTTP utilisées pour cloner (et pour puller.)

Je dois dire que cela s’est très bien passé, bien plus que je ne l’aurai pensé, grâce notamment à l’aide de la mailing-list (et en particulier de Dirkjan Ochtman [en] et Benoît Boussicot [en].) En fait, ce qui m’a le plus retenu, c’est que je m’attendais tellement à galérer que j’ai fait des erreurs bêtes et tout développeur sait bien que ce sont les erreurs les plus évidentes qui sont les plus difficiles à corriger !

Faire traiter par django les requêtes d’un clone est donc très simple:

# views.py
def commands(request, name): 
    """ handles the commands sended by an hg client (clone, pull, push) 
    ``name`` 
        the name of the repository 
    """
    from mercurial import hg, ui 
    from mercurial.hgweb.request import wsgirequest 
    from mercurial.hgweb import protocol 

    cmds = ['between', 'branches', 'capabilities', 'changegroupsubset', 'heads'] 
    r = hg.repository(ui.ui(), name) 
    req = wsgirequest(request.META, None) 

    if request.GET.get('cmd') in cmds: 
        resp = protocol.__getattribute__(request.GET.get('cmd'))(r, req) 

    return HttpResponse(resp, protocol.HGTYPE)

Les imports correspondent aux différents objets de Mercurial. On note en particulier que les requêtes sont des objets wsgirequest. Cela tombe bien, django aussi, à ceci prêt que la request django engloble les données nécessaires à la création d’une requête wsgi dans le dictionnaire request.META.)

La liste des commandes disponibles est décrite dans cmds. Il suffit de créer en plus un objet repository r et après avoir vérifié que la commande est bien une des commandes attendues par l’objet protocol qui gère toutes les étapes [en] d’un clone, on obtient un flux qu’il suffit de retourner à Mercurial avec le bon type MIME, avec l’objet HttpResponse de django, dont l’inclusion se fait au niveau du module.

Simplissime, non ? Vous pouvez voir le code en situation sur bitbucket [en].

Ce code ne gère pas l’authentification. Je compte me pencher sur la question cette semaine, même si auparavant, je voudrais me pencher un peu sur ce blog qui est un peu en vrac depuis que Free, sur lequel j’héberge encore les images, est en carafe…

Filed under django django_hg python mercurial

0 notes &

django_hg

Je viens de mettre à disposition django_hg [en] sur bitbucket [en].

L’objectif de django_hg est de permettre la visualisation dans un projet django [en] d’un repository Mercurial [en].

Si vous connaissez un peu Mercurial, vous savez qu’un serveur web est intégré par défaut. Pourquoi donc refaire ce qui existe déjà ?

  • Pour apprendre Python !
  • Pour apprendre django !!
  • Pour apprendre Mercurial !!! Sur ces points, je trouve que ça se passe plutôt bien, dans l’ensemble, pas trop de galère, à part la mise en place des doctests sur les templatetags.
  • et aussi parce que je pense qu’il est plus facile d’adapter des templates django que ceux de Mercurial,
  • parce qu’à terme je souhaite ajouter des fonctions telles que l’intégration de l’authentification django. Cela va d’ailleurs être mon prochain travail, en me basant sur django-projectmgr [en].

Filed under django mercurial hg python