Sam & Max » flask 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 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
Le potentiel de WAMP, autobahn et crossbar.io 27 http://sametmax.com/le-potentiel-de-wamp-autobahn-et-crossbar-io/ http://sametmax.com/le-potentiel-de-wamp-autobahn-et-crossbar-io/#comments Sun, 01 Jun 2014 10:09:32 +0000 http://sametmax.com/?p=10380 crossbar et autobahn. Mais ça me tue de ne pas voir plus de monde exploiter cette techno.]]> Je sais, je sais, je vous fais chier avec crossbar et autobahn.

Mais ça me tue de ne pas voir plus de monde exploiter cette techno.

Pendant que Max fait la sieste, j’ai pris mon stylo et j’ai fait la liste des besoins d’une app Web actuelle. Quels sont les composants qu’on utilise presque systématiquement, mais en agrégeant divers bouts de trucs à droite et à gauche ?

Ensuite j’ai regardé les possibilités des outils WAMP :

  • PUB/SUB et RPC.
  • Asynchrone.
  • Gestionnaire de process intégré.
  • Serveur stand alone qui n’a pas besoin d’un proxy pour être en prod.

M’inspirant de cela, et du travail que je suis en train de faire avec l’équipe de Tavendo pour faire une API flaskesque pour autobahn, j’ai prototypé une API d’un framework Web qu’on pourrait coder au dessus de cette techno.

Voilà ce que ça donne…

Une API qui mélange flask et nodejs pour le Web

app = Application('YourProjectName')
 
# Envoyer et recevoir des requêtes HTTP
@app.http.post(r'/form')
def _(req, res):
    res.json({'data': 'pouet'})
 
@app.http.get(r'/user/:id/')
def _(req, res):
    res.render('index.html', {'data': 'pouet'})
 
# Servir des fichiers statiques
@app.http.serve('uri', '/path/to/dir', [allow_index])
 
app.run()

Comme c’est asynchrone, on a de très bonnes perfs. Comme c’est basé sur Twisted, on a pas besoin d’un serveur wsgi (gunicorn, uwsgi, etc) ni d’un proxy (nginx) devant. On peut le mettre en prod tel quel.

Parti de ce principe, on peut ajouter la gestion du PUB/SUB et du RPC pour WAMP :

# Callback attendant l'événement
@app.wamp.event('auth.signedin')
def _(ctx, a, b, c):
    pass
 
# déclenchement de l'événément
app.wamp.pub('auth.signedin')
 
# Déclaration du fonnction appelable à distance
@app.wamp.remote('auth.signin')
def _(ctx, a, b, c):
    pass
 
# appel de la fonnction
app.wamp.call('auth.signin')

On est souvent perdu quand on fait de l’asynchrone pour la première fois avec Python car on ne sait pas comment lancer du code après .run(). On peut régler la question proposant des hooks pour les instants clés de l’app.

# Callback à lancer quand l'app est prête
@app.on('app.ready')
def _(ctx, args):
    pass
 
# Signalement que l'app est prête (fait automatiquement en interne
# pour les moments les plus importants)
app.emit('app.ready')

Et tant qu’on y est, puisqu’on a une event loop, profitons en pour proposer du CRON intégré à l’app. C’est moins chiant à déployer qu’un script CRON, c’est cross plateforme, et on a accès facilement à toute sa stack.

# Lancer du code tous les x temps ou a une date précise
@app.cron(every=seconds)
@app.cron(every=timedelta, overlap=False)
@app.cron(hour=7, minute=30, day_of_week=1)
@app.cron(when=datetime)
def _(ctx, args):
    pass

Pourquoi s’arrêter là ? Event loop + message passing + safe queues + workers = tasks queues !

# Créer une file d'attente
queue = @app.queue('name', [workers], [result_backend])
 
# Callback appelé par un worker quand il depop ce 
# message dans la file
@queue.task('encode.video')
def _(ctx, data):
    pass
 
# Envoie d'une tache dans la queu
queue.append('encode.video', data)

Comme on utilise Twisted, on a accès à une chiée de protocoles, et on peut aussi créer les siens. On peut donc imaginer un système de plugins qui rajoute des protocoles supportés :

app = Application('YourProjectName')
app.plug('lib.ajoutant.sms', [namespace])

Si on en a beaucoup et que le namespace nous convient :

app = Application('YourProjectName', plugins=('lib1', 'lib2', 'etc'))

Exemples de plugins possibles :

# Recevoir et envoyer des SMS (via un service type twilio, une gateway kannel ou
# un modem physique)
@app.sms.receive(r'LOVE \w+ \w+')
def _(ctx, args):
    pass
app.sms.send('test', [contact])
 
 
# Envoyer et recevoir des emails (via un server SMTP ou IMAP)
@app.email.receive(src=r'.*@sametmax.com', dest=r'spam.*@*.')
def _(ctx, args):
    pass
app.email.send('test', [contact, title, attachments])
 
 
# techniquement n'importe quel service de message pour lequel on peut écrire
# un backend
@app.tweet.receive(r'Chat')
@app.fb.receive(r'Like')
@app.instagram.receive(r'Bouffe')
@app.irc.message(r'dtc')
def _(ctx, args):
    pass

Le problème des apps centrées sur un objet, c’est qu’elles ont souvent un design monolithique. Ce n’est pas un problème du concept d’app, c’est juste que les auteurs ont pensé “point d’entrée”, et pas “élément composable”.

Si besoin, on doit pouvoir composer une app via plusieurs sous-app :

app = Application()
app.embed('autre.app')

ou

app = Application(embed=['app1', 'app2', 'app3'])

Il faut des hooks pour overrider la configuration, mais vous avez compris le principe.

Un autre problème avec les plateformes comme NodeJS, c’est qu’il est difficile d’utiliser plusieurs coeurs. C’est une des raisons du succès de Go.

Or, Crossbar encourage la division en plusieurs process qui communiquent entre eux (un peu comme les channels). Créons aussi une API pour ça :

p1 = app.process()
p2 = app.process()
 
# Déclarer et appeler une procédure dans process 1
@p1.wamp.remote('auth.signin')
def _(ctx, args):
    pass
 
# Déclarer et appeler une procédure dans process 2
@p2.wamp.event('auth.signedin')
def _(ctx, args):
    pass

Ainsi on profite enfin de plusieurs CPU. La même chose en plus facile à changer:

# Déclarer et appeler une procédure
@app.wamp.remote('auth.signin')
def _(ctx, args):
    pass
 
# Déclarer et appeler une procédure
@app.wamp.event('auth.signedin')
def _(ctx, args):
    pass
 
app.processes({
    1: ['wamp.remote:auth.signin']
    2: ['wamp.event:auth.signedin']
})

En bonus, on fait la nique au GIL.

Mieux, on peut bouger ses process sur plusieurs machines :

Machine 1 (routeur):

router = Application(endpoint="0.0.0.0:8080")
router.run()

Machine 2 (authentification):

# IP du router
auth = Application('auth', connect_to="182.64.1.15:8080")
 
# Nommage automatique en fonction du nom de la fonction
# et de l'app, avec possibilité d'annuler ou overrider le prefix.
# Ici du coup la fonction s'appellera en RPC via 'auth.signin'
@auth.wamp.remote()
def signin(ctx, args):
    pass
 
auth.run()

Machine 3 (API REST):

web = Application('site', connect_to="182.64.1.15:8080")
 
@web.http.post(r'api/auth/')
def _(req, res):
    user = yield res.wamp.call('auth.signin',
                               req.POST['username'],
                               req.POST['password'])*
    if user
        user = yield res.wamp.pub('auth.signedin', user.userid)
        res.json({'token': user.token})
    else:
        res.json({'error': 'nope'})
 
 
@web.http.get(r'api/stuff/')
def _(req, res):
    res.json(get_stuff())
 
@web.http.serve('uri', '/path/to/dir', [allow_index])
 
web.run()

Et vous savez le plus beau dans tout ça ? En Python on a plein de libs qui sont encore bloquantes. En théorie on ne peut pas les utiliser dans les apps asynchrones. Quand on a toute sa logique métiers dans des classes d’ORM, c’est balot. Mais pas ici ! On met un process avec tous ces appels bloquants, et on les appelle depuis des process non bloquant en RPC de manière asynchrone. Pif, paf, pouf, problème isolé.

Après, libre à son imagination de rajouter des fonctionnalités de confort…

Callback qui sera appelé seulement x fois :

# Déclarer et appeler une procédure
@p1.wamp.event('auth.signedin', options={'limit_calls': x} )
def _(ctx, args):
    pass

Raccourcis pour les opérations courantes :

# Recevoir et envoyer un événement
@app.sub('auth.signin')
def _(ctx, *args):
    # ctx.pub
@app.pub('auth.signedin')
 
# Déclarer et appeler une procédure
@app.proc('auth.signedin')
def _(ctx, args):
    # ctx.call
app.rpc()

Comme je vous l’avais expliqué, crossbar peut gérer le cycle de vie de services externes à votre application au démarrage. Autant exposer cette API programativement :

@app.service(['/urs/bin/nodejs', 'script.js'], [user], [group])

.run(), c’est cool, mais si on veut changer des options via la ligne de commande, faut se taper tout le boulot alors que ça pourrait très bien se générer automatiquement :

@app.cmd_run()

Et si vous faites : python sites.py --debug=true --endpoint=0.0.0.0:5252, ça le prend automatiquement en compte. Y a pas de raison de se faire chier.

En parlant de générer automatiquement des trucs, le fichiers de configs pour les services externes sur lesquels on peut avoir envie de brancher notre app, c’est toujours galère. Autant fournir un exemple de base qui est sûr de toujours marcher, généré avec les paramètres de notre app :

python site.py template centos:nginx
python site.py template ubuntu:upstart
python site.py template bsd:systemd # :D

On peut partir très loin dans le délire “battery included”. Typiquement, on peut fournir des services externes nous même puisque crossbar nous le propose, et coder des versions moins bien, mais compatibles (et suffisantes pour les petits sites), de projets toujours utilses :

  • cache (compatible redis)
  • live settings (compatible etcd) mais avec en prime un event wamp propagé à chaque
  • changement de valeur

  • build (compatible, heu, j’en sais rien) qui s’occupe en tâche de fond de surveiller le >système de fichier et lancer les compilations, les minifications, les copies, les tests unittaires, etc.
  • logging centralisé (compatible sentry).
  • Un bridge WAMP/REST qui permet d’envoyer et recevoir des events WAMP sur votre app Django ou flask en utilisant HTTP.

On plug tout ça a une admin Web.

J’espère que je vous ai donné maintenant l’envie de vous plonger un peu plus dans cette techno, et peut être coder quelque chose avec.

Il n’y a plus d’excuses pour ne pas avoir de framework web next gen, ultime de la mort qui tue en Python. A part le fait qu’on soit des feignasses.

Ah, merde, on est foutus.

]]>
http://sametmax.com/le-potentiel-de-wamp-autobahn-et-crossbar-io/feed/ 27