Sam & Max » wamp http://sametmax.com Du code, du cul Sat, 07 Nov 2015 10:56:13 +0000 en-US hourly 1 http://wordpress.org/?v=4.1 Jouons un peu avec Python 3.5 27 http://sametmax.com/jouons-un-peu-avec-python-3-5/ http://sametmax.com/jouons-un-peu-avec-python-3-5/#comments Wed, 16 Sep 2015 16:31:38 +0000 http://sametmax.com/?p=16918 fantastique article présentant Python 3.5, je ne vais donc pas pas répéter inutilement ce qu’ils ont dit. Le but de ce post est plutôt de faire mumuse avec le nouveau joujou.]]> Zeste de savoir a fait un fantastique article présentant Python 3.5, je ne vais donc pas répéter inutilement ce qu’ils ont dit. Le but de ce post est plutôt de faire mumuse avec le nouveau joujou.

La release est récente, mais fort heureusement on peut facilement l’installer. Sous Windows et Mac, il y a des builds tout chauds.

Pour linux, en attendant un repo tierce partie ou l’upgrade du système, on peut l’installer quelques commandes depuis les sources. Par exemple pour les distros basées sur Debian comme Ubuntu, ça ressemble à :

$ # dependances pour compiler python
$ sudo apt-get install build-essential libreadline-dev tk8.4-dev libsqlite3-dev libgdbm-dev libreadline6-dev liblzma-dev libbz2-dev libncurses5-dev libssl-dev python3-dev tk-dev
$ sudo apt-get build-dep python3 # juste pour etre sur :)
 
$ # téléchargement des sources
$ cd /tmp
$ wget https://www.python.org/ftp/python/3.5.0/Python-3.5.0.tar.xz
$ tar -xvf Python-3.5.0.tar.xz
$ cd Python-3.5.0
 
$ # et on build
$ ./configure
$ make
$ sudo make altinstall 
# pas 'make install' qui écrase le python du système !
 
$ python3.5 # ahhhhhh
Python 3.5.0 (default, Sep 16 2015, 10:44:14) 
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

Sur les centos-likes, c’est grosso merdo la même chose, sans le build-dep (mais plutôt un truc genre sudo yum groupinstall 'Development Tools'), et en remplaçant les -dev par -devel.

Nouvel opérateur

@ est maintenant le nouvel opérateur de produit matriciel, mais il ne fait officiellement rien.

Comprenez par là que Python implémente l’opérateur, mais pas le produit en lui-même, la feature ayant été spécialement incluse pour faire plaisir aux utilisateurs de libs scientifiques type numpy.

On va donc tester ça sur le terrain. On se fait un petit env temporaire avec pew et on s’installe numpy :

pew mktmpenv -p python3.5
pip install pip setuptools --upgrade
pip install numpy 
# encore un peu de compilation

Testons mon bon. L’ancienne manière de faire :

>>> a = np.array([[1, 0], [0, 1]])
>>> b = np.array([[4, 1], [2, 2]])
>>> np.dot(a, b)
array([[4, 1],
       [2, 2]])

Et la nouvelle :

>>> a @ b
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-10-3d41f06f59bb> in <module>()
----> 1 a @ b
 
TypeError: unsupported operand type(s) for @: 'numpy.ndarray' and 'numpy.ndarray'

Woops, apparemment numpy n’a pas encore implémenté le truc.

Bon. Bon, bon, bon. Comment on va tester alors… Ah, oui, y a une magic method :

class Array(np.ndarray):
    def __matmul__(self, other):
        return np.dot(self, other)
 
>>> a = a.view(Array)
>>> b = b.view(Array)
>>> a @ b
Array([[4, 1],
       [2, 2]])

Bon, voilà ce que ça donnera quand les devs de numpy auront implémenté le bouzin (la dernière ligne hein, pas tout le bordel avant).

Apparemment ça fait bander les matheux, donc je suppose que c’est une super nouvelle.

% is back on bytes

En python 2, on pouvait faire "truc %s" % "bidule" et u"truc %s" % u"bidule" et b"truc %s" % u"bidule" et ça a été viré en python 3 qui ne garde % que pour str et pas pour bytes.

Ca n’aurait pas été un problème si ce n’est que Python est très utilisé pour le réseau, et que construire un paquet qui mélange de la sémantique binaire et textuelle devient soudainement une grosse soupe de decode() et encode().

Jour 1, test 3, suspense…

>>> bytearray([66, 108, 117, 101, 32, 112, 114, 105, 101, 115, 116, 32, 115, 97, 121, 115, 58, 32, 37, 115]) % b"wololo"
bytearray(b'Blue priest says: wololo')

Voilà ça c’est fait !

os.scandir()

os.path.walk() est dans mon top 10 des APIs que je déteste le plus en Python, juste à côté de la gestion timezone. Avoir os.walk() en Python 3 qui retourne un générateur me ravit. Avoir une version 10 X plus rapide avec scandir, n’est-ce pas choupinet ?

>>> import os
>>> list(os.scandir('/tmp/'))
                       [<DirEntry 'systemd-private-316509818ceb41488a4721c78dabb603-colord.service-eXUfPo'>,
 <DirEntry 'unity_support_test.0'>,
 <DirEntry 'config-err-7UpWeO'>,
 <DirEntry '.ICE-unix'>,
 <DirEntry 'pip-rw_63q0_-unpack'>,
 <DirEntry 'systemd-private-316509818ceb41488a4721c78dabb603-systemd-timesyncd.service-eZumpq'>]

C’est très dommage que ça ne retourne pas des objets Path de pathlib, mais bon, les perfs, tout ça…

Zipapp, le grand inaperçu

Le saviez-vous ? Python peut exécuter un zip, ce qui permet de créer un script en plusieurs fichiers et de le partager comme un seul fichier. Non vous ne le saviez-vous-te-pas car personne n’en parle jamais.

La 3.5 vient avec un outil en ligne de commande pour faciliter la création de tels zip et une nouvelle extension (que l’installeur fera reconnaitre à Windows) pour cesdits fichiers : .pyz.

Je fais mon script :

foo
├── bar.py
├── __init__.py
└── __main__.py

__main__.py est obligatoire, c’est ce qui sera lancé quand on exécutera notre script. Dedans je mets import bar et dans bar print('wololo again').

Ensuite je fusionne tout ça :

python -m zipapp foo

Et pouf, j’ai mon fichier foo.pyz :

$ python3.5  foo.pyz
wololo again

Attention aux imports dedans, ils sont assez chiants à gérer.

L’unpacking généralisé

J’adore cette feature. J’adore toutes les features de la 3.5. Cette release est fantastique. Depuis la 3.3 chaque release est fantastique.

Mais bon, zeste de savoir l’a traité en long et en large donc rien à dire de plus, si ce n’est que j’avais raté un GROS truc :

  • On peut faire de l’unpacking sur n’importe quel itérable.
  • On peut faire de l’unpacking dans les tuples.
  • Les parenthèses des tuples sont facultatives.

Donc ces syntaxes sont valides :

>>> *range(2), *[1, 3], *'ea'
(0, 1, 1, 3, 'e', 'a')
>>> *[x * x for x in range(3)], *{"a": 1}.values()
(0, 1, 4, 1)

Ce qui peut être très chouette et aussi la porte ouverte à l’implémentation d’un sous-ensemble de Perl en Python. C’est selon l’abruti qui code.

Type hints

Ce qu’il faut bien comprendre avec les types hints, c’est que Python ne s’en sert pas. Il n’en fait rien. Que dalle. Nada. Peau de balle. Zob. Niet. Zero. La bulle. Néant. Null. None. Réforme gouvernementale.

Les types hints sont disponibles, mais Python ne va pas les traiter différemment d’autres annotations. Le but est de permettre à des outils externes (linter, IDE, etc) de se baser sur ces informations pour ajouter des fonctionnalités.

Pour l’instant, un seul le fait : mypy.

Et là on sent bien que tout ça est tout neuf car si on fait pip install mypy-lang, on tombe sur une version buggée. Il faut donc l’installer directement depuis le repo, soit :

pip install https://github.com/JukkaL/mypy/archive/master.zip

Puis écriture d’une fonction annotée avec des types hints :

 
from typing import Iterable, Tuple
 
PixelArray = Iterable[Tuple[int, int, int]]
 
def rgb2hex(pixels: PixelArray) -> list:
    pattern = "#{0:02x}{1:02x}{2:02x}"
    return [pattern.format(r, g, b) for r, g, b in pixels]
 
 
# ça marche :
rgb2hex([(1, 2, 3), (1, 2, 3)])
# ['#010203', '#010203']

La preuve que Python n’en fait rien :

>>> hex("fjdkls")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-2-be62b8f062fe> in <module>()
----> 1 hex("fjdkls")
 
TypeError: 'str' object cannot be interpreted as an integer

Même la doc profite peu du typage :

Help on function rgb2hex in module __main__:

rgb2hex(pixels:typing.Iterable) -> list

Mais si on met ça dans un fichier foo.py :

from essai import rgb2hex
 
print(rgb2hex("fdjksl"))
res = rgb2hex([(1, 2, 3), (3, 4, 5)])
print(res + 1)

Et qu’on le passe à la moulinette :

$ mypy foo.py 
foo.py:3: error: Argument 1 to "rgb2hex" has incompatible type "str"; expected Iterable[...]
foo.py:5: error: Unsupported operand types for + (List[Any] and "int")

Ensuite j’ai essayé de créer un stub file, c’est-à-dire de mettre les hints dans un fichier à part plutôt que directement dans le code. Ma fonction redevient :

def rgb2hex(pixels):
    pattern = "#{0:02x}{1:02x}{2:02x}"
    return [pattern.format(r, g, b) for r, g, b in pixels]

Et mon fichier stub (même nom, mais avec extension .pyi) contient :

from typing import Iterable, Tuple
 
PixelArray = Iterable[Tuple[int, int, int]]
 
def rgb2hex(pixels: PixelArray) -> list:...

Les stubs sont donc bien des fichiers Python valides, mais avec une extension différente, et juste les signatures des fonctions (le corps du bloc est une Ellipsis).

Et poof, ça marche pareil :

$ mypy foo.py 
foo.py:3: error: Argument 1 to "rgb2hex" has incompatible type "str"; expected Iterable[...]
foo.py:5: error: Unsupported operand types for + (List[Any] and "int")

Il y a un repo qui contient des fichiers stubs pour la stdlib. Vous pouvez y participer, c’est un moyen simple de contribuer à Python.

Bref, pour le moment ça demande encore un peu de maturité, mais je pense que d’ici quelques mois on aura des outils bien rodés pour faire tout ça automatiquement.

Async/await

La feature pub. Techniquement le truc qui a fait dire à tous ceux qui voulaient de l’asyncrone que Python en fait, c’était trop cool. Sauf que Python pouvait faire ça avec yield from avant, mais c’est sur que c’était super confusionant.

Maintenant on a un truc propre : pas de décorateur @coroutine, pas de syntaxe semblable aux générateurs, mais des méthodes magiques comme __await__ et de jolis mots-clés async et await.

Vu que Crossbar est maintenant compatible Python 3, et qu’il supporte asyncio pour les clients… Si on s’implémentait un petit wrapper WAMP pour s’amuser à voir ce que ressemblerait une API moderne pour du Websocket en Python ?

pip install crossbar
crossbar init
crossbar start

(Ouhhhh, plein de zolies couleurs apparaissent dans ma console ! Ils ont fait des efforts cosmétiques chez Tavendo)

Bien, voici maintenant l’exemple d’un client WAMP de base codé avec asyncio selon l’ancienne API :

import asyncio
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
 
class MyComponent(ApplicationSession):
 
    @asyncio.coroutine
    def onJoin(self, details):
 
        # on marque cette fonction comme appelable
        # a distance en RPC
        def add(a, b):
            return a + b
        self.register(add, "add")
 
        # et on triche en l'appelant cash. J'ai
        # la flemme de coder un deuxième client
        # et ça passe quand même par le routeur
        # donc merde
        res = yield from self.call("add", 2, 3)
        print("Got result: {}".format(res))
 
 
if __name__ == '__main__':
    runner = ApplicationRunner("ws://127.0.0.1:8080/ws",
        u"crossbardemo",
        debug_wamp=False,  # optional; log many WAMP details
        debug=False,  # optional; log even more details
    )
    runner.run(MyComponent)

Et ça marche nickel en 3.5. Mais qu’est-ce que c’est moche !

On est en train de bosser sur l’amélioration de l’API, mais je pense que ça va reste plus bas niveau que je le voudrais.

Donc, amusons-nous un peu à coder un truc plus sexy. Je vous préviens, le code du wrapper est velu, j’avais envie de me marrer un peu après les exemples ballots plus haut :

import asyncio
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
 
class App:
 
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.procedures = []
        self.subscriptions = []
        self.event_handlers  = {}
 
    def run(self, url="ws://127.0.0.1:8080/ws",
                realm="realm1", debug_wamp=False, debug=False):
        runner = ApplicationRunner(url, realm,
                                                     debug_wamp=debug_wamp,
                                                     debug=debug)
        runner.run(self)
 
    def run_cmd(self, *args, **kwargs):
        # et on pourrait même ici mettre du parsing d'argument
        # et de os.environ, mais j'ai la flemme
        if __name__ == '__main__':
            self.run(*args, **kwargs)
 
    # quelques décorateurs pour faire du déclaratif
    # et remettre les paramètres dans le bon ordre
    def register(self, name, *args, **kwargs):
            def wrapper(proc):
                self.procedures.append([name, proc, args, kwargs])
                return proc
            return wrapper
 
    def subscribe(self, topic, *args, **kwargs):
            def wrapper(callback):
                self.procedures.append([topic, callback, args, kwargs])
                return callback
            return wrapper
 
    # un système d'event interne
    def on(self, event):
            def wrapper(callback):
                self.event_handlers.setdefault(event, []).append(callback)
                return callback
            return wrapper
 
    async def trigger(self, event):
        for callback in self.event_handlers.get(event, ()):
            await callback(self.session)
 
    # un peu de code de compatibilité avec l'API initiale
    def __call__(self, *args):
        class CustomeSession(ApplicationSession):
            async def onJoin(session_self, details):
 
                # on joint on fait tous les registers et tous les
                # subscribes
                for name, proc, args, kwargs in self.procedures:
                     session_self.register(proc, name, *args, **kwargs)
 
                for topic, callback, args, kwargs in self.subscriptions:
                     session_self.subscribe(proc, topic, *args, **kwargs)
 
                # on appelle les handlers de notre event
                await self.trigger('joined')
        self.session = CustomeSession(*args)
        return self.session

Évidement la coloration syntaxique ne suit pas sur nos async/await.

Bon, vous allez me dire, mais ça quoi ça sert tout ça ? Et bien, c’est une version tronquée et codée à l’arrache de l’API Application pour Twisted… mais version asyncio.

C’est-à-dire que c’est une lib qui permet de faire le même exemple que le tout premier qu’on a vu dans cette partie – qui souvenez-vous était fort moche -, mais comme ça :

app = App()
 
@app.register('add')
async def add(a, b):
    return a + b
 
@app.on('joined')
async def _(session):
    res = await session.call("add", 2, 3)
    print("Got result: {}".format(res))
 
app.run_cmd()

Des jolis décorateurs ! Des jolis async ! Des jolis await !

Et tout ça tourne parfaitement sur 3.5 messieurs-dames.

Bref, on peut faire du WAMP avec une syntaxe claire et belle, il faut juste se bouger le cul pour coder une abstraction un peu propre.

Je pense que autobahn restera toujours un peu bas niveau. Donc il va falloir que quelqu’un se colle à faire une lib pour wrapper tout ça.

Des volontaires ?

Arf, je savais bien que ça allait me retomber sur la gueule.

]]>
http://sametmax.com/jouons-un-peu-avec-python-3-5/feed/ 27
Nouvelle release de crossbar : support de Python 3 ! 9 http://sametmax.com/nouvelle-release-de-crossbar-support-de-python-3/ http://sametmax.com/nouvelle-release-de-crossbar-support-de-python-3/#comments Wed, 09 Sep 2015 15:13:22 +0000 http://sametmax.com/?p=16908 WAMP en général, et je me suis fais un plaisir de leur rapporter toutes les merdes donc vous m'avez fait part.]]> L’équipe de Tavendo est à l’écoute de toutes les critiques de Crossbar et WAMP en général, et je me suis fait un plaisir de leur rapporter toutes les merdes dont vous m’avez fait part.

Cette nouvelle release contient beaucoup de choses qui corrigent ou pallient un paquet de trucs relou dans le routeur Crossbar (et par conséquent la lib client Autobahn) :

  • Support officiel de Python 3. Yes. Yes, yes yes !
  • Le debug a été complètement revu : meilleure console, meilleur login, meilleurs messages d’erreur et meilleur comportement en cas d’exceptions.
  • Un service dédié à l’upload de fichier intégré.
  • Un bridge HTTP complet qui permet d’utiliser Crossbar depuis n’importe quelle app qui peut faire des requêtes HTTP.

Pour la suite, ils travaillent sur la doc, et l’amélioration de l’API. En attendant, on peut pip install crossbar et profiter de ces nouveautés sans avoir à passer par github.

De mon côté j’ai un article sur l’authentification avec Crossbar dans les cartons. ETA dans les 10 prochains jours.

]]>
http://sametmax.com/nouvelle-release-de-crossbar-support-de-python-3/feed/ 9
Today is a glorious day 15 http://sametmax.com/today-is-a-glorious-day/ http://sametmax.com/today-is-a-glorious-day/#comments Sun, 12 Jul 2015 21:29:59 +0000 http://sametmax.com/?p=16600
>>> import crossbar
>>> crossbar.__version__
'0.10.4'
>>> import twisted
>>> twisted.__version__
'15.2.1'
>>> import sys
>>> print('Wait for it...')
Wait for it...
>>> sys.version
'3.4.0 (default, Apr 11 2014, 13:05:11) \n[GCC 4.8.2]'
]]>
http://sametmax.com/today-is-a-glorious-day/feed/ 15
Pendant ce temps, à Vera Cruz 8 http://sametmax.com/pendant-ce-temps-a-vera-cruz/ http://sametmax.com/pendant-ce-temps-a-vera-cruz/#comments Sun, 10 May 2015 09:29:24 +0000 http://sametmax.com/?p=16198 Pour une fois, ce n’est pas un article payé par Tavendo, mais bien un truc que je ponds par enthousiasme :)

Pendant qu’on en parle pas, la stack WAMP continue d’évoluer, des mises à jours significatives ayant été apportées à Crossbar.io, ainsi qu’aux libs Python et JS d’autobahn. Parmi les plus intéressantes :

  • Le code passe de la licence Apache 2 à la licence MIT, augmentant la compatibilité avec un tas d’autres licences.
  • On peut faire un SUB avec un joker, et donc lier un seul callback à plusieurs événements.
  • On peut faire un register avec un joker également.
  • On peut choisir la stratégie à appliquer si plusieurs registers sont faits sur le même nom.
  • Une meta API permet d’être prévenu quand un client fait quelque chose ou de demander l’état des nœuds en cours.

Inutile de dire que c’est trop cool.

Pour profiter de tout ça, il suffit de faire :

pip install crossbar autobahn --upgrade

Et de télécharger la nouvelle version de la dernière version de la lib JS.

Licence MIT

Auparavant le travail de Tavendo était essentiellement sous Licence Apache. Une licence libre, certes, mais qui pouvait poser problème quand on mélangeait tout ça avec d’autres licences (par exemple, elle n’est pas compatible avec la GPL2). Avec la version 0.10, le code est maintenant sous licence MIT, beaucoup plus permissive.

Joker pour les subs

Supposez que vous faites un système de jeu d’échec donc chaque coup déclenche un événement “chess.game.[id_de_partie]”. C’est pratique, car seuls les clients intéressés à cette partie vont recevoir les événements. Mais si votre serveur doit enregistrer un log de tous les coups d’une partie, il faut que chaque client envoie AUSSI les coups au serveur explicitement.

C’était en tout cas vrai avant cette mise à jour, puisque maintenant on peut spécifier des jokers dans les noms des topics au moment de l’abonnement.

Essentiellement il y a deux modes.

Le mode “prefix”, qui match tous les events qui commencent par ce nom :

session.subscribe("debut.du.nom.du.topic", callback, { match: "prefix" });
# matchera debut.du.nom.du.topic.genial et debut.du.nom.du.topic.trop.cool

Et le mode “wildcard” qui permet, un peu comme les glob Unix (mais on utilise “..” au lieu de “*””), de faire un texte à trou :

session.subscribe("nom.du.topic..general", callback, { match: "wildcard" });
# matchera "nom.du.topic.moins.general" et "nom.du.topic.oui.mon.general"

Tous les callbacks qui matchent un topic seront appelés.

Plusieurs clients pour la même procédure

On peut utiliser le même principe que pour les sub avec joker, mais pour les procédures.

session.register("debut.du.nom.de.la.procedure", callback, { match: "prefix" });    
session.register("nom.de.la.procedure..generale", callback, { match: "wildcard" });

La différence avec le subscribe, c’est que seule UNE procédure est appelée. Dans les cas simples, un match exact prend le dessus sur un prefix (et le plus long prefix gagne toujours), qui prend le dessus sur un wildcard. Crossbar n’implemente pas encore de résolution pour deux wildcards en conflits, et je ne sais pas ce qu’il fait dans ce cas.

Il est aussi possible de de définir des règles d’appels en faisant :

session.register("nom.de.la.procedure..generale", procedure1, { invoke: "regle"});

La règle peut être :

  • roundrobin: on prend la liste de clients, on regarde le dernier appelé, et on utilise le suivant.
  • random: on prend un client au hasard.
  • last: on prend le dernier client ajouté de la liste.
  • first: on prend premier client ajouté à la liste.

“roundrobin” et “random” sont pratiques pour faire du load balancing.

“last” et “first” sont pratique pour les mises à jour d’un client sans arrêter le serveur. En gros on rajoute un client, on attend un peu, “last” route tout sur le dernier client, donc le nouveau client prend les requêtes, et on peut arrêter le vieux clients sans souci.

Meta RPC

Crossbar met automatiquement à notre disposition des procédures distantes toutes faites qui donnent des informations sur l’état des clients et du routeur. Voici les RPC que vous pouvez maintenant faire :

  • wamp.session.list: lister les sessions des clients connectés au routeur.
  • wamp.session.get: obtenir les infos d’un session pour un ID en particulier.
  • wamp.session.count: obtenir le nombre de client connectés.
  • wamp.registration.lookup: absolument aucune idée.
  • wamp.registration.get: obtenir des infos sur une procédure distante enregistrée.
  • wamp.registration.list_callees: lister les clients ayant enregistré pour une procédure avec ce nom.
  • wamp.registration.count_callees: compter les clients ayant enregistré une procédure avec ce nom.
  • wamp.registration.list: lister toutes les procédures distantes disponibles.
  • wamp.registration.remove_callee: virer un client de la liste de des clients enregistrés pour cet procédure.
  • wamp.subscription.lookup: toujours aucune idée.
  • wamp.subscription.get: récupérer des infos sur l’abonnement avec cet ID.
  • wamp.subscription.list_subscribers: lister les clients qui sont abonnés à ce sujet.
  • wamp.subscription.count_subscribers: compter les clients abonnés à ce sujet.
  • wamp.subscription.match: aucune idée.
  • wamp.subscription.list: lister tous les sujets d’abonnement disponibles.
  • wamp.subscription.remove_subscriber:
  • virer un client de la liste des abonnés à ce sujet.

En gros, si vous voulez faire une admin qui vous permet de killer certains client ou rechercher si des events existent, vous utilisez ça.

Meta SUB

De même, le routeur envoie maintenant des publications sur des sujets concernant le cycle son cycle de vie et celui des clients. On peut donc s’abonner à ces meta topic pour réagir à l’activité de son système :

  • wamp.session.on_join : un client s’est connecté au routeur.
  • wamp.session.on_leave : un client s’est déconnecté du routeur.
  • wamp.subscription.on_create : un nouveau topic existe.
  • wamp.subscription.on_subscribe : un client s’est abonné à un topic.
  • wamp.subscription.on_unsubscribe : un client s’est désabonné à un topic.
  • wamp.subscription.on_delete : un topic est retiré de la liste des topics disponibles.
  • wamp.registration.on_create : une procédure distante porte ce nom pour la première fois.
  • wamp.registration.on_register : un client propose ajoute un callable pour ce nom de procédure distante..
  • wamp.registration.on_unregister : un client retire son callable pour ce nom de procédure distante.
  • wamp.registration.on_delete : le nom de cette procédure n’a plus aucun callable lié.
  • wamp.schema.on_define : aucune idée.
  • wamp.schema.on_undefine : kamolox.

Ce genre de truc est idéal pour faire un petit outil de monitoring pour son archi et voir ce qui se passe en temps réel.

Le HTTP bridge est complet

Le bridge HTTP propose maintenant PUB/SUB, et tout RPC. On peut donc maintenant utiliser crossbar depuis n’importe quel app qui peut faire du HTTP : flask, pyramid, ruby on rails, du PHP pur, wget en ligne de commande et tout le bordel. C’est plus verbeux, mais ça dépanne bien.

]]>
http://sametmax.com/pendant-ce-temps-a-vera-cruz/feed/ 8
Un petit dashboard de monitoring avec Django et WAMP 7 http://sametmax.com/un-petit-dashboard-de-monitoring-avec-django-et-wamp/ http://sametmax.com/un-petit-dashboard-de-monitoring-avec-django-et-wamp/#comments Sat, 07 Feb 2015 10:58:43 +0000 http://sametmax.com/?p=15872 Cet article est écrit dans le cadre de ma collaboration avec Tavendo.

On a déjà vu que WAMP c’est cool, mais c’est asynchrone et nos frameworks Web chéris WSGI sont synchrones.

J’ai donné une solution de contournement avec la lib crochet qui permet de faire tourner du twisted de manière synchrone dans son projet.

Néanmoins, beaucoup sont, j’en suis certain, à la recherche d’un truc plus simple. En effet, le bénéfice le plus immédiat de WAMP sont les notifications en temps réel. Et pour ça, crossbar vient avec le HTTP PUSHER service : quelques lignes de JSON dans le fichier de config de crossbar et zou, on peut publier sur un topic WAMP avec une simple requête POST :

 "transports": [
    {
       "type": "web",
       "endpoint": {
          "type": "tcp",
          "port": 8080
       },
       "paths": {
          ...
          "notify": {
             "type": "pusher",
             "realm": "realm1",
             "role": "anonymous"
          }
       }
    }
 ]

Et derrière, pour publier un event sur le sujet “super_sujet”, on peut faire :

import requets
requests.post("http://ip_du_router/pusher",
                  json={
                      'topic': 'super_sujet'
                      'args': [queques, params, a, passer, si, on veut]
                  })

Ceci va envoyer une requête POST à un service de crossbar qui va transformer ça en véritable publish WAMP.

Histoire d’illustrer tout ça, je vais vous montrer comment construire un petit service de monitoring avec Crossbar.io et Django. Pour suivre le tuto vous aurez besoin :

  • De connaissances de base en JS.
  • De connaître le principe de WAMP.
  • De savoir installer des bibliothèques Python avec extensions sur votre machine. pip et virtualenv sont vos amis.
  • De connaître Django. Même si le concept peut s’appliquer à Flask, Pyramid, ou autre.

Premiers pas

Le but du jeu est d’avoir un petit client WAMP qu’on lance sur chaque machine qu’on veut monitorer. Celui-ci va, toutes les x secondes, récupérer l’usage CPU, RAM et disque et faire un publish WAMP.

Chaque machine possède un client WAMP

Chaque machine possède un client WAMP

A l’autre bout, on a un site Django qui a un modèle pour chaque machine monitorée, avec des valeurs pour dire si on est intéressé par le CPU, la RAM ou le disque et la valeur de x.

Une page affiche en temps réel tous les relevés pour toutes les machines. Si dans l’admin de Django on change un modèle, la page reflète ce changement.

Si je déclique "CPU" dans l'admin Django, les CPUs ne sont plus affichés

Si je déclique “CPU” dans l’admin Django, les CPUs ne sont plus affichés

On aura donc besoin de django (pip install Django, ça c’est pas trop dur), requests (pip install requests, jusqu’ici tout va bien), et psutil.

psutil est la lib Python qui va nous permettre de récupérer toutes le valeurs pour la RAM, le disque et le CPU. Elle utilise des extensions en C, il faut donc un compilateur et les headers Python. Sous Ubuntu, il faut donc faire :

sudo apt-get install gcc python-dev

Sous CentOS ça donne :

yum groupinstall "Development tools"
yum install python-devel

Sous Mac, les headers Python devraient être inclus, mais il vous faut aussi GCC. Si vous avez xcode, vous avez déjà un compilateur, sinon, il existe un installeur plus léger.

Sous windows, c’est un wheel donc rien à faire normalement.

Et reste plus qu’à pip install psutil.

Enfin il nous faudra, logique, installer crossbar. pip install crossbar, sachant que sous Windows vous aurez besoin de PyWin32 et comme toujours, d’avoir les dossiers C:\Python27\ and C:\Python27\Scripts dans votre PATH.

Le HTML

On a besoin que d’une page. Afin de rendre le tuto agnostique, je l’ai fait en pur JS, pas de jQuery, pas d’Angular. Donc c’est verbeux :)

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
 
    <!-- De quoi cacher un bloc facilement -->
    <style type="text/css">
        .hide {display:none;}
    </style>
 
    <!--
        La lib JS qui permet de parler WAMP .
 
        Ici je suppose qu'on utilise un navigateur qui support websocket.
        Il est possible de faire du fallback sur flash ou long poll, mais
        ce sont des dépendances en plus.
    -->
    <script src="https://autobahn.s3.amazonaws.com/autobahnjs/latest/autobahn.min.jgz"
           type="text/javascript"></script>
 
 
    <!-- Tout notre code client, inline pour faciliter votre lecture -->
    <script type="text/javascript">
 
      /* Connexion à notre serveur WAMP */
      window.addEventListener("load", function(){
        var connection = new autobahn.Connection({
           url: 'ws://127.0.0.1:8080/ws',
           realm: 'realm1'
        });
 
        /* Quand la connexion est ouverte, exécuter ce code */
        connection.onopen = function(session) {
 
          var clients = document.getElementById("clients");
 
          /* Quand on reçoit l'événement clientstats, lancer cette fonction */
          session.subscribe('clientstats', function(args){
            var stats = args[0];
            var serverNode = document.getElementById(stats.ip);
 
            /*
                 Créer un li contenant un h2 et un dl pour ce client si
                 il n'est pas encore dans la page.
            */
            if (!serverNode){
                serverNode = document.createElement("li");
                serverNode.id = stats.ip;
                serverNode.appendChild(document.createElement("h2"));
                serverNode.appendChild(document.createElement("dl"));
                serverNode.firstChild.innerHTML = stats.name + " (" + stats.ip + ")";
                clients.appendChild(serverNode);
 
                // Cacher les infos du serveur si il est désactivé.
                session.subscribe('clientconfig.' + stats.ip, function(args){
                    var config = args[0];
                    if (config.disabled){
                        var serverNode = document.getElementById(config.ip);
                        serverNode.className = "hide";
                    }
                });
 
            }
 
            // Remettre à zéro le contenu du li du serveur.
            serverNode.className = "";
            var dl = serverNode.lastChild;
            while (dl.hasChildNodes()) {
                dl.removeChild(dl.lastChild);
            }
 
            // Si on a des infos sur le CPU, les afficher
            if (stats.cpus){
                var cpus = document.createElement("dt");
                cpus.innerHTML = "CPUs:";
                dl.appendChild(cpus);
                for (var i = 0; i < stats.cpus.length; i++) {
                    var cpu = document.createElement("dd");
                    cpu.innerHTML = stats.cpus[i];
                    dl.appendChild(cpu);
                };
            }
 
            // Si on a des infos sur l'espace disque, les afficher
            if (stats.disks){
                var disks = document.createElement("dt");
                disks.innerHTML = "Disk usage:";
                dl.appendChild(disks);
                for (key in stats.disks) {
                    var disk = document.createElement("dd");
                    disk.innerHTML = "<strong>" + key + "</strong>: " + stats.disks[key];
                    dl.appendChild(disk);
                };
            }
 
            // Si on a des infos sur l'usage mémoire, les afficher.
            if (stats.memory){
                var memory = document.createElement("dt");
                memory.innerHTML = "Memory:";
                dl.appendChild(memory);
                var memVal = document.createElement("dd");
                memVal.innerHTML = stats.memory;
                dl.appendChild(memVal);
            }
 
          });
 
        };
 
        // Ouvrir la connexion avec le routeur WAMP.
        connection.open();
 
      });
    </script>
 
    <title> Monitoring</title>
</head>
<body>
    <h1> Monitoring </h1>
    <ul id="clients"></ul>
</body>
 
</html>

Comme vous pouvez le voir, c’est beaucoup de JS ordinaire et du DOM. Les seules parties spécifiques à WAMP sont :

var connection = new autobahn.Connection({
           url: 'ws://127.0.0.1:8080/ws',
           realm: 'realm1'
        });
connection.onopen = function(session) {
...
}
connection.open();

Pour se connecter au serveur.

Et :

session.subscribe('nom_du_sujet', function(args){
...
}

Pour réagir à la publication d’un sujet WAMP.

Le client de monitoring

C’est la partie qui va aller sur chaque machine qu’on veut surveiller.

# -*- coding: utf-8 -*-
 
from __future__ import division
 
import socket
 
import requests
import psutil
 
from autobahn.twisted.wamp import Application
from autobahn.twisted.util import sleep
 
from twisted.internet.defer import inlineCallbacks
 
def to_gib(bytes, factor=2**30, suffix="GiB"):
    """ Converti un nombre d'octets en gibioctets.
 
        Ex : 1073741824 octets = 1073741824/2**30 = 1GiO
    """
    return "%0.2f%s" % (bytes / factor, suffix)
 
def get_infos(filters={}):
    """ Retourne la valeur actuelle de l'usage CPU, mémoire et disque.
 
        Ces valeurs sont retournées sous la forme d'un dictionnaire :
 
            {
                'cpus': ['x%', 'y%', etc],
                'memory': "z%",
                'disk':{
                    '/partition/1': 'x/y (z%)',
                    '/partition/2': 'x/y (z%)',
                    etc
                }
            }
 
        Le paramètre filter est un dico de la forme :
 
            {'cpus': bool, 'memory':bool, 'disk':bool}
 
        Il est utilisé pour décider d'inclure ou non les résultats des mesures
        pour les 3 types de ressource.
 
    """
 
    results = {}
 
    if (filters.get('show_cpus', True)):
        results['cpus'] = tuple("%s%%" % x for x in psutil.cpu_percent(percpu=True))
 
    if (filters.get('show_memory', True)):
        memory = psutil.phymem_usage()
        results['memory'] = '{used}/{total} ({percent}%)'.format(
            used=to_gib(memory.active),
            total=to_gib(memory.total),
            percent=memory.percent
        )
 
    if (filters.get('show_disk', True)):
        disks = {}
        for device in psutil.disk_partitions():
            usage = psutil.disk_usage(device.mountpoint)
            disks[device.mountpoint] = '{used}/{total} ({percent}%)'.format(
                used=to_gib(usage.used),
                total=to_gib(usage.total),
                percent=usage.percent
            )
        results['disks'] = disks
 
    return results
 
# On créé le client WAMP.
app = Application('monitoring')
 
# Ceci est l'IP publique de ma machine puisque
# ce client doit pouvoir accéder à mon serveur
# depuis l'extérieur.
SERVER = '172.17.42.1'
 
# D'abord on utilise une astuce pour connaître l'IP publique de cette
# machine.
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
# On attache un dictionnaire à l'app, ainsi
# sa référence sera accessible partout.
app._params = {'name': socket.gethostname(), 'ip': s.getsockname()[0]}
s.close()
 
@app.signal('onjoined')
@inlineCallbacks
def called_on_joinded():
    """ Boucle envoyant l'état de cette machine avec WAMP toutes les x secondes.
 
        Cette fonction est exécutée quand le client "joins" le router, c'est
        à dire qu'il est connecté et authentifié, prêt à envoyer des messages
        WAMP.
    """
    # Ensuite on fait une requête post au serveur pour dire qu'on est
    # actif et récupérer les valeurs de configuration de notre client.
    app._params.update(requests.post('http://' + SERVER + ':8080/clients/',
                                    data={'ip': app._params['ip']}).json())
 
 
    # Puis on boucle indéfiniment
    while True:
        # Chaque tour de boucle, on récupère les infos de notre machine
        infos = {'ip': app._params['ip'], 'name': app._params['name']}
        infos.update(get_infos(app._params))
 
        # Si les stats sont a envoyer, on fait une publication WAMP.
        if not app._params['disabled']:
            app.session.publish('clientstats', infos)
 
        # Et on attend. Grâce à @inlineCallbacks, utiliser yield indique
        # qu'on ne bloque pas ici, donc pendant ce temps notre client
        # peut écouter les événements WAMP et y réagir.
        yield sleep(app._params['frequency'])
 
 
# On dit qu'on est intéressé par les événements concernant clientconfig
@app.subscribe('clientconfig.' + app._params['ip'])
def update_configuration(args):
    """ Met à jour la configuration du client quand Django nous le demande. """
    app._params.update(args)
 
# On démarre notre client.
if __name__ == '__main__':
    app.run(url="ws://%s:8080/ws" % SERVER)

Le plus gros du code est get_infos() qui n’a rien à voir avec WAMP. C’est nous, manipulant psutil pour obtenir les relevés de cette machine. Je ne recommande bien évidement pas de faire ça en prod : une grosse fonction monolithique qui prend un dico en param. Mais c’est pour une démo, et ça me permet de grouper les instructions qui vont ensemble pour faciliter votre compréhension.

La partie qui concerne WAMP :

app = Application('monitoring')
 
@app.signal('onjoined')
@inlineCallbacks
def called_on_joinded():
    ...
 
    while True:
 
        ...
        app.session.publish('clientstats', infos)
        ...
        yield sleep(app._params['frequency'])

app = Application('monitoring') créé un client WAMP, et @app.signal('onjoined') nous dit de lancer la fonction quand notre client est connecté et prêt à envoyer des événements. @inlineCallbacks est une spécificité de Twisted qui nous permet d’écrire du code asynchrone sans avoir à mettre des callback partout : à la place on met des yield.

Tout le boulot de notre client a lieu dans la boucle : app.session.publish('clientstats', infos) publie les nouvelles mesures de CPU/RAM/Disque via WAMP, puis attend un certain temps (yield sleep(app._params['frequency'])) avant de le faire à nouveau. L’attente n’est pas bloquante car elle se fait avec le sleep de Twisted.

N’oublions pas :

@app.subscribe('clientconfig.' + app._params['ip'])
def update_configuration(args):
    app._params.update(args)

La fonction update_configuration() sera appelée à chaque fois qu’une publication WAMP sera faite sur le sujet clientconfig.<ip_du_client>. Notre fonction ne fait que mettre à jour la configuration du client, qui est un dico de la forme :

    {'cpus': True,
    'memory': False,
    'disk': True,
    'disabled': False,
    'frequency': 1}

C’est ce dico qui est utilisé par get_infos() pour choisir quelles mesures récupérer, et aussi par sleep() pour savoir combien de secondes attendre avant la prochaine mesure.

La valeur initiale de ce dico est récupérée au lancement du client, en faisant :

app._params.update(requests.post('http://' + SERVER + ':8080/clients/',
                                    data={'ip': app._params['ip']}).json())

requests.post(url_du_serveur, data={'ip': app._params['ip']}).json() fait en effet une requête POST vers une URL de django qui nous allons voir plus loin, et qui retourne la configuration du client portant cette IP sous forme de JSON.

On utilise donc une fois HTTP pour obtenir les valeurs de départs, et ensuite WAMP pour les mises à jours des futures valeurs. WAMP et HTTP ne s’excluent pas : ils sont complémentaires.

Petite parenthèse sur :

SERVER = '172.17.42.1'
 
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
app._params = {'name': socket.gethostname(), 'ip': s.getsockname()[0]}
s.close()

D’une part, j’ai mis l’IP du serveur qui va contenir Crossbar.io et Django en dur car je suis, je pense que maintenant vous le savez, une grosse feignasse. Mais en prod, vous me faites un paramètre, on est d’accord ? Ensuite, il faut que j’identifie mon client, ce que je fais avec l’adresse IP. Il me faut donc son adresse IP externe, et je l’obtiens avec une astuce consistant à me connecter à l’IP 8.8.8.8 (les DNS google \o/) et en fermant la connexion juste derrière. Ce me permet de voir comment les autres machines me voit depuis l’extérieur.

Le site Django

Puisque le prérequis de l’article et de connaître Django, ça va pas être trop dur.

On créé son projet et son app :

django-admin startproject django_project
./manage.py startapp django_app

On se rajoute un petit modèle qui contient la configuration de chaque client (vous vous souvenez, le fameux dico) :

# -*- coding: utf-8 -*-
 
import requests
 
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.forms.models import model_to_dict
 
 
class Client(models.Model):
    """ Configuration de notre client. """
 
    # Pour l'identifier.
    ip = models.GenericIPAddressField()
 
    # Quelles données envoyer à notre dashboard
    show_cpus = models.BooleanField(default=True)
    show_memory = models.BooleanField(default=True)
    show_disk = models.BooleanField(default=True)
 
    # Arrêter d'envoyer les données
    disabled = models.BooleanField(default=False)
 
    # Fréquence de rafraîchissement des données
    frequency = models.IntegerField(default=1)
 
    def __unicode__(self):
        return self.ip
 
 
@receiver(post_save, sender=Client, dispatch_uid="server_post_save")
def notify_server_config_changed(sender, instance, **kwargs):
    """ Notifie un client que sa configuration a changé.
 
        Cette fonction est lancée quand on sauvegarde un modèle Client,
        et fait une requête POST sur le bridge WAMP-HTTP, nous permettant
        de faire un publish depuis Django.
    """
    requests.post("http://127.0.0.1:8080/notify",
                  json={
                      'topic': 'clientconfig.' + instance.ip,
                      'args': [model_to_dict(instance)]
                  })

La partie modèle est connue. L’astuce est dans :

@receiver(post_save, sender=Client, dispatch_uid="server_post_save")
def notify_server_config_changed(sender, instance, **kwargs):
    requests.post("http://127.0.0.1:8080/notify",
                  json={
                      'topic': 'clientconfig.' + instance.ip,
                      'args': [model_to_dict(instance)]
                  })

On utilise ici les signaux Django, une fonctionnalité du framework qui nous permet de lancer une fonction quand quelque chose se passe. Ici on dit “lance cette fonction quand le modèle Client est modifié”.

Donc notify_server_config_changed va se lancer quand la config d’un client est modifiée, par exemple dans l’admin, et recevoir l’objet modifié via son paramètre instance.

On fait alors une petite requête POST sur http://127.0.0.1:8080/notify, l’URL sur laquelle on configurera plus loin notre service de push. En faisant une requête dessus, on va demander à Crossbar.io de transformer la requête HTTP en message publish WAMP, ici sur le sujet ‘clientconfig.<ip_du_client>’. On publie donc un message WAMP, depuis Django.

Ca marche depuis n’importe où, pas juste Django. Depuis le shell, depuis Flask, n’importe où on peut faire une requête HTTP vers le service de push de crossbar.

Ce message va être récupéré par notre client, où qu’il soit, puisqu’il est aussi connecté au routeur WAMP. Comme, je vous le rappelle, notre client fait ça :

@app.subscribe('clientconfig.' + app._params['ip'])
def update_configuration(args):
    app._params.update(args)

Il va recevoir ce message, et donc le contenu de 'args': [model_to_dict(instance)], c’est à dire la nouvelle configuration qu’on a changé en base de donnée. Il se met ainsi à jour immédiatement. La boucle est bouclée.

Comme on veut profiter de notre boucle toute bouclée, on rajoute le modèle dans l’admin :

from django.contrib import admin
 
# Register your models here.
 
from django_app.models import Client
 
admin.site.register(Client)

Ainsi, les configs des clients seront éditables dans l’admin, et quand on cliquera sur “save”, ça va lancer notre publish WAMP qui mettra à jour le bon client.

Le reste, c’est du fignolage. Une petite vue pour créer ou récupérer notre configuration de client au démarrage :

# -*- coding: utf-8 -*-
 
import json
 
from django.http import HttpResponse
from django_app.models import Client
from django.views.decorators.csrf import csrf_exempt
from django.forms.models import model_to_dict
 
 
@csrf_exempt
def clients(request):
    """ Récupère la config d'un client en base de donnée et lui envoie."""
    client, created = Client.objects.get_or_create(ip=request.POST['ip'])
    return HttpResponse(json.dumps(model_to_dict(client)), content_type='application/json')

On désactive la protection CSRF pour la démo, mais encore une fois, en prod, faites ça proprement, avec une jolie authentification pour protéger la vue, et tout, et tout.

Donc, cette vue récupère la configuration d’un client avec cette IP (la créant au besoin), et la retourne en JSON. Souvenez-vous, cela permet à notre client de faire :

    app._params.update(requests.post('http://' + SERVER + ':8080/clients/',
                                    data={'ip': app._params['ip']}).json())

Au démarrage et se déclarer dans la base de données, tout en récupérant sa config.

On branche tout ça via urls.py :

from django.conf.urls import patterns, include, url
from django.contrib import admin
from django.views.generic import TemplateView
 
urlpatterns = patterns('',
    url(r'^admin/', include(admin.site.urls)),
    url(r'^clients/', 'django_app.views.clients'),
    url(r'^$', TemplateView.as_view(template_name='dashboard.html')),
)

L’admin, notre vue toute fraiche, et de quoi servir le HTML du début de l’article.

Y plus qu’à :

./manage.py syncdb

Crossbar.io

Finalement, tout ce qu’il reste, c’est notre bon crossbar :

crossbar init

Ceci nous pond le dossier .crossbar dans lequel on a le fichier config.json qu’on édite pour qu’il ressemble à ça :

{
   "workers": [
      {
         "type": "router",
         "realms": [
            {
               "name": "realm1",
               "roles": [
                  {
                     "name": "anonymous",
                     "permissions": [
                        {
                           "uri": "*",
                           "publish": true,
                           "subscribe": true,
                           "call": true,
                           "register": true
                        }
                     ]
                  }
               ]
            }
         ],
         "transports": [
            {
               "type": "web",
               "endpoint": {
                  "type": "tcp",
                  "port": 8080
               },
               "paths": {
                  "/": {
                     "type": "wsgi",
                     "module": "django_project.wsgi",
                     "object": "application"
                  },
                  "ws": {
                     "type": "websocket"
                  },
                  "notify": {
                     "type": "pusher",
                     "realm": "realm1",
                     "role": "anonymous"
                  },
                  "static": {
                     "type": "static",
                     "directory": "../static"
                  }
               }
            }
         ]
      }
   ]
}

La partie du haut c’est un peu l’équivalent du chmod 777 de crossbar :

         "type": "router",
         "realms": [
            {
               "name": "realm1",
               "roles": [
                  {
                     "name": "anonymous",
                     "permissions": [
                        {
                           "uri": "*",
                           "publish": true,
                           "subscribe": true,
                           "call": true,
                           "register": true
                        }
                     ]
                  }
               ]
            }
         ],

“Met moi en place un router avec un accès nommé realm1 qui autorise à tous les anonymes de tout faire”. Un realm est une notion de sécurité dans Crossbar.io qui permet de cloisonner les clients connectés, nous on va tout mettre sur le même realm, c’est pour une démo je vous dis.

Ensuite on rajoute les transports pour chaque techno qui nous intéresse. On va tout regrouper sur le port 8080 car Twisted peut écouter en HTTP et Websocket sur le même port :

"transports": [
{
   "type": "web",
   "endpoint": {
      "type": "tcp",
      "port": 8080
   },

A la racine, on sert notre app Django :

  "/": {
     "type": "wsgi",
     "module": "django_project.wsgi",
     "object": "application"
  },

Car oui, crossbar peut servir votre app django en prod. Pas besoin de gunicorn. En fait même pas besoin d’nginx pour un site simple, car ça tient très bien la charge. On a juste à lui indiquer quelle variable (application) de quel fichier WSGI (django_project/wsgi.py) charger, et il s’occupe du reste.

Sur ‘/ws’, on écoute en Websocket :

"ws": {
 "type": "websocket"
},

WAMP passe par là, et c’est pour ça que nos clients se connectent en faisant app.run(url="ws://%s:8080/ws" % SERVER) et autobahn.Connection({url: 'ws://127.0.0.1:8080/ws', realm: 'realm1'});.

‘/notify’ va recevoir le bridge WAMP-HTTP :

"notify": {
     "type": "pusher",
     "realm": "realm1",
     "role": "anonymous"
  }

Tous les anonymes du realm1 peuvent l’utiliser. Grâce à ça, on a pu faire depuis notre signal Django :

    requests.post("http://127.0.0.1:8080/notify",
                  json={
                      'topic': 'clientconfig.' + instance.ip,
                      'args': [model_to_dict(instance)]
                  })

Et donc publier un message WAMP, via un POST HTTP.

Enfin, on sert les fichiers statiques Django avec Crossbar (oui, il fait aussi ça :):

 "static": {
    "type": "static",
    "directory": "../static"
}

N’oubliez pas le de spécifier STATIC_ROOT dans le fichier settings et lancer ./manage.py collecstatic.

Tout ça en place, on lance notre routeur :

export PYTHONPATH=/chemin/vers/votre/project
crossbar start

(Remplacer export par set sous Windows>

La modification de PYTHONPATH est nécessaire pour que crossbar trouve votre fichier WSGI.

On visite http:127.0.0.1:8080/, qui va charger notre template Django dashboard.html.

Chaque machine qui lance un client via python client.py va déclencher l’apparition des stats sur notre dashboard, qui seront mises à jour en temps réel.

Si on va sur http:127.0.0.1:8080/admin/ et qu’on change la config d’un client, notre client s’adapte, et notre dashboard se met à jour automatiquement.

Conclusion

Notre projet ressemble à ceci au final :

.
├── client.py
├── .crossbar
│   ├── config.json
├── db.sqlite3
├── django_app
│   ├── admin.py
│   ├── __init__.py
│   ├── models.py
│   ├── templates
│   │   └── dashboard.html
│   └── views.py
├── django_project
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── static
└── manage.py

Vous pouvez récupérer le code ici.

Finalement, très peu de code WAMP : un peu dans le JS, un peu dans le client. Et la seule chose qui lie WAMP à Django est la config crossbar qui ajoute le service HTTP PUSHER et notre requête POST dans models.py

Cette technique n’est pas limitée à Django, et fonctionne bien pour toutes techno synchrones qui ne peut pas lancer un client WAMP directement en son sein. Pour le moment, le bridge HTTP-WAMP ne propose que PUB, pas de SUB, de pas de RPC. C’est déjà assez sympa pour avoir les notifications en temps réel un peu partout, et ça Tobias m’a dit qu’il ajoutera les autres actions dans un future proche.

En attendant, vous voyez le deal : on peut mélanger allègrement HTTP, WAMP, Python, JS, Client, Serveur, et monter sa petite architecture comme on le souhaite. Crossbar permet de démarrer du WSGI, mais aussi les clients WAMP sur la même machine et même n’importe quel process en ligne de commande (par exemple NodeJS) si besoin. C’est Mac Gyver ce truc.

On aurait pu écrire le client en Python 3 puisqu’il est sur une autre machine. Et en fait, si on lance Django en dehors de crossbar, aussi la partie Django en Python 3. Le code de crossbar n’est jamais modifié, on touche juste la configuration JSON.

Personnellement j’ai lancé plusieurs images dockers avec un client dedans à chaque fois, et c’est vraiment sympas de voir les machines se rajouter sur le dashboard en temps réel. On a une super sensation d’interactivité quand on change une valeur dans l’admin et qu’on voit le dashboard bouger.

]]>
http://sametmax.com/un-petit-dashboard-de-monitoring-avec-django-et-wamp/feed/ 7
Les managers le détestent : faites tourner WAMP dans Django avec cette astuce insolite 11 http://sametmax.com/les-managers-le-detestent-faites-tourner-wamp-dans-django-avec-cette-astuce-insolite/ http://sametmax.com/les-managers-le-detestent-faites-tourner-wamp-dans-django-avec-cette-astuce-insolite/#comments Sun, 04 Jan 2015 19:45:07 +0000 http://sametmax.com/?p=15665 directement dans Django.]]> Il existe une lib appelée crochet qui permet de faire marcher des API de twisted entre deux bouts de code bloquants. Certes, ça ne marche qu’en 2.7 et c’est pas hyper performant, mais on peut faire des trucs mignons du genre cette démo qui mélange flask et WAMP.

C’est du pur Python, pas de process externe à gérer, c’est presque simple.

Bref, si on veut utiliser WAMP avec une app synchrone comme flask, c’est un bon moyen de s’y mettre. On aura jamais des perfs fantastiques, mais on peut pusher vers le browser.

Du coup je me suis demandé si on pouvait faire ça avec Django.

Évidement, ça a été un peu plus compliqué car par défaut runserver lance plusieurs workers et fait un peu de magie avec les threads. Mais après un peu de bidouillage, ça marche !

On peut utiliser WAMP, directement dans Django.

Suivez le guide

D’abord, on installe tout le bouzin (python 2.7, souvenez-vous) :

pip install crossbar crochet django

Il vous faudra un Django 1.7, le tout dernier, car il possède une fonctionnalité qui nous permet de lancer du code quand tout le framework est chargé.

Vous vous faites votre projet comme d’hab, et vous ouvrez le fichier de settings et au lieu de mettre votre app dans INSTALLED_APPS, vous rajoutez ça :

INSTALLED_APPS = (
    '...',
    'votreapp.app.VotreAppConfig'
)

Puis dans le module de votre app, vous créez un fichier app.py, qui va contenir ça:

# -*- coding: utf-8 -*-
 
import crochet
 
from django.apps import AppConfig
 
# On charge l'objet contenant la session WAMP définie dans la vue
from votreapp.views import wapp
 
class VotreAppConfig(AppConfig):
    name = 'votreapp'
    def ready(self):
        # On dit a crochet de faire tourner notre app wamp dans sa popote qui
        # isole le reactor de Twisted
        @crochet.run_in_reactor
        def start_wamp():
           # On démarre la session WAMP en se connectant au serveur
           # publique de test
           wapp.run("wws://demo.crossbar.io/ws", "realm1", start_reactor=False)
        start_wamp()

On passe à urls.py dans lequel on se rajoute des vues de démo :

    url(r'^ping/', 'votreapp.views.ping'),
    url(r'^$', 'votreapp.views.index')

Puis dans notre fichier views.py, on met :

# -*- coding: utf-8 -*-
 
import uuid
 
from django.shortcuts import render
 
import crochet
 
# Crochet se démerde pour faire tourner le reactor twisted de
# manière invisible. A lancer avant d'importer autobahn
crochet.setup()
 
from autobahn.twisted.wamp import Application
 
# un objet qui contient une session WAMP
wapp = Application()
 
# On enrobe les primitives de WAMP pour les rendre synchrones
@crochet.wait_for(timeout=1)
def publish(topic, *args, **kwargs):
   return wapp.session.publish(topic, *args, **kwargs)
 
@crochet.wait_for(timeout=1)
def call(name, *args, **kwargs):
   return wapp.session.call(name, *args, **kwargs)
 
def register(name, *args, **kwargs):
    @crochet.run_in_reactor
    def decorator(func):
        wapp.register(name, *args, **kwargs)(func)
    return decorator
 
def subscribe(name, *args, **kwargs):
    @crochet.run_in_reactor
    def decorator(func):
        wapp.subscribe(name, *args, **kwargs)(func)
    return decorator
 
# Et hop, on peut utiliser nos outils WAMP !
 
@register('uuid')
def get_uuid():
    return uuid.uuid4().hex
 
@subscribe('ping')
def onping():
    with open('test', 'w') as f:
        f.write('ping')
 
# Et à côté, quelques vues django normales
 
def index(request):
    # pub et RPC en action côté Python
    publish('ping')
    print call('uuid')
 
    with open('test') as f:
        print(f.read())
    return render(request, 'index.html')
 
def ping(request):
    return render(request, 'ping.html')

Après, un peu de templating pour que ça marche…

Index.html :

{% load staticfiles %}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>
       UUID
    </title>
 
    <script src="{% static 'autobahn.min.js' %}"></script>
    <script type="text/javascript">
      var connection = new autobahn.Connection({
         url: "ws://localhost:8080/ws",
         realm: "realm1"
      });
 
     connection.onopen = function (session) {
 
        session.call("uuid").then(function (uuid) {
          var p = document.getElementById('uuid');
          p.innerHTML = uuid;
        });
     };
 
     connection.open();
    </script>
</head>
<body>
<h2>UUID</h2>
<p id="uuid"></p>
</body>
</html>

ping.html :

{% load staticfiles %}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>
       Ping
    </title>
 
    <script src="{% static 'autobahn.min.js' %}"></script>
    <script type="text/javascript">
      var connection = new autobahn.Connection({
         url: "ws://localhost:8080/ws",
         realm: "realm1"
      });
 
     connection.onopen = function (session) {
 
        session.subscribe("ping", function () {
          var ul = document.getElementById('ping');
          var li = document.createElement('li');
          li.innerHTML = 'Ping !'
          ul.appendChild(li);
        });
     };
 
     connection.open();
    </script>
</head>
<body>
<h2>Ping me !</h2>
 
<ul id="ping">
</ul>
</body>
</html>

On ouvre la console, on lance son routeur :

    crossbar init
    crossbar start

On lance dans une autre console son serveur Django :

./manage.py runserver

Et si on navigue sur http://127.0.0.1:8000, on récupère un UUID tout frais via RCP.

On peut aussi voir dans le shell que ça marche côté Python :

94cfccf0899d4c42950788fa655b65ed
ping

D’ailleurs un fichier nommé “test” est créé à la racine du projet.

Et si on navigue sur http://127.0.0.1:8000/ping/ et qu’on refresh http://127.0.0.1:8000 plusieurs fois, on voit la page se mettre à jour.

Achievement unlock : use WAMP and Django code in the same file.

A partir de là

Il y a plein de choses à faire.

On pourrait faire une lib qui wrap tout ça pour pas à avoir à le mettre dans son fichier de vue et qui utilise settings.py pour la configuration.

Il faut tester ça avec des setups plus gros pour voir comment ça se comporte avec gunicorn, plusieurs workers, le logging de Django, etc. Je suis à peu près sûr que les callbacks vont être registrés plusieurs fois et ça devrait faire des erreurs dans les logs (rien de grave ceci dit).

On pourrait aussi adapter le RPC pour qu’il utilise les cookies d’authentification Django, et pouvoir les protéger avec @login_required.

Mais un monde d’opportunités s’offrent à vous à partir de là.

Moi, ça fait 6 h que je taffe dessus, je vais me pieuter.


Télécharger le code de l’article

]]>
http://sametmax.com/les-managers-le-detestent-faites-tourner-wamp-dans-django-avec-cette-astuce-insolite/feed/ 11
Bridge HTTP/WAMP 5 http://sametmax.com/bridge-httpwamp/ http://sametmax.com/bridge-httpwamp/#comments Mon, 29 Dec 2014 10:07:59 +0000 http://sametmax.com/?p=13072 Il y a 4 gros freins à l’adoption de WAMP :

  1. L’incompréhension de la techno. Les gens ne voient pas à quoi ça sert, ce qu’ils peuvent faire avec, et ont peur. Logique. Je vais pas les blâmer, c’est pareil avec toute nouvelle techo. S’investir dans un nouveau truc a un coût, surtout un truc jeune.
  2. La doc. je vais travailler sur la question, mais c’est un travail de fond.
  3. L’API : ça va avec les deux points plus haut. Il faut quelque chose de plus haut niveau. L’API flaskesque est un bon début, il faut maintenant continuer dans ce sens.
  4. L’intégration avec les anciennes technos. Par exemple, un site Django. Personne n’a envie de mettre toute son ancienne stack à la poubelle.

Toute les libs qui introduisent une nouvelle façon de travailler rencontrent ce problème. Quand les ORM sont sortis, c’était pareil. Quand les Django et Rails, et Symfony sont sortis, c’était pareil.

Mais puisqu’on le sait, on peut agir.

Les 3 premiers points ont déjà un début de solution, ce qui nous intéresse c’est donc le 4ème point.

Il y a de nombreuses choses à faire pour l’intégration : authentification, actions bloquantes, communications…

On ne peut pas tout résoudre d’un coup, mais une solution qui ratisse large serait de créer un bridge HTTP/WAMP.

Le principe : faire un client WAMP qui soit aussi client HTTP avec une API REST.

Fonctionnement

En envoyant des requêtes HTTP avec un webtoken pour s’authentifier, on peut faire un register/subscribe/call/publish sur le bridge en spécifiant une URL de callback. Le bridge transmet tout ça à routeur WAMP. Quand un événement arrive sur le bridge qui concerne une des URLs de callback, il fait une requête sur l’URL avec les infos arrivées via WAMP.

API :

POST /register/

{
    // token d'authentification
    token: "fdjsklfqsdjm",
    // On enregistre des urls de callbacks pour chaque "function" exposées en RPC
    // Quand le bridge reçoit un appel RPC via WAMP, il fera une requête POST
    // sur la bonne URL. Votre app récupère les données via POST, et retourne
    // du JSON que le bridge va transmettre via WAMP.
    endpoints: {
        "nom_fonction_1":  "http://localhost:8080/wamp/rpc/nom_fonction_1/",
        "nom_fonction_2":  "http://localhost:8080/wamp/rpc/nom_fonction_2/"
    }
}

POST /subscribe/

{
    token: "fdjsklfqsdjm",
    // On enregistre des urls de callbacks chaque abonnement à un topic.
    // Quand le bridge reçoit un message PUB via WAMP, il fera une requête POST
    // sur la bonne URL. Votre app récupère les données via POST.
    endpoints: {
        "nom_fonction_1":  "http://localhost:8080/wamp/pub/nom_fonction_1/",
        "nom_fonction_2":  "http://localhost:8080/wamp/pub/nom_fonction_2/"
    }
}

POST /call/nom_fonction/

{
    token: "fdjsklfqsdjm",
    // Le bridge fera l'appel RPC, récupère la valeur de retour, et la renvoie
    // à cette URL via POST ou une erreur 500 en cas d'exception.
    callback: "http://localhost:8080/wamp/callback/nom_fonction",
    // Les params à passer à l'appel RPC
    params: ['param1', 'param2']
}

POST /publish/nom_sujet/

{
    token: "fdjsklfqsdjm",
    // Le bridge fera la publication WAMP
    // Les params à passer lors de la publication
    params: ['param1', 'param2']
}

On peut rajouter des fioritures : recharger le fichier de config qui contient les API keys, se désabonner, désinscrire un callback RPC, etc.

Bien entendu, du fait d’avoir un intermédiaire, c’est peu performant, mais suffisant pour permettre une communication entre des apps existantes et vos apps WAMP et mettre un pied dedans.

Intégration poussée

Ceci permet une intégration générique, de telle sorte que tout le monde puisse intégrer son app facilement avec quelques requêtes HTTP. Néanmoins, avoir un plugin pour son framework plug and play faciliterait la vie de beaucoup de gens.

Donc la partie 2, c’est de faire des apps pour les frameworks les plus courants qui wrappent tout ça.

Par exemple, pour Django, ça permettrait de faire :

settings.py

WAMP_BRIDGE_URL = "http://localhost:8181/"

urls.py

from wamp_bridge.adapters.django import dispatcher
 
urlpatterns += ('',
    url('/wamp/, dispatcher),
    ...
)

views.py

from wamp_bridge.adapters.django import publish, call, rpc, sub
 
@rpc()
def nom_fonction_1(val1, val2):
    # faire un truc
 
@sub()
def nom_topic_1(val1, val2):
    # faire un truc
 
def vue_normale(request):
    publish('sujet', ['arg1'])
    resultat = call('function', ['arg1'])

Ca répond pas à des questions du genre : “comment je fais pour garder mon authentification Django” mais c’est déjà super glucose.

Implémentation

On peut créer un bridge dans plein de langages, mais je pense que Python est le plus adapté.

Les clients JS utiliseraientt de toute façon nodeJS, qui n’a pas besoin de bridge, puisque déjà asynchrone. Un routeur en C demanderait de la compilation. PHP est trop moche. Java, c’est tout un bordel à setuper à chaque fois. C#, si il faut se taper Mono sous Linux…

En prime, le routeur crossbar est déjà en Python, donc a déjà tout sous la main pour le faire. On peut même penser à l’intégrer à crossbar plus tard histoire que ce soit batteries included.

Il faut une implémentation Python 2 et Python 3, donc une avec Twisted, et une avec asyncio.

Pour le client twisted, on peut fusionner le client WAMP ordinaire avec treq.

Pour asyncio, il y a aiohttp qui fait client et serveur.

On met les clés API dans un fichier de conf ou une variable d’env, une auth via token, et yala.

C’est le genre de projet intéressant car pas trop gros, mais suffisamment complexe pour être un challenge surtout qu’il faudra des tests unitaires partout, de la doc, bref un truc propre.

Je laisse les specs là, des fois qu’il y ait quelqu’un qui ait des envies de code cet hiver et cherche un projet open source dans lequel se lancer.

Si on se sent un peut foufou, on peut même transformer ça en bridget HTTP <=> anything, avec des backends pour ce qu’on veut : IRC, XMPP, Redis, Trigger Happy…

]]>
http://sametmax.com/bridge-httpwamp/feed/ 5
Corrections des slides WAMP 8 http://sametmax.com/corrections-des-slides-wamp/ http://sametmax.com/corrections-des-slides-wamp/#comments Thu, 25 Dec 2014 09:58:47 +0000 http://sametmax.com/?p=13011 Suite aux commentaires, j’ai fais une refonte des dispos :

  • Plus d’insistance sur la différence entre RPC et PUB/SUB.
  • Les exemples sont amenés plus tôt, et les schémas sont en premier.
  • Des lourdeurs et des redondances sont supprimées.
  • J’ai ajouté des réponses à quelques questions posées : perf, sécu, etc.

Histoire d’éviter d’éparpiller des versions partout, je l’ai juste réup au même endroit.

Merci, donc, pour toutes les remarques qui ont significativement permises d’améliorer la prez.

]]>
http://sametmax.com/corrections-des-slides-wamp/feed/ 8
Full disclosure 28 http://sametmax.com/full-disclosure/ http://sametmax.com/full-disclosure/#comments Tue, 16 Dec 2014 15:32:26 +0000 http://sametmax.com/?p=12883 Depuis quelques jours je suis en discussion avec Tobias de Tavendo. Comme vous avez pu le remarquer avec mes précédents articles sur WAMP et Crossbar :

  • Ils sont bons techniquement, et nuls pour expliquer ce qu’ils ont techniqué.
  • Cette techno est une techno de rêve pour moi. J’y crois à mort.
  • Je suis le seul à avoir pondu des explications décentes sur WAMP et Crossbar. Et ça n’a pas suffit à faire battre un cil.

Bref, ils ont embauché des mecs de haute voltige pour la technique (du genre un contributeur PyPy). Et ils m’ont contacté pour me demander si je n’étais pas chaud pour faire de l’évangélisme, rémunéré, autour de WAMP, Autobahn et Crossbar.

L’idée : écrire des tutos, des articles, améliorer la doc, répondre sur le chan IRC, etc.

J’adore le concept, vu que j’aime leur projet et que je le faisais gratos avant, surtout qu’ils sont pas trop contraignants sur le temps que je vais passer dessus.

Donc voilà le deal : quand je vais pondre des tutos et des articles sur WAMP et Co, je vais d’abord les faire en français ici. Comme ça j’aurai les retours des lecteurs du blog qui pourront, comme d’habitude, me faire part de leurs douces remarques sur à quel point on ne pige rien.

Une fois la prose aiguisée, je traduis et je publie chez Tavendo.

Je disclose donc ici que vous verrez peut-être des prochaines rédactions qui seront attachées à une activité pro. Pas impartial donc. Mais bon, depuis quand je suis impartial ? Javascript c’est de la merde, et je préfère les rousses.

Par saucisse d’honnêteté, je signalerai chaque choucroute concernée avec un lien vers ce post.

Enfin, le contrat est pas signé encore, mais vu que je vais commencer à taffer dessus aujourd’hui, je pense à une première publication demain sous la forme d’un slide show expliquant avec de jolies diapos ce que sont WAMP, Autobahn et Crossbar. À quoi ça sert et ce qu’on peut faire avec.

]]>
http://sametmax.com/full-disclosure/feed/ 28
WAMP et les outils de dev Web Python existants 8 http://sametmax.com/wamp-et-les-outils-de-dev-web-python-existants/ http://sametmax.com/wamp-et-les-outils-de-dev-web-python-existants/#comments Wed, 02 Jul 2014 05:21:09 +0000 http://sametmax.com/?p=11259 Même si on peut créer un site Web en utilisant uniquement des libs WAMP, tout comme on peut le faire en utilisant uniquement flask ou tornado, il arrive immanquablement le moment où on veut mélanger, intégrer, faire cohabiter les techos ensemble. D’abord parce qu’il y a des sites existants, et qu’on va pas les jeter à la poubelle. Ensuite parce que ce sont des techos qu’on connaît et pour lesquelles ils y a beaucoup d’outils, que l’on veut mettre à profit.

C’est tout naturellement qu’on a fini par me poser (par mail), ze question :

Subject: crossabar et autobahn

Message Body:
Ha bah oui vous l’avez cherché à nous parler de truc comme ça: ça interroge !

Je m’interroge donc sur la manière d’intégrer WAMP à django. Pour la perstistence des données (l’ORM qui simplifie la création des tables tout de même), pour l’authentication et l’authorization, pour la robustesse et versatilité apportée par django…

J’ai pour habitude de mettre pas mal de logique dans mes modèles et je me demandais si il n’y aurait pas moyen de pluguer WAMP dans ceux ci… exposer une méthode update en RPC avec passage de JSON ? Avec namespace automatique à partir de la classe ?

En fait remplacer: Angular+tastypie+django+nginx par Angular+WAMP+django+crossbar avec du coup le bonus de WAMP pour le pubsub que n’a pas AJAX.

Comment vous verriez un (petit – après relecture ça parait dur) mixin WAMP pour des modèles django auto-détectés, auto-exposés en RPC ?

J’ai du mal à voir quelle tactique utiliser. J’ai peur que ça finisse en refaire tout le travail que fait déjà (très bien) tastypie/RESTframework.

Comment modulariser (là ou est sensé briller WAMP) les services déjà offerts par django: celery, authentication, etc ?
Ces services sont tous très dépendant du système de persistance des données (check des users, permissions, query) et donc l’approche plus monolithique de django n’est pas mauvaise car tout est lié et donc facilement manipulable au même endroit… où est le gain de WAMP dans ce cas, pour une application assez classique en fait ?

Merci encore pour cette découverte dans tous les cas. C’est top.

Du coup je me relis et ça part un peu dans tous les sens… désolé.

Comme je me suis fendu d’une réponse bien longue, je vais la paster verbatim :

Bonjour,

C’est une très bonne question.

D’abord, il faut savoir qu’on ne peut pas faire tourner un routeur WAMP dans un process Django (ou tout autre app WSGI) car Django est synchrone. En plus, l’ORM de django est bloquant, donc même sans utiliser django, utiliser son ORM au sein de WAMP va bloquer la boucle d’événements et on perdra tout l’interêt d’avoir une techno temps réel.

(Note a posteriori : y a surement un truc à faire avec gevent ou des threads ici, mais je sais pas encore quoi)

Ici on a donc 3 problèmes à résoudre :

– comment faire communiquer django et son app WAMP ?
– comment utiliser un ORM bloquant avec WAMP ?
– comment auto générer une API WAMP ?

Ces 3 questions n’ont pas encore de réponse définitive puisque, comme je l’ai précisé, WAMP est une techno jeune, et donc il y a beaucoup à faire. Mes articles sont précisément là pour tenter de générer un enthousiasme et pousser les gens à améliorer les outils autour de WAMP.

Prenons les problèmes un par un :

Comment faire communiquer django et son app WAMP ?
====================================

C’est le problème le plus facile à mon sens. Il faut coder une app WAMP qui fasse le bridge entre HTTP et WAMP. Quand on register côté app HTTP, on fait un post sur l’app WAMP (qui écoute aussi sur HTTP du coup) en fournissant une URL de callback. L’app WAMP fait le register, et quand on l’appelle, elle fait l’appel à l’app HTTP via l’url de callback, et retourne le résultat. On peut faire ça pour register, subscribe, call et publish, c’est le même principe.

(Note a posteriori : en me relisant moi-même je m’aperçois à quel point c’est pas clair. De toute façon il faudra que je le code un jour où l’autre, et avec un bon tuto pratique, la pilule passera mieux).

Ce faisant, on pourra appeler du WAMP côté app HTTP, et taper dans l’app HTTP côté client WAMP.

Une amorce de travail a été fait pour coder un tel bridge. Pour le moment il n’y a que le publish :

https://groups.google.com/forum/#!searchin/autobahnws/http/autobahnws/SbobAnoWVlQ/FnGhdYXj9aIJ

Ce n’est pas très dur à coder, c’est juste un boulot chiant à faire.

Cela dit, ça ne résout pas le problème de l’authentification, qu’il faudra à un moment on un autre, se poser. Je pense qu’on va se diriger vers une authentification hybride, qui va utiliser le session ID en cookie, mais l’envoyer via un token. Encore un truc à travailler.

De même, on voudra sûrement créer quelques facilités pour intégrer ça dans les frameworks les plus connus en proposant une app prêt à plugger. Rien d’insurmontable donc, mais pas mal de taff.

Par contre, pour ce qui est des tasks queues, à mon avis une solution de task queue WAMP sera bien plus intéressante qu’une solution type celery car on peut envoyer des messages WAMP depuis les tâches et donc avertir en temps réel de l’avancement du process. Je voterais donc pour coder soi-même une alternative.

Comment utiliser un ORM bloquant avec WAMP ?
===============================

Idéalement, il faudrait avoir des ORM non bloquant, mais on Python, on en a pas. On a quelques drivers non bloquant, notamment pour PostGres et Mongo, mais pas d’ORM, et ils demandent une forme de compilation d’extension C.

C’est là qu’on voit qu’on se traine la culture de l’API synchrone en Python, car côté NodeJS, ils commencent à avoir pas mal de solutions.

En l’occurrence, on a 3 solutions :

– utiliser le bridge dont je viens de parler pour garder les appels dans l’app HTTP. Ca veut dire que quand on veut faire un appel à de la base de données, ça fait WAMP => HTTP => connexion à la base, aller-retour. C’est pas idéal.
– créer une app WAMP pour héberger les appels bloquants et taper dedans en RPC. Une bonne solution à mon avis. Mais assez peu intuitive.
– faire tous les appels dans un threads à part. Le plus simple. Un peu verbeux par contre.

Dans les deux derniers cas, on à quand même le problème des querysets qui sont lazy, notamment au niveau des foreign keys. Il faudra faire particulièrement attention à ne pas accidentellement faire des appels bloquant, par exemple dans le rendu du template. Une solution viable est de créer un wrapper qui fait le rendu du template dans un threads.

Bref, encore pas mal d’outils à developper.

On peut aussi se lancer dans l’écriture d’un ORM non bloquant. Une bonne année de travail avant d’avoir quelque chose qui soit compétitif.

Comment auto générer une API WAMP ?
==========================

Là tu m’en poses une bonne.

C’est la suite logique, évidement, mais je n’avais jamais réfléchi aussi loin. C’est un taff énorme, surtout que ça dépend de l’outil derrière. La solution la plus simple c’est encore de faire un mapper dans le bridge HTTP-WAMP qui va traduire directement un appel WAMP en un appel JSON vers l’API générée par django-rest-framework ou autre.

Mais bon, je suis pas certains de la valeur ajoutée.

Je pense qu’il est difficile pour moi de répondre à cette question pour le moment car :

– je ne suis pas certain que WAMP soit un bon remplacement pour les API REST. Je pense plutôt que c’est un complément.
– il y a toute la question de l’authentification. Encore et toujours.
– il va falloir pas mal d’essais avec plusieurs architectures en prod (séparées, mixtes, mono culture…) pour pouvoir déterminer ce qui rend le mieux.

Mon intuition est qu’on utilise généralement 10% de l’API générée par les frameworks, et que la partie dont on a besoin à peut très bien se faire à la main. La raison pour laquelle les trucs comme django-rest-framework sont si pratiques, c’est qu’ils gèrent des problématiques comme l’authentification, la sérialisation et la pagination.

Je serais plutôt d’avis de s’attaquer à ça pour WAMP, et je pense qu’on s’apercevra que finalement, pour ses propres besoins, un API complète est overkill. Par contre, pour exposer une API au monde, c’est une autre histoire. J’ai eu récemment une discussion à propos de faire des APIs WAMP :) Il y a des possibilités fascinantes. Mais c’est peut être encore un peu loin tout ça.

Je pense que je vais publier cette réponse sur le blog, car tu soulèves des points très importants.

]]>
http://sametmax.com/wamp-et-les-outils-de-dev-web-python-existants/feed/ 8