# Fichier: python_cheats/cheatsheets/rest_api.txt
# Cheatsheet REST API - Guide Ultra-Détaillé pour Grands Débutants


[OK] CONCEPTS FONDAMENTAUX (EXPLICATIONS TRÈS DÉTAILLÉES)

# === QU'EST-CE QU'UNE REST API? ===

# Imagine que tu as une application web sur un serveur
# Des milliers de clients (applications, navigateurs, appareils mobiles) veulent:
#   - Récupérer des données
#   - Créer de nouvelles données
#   - Modifier des données existantes
#   - Supprimer des données

# Question: Comment les clients communiquent avec le serveur?

# Solution ANCIENNE (compliquée):
# - Serveur envoie une PAGE HTML complète
# - Client (navigateur) affiche la page
# - Problème: C'est lent! Trop de données!
# - Problème: Seulement les navigateurs peuvent l'utiliser!

# Solution MODERNE (REST API):
# - Client demande: "Donne-moi les utilisateurs au format JSON"
# - Serveur répond avec UNIQUEMENT les données: [{"id": 1, "nom": "Alice"}, ...]
# - Client (n'importe quoi: navigateur, appli mobile, autre serveur) peut l'utiliser!
# - Rapide! Léger! Universel!

# REST API = Interface standardisée pour communiquer entre applications
# REST = REpresentational State Transfer = Transfert d'état représentationnel

# === ANALOGIE SIMPLE ===

# Imagine un restaurant:
# 
# ANCIENNE MÉTHODE (pas REST):
# - Tu demandes au serveur: "Je veux de la nourriture"
# - Le serveur te donne TOUT: soupe, plat, dessert, boisson, pain
# - Tu dois trier toi-même
# - Lent et inefficace!
#
# MÉTHODE REST:
# - Tu demandes au serveur: "Donne-moi 2 pizzas et 1 salade"
# - Le serveur te donne EXACTEMENT ça, pas plus, pas moins
# - Vite et efficace!
#
# La REST API = Le serveur qui écoute tes demandes précises


# === VOCABULAIRE REST API (TRÈS IMPORTANT!) ===

# CLIENT
# = L'application qui DEMANDE les données
# Exemples: navigateur web, appli mobile, autre serveur, script Python
# = Fait les requêtes HTTP

# SERVEUR
# = L'application qui FOURNIT les données
# = Répond aux requêtes HTTP
# Exemple: Flask, Django, Node.js, appli Java

# RESSOURCE
# = Un "objet" que tu peux manipuler via l'API
# Exemples: Utilisateurs, Articles, Produits, Commentaires
# = Dans une base de données: une table = une ressource

# ENDPOINT (Chemin de l'API)
# = Une URL qui correspond à une action spécifique
# Format: /api/ressource/action
# Exemples:
#   /api/users          (liste des utilisateurs)
#   /api/users/1        (utilisateur avec id=1)
#   /api/articles       (liste des articles)
#   /api/articles/1/comments  (commentaires de l'article 1)

# REQUEST (Requête)
# = Un message du CLIENT au SERVEUR
# Contient:
#   - Une URL (endpoint)
#   - Une méthode HTTP (GET, POST, PUT, DELETE)
#   - Des en-têtes (headers) - informations supplémentaires
#   - Un corps (body) - données à envoyer (optionnel)
# Analogie: C'est comme une lettre avec l'adresse, le contenu, et des timbres

# RESPONSE (Réponse)
# = Un message du SERVEUR au CLIENT
# Contient:
#   - Un code de statut HTTP (200, 404, 500, etc)
#   - Des en-têtes (headers)
#   - Un corps (body) - données retournées (optionnel)
# Analogie: C'est la réponse à ta lettre avec les informations demandées

# JSON (JavaScript Object Notation)
# = Format pour structurer les données
# Format texte lisible par humain et machines
# Exemple JSON:
{
  "id": 1,
  "nom": "Alice",
  "email": "alice@example.com",
  "age": 25
}
# Format très courant dans les REST APIs


# === MÉTHODES HTTP (VERBES POUR LES ACTIONS) ===

# HTTP = HyperText Transfer Protocol = Protocole pour transférer des documents web

# Les méthodes HTTP = Les actions qu'un client peut demander

# GET (RÉCUPÉRER)
# = "Donne-moi des données"
# = Ne modifie JAMAIS les données sur le serveur
# = Utilisation: Récupérer une liste, voir un utilisateur, etc
# Exemple de requête:
#   GET /api/users
#   (Donne-moi la liste de tous les utilisateurs)
# Réponse attendue:
#   [{"id": 1, "nom": "Alice"}, {"id": 2, "nom": "Bob"}]

# POST (CRÉER)
# = "Crée une nouvelle donnée"
# = Ajoute quelque chose au serveur
# = Le corps de la requête contient les données à créer
# Exemple de requête:
#   POST /api/users
#   Body: {"nom": "Charlie", "email": "charlie@example.com"}
# Réponse attendue:
#   {"id": 3, "nom": "Charlie", "email": "charlie@example.com"}

# PUT (REMPLACER)
# = "Remplace cette ressource complètement"
# = Modifie UNE ressource existante
# = Remplace TOUS les champs (ou fournis tous les champs)
# Exemple de requête:
#   PUT /api/users/1
#   Body: {"nom": "Alice2", "email": "alice2@example.com", "age": 26}
# Réponse attendue:
#   {"id": 1, "nom": "Alice2", "email": "alice2@example.com", "age": 26}

# PATCH (MODIFIER PARTIELLEMENT)
# = "Modifie seulement certains champs"
# = Comme PUT mais tu ne dois pas fournir TOUS les champs
# Exemple de requête:
#   PATCH /api/users/1
#   Body: {"age": 26}  (Seulement le champ age)
# Réponse attendue:
#   {"id": 1, "nom": "Alice", "email": "alice@example.com", "age": 26}
#   (Les autres champs gardent leurs valeurs)

# DELETE (SUPPRIMER)
# = "Supprime cette ressource"
# = Supprime complètement une ressource du serveur
# Exemple de requête:
#   DELETE /api/users/1
# Réponse attendue:
#   {} ou {"message": "Utilisateur supprimé"}

# Résumé:
# GET = Lire
# POST = Créer
# PUT = Remplacer complètement
# PATCH = Modifier partiellement
# DELETE = Supprimer


# === CODES DE STATUT HTTP (RÉPONSES DU SERVEUR) ===

# Chaque réponse commence par un code de statut = 3 chiffres

# 2XX = SUCCÈS (Tout va bien!)
# 200 OK = Requête réussie
#   Exemple: GET /api/users retourne les utilisateurs
# 201 Created = Ressource créée avec succès
#   Exemple: POST /api/users crée un nouvel utilisateur
# 204 No Content = Succès mais pas de contenu à retourner
#   Exemple: DELETE /api/users/1 supprime sans rien retourner

# 3XX = REDIRECTION (Va ailleurs!)
# 301 Moved Permanently = La ressource a été déplacée définitivement
# 302 Found = Redirection temporaire

# 4XX = ERREUR CLIENT (C'est ta faute!)
# 400 Bad Request = Requête mal formée
#   Exemple: Tu oublies un champ obligatoire
# 401 Unauthorized = Non authentifié (tu dois te connecter)
#   Exemple: Tu essaies d'accéder à des données privées sans token
# 403 Forbidden = Authentifié mais pas autorisé
#   Exemple: Tu n'as pas la permission
# 404 Not Found = Ressource n'existe pas
#   Exemple: GET /api/users/999 (l'utilisateur n'existe pas)
# 409 Conflict = Conflit (ex: ID déjà existant)
# 422 Unprocessable Entity = Données invalides
#   Exemple: Email mal formé

# 5XX = ERREUR SERVEUR (C'est pas ta faute!)
# 500 Internal Server Error = Bug sur le serveur
# 502 Bad Gateway = Serveur indisponible
# 503 Service Unavailable = Serveur surchargé

# Les codes 2XX = tout va bien!
# Les codes 4XX = c'est ta requête qui est mauvaise
# Les codes 5XX = c'est le serveur qui a un problème


# === ARCHITECTURE REST API (QUI FAIT QUOI?) ===

# Structure complète:
#
# CLIENT                          SERVEUR
# ├── App Mobile                  ├── Framework Flask/Django
# ├── App Web (JavaScript)        ├── Base de données
# ├── Script Python               ├── Logique métier
# └── Autre serveur               └── Endpoints REST
#
# Communication via HTTP:
# Client -> GET /api/users -> Serveur
# Serveur -> 200 OK + [users] -> Client

# Un même serveur peut servir:
# - Une app web (HTML/CSS/JS)
# - Une app mobile (iOS/Android)
# - D'autres services/serveurs
# = Tout via la même REST API!


[OK] CONCEPTS REST DETAILLÉS

# === RESTful vs Non-RESTful ===

# NON-RESTFUL (Mauvaise pratique):
# /api/getUser?id=1         (GET est dans l'URL, bizarre!)
# /api/updateUser?id=1      (Mélange action et ressource)
# /api/deleteUser?id=1
# = Pas standardisé, difficile à comprendre, pas professionnel

# RESTFUL (Bonne pratique):
# GET /api/users/1          (Clair: récupérer utilisateur 1)
# POST /api/users          (Clair: ajouter utilisateur 1)
# PUT /api/users/1          (Clair: modifier utilisateur 1)
# PATCH /api/users/1          (Clair: modifier nom utilisateur 1)
# DELETE /api/users/1       (Clair: supprimer utilisateur 1)
# = Standardisé, facile à comprendre, professionnel

# Règles RESTful:
# 1. Utilise les bonnes méthodes HTTP (GET, POST, PUT, DELETE)
# 2. Les URLs sont les RESSOURCES, pas les ACTIONS
#    Mauvais: /api/creerUser
#    Bon: POST /api/users
# 3. Utilise des noms pluriels pour les ressources
#    Mauvais: /api/user/1
#    Bon: /api/users/1
# 4. Utilise des codes de statut HTTP appropriés
#    Mauvais: 200 OK même en cas d'erreur
#    Bon: 404 Not Found si l'utilisateur n'existe pas


# === URL PATTERNS COMMUNS ===

# GET /api/users
# = Récupérer TOUS les utilisateurs

# GET /api/users/1
# = Récupérer l'utilisateur avec id=1

# GET /api/users?limit=10&offset=0
# = Récupérer 10 utilisateurs en commençant à 0 (pagination)

# GET /api/users?nom=alice
# = Récupérer les utilisateurs dont le nom contient "alice" (filtrage)

# POST /api/users
# = Créer un nouvel utilisateur
# Body: {"nom": "Charlie", "email": "charlie@example.com"}

# PUT /api/users/1
# = Remplacer complètement l'utilisateur 1

# PATCH /api/users/1
# = Modifier partiellement l'utilisateur 1

# DELETE /api/users/1
# = Supprimer l'utilisateur 1

# GET /api/users/1/articles
# = Récupérer les articles de l'utilisateur 1 (ressources imbriquées)

# POST /api/users/1/articles
# = Créer un article pour l'utilisateur 1


# === JSON (FORMAT DES DONNÉES) ===

# JSON = Format structuré pour les données

# Types JSON:
# String: "Alice", "alice@example.com"
# Number: 25, 3.14
# Boolean: true, false
# Null: null (pas de valeur)
# Array: [1, 2, 3, "Alice"]
# Object: {"nom": "Alice", "age": 25}

# Exemple complet:
{
  "id": 1,
  "nom": "Alice",
  "email": "alice@example.com",
  "age": 25,
  "actif": true,
  "tags": ["python", "web", "api"],
  "adresse": {
    "rue": "123 Main St",
    "ville": "Paris",
    "codepostal": "75001"
  }
}

# Règles JSON:
# - Les clés DOIVENT être entre guillemets doubles: "nom"
# - Les valeurs string DOIVENT être entre guillemets doubles: "Alice"
# - Pas de virgules après le dernier élément
# - Pas de commentaires


# === AUTHENTIFICATION & AUTORISATION ===

# AUTHENTIFICATION = Vérifier QUI tu es
# = Exemple: Vérifier ton identifiant et mot de passe

# AUTORISATION = Vérifier QUOI tu peux faire
# = Exemple: Vérifier si tu as la permission d'accéder à /api/admin

# Méthodes courantes:

# 1. API KEY (Clé simple)
# = Une clé secrète pour identifier l'application
# Utilisée dans l'en-tête:
# Authorization: API_KEY sk_test_12345abcde
# Bon pour: Services simples, scripts
# Problème: La clé est identique pour tous

# 2. TOKEN JWT (JSON Web Token)
# = Un token qui contient des informations encodées
# Utilisée dans l'en-tête:
# Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# Bon pour: Applications web, apps mobiles
# Avantage: Le token expire automatiquement

# 3. OAuth 2.0
# = Système complexe pour déléguer l'authentification
# Exemple: "Se connecter avec Google" sur un site
# Bon pour: Grandes applications, sécurité maximale

# Pour commencer: Utilise une API KEY simple


[OK] CRÉER UNE REST API AVEC FLASK (ÉTAPE PAR ÉTAPE)

# === ÉTAPE 1: STRUCTURE DU PROJET ===

# Créer le dossier:
mkdir api-blog && cd api-blog

# Structure:
# api-blog/
# ├── .git/
# ├── venv/
# ├── app.py              (L'app Flask)
# ├── models.py           (Les modèles de données)
# ├── Procfile            (Pour Heroku)
# ├── requirements.txt    (Les dépendances)
# ├── .gitignore
# └── README.md


# === ÉTAPE 2: SETUP INITIAL ===

git init
python -m venv venv
source venv/bin/activate  # Linux/macOS
venv\Scripts\activate     # Windows

# Installer les packages:
pip install flask
pip install flask-sqlalchemy
pip install flask-cors    # Pour les requêtes cross-origin
pip install gunicorn      # Pour production (Heroku)

# Créer requirements.txt:
pip freeze > requirements.txt


# === ÉTAPE 3: CRÉER UN MODÈLE DE DONNÉES (models.py) ===

"""
models.py - Modèles SQLAlchemy
"""

from datetime import datetime
from app import db

class Article(db.Model):
    '''Modèle pour les articles du blog'''
    id = db.Column(db.Integer, primary_key=True)
    titre = db.Column(db.String(200), nullable=False)
    contenu = db.Column(db.Text, nullable=False)
    auteur = db.Column(db.String(100), nullable=False)
    date_creation = db.Column(db.DateTime, default=datetime.now)
    
    def to_dict(self):
        '''Convertir le modèle en dictionnaire (pour JSON)'''
        return {
            'id': self.id,
            'titre': self.titre,
            'contenu': self.contenu,
            'auteur': self.auteur,
            'date_creation': self.date_creation.isoformat()
        }

class Utilisateur(db.Model):
    '''Modèle pour les utilisateurs'''
    id = db.Column(db.Integer, primary_key=True)
    nom = db.Column(db.String(100), nullable=False, unique=True)
    email = db.Column(db.String(100), nullable=False, unique=True)
    age = db.Column(db.Integer)
    
    def to_dict(self):
        '''Convertir le modèle en dictionnaire (pour JSON)'''
        return {
            'id': self.id,
            'nom': self.nom,
            'email': self.email,
            'age': self.age
        }


# === ÉTAPE 4: CRÉER L'APPLICATION FLASK (app.py) ===

"""
app.py - REST API Flask
"""

from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
import os
from dotenv import load_dotenv

# Charger les variables d'environnement
load_dotenv()

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

# CORS = Cross-Origin Resource Sharing
# Permet aux clients d'autres domaines d'accéder à l'API
CORS(app)

# === CONFIGURATION ===

# Base de données
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get(
    'DATABASE_URL',
    'sqlite:///blog.db'
)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# Clé secrète
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-key')

# Initialiser la BD
db = SQLAlchemy(app)

# === IMPORTER LES MODÈLES ===
from models import Article, Utilisateur

# Créer les tables au démarrage
with app.app_context():
    db.create_all()


# === ENDPOINTS GET (RÉCUPÉRER DES DONNÉES) ===

@app.route('/api/articles', methods=['GET'])
def get_articles():
    """
    Récupérer tous les articles
    GET /api/articles
    Paramètres optionnels:
    - limit: nombre d'articles à retourner (défaut: tous)
    - offset: nombre d'articles à sauter (pagination)
    - auteur: filtrer par auteur
    """
    
    # Récupérer les paramètres de la requête
    limit = request.args.get('limit', type=int)
    offset = request.args.get('offset', default=0, type=int)
    auteur = request.args.get('auteur', type=str)
    
    # Construire la requête
    query = Article.query
    
    # Filtrer par auteur si fourni
    if auteur:
        query = query.filter_by(auteur=auteur)
    
    # Pagination
    if limit:
        query = query.limit(limit).offset(offset)
    
    # Récupérer les articles
    articles = query.all()
    
    # Convertir en JSON
    return jsonify([article.to_dict() for article in articles])


@app.route('/api/articles/<int:id>', methods=['GET'])
def get_article(id):
    """
    Récupérer UN article spécifique
    GET /api/articles/1
    """
    
    # Récupérer l'article
    article = Article.query.get_or_404(id)
    
    # Retourner en JSON
    return jsonify(article.to_dict())


@app.route('/api/utilisateurs', methods=['GET'])
def get_utilisateurs():
    """
    Récupérer tous les utilisateurs
    GET /api/utilisateurs
    """
    
    utilisateurs = Utilisateur.query.all()
    return jsonify([u.to_dict() for u in utilisateurs])


@app.route('/api/utilisateurs/<int:id>', methods=['GET'])
def get_utilisateur(id):
    """
    Récupérer UN utilisateur spécifique
    GET /api/utilisateurs/1
    """
    
    utilisateur = Utilisateur.query.get_or_404(id)
    return jsonify(utilisateur.to_dict())


# === ENDPOINTS POST (CRÉER DES DONNÉES) ===

@app.route('/api/articles', methods=['POST'])
def create_article():
    """
    Créer un nouvel article
    POST /api/articles
    Body JSON requis:
    {
        "titre": "Mon article",
        "contenu": "Contenu de l'article",
        "auteur": "Alice"
    }
    """
    
    # Récupérer les données JSON du body
    data = request.get_json()
    
    # Vérifier que les champs obligatoires sont présents
    if not data or not all(key in data for key in ['titre', 'contenu', 'auteur']):
        return jsonify({'erreur': 'Champs manquants: titre, contenu, auteur'}), 400
    
    # Créer un nouvel article
    nouvel_article = Article(
        titre=data['titre'],
        contenu=data['contenu'],
        auteur=data['auteur']
    )
    
    # Ajouter à la base de données
    db.session.add(nouvel_article)
    db.session.commit()
    
    # Retourner le nouvel article avec code 201 (Created)
    return jsonify(nouvel_article.to_dict()), 201


@app.route('/api/utilisateurs', methods=['POST'])
def create_utilisateur():
    """
    Créer un nouvel utilisateur
    POST /api/utilisateurs
    Body JSON requis:
    {
        "nom": "Charlie",
        "email": "charlie@example.com",
        "age": 28
    }
    """
    
    # Récupérer les données
    data = request.get_json()
    
    # Validation
    if not data or not all(key in data for key in ['nom', 'email']):
        return jsonify({'erreur': 'Champs manquants: nom, email'}), 400
    
    # Vérifier que le nom/email n'existe pas déjà
    if Utilisateur.query.filter_by(nom=data['nom']).first():
        return jsonify({'erreur': 'Utilisateur existe déjà'}), 409
    
    # Créer l'utilisateur
    nouvel_utilisateur = Utilisateur(
        nom=data['nom'],
        email=data['email'],
        age=data.get('age')  # age est optionnel
    )
    
    db.session.add(nouvel_utilisateur)
    db.session.commit()
    
    return jsonify(nouvel_utilisateur.to_dict()), 201


# === ENDPOINTS PUT (REMPLACER COMPLÈTEMENT) ===

@app.route('/api/articles/<int:id>', methods=['PUT'])
def update_article(id):
    """
    Remplacer complètement un article
    PUT /api/articles/1
    Body JSON (tous les champs):
    {
        "titre": "Nouveau titre",
        "contenu": "Nouveau contenu",
        "auteur": "Bob"
    }
    """
    
    # Récupérer l'article
    article = Article.query.get_or_404(id)
    
    # Récupérer les données
    data = request.get_json()
    
    # Vérifier que tous les champs sont présents
    if not data or not all(key in data for key in ['titre', 'contenu', 'auteur']):
        return jsonify({'erreur': 'Tous les champs sont obligatoires'}), 400
    
    # Modifier l'article
    article.titre = data['titre']
    article.contenu = data['contenu']
    article.auteur = data['auteur']
    
    db.session.commit()
    
    return jsonify(article.to_dict()), 200


@app.route('/api/utilisateurs/<int:id>', methods=['PUT'])
def update_utilisateur(id):
    """
    Remplacer complètement un utilisateur
    PUT /api/utilisateurs/1
    Body JSON:
    {
        "nom": "Charlie2",
        "email": "charlie2@example.com",
        "age": 29
    }
    """
    
    utilisateur = Utilisateur.query.get_or_404(id)
    data = request.get_json()
    
    if not data or not all(key in data for key in ['nom', 'email', 'age']):
        return jsonify({'erreur': 'Tous les champs sont obligatoires'}), 400
    
    utilisateur.nom = data['nom']
    utilisateur.email = data['email']
    utilisateur.age = data['age']
    
    db.session.commit()
    
    return jsonify(utilisateur.to_dict()), 200


# === ENDPOINTS PATCH (MODIFIER PARTIELLEMENT) ===

@app.route('/api/articles/<int:id>', methods=['PATCH'])
def partial_update_article(id):
    """
    Modifier partiellement un article
    PATCH /api/articles/1
    Body JSON (seulement les champs à modifier):
    {
        "titre": "Nouveau titre"
    }
    """
    
    article = Article.query.get_or_404(id)
    data = request.get_json()
    
    # Modifier seulement les champs fournis
    if 'titre' in data:
        article.titre = data['titre']
    if 'contenu' in data:
        article.contenu = data['contenu']
    if 'auteur' in data:
        article.auteur = data['auteur']
    
    db.session.commit()
    
    return jsonify(article.to_dict()), 200


@app.route('/api/utilisateurs/<int:id>', methods=['PATCH'])
def partial_update_utilisateur(id):
    """
    Modifier partiellement un utilisateur
    PATCH /api/utilisateurs/1
    Body JSON:
    {
        "age": 30
    }
    """
    
    utilisateur = Utilisateur.query.get_or_404(id)
    data = request.get_json()
    
    if 'nom' in data:
        utilisateur.nom = data['nom']
    if 'email' in data:
        utilisateur.email = data['email']
    if 'age' in data:
        utilisateur.age = data['age']
    
    db.session.commit()
    
    return jsonify(utilisateur.to_dict()), 200


# === ENDPOINTS DELETE (SUPPRIMER) ===

@app.route('/api/articles/<int:id>', methods=['DELETE'])
def delete_article(id):
    """
    Supprimer un article
    DELETE /api/articles/1
    """
    
    article = Article.query.get_or_404(id)
    db.session.delete(article)
    db.session.commit()
    
    # Retourner un message de confirmation
    return jsonify({'message': 'Article supprimé avec succès'}), 200


@app.route('/api/utilisateurs/<int:id>', methods=['DELETE'])
def delete_utilisateur(id):
    """
    Supprimer un utilisateur
    DELETE /api/utilisateurs/1
    """
    
    utilisateur = Utilisateur.query.get_or_404(id)
    db.session.delete(utilisateur)
    db.session.commit()
    
    return jsonify({'message': 'Utilisateur supprimé avec succès'}), 200


# === GESTION DES ERREURS ===

@app.errorhandler(404)
def not_found(error):
    """Erreur 404: ressource non trouvée"""
    return jsonify({'erreur': 'Ressource non trouvée'}), 404

@app.errorhandler(500)
def internal_error(error):
    """Erreur 500: erreur serveur"""
    return jsonify({'erreur': 'Erreur serveur interne'}), 500


# === LANCER L'APP ===

if __name__ == '__main__':
    port = int(os.environ.get('PORT', 5000))
    app.run(host='0.0.0.0', port=port, debug=False)


[OK] CONSOMMER UNE API REST (UTILISER DEPUIS UN CLIENT)

[OK] CONSOMMER UNE API REST AVEC PYTHON

# === AVEC PYTHON ET LA LIBRARY requests ===

import requests
import json

# URL de base de l'API
BASE_URL = "http://localhost:5000/api"

# === GET: Récupérer des données (EXEMPLES DÉTAILLÉS) ===

# Exemple 1: Récupérer tous les articles
response = requests.get(f"{BASE_URL}/articles")

# Vérifier le code de statut
print(f"Code de statut: {response.status_code}")
# Affiche: 200 (si succès)

# Récupérer les données JSON
articles = response.json()
print(articles)
# Affiche: [{"id": 1, "titre": "...", ...}, ...]

# Exemple 2: Récupérer un article spécifique
article_id = 1
response = requests.get(f"{BASE_URL}/articles/{article_id}")
article = response.json()
print(f"Article: {article['titre']}")

# Exemple 3: Utiliser des paramètres de requête (query params)
params = {
    'limit': 10,
    'offset': 0,
    'auteur': 'Alice'
}
response = requests.get(f"{BASE_URL}/articles", params=params)
articles_filtres = response.json()
print(f"Nombre d'articles: {len(articles_filtres)}")


# === POST: Créer des données (EXEMPLES DÉTAILLÉS) ===

# Exemple 1: Créer un nouvel article
nouvel_article = {
    'titre': 'Mon premier article via API',
    'contenu': 'Ceci est le contenu de mon article.',
    'auteur': 'Bob'
}

# Envoyer la requête POST avec les données JSON
response = requests.post(
    f"{BASE_URL}/articles",
    json=nouvel_article  # Convertit automatiquement en JSON
)

# Vérifier le résultat
if response.status_code == 201:
    article_cree = response.json()
    print(f"Article créé avec l'ID: {article_cree['id']}")
else:
    print(f"Erreur: {response.status_code}")
    print(response.json())

# Exemple 2: Créer un utilisateur
nouvel_utilisateur = {
    'nom': 'Charlie',
    'email': 'charlie@example.com',
    'age': 28
}

response = requests.post(
    f"{BASE_URL}/utilisateurs",
    json=nouvel_utilisateur
)

if response.status_code == 201:
    print("Utilisateur créé avec succès!")
    print(response.json())
elif response.status_code == 409:
    print("Erreur: Utilisateur existe déjà")
else:
    print(f"Erreur inconnue: {response.status_code}")


# === PUT: Remplacer complètement (EXEMPLES DÉTAILLÉS) ===

# Remplacer l'article 1 complètement
article_id = 1
article_mis_a_jour = {
    'titre': 'Titre modifié',
    'contenu': 'Contenu complètement modifié',
    'auteur': 'Alice'
}

response = requests.put(
    f"{BASE_URL}/articles/{article_id}",
    json=article_mis_a_jour
)

if response.status_code == 200:
    print("Article mis à jour!")
    print(response.json())
elif response.status_code == 404:
    print("Article non trouvé")


# === PATCH: Modifier partiellement (EXEMPLES DÉTAILLÉS) ===

# Modifier seulement le titre de l'article 1
article_id = 1
modifications_partielles = {
    'titre': 'Nouveau titre seulement'
    # Les autres champs (contenu, auteur) restent inchangés
}

response = requests.patch(
    f"{BASE_URL}/articles/{article_id}",
    json=modifications_partielles
)

if response.status_code == 200:
    print("Article partiellement modifié!")
    print(response.json())


# === DELETE: Supprimer (EXEMPLES DÉTAILLÉS) ===

# Supprimer l'article 1
article_id = 1
response = requests.delete(f"{BASE_URL}/articles/{article_id}")

if response.status_code == 200:
    print("Article supprimé avec succès!")
    print(response.json())
elif response.status_code == 404:
    print("Article non trouvé")


# === GESTION DES ERREURS (TRÈS IMPORTANT!) ===

def get_article_securise(article_id):
    """
    Fonction qui gère proprement les erreurs
    """
    try:
        response = requests.get(
            f"{BASE_URL}/articles/{article_id}",
            timeout=5  # Timeout après 5 secondes
        )
        
        # Vérifier le code de statut
        if response.status_code == 200:
            return response.json()
        elif response.status_code == 404:
            print(f"Article {article_id} non trouvé")
            return None
        else:
            print(f"Erreur: {response.status_code}")
            return None
            
    except requests.exceptions.Timeout:
        print("La requête a pris trop de temps")
        return None
    except requests.exceptions.ConnectionError:
        print("Impossible de se connecter au serveur")
        return None
    except Exception as e:
        print(f"Erreur inattendue: {e}")
        return None

# Utilisation:
article = get_article_securise(1)
if article:
    print(f"Article récupéré: {article['titre']}")


# === HEADERS PERSONNALISÉS (AUTHENTIFICATION) ===

# Exemple avec une API key
API_KEY = "sk_test_12345abcde"

headers = {
    'Authorization': f'ApiKey {API_KEY}',
    'Content-Type': 'application/json'
}

response = requests.get(
    f"{BASE_URL}/articles",
    headers=headers
)

# Exemple avec un token JWT
TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

headers = {
    'Authorization': f'Bearer {TOKEN}',
    'Content-Type': 'application/json'
}

response = requests.get(
    f"{BASE_URL}/articles",
    headers=headers
)


# === SESSION POUR RÉUTILISER LA CONNEXION ===

# Créer une session (plus efficace pour plusieurs requêtes)
session = requests.Session()

# Définir les headers une seule fois
session.headers.update({
    'Authorization': f'ApiKey {API_KEY}',
    'Content-Type': 'application/json'
})

# Faire plusieurs requêtes avec la même session
articles = session.get(f"{BASE_URL}/articles").json()
utilisateurs = session.get(f"{BASE_URL}/utilisateurs").json()
nouvel_article = session.post(
    f"{BASE_URL}/articles",
    json={'titre': 'Test', 'contenu': '...', 'auteur': 'Bob'}
).json()

# Fermer la session
session.close()


[OK] CONSOMMER UNE API REST AVEC JAVASCRIPT (FRONTEND)

# === AVEC FETCH API (NATIF JAVASCRIPT) ===

// URL de base de l'API
const BASE_URL = "http://localhost:5000/api";

// === GET: Récupérer des données ===

// Exemple 1: Récupérer tous les articles
async function getArticles() {
    try {
        const response = await fetch(`${BASE_URL}/articles`);
        
        // Vérifier si la requête a réussi
        if (!response.ok) {
            throw new Error(`Erreur HTTP: ${response.status}`);
        }
        
        const articles = await response.json();
        console.log('Articles:', articles);
        return articles;
        
    } catch (error) {
        console.error('Erreur:', error);
        return null;
    }
}

// Exemple 2: Récupérer un article spécifique
async function getArticle(id) {
    try {
        const response = await fetch(`${BASE_URL}/articles/${id}`);
        
        if (response.status === 404) {
            console.log('Article non trouvé');
            return null;
        }
        
        if (!response.ok) {
            throw new Error(`Erreur: ${response.status}`);
        }
        
        const article = await response.json();
        return article;
        
    } catch (error) {
        console.error('Erreur:', error);
        return null;
    }
}

// Exemple 3: Avec paramètres de requête
async function getArticlesFiltres(auteur, limit = 10) {
    const params = new URLSearchParams({
        auteur: auteur,
        limit: limit
    });
    
    const response = await fetch(`${BASE_URL}/articles?${params}`);
    const articles = await response.json();
    return articles;
}


// === POST: Créer des données ===

async function creerArticle(titre, contenu, auteur) {
    try {
        const response = await fetch(`${BASE_URL}/articles`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                titre: titre,
                contenu: contenu,
                auteur: auteur
            })
        });
        
        if (response.status === 201) {
            const articleCree = await response.json();
            console.log('Article créé:', articleCree);
            return articleCree;
        } else if (response.status === 400) {
            const erreur = await response.json();
            console.error('Données invalides:', erreur);
            return null;
        } else {
            throw new Error(`Erreur: ${response.status}`);
        }
        
    } catch (error) {
        console.error('Erreur:', error);
        return null;
    }
}

// Utilisation:
creerArticle(
    'Mon article JavaScript',
    'Contenu de mon article créé depuis le frontend',
    'Alice'
);


// === PUT: Remplacer complètement ===

async function modifierArticle(id, titre, contenu, auteur) {
    try {
        const response = await fetch(`${BASE_URL}/articles/${id}`, {
            method: 'PUT',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                titre: titre,
                contenu: contenu,
                auteur: auteur
            })
        });
        
        if (response.ok) {
            const articleModifie = await response.json();
            console.log('Article modifié:', articleModifie);
            return articleModifie;
        } else {
            throw new Error(`Erreur: ${response.status}`);
        }
        
    } catch (error) {
        console.error('Erreur:', error);
        return null;
    }
}


// === PATCH: Modifier partiellement ===

async function modifierArticlePartiel(id, modifications) {
    try {
        const response = await fetch(`${BASE_URL}/articles/${id}`, {
            method: 'PATCH',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(modifications)
        });
        
        if (response.ok) {
            const articleModifie = await response.json();
            console.log('Article partiellement modifié:', articleModifie);
            return articleModifie;
        } else {
            throw new Error(`Erreur: ${response.status}`);
        }
        
    } catch (error) {
        console.error('Erreur:', error);
        return null;
    }
}

// Utilisation: modifier seulement le titre
modifierArticlePartiel(1, { titre: 'Nouveau titre' });


// === DELETE: Supprimer ===

async function supprimerArticle(id) {
    try {
        const response = await fetch(`${BASE_URL}/articles/${id}`, {
            method: 'DELETE'
        });
        
        if (response.ok) {
            const resultat = await response.json();
            console.log('Article supprimé:', resultat);
            return true;
        } else if (response.status === 404) {
            console.log('Article non trouvé');
            return false;
        } else {
            throw new Error(`Erreur: ${response.status}`);
        }
        
    } catch (error) {
        console.error('Erreur:', error);
        return false;
    }
}


// === AVEC AUTHENTIFICATION (TOKEN JWT) ===

const TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";

async function getArticlesAuthentifies() {
    const response = await fetch(`${BASE_URL}/articles`, {
        headers: {
            'Authorization': `Bearer ${TOKEN}`,
            'Content-Type': 'application/json'
        }
    });
    
    const articles = await response.json();
    return articles;
}


// === EXEMPLE COMPLET: APPLICATION FRONTEND ===

// HTML pour afficher les articles
/*
<!DOCTYPE html>
<html>
<head>
    <title>Blog API</title>
</head>
<body>
    <h1>Articles du Blog</h1>
    <div id="articles-liste"></div>
    
    <h2>Créer un article</h2>
    <form id="form-article">
        <input type="text" id="titre" placeholder="Titre" required>
        <textarea id="contenu" placeholder="Contenu" required></textarea>
        <input type="text" id="auteur" placeholder="Auteur" required>
        <button type="submit">Créer</button>
    </form>
    
    <script src="app.js"></script>
</body>
</html>
*/

// JavaScript (app.js)
const BASE_URL = "http://localhost:5000/api";

// Charger et afficher tous les articles
async function afficherArticles() {
    try {
        const response = await fetch(`${BASE_URL}/articles`);
        const articles = await response.json();
        
        const listeDiv = document.getElementById('articles-liste');
        listeDiv.innerHTML = ''; // Vider la liste
        
        articles.forEach(article => {
            const articleDiv = document.createElement('div');
            articleDiv.innerHTML = `
                <h3>${article.titre}</h3>
                <p>${article.contenu}</p>
                <small>Par ${article.auteur} le ${article.date_creation}</small>
                <button onclick="supprimerArticle(${article.id})">Supprimer</button>
                <hr>
            `;
            listeDiv.appendChild(articleDiv);
        });
        
    } catch (error) {
        console.error('Erreur lors du chargement des articles:', error);
    }
}

// Gérer la soumission du formulaire
document.getElementById('form-article').addEventListener('submit', async (e) => {
    e.preventDefault(); // Empêcher le rechargement de la page
    
    const titre = document.getElementById('titre').value;
    const contenu = document.getElementById('contenu').value;
    const auteur = document.getElementById('auteur').value;
    
    try {
        const response = await fetch(`${BASE_URL}/articles`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                titre: titre,
                contenu: contenu,
                auteur: auteur
            })
        });
        
        if (response.status === 201) {
            console.log('Article créé avec succès!');
            
            // Vider le formulaire
            document.getElementById('form-article').reset();
            
            // Recharger la liste des articles
            afficherArticles();
        } else {
            alert('Erreur lors de la création de l\'article');
        }
        
    } catch (error) {
        console.error('Erreur:', error);
        alert('Erreur de connexion');
    }
});

// Supprimer un article
async function supprimerArticle(id) {
    if (!confirm('Êtes-vous sûr de vouloir supprimer cet article?')) {
        return;
    }
    
    try {
        const response = await fetch(`${BASE_URL}/articles/${id}`, {
            method: 'DELETE'
        });
        
        if (response.ok) {
            console.log('Article supprimé');
            afficherArticles(); // Recharger la liste
        } else {
            alert('Erreur lors de la suppression');
        }
        
    } catch (error) {
        console.error('Erreur:', error);
        alert('Erreur de connexion');
    }
}

// Charger les articles au démarrage
afficherArticles();


[OK] PAGINATION AVANCÉE

# === PAGINATION CÔTÉ SERVEUR (FLASK) ===

"""
Ajouter la pagination à l'endpoint /api/articles
"""

@app.route('/api/articles', methods=['GET'])
def get_articles_avec_pagination():
    """
    GET /api/articles?page=1&per_page=10
    Retourne:
    {
        "articles": [...],
        "total": 100,
        "page": 1,
        "per_page": 10,
        "total_pages": 10
    }
    """
    
    # Récupérer les paramètres de pagination
    page = request.args.get('page', 1, type=int)
    per_page = request.args.get('per_page', 10, type=int)
    
    # Limiter per_page à un maximum (éviter les abus)
    per_page = min(per_page, 100)
    
    # Récupérer les articles avec pagination
    pagination = Article.query.paginate(
        page=page,
        per_page=per_page,
        error_out=False  # Ne pas générer d'erreur si la page n'existe pas
    )
    
    # Construire la réponse
    return jsonify({
        'articles': [article.to_dict() for article in pagination.items],
        'total': pagination.total,
        'page': pagination.page,
        'per_page': pagination.per_page,
        'total_pages': pagination.pages,
        'has_next': pagination.has_next,
        'has_prev': pagination.has_prev
    })


# === PAGINATION CÔTÉ CLIENT (PYTHON) ===

def get_tous_les_articles():
    """
    Récupérer TOUS les articles en gérant la pagination automatiquement
    """
    tous_les_articles = []
    page = 1
    
    while True:
        # Récupérer une page
        response = requests.get(
            f"{BASE_URL}/articles",
            params={'page': page, 'per_page': 50}
        )
        
        data = response.json()
        articles = data['articles']
        
        # Ajouter les articles à la liste
        tous_les_articles.extend(articles)
        
        # Vérifier s'il y a une page suivante
        if not data['has_next']:
            break
        
        page += 1
    
    return tous_les_articles

# Utilisation:
tous_articles = get_tous_les_articles()
print(f"Total d'articles récupérés: {len(tous_articles)}")


# === PAGINATION CÔTÉ CLIENT (JAVASCRIPT) ===

async function getTousLesArticles() {
    let tousLesArticles = [];
    let page = 1;
    let hasNext = true;
    
    while (hasNext) {
        const response = await fetch(
            `${BASE_URL}/articles?page=${page}&per_page=50`
        );
        const data = await response.json();
        
        // Ajouter les articles
        tousLesArticles = tousLesArticles.concat(data.articles);
        
        // Vérifier s'il y a une page suivante
        hasNext = data.has_next;
        page++;
    }
    
    return tousLesArticles;
}

// Utilisation:
getTousLesArticles().then(articles => {
    console.log(`Total d'articles: ${articles.length}`);
});


[OK] FILTRAGE ET RECHERCHE AVANCÉS

# === FILTRAGE CÔTÉ SERVEUR (FLASK) ===

@app.route('/api/articles/recherche', methods=['GET'])
def rechercher_articles():
    """
    Recherche avancée d'articles
    GET /api/articles/recherche?q=python&auteur=Alice&date_min=2024-01-01
    """
    
    # Récupérer les paramètres de recherche
    query_text = request.args.get('q', '')
    auteur = request.args.get('auteur', '')
    date_min = request.args.get('date_min', '')
    date_max = request.args.get('date_max', '')
    
    # Construire la requête de base
    query = Article.query
    
    # Filtrer par texte (titre ou contenu)
    if query_text:
        query = query.filter(
            db.or_(
                Article.titre.ilike(f'%{query_text}%'),
                Article.contenu.ilike(f'%{query_text}%')
            )
        )
    
    # Filtrer par auteur
    if auteur:
        query = query.filter(Article.auteur.ilike(f'%{auteur}%'))
    
    # Filtrer par date minimum
    if date_min:
        date_min_obj = datetime.fromisoformat(date_min)
        query = query.filter(Article.date_creation >= date_min_obj)
    
    # Filtrer par date maximum
    if date_max:
        date_max_obj = datetime.fromisoformat(date_max)
        query = query.filter(Article.date_creation <= date_max_obj)
    
    # Trier par date de création (plus récent en premier)
    query = query.order_by(Article.date_creation.desc())
    
    # Exécuter la requête
    articles = query.all()
    
    return jsonify({
        'resultats': [article.to_dict() for article in articles],
        'nombre': len(articles)
    })


# === FILTRAGE CÔTÉ CLIENT (PYTHON) ===

def rechercher_articles(query_text='', auteur='', date_min='', date_max=''):
    """
    Rechercher des articles avec plusieurs filtres
    """
    params = {}
    
    if query_text:
        params['q'] = query_text
    if auteur:
        params['auteur'] = auteur
    if date_min:
        params['date_min'] = date_min
    if date_max:
        params['date_max'] = date_max
    
    response = requests.get(
        f"{BASE_URL}/articles/recherche",
        params=params
    )
    
    data = response.json()
    return data['resultats']

# Utilisation:
articles_python = rechercher_articles(query_text='python', auteur='Alice')
print(f"Trouvé {len(articles_python)} articles")


# === FILTRAGE CÔTÉ CLIENT (JAVASCRIPT) ===

async function rechercherArticles(queryText = '', auteur = '', dateMin = '', dateMax = '') {
    const params = new URLSearchParams();
    
    if (queryText) params.append('q', queryText);
    if (auteur) params.append('auteur', auteur);
    if (dateMin) params.append('date_min', dateMin);
    if (dateMax) params.append('date_max', dateMax);
    
    const response = await fetch(
        `${BASE_URL}/articles/recherche?${params}`
    );
    
    const data = await response.json();
    return data.resultats;
}

// Utilisation:
const articles = await rechercherArticles('python', 'Alice');
console.log(`Trouvé ${articles.length} articles`);


[OK] TRI (ORDERING)

# === TRI CÔTÉ SERVEUR (FLASK) ===

@app.route('/api/articles', methods=['GET'])
def get_articles_avec_tri():
    """
    GET /api/articles?order_by=date&order=desc
    order_by: titre, auteur, date (date_creation)
    order: asc (ascendant), desc (descendant)
    """
    
    # Récupérer les paramètres de tri
    order_by = request.args.get('order_by', 'date')
    order = request.args.get('order', 'desc')
    
    # Construire la requête de base
    query = Article.query
    
    # Appliquer le tri
    if order_by == 'titre':
        if order == 'asc':
            query = query.order_by(Article.titre.asc())
        else:
            query = query.order_by(Article.titre.desc())
    elif order_by == 'auteur':
        if order == 'asc':
            query = query.order_by(Article.auteur.asc())
        else:
            query = query.order_by(Article.auteur.desc())
    else:  # date par défaut
        if order == 'asc':
            query = query.order_by(Article.date_creation.asc())
        else:
            query = query.order_by(Article.date_creation.desc())
    
    articles = query.all()
    return jsonify([article.to_dict() for article in articles])


# === TRI CÔTÉ CLIENT (PYTHON) ===

def get_articles_tries(order_by='date', order='desc'):
    """
    Récupérer les articles triés
    """
    params = {
        'order_by': order_by,
        'order': order
    }
    
    response = requests.get(
        f"{BASE_URL}/articles",
        params=params
    )
    
    return response.json()

# Utilisation:
articles_par_titre = get_articles_tries(order_by='titre', order='asc')
articles_recents = get_articles_tries(order_by='date', order='desc')


[OK] RELATIONS ENTRE RESSOURCES (NESTED RESOURCES)

# === RELATIONS DANS LES MODÈLES (models.py) ===

"""
Ajouter une relation entre Utilisateur et Article
Un utilisateur peut avoir plusieurs articles
"""

from datetime import datetime
from app import db

class Utilisateur(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    nom = db.Column(db.String(100), nullable=False, unique=True)
    email = db.Column(db.String(100), nullable=False, unique=True)
    age = db.Column(db.Integer)
    
    # Relation: un utilisateur a plusieurs articles
    articles = db.relationship('Article', backref='utilisateur_obj', lazy=True)
    
    def to_dict(self, include_articles=False):
        data = {
            'id': self.id,
            'nom': self.nom,
            'email': self.email,
            'age': self.age
        }
        
        # Optionnellement inclure les articles
        if include_articles:
            data['articles'] = [article.to_dict() for article in self.articles]
        
        return data


class Article(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    titre = db.Column(db.String(200), nullable=False)
    contenu = db.Column(db.Text, nullable=False)
    date_creation = db.Column(db.DateTime, default=datetime.now)
    
    # Clé étrangère vers Utilisateur
    utilisateur_id = db.Column(db.Integer, db.ForeignKey('utilisateur.id'), nullable=False)
    
    def to_dict(self, include_utilisateur=False):
        data = {
            'id': self.id,
            'titre': self.titre,
            'contenu': self.contenu,
            'date_creation': self.date_creation.isoformat(),
            'utilisateur_id': self.utilisateur_id
        }
        
        # Optionnellement inclure l'utilisateur
        if include_utilisateur:
            data['utilisateur'] = self.utilisateur_obj.to_dict()
        
        return data


# === ENDPOINTS POUR RELATIONS (app.py) ===

@app.route('/api/utilisateurs/<int:user_id>/articles', methods=['GET'])
def get_articles_utilisateur(user_id):
    """
    Récupérer tous les articles d'un utilisateur
    GET /api/utilisateurs/1/articles
    """
    
    # Vérifier que l'utilisateur existe
    utilisateur = Utilisateur.query.get_or_404(user_id)
    
    # Récupérer ses articles
    articles = Article.query.filter_by(utilisateur_id=user_id).all()
    
    return jsonify({
        'utilisateur': utilisateur.to_dict(),
        'articles': [article.to_dict() for article in articles],
        'nombre': len(articles)
    })


@app.route('/api/utilisateurs/<int:user_id>/articles', methods=['POST'])
def creer_article_utilisateur(user_id):
    """
    Créer un article pour un utilisateur spécifique
    POST /api/utilisateurs/1/articles
    Body: {"titre": "...", "contenu": "..."}
    """
    
    # Vérifier que l'utilisateur existe
    utilisateur = Utilisateur.query.get_or_404(user_id)
    
    # Récupérer les données
    data = request.get_json()
    
    if not data or not all(key in data for key in ['titre', 'contenu']):
        return jsonify({'erreur': 'Champs manquants'}), 400
    
    # Créer l'article
    nouvel_article = Article(
        titre=data['titre'],
        contenu=data['contenu'],
        utilisateur_id=user_id
    )
    
    db.session.add(nouvel_article)
    db.session.commit()
    
    return jsonify(nouvel_article.to_dict(include_utilisateur=True)), 201


@app.route('/api/articles/<int:article_id>', methods=['GET'])
def get_article_avec_utilisateur(article_id):
    """
    Récupérer un article avec les informations de son utilisateur
    GET /api/articles/1?include_utilisateur=true
    """
    
    # Récupérer l'article
    article = Article.query.get_or_404(article_id)
    
    # Vérifier si on doit inclure l'utilisateur
    include_utilisateur = request.args.get('include_utilisateur', 'false').lower() == 'true'
    
    return jsonify(article.to_dict(include_utilisateur=include_utilisateur))


@app.route('/api/utilisateurs/<int:user_id>/articles/<int:article_id>', methods=['GET'])
def get_article_specifique_utilisateur(user_id, article_id):
    """
    Récupérer un article spécifique d'un utilisateur
    GET /api/utilisateurs/1/articles/5
    """
    
    # Vérifier que l'utilisateur existe
    utilisateur = Utilisateur.query.get_or_404(user_id)
    
    # Récupérer l'article qui appartient à cet utilisateur
    article = Article.query.filter_by(
        id=article_id,
        utilisateur_id=user_id
    ).first_or_404()
    
    return jsonify(article.to_dict(include_utilisateur=True))


@app.route('/api/utilisateurs/<int:user_id>/articles/<int:article_id>', methods=['PUT'])
def modifier_article_utilisateur(user_id, article_id):
    """
    Modifier un article d'un utilisateur spécifique
    PUT /api/utilisateurs/1/articles/5
    Body: {"titre": "Nouveau titre", "contenu": "Nouveau contenu"}
    """
    
    # Vérifier que l'utilisateur existe
    utilisateur = Utilisateur.query.get_or_404(user_id)
    
    # Récupérer l'article
    article = Article.query.filter_by(
        id=article_id,
        utilisateur_id=user_id
    ).first_or_404()
    
    # Récupérer les données
    data = request.get_json()
    
    if not data:
        return jsonify({'erreur': 'Données manquantes'}), 400
    
    # Modifier l'article
    if 'titre' in data:
        article.titre = data['titre']
    if 'contenu' in data:
        article.contenu = data['contenu']
    
    db.session.commit()
    
    return jsonify(article.to_dict(include_utilisateur=True))


@app.route('/api/utilisateurs/<int:user_id>/articles/<int:article_id>', methods=['DELETE'])
def supprimer_article_utilisateur(user_id, article_id):
    """
    Supprimer un article d'un utilisateur spécifique
    DELETE /api/utilisateurs/1/articles/5
    """
    
    # Vérifier que l'utilisateur existe
    utilisateur = Utilisateur.query.get_or_404(user_id)
    
    # Récupérer l'article
    article = Article.query.filter_by(
        id=article_id,
        utilisateur_id=user_id
    ).first_or_404()
    
    db.session.delete(article)
    db.session.commit()
    
    return jsonify({'message': 'Article supprimé avec succès'}), 200


# === RELATIONS MULTIPLES: COMMENTAIRES ===

"""
Ajouter un modèle Commentaire lié aux Articles
Un article peut avoir plusieurs commentaires
"""

class Commentaire(db.Model):
    """
    Modèle pour les commentaires d'articles
    """
    id = db.Column(db.Integer, primary_key=True)
    contenu = db.Column(db.Text, nullable=False)
    auteur = db.Column(db.String(100), nullable=False)
    date_creation = db.Column(db.DateTime, default=datetime.now)
    
    # Clé étrangère vers Article
    article_id = db.Column(db.Integer, db.ForeignKey('article.id'), nullable=False)
    
    def to_dict(self):
        return {
            'id': self.id,
            'contenu': self.contenu,
            'auteur': self.auteur,
            'date_creation': self.date_creation.isoformat(),
            'article_id': self.article_id
        }


# Modifier le modèle Article pour inclure les commentaires
class Article(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    titre = db.Column(db.String(200), nullable=False)
    contenu = db.Column(db.Text, nullable=False)
    date_creation = db.Column(db.DateTime, default=datetime.now)
    utilisateur_id = db.Column(db.Integer, db.ForeignKey('utilisateur.id'), nullable=False)
    
    # Relation: un article a plusieurs commentaires
    commentaires = db.relationship('Commentaire', backref='article_obj', lazy=True, cascade='all, delete-orphan')
    
    def to_dict(self, include_utilisateur=False, include_commentaires=False):
        data = {
            'id': self.id,
            'titre': self.titre,
            'contenu': self.contenu,
            'date_creation': self.date_creation.isoformat(),
            'utilisateur_id': self.utilisateur_id
        }
        
        if include_utilisateur:
            data['utilisateur'] = self.utilisateur_obj.to_dict()
        
        if include_commentaires:
            data['commentaires'] = [c.to_dict() for c in self.commentaires]
            data['nombre_commentaires'] = len(self.commentaires)
        
        return data


# === ENDPOINTS POUR COMMENTAIRES ===

@app.route('/api/articles/<int:article_id>/commentaires', methods=['GET'])
def get_commentaires_article(article_id):
    """
    Récupérer tous les commentaires d'un article
    GET /api/articles/1/commentaires
    """
    
    # Vérifier que l'article existe
    article = Article.query.get_or_404(article_id)
    
    # Récupérer les commentaires
    commentaires = Commentaire.query.filter_by(article_id=article_id).all()
    
    return jsonify({
        'article': article.to_dict(),
        'commentaires': [c.to_dict() for c in commentaires],
        'nombre': len(commentaires)
    })


@app.route('/api/articles/<int:article_id>/commentaires', methods=['POST'])
def creer_commentaire(article_id):
    """
    Créer un commentaire sur un article
    POST /api/articles/1/commentaires
    Body: {"contenu": "Super article!", "auteur": "Bob"}
    """
    
    # Vérifier que l'article existe
    article = Article.query.get_or_404(article_id)
    
    # Récupérer les données
    data = request.get_json()
    
    if not data or not all(key in data for key in ['contenu', 'auteur']):
        return jsonify({'erreur': 'Champs manquants: contenu, auteur'}), 400
    
    # Créer le commentaire
    nouveau_commentaire = Commentaire(
        contenu=data['contenu'],
        auteur=data['auteur'],
        article_id=article_id
    )
    
    db.session.add(nouveau_commentaire)
    db.session.commit()
    
    return jsonify(nouveau_commentaire.to_dict()), 201


@app.route('/api/articles/<int:article_id>/commentaires/<int:commentaire_id>', methods=['GET'])
def get_commentaire_specifique(article_id, commentaire_id):
    """
    Récupérer un commentaire spécifique d'un article
    GET /api/articles/1/commentaires/3
    """
    
    # Vérifier que l'article existe
    article = Article.query.get_or_404(article_id)
    
    # Récupérer le commentaire
    commentaire = Commentaire.query.filter_by(
        id=commentaire_id,
        article_id=article_id
    ).first_or_404()
    
    return jsonify(commentaire.to_dict())


@app.route('/api/articles/<int:article_id>/commentaires/<int:commentaire_id>', methods=['DELETE'])
def supprimer_commentaire(article_id, commentaire_id):
    """
    Supprimer un commentaire
    DELETE /api/articles/1/commentaires/3
    """
    
    # Vérifier que l'article existe
    article = Article.query.get_or_404(article_id)
    
    # Récupérer le commentaire
    commentaire = Commentaire.query.filter_by(
        id=commentaire_id,
        article_id=article_id
    ).first_or_404()
    
    db.session.delete(commentaire)
    db.session.commit()
    
    return jsonify({'message': 'Commentaire supprimé avec succès'}), 200


[OK] AUTHENTIFICATION ET SÉCURITÉ AVANCÉE

# === QU'EST-CE QUE L'AUTHENTIFICATION? ===

# Imagine que tu as une API qui gère des informations personnelles
# Problème: N'importe qui peut accéder à /api/utilisateurs !
# Danger: Tout le monde peut voir/modifier/supprimer les données!

# Solution: L'AUTHENTIFICATION
# = Vérifier QUI accède à l'API
# = Donner un "laissez-passer" seulement aux personnes autorisées

# Analogie:
# Sans authentification = Maison sans porte (n'importe qui entre!)
# Avec authentification = Maison avec serrure (seulement ceux avec la clé!)


# === MÉTHODE 1: API KEY (LA PLUS SIMPLE) ===

# Concept:
# 1. Tu génères une clé secrète unique (ex: "sk_live_abc123xyz")
# 2. Le client inclut cette clé dans CHAQUE requête
# 3. Le serveur vérifie: "Cette clé est-elle valide?"
# 4. Si OUI: accès autorisé. Si NON: 401 Unauthorized

# === GÉNÉRER UNE API KEY (Flask) ===

"""
Fichier: auth.py - Système d'API Key
"""

import secrets
import hashlib
from datetime import datetime
from app import db

class APIKey(db.Model):
    """
    Modèle pour stocker les API keys
    """
    id = db.Column(db.Integer, primary_key=True)
    nom = db.Column(db.String(100), nullable=False)  # Nom de l'app/utilisateur
    key_hash = db.Column(db.String(256), nullable=False, unique=True)  # Hash de la clé
    key_prefix = db.Column(db.String(20))  # Préfixe pour identifier (ex: "sk_live_")
    date_creation = db.Column(db.DateTime, default=datetime.now)
    active = db.Column(db.Boolean, default=True)
    
    @staticmethod
    def generer_cle():
        """
        Générer une nouvelle API key
        Format: sk_live_XXXXXXXXXXXXXXXXXXXX (32 caractères aléatoires)
        """
        # Générer une chaîne aléatoire sécurisée
        random_part = secrets.token_urlsafe(32)
        
        # Créer la clé complète avec préfixe
        api_key = f"sk_live_{random_part}"
        
        return api_key
    
    @staticmethod
    def hasher_cle(api_key):
        """
        Hasher la clé pour la stocker en sécurité
        On ne stocke JAMAIS la clé en clair!
        """
        return hashlib.sha256(api_key.encode()).hexdigest()
    
    def verifier_cle(self, api_key):
        """
        Vérifier si une clé correspond à ce hash
        """
        return self.key_hash == self.hasher_cle(api_key)


# === CRÉER UNE API KEY (Endpoint) ===

"""
Ajouter dans app.py
"""

@app.route('/api/auth/creer-cle', methods=['POST'])
def creer_api_key():
    """
    Créer une nouvelle API key
    POST /api/auth/creer-cle
    Body: {"nom": "Mon Application"}
    
    ATTENTION: Cette route devrait être protégée en production!
    (Seulement les admins peuvent créer des clés)
    """
    
    data = request.get_json()
    
    if not data or 'nom' not in data:
        return jsonify({'erreur': 'Nom requis'}), 400
    
    # Générer une nouvelle clé
    api_key = APIKey.generer_cle()
    
    # Créer l'enregistrement
    nouvelle_cle = APIKey(
        nom=data['nom'],
        key_hash=APIKey.hasher_cle(api_key),
        key_prefix=api_key[:8]  # Stocker seulement le préfixe
    )
    
    db.session.add(nouvelle_cle)
    db.session.commit()
    
    # IMPORTANT: Retourner la clé UNE SEULE FOIS!
    # L'utilisateur doit la sauvegarder car on ne peut plus la récupérer
    return jsonify({
        'api_key': api_key,
        'message': 'IMPORTANT: Sauvegardez cette clé! Vous ne pourrez plus la voir.',
        'nom': nouvelle_cle.nom,
        'date_creation': nouvelle_cle.date_creation.isoformat()
    }), 201


# === MIDDLEWARE POUR VÉRIFIER L'API KEY ===

"""
Décorateur pour protéger les routes
"""

from functools import wraps
from flask import request, jsonify

def api_key_requise(f):
    """
    Décorateur qui vérifie l'API key avant d'autoriser l'accès
    
    Utilisation:
    @app.route('/api/protected')
    @api_key_requise
    def route_protegee():
        return jsonify({'message': 'Accès autorisé!'})
    """
    
    @wraps(f)
    def fonction_decoree(*args, **kwargs):
        # Récupérer l'API key depuis le header Authorization
        auth_header = request.headers.get('Authorization')
        
        if not auth_header:
            return jsonify({'erreur': 'API key manquante'}), 401
        
        # Format attendu: "ApiKey sk_live_XXXX"
        try:
            type_auth, api_key = auth_header.split(' ', 1)
            
            if type_auth != 'ApiKey':
                return jsonify({'erreur': 'Format d\'authentification invalide'}), 401
        
        except ValueError:
            return jsonify({'erreur': 'Format Authorization invalide'}), 401
        
        # Hasher la clé fournie
        key_hash = APIKey.hasher_cle(api_key)
        
        # Chercher dans la base de données
        cle_db = APIKey.query.filter_by(key_hash=key_hash, active=True).first()
        
        if not cle_db:
            return jsonify({'erreur': 'API key invalide ou désactivée'}), 401
        
        # Clé valide! Continuer avec la fonction
        return f(*args, **kwargs)
    
    return fonction_decoree


# === EXEMPLE D'UTILISATION ===

@app.route('/api/utilisateurs', methods=['GET'])
@api_key_requise  # Cette route est maintenant protégée!
def get_utilisateurs_protege():
    """
    Cette route nécessite une API key valide
    """
    utilisateurs = Utilisateur.query.all()
    return jsonify([u.to_dict() for u in utilisateurs])


@app.route('/api/public/info', methods=['GET'])
def info_publique():
    """
    Cette route est publique (pas de @api_key_requise)
    """
    return jsonify({
        'message': 'Ceci est une route publique',
        'version': '1.0'
    })


# === UTILISER L'API KEY DEPUIS UN CLIENT (PYTHON) ===

import requests

# Ton API key (à garder SECRET!)
API_KEY = "sk_live_abc123xyz456..."

# Inclure l'API key dans TOUTES les requêtes
headers = {
    'Authorization': f'ApiKey {API_KEY}',
    'Content-Type': 'application/json'
}

# Faire une requête protégée
response = requests.get(
    'http://localhost:5000/api/utilisateurs',
    headers=headers
)

if response.status_code == 200:
    utilisateurs = response.json()
    print(f"Récupéré {len(utilisateurs)} utilisateurs")
elif response.status_code == 401:
    print("Erreur: API key invalide ou manquante")
else:
    print(f"Erreur: {response.status_code}")


# === UTILISER L'API KEY DEPUIS UN CLIENT (JAVASCRIPT) ===

const API_KEY = "sk_live_abc123xyz456...";

async function getUtilisateurs() {
    try {
        const response = await fetch('http://localhost:5000/api/utilisateurs', {
            headers: {
                'Authorization': `ApiKey ${API_KEY}`,
                'Content-Type': 'application/json'
            }
        });
        
        if (response.status === 401) {
            console.error('API key invalide');
            return null;
        }
        
        if (!response.ok) {
            throw new Error(`Erreur HTTP: ${response.status}`);
        }
        
        const utilisateurs = await response.json();
        return utilisateurs;
        
    } catch (error) {
        console.error('Erreur:', error);
        return null;
    }
}


# === MÉTHODE 2: JWT (JSON WEB TOKEN) - PLUS AVANCÉ ===

# Concept JWT:
# 1. Utilisateur se connecte avec email/mot de passe
# 2. Serveur génère un TOKEN (chaîne encodée)
# 3. Token contient: user_id, date expiration, permissions
# 4. Client envoie ce token dans CHAQUE requête
# 5. Serveur décode le token et vérifie sa validité

# Avantages JWT vs API Key:
# - Token expire automatiquement (ex: 24h)
# - Token contient des infos (user_id, rôle, etc)
# - Plus sécurisé pour les applications web/mobiles

# === INSTALLER PyJWT ===

# Dans le terminal (venv activé):
pip install pyjwt

# Ajouter à requirements.txt:
pip freeze > requirements.txt


# === CRÉER UN SYSTÈME JWT (Flask) ===

"""
Fichier: jwt_auth.py - Système JWT
"""

import jwt
import os
from datetime import datetime, timedelta
from functools import wraps
from flask import request, jsonify
from werkzeug.security import generate_password_hash, check_password_hash
from app import db

# Clé secrète pour signer les tokens (à garder SECRET!)
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-key-change-this')

# Durée de validité du token (1 jour)
TOKEN_EXPIRATION = timedelta(days=1)


class Utilisateur(db.Model):
    """
    Modèle Utilisateur avec authentification
    """
    id = db.Column(db.Integer, primary_key=True)
    nom = db.Column(db.String(100), nullable=False)
    email = db.Column(db.String(100), nullable=False, unique=True)
    mot_de_passe_hash = db.Column(db.String(256), nullable=False)
    role = db.Column(db.String(20), default='user')  # 'user' ou 'admin'
    
    def definir_mot_de_passe(self, mot_de_passe):
        """
        Hasher et stocker le mot de passe
        JAMAIS stocker le mot de passe en clair!
        """
        self.mot_de_passe_hash = generate_password_hash(mot_de_passe)
    
    def verifier_mot_de_passe(self, mot_de_passe):
        """
        Vérifier si le mot de passe est correct
        """
        return check_password_hash(self.mot_de_passe_hash, mot_de_passe)
    
    def generer_token(self):
        """
        Générer un token JWT pour cet utilisateur
        """
        payload = {
            'user_id': self.id,
            'email': self.email,
            'role': self.role,
            'exp': datetime.utcnow() + TOKEN_EXPIRATION,  # Date d'expiration
            'iat': datetime.utcnow()  # Date de création
        }
        
        # Créer le token
        token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
        return token
    
    @staticmethod
    def verifier_token(token):
        """
        Décoder et vérifier un token JWT
        Retourne: user_id si valide, None sinon
        """
        try:
            # Décoder le token
            payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
            
            # Récupérer l'utilisateur
            user_id = payload['user_id']
            return user_id
            
        except jwt.ExpiredSignatureError:
            # Token expiré
            return None
        except jwt.InvalidTokenError:
            # Token invalide
            return None
    
    def to_dict(self):
        return {
            'id': self.id,
            'nom': self.nom,
            'email': self.email,
            'role': self.role
        }


# === ENDPOINTS D'AUTHENTIFICATION ===

"""
Ajouter dans app.py
"""

@app.route('/api/auth/inscription', methods=['POST'])
def inscription():
    """
    Créer un nouveau compte utilisateur
    POST /api/auth/inscription
    Body:
    {
        "nom": "Alice",
        "email": "alice@example.com",
        "mot_de_passe": "motdepasse123"
    }
    """
    
    data = request.get_json()
    
    # Validation
    if not data or not all(key in data for key in ['nom', 'email', 'mot_de_passe']):
        return jsonify({'erreur': 'Champs manquants'}), 400
    
    # Vérifier si l'email existe déjà
    if Utilisateur.query.filter_by(email=data['email']).first():
        return jsonify({'erreur': 'Email déjà utilisé'}), 409
    
    # Créer l'utilisateur
    nouvel_utilisateur = Utilisateur(
        nom=data['nom'],
        email=data['email']
    )
    nouvel_utilisateur.definir_mot_de_passe(data['mot_de_passe'])
    
    db.session.add(nouvel_utilisateur)
    db.session.commit()
    
    # Générer un token
    token = nouvel_utilisateur.generer_token()
    
    return jsonify({
        'message': 'Compte créé avec succès',
        'token': token,
        'utilisateur': nouvel_utilisateur.to_dict()
    }), 201


@app.route('/api/auth/connexion', methods=['POST'])
def connexion():
    """
    Se connecter et obtenir un token
    POST /api/auth/connexion
    Body:
    {
        "email": "alice@example.com",
        "mot_de_passe": "motdepasse123"
    }
    """
    
    data = request.get_json()
    
    # Validation
    if not data or not all(key in data for key in ['email', 'mot_de_passe']):
        return jsonify({'erreur': 'Email et mot de passe requis'}), 400
    
    # Chercher l'utilisateur
    utilisateur = Utilisateur.query.filter_by(email=data['email']).first()
    
    # Vérifier les credentials
    if not utilisateur or not utilisateur.verifier_mot_de_passe(data['mot_de_passe']):
        return jsonify({'erreur': 'Email ou mot de passe incorrect'}), 401
    
    # Générer un token
    token = utilisateur.generer_token()
    
    return jsonify({
        'message': 'Connexion réussie',
        'token': token,
        'utilisateur': utilisateur.to_dict()
    }), 200


# === MIDDLEWARE JWT ===

def token_requis(f):
    """
    Décorateur pour protéger les routes avec JWT
    """
    
    @wraps(f)
    def fonction_decoree(*args, **kwargs):
        # Récupérer le token depuis le header
        auth_header = request.headers.get('Authorization')
        
        if not auth_header:
            return jsonify({'erreur': 'Token manquant'}), 401
        
        # Format attendu: "Bearer eyJhbGciOiJIUzI1NiIs..."
        try:
            type_auth, token = auth_header.split(' ', 1)
            
            if type_auth != 'Bearer':
                return jsonify({'erreur': 'Format d\'authentification invalide'}), 401
        
        except ValueError:
            return jsonify({'erreur': 'Format Authorization invalide'}), 401
        
        # Vérifier le token
        user_id = Utilisateur.verifier_token(token)
        
        if not user_id:
            return jsonify({'erreur': 'Token invalide ou expiré'}), 401
        
        # Récupérer l'utilisateur
        utilisateur_courant = Utilisateur.query.get(user_id)
        
        if not utilisateur_courant:
            return jsonify({'erreur': 'Utilisateur non trouvé'}), 401
        
        # Passer l'utilisateur à la fonction
        return f(utilisateur_courant, *args, **kwargs)
    
    return fonction_decoree


# === MIDDLEWARE RÔLE ADMIN ===

def admin_requis(f):
    """
    Décorateur pour routes réservées aux admins
    """
    
    @wraps(f)
    @token_requis
    def fonction_decoree(utilisateur_courant, *args, **kwargs):
        if utilisateur_courant.role != 'admin':
            return jsonify({'erreur': 'Accès réservé aux administrateurs'}), 403
        
        return f(utilisateur_courant, *args, **kwargs)
    
    return fonction_decoree


# === EXEMPLE DE ROUTES PROTÉGÉES ===

@app.route('/api/profil', methods=['GET'])
@token_requis
def mon_profil(utilisateur_courant):
    """
    Voir son propre profil
    Nécessite un token JWT valide
    """
    return jsonify(utilisateur_courant.to_dict())


@app.route('/api/profil', methods=['PATCH'])
@token_requis
def modifier_profil(utilisateur_courant):
    """
    Modifier son propre profil
    """
    data = request.get_json()
    
    if 'nom' in data:
        utilisateur_courant.nom = data['nom']
    
    db.session.commit()
    
    return jsonify(utilisateur_courant.to_dict())


@app.route('/api/admin/utilisateurs', methods=['GET'])
@admin_requis
def liste_utilisateurs_admin(utilisateur_courant):
    """
    Liste de tous les utilisateurs (admin seulement)
    """
    utilisateurs = Utilisateur.query.all()
    return jsonify([u.to_dict() for u in utilisateurs])


# === UTILISER JWT DEPUIS UN CLIENT (PYTHON) ===

import requests

# === ÉTAPE 1: Inscription ===

response = requests.post(
    'http://localhost:5000/api/auth/inscription',
    json={
        'nom': 'Alice',
        'email': 'alice@example.com',
        'mot_de_passe': 'motdepasse123'
    }
)

if response.status_code == 201:
    data = response.json()
    token = data['token']
    print(f"Compte créé! Token: {token[:20]}...")
else:
    print(f"Erreur: {response.json()}")


# === ÉTAPE 2: Connexion ===

response = requests.post(
    'http://localhost:5000/api/auth/connexion',
    json={
        'email': 'alice@example.com',
        'mot_de_passe': 'motdepasse123'
    }
)

if response.status_code == 200:
    data = response.json()
    token = data['token']
    print(f"Connecté! Token: {token[:20]}...")
else:
    print(f"Erreur: {response.json()}")


# === ÉTAPE 3: Utiliser le token pour accéder aux routes protégées ===

# IMPORTANT: Sauvegarder le token (variable, fichier, base de données)
TOKEN = token  # Token obtenu lors de la connexion

# Inclure le token dans le header Authorization
headers = {
    'Authorization': f'Bearer {TOKEN}',
    'Content-Type': 'application/json'
}

# Accéder à une route protégée
response = requests.get(
    'http://localhost:5000/api/profil',
    headers=headers
)

if response.status_code == 200:
    profil = response.json()
    print(f"Profil: {profil}")
elif response.status_code == 401:
    print("Token invalide ou expiré")


# === CLASSE CLIENT POUR SIMPLIFIER ===

class APIClient:
    """
    Client Python pour faciliter l'utilisation de l'API
    """
    
    def __init__(self, base_url):
        self.base_url = base_url
        self.token = None
    
    def inscription(self, nom, email, mot_de_passe):
        """S'inscrire et stocker le token"""
        response = requests.post(
            f"{self.base_url}/api/auth/inscription",
            json={
                'nom': nom,
                'email': email,
                'mot_de_passe': mot_de_passe
            }
        )
        
        if response.status_code == 201:
            data = response.json()
            self.token = data['token']
            return data['utilisateur']
        else:
            raise Exception(f"Erreur inscription: {response.json()}")
    
    def connexion(self, email, mot_de_passe):
        """Se connecter et stocker le token"""
        response = requests.post(
            f"{self.base_url}/api/auth/connexion",
            json={
                'email': email,
                'mot_de_passe': mot_de_passe
            }
        )
        
        if response.status_code == 200:
            data = response.json()
            self.token = data['token']
            return data['utilisateur']
        else:
            raise Exception(f"Erreur connexion: {response.json()}")
    
    def _get_headers(self):
        """Obtenir les headers avec le token"""
        if not self.token:
            raise Exception("Non connecté! Appelez connexion() d'abord")
        
        return {
            'Authorization': f'Bearer {self.token}',
            'Content-Type': 'application/json'
        }
    
    def get_profil(self):
        """Récupérer son profil"""
        response = requests.get(
            f"{self.base_url}/api/profil",
            headers=self._get_headers()
        )
        
        if response.status_code == 200:
            return response.json()
        else:
            raise Exception(f"Erreur: {response.json()}")
    
    def get(self, endpoint):
        """Faire une requête GET"""
        response = requests.get(
            f"{self.base_url}{endpoint}",
            headers=self._get_headers()
        )
        return response.json()
    
    def post(self, endpoint, data):
        """Faire une requête POST"""
        response = requests.post(
            f"{self.base_url}{endpoint}",
            json=data,
            headers=self._get_headers()
        )
        return response.json()


# === UTILISATION DU CLIENT ===

# Créer le client
client = APIClient('http://localhost:5000')

# S'inscrire
utilisateur = client.inscription('Alice', 'alice@example.com', 'pass123')
print(f"Inscrit: {utilisateur}")

# Voir son profil
profil = client.get_profil()
print(f"Profil: {profil}")

# Utiliser d'autres routes
articles = client.get('/api/articles')
print(f"Articles: {len(articles)}")


# === UTILISER JWT DEPUIS UN CLIENT (JAVASCRIPT) ===

// === CLASSE CLIENT JAVASCRIPT ===

class APIClient {
    constructor(baseURL) {
        this.baseURL = baseURL;
        this.token = null;
    }
    
    async inscription(nom, email, motDePasse) {
        const response = await fetch(`${this.baseURL}/api/auth/inscription`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                nom: nom,
                email: email,
                mot_de_passe: motDePasse
            })
        });
        
        if (response.status === 201) {
            const data = await response.json();
            this.token = data.token;
            
            // Sauvegarder le token dans localStorage
            localStorage.setItem('token', this.token);
            
            return data.utilisateur;
        } else {
            const error = await response.json();
            throw new Error(`Erreur inscription: ${error.erreur}`);
        }
    }
    
    async connexion(email, motDePasse) {
        const response = await fetch(`${this.baseURL}/api/auth/connexion`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                email: email,
                mot_de_passe: motDePasse
            })
        });
        
        if (response.status === 200) {
            const data = await response.json();
            this.token = data.token;
            
            // Sauvegarder le token
            localStorage.setItem('token', this.token);
            
            return data.utilisateur;
        } else {
            const error = await response.json();
            throw new Error(`Erreur connexion: ${error.erreur}`);
        }
    }
    
    deconnexion() {
        this.token = null;
        localStorage.removeItem('token');
    }
    
    chargerToken() {
        this.token = localStorage.getItem('token');
        return this.token !== null;
    }
    
    getHeaders() {
        if (!this.token) {
            throw new Error('Non connecté!');
        }
        
        return {
            'Authorization': `Bearer ${this.token}`,
            'Content-Type': 'application/json'
        };
    }
    
    async get(endpoint) {
        const response = await fetch(`${this.baseURL}${endpoint}`, {
            headers: this.getHeaders()
        });
        
        if (response.status === 401) {
            this.deconnexion();
            throw new Error('Token expiré, reconnectez-vous');
        }
        
        if (!response.ok) {
            throw new Error(`Erreur HTTP: ${response.status}`);
        }
        
        return await response.json();
    }
    
    async post(endpoint, data) {
        const response = await fetch(`${this.baseURL}${endpoint}`, {
            method: 'POST',
            headers: this.getHeaders(),
            body: JSON.stringify(data)
        });
        
        if (response.status === 401) {
            this.deconnexion();
            throw new Error('Token expiré, reconnectez-vous');
        }
        
        if (!response.ok) {
            throw new Error(`Erreur HTTP: ${response.status}`);
        }
        
        return await response.json();
    }
    
    async getProfil() {
        return await this.get('/api/profil');
    }
}

// === UTILISATION ===

const client = new APIClient('http://localhost:5000');

// S'inscrire
try {
    const utilisateur = await client.inscription(
        'Alice',
        'alice@example.com',
        'pass123'
    );
    console.log('Inscrit:', utilisateur);
} catch (error) {
    console.error(error);
}

// Se connecter
try {
    const utilisateur = await client.connexion(
        'alice@example.com',
        'pass123'
    );
    console.log('Connecté:', utilisateur);
} catch (error) {
    console.error(error);
}

// Voir son profil
try {
    const profil = await client.getProfil();
    console.log('Profil:', profil);
} catch (error) {
    console.error(error);
}

// Au chargement de la page: charger le token
if (client.chargerToken()) {
    console.log('Token trouvé, utilisateur déjà connecté');
} else {
    console.log('Pas de token, utilisateur non connecté');
}


[OK] VERSIONING DE L'API

# === POURQUOI VERSIONNER UNE API? ===

# Imagine que ton API est utilisée par 1000 applications
# Tu veux changer le format des données (breaking change)
# Problème: Toutes les apps vont casser!

# Solution: VERSIONING
# = Garder plusieurs versions de l'API en parallèle
# = Les anciennes apps utilisent v1, les nouvelles apps utilisent v2

# Exemple:
# /api/v1/utilisateurs (ancienne version)
# /api/v2/utilisateurs (nouvelle version)


# === MÉTHODE 1: VERSIONING PAR URL ===

"""
Structure:
/api/v1/utilisateurs
/api/v2/utilisateurs
"""

from flask import Blueprint

# Créer un blueprint pour la version 1
api_v1 = Blueprint('api_v1', __name__, url_prefix='/api/v1')

@api_v1.route('/utilisateurs', methods=['GET'])
def get_utilisateurs_v1():
    """
    Version 1: Retourne seulement id et nom
    """
    utilisateurs = Utilisateur.query.all()
    return jsonify([
        {'id': u.id, 'nom': u.nom}
        for u in utilisateurs
    ])


@api_v1.route('/utilisateurs/<int:id>', methods=['GET'])
def get_utilisateur_v1(id):
    """
    Version 1: Format simple
    """
    utilisateur = Utilisateur.query.get_or_404(id)
    return jsonify({
        'id': utilisateur.id,
        'nom': utilisateur.nom
    })


# Créer un blueprint pour la version 2
api_v2 = Blueprint('api_v2', __name__, url_prefix='/api/v2')

@api_v2.route('/utilisateurs', methods=['GET'])
def get_utilisateurs_v2():
    """
    Version 2: Retourne plus de détails + pagination
    """
    page = request.args.get('page', 1, type=int)
    per_page = request.args.get('per_page', 10, type=int)
    
    pagination = Utilisateur.query.paginate(
        page=page,
        per_page=per_page,
        error_out=False
    )
    
    return jsonify({
        'utilisateurs': [
            {
                'id': u.id,
                'nom': u.nom,
                'email': u.email,
                'age': u.age,
                'date_creation': u.date_creation.isoformat() if hasattr(u, 'date_creation') else None
            }
            for u in pagination.items
        ],
        'total': pagination.total,
        'page': pagination.page,
        'per_page': pagination.per_page,
        'total_pages': pagination.pages
    })

@api_v2.route('/utilisateurs/<int:id>', methods=['GET'])
def get_utilisateur_v2(id):
    """
    Version 2: Format enrichi
    """
    utilisateur = Utilisateur.query.get_or_404(id)
    return jsonify({
        'id': utilisateur.id,
        'nom': utilisateur.nom,
        'email': utilisateur.email,
        'age': utilisateur.age,
        'profil': {
            'date_creation': utilisateur.date_creation.isoformat() if hasattr(utilisateur, 'date_creation') else None,
            'role': utilisateur.role if hasattr(utilisateur, 'role') else 'user'
        }
    })


# Enregistrer les blueprints dans l'app
app.register_blueprint(api_v1)
app.register_blueprint(api_v2)


# === EXEMPLE UTILISATION CLIENT (PYTHON) ===

# Utiliser la version 1
response_v1 = requests.get('http://localhost:5000/api/v1/utilisateurs')
utilisateurs_v1 = response_v1.json()
print("Version 1:", utilisateurs_v1)
# Affiche: [{"id": 1, "nom": "Alice"}, ...]

# Utiliser la version 2
response_v2 = requests.get('http://localhost:5000/api/v2/utilisateurs')
data_v2 = response_v2.json()
print("Version 2:", data_v2)
# Affiche: {"utilisateurs": [...], "total": 10, "page": 1, ...}


# === MÉTHODE 2: VERSIONING PAR HEADER ===

Le client spécifie la version dans un header
Header: API-Version: 1 ou API-Version: 2

"""
Alternative: Utiliser un header custom
Accept: application/vnd.myapi.v1+json
Accept: application/vnd.myapi.v2+json
"""

@app.route('/api/utilisateurs', methods=['GET'])
def get_utilisateurs_versioned():
    """
    Route unique qui détecte la version via header
    """
    
    # Récupérer le header Accept
    accept_header = request.headers.get('Accept', 'application/json')
    
    # Déterminer la version
    if 'vnd.myapi.v2' in accept_header:
        # Version 2: format enrichi
        utilisateurs = Utilisateur.query.all()
        return jsonify({
            'utilisateurs': [u.to_dict_v2() for u in utilisateurs],
            'version': 2
        })
    else:
        # Version 1 par défaut: format simple
        utilisateurs = Utilisateur.query.all()
        return jsonify([u.to_dict_v1() for u in utilisateurs])


# Ajouter les méthodes dans le modèle
class Utilisateur(db.Model):
    # ... (colonnes existantes)
    
    def to_dict_v1(self):
        """Format version 1: simple"""
        return {
            'id': self.id,
            'nom': self.nom
        }
    
    def to_dict_v2(self):
        """Format version 2: enrichi"""
        return {
            'id': self.id,
            'nom': self.nom,
            'email': self.email,
            'age': self.age,
            'profil': {
                'role': self.role if hasattr(self, 'role') else 'user'
            }
        }


# === UTILISATION CLIENT AVEC HEADER (PYTHON) ===

# Version 1 (header par défaut)
response = requests.get(
    'http://localhost:5000/api/utilisateurs',
    headers={'Accept': 'application/json'}
)

# Version 2 (header spécifique)
response = requests.get(
    'http://localhost:5000/api/utilisateurs',
    headers={'Accept': 'application/vnd.myapi.v2+json'}
)


# === BONNES PRATIQUES VERSIONING ===

# 1. Toujours démarrer avec v1 (même si c'est ta première version)
# 2. Ne JAMAIS supprimer une ancienne version sans prévenir (au moins 6 mois)
# 3. Documenter clairement les différences entre versions
# 4. Indiquer la version par défaut
# 5. Ajouter un header de réponse indiquant la version utilisée

@app.after_request
def add_version_header(response):
    """
    Ajouter un header indiquant la version de l'API
    """
    response.headers['X-API-Version'] = '2.0'
    return response


[OK] RATE LIMITING (LIMITATION DU NOMBRE DE REQUÊTES)

# === QU'EST-CE QUE LE RATE LIMITING? ===

# Problème:
# Un utilisateur malveillant fait 10,000 requêtes par seconde
# Ton serveur tombe en panne!
# Coût: Très cher (serveur surchargé)

# Solution: RATE LIMITING
# = Limiter le nombre de requêtes par utilisateur/IP
# Exemple: 100 requêtes par heure maximum

# Analogie:
# Magasin sans limite = Files infinies, magasin débordé
# Magasin avec limite = "Seulement 50 clients à la fois", tout le monde est servi


# === INSTALLER FLASK-LIMITER ===

# Dans le terminal (venv activé):
pip install Flask-Limiter

# Ajouter à requirements.txt:
pip freeze > requirements.txt


# === CONFIGURATION FLASK-LIMITER ===

"""
Ajouter dans app.py
"""

from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

# Créer le limiter
limiter = Limiter(
    app=app,
    key_func=get_remote_address,  # Limite par IP
    default_limits=["200 per day", "50 per hour"],  # Limites par défaut
    storage_uri="memory://"  # Stockage en mémoire (simple mais volatile)
)

# Pour production, utilise Redis:
# storage_uri="redis://localhost:6379"


# === APPLIQUER DES LIMITES SUR DES ROUTES ===

@app.route('/api/public', methods=['GET'])
@limiter.limit("10 per minute")  # 10 requêtes par minute max
def route_publique():
    """
    Route avec limite stricte pour éviter les abus
    """
    return jsonify({'message': 'Route publique avec rate limiting'})


@app.route('/api/recherche', methods=['GET'])
@limiter.limit("30 per minute")  # 30 requêtes par minute
def recherche_api():
    """
    Route de recherche avec limite moyenne
    """
    query = request.args.get('q', '')
    # ... logique de recherche ...
    return jsonify({'resultats': []})


@app.route('/api/premium', methods=['GET'])
@limiter.exempt  # Pas de limite pour cette route
@token_requis
def route_premium(utilisateur_courant):
    """
    Route premium sans limite (utilisateurs payants)
    """
    return jsonify({'message': 'Accès illimité'})


# === LIMITES DYNAMIQUES BASÉES SUR L'UTILISATEUR ===

def get_limite_utilisateur():
    """
    Retourner une limite différente selon le type d'utilisateur
    """
    # Récupérer le token depuis le header
    auth_header = request.headers.get('Authorization')
    
    if not auth_header:
        # Utilisateur non authentifié: limite basse
        return "10 per minute"
    
    try:
        # Extraire et vérifier le token
        type_auth, token = auth_header.split(' ', 1)
        user_id = Utilisateur.verifier_token(token)
        
        if user_id:
            utilisateur = Utilisateur.query.get(user_id)
            
            # Limite selon le rôle
            if utilisateur.role == 'admin':
                return "1000 per minute"  # Admin: limite très haute
            elif utilisateur.role == 'premium':
                return "200 per minute"  # Premium: limite haute
            else:
                return "50 per minute"  # User normal: limite moyenne
        else:
            return "10 per minute"
    
    except:
        return "10 per minute"


@app.route('/api/articles', methods=['GET'])
@limiter.limit(get_limite_utilisateur)  # Limite dynamique!
def get_articles_limite():
    """
    Route avec limite basée sur le type d'utilisateur
    """
    articles = Article.query.all()
    return jsonify([article.to_dict() for article in articles])


# === VÉRIFIER LES LIMITES RESTANTES ===

def verifier_limites(url):
    """
    Vérifier combien de requêtes restent disponibles
    """
    response = requests.get(url)
    
    limit = response.headers.get('X-RateLimit-Limit')
    remaining = response.headers.get('X-RateLimit-Remaining')
    reset = response.headers.get('X-RateLimit-Reset')
    
    print(f"Limite: {limit}")
    print(f"Restantes: {remaining}")
    print(f"Reset: {reset}")
    
    return int(remaining) if remaining else 0

# === GÉRER LES ERREURS DE RATE LIMITING ===

@app.errorhandler(429)
def ratelimit_handler(e):
    """
    Erreur 429: Too Many Requests
    """
    return jsonify({
        'erreur': 'Trop de requêtes',
        'message': 'Vous avez dépassé votre limite. Réessayez dans quelques minutes.',
        'retry_after': e.description  # Temps d'attente en secondes
    }), 429


# === HEADERS DE RÉPONSE RATE LIMITING ===

# Flask-Limiter ajoute automatiquement des headers:
# X-RateLimit-Limit: 100       (Limite totale)
# X-RateLimit-Remaining: 95    (Requêtes restantes)
# X-RateLimit-Reset: 1672531200  (Timestamp de réinitialisation)

# Exemple lecture des headers (Python client):
response = requests.get('http://localhost:5000/api/articles')

limite = response.headers.get('X-RateLimit-Limit')
restantes = response.headers.get('X-RateLimit-Remaining')
reset = response.headers.get('X-RateLimit-Reset')

print(f"Limite: {limite} requêtes")
print(f"Restantes: {restantes} requêtes")
print(f"Réinitialisation dans: {reset} secondes")


# === EXEMPLES DE SYNTAXE RATE LIMITING ===

# Par minute
@limiter.limit("10 per minute")

# Par heure
@limiter.limit("100 per hour")

# Par jour
@limiter.limit("1000 per day")

# Plusieurs limites combinées
@limiter.limit("10 per minute; 100 per hour; 1000 per day")

# Limites différentes selon la méthode HTTP
@app.route('/api/data', methods=['GET', 'POST'])
@limiter.limit("100 per hour", methods=['GET'])  # GET: 100/h
@limiter.limit("20 per hour", methods=['POST'])  # POST: 20/h
def route_mixte():
    if request.method == 'GET':
        return jsonify({'data': []})
    else:
        return jsonify({'message': 'Créé'}), 201


# === RATE LIMITING AVEC REDIS (PRODUCTION) ===

"""
Redis = Base de données en mémoire ultra-rapide
Parfait pour stocker les compteurs de rate limiting
"""

# Installer Redis:
# macOS: brew install redis
# Linux: sudo apt-get install redis-server
# Windows: https://redis.io/download

# Installer la bibliothèque Python:
pip install redis

# Configuration avec Redis:
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(
    app=app,
    key_func=get_remote_address,
    default_limits=["200 per day", "50 per hour"],
    storage_uri="redis://localhost:6379"  # Connexion Redis
)

# Sur Heroku avec Redis add-on:
import os
redis_url = os.environ.get('REDIS_URL', 'redis://localhost:6379')

limiter = Limiter(
    app=app,
    key_func=get_remote_address,
    default_limits=["200 per day", "50 per hour"],
    storage_uri=redis_url
)


[OK] CORS (CROSS-ORIGIN RESOURCE SHARING)

# === QU'EST-CE QUE CORS? ===

# Problème:
# Tu crées une API sur: http://api.monsite.com
# Ton frontend tourne sur: http://monsite.com
# Le navigateur BLOQUE les requêtes! (Erreur CORS)

# Pourquoi?
# Sécurité du navigateur: empêche un site malveillant de voler tes données

# Solution: CORS
# = Dire au navigateur: "Oui, monsite.com peut accéder à api.monsite.com"

# Analogie:
# Sans CORS = Maison avec porte fermée à clé
# Avec CORS = Maison avec liste d'invités autorisés


# === INSTALLER FLASK-CORS ===

pip install flask-cors
pip freeze > requirements.txt


# === CONFIGURATION CORS (SIMPLE) ===

"""
Autoriser TOUS les domaines (développement seulement!)
"""

from flask_cors import CORS

# Activer CORS pour toutes les routes
CORS(app)

# Maintenant n'importe quel site peut accéder à ton API
# ATTENTION: Dangereux en production!


# === CONFIGURATION CORS (SÉCURISÉE) ===

"""
Autoriser SEULEMENT des domaines spécifiques
"""

from flask_cors import CORS

# Liste des domaines autorisés
ORIGINS_AUTORISEES = [
    'http://localhost:3000',  # Frontend en développement
    'https://monsite.com',    # Frontend en production
    'https://www.monsite.com'
]

CORS(app, origins=ORIGINS_AUTORISEES)


# === CORS SUR DES ROUTES SPÉCIFIQUES ===

"""
Activer CORS seulement pour certaines routes
"""

from flask_cors import cross_origin

@app.route('/api/public', methods=['GET'])
@cross_origin()  # CORS activé pour cette route
def route_publique():
    return jsonify({'message': 'Accessible depuis n\'importe quel domaine'})


@app.route('/api/private', methods=['GET'])
def route_privee():
    # Pas de @cross_origin = CORS désactivé
    return jsonify({'message': 'Accessible seulement depuis le même domaine'})


# === CONFIGURATION CORS AVANCÉE ===

"""
Contrôler en détail les permissions CORS
"""

CORS(app, 
    origins=ORIGINS_AUTORISEES,
    methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],  # Méthodes autorisées
    allow_headers=['Content-Type', 'Authorization'],     # Headers autorisés
    expose_headers=['X-RateLimit-Remaining'],            # Headers exposés au client
    supports_credentials=True,                           # Autoriser les cookies
    max_age=3600                                         # Cache preflight 1h
)


# === COMPRENDRE PREFLIGHT REQUEST ===

# Quand tu fais une requête complexe (POST, PUT, DELETE, headers custom):
# 1. Navigateur envoie d'abord OPTIONS /api/endpoint (preflight)
# 2. Serveur répond: "Oui, cette origine est autorisée"
# 3. Navigateur envoie la vraie requête POST /api/endpoint

# Flask-CORS gère tout ça automatiquement!


# === TESTER CORS DEPUIS LE NAVIGATEUR ===

// JavaScript dans le navigateur
fetch('http://localhost:5000/api/articles', {
    method: 'GET',
    headers: {
        'Content-Type': 'application/json'
    }
})
.then(response => response.json())
.then(data => console.log('Articles:', data))
.catch(error => {
    // Si CORS n'est pas configuré, tu verras:
    // "Access to fetch at 'http://localhost:5000/api/articles' 
    //  from origin 'http://localhost:3000' has been blocked by CORS policy"
    console.error('Erreur CORS:', error);
});


# === CORS AVEC CREDENTIALS (COOKIES/AUTH) ===

"""
Permettre l'envoi de cookies/tokens dans les requêtes cross-origin
"""

CORS(app, 
    origins=['http://localhost:3000'],
    supports_credentials=True  # IMPORTANT pour cookies/auth
)

// JavaScript côté client
fetch('http://localhost:5000/api/profil', {
    method: 'GET',
    credentials: 'include',  // Envoyer les cookies
    headers: {
        'Content-Type': 'application/json'
    }
})


# === DÉBOGUER LES PROBLÈMES CORS ===

# Erreur: "No 'Access-Control-Allow-Origin' header is present"
# Solution: Activer CORS avec flask-cors

# Erreur: "Response to preflight request doesn't pass access control check"
# Solution: Autoriser la méthode HTTP (POST, PUT, DELETE) dans CORS

# Erreur: "Credentials flag is 'true', but the 'Access-Control-Allow-Credentials' header is ''"
# Solution: Ajouter supports_credentials=True


[OK] DOCUMENTATION API (SWAGGER/OPENAPI)

# === POURQUOI DOCUMENTER UNE API? ===

# Sans documentation:
# - Développeurs ne savent pas comment utiliser ton API
# - Doivent deviner les endpoints, paramètres, formats
# - Frustration et erreurs

# Avec documentation:
# - Liste claire de tous les endpoints
# - Exemples de requêtes et réponses
# - Interface interactive pour tester l'API
# - Génération automatique de code client


# === SWAGGER/OPENAPI ===

# OpenAPI = Spécification standard pour documenter les APIs REST
# Swagger = Outils pour créer/afficher la documentation OpenAPI

# Avantages:
# - Documentation automatique depuis le code
# - Interface web interactive (tester l'API sans code!)
# - Génération de clients (Python, JavaScript, etc)


# === INSTALLER FLASGGER ===

# Flasgger = Intégration Swagger pour Flask

pip install flasgger
pip freeze > requirements.txt


# === CONFIGURATION FLASGGER ===

"""
Ajouter dans app.py
"""

from flasgger import Swagger

# Configuration Swagger
swagger_config = {
    "headers": [],
    "specs": [
        {
            "endpoint": 'apispec',
            "route": '/apispec.json',
            "rule_filter": lambda rule: True,
            "model_filter": lambda tag: True,
        }
    ],
    "static_url_path": "/flasgger_static",
    "swagger_ui": True,
    "specs_route": "/docs"  # URL de la documentation
}

swagger_template = {
    "info": {
        "title": "API Blog",
        "description": "API REST pour gérer un blog avec articles et utilisateurs",
        "version": "2.0.0",
        "contact": {
            "name": "Support API",
            "email": "support@monapi.com"
        }
    },
    "securityDefinitions": {
        "Bearer": {
            "type": "apiKey",
            "name": "Authorization",
            "in": "header",
            "description": "JWT Authorization header. Format: 'Bearer {token}'"
        }
    }
}

# Initialiser Swagger
swagger = Swagger(app, config=swagger_config, template=swagger_template)


# === DOCUMENTER UN ENDPOINT ===

@app.route('/api/articles', methods=['GET'])
def get_articles_doc():
    """
    Récupérer tous les articles
    ---
    tags:
      - Articles
    parameters:
      - name: page
        in: query
        type: integer
        required: false
        default: 1
        description: Numéro de la page (pagination)
      - name: per_page
        in: query
        type: integer
        required: false
        default: 10
        description: Nombre d'articles par page
      - name: auteur
        in: query
        type: string
        required: false
        description: Filtrer par nom d'auteur
    responses:
      200:
        description: Liste des articles récupérée avec succès
        schema:
          type: object
          properties:
            articles:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: integer
                    example: 1
                  titre:
                    type: string
                    example: "Mon premier article"
                  contenu:
                    type: string
                    example: "Contenu de l'article..."
                  auteur:
                    type: string
                    example: "Alice"
                  date_creation:
                    type: string
                    format: date-time
                    example: "2024-01-15T10:30:00"
            total:
              type: integer
              example: 100
            page:
              type: integer
              example: 1
            per_page:
              type: integer
              example: 10
      400:
        description: Paramètres invalides
    """
    
    page = request.args.get('page', 1, type=int)
    per_page = request.args.get('per_page', 10, type=int)
    auteur = request.args.get('auteur', type=str)
    
    query = Article.query
    
    if auteur:
        query = query.filter_by(auteur=auteur)
    
    pagination = query.paginate(page=page, per_page=per_page, error_out=False)
    
    return jsonify({
        'articles': [article.to_dict() for article in pagination.items],
        'total': pagination.total,
        'page': pagination.page,
        'per_page': pagination.per_page
    })


@app.route('/api/articles/<int:id>', methods=['GET'])
def get_article_doc(id):
    """
    Récupérer un article spécifique
    ---
    tags:
      - Articles
    parameters:
      - name: id
        in: path
        type: integer
        required: true
        description: ID de l'article
    responses:
      200:
        description: Article récupéré avec succès
        schema:
          type: object
          properties:
            id:
              type: integer
            titre:
              type: string
            contenu:
              type: string
            auteur:
              type: string
            date_creation:
              type: string
              format: date-time
      404:
        description: Article non trouvé
    """
    
    article = Article.query.get_or_404(id)
    return jsonify(article.to_dict())


@app.route('/api/articles', methods=['POST'])
def create_article_doc():
    """
    Créer un nouvel article
    ---
    tags:
      - Articles
    security:
      - Bearer: []
    parameters:
      - name: body
        in: body
        required: true
        schema:
          type: object
          required:
            - titre
            - contenu
            - auteur
          properties:
            titre:
              type: string
              example: "Mon nouvel article"
            contenu:
              type: string
              example: "Contenu détaillé de l'article..."
            auteur:
              type: string
              example: "Bob"
    responses:
      201:
        description: Article créé avec succès
        schema:
          type: object
          properties:
            id:
              type: integer
            titre:
              type: string
            contenu:
              type: string
            auteur:
              type: string
            date_creation:
              type: string
              format: date-time
      400:
        description: Données invalides
      401:
        description: Non authentifié
    """
    
    data = request.get_json()
    
    if not data or not all(key in data for key in ['titre', 'contenu', 'auteur']):
        return jsonify({'erreur': 'Champs manquants'}), 400
    
    nouvel_article = Article(
        titre=data['titre'],
        contenu=data['contenu'],
        auteur=data['auteur']
    )
    
    db.session.add(nouvel_article)
    db.session.commit()
    
    return jsonify(nouvel_article.to_dict()), 201


# === ACCÉDER À LA DOCUMENTATION ===

# Lancer l'app:
# python app.py

# Ouvrir dans le navigateur:
# http://localhost:5000/docs

# Tu verras:
# - Interface Swagger UI interactive
# - Liste de tous les endpoints
# - Possibilité de tester directement depuis le navigateur
# - Exemples de requêtes/réponses


# === TESTER L'API DEPUIS SWAGGER UI ===

# 1. Ouvre http://localhost:5000/docs
# 2. Clique sur un endpoint (ex: GET /api/articles)
# 3. Clique "Try it out"
# 4. Remplis les paramètres (si nécessaire)
# 5. Clique "Execute"
# 6. Voir la réponse en direct!


# === DOCUMENTATION AVEC AUTHENTIFICATION ===

# Pour les routes protégées:

# 1. Clique sur le bouton "Authorize" ([VERROUILLE]) en haut
# 2. Entre ton token JWT: Bearer eyJhbGciOiJIUzI1NiIs...
# 3. Clique "Authorize"
# 4. Maintenant toutes les requêtes incluent le token!


# === GÉNÉRER UN FICHIER OPENAPI.JSON ===

# Le fichier apispec.json est automatiquement généré
# Accessible à: http://localhost:5000/apispec.json

# Tu peux le télécharger et l'utiliser pour:
# - Générer des clients automatiquement
# - Importer dans Postman
# - Partager avec d'autres développeurs

import requests
import json

# Télécharger le spec OpenAPI
response = requests.get('http://localhost:5000/apispec.json')
spec = response.json()

# Sauvegarder dans un fichier
with open('openapi.json', 'w') as f:
    json.dump(spec, f, indent=2)


[OK] TESTING (TESTER TON API)

# === POURQUOI TESTER UNE API? ===

# Sans tests:
# - Tu dois tester manuellement après chaque modification
# - Risque de casser quelque chose sans t'en rendre compte
# - Bugs en production = clients mécontents

# Avec tests:
# - Tests automatiques après chaque modification
# - Confiance que tout fonctionne
# - Détection rapide des bugs


# === INSTALLER PYTEST ===

pip install pytest pytest-flask
pip freeze > requirements.txt


# === STRUCTURE DES TESTS ===

# Créer un dossier tests/:
mkdir tests

# Structure:
# myproject/
# ├── app.py
# ├── models.py
# ├── tests/
# │   ├── __init__.py
# │   ├── conftest.py       (Configuration des tests)
# │   ├── test_articles.py  (Tests pour les articles)
# │   ├── test_utilisateurs.py
# │   └── test_auth.py


# === CONFIGURATION DES TESTS (conftest.py) ===

"""
Fichier: tests/conftest.py
"""

import pytest
from app import app, db
from models import Article, Utilisateur

@pytest.fixture
def client():
    """
    Créer un client de test Flask
    """
    app.config['TESTING'] = True
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'  # DB en mémoire
    
    with app.test_client() as client:
        with app.app_context():
            db.create_all()
            yield client
            db.drop_all()


@pytest.fixture
def utilisateur_test(client):
    """
    Créer un utilisateur de test
    """
    utilisateur = Utilisateur(
        nom='TestUser',
        email='test@example.com'
    )
    utilisateur.definir_mot_de_passe('password123')
    
    db.session.add(utilisateur)
    db.session.commit()
    
    return utilisateur


@pytest.fixture
def