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


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

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

# API = Application Programming Interface = Interface de Programmation d'Application

# Imagine que tu es dans un restaurant:
# - Tu (client) veux manger
# - La cuisine (serveur) prépare la nourriture
# - Le serveur (API) fait le lien entre toi et la cuisine

# Sans API:
# Tu devrais aller en cuisine, chercher les ingrédients, cuisiner toi-même
# = IMPOSSIBLE et DANGEREUX!

# Avec API:
# Tu commandes via le serveur (API)
# Le serveur transmet ta commande à la cuisine
# La cuisine prépare
# Le serveur te ramène le plat
# = SIMPLE et ORGANISÉ!

# Dans le monde informatique:
# Client (app mobile, site web, autre service) <--> API <--> Serveur (base de données, logique métier)

# Exemple concret:
# Application météo sur ton téléphone:
# 1. Tu ouvres l'app (CLIENT)
# 2. L'app demande à l'API: "Quelle est la météo à Paris?"
# 3. L'API interroge le serveur/base de données
# 4. Le serveur répond: "25°C, ensoleillé"
# 5. L'API transmet la réponse à l'app
# 6. Tu vois: "25°C [BLACK_SUN_WITH_RAYS]"


# === POURQUOI UTILISER UNE API? ===

# RAISON 1: Séparation des responsabilités
# Frontend (interface utilisateur) ≠ Backend (logique/données)
# Exemple:
# - Site web (HTML/CSS/JS) parle à l'API
# - App mobile (Swift/Kotlin) parle à la MÊME API
# - Application desktop parle à la MÊME API
# = UNE SEULE API pour PLUSIEURS clients!

# RAISON 2: Sécurité
# Le client ne touche JAMAIS directement la base de données
# Tout passe par l'API qui contrôle:
# - Qui peut accéder (authentification)
# - Ce qu'on peut faire (autorisation)
# - Validation des données

# RAISON 3: Évolutivité
# Tu peux changer la base de données sans casser les clients
# Exemple: Passer de MySQL à PostgreSQL
# Les clients ne voient PAS le changement
# Seule l'API s'adapte

# RAISON 4: Réutilisabilité
# D'autres développeurs peuvent utiliser ton API
# Exemple: API Twitter, API Google Maps, API Stripe
# Des millions d'apps utilisent ces APIs!

# RAISON 5: Performance
# L'API peut mettre en cache les données
# = Réponses plus rapides
# = Moins de charge sur la base de données


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

# ENDPOINT (Point de terminaison)
# = Une URL spécifique de ton API
# = Représente une ressource ou une action
# Exemples:
#   - GET /api/users -> Liste tous les utilisateurs
#   - GET /api/users/123 -> Récupère l'utilisateur avec ID 123
#   - POST /api/users -> Crée un nouvel utilisateur
#   - PUT /api/users/123 -> Modifie l'utilisateur 123
#   - DELETE /api/users/123 -> Supprime l'utilisateur 123

# REQUEST (Requête)
# = Message envoyé par le client à l'API
# Contient:
#   - Méthode HTTP (GET, POST, PUT, DELETE, PATCH...)
#   - URL (endpoint)
#   - Headers (métadonnées: authentification, type de contenu...)
#   - Body (données envoyées, pour POST/PUT/PATCH)
# Exemple:
# POST /api/users
# Headers: { "Content-Type": "application/json" }
# Body: { "name": "Alice", "email": "alice@example.com" }

# RESPONSE (Réponse)
# = Message renvoyé par l'API au client
# Contient:
#   - Status Code (200, 404, 500...)
#   - Headers (métadonnées de la réponse)
#   - Body (données renvoyées, souvent en JSON)
# Exemple:
# Status: 201 Created
# Body: { "id": 123, "name": "Alice", "email": "alice@example.com" }

# HTTP METHODS (Méthodes HTTP)
# = Verbes qui indiquent l'action à effectuer
# GET = Lire/Récupérer des données (ne modifie rien)
# POST = Créer une nouvelle ressource
# PUT = Remplacer complètement une ressource existante
# PATCH = Modifier partiellement une ressource
# DELETE = Supprimer une ressource
# OPTIONS = Demander les méthodes supportées
# HEAD = Comme GET mais sans le body (juste les headers)

# STATUS CODES (Codes de statut HTTP)
# = Nombres à 3 chiffres qui indiquent le résultat de la requête
# 
# 2xx = Succès
#   200 OK: Requête réussie
#   201 Created: Ressource créée avec succès
#   204 No Content: Succès mais pas de données à renvoyer
# 
# 3xx = Redirection
#   301 Moved Permanently: Ressource déplacée définitivement
#   302 Found: Redirection temporaire
# 
# 4xx = Erreur du client
#   400 Bad Request: Requête mal formée
#   401 Unauthorized: Non authentifié (pas de token/login)
#   403 Forbidden: Authentifié mais pas autorisé
#   404 Not Found: Ressource introuvable
#   422 Unprocessable Entity: Validation échouée
# 
# 5xx = Erreur du serveur
#   500 Internal Server Error: Erreur interne du serveur
#   502 Bad Gateway: Problème de proxy/passerelle
#   503 Service Unavailable: Service temporairement indisponible

# JSON (JavaScript Object Notation)
# = Format de données le plus utilisé pour les APIs
# = Facile à lire pour les humains ET les machines
# Exemple:
# {
#   "user": {
#     "id": 123,
#     "name": "Alice",
#     "email": "alice@example.com",
#     "is_active": true,
#     "roles": ["admin", "editor"],
#     "created_at": "2024-01-15T10:30:00Z"
#   }
# }

# REST (Representational State Transfer)
# = Style d'architecture pour concevoir des APIs
# = Ensemble de règles et bonnes pratiques
# Principes REST:
#   1. Utiliser les méthodes HTTP correctement (GET, POST, PUT, DELETE)
#   2. URLs représentent des ressources (/users, /posts, pas /getUsers, /createPost)
#   3. Stateless (sans état): chaque requête est indépendante
#   4. Réponses en JSON (généralement)
#   5. Utiliser les codes HTTP appropriés

# CRUD (Create, Read, Update, Delete)
# = Les 4 opérations de base sur des données
# Create = POST /api/users (créer)
# Read = GET /api/users (lire tous) ou GET /api/users/123 (lire un)
# Update = PUT ou PATCH /api/users/123 (modifier)
# Delete = DELETE /api/users/123 (supprimer)

# AUTHENTIFICATION
# = Vérifier l'identité de l'utilisateur
# "Qui es-tu?"
# Méthodes:
#   - API Key: Clé secrète dans les headers
#   - JWT (JSON Web Token): Token contenant des infos encodées
#   - OAuth: Protocole complexe pour déléguer l'authentification
#   - Basic Auth: Username/password dans les headers (peu sécurisé)

# AUTORISATION
# = Vérifier ce que l'utilisateur peut faire
# "Que peux-tu faire?"
# Exemple:
# - User normal peut lire ses propres posts
# - Admin peut lire/modifier/supprimer TOUS les posts
# - Utilisateur non connecté ne peut rien faire

# CORS (Cross-Origin Resource Sharing)
# = Mécanisme de sécurité des navigateurs
# Problème: Par défaut, un site web ne peut PAS faire de requêtes vers un autre domaine
# Exemple:
#   - Frontend sur https://monsite.com
#   - API sur https://api.monsite.com
#   - Le navigateur BLOQUE par défaut!
# Solution: L'API doit envoyer des headers CORS pour autoriser
# Headers:
#   Access-Control-Allow-Origin: https://monsite.com
#   Access-Control-Allow-Methods: GET, POST, PUT, DELETE
#   Access-Control-Allow-Headers: Content-Type, Authorization

# RATE LIMITING
# = Limiter le nombre de requêtes par utilisateur
# Pourquoi?
#   - Éviter les abus (attaques DDoS)
#   - Préserver les ressources du serveur
# Exemple:
#   - 100 requêtes par minute par utilisateur
#   - Si dépassé: renvoyer 429 Too Many Requests
# Implémentation:
#   - Compter les requêtes dans Redis
#   - Utiliser des bibliothèques comme Flask-Limiter


# === TYPES D'APIS ===

# API REST (Representational State Transfer)
# = Le plus utilisé, simple, basé sur HTTP
# Avantages:
#   - Simple à comprendre et implémenter
#   - Largement supporté
#   - Cache HTTP natif
# Inconvénients:
#   - Peut nécessiter plusieurs requêtes pour des données complexes
#   - Over-fetching (recevoir trop de données) ou under-fetching (pas assez)
# Utilisation:
#   - APIs publiques
#   - Applications web/mobile classiques
# Exemple:
#   GET /api/users/123
#   Réponse: { "id": 123, "name": "Alice", "email": "alice@example.com" }

# API GraphQL
# = Langage de requête développé par Facebook
# = Le client spécifie EXACTEMENT les données qu'il veut
# Avantages:
#   - Pas d'over-fetching ni under-fetching
#   - Une seule requête pour des données complexes
#   - Introspection (auto-documentation)
# Inconvénients:
#   - Plus complexe à implémenter
#   - Pas de cache HTTP natif
#   - Peut être lent si mal optimisé
# Utilisation:
#   - Applications complexes avec beaucoup de relations
#   - Quand les besoins varient beaucoup selon les clients
# Exemple:
#   POST /graphql
#   Body:
#   {
#     "query": "{ user(id: 123) { name email posts { title } } }"
#   }
#   Réponse:
#   {
#     "data": {
#       "user": {
#         "name": "Alice",
#         "email": "alice@example.com",
#         "posts": [
#           { "title": "Mon premier post" },
#           { "title": "Deuxième post" }
#         ]
#       }
#     }
#   }

# API SOAP (Simple Object Access Protocol)
# = Protocole XML, ancien mais encore utilisé
# Avantages:
#   - Très structuré
#   - Sécurité intégrée (WS-Security)
#   - Support des transactions complexes
# Inconvénients:
#   - Verbeux (beaucoup de XML)
#   - Lent
#   - Complexe
# Utilisation:
#   - Systèmes bancaires
#   - Entreprises legacy
#   - Systèmes critiques nécessitant transactions ACID
# Note: Rarement utilisé pour de nouvelles APIs!

# API WebSocket
# = Communication bidirectionnelle en temps réel
# = Connexion persistante (pas request/response)
# Avantages:
#   - Temps réel (chat, notifications, jeux)
#   - Moins de latence
# Inconvénients:
#   - Plus complexe à gérer
#   - Nécessite infrastructure spécifique
# Utilisation:
#   - Chat en temps réel
#   - Notifications push
#   - Jeux multijoueurs
#   - Dashboards live
# Exemple:
#   Client se connecte à ws://api.example.com/chat
#   Serveur envoie des messages en continu
#   Client peut envoyer des messages aussi

# API gRPC (Google Remote Procedure Call)
# = Protocole développé par Google
# = Utilise Protocol Buffers (binaire, pas JSON)
# Avantages:
#   - Très rapide (binaire)
#   - Support streaming
#   - Fortement typé
# Inconvénients:
#   - Pas lisible par humain (binaire)
#   - Moins de support navigateur
# Utilisation:
#   - Microservices
#   - Communication serveur-serveur
#   - Applications nécessitant haute performance


# === COMMENT ÇA MARCHE? (FLUX COMPLET) ===

# Exemple: Application de gestion de tâches (TODO app)

# 1. CLIENT (App mobile) veut voir la liste des tâches
#    Action: Envoie une requête GET

# 2. REQUÊTE HTTP envoyée:
#    GET https://api.todoapp.com/api/tasks
#    Headers:
#      Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
#      Content-Type: application/json

# 3. API reçoit la requête
#    - Vérifie le token d'authentification
#    - Si valide: continue
#    - Si invalide: renvoie 401 Unauthorized

# 4. API interroge la BASE DE DONNÉES
#    SQL: SELECT * FROM tasks WHERE user_id = 123
#    Résultat: [
#      { id: 1, title: "Faire les courses", completed: false },
#      { id: 2, title: "Appeler maman", completed: true }
#    ]

# 5. API formate la RÉPONSE en JSON
#    Status: 200 OK
#    Headers:
#      Content-Type: application/json
#    Body:
#    {
#      "tasks": [
#        {
#          "id": 1,
#          "title": "Faire les courses",
#          "completed": false,
#          "created_at": "2024-01-15T10:00:00Z"
#        },
#        {
#          "id": 2,
#          "title": "Appeler maman",
#          "completed": true,
#          "created_at": "2024-01-14T15:30:00Z"
#        }
#      ],
#      "total": 2
#    }

# 6. CLIENT reçoit la réponse
#    Parse le JSON
#    Affiche les tâches à l'utilisateur


[OK] CRÉATION D'API AVEC FLASK (POUR DÉBUTANTS)

# === POURQUOI FLASK? ===

# Flask = Micro-framework Python pour créer des APIs
# Avantages:
#   - Très simple pour débuter
#   - Flexible
#   - Peu de magie, tout est explicite
#   - Grande communauté
# Inconvénients:
#   - Pas de validation automatique des données
#   - Pas de documentation auto-générée
#   - Manque de fonctionnalités avancées par défaut
# Utilisation recommandée:
#   - Petites APIs
#   - Prototypes rapides
#   - APIs simples sans beaucoup de validation


# === INSTALLATION ===

# Créer un environnement virtuel:
python -m venv venv
source venv/bin/activate  # Linux/macOS
venv\Scripts\activate     # Windows

# Installer Flask:
pip install flask
pip install flask-cors  # Pour gérer CORS
pip install flask-sqlalchemy  # Pour la base de données

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


# === EXEMPLE 1: API MINIMALE FLASK ===

# Fichier: app.py

from flask import Flask, jsonify, request

app = Flask(__name__)

# === ROUTE DE BASE ===
@app.route('/')
def home():
    return jsonify({
        "message": "Bienvenue sur mon API!",
        "version": "1.0.0",
        "endpoints": [
            "GET /",
            "GET /api/tasks",
            "POST /api/tasks",
            "GET /api/tasks/<id>",
            "PUT /api/tasks/<id>",
            "DELETE /api/tasks/<id>"
        ]
    })

# === DONNÉES EN MÉMOIRE (pour l'exemple) ===
# ATTENTION: En production, utiliser une vraie base de données!

tasks = [
    {"id": 1, "title": "Faire les courses", "completed": False},
    {"id": 2, "title": "Appeler maman", "completed": True}
]

# === ROUTE GET: LIRE TOUTES LES TÂCHES ===
@app.route('/api/tasks', methods=['GET'])
def get_tasks():
    """
    Récupère toutes les tâches
    Retourne: Liste des tâches en JSON
    """
    return jsonify({
        "tasks": tasks,
        "total": len(tasks)
    }), 200

# === ROUTE GET: LIRE UNE TÂCHE SPÉCIFIQUE ===
@app.route('/api/tasks/<int:task_id>', methods=['GET'])
def get_task(task_id):
    """
    Récupère une tâche par son ID
    Paramètre: task_id (int)
    Retourne: La tâche ou 404
    """
    # Chercher la tâche
    task = next((t for t in tasks if t['id'] == task_id), None)
    
    if task is None:
        return jsonify({
            "error": "Tâche non trouvée",
            "task_id": task_id
        }), 404
    
    return jsonify(task), 200

# === ROUTE POST: CRÉER UNE NOUVELLE TÂCHE ===
@app.route('/api/tasks', methods=['POST'])
def create_task():
    """
    Crée une nouvelle tâche
    Body attendu: { "title": "..." }
    Retourne: La tâche créée
    """
    # Récupérer les données du body
    data = request.get_json()
    
    # Validation simple
    if not data or 'title' not in data:
        return jsonify({
            "error": "Le champ 'title' est requis"
        }), 400
    
    # Créer la nouvelle tâche
    new_task = {
        "id": len(tasks) + 1,  # ID auto-incrémenté (simplifié)
        "title": data['title'],
        "completed": data.get('completed', False)  # False par défaut
    }
    
    tasks.append(new_task)
    
    return jsonify(new_task), 201  # 201 = Created

# === ROUTE PUT: MODIFIER UNE TÂCHE ===
@app.route('/api/tasks/<int:task_id>', methods=['PUT'])
def update_task(task_id):
    """
    Modifie complètement une tâche
    Body attendu: { "title": "...", "completed": true/false }
    """
    task = next((t for t in tasks if t['id'] == task_id), None)
    
    if task is None:
        return jsonify({"error": "Tâche non trouvée"}), 404
    
    data = request.get_json()
    
    if not data or 'title' not in data:
        return jsonify({"error": "Le champ 'title' est requis"}), 400
    
    # Modifier la tâche
    task['title'] = data['title']
    task['completed'] = data.get('completed', task['completed'])
    
    return jsonify(task), 200

# === ROUTE PATCH: MODIFIER PARTIELLEMENT UNE TÂCHE ===
@app.route('/api/tasks/<int:task_id>', methods=['PATCH'])
def patch_task(task_id):
    """
    Modifie partiellement une tâche
    Body: Seulement les champs à modifier
    """
    task = next((t for t in tasks if t['id'] == task_id), None)
    
    if task is None:
        return jsonify({"error": "Tâche non trouvée"}), 404
    
    data = request.get_json()
    
    # Modifier seulement les champs fournis
    if 'title' in data:
        task['title'] = data['title']
    if 'completed' in data:
        task['completed'] = data['completed']
    
    return jsonify(task), 200

# === ROUTE DELETE: SUPPRIMER UNE TÂCHE ===
@app.route('/api/tasks/<int:task_id>', methods=['DELETE'])
def delete_task(task_id):
    """
    Supprime une tâche
    """
    global tasks
    
    task = next((t for t in tasks if t['id'] == task_id), None)
    
    if task is None:
        return jsonify({"error": "Tâche non trouvée"}), 404
    
    tasks = [t for t in tasks if t['id'] != task_id]
    
    return jsonify({
        "message": "Tâche supprimée avec succès",
        "task_id": task_id
    }), 200

# === GESTION DES ERREURS ===

@app.errorhandler(404)
def not_found(error):
    """Gestion personnalisée des erreurs 404"""
    return jsonify({
        "error": "Endpoint non trouvé",
        "path": request.path
    }), 404

@app.errorhandler(500)
def internal_error(error):
    """Gestion des erreurs serveur"""
    return jsonify({
        "error": "Erreur interne du serveur",
        "message": str(error)
    }), 500

# === DÉMARRAGE DU SERVEUR ===

if __name__ == '__main__':
    app.run(
        host='0.0.0.0',  # Accessible depuis l'extérieur
        port=5000,       # Port 5000
        debug=True       # Mode debug (développement seulement!)
    )


# === TESTER L'API FLASK ===

# Lancer le serveur:
python app.py
# Affiche: Running on http://127.0.0.1:5000

# Tester avec curl (ligne de commande):

# 1. Récupérer toutes les tâches:
curl http://localhost:5000/api/tasks

# 2. Récupérer une tâche spécifique:
curl http://localhost:5000/api/tasks/1

# 3. Créer une nouvelle tâche:
curl -X POST http://localhost:5000/api/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "Nouvelle tâche", "completed": false}'

# 4. Modifier une tâche:
curl -X PUT http://localhost:5000/api/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"title": "Tâche modifiée", "completed": true}'

# 5. Modifier partiellement:
curl -X PATCH http://localhost:5000/api/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"completed": true}'

# 6. Supprimer une tâche:
curl -X DELETE http://localhost:5000/api/tasks/1

# Tester avec Postman (GUI):
# 1. Télécharger Postman: https://www.postman.com/downloads/
# 2. Créer une nouvelle requête
# 3. Sélectionner la méthode (GET, POST, etc.)
# 4. Entrer l'URL: http://localhost:5000/api/tasks
# 5. Pour POST/PUT: Aller dans Body > raw > JSON
# 6. Cliquer "Send"


[OK] CRÉATION D'API AVEC FASTAPI (MODERNE ET RAPIDE)

# === POURQUOI FASTAPI? ===

# FastAPI = Framework Python moderne pour créer des APIs
# Avantages:
#   - TRÈS RAPIDE (un des plus rapides)
#   - Validation automatique des données (Pydantic)
#   - Documentation auto-générée (Swagger UI)
#   - Support async natif
#   - Type hints Python (meilleure auto-complétion)
# Inconvénients:
#   - Courbe d'apprentissage un peu plus raide
#   - Plus récent (moins de ressources)
# Utilisation recommandée:
#   - APIs modernes
#   - APIs nécessitant performance
#   - Projets professionnels


# === INSTALLATION ===

pip install fastapi
pip install uvicorn[standard]  # Serveur ASGI
pip install pydantic  # Validation de données
pip install sqlalchemy  # ORM pour base de données
pip install python-jose[cryptography]  # Pour JWT
pip install passlib[bcrypt]  # Pour hasher les mots de passe


# === EXEMPLE 2: API COMPLÈTE AVEC FASTAPI ===

# Fichier: main.py

from fastapi import FastAPI, HTTPException, status, Depends
from pydantic import BaseModel, Field, EmailStr
from typing import List, Optional
from datetime import datetime
import uvicorn

app = FastAPI(
    title="API de Gestion de Tâches",
    description="API complète pour gérer des tâches avec FastAPI",
    version="1.0.0"
)

# === MODÈLES PYDANTIC (Validation automatique) ===

class TaskBase(BaseModel):
    """
    Schéma de base pour une tâche
    """
    title: str = Field(..., min_length=1, max_length=200, description="Titre de la tâche")
    description: Optional[str] = Field(None, max_length=1000, description="Description détaillée")
    completed: bool = Field(default=False, description="Statut de complétion")
    priority: int = Field(default=1, ge=1, le=5, description="Priorité de 1 à 5")

class TaskCreate(TaskBase):
    """
    Schéma pour créer une tâche
    """
    pass

class TaskUpdate(BaseModel):
    """
    Schéma pour modifier une tâche (tous les champs optionnels)
    """
    title: Optional[str] = Field(None, min_length=1, max_length=200)
    description: Optional[str] = Field(None, max_length=1000)
    completed: Optional[bool] = None
    priority: Optional[int] = Field(None, ge=1, le=5)

class Task(TaskBase):
    """
    Schéma complet d'une tâche (avec ID et dates)
    """
    id: int
    created_at: datetime
    updated_at: datetime
    
    class Config:
        # Permet de convertir les objets ORM en modèles Pydantic
        from_attributes = True

# === BASE DE DONNÉES EN MÉMOIRE (pour l'exemple) ===

tasks_db = []
task_counter = 0

# === ENDPOINTS CRUD ===

@app.get("/", tags=["Root"])
async def root():
    """
    Endpoint racine - Informations sur l'API
    """
    return {
        "message": "API de Gestion de Tâches",
        "version": "1.0.0",
        "docs": "/docs",  # Documentation Swagger auto-générée!
        "redoc": "/redoc"  # Documentation ReDoc alternative
    }

@app.get("/api/tasks", response_model=List[Task], tags=["Tasks"])
async def get_all_tasks(
    skip: int = 0,
    limit: int = 100,
    completed: Optional[bool] = None
):
    """
    Récupère toutes les tâches avec pagination et filtrage
    
    Args:
        skip: Nombre de tâches à sauter (pagination)
        limit: Nombre maximum de tâches à retourner
        completed: Filtrer par statut (True/False/None)
    
    Returns:
        Liste des tâches
    """
    # Filtrage
    if completed is not None:
        filtered_tasks = [t for t in tasks_db if t['completed'] == completed]
    else:
        filtered_tasks = tasks_db
    
    # Pagination
    return filtered_tasks[skip : skip + limit]

@app.get("/api/tasks/{task_id}", response_model=Task, tags=["Tasks"])
async def get_task(task_id: int):
    """
    Récupère une tâche par son ID
    
    Args:
        task_id: ID de la tâche
    
    Returns:
        La tâche demandée
    
    Raises:
        HTTPException 404: Tâche non trouvée
    """
    task = next((t for t in tasks_db if t['id'] == task_id), None)
    
    if task is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Tâche avec l'ID {task_id} non trouvée"
        )
    
    return task

@app.post("/api/tasks", response_model=Task, status_code=status.HTTP_201_CREATED, tags=["Tasks"])
async def create_task(task: TaskCreate):
    """
    Crée une nouvelle tâche
    
    Args:
        task: Données de la tâche à créer
    
    Returns:
        La tâche créée avec son ID
    """
    global task_counter
    task_counter += 1
    
    now = datetime.now()
    new_task = {
        "id": task_counter,
        "title": task.title,
        "description": task.description,
        "completed": task.completed,
        "priority": task.priority,
        "created_at": now,
        "updated_at": now
    }
    
    tasks_db.append(new_task)
    return new_task

@app.put("/api/tasks/{task_id}", response_model=Task, tags=["Tasks"])
async def update_task(task_id: int, task_update: TaskUpdate):
    """
    Modifie une tâche existante
    
    Args:
        task_id: ID de la tâche à modifier
        task_update: Nouvelles données
    
    Returns:
        La tâche modifiée
    """
    task = next((t for t in tasks_db if t['id'] == task_id), None)
    
    if task is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Tâche avec l'ID {task_id} non trouvée"
        )
    
    # Mettre à jour seulement les champs fournis
    update_data = task_update.dict(exclude_unset=True)
    for field, value in update_data.items():
        task[field] = value
    
    task['updated_at'] = datetime.now()
    
    return task

@app.delete("/api/tasks/{task_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["Tasks"])
async def delete_task(task_id: int):
    """
    Supprime une tâche
    
    Args:
        task_id: ID de la tâche à supprimer
    """
    global tasks_db
    
    task = next((t for t in tasks_db if t['id'] == task_id), None)
    
    if task is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Tâche avec l'ID {task_id} non trouvée"
        )
    
    tasks_db = [t for t in tasks_db if t['id'] != task_id]
    
    # 204 No Content = pas de body dans la réponse
    return None

# === GESTION DES ERREURS ===

@app.exception_handler(ValueError)
async def value_error_handler(request, exc):
    """Gestion des erreurs de validation personnalisées"""
    return HTTPException(
        status_code=status.HTTP_400_BAD_REQUEST,
        detail=str(exc)
    )

# === DÉMARRAGE DU SERVEUR ===

if __name__ == "__main__":
    uvicorn.run(
        "main:app",
        host="0.0.0.0",
        port=8000,
        reload=True  # Auto-reload en développement
    )


# === TESTER L'API FASTAPI ===

# Lancer le serveur:
python main.py
# ou:
uvicorn main:app --reload

# Affiche: Uvicorn running on http://127.0.0.1:8000

# DOCUMENTATION AUTO-GÉNÉRÉE (SUPER COOL!):
# Ouvre ton navigateur: http://localhost:8000/docs
# Tu vois une interface Swagger UI complète!
# Tu peux TESTER toutes les routes directement depuis le navigateur!

# Alternative ReDoc:
# http://localhost:8000/redoc

# Tester avec curl:

# 1. Créer une tâche:
curl -X POST http://localhost:8000/api/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Apprendre FastAPI",
    "description": "Comprendre comment créer des APIs modernes",
    "priority": 5
  }'

# 2. Récupérer toutes les tâches:
curl http://localhost:8000/api/tasks

# 3. Filtrer par statut:
curl "http://localhost:8000/api/tasks?completed=false"

# 4. Pagination:
curl "http://localhost:8000/api/tasks?skip=0&limit=10"

# 5. Récupérer une tâche:
curl http://localhost:8000/api/tasks/1

# 6. Modifier une tâche:
curl -X PUT http://localhost:8000/api/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"title": "Tâche modifiée", "completed": true}'

# 7. Supprimer une tâche:
curl -X DELETE http://localhost:8000/api/tasks/1


[OK] BASE DE DONNÉES AVEC SQLALCHEMY (POUR PRODUCTION)

# === POURQUOI SQLALCHEMY? ===

# SQLAlchemy = ORM (Object-Relational Mapping) Python
# = Permet d'utiliser Python au lieu de SQL
# Avantages:
#   - Code Python (pas de SQL brut)
#   - Protection contre SQL injection
#   - Abstraction de la base de données (MySQL, PostgreSQL, SQLite...)
#   - Migrations automatiques
# Utilisation:
#   - Toutes les APIs en production
#   - Dès que tu as besoin de persister des données


# === INSTALLATION ===

pip install sqlalchemy
pip install psycopg2-binary  # Pour PostgreSQL
# ou
pip install pymysql  # Pour MySQL


# === CONFIGURATION DATABASE ===

# Fichier: database.py

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import os

# === URL DE LA BASE DE DONNÉES ===

# Format général:
# postgresql://user:password@host:port/database
# mysql://user:password@host:port/database
# sqlite:///./database.db

# Utiliser des variables d'environnement pour la sécurité!
DATABASE_URL = os.getenv(
    "DATABASE_URL",
    "sqlite:///./tasks.db"  # SQLite par défaut pour dev
)

# Si Heroku, fix l'URL PostgreSQL:
if DATABASE_URL.startswith("postgres://"):
    DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://", 1)

# === CRÉER LE MOTEUR ===

engine = create_engine(
    DATABASE_URL,
    connect_args={"check_same_thread": False} if "sqlite" in DATABASE_URL else {}
)

# === SESSION LOCAL ===

SessionLocal = sessionmaker(
    autocommit=False,
    autoflush=False,
    bind=engine
)

# === BASE POUR LES MODÈLES ===

Base = declarative_base()

# === DEPENDENCY POUR OBTENIR UNE SESSION ===

def get_db():
    """
    Fonction de dépendance pour obtenir une session DB
    Utilisée avec Depends() dans FastAPI
    """
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


# === MODÈLES SQLALCHEMY ===

# Fichier: models.py

from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text
from datetime import datetime
from database import Base

class Task(Base):
    """
    Modèle SQLAlchemy pour les tâches
    """
    __tablename__ = "tasks"
    
    id = Column(Integer, primary_key=True, index=True)
    title = Column(String(200), nullable=False)
    description = Column(Text, nullable=True)
    completed = Column(Boolean, default=False)
    priority = Column(Integer, default=1)
    created_at = Column(DateTime, default=datetime.now)
    updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
    
    def __repr__(self):
        return f"<Task(id={self.id}, title='{self.title}')>"


# === API FASTAPI AVEC SQLALCHEMY ===

# Fichier: main.py (version complète avec DB)

from fastapi import FastAPI, HTTPException, status, Depends
from sqlalchemy.orm import Session
from typing import List
import models
import schemas
from database import engine, get_db

# Créer les tables si elles n'existent pas
models.Base.metadata.create_all(bind=engine)

app = FastAPI(title="API avec Base de Données")

# === ENDPOINTS AVEC DB ===

@app.post("/api/tasks", response_model=schemas.Task, status_code=status.HTTP_201_CREATED)
async def create_task(
    task: schemas.TaskCreate,
    db: Session = Depends(get_db)
):
    """
    Crée une tâche dans la base de données
    """
    db_task = models.Task(
        title=task.title,
        description=task.description,
        completed=task.completed,
        priority=task.priority
    )
    
    db.add(db_task)
    db.commit()
    db.refresh(db_task)  # Récupère l'ID auto-généré
    
    return db_task

@app.get("/api/tasks", response_model=List[schemas.Task])
async def get_tasks(
    skip: int = 0,
    limit: int = 100,
    db: Session = Depends(get_db)
):
    """
    Récupère toutes les tâches de la DB
    """
    tasks = db.query(models.Task).offset(skip).limit(limit).all()
    return tasks

@app.get("/api/tasks/{task_id}", response_model=schemas.Task)
async def get_task(
    task_id: int,
    db: Session = Depends(get_db)
):
    """
    Récupère une tâche par ID
    """
    task = db.query(models.Task).filter(models.Task.id == task_id).first()
    
    if task is None:
        raise HTTPException(status_code=404, detail="Tâche non trouvée")
    
    return task

@app.put("/api/tasks/{task_id}", response_model=schemas.Task)
async def update_task(
    task_id: int,
    task_update: schemas.TaskUpdate,
    db: Session = Depends(get_db)
):
    """
    Modifie une tâche
    """
    db_task = db.query(models.Task).filter(models.Task.id == task_id).first()
    
    if db_task is None:
        raise HTTPException(status_code=404, detail="Tâche non trouvée")
    
    # Mettre à jour les champs fournis
    update_data = task_update.dict(exclude_unset=True)
    for field, value in update_data.items():
        setattr(db_task, field, value)
    
    db_task.updated_at = datetime.now()
    
    db.commit()
    db.refresh(db_task)
    
    return db_task

@app.delete("/api/tasks/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_task(
    task_id: int,
    db: Session = Depends(get_db)
):
    """
    Supprime une tâche
    """
    db_task = db.query(models.Task).filter(models.Task.id == task_id).first()
    
    if db_task is None:
        raise HTTPException(status_code=404, detail="Tâche non trouvée")
    
    db.delete(db_task)
    db.commit()
    
    return None


[OK] AUTHENTIFICATION JWT (JSON WEB TOKEN)

# === POURQUOI JWT? ===

# JWT = Standard pour transmettre des informations de manière sécurisée
# Format: header.payload.signature
# Exemple: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

# Avantages:
#   - Stateless (pas besoin de stocker les sessions)
#   - Contient des informations (user_id, rôles...)
#   - Signé cryptographiquement (impossible de falsifier)
# Utilisation:
#   - APIs modernes
#   - Applications web/mobile
#   - Microservices


# === INSTALLATION ===

pip install python-jose[cryptography]
pip install passlib[bcrypt]
pip install python-multipart


# === CONFIGURATION JWT ===

# Fichier: auth.py

from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel

# === CONFIGURATION ===

SECRET_KEY = "your-secret-key-change-this-in-production"  # CHANGER EN PRODUCTION!
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

# === PASSWORD HASHING ===

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def hash_password(password: str) -> str:
    """
    Hash un mot de passe
    """
    return pwd_context.hash(password)

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """
    Vérifie un mot de passe
    """
    return pwd_context.verify(plain_password, hashed_password)

# === TOKEN ===

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

class Token(BaseModel):
    access_token: str
    token_type: str

class TokenData(BaseModel):
    username: Optional[str] = None

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
    """
    Crée un token JWT
    
    Args:
        data: Données à encoder dans le token (user_id, username, etc.)
        expires_delta: Durée de validité du token
    
    Returns:
        Le token JWT signé
    """
    to_encode = data.copy()
    
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    
    to_encode.update({"exp": expire})
    
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def verify_token(token: str, credentials_exception):
    """
    Vérifie et décode un token JWT
    """
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        
        if username is None:
            raise credentials_exception
        
        token_data = TokenData(username=username)
        return token_data
    except JWTError:
        raise credentials_exception

# === DÉPENDANCE POUR VÉRIFIER L'AUTHENTIFICATION ===

async def get_current_user(token: str = Depends(oauth2_scheme)):
    """
    Récupère l'utilisateur courant à partir du token
    """
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Impossible de valider les credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    
    return verify_token(token, credentials_exception)


# === MODÈLES UTILISATEUR ===

# Fichier: user_models.py

from sqlalchemy import Column, Integer, String, Boolean
from database import Base

class User(Base):
    """
    Modèle utilisateur
    """
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True, index=True)
    username = Column(String(50), unique=True, index=True, nullable=False)
    email = Column(String(100), unique=True, index=True, nullable=False)
    hashed_password = Column(String(255), nullable=False)
    is_active = Column(Boolean, default=True)
    is_admin = Column(Boolean, default=False)


# === ENDPOINTS D'AUTHENTIFICATION ===

# Fichier: main.py (ajouter ces routes)

from fastapi.security import OAuth2PasswordRequestForm
from auth import (
    create_access_token,
    hash_password,
    verify_password,
    get_current_user,
    Token,
    ACCESS_TOKEN_EXPIRE_MINUTES
)
import user_models

# Créer les tables utilisateur
user_models.Base.metadata.create_all(bind=engine)

@app.post("/register", status_code=status.HTTP_201_CREATED)
async def register(
    username: str,
    email: str,
    password: str,
    db: Session = Depends(get_db)
):
    """
    Inscription d'un nouvel utilisateur
    """
    # Vérifier si l'utilisateur existe déjà
    existing_user = db.query(user_models.User).filter(
        (user_models.User.username == username) | (user_models.User.email == email)
    ).first()
    
    if existing_user:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Username ou email déjà utilisé"
        )
    
    # Créer l'utilisateur
    hashed_pwd = hash_password(password)
    new_user = user_models.User(
        username=username,
        email=email,
        hashed_password=hashed_pwd
    )
    
    db.add(new_user)
    db.commit()
    db.refresh(new_user)
    
    return {
        "message": "Utilisateur créé avec succès",
        "username": new_user.username,
        "email": new_user.email
    }

@app.post("/token", response_model=Token)
async def login(
    form_data: OAuth2PasswordRequestForm = Depends(),
    db: Session = Depends(get_db)
):
    """
    Connexion et obtention d'un token JWT
    """
    # Récupérer l'utilisateur
    user = db.query(user_models.User).filter(
        user_models.User.username == form_data.username
    ).first()
    
    # Vérifier l'utilisateur et le mot de passe
    if not user or not verify_password(form_data.password, user.hashed_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Username ou mot de passe incorrect",
            headers={"WWW-Authenticate": "Bearer"},
        )
    
    # Créer le token
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username},
        expires_delta=access_token_expires
    )
    
    return {"access_token": access_token, "token_type": "bearer"}

@app.get("/users/me")
async def read_users_me(current_user: TokenData = Depends(get_current_user)):
    """
    Récupère les informations de l'utilisateur courant
    Nécessite un token valide!
    """
    return {
        "username": current_user.username,
        "message": "Vous êtes authentifié!"
    }

# === PROTÉGER LES ENDPOINTS ===

# Modifier les endpoints existants pour exiger l'authentification:

@app.post("/api/tasks", response_model=schemas.Task, status_code=status.HTTP_201_CREATED)
async def create_task(
    task: schemas.TaskCreate,
    db: Session = Depends(get_db),
    current_user: TokenData = Depends(get_current_user)  # PROTECTION!
):
    """
    Crée une tâche (nécessite authentification)
    """
    # ... code de création ...
    pass


# === TESTER L'AUTHENTIFICATION ===

# 1. Créer un utilisateur:
curl -X POST http://localhost:8000/register \
  -H "Content-Type: application/json" \
  -d '{
    "username": "alice",
    "email": "alice@example.com",
    "password": "motdepasse123"
  }'

# 2. Se connecter et obtenir un token:
curl -X POST http://localhost:8000/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "username=alice&password=motdepasse123"

# Réponse:
# {
#   "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
#   "token_type": "bearer"
# }

# 3. Utiliser le token pour accéder aux endpoints protégés:
curl http://localhost:8000/users/me \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

# 4. Créer une tâche avec authentification:
curl -X POST http://localhost:8000/api/tasks \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -d '{
    "title": "Tâche protégée",
    "description": "Créée avec authentification"
  }'


[OK] CORS (CROSS-ORIGIN RESOURCE SHARING)

# === POURQUOI CORS? ===

# Problème: Par défaut, un navigateur BLOQUE les requêtes vers un domaine différent
# Exemple:
#   - Frontend sur https://monsite.com
#   - API sur https://api.monsite.com
#   - Le navigateur bloque par défaut!

# Solution: L'API doit autoriser explicitement les requêtes depuis d'autres domaines


# === CONFIGURATION CORS AVEC FLASK ===

from flask import Flask
from flask_cors import CORS

app = Flask(__name__)

# === CORS SIMPLE (Autoriser tout) - DÉVELOPPEMENT SEULEMENT ===
CORS(app)

# === CORS AVEC CONFIGURATION (RECOMMANDÉ POUR PRODUCTION) ===
CORS(app, resources={
    r"/api/*": {
        "origins": ["https://monsite.com", "https://www.monsite.com"],
        "methods": ["GET", "POST", "PUT", "DELETE", "PATCH"],
        "allow_headers": ["Content-Type", "Authorization"]
    }
})


# === CONFIGURATION CORS AVEC FASTAPI ===

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# === LISTE DES ORIGINES AUTORISÉES ===
origins = [
    "http://localhost",
    "http://localhost:3000",  # React dev server
    "http://localhost:8080",  # Vue dev server
    "https://monsite.com",
    "https://www.monsite.com"
]

# === AJOUTER LE MIDDLEWARE CORS ===
app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,  # Liste des origines autorisées
    allow_credentials=True,  # Autoriser les cookies
    allow_methods=["*"],  # Autoriser toutes les méthodes
    allow_headers=["*"],  # Autoriser tous les headers
)

# === CORS POUR TOUT LE MONDE (DÉVELOPPEMENT SEULEMENT) ===
# app.add_middleware(
#     CORSMiddleware,
#     allow_origins=["*"],  # DANGEREUX EN PRODUCTION!
#     allow_credentials=True,
#     allow_methods=["*"],
#     allow_headers=["*"],
# )


[OK] RATE LIMITING (LIMITATION DU DÉBIT)

# === POURQUOI RATE LIMITING? ===

# Protéger ton API contre:
#   - Les abus (spam de requêtes)
#   - Les attaques DDoS
#   - L'utilisation excessive par un utilisateur

# Limiter le nombre de requêtes par:
#   - IP
#   - Utilisateur
#   - Endpoint


# === RATE LIMITING AVEC FLASK ===

from flask import Flask
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

app = Flask(__name__)

# === INITIALISER 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
)

# === APPLIQUER DES LIMITES SPÉCIFIQUES ===

@app.route("/api/tasks")
@limiter.limit("10 per minute")  # 10 requêtes par minute maximum
def get_tasks():
    return {"tasks": []}

@app.route("/api/search")
@limiter.limit("3 per minute")  # Limite plus stricte pour la recherche
def search():
    return {"results": []}

# === LIMITES PAR UTILISATEUR (AU LIEU DE PAR IP) ===

def get_user_id():
    """Récupère l'ID utilisateur depuis le token"""
    # ... logique pour extraire user_id du token JWT
    return "user_123"

limiter_user = Limiter(
    app=app,
    key_func=get_user_id,
    default_limits=["1000 per day"]
)


# === RATE LIMITING AVEC FASTAPI ===

from fastapi import FastAPI, Request, HTTPException
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded

app = FastAPI()

# === INITIALISER SLOWAPI ===
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

# === APPLIQUER DES LIMITES ===

@app.get("/api/tasks")
@limiter.limit("10/minute")  # 10 requêtes par minute
async def get_tasks(request: Request):
    return {"tasks": []}

@app.post("/api/tasks")
@limiter.limit("5/minute")  # Limite plus stricte pour POST
async def create_task(request: Request):
    return {"message": "Tâche créée"}


[OK] DOCUMENTATION API (SWAGGER/OPENAPI)

# === POURQUOI DOCUMENTER? ===

# Documentation = Mode d'emploi de ton API
# Explique:
#   - Quels endpoints existent
#   - Quels paramètres sont requis
#   - Quels sont les formats de réponse
#   - Comment s'authentifier

# Avantages:
#   - Les autres développeurs peuvent utiliser ton API
#   - Tu te souviens de comment ça marche (après 6 mois!)
#   - Tests interactifs


# === DOCUMENTATION AUTOMATIQUE AVEC FASTAPI ===

# FastAPI génère automatiquement la documentation!
# Swagger UI: http://localhost:8000/docs
# ReDoc: http://localhost:8000/redoc

# Pour améliorer la documentation, ajoute des descriptions:

from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI(
    title="API de Gestion de Tâches",
    description="""
    Cette API permet de gérer des tâches.
    
    ## Fonctionnalités
    
    - **Créer** des tâches
    - **Lire** la liste des tâches
    - **Modifier** une tâche existante
    - **Supprimer** une tâche
    
    ## Authentification
    
    Utiliser un token JWT dans le header Authorization.
    """,
    version="1.0.0",
    contact={
        "name": "Support API",
        "email": "support@example.com"
    },
    license_info={
        "name": "MIT",
        "url": "https://opensource.org/licenses/MIT"
    }
)

class Task(BaseModel):
    """Modèle d'une tâche"""
    
    title: str = Field(
        ...,
        description="Titre de la tâche",
        example="Faire les courses"
    )
    description: str = Field(
        None,
        description="Description détaillée de la tâche",
        example="Acheter du lait, du pain et des œufs"
    )
    completed: bool = Field(
        False,
        description="Statut de complétion",
        example=False
    )

@app.post(
    "/api/tasks",
    response_model=Task,
    summary="Créer une nouvelle tâche",
    description="Crée une nouvelle tâche dans le système",
    response_description="La tâche créée",
    tags=["Tasks"]
)
async def create_task(task: Task):
    """
    Crée une nouvelle tâche avec:
    
    - **title**: Titre obligatoire
    - **description**: Description optionnelle
    - **completed**: Statut (False par défaut)
    """
    return task


# === DOCUMENTATION AVEC FLASK (APISPEC) ===

from flask import Flask
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from flask_apispec import FlaskApiSpec

app = Flask(__name__)

app.config.update({
    'APISPEC_SPEC': APISpec(
        title='API de Gestion de Tâches',
        version='1.0.0',
        openapi_version='3.0.0',
        plugins=[MarshmallowPlugin()],
    ),
    'APISPEC_SWAGGER_URL': '/swagger/',
    'APISPEC_SWAGGER_UI_URL': '/docs/'
})

docs = FlaskApiSpec(app)

@app.route('/api/tasks', methods=['POST'])
def create_task():
    """
    Crée une nouvelle tâche
    ---
    post:
      tags:
        - Tasks
      summary: Créer une tâche
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                  example: "Faire les courses"
      responses:
        201:
          description: Tâche créée
    """
    return {"message": "Tâche créée"}

docs.register(create_task)


[OK] TESTS D'API (PYTEST)

# === POURQUOI TESTER? ===

# Tests = S'assurer que ton API fonctionne correctement
# Types de tests:
#   - Tests unitaires: Tester chaque fonction individuellement
#   - Tests d'intégration: Tester les endpoints complets
#   - Tests de charge: Tester la performance


# === INSTALLATION ===

pip install pytest
pip install pytest-asyncio  # Pour tester FastAPI
pip install httpx  # Client HTTP pour tests


# === TESTS AVEC FASTAPI ===

# Fichier: test_main.py

from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_read_root():
    """Test de l'endpoint racine"""
    response = client.get("/")
    assert response.status_code == 200
    assert "message" in response.json()

def test_create_task():
    """Test de création de tâche"""
    response = client.post(
        "/api/tasks",
        json={
            "title": "Test task",
            "description": "This is a test",
            "priority": 3
        }
    )
    assert response.status_code == 201
    data = response.json()
    assert data["title"] == "Test task"
    assert "id" in data

def test_get_tasks():
    """Test de récupération des tâches"""
    response = client.get("/api/tasks")
    assert response.status_code == 200
    assert isinstance(response.json(), list)

def test_get_task_not_found():
    """Test d'une tâche inexistante"""
    response = client.get("/api/tasks/99999")
    assert response.status_code == 404

def test_update_task():
    """Test de modification de tâche"""
    # Créer d'abord une tâche
    create_response = client.post(
        "/api/tasks",
        json={"title": "Original", "priority": 1}
    )
    task_id = create_response.json()["id"]
    
    # Modifier la tâche
    update_response = client.put(
        f"/api/tasks/{task_id}",
        json={"title": "Modified", "completed": True}
    )
    assert update_response.status_code == 200
    assert update_response.json()["title"] == "Modified"
    assert update_response.json()["completed"] is True

def test_delete_task():
    """Test de suppression de tâche"""
    # Créer une tâche
    create_response = client.post(
        "/api/tasks",
        json={"title": "To delete"}
    )
    task_id = create_response.json()["id"]
    
    # Supprimer la tâche
    delete_response = client.delete(f"/api/tasks/{task_id}")
    assert delete_response.status_code == 204
    
    # Vérifier que la tâche n'existe plus
    get_response = client.get(f"/api/tasks/{task_id}")
    assert get_response.status_code == 404


# === LANCER LES TESTS ===

# Tous les tests:
pytest

# Avec détails:
pytest -v

# Un fichier spécifique:
pytest test_main.py

# Une fonction spécifique:
pytest test_main.py::test_create_task

# Avec couverture:
pytest --cov=main


# === TESTS D'AUTHENTIFICATION ===

def test_register():
    """Test d'inscription"""
    response = client.post(
        "/register",
        json={
            "username": "testuser",
            "email": "test@example.com",
            "password": "testpass123"
        }
    )
    assert response.status_code == 201

def test_login():
    """Test de connexion"""
    # D'abord créer un utilisateur
    client.post(
        "/register",
        json={
            "username": "logintest",
            "email": "login@example.com",
            "password": "pass123"
        }
    )
    
    # Se connecter
    response = client.post(
        "/token",
        data={
            "username": "logintest",
            "password": "pass123"
        }
    )
    assert response.status_code == 200
    assert "access_token" in response.json()
    assert response.json()["token_type"] == "bearer"

def test_protected_endpoint():
    """Test d'un endpoint protégé"""
    # Sans token
    response = client.get("/users/me")
    assert response.status_code == 401
    
    # Avec token
    # ... obtenir un token valide ...
    token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
    response = client.get(
        "/users/me",
        headers={"Authorization": f"Bearer {token}"}
    )
    assert response.status_code == 200


[OK] DÉPLOIEMENT SUR HEROKU

# === FICHIERS NÉCESSAIRES ===

# 1. requirements.txt
fastapi==0.104.1
uvicorn[standard]==0.24.0
sqlalchemy==2.0.23
psycopg2-binary==2.9.9
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-multipart==0.0.6

# 2. Procfile
web: uvicorn main:app --host 0.0.0.0 --port $PORT

# 3. runtime.txt
python-3.11.0

# === DÉPLOIEMENT ===

# Initialiser git:
git init
git add .
git commit -m "Initial commit"

# Se connecter à Heroku:
heroku login

# Créer l'app:
heroku create mon-api

# Ajouter PostgreSQL:
heroku addons:create heroku-postgresql:hobby-dev

# Configurer les variables:
heroku config:set SECRET_KEY="your-secret-key"

# Déployer:
git push heroku main

# Ouvrir:
heroku open


[OK] DÉPLOIEMENT AVEC DOCKER

# === Dockerfile ===

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

# === docker-compose.yml ===

version: '3.9'

services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/apidb
      - SECRET_KEY=your-secret-key
    depends_on:
      - db
  
  db:
    image: postgres:15
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=apidb
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

# === LANCER ===

docker-compose up -d


[OK] BONNES PRATIQUES

# === VERSIONING ===

# Toujours versionner ton API!
# Méthode 1: Dans l'URL
#   GET /api/v1/tasks
#   GET /api/v2/tasks

# Méthode 2: Dans les headers
#   GET /api/tasks
#   Header: Accept: application/vnd.api.v1+json

# Exemple avec FastAPI:
from fastapi import FastAPI

app_v1 = FastAPI()
app_v2 = FastAPI()

@app_v1.get("/tasks")
async def get_tasks_v1():
    return {"version": "1.0", "tasks": []}

@app_v2.get("/tasks")
async def get_tasks_v2():
    return {"version": "2.0", "tasks": [], "total": 0}

app = FastAPI()
app.mount("/api/v1", app_v1)
app.mount("/api/v2", app_v2)


# === PAGINATION ===

# Ne JAMAIS renvoyer tous les résultats d'un coup!
# Toujours paginer:

@app.get("/api/tasks")
async def get_tasks(
    page: int = 1,
    page_size: int = 20,
    db: Session = Depends(get_db)
):
    """
    Récupère les tâches avec pagination
    """
    offset = (page - 1) * page_size
    
    tasks = db.query(models.Task)\
        .offset(offset)\
        .limit(page_size)\
        .all()
    
    total = db.query(models.Task).count()
    
    return {
        "tasks": tasks,
        "page": page,
        "page_size": page_size,
        "total": total,
        "total_pages": (total + page_size - 1) // page_size
    }


# === FILTRAGE ET TRI ===

@app.get("/api/tasks")
async def get_tasks(
    completed: Optional[bool] = None,
    priority: Optional[int] = None,
    sort_by: str = "created_at",
    order: str = "desc",
    db: Session = Depends(get_db)
):
    """
    Récupère les tâches avec filtrage et tri
    """
    query = db.query(models.Task)
    
    # Filtrage
    if completed is not None:
        query = query.filter(models.Task.completed == completed)
    if priority is not None:
        query = query.filter(models.Task.priority == priority)
    
    # Tri
    if order == "desc":
        query = query.order_by(getattr(models.Task, sort_by).desc())
    else:
        query = query.order_by(getattr(models.Task, sort_by).asc())
    
    return query.all()


# === VALIDATION DES DONNÉES ===

# Toujours valider les données entrantes!

from pydantic import BaseModel, Field, validator

class TaskCreate(BaseModel):
    title: str = Field(..., min_length=1, max_length=200)
    priority: int = Field(..., ge=1, le=5)
    
    @validator('title')
    def title_must_not_be_empty(cls, v):
        if not v.strip():
            raise ValueError('Le titre ne peut pas être vide')
        return v.strip()
    
    @validator('priority')
    def priority_must_be_valid(cls, v):
        if v < 1 or v > 5:
            raise ValueError('La priorité doit être entre 1 et 5')
        return v


# === GESTION DES ERREURS ===

# Créer des exceptions personnalisées:

class TaskNotFoundError(Exception):
    """Tâche non trouvée"""
    pass

class ValidationError(Exception):
    """Erreur de validation"""
    pass

# Gérer les exceptions:
@app.exception_handler(TaskNotFoundError)
async def task_not_found_handler(request, exc):
    return JSONResponse(
        status_code=404,
        content={"error": "Tâche non trouvée"}
    )


# === LOGGING ===

import logging

# Configurer le logging:
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

logger = logging.getLogger(__name__)

@app.get("/api/tasks")
async def get_tasks():
    logger.info("Récupération des tâches demandée")
    try:
        tasks = []  # ... logique ...
        logger.info(f"{len(tasks)} tâches récupérées")
        return tasks
    except Exception as e:
        logger.error(f"Erreur lors de la récupération des tâches: {str(e)}")
        raise


# === SÉCURITÉ ===

# 1. HTTPS obligatoire en production
# 2. Validation stricte des entrées
# 3. Rate limiting
# 4. Protection CSRF pour les formulaires
# 5. Headers de sécurité:

from fastapi.middleware.trustedhost import TrustedHostMiddleware
from fastapi.middleware.gzip import GZipMiddleware

app.add_middleware(
    TrustedHostMiddleware,
    allowed_hosts=["example.com", "*.example.com"]
)

app.add_middleware(GZipMiddleware, minimum_size=1000)

# Headers de sécurité:
@app.middleware("http")
async def add_security_headers(request, call_next):
    response = await call_next(request)
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-Frame-Options"] = "DENY"
    response.headers["X-XSS-Protection"] = "1; mode=block"
    return response


[OK] EXEMPLE COMPLET: API DE BLOG

# Structure complète d'une API de blog professionnelle

# === STRUCTURE DU PROJET ===

blog-api/
├── app/
│   ├── __init__.py
│   ├── main.py
│   ├── database.py
│   ├── models/
│   │   ├── __init__.py
│   │   ├── user.py
│   │   └── post.py
│   ├── schemas/
│   │   ├── __init__.py
│   │   ├── user.py
│   │   └── post.py
│   ├── routers/
│   │   ├── __init__.py
│   │   ├── auth.py
│   │   ├── users.py
│   │   └── posts.py
│   ├── auth/
│   │   ├── __init__.py
│   │   ├── jwt.py
│   │   └── dependencies.py
│   └── core/
│       ├── __init__.py
│       ├── config.py
│       └── security.py
├── tests/
│   ├── __init__.py
│   ├── test_auth.py
│   ├── test_users.py
│   └── test_posts.py
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
└── .env.example

# Ce guide couvre TOUT ce dont tu as besoin pour créer des APIs!
# De débutant complet à niveau professionnel.

[OK] RESSOURCES SUPPLÉMENTAIRES

# Documentation officielle:
# FastAPI: https://fastapi.tiangolo.com/
# Flask: https://flask.palletsprojects.com/
# SQLAlchemy: https://www.sqlalchemy.org/
# Pydantic: https://docs.pydantic.dev/

# Outils de test:
# Postman: https://www.postman.com/
# Insomnia: https://insomnia.rest/
# HTTPie: https://httpie.io/

# Standards:
# OpenAPI: https://swagger.io/specification/
# JWT: https://jwt.io/
# REST: https://restfulapi.net/

# Tutoriels:
# FastAPI Tutorial: https://fastapi.tiangolo.com/tutorial/
# Flask Mega-Tutorial: https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world

# === FIN DU GUIDE API ===