Project Oxygen & Ideo-LabIDEO LAB Dashboard 2026

🐍 Flask & SQLAlchemy – Micro-framework & ORM

Guide complet IDEO‑Lab sur Flask (Routage, Jinja2, Blueprints) et SQLAlchemy (ORM, Migrations).

1.1 Facile

Vue d'ensemble

Micro-framework Python, WSGI, "non-opinionated".

Micro-framework WSGI
1.2 Facile

Flask vs Django

Micro (Flexibilité) vs Macro (Batteries incluses).

Flask Django
1.3 Facile

Installation (venv)

venv, pip install flask, .env, flask run.

venv pip
1.4 Facile

Hello World (app.py)

Flask(__name__), @app.route("/").

@app.route flask run
2.1 Facile

Routage & Vues

@app.route(), methods=['GET', 'POST'].

@app.route methods
2.2 Facile

ParamĂštres (URL & Query)

<id> (Path) vs request.args (Query).

<variable> request.args
2.3 Facile

Templates (Jinja2)

render_template(), /templates, {{}}.

Jinja2 render_template
2.4 Moyen

Héritage Jinja2

{% extends %}, {% block content %}.

{%extends%} {%block%}
2.5 Moyen

Formulaires (request.form)

request.form['...'], CSRF (Flask-WTF).

request.form Flask-WTF
3.1 Moyen

Intro SQLAlchemy

Core (SQL Builder) vs ORM (Objets).

SQLAlchemy ORM
3.2 Moyen

Installation Flask-SQLAlchemy

pip install, SQLALCHEMY_DATABASE_URI.

Flask-SQLAlchemy app.config
3.3 Moyen

Définir les ModÚles

db.Model, db.Column, __repr__.

db.Model db.Column
3.4 Avancé

Relations SQLAlchemy

db.ForeignKey, db.relationship, backref.

db.relationship db.ForeignKey
3.5 Moyen

Migrations (Flask-Migrate)

Alembic, flask db init/migrate/upgrade.

Flask-Migrate Alembic
4.1 Moyen

CRUD : Create

db.session.add(), db.session.commit().

db.session.add db.session.commit
4.2 Moyen

CRUD : Read

.get(), .filter_by(), .first(), .all(), .one().

.get() .filter_by()
4.3 Moyen

CRUD : Update & Delete

.delete(), db.session.commit().

.delete() UPDATE
4.4 Avancé

Blueprints (Organisation)

Factoriser le code (similaire aux "Apps" Django).

Blueprints app.register_blueprint
5.1 Moyen

Déploiement (Gunicorn)

gunicorn -w 4 'app:create_app()', systemd.

Gunicorn WSGI
5.2 Moyen

Déploiement (Nginx)

Reverse Proxy, proxy_pass (Socket Gunicorn).

Nginx proxy_pass
6.1 Facile

Cheat-sheet

flask run, flask db, request, session.

cheat flask db
1.1 Vue d'ensemble : Le Micro-framework
Qu'est-ce que Flask ?

Flask est un **micro-framework** web Python. "Micro" ne signifie pas "limité", mais "minimaliste".

Par défaut, Flask ne fait que deux choses :

  1. Routage : Mapper des URLs Ă  des fonctions Python (@app.route).
  2. Templating : Rendre du HTML via le moteur de template Jinja2.

Tout le reste (BDD, formulaires, authentification, admin) est ajouté par des **extensions** (ex: Flask-SQLAlchemy, Flask-WTF, Flask-Login).

Philosophie

"Non-opinionated" (Non-directif) : Flask ne vous impose aucune structure de projet ni aucun composant. Vous ĂȘtes libre de choisir vos outils.

WSGI : C'est une application WSGI (Web Server Gateway Interface), le standard Python pour communiquer avec les serveurs web (Gunicorn, uWSGI).

1.2 Flask vs Django (La Grande Comparaison)
CritĂšreFlaskDjango
TypeMicro-framework (Minimaliste)Macro-framework ("Batteries incluses")
ObjectifFlexibilité, APIs, petits projets.Projets complexes, Admin, "out-of-the-box".
ComposantsVous choisissez (ex: SQLAlchemy, WTForms).Tout est inclus (ORM, Forms, Admin, Auth).
Admin UINon (via Flask-Admin, manuel).Oui (Auto-générée).
ORMAucun (Recommandé: SQLAlchemy).Django ORM (Intégré).
Courbe d'apprentissageFacile (au début), Moyenne (extensions).Moyenne (structure imposée).
1.3 Installation & Serveur de Dev
1. Environnement Virtuel (venv)

Toujours isoler les projets Python.

# (Linux / macOS)
python3 -m venv .venv
source .venv/bin/activate

# (Windows)
py -m venv .venv
.\.venv\Scripts\activate
2. Installer Flask
pip install Flask
3. (Optionnel) Fichier .env

Flask (via python-dotenv) lit automatiquement les fichiers .env et .flaskenv.

pip install python-dotenv
# Fichier: .flaskenv
FLASK_APP=app.py
FLASK_DEBUG=1
Lancer le serveur de développement

Créez un fichier app.py (voir 1.4).

Méthode 1 (avec .flaskenv)
# (FLASK_APP et FLASK_DEBUG sont chargés depuis .flaskenv)
flask run
Méthode 2 (Manuelle)
export FLASK_APP=app.py
export FLASK_DEBUG=1
flask run

# (ou en une ligne)
FLASK_APP=app.py FLASK_DEBUG=1 flask run

Serveur démarré sur http://127.0.0.1:5000.
FLASK_DEBUG=1 active le mode debug et le **re-chargement automatique** (hot-reload).

1.4 "Hello World" (app.py)
app.py (Fichier minimal)

C'est le "Hello World" classique de Flask.

from flask import Flask

# 1. Créer l'instance de l'application
app = Flask(__name__)

# 2. Définir une "route" (un "décorateur")
#    (Lier l'URL "/" Ă  la fonction 'home')
@app.route("/")
def home():
    # 3. Retourner la réponse
    return "Hello, IDEO-Lab!"

@app.route("/about")
def about():
    return "Page Ă  propos."

# (Optionnel: Permet 'python app.py')
if __name__ == "__main__":
    app.run(debug=True)
Lancement
# (Si .flaskenv est configuré)
$ flask run
 * Running on http://127.0.0.1:5000

# (Si on lance le fichier directement)
$ python app.py
2.1 Routage & Méthodes HTTP
@app.route()

Le décorateur @app.route lie une URL à une fonction (une "vue").

Méthodes HTTP (methods)

Par défaut, une route n'accepte que GET. Pour POST (formulaires), il faut l'autoriser.

from flask import request

# Accepte GET (défaut)
@app.route('/articles')
def article_list():
    return "Liste des articles"

# Accepte GET et POST
@app.route('/contact', methods=['GET', 'POST'])
def contact():
    if request.method == 'POST':
        # (request.form['email']...)
        return "Formulaire reçu !"
    else:
        # (render_template('contact.html')...)
        return "Affiche le formulaire"
        
# Raccourcis (préférés)
@app.get('/profile')
def profile_get(): ...
    
@app.post('/profile')
def profile_post(): ...
Objets request & response

Flask (comme Django) fournit des objets pour gĂ©rer la requĂȘte et la rĂ©ponse.

from flask import request, make_response, redirect, url_for

@app.route('/data')
def get_data():
    # --- Request (RequĂȘte) ---
    methode = request.method # 'GET'
    user_agent = request.headers.get('User-Agent')
    
    # --- Response (Réponse) ---
    
    # 1. Simple (string)
    # return "OK"
    
    # 2. make_response (pour headers/status)
    response = make_response("Contenu custom", 201)
    response.headers['X-Mon-Header'] = 'valeur'
    return response
    
    # 3. Redirect
    # return redirect('/login')
    # (Mieux: utiliser 'url_for' (comme 'name' Django))
    # return redirect(url_for('login_page'))
2.2 ParamĂštres (URL Path vs Query)
1. Path Parameters (<variable>)

Variables dans l'URL. Passées en argument à la fonction.

# (Route: /articles/123)
@app.route('/articles/<int:article_id>')
def show_article(article_id):
    # 'article_id' est 123 (auto-converti en 'int')
    return f"Affichage de l'article {article_id}"

# (Route: /users/alice)
@app.route('/users/<string:username>')
def show_user(username):
    # 'username' est "alice" (string)
2. Query Parameters (request.args)

Variables aprÚs le ? (/search?q=test&page=2). Gérées via l'objet global request.

from flask import request

@app.route('/search')
def search():
    # request.args est un dictionnaire
    
    # ('q' est requis, 'page' est optionnel)
    terme = request.args.get('q')
    page = request.args.get('page', default=1, type=int)
    
    return f"Résultats pour '{terme}' (Page {page})"
2.3 Templates (Jinja2) & render_template
Jinja2 (Le Moteur de Template)

Jinja2 (par le crĂ©ateur de Flask) est le moteur de template. C'est (presque) la mĂȘme syntaxe que Django.

Flask cherche les templates dans un dossier nommé templates/ à la racine du projet.

app.py (Passer le contexte)
from flask import render_template

@app.route('/profile')
def profile():
    # Données à passer au template
    user_data = {
        'nom': 'Alice',
        'email': 'alice@mail.com',
        'tags': ['python', 'flask', 'django']
    }
    
    # 1. Nom du fichier (dans templates/)
    # 2. Variables (contexte)
    return render_template(
        'profile.html',
        user=user_data,
        is_admin=True
    )
templates/profile.html

Profil de {{ user.nom }}

{% if is_admin %}

AccĂšs Admin

{% endif %}
    {% for tag in user.tags %}
  • {{ tag|capitalize }}
  • {% else %}
  • Aucun tag.
  • {% endfor %}
{{ "

Titre

" | safe }}
2.4 Héritage Jinja2 ({% extends %})
templates/layout.html (Le Parent)

(Identique Ă  Django base.html)


    {% block title %}Mon Site{% endblock %}


    
    
    
{% block content %} {% endblock %}
{% block footer %} © 2025 IDEO-Lab {% endblock %}
templates/home.html (L'Enfant)

Utilise {% extends %} et remplit les blocs.

{% extends "layout.html" %}

{% block title %}Page d'Accueil{% endblock %}

{% block content %}
  

Bienvenue !

Ceci est la page d'accueil.

{% endblock %} {% block footer %} {{ super() }} - Tous droits réservés. {% endblock %}
2.5 Gestion des Formulaires (request.form)
request.form (Basique)

Pour les formulaires simples, vous pouvez lire request.form (un dictionnaire) sur une route POST.

@app.route('/contact', methods=['GET', 'POST'])
def contact():
    if request.method == 'POST':
        # 1. Lire les données (non validé)
        email = request.form.get('email')
        message = request.form.get('message')
        
        # (Logique d'envoi d'email...)
        
        # 2. Flasher un message
        flash('Merci pour votre message !', 'success')
        return redirect(url_for('home'))
        
    return render_template('contact.html')

⚠ SĂ©curitĂ© : Cette mĂ©thode n'a **pas** de protection CSRF (Cross-Site Request Forgery) par dĂ©faut.

Flask-WTF (Sécurisé & Validation)

(Recommandé) L'extension Flask-WTF (basée sur WTForms) gÚre la validation, le rendu des champs, et la protection CSRF.

pip install Flask-WTF
# app.py
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, validators
app.config['SECRET_KEY'] = 'un-secret-pour-csrf'

class ContactForm(FlaskForm):
    email = StringField('Email', [validators.Email(), validators.InputRequired()])
    message = TextAreaField('Message', [validators.InputRequired()])

@app.route('/contact-wtf', methods=['GET', 'POST'])
def contact_wtf():
    form = ContactForm()
    if form.validate_on_submit(): # (GĂšre POST + CSRF + Validation)
        # (form.email.data, form.message.data)
        flash('Merci !', 'success')
        return redirect(url_for('home'))
    return render_template('contact_wtf.html', form=form)
3.1 Intro SQLAlchemy (Core vs ORM)
L'ORM de référence en Python

SQLAlchemy est le principal "Toolkit SQL" et "Object Relational Mapper (ORM)" de Python. Il est indépendant de Flask.

Il offre deux modes d'utilisation :

1. Core (Query Builder)

Une abstraction "Pythonique" de SQL. Vous manipulez des objets select(), table(). Plus proche de SQL, moins "magique".

sel = select(users_table).where(users_table.c.name == 'alice')
2. ORM (ModĂšle Objet)

(Le plus utilisé avec Flask). Vous définissez des classes (ModÚles) qui mappent vos tables. Vous ne manipulez que des objets.

# (SQLAlchemy ORM)
users = session.query(User).filter_by(name='alice').all()

# (Flask-SQLAlchemy - encore plus simple)
users = User.query.filter_by(name='alice').all()
Flask-SQLAlchemy

Flask-SQLAlchemy est une extension qui intĂšgre SQLAlchemy (ORM) Ă  Flask (gestion de la session, config).

3.2 Installation Flask-SQLAlchemy & Config
1. Installation
# (Activez votre venv)
pip install Flask-SQLAlchemy

# (Installez le "driver" de BDD, ex: Postgres)
pip install psycopg2-binary
# (ou 'pip install mysqlclient' pour MariaDB)
2. Configuration (.env)

Définissez l'URL de connexion (similaire à Django).

# .env
# (PostgreSQL)
DATABASE_URI="postgresql://user:pass@host:port/db_name"

# (MariaDB)
# DATABASE_URI="mysql+mysqlclient://user:pass@host:port/db_name"

# (SQLite)
# DATABASE_URI="sqlite:///project.db"
3. Initialisation (app.py)

Liez l'app Flask Ă  l'objet db.

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os

# 1. Créer l'app
app = Flask(__name__)

# 2. Charger la config depuis .env
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("DATABASE_URI")
# (Optionnel: désactiver le tracking, gourmand)
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

# 3. Initialiser l'objet 'db'
db = SQLAlchemy(app)

# (Vos modĂšles viendront ici...)
# class User(db.Model): ...

# (Vos routes)
@app.route("/")
def home():
    return "OK"
3.3 Définition des ModÚles (db.Model)

Les modÚles (classes) héritent de db.Model. (Exemple basé sur app.py de 3.2).

# ... (aprĂšs db = SQLAlchemy(app))

# --- Définition des ModÚles ---

class User(db.Model):
    # __tablename__ = 'custom_name' (Optionnel)
    
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    is_admin = db.Column(db.Boolean, default=False)
    
    # (Relation: Un User a plusieurs Posts)
    posts = db.relationship('Post', backref='auteur', lazy=True)

    def __repr__(self):
        return f""

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    titre = db.Column(db.String(200), nullable=False)
    contenu = db.Column(db.Text)
    date_cree = db.Column(db.DateTime, default=datetime.utcnow)
    
    # (Clé étrangÚre: lie à la table 'user' (minuscule))
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)

    def __repr__(self):
        return f""

# --- Créer les tables (si elles n'existent pas) ---
# (À utiliser 1 fois en dev, ou utiliser Migrations (3.5))
with app.app_context():
    db.create_all()
3.4 Relations (db.relationship)
db.ForeignKey (Le "Lien" SQL)

Définit la contrainte physique (colonne) dans la BDD.
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))

db.relationship (La "Magie" ORM)

Définit la relation *objet*. Ne crée *pas* de colonne. Permet d'accéder aux objets liés (ex: user.posts).

backref (Référence arriÚre)

posts = db.relationship('Post', backref='auteur') (dans User) est un raccourci.
Il crée la relation user.posts (Liste de Posts).
Il crée *aussi* la relation post.auteur (l'objet User).

Table "Many-to-Many" (Pivot)

(Ex: Post <-> Tag)

# 1. Table d'association
tags_pivot = db.Table('tags_pivot',
    db.Column('post_id', db.Integer, db.ForeignKey('post.id')),
    db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'))
)

class Post(db.Model):
    # ...
    tags = db.relationship('Tag', secondary=tags_pivot,
                           backref='posts', lazy='dynamic')
                           
class Tag(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), unique=True)
3.5 Migrations (Flask-Migrate & Alembic)
Flask-Migrate

Ne *jamais* utiliser db.create_all() en production.
Flask-Migrate (basé sur Alembic) est l'outil (similaire à manage.py migrate) pour gérer les changements de schéma BDD (migrations).

Installation
pip install Flask-Migrate
Configuration (app.py)
...
from flask_migrate import Migrate

app = Flask(__name__)
db = SQLAlchemy(app)
# (Lier l'app et la BDD Ă  Migrate)
migrate = Migrate(app, db)

# (ModĂšles...)
class User(db.Model): ...

# (Routes...)
Flux de travail (flask db ...)

Nécessite FLASK_APP=app.py (ou .flaskenv).

# 1. (Une fois) Créer le dossier 'migrations/'
$ flask db init

# 2. (Chaque fois que 'models.py' change)
#    Générer la migration (compare ModÚles vs BDD)
$ flask db migrate -m "Ajout table Post"
# (Crée 'migrations/versions/abc_ajout_table_post.py')

# 3. Appliquer la migration Ă  la BDD
$ flask db upgrade

# (Annuler)
$ flask db downgrade
4.1 CRUD : Create (db.session)
La Session (La transaction)

SQLAlchemy fonctionne avec une "Session" (transaction). (Flask-SQLAlchemy la gĂšre automatiquement Ă  la fin de la requĂȘte).

@app.route('/create_user', methods=['POST'])
def create_user():
    # 1. Récupérer les données (ex: request.form)
    username = request.form['username']
    email = request.form['email']
    
    # 2. Créer l'objet Python
    new_user = User(username=username, email=email)
    
    try:
        # 3. Ajouter l'objet Ă  la session
        db.session.add(new_user)
        
        # 4. "Valider" (Exécute l'INSERT)
        db.session.commit()
        
        return f"Utilisateur {new_user.username} créé (ID: {new_user.id})"
        
    except Exception as e:
        # 5. Annuler (en cas d'erreur, ex: email unique)
        db.session.rollback()
        return f"Erreur: {e}", 500
4.2 CRUD : Read (Querying)
Méthodes de .query (Flask-SQLAlchemy v2)

(Note: Flask-SQLAlchemy v3+ préfÚre db.session.execute(select(...)), mais .query est plus simple pour les débutants).

# --- Récupérer 1 seul ---

# (Recommandé) Get by ID (PK) (Renvoie 404 si non trouvé)
# (Nécessite Flask-SQLAlchemy 3+)
user = db.session.get(User, 1)
if not user:
    abort(404)

# (Ancien)
# user = User.query.get_or_404(1)

# (Recommandé) Premier résultat (ou 404)
user = User.query.filter_by(username='alice').first_or_404()

# --- Récupérer Plusieurs ---

# (Tout)
all_users = User.query.all()

# (Filtre 'filter_by' -> K/V simple)
admins = User.query.filter_by(is_admin=True).all()

# (Filtre 'filter' -> Opérateurs)
users = User.query.filter(User.username.like('A%')) \
                  .filter(User.age > 18) \
                  .order_by(User.username.asc()) \
                  .all()
4.3 CRUD : Update & Delete
Update (Mise Ă  jour)
@app.route('/promote/<int:id>')
def promote_user(id):
    # 1. Lire (depuis BDD)
    user = db.session.get(User, id)
    if not user:
        abort(404)
        
    # 2. Modifier (en mémoire)
    user.is_admin = True
    
    # 3. Valider (exécute l'UPDATE)
    db.session.commit()
    
    return f"{user.username} est admin."
Delete (Suppression)
@app.route('/delete/<int:id>')
def delete_user(id):
    # 1. Lire (depuis BDD)
    user = db.session.get(User, id)
    if not user:
        abort(404)
        
    # 2. Supprimer (de la session)
    db.session.delete(user)
    
    # 3. Valider (exécute le DELETE)
    db.session.commit()
    
    return f"{user.username} supprimé."
4.4 Blueprints (Organisation de projet)
ProblĂšme (Gros app.py)

Mettre toutes les routes dans app.py devient ingérable.
Les Blueprints (Schémas) sont l'équivalent des "Apps" Django. Ils permettent de grouper les routes, templates et statiques par fonctionnalité (ex: auth, articles).

Structure (Factory Pattern)
mon_projet/
├── app/
│   ├── __init__.py      (Contient create_app())
│   ├── models.py        (Modùles DB)
│   ├── routes_auth.py   (Blueprint 'auth')
│   ├── routes_article.py(Blueprint 'articles')
│   └── templates/
└── run.py               (Lanceur)
Définition (routes_auth.py)
from flask import Blueprint, render_template

# 1. Créer le Blueprint
auth_bp = Blueprint('auth_bp', __name__)

@auth_bp.route('/login')
def login():
    return render_template('auth/login.html')
Enregistrement (app/__init__.py)
from flask import Flask
from .models import db

def create_app():
    app = Flask(__name__)
    app.config.from_envvar(...)
    db.init_app(app)
    
    # 2. Enregistrer les Blueprints
    from . import routes_auth
    app.register_blueprint(routes_auth.auth_bp, url_prefix='/auth')
    
    return app
5.1 Déploiement (Gunicorn)
Serveur WSGI (Gunicorn)

Le flask run est un serveur de **développement**. En production, on utilise un serveur WSGI (comme **Gunicorn**) pour gérer les workers (processus).

pip install gunicorn
Lancement (Manuel)

([module]:[variable])

# (Si app.py contient 'app = Flask(...)')
gunicorn -w 4 app:app

# (Si on utilise le pattern 'Factory' (create_app))
gunicorn -w 4 'app:create_app()'
Service systemd

(Voir guide Nginx 3.1 pour le gunicorn.service complet)

[Service]
User=ideo_user
WorkingDirectory=/home/ideo_user/mon_projet_flask
ExecStart=/home/ideo_user/mon_projet_flask/.venv/bin/gunicorn \
    --workers 4 \
    --bind unix:/run/gunicorn_flask.sock \
    'app:create_app()'
...
5.2 Déploiement (Nginx)
Nginx (Reverse Proxy)

Nginx (le "MaĂźtre d'HĂŽtel") reçoit la requĂȘte (Port 80) et la transmet Ă  Gunicorn (via le socket).

Il sert aussi les fichiers statiques (/static/).

# /etc/nginx/sites-available/flask_app
server {
    listen 80;
    server_name flask-app.ideolab.com;
    
    # 1. Servir les statiques (via 'alias')
    location /static/ {
        alias /home/ideo_user/mon_projet_flask/static/;
        expires 1y;
    }

    # 2. Passer le reste Ă  Gunicorn
    location / {
        include proxy_params;
        proxy_pass http://unix:/run/gunicorn_flask.sock;
    }
}
6.1 Cheat-sheet (Flask & SQLAlchemy)
Flask CLI & App
# (Configurer .flaskenv)
FLASK_APP=app.py
FLASK_DEBUG=1

# Lancer le serveur
flask run

# Lancer la CLI (REPL)
flask shell

# Lancer les commandes 'db' (Migrate)
flask db init
flask db migrate -m "..."
flask db upgrade
SQLAlchemy (db.session)
# Create
user = User(username='new')
db.session.add(user)
db.session.commit()

# Read
user = db.session.get(User, 1)
user = User.query.get_or_404(1)
users = User.query.filter_by(is_admin=True).all()
user = User.query.filter(User.age > 18).first()

# Update
user = db.session.get(User, 1)
user.username = 'new_name'
db.session.commit()

# Delete
user = db.session.get(User, 1)
db.session.delete(user)
db.session.commit()