Python Flask
Structure de base d’une application Web Python+Flask
Installation de Flask
Le fichier Python principal
Ce fichier est exécuté par un import depuis le fichier script wsgi . Il doit impérativement instancier un objet Flask(__name__) , stocké dans une variable (ici app ) pour être utilisé par le script wsgi.
Il doit se trouver à la racine du dossier du site.
#!/usr/bin/env python # -*- coding: utf-8 -*- # Le Framework (générateur de page HTML) from flask import * # Application Flask app = Flask(__name__) # le nom app est utilisé par le fichier wsgi # Page HTML de base pageHTML = "index.html" ############################################################################################ @app.route("/") def index(): return render_template(pageHTML) # Script de débuggage (n'est pas exécuté quand le module est importé par wsgi) if __name__ == "__main__": app.run(host='0.0.0.0', debug = True)
Les fichiers HTML
Il est conseillé de créer des fichiers HTML qui serviront de base au contenu.
Les fichiers HTML doivent se trouver dans le sous-dossier template (modèles) du dossier du site.
<!DOCTYPE html > <html> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8"/> <!-- Encodage de la page --> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Mon site Web</title> </head> <!-- Corps principal de la page --> <body> <p> Bonjour tout le monde ! </p> </body> </html>
Remarque : la ligne <meta name= »viewport » content= »width=device-width, initial-scale=1.0″> permet d’adapter correctement les dimensions de la page aux écrans de smartphone.
Toutes les explications ici : https://www.alsacreations.com/article/lire/1490-comprendre-le-viewport-dans-le-web-mobile.html
Les autres fichiers
Tous les autres fichiers nécessaires à l’élaboration des pages Web doivent se trouver dans le sous-dossier static du dossier du site :
- feuilles de style css
- scripts javascript
- images
- …
Routage
Lorsque l’utilisateur saisit une URL dans la barre d’adresse du navigateur, ce dernier envoie une requête au serveur Apache. Grâce à l’interface WSGI, cette requête se traduit par l’exécution d’une fonction du script principal Python, qui retourne le contenu de la page.
Le lien entre l’URL de la requête et la fonction à exécuter est réalisé par un décorateur :
@app.route("/") def index(): return 'Page principale'
Dans cet exemple, l’URL « / » correspond à l’adresse IP du Raspberry, sans rien d’autre :
@app.route("/ma_page") def page(): return 'Autre page'
On peut également utiliser des URL avec des « variables » :
@app.route('/user/<username>') def show_user_profile(username): return 'User %s' % username @app.route('/page/<int:num_page>') def afficher_page(num_page): return 'Page n°%s' %num_page
Rendu des modèles
Les modèles (templates) sont des fichiers HTML qui peuvent être complétés dynamiquement par Flask. Le rendu est effectué par la fonction render_template() :
Avec la fonction suivante …
import datetime @app.route("/") def index(): now = datetime.datetime.now() date = "%s-%s-%s" %(now.year, now.month, now.day) liste = ['a', 'b', 'c'] dico = {'Nom' : 'PAGE', 'Prénom' : 'Jimmy'} return render_template(pageHTML, date = date, liste = liste, dico = dico)
… le modèle pageHTML ci-dessous …
<!DOCTYPE html > <html> <head> <title>Mon site Web</title> </head> <body> <h1>Bonjour tout le monde !</h1> <p>Nous sommes le {{ date }}</p> {% if liste|length > 0 %} <ul> {% for i in liste %} <li>{{ i }}</li> {% endfor %} </ul> {% else %} <p> la liste est vide !</p> {% endif %} <dl> {% for cle, valeur in dico.iteritems() %} <dt>{{ cle}} :</dt> <dd>{{ valeur }}</dd> {% endfor %} </dl> </body> </html>
… s’affichera ainsi :
Remarques :
- On peut passer à render_template() toutes les variables locales de la fonction grâce à la fonction locals() :
render_template(pageHTML, **locals())ne pas oublier les astérisques ** !!
- Toute la documentation sur les rendus : http://jinja.pocoo.org/docs/2.10/templates/
Quelques fonctions utiles
Accès restreint
Flask peut prendre en charge la gestion des sessions utilisateur.
Les identifiants de connexion sont stockés dans un fichier users.txt , sous la forme :
identifiant,hash_du_mot_de_passe
Pour hacher le mot de passe, on peut utiliser la fonction suivante :
import hashlib def hash_mdp(mdp): return hashlib.sha256(str(mdp).encode('utf-8')).hexdigest()
Remarque : la fonction de hachage permet d’obtenir une empreinte du mot de passe. Chaque mot de passe possède une unique empreinte, et il est impossible de retrouver le mot de passe en connaissant l’empreinte.
On peut récupérer la liste des utilisateurs sous forme d’un dictionnaire avec la fonction suivante :
import os def get_users(path_dir): users = {} with open(os.path.join(path_dir, "users.txt")) as f: for l in f: u, m = l.split(',') users[u] = m return users
Du coté de Flask, on utilise l’objet session , qui permet de stocker dans le navigateur, sous forme d’un cookie signé, et au contenu crypté, les données de l’utilisateur connecté.
Il faut importer l’objet session puis créer une clef de cryptage (secrète, sinon on pourrait modifier le cookie !)
from flask import session app.secret_key = 'z\x82sIL8\xgg\x16\xf3\xb1Q\x84\xc0E\x8f\r\r{\xd2\x81H<:f'
Pour générer une clef secrète, on peut utiliser le code suivant, en ligne de commande :
python -c 'import os; print(os.urandom(16))'
Il faut bien sûr prévoir une page de connexion, (par exemple login.html), contenant un formulaire :
<form method="post"> <p><input type=text name=id> <p><input type=password name=mdp> <p><input type=submit value='Se connecter'> </form>
Et créer le routage vers cette page :
@app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': # L'utilisateur a saisi un identifiant identifiant= request.form.get('id') # Récupération de l'identifiant sur le formulaire users = get_users("") # Récupération des identifiants des utilisateurs enregistrés if identifiantin users.keys(): # L'identifiant correspond à un utilisateur enregistré mdp = request.form.get('mdp') # Récupération du mot de passe sur le formulaire mdp_chiffre = hash_mdp(mdp) # Hachage du mot de passe if users[identifiant] == mdp_chiffre: # Le mot de passe correspond session['username'] = identifiant # Inscription de l'utilisateur return redirect(url_for('index')) # Retour à la page d'accueil else: # L'utilisateur demande l'accès à la page de connexion return render_template("login.html")
Remarque : l’emploi de la méthode POST pour envoyer les données saisies dans le formulaire permet d’éviter qu’elles apparaissent dans l’URL.
Et il faut aussi prévoir une fonction de déconnexion :
@app.route('/logout') def logout(): session.pop('username', None) return redirect(url_for('index'))
Par la suite, on pourra savoir si un utilisateur est connecté (et lequel !) en utilisant une fonction de ce type :
def is_logged(): return 'username' in session
Actions diverses
Il est parfois nécessaire de réaliser des opérations à distance sur le Raspberry.
Il est fortement conseillé de réserver ces actions aux utilisateurs « autorisés » !
Extinction du serveur
@app.route('/shutdown_server') def shutdown_server(): func = request.environ.get('werkzeug.server.shutdown') if func is None: raise RuntimeError('Not running with the Werkzeug Server') func()
Extinction du Raspberry
@app.route('/shutdown') def shutdown(): if is_logged(): shutdown_server() return 'Server shutting down...' else: return redirect(url_for('index'))
Redémarrage du Raspberry
import os @app.route('/reboot') def reboot_system(): if is_logged(): os.system("sudo reboot") return 'Rebooting ...' else: return redirect(url_for('index'))
Gestion des robots
Il existe un protocole d’exclusion des robots, appelé robot.txt,
Le fichier robots.txt, à placer la racine d’un site web, contient une liste de ressources du site qui ne sont pas censées être explorées par les moteurs de recherches.
@app.route('/robots.txt') @app.route('/sitemap.xml') def static_from_root(): return send_from_directory(app.static_folder, request.path[1:])