Introduction à Flask, le micro système maousse costaud

GNU/Linux Magazine n° 166 | décembre 2013 | Emile (iMil) Heitor
Creative Commons
  • Actuellement 0 sur 5 étoiles
0
Merci d'avoir participé !
Vous avez déjà noté cette page, vous ne pouvez la noter qu'une fois !
Votre note a été changée, merci de votre participation !
Je sais pas pour vous, mais moi, le clicka-web, ça m'ennuie. J'éprouve un profond respect pour ces génies de l'ergnonomie qui arrivent à produire des interfaces comme GitHub ou Twitter, truffées d'Ajax/jQuery[1], super inuitives, rapides et branchées sur des systèmes d'information ultra-complexes. Récemment, j'ai eu à produire une interface de gestion de déploiement de services au sein d'un parc de machines virtuelles. Cette interface devait se brancher sur un Web-service et s'interconnecter aux mécanismes d'orchestration Fabric[2] et Salt[3], tous deux en Python. Afin de garder une certaine cohérence dans le choix des langages, j'ai finalement jeté mon dévolu sur Flask[4], le “micro framework” qui m'a fait aimer développer du clicka-web.

1. C'est mou ?

Flask est décrit comme un “microframework” en Python, il est basé sur Werkzeug pour la partie serveur / WSGI[6] et sur Jinja 2[7] pour le templating. Pour ne rien gâcher, Flask, ainsi que ses dépendances précédemment citées, sont sous licence BSD.
 Puisque Flask s'appuie sur Werkzeug, il est en mesure de fonctionner de façon autonome, sans l'aide d'un serveur WSGI tiers. C'est cette méthode que nous allons utiliser au début de cet article afin de rapidement mettre le pied à l'étrier, nous verrons plus tard comment configurer le serveur uWSGI[8] de façon à disposer d'une installation prête pour la production.

Flask est disponible dans la plupart des systèmes de paquets de vos UNIX-like favoris, mais nous choisirons ici d'utiliser la version disponible via pip, ainsi nous manipulerons la dernière version en date, la 0.10 :

$ pip install Flask

Munis du framework, nous allons pouvoir écrire notre première application Flask :

$ cat glmf.py
from flask import Flask
app = Flask(__name__)
@app.route(‘/’)
def glmf_rules():
return ‘GLMF rules!\n’
if __name__ == ‘__main__’:
app.run()

Dans ce minuscule bout de code, après avoir importé puis instancié la classe Flask, nous utilisons le décorateur @app.route de façon à définir l'URL qui sera gérée par la fonction à suivre, ici, nous prenons en charge la racine du site. Comme on peut s'en douter, l'application basique que nous avons écrite répondra «GLMF rules!» lorsqu'elle sera interrogée sur l'URL déclarée. Finalement, lorsque le présent script est appelé par une application tierce, nous invoquons la fonction run() qui démarrera le serveur Web qu'embarque Flask.

On commence par démarrer le service :

$ python glmf.py
* Running on http://127.0.0.1:5000/
127.0.0.1 - - [20/Jul/2013 10:51:47] "GET /
HTTP/1.1" 200 -

Et on teste simplement l'URL définie :

$ curl -o- -s http://localhost:5000/
GLMF rules!

Nous constatons que, sans plus de paramètres passés à la fonction run(), Flask écoute sur l'interface loopback, pour modifier ce comportement, il suffit de passer un paramètre host :

if __name__ == ‘__main__’:
app.run(host=’0.0.0.0’)

Ici, l'application écoutera sur l'ensemble des interfaces réseau du système. Afin de déboguer efficacement notre application, il est également possible de passer un paramètre debug à la fonction run() :

if __name__ == ‘__main__’:
app.run(host=’0.0.0.0’, debug=True)

Ce qui aura pour effet de proposer une fenêtre de débogage interactive utilisable depuis un navigateur. Il est possible, à travers la fonction route(), de passer des arguments possiblement typés à une fonction associée à une URL :

@app.route(‘/<mag>’)
def glmf_rules(mag):
return ‘{0} rules!\n’.format(mag)

Et de tester :

$ curl -o- -s http://localhost:5000/GLMF%20
Le%20Meilleur%20Mag%20De%20La%20Galaxie
GLMF Le Meilleur Mag De La Galaxie rules!

2. Des serpents dans mon Web 2.0

Muni de ces basiques concepts, nous allons maintenant découvrir les fonctionnalités qui font la force de Flask, en particulier comment produire une véritable page Web dynamique en un temps record.

La fonction route() utilisée dans les décorateurs de fonctions accepte le paramètre methods, c'est à travers ce dernier que l'on pourra choisir, au sein de la fonction, l'action à mener lorsqu'on atterrit sur l'URL déclarée. On passe au paramètre methods un tableau contenant la liste des méthodes supportées par la fonction :

@app.route(‘/’, methods=[‘GET’, ‘POST’])

On choisit ensuite l'action à mener grace à la variable request.method. L'objet global request devra être importé, comme l'objet Flask :

from flask import Flask, request
@app.route(‘/)
def entree():
if request.method == ‘GET’:
return fais_des_trucs()
else:
return fais_d_autres_trucs()

L'objet request, comme on peut s'en douter, regroupe les informations relatives à la requête effectuée, par exemple on accède aux paramètres passés via la méthode GET à l'aide de request.args.get :

def test():
return ‘{0}\n’.format(request.args.get(‘foo’))
...
$ curl -o- -s http://localhost:5000/?foo=bar
bar

Ou encore aux valeurs d'un formulaire en appelant request.form['champs'], nous reviendrons sur cette fonctionnalité plus tard.

3. Structure d'un projet Flask

Un projet Flask typique consiste en l'arborescence suivante :

|-- gmlf.py
|-- static
| |-- style.css
`-- templates
|-- layout.html
`-- site.html

À la racine du projet on trouve le ou les fichiers Python moteur de l'application, un repertoire static qui, comme son nom l'indique, regroupe les fichiers statiques utilisés, et un répertoire template contenant les templates au format Jinja 2 qui seront utilisés pour générer des pages HTML de façon simple et intuitive.

C'est cette capacité que nous allons explorer immédiatement, afin de rapidement s'apercevoir du potentiel de ce microframework.

3.1 Templates

Il serait évidemment extrêmement fastidieux d'écrire l'intégralité du code HTML au sein du moteur lui même, aussi, la pierre angulaire du rendu de pages passe par l'écriture de simples templates qui savent récupérer des variables, voire des fonctions, issues du moteur de l'application. Créons pour l'occasion un template simpliste qui affichera dans une page HTML correctement formatée l'argument passé au paramètre foo via la méthode GET :

$ cat templates/main.html
<!doctype html>
<title>En voila une belle page</title>
Valeur de foo: {{ foo }}
$ cat glmf.py
from flask import Flask, request, render_
template
app = Flask(__name__)
@app.route(‘/’)
def test():
return render_template(‘main.html’,
foo=request.args.get(‘foo’))
if __name__ == ‘__main__’:
app.run(debug=True)

On remarquera l'import d'un nouvel objet render_template, qui très naturellement est l'objet utilisé pour réaliser le rendu du template Jinja. L'appel à la fonction associée est limpide, on passe en premier paramètre le nom du template, puis ensuite la liste des paramètres que l'on souhaite manipuler au sein du template, ici uniquement la variable foo à laquelle on affecte le contenu du paramètre passé en GET. On vérifie simplement le bon fonctionnement du mécanisme :

$ curl -o- -s http://localhost:5000/?foo=bar
<!doctype html>
<title>En voila une belle page</title>
Valeur de foo: bar

Ceux d'entre vous ayant fait l'acquisition du GNU/Linux Magazine hors série sur Python le savent probablement déjà, Jinja 2 propose une panoplie d'outils bien plus puissants que le simple affichage de variables passées au template, en l'occurrence, nous bénéficions ici d'un mini-langage de programmation muni entre autres de boucles, conditions et opérations sur chaînes basiques. Modifions notre exemple en ce sens :

$ cat templates/main.html
<!doctype html>
<title>En voila une belle page</title>
{% if foo %}
Valeur de foo: {{ foo }}
{% else %}
J’ai rien dans foo, par contre, j’ai :
{% for arg in request.args %}
- {{ arg }} qui contient {{ request.args.get(arg) }}
{% endfor %}
{% endif%}
...
$ curl -o- -s "http://localhost:5000/?fox=bar&foy=baz"
<!doctype html>
<title>En voila une belle page</title>
J’ai rien dans foo, par contre, j’ai :
- fox qui contient bar
- foy qui contient baz

Ah oui là tout de suite ça cause un peu plus. Comme on peut le voir, nous avons un accès total à l'objet request depuis le template Jinja en plus de la variable explicitement passée. Dans ce template nous testons l'existence d'une variable et bouclons sur les différents paramètres “GET” disponibles à travers l'objet request, une belle liste de possibilités en perspective…

3.2 Fichiers statiques

Nous l'avons vu, un projet Flask peut contenir dans son arborescence un repertoire static, ce dernier contiendra par exemple des fichiers JavaScript, CSS ou encore des fichiers média. Il sera de bon ton de ne pas simplement déclarer un fichier statique via son chemin relatif ou absolu dans le template mais d'utiliser plutôt la fonction url_for(), qu'il conviendra d'importer également dans l'application :

$ head -1 glmf.py
from flask import Flask, request, render_template, url_for
...
$ head -3 templates/main.html
<!doctype html>
<title>En voila une belle page</title>
<link rel=stylesheet type=text/css href="{{ url_for(‘static’,
filename=’style.css’) }}">

Le mot clé static, passé à la fonction url_for est défini par Flask, cependant, url_for() est utilisable à souhaits tant dans le code de l'application que dans les templates, cette fonction très pratique renvoie simplement l'URL à laquelle correspond une fonction, soit donc la route associée.

4. Et maintenant, on mélange

Pour se familiariser avec les notions que nous venons de découvrir, nous allons réaliser un minuscule formulaire qui pourrait servir de page d'authentification simple d'un système d'information. Le moteur Jinja 2 permet l'utilisation de blocks, ce qui permet d'écrire des templates minimaux qui viendront s'imbriquer les uns aux autres. Voici par exemple une structure générique qui accueillera l'ensemble des blocks spécifiques :

$ cat templates/layout.html
<!doctype html>
<title>Bienvenue, visiteur du futur !</title>
<link rel=stylesheet type=text/css href="{{ url_for(‘static’,
filename=’style.css’) }}">
<div class="page">
{% block body %}{% endblock %}
</div>

Ici, les directives {% block body %}{% endblock %} indiquent où pourra se raccrocher un block fils qui portera le nom body. Dans notre cas, login.html aura la forme suivante :

$ cat templates/login.html
{% extends "layout.html" %}
{% block body %}
<h2>Identification</h2>
<hr>
<form method="POST" action="/">
<label>Utilisateur</label><br />
<input type="text" name="user" id="user"><br />
<label>Mot de passe</label><br />
<input type="password" name="passwd" id="passwd"><br />
<input type="submit" name="action" value="login">
</form>
{% endblock %}

Le formalisme de ce template est assez explicite, on étend le template layout.html avec un block nommé body, que Jinja identifiera comme étant à insérer dans le block du même nom. Maintenant que nous disposons de nos templates de base, passons au code Python de l'application :

from flask import Flask, request, render_template, url_for, \
session, redirect, abort
app = Flask(__name__)
app.secret_key = ‘la cle en toc’
lps = { ‘imil’: ‘passpourri’, ‘jeanclaude’: ‘tergal’, ‘pascal’:
‘brutal’ }
@app.route(‘/welcome’)
def welcome():
if ‘user’ in session:
return "Identification reussie, {0} !\n".format(session[‘user’])
else:
return "Dehors, manant\n"
@app.route(‘/’, methods=[‘GET’, ‘POST’])
def login():
if request.method == ‘POST’:
for u in lps.keys():
if u == request.form[‘user’] and lps[u] == request.form[‘passwd’]:
session[‘user’] = request.form[‘user’]
return redirect(url_for(‘welcome’))
return abort(401)
else:
return render_template(‘login.html’)
if __name__ == ‘__main__’:
app.run(debug=True)

Quelques explications s'imposent. Nous importons en premier lieu trois nouveaux objets :

- session contiendra des éléments de session propres à l'utilisateur, nous nous contenterons d'y stocker le nom de l'utilisateur ayant réussi son identification. Comme il est évidemment nécessaire de sécuriser cette session et les informations qu'elle transporte, il est impératif de déclarer une clé secrète au sein de l'application.

- redirect permet de rediriger la navigation à travers un code HTTP 302.

- abort stoppe toute activité de navigation en envoyant un code de retour au navigateur.

Comme cela est nécessaire dans un contexte de session, nous plaçons une clé secrète dans la variable app.secret_key. Suite à quoi, pour les besoins de ce test basique, nous codons en dur quelques utilisateurs dans un dictionnaire Python. Ces prérequis honorés, nous déclarons une fonction qui n'aura pour seul but que de signaler à l'utilisateur qu'il a réussi son identification en vérifiant si le nom login est bien présent dans la session en cours.

Le vrai travail s'effectue dans la fonction d'entrée, login, déclarée comme la racine du “site”. S'il s'agit d'une requête de type POST, et donc qu'il s'agit très certainement du résultat de notre formulaire dûment rempli, nous parcourons l'ensemble des couples utilisateur / mots de passe du dictionnaire lps. Si l'un des couples correspond aux valeurs passées dans le formulaire et récupérées via request.form, nous déclarons l'utilisateur dans la session courante, puis le redirigeons sur la route qui tient la fonction welcome qui lui affichera un message de bienvenue. Si l'utilisateur n'est pas reconnu, nous utiliserons la fonction abort qui renverra le code HTTP de retour passé en paramètre, ici 401, code classiquement utilisé pour signifier un échec d'authentification.

Évidemment, il est non seulement possible mais largement conseillé d'utiliser des méthodes d'authentification autrement plus sophistiquées, et il existe pour cela un module Flask tout trouvé répondant au nom de Flask-login[9]. J'ai moi-même utilisé ce module pour les besoins de notre système de déploiement et couplé à ce dernier le module simpleldap[10], un exemple d'utilisation prêt à l'emploi de ce combo est visible sur mon blog[11].

5. Allez, on met en prod

Si l'utilisation du moteur HTTP Werkzeug embarqué dans Flask rend très pratique la phase de débogage, il est évidemment hors de question de reposer sur un système démarré “à la main” en production. Nous nous appuierons sur deux briques connues et robustes pour diffuser notre application sur le réseau: nginx et uWSGI.

Le fâmeux serveur HTTP nginx dispose en effet d'une fonctionnalité lui permettant de s'adresser à un serveur WSGI, la seule configuration requise est la suivante :

location / {
include uwsgi_params;
uwsgi_pass unix:///var/run/uwsgi/app/
gmlf/socket;
}

Ces quatre lignes sont à placer dans le virtual host de votre choix au sein de la configuration du serveur nginx. Le chemin déclaré pour la directive uwsgi_pass est variable selon l'installation de uWSGI réalisée par votre distribution et/ou UNIX-like favori, dans le cas présent il s'agit d'une machine Debian GNU/Linux. J'ai choisi ici d'utiliser une socket UNIX mais il est bien sûr également possible de communiquer avec le serveur WSGI sur une socket TCP.

La configuration du serveur WSGI uWSGI, sur une machine Debian, requiert la création d'un fichier .ini dans le répertoire /etc/uwsgi/apps-available puis un lien logique du fichier créé dans /etc/uwsgi/apps-enabled/ :

$ cat /etc/uwsgi/apps-available/glmf.ini
[uwsgi]
workers = 2
log-date = true
plugins = python
chdir = /var/www/glmf
module = gmlf
callable = app

Où :

- workers représente le nombre de processus uWSGI démarrés,

- log-date demande de préfixer les lignes de logs par la date,

- plugins demande le chargement du plugin Python,

- chdir change le repertoire de travail à la valeur assignée,

- module est le nom du module à charger,

- callable donne le nom de l'application WSGI à appeler par défaut.

Il n'est pas rare de lire les fichiers .ini uWSGI concaténant module et callable de cette façon :

module = gmlf:app

Une fois les deux serveurs configurés, il ne nous reste plus qu'à les (re-)démarrer, ce qui nous donne chez Debian :

$ sudo /etc/init.d/uwsgi restart
$ sudo /etc/init.d/nginx restart

Cette fois, notre application écoute, à travers nginx, sur le port 80 (si ce dernier à été configuré ainsi) et dispose d'un système de gestion de service digne de ce nom. À nous la prod !

6. Il est pourri ton Web 2.0 mec...

Oui, bon, ok, ça fait pas encore des animations au chargement et ça autocomplète pas les champs, mais ne jugez pas trop vite, car Flask coopère nativement parfaitement avec des bibliothèques comme jQuery, en effet, à l'aide de la fonction jsonify(), il est aisé de déclarer des routes spécialement dédiées à répondre à des appels AJAX et ainsi remplir «automagiquement» un drop-down menu ou encore pré-remplir des listes afin de proposer des champs disposant de fonctions d'autocomplete. Ajoutons à cela plus de 250 plugins permettant d'interfacer à peu près n'importe quoi, après avoir passé quelques heures à manipuler ce soit-disant microframework et quelques-unes de ses extensions, on s'aperçoit que ses possibilités sont à la mesure de son accessibilité.

Références

[1] http://jquery.com/

[2] http://docs.fabfile.org/en/1.6/

[3] http://saltstack.com/community.html

[4] http://flask.pocoo.org/

[5] http://werkzeug.pocoo.org/

[6] http://fr.wikipedia.org/wiki/Web_Server_Gateway_Interface

[7] http://jinja.pocoo.org/

[8] http://uwsgi-docs.readthedocs.org/en/latest/

[9] https://flask-login.readthedocs.org/en/latest/

[10] https://pypi.python.org/pypi/simpleldap

[11] http://imil.net/wp/2013/07/06/ldap-flask-login-snippet/

Tags : Flask, Python, Web