Python Flask
Structure de base d’une application Web Python+Flask
Environnement virtuel
Afin de ne pas perturber l’installation principale de Python, il est conseillé de travailler dans un environnement virtuel.
Installation des paquets nécessaires
sudo apt install python3-venv python3-pip
Création d’un environnement virtuel
Se placer dans le dossier dans lequel on souhaite travailler (celui qui contiendra les fichiers Python).
mkdir monprojet cd monprojet
Le prompt du terminal doit être :
pi@raspberrypinsi1:~/monprojet $
Créer l’environnement virtuel (par exemple nommé env
):
python -m venv env
Patienter …
Activer l’environnement :
. env/bin/activate
L’environnement actif apparaît au début du prompt du terminal :
(env) pi@raspberrypinsi1:~/monprojet $
Environnement virtuel avec VSCodium
Voir VSCodium
Installation de Flask
Lorsque l’environnement virtuel est activé :
Structure d’une application Flask
La structure usuelle des fichiers sur un serveur basé sur Flask est la suivante :
- logs
- erreurs.log
- …
- scripts
- user.py
- …
- static
- img
- css
- js
- templates
- base.html
- page_1.html
- …
- index.py
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("/") # URL relatif à l'adresse IP/non d'hôte du serveur 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()
.
Les modèles doivent se situer dans le dossier
templates
!
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 fonctionlocals()
:render_template(pageHTML, **locals())ne pas oublier les astérisques
**
!!
les noms des variables sont identiques dans le modèle et dans la fonction
- 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 peuvent être stockés dans un fichier users.txt
, sous la forme :
identifiant, hash_du_mot_de_passe
Pour hacher le mot de passe (en obtenir une empreinte), 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")
Rappel : 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 la machine (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 de la machine
@app.route('/shutdown') def shutdown(): if is_logged(): shutdown_server() return 'Server shutting down...' else: return redirect(url_for('index'))
Redémarrage de la machine
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:])