Les managers le détestent : faites tourner WAMP dans Django avec cette astuce insolite


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

16 thoughts on “Les managers le détestent : faites tourner WAMP dans Django avec cette astuce insolite

  • Yamakaky

    Comme quoi, on peut tout faire avec WAMP ^^

    J’ai un gros problème de lag sur vos pages à cause des codes sources. Peut-être le plugin de coloration syntaxique ? Pb présent sur firefox et chromium.

  • Greg

    +1 pour le lag de la coloration syntaxique. Le scrolling lag énormément.

    Sinon c’est quoi ce titre click-bait è_é ?

  • Sam Post author

    Ouai même problème mais va débugger un plugin wordpress…

  • foxmask

    Alors là ca va etre cool pour bibi ;)

    Ca devrait pouvoir permettre de lancer des requêtes de traitements de triggers unitairement et en récupérer le résultat dans le browser directement. Bon c’est pas le plus urgent mais ca permettrait ça !

    Merci pour ces 6 heures passées là dessus.

    Comme d’hab’ une mine d’or :)

  • Morkav

    Pour la coloration syntaxique, y a u’ truc App prism.JS qui est bien cool, pas trop lourd et w3c compliant. Sais pas comment intégrer dans un wordpress par contre..

  • Pezioz

    Bonjour,

    C’est la première fois que je commente même si ca fait maintenant un an que je vous suis. Je me suis mis au python en partie grâce à vous. Et c’est tous les jours un plaisir de venir vous lire pour voir les nouvelles choses que vous nous avez trouvé…la veille techno est un plaisir avec des sites comme le votre :)

    Sinon en ce qui concerne Wamp j’ai commencé à jouer avec et c’est vrai que c’est une tuerie.

    Petit commentaire inutile mais qui me permet de vous dire merci pour tout ce que vous faites!

  • Sam Post author

    Je suppose qu’il manque un point d’interrogation ?

    Pas encore, car c’est basé sur twisted, et donc en cours de portage sur python 3.

  • Nasjo

    Youhou ! La suite la suite ! Ya eu quoi de neuf depuis ? :)

    Y a t il eu des changements avec asyncio et tout et tout ?

    Merci pour tout ces éléments !

  • touilleMan

    Hé bien un an après, y’a moi qui débarque avec une petite lib pour packager cette super astuce (à voir ici

    https://github.com/Scille/autobahn-sync) ^^

    C’est encore jeune (faut que je fasse plus de doc notamment) donc si vous avez des remarques/critiques/insultes je suis preneur !

  • Sam Post author

    Ca mérite un tweet. Et de ton côté, n’hésite pas à publier ça sur reddit.

  • .mobo

    Super astuce, c’est exactement ce que je recherchais pour mon problème actuel :). Et gros +1 pour autobahn-sync.

  • Foxmask

    ya une typo dans le app.py sur le protocol qui semble etre soit ws soit wss

    apres le code je me mange un ‘NoneType’ object has no attribute ‘publish’ sur le publish(‘ping’) parce qu’a priori dans la methode publish ; wapp.session vaudrait None

    Et en creusant, Il semblerait que le code correct (à moins que ca ait bougé depuis ces années) soit :

    from autobahn.twisted.wamp import ApplicationSession
     
    # un objet qui contient une session WAMP
    wapp = ApplicationSession()

    apres j’ai une erreur

    TransportLost at /

    mais j’ai aucune idée du pourquoi du comment

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.