# Fichier: python_cheats/cheatsheets/RDS.txt
# Cheatsheet AWS RDS - Guide Complet et Pratique


═══════════════════════════════════════════════════════════════════════════════
[OK] RDS (RELATIONAL DATABASE SERVICE) - C'EST QUOI?
═══════════════════════════════════════════════════════════════════════════════

RDS = "Base de données managée dans le cloud"

Imaginez:
- AU LIEU DE: Installer/gérer vous-même un serveur BD
- VOUS AVEZ: AWS qui gère tout (backups, updates, sécurité, HA)
- VOUS PAYEZ: Seulement pour ce que vous utilisez

ANALOGIE: Louer un administrateur de base de données professionnel
- AWS configure, monitore, patche, sauvegarde
- Vous accédez juste à la DB
- Vous payez mensuellement

MOTEURS SUPPORTÉS:
- MySQL        = Gratuit, populaire, open-source
- PostgreSQL   = Puissant, libre, ultra-fiable
- MariaDB      = Alternative MySQL, compatible
- Oracle DB    = Entreprise, très cher
- SQL Server   = Microsoft, payant
- Aurora       = Version AWS optimisée (5x plus rapide!)

CAS D'USAGE:
- Applications web (stocker utilisateurs, articles, etc.)
- E-commerce (produits, commandes, paiements)
- Analyses et reporting
- Systèmes CRM/ERP
- Data warehouse


═══════════════════════════════════════════════════════════════════════════════
[OK] TARIFICATION RDS (CE QUE VOUS ALLEZ PAYER)
═══════════════════════════════════════════════════════════════════════════════

GRATUIT (Free Tier - 12 mois):
- db.t3.micro: 750 heures/mois (24/7 pendant ~31 jours!)
- 20 GB de stockage (gp2/gp3)
- 20 GB de backups
- Données entrantes: Gratuit
- Données sortantes: 1 GB/mois gratuit

APRÈS FREE TIER (tarif horaire):
- db.t3.micro:    ~$0.02/h  (~$15/mois 24/24)
- db.t3.small:    ~$0.04/h  (~$30/mois 24/24)
- db.t3.medium:   ~$0.08/h  (~$60/mois 24/24)
- db.m5.large:    ~$0.30/h  (~$220/mois 24/24)
- db.r5.large:    ~$0.60/h  (~$450/mois 24/24, optimisé mémoire)

COÛTS ADDITIONNELS:
- Stockage:  ~$0.11 par GB/mois (gp3)
- Backups:   ~$0.023 par GB/mois
- Multi-AZ:  +100% du coût instance
- Data out:  ~$0.02 par GB

EXEMPLE RÉALISTE:
- 1x db.t3.micro + 20 GB gp3: ~$15 + $2.20 = ~$17/mois
- 1x db.t3.small + 50 GB gp3: ~$30 + $5.50 = ~$35/mois
- Production MySQL + Multi-AZ: ~$60 + $60 + $6.50 = ~$126/mois

[IDEE] CONSEIL: RDS Free Tier = EXCELLENT pour apprendre!


═══════════════════════════════════════════════════════════════════════════════
[OK] TERMINOLOGIE CLÉS RDS
═══════════════════════════════════════════════════════════════════════════════

DB Instance:
  = Une base de données individuelle
  = Exemple: mydb, prod-mysql, analytics-db
  = Identifié par son "DB Instance Identifier"

Engine:
  = Type de moteur (mysql, postgres, mariadb, oracle, sqlserver)
  = Une instance = 1 seul moteur
  = Ne peut pas être changé après création

Instance Class:
  = Puissance (CPU, RAM, réseau)
  = db.t3.micro (1 GB RAM) vs db.m5.large (8 GB RAM)
  = Plus grand = Plus puissant et cher

Endpoint:
  = URL + port pour se connecter
  = Exemple: mydb.c9akciq32.us-east-1.rds.amazonaws.com:3306
  = Change si vous migrez ou changez region

Port:
  = 3306 = MySQL
  = 5432 = PostgreSQL
  = 3306 = MariaDB
  = 1521 = Oracle
  = 1433 = SQL Server

Security Group:
  = Pare-feu pour la base
  = Contrôle: qui peut accéder, sur quel port
  = OBLIGATOIRE pour sécurité

Parameter Group:
  = Configuration de la BD (max_connections, timeout, charset, etc.)
  = Appliquées à la création ou via modify
  = Certains paramètres = redémarrage nécessaire

Backup:
  = Sauvegarde complète automatique (quotidienne)
  = Retention: 1-35 jours configurable
  = Utilisé pour recovery et PITR

Snapshot:
  = Photo manuelle d'une BD
  = Gardé indéfiniment (vous décidez quand supprimer)
  = Idéal: avant modification, avant suppression

Read Replica:
  = Copie en lecture seule (asynchrone)
  = Réduit charge du master
  = Peut être en autre région (pour DR)

Multi-AZ:
  = 2 zones de disponibilité différentes
  = Failover automatique en cas de problème
  = 2x plus cher mais haute disponibilité garantie

Aurora:
  = Version AWS ultra-optimisée (MySQL/PostgreSQL)
  = 5x plus rapide, réplication 3 zones automatique
  = Très cher mais performance premium


═══════════════════════════════════════════════════════════════════════════════
[RECHERCHE] EXPLICATIONS DÉTAILLÉES DES COMMANDES AWS CLI
═══════════════════════════════════════════════════════════════════════════════

# Comprendre la structure des commandes AWS RDS:

# SYNTAXE GÉNÉRALE:
# aws rds <action> --paramètre-1 valeur-1 --paramètre-2 valeur-2

# EXEMPLE:
aws rds create-db-instance \
  --db-instance-identifier mydb

# DÉCOMPOSITION:
# "aws"                           = CLI AWS
# "rds"                           = Service (RDS)
# "create-db-instance"            = Action (créer une instance)
# "--db-instance-identifier mydb" = Paramètre (nom de la DB)

# FORMATS DE RÉPONSE:
# Par défaut: JSON (très verbeux)
# --output table  = Format tableau lisible
# --output text   = Texte simple (pour scripts)
# --output json   = JSON explicite

# FILTER & QUERY:
# --query 'DBInstances[0].Endpoint.Address' = Extraire info spécifique
# --filters Name=key,Values=value = Filtrer résultats

# EXEMPLE COMPLET:
aws rds describe-db-instances \
  --db-instance-identifier mydb \
  --query 'DBInstances[0].[DBInstanceIdentifier,DBInstanceStatus,Engine]' \
  --output table

# EXPLICATION:
# describe-db-instances  = Afficher infos
# --db-instance-identifier = Filtrer par ID
# --query                = Extraire colonnes spécifiques
# --output table         = Afficher en tableau


═══════════════════════════════════════════════════════════════════════════════
[OK] PRÉPARER LE RÉSEAU (AVANT DE CRÉER)
═══════════════════════════════════════════════════════════════════════════════

Avant de créer une BD, vous avez besoin:
1. Un Security Group (pare-feu)
2. Optionnel: DB Subnet Group (pour VPC)

# Créer Security Group pour RDS
aws ec2 create-security-group \
  --group-name rds-mysql-sg \
  --description "Security group for RDS MySQL"

# EXPLICATIONS:
# "aws ec2"               = Service EC2 (gère security groups)
# "create-security-group" = Créer nouveau group
# "--group-name"          = Nom du group (lisible)
# "--description"         = Description (optionnel mais recommandé)
#
# RÉSULTAT: sg-0123456789abcdef0
# Cet ID est utilisé partout après!
# Sauvegardez-le!

# Résultat JSON:
# {
#     "GroupId": "sg-0123456789abcdef0",
#     "Tags": []
# }

# Extraire SEULEMENT l'ID:
aws ec2 create-security-group \
  --group-name rds-mysql-sg \
  --description "Security group for RDS MySQL" \
  --query 'GroupId' \
  --output text

# RÉSULTAT: sg-0123456789abcdef0 (plus propre!)

# OPTION 1: Autoriser depuis votre IP (développement)
# Remplacez 203.0.113.25 par VOTRE IP (curl https://icanhazip.com)

aws ec2 authorize-security-group-ingress \
  --group-id sg-0123456789abcdef0 \
  --protocol tcp \
  --port 3306 \
  --cidr 203.0.113.25/32

# EXPLICATION:
# "--port 3306" = Port MySQL standard
# "203.0.113.25/32" = Votre IP exacte
# /32 = Une IP unique (si 0.0.0.0/0 = ouvert à tous, DANGEREUX!)

# OPTION 2: Autoriser depuis instance EC2 (meilleur)
aws ec2 authorize-security-group-ingress \
  --group-id sg-0123456789abcdef0 \
  --protocol tcp \
  --port 3306 \
  --source-group sg-app-0123456789abcdef0

# EXPLICATION:
# Les instances du security group "sg-app-..." peuvent accéder
# Plus sécurisé (pas d'exposition Internet)

# OPTION 3: Autoriser PostgreSQL depuis un range
aws ec2 authorize-security-group-ingress \
  --group-id sg-0123456789abcdef0 \
  --protocol tcp \
  --port 5432 \
  --cidr 192.168.0.0/16

# Vérifier les règles
aws ec2 describe-security-groups \
  --group-ids sg-0123456789abcdef0

# Créer DB Subnet Group (optionnel, pour VPC)
aws rds create-db-subnet-group \
  --db-subnet-group-name my-db-subnets \
  --db-subnet-group-description "My private DB subnets" \
  --subnet-ids subnet-12345678 subnet-87654321

# EXPLICATION:
# "subnet-12345678" et "subnet-87654321" = 2 sous-réseaux différents
# RDS va placer la DB (et replica) dans ces subnets
# OBLIGATOIRE si vous utilisez une VPC privée


═══════════════════════════════════════════════════════════════════════════════
[OK] CRÉER UNE BASE DE DONNÉES
═══════════════════════════════════════════════════════════════════════════════

# CRÉER MYSQL (SIMPLE)
aws rds create-db-instance \
  --db-instance-identifier mydb \
  --db-instance-class db.t3.micro \
  --engine mysql \
  --engine-version 8.0.35 \
  --master-username admin \
  --master-user-password MySecurePassword123! \
  --allocated-storage 20 \
  --storage-type gp3 \
  --vpc-security-group-ids sg-0123456789abcdef0 \
  --backup-retention-period 7 \
  --publicly-accessible

# CRÉER MYSQL (PRODUCTION)
aws rds create-db-instance \
  --db-instance-identifier prod-mysql \
  --db-instance-class db.m5.large \
  --engine mysql \
  --engine-version 8.0.35 \
  --master-username admin \
  --master-user-password MySecurePassword123! \
  --allocated-storage 100 \
  --storage-type gp3 \
  --iops 3000 \
  --vpc-security-group-ids sg-0123456789abcdef0 \
  --db-subnet-group-name my-db-subnets \
  --backup-retention-period 30 \
  --preferred-backup-window "03:00-04:00" \
  --preferred-maintenance-window "mon:04:00-mon:05:00" \
  --multi-az \
  --no-publicly-accessible \
  --enable-cloudwatch-logs-exports error,general,slowquery \
  --deletion-protection

# CRÉER POSTGRESQL
aws rds create-db-instance \
  --db-instance-identifier mypgdb \
  --db-instance-class db.t3.micro \
  --engine postgres \
  --engine-version 15.4 \
  --master-username postgres \
  --master-user-password MySecurePassword123! \
  --allocated-storage 20 \
  --storage-type gp3 \
  --vpc-security-group-ids sg-0123456789abcdef0 \
  --backup-retention-period 7

# EXPLICATIONS DÉTAILLÉES:

# "--db-instance-identifier mydb"
#   = Nom unique de la DB (obligatoire)
#   = Minuscules, chiffres, hyphens (pas d'underscore)
#   = Conseils: prod-mysql, dev-pg, analytics-db, etc.

# "--db-instance-class db.t3.micro"
#   = Puissance de l'instance
#   = COMMON OPTIONS:
#     - db.t3.micro   (2 vCPU, 1 GB RAM)  -> Gratuit 12 mois
#     - db.t3.small   (2 vCPU, 2 GB RAM)  -> ~$30/mois
#     - db.t3.medium  (2 vCPU, 4 GB RAM)  -> ~$60/mois
#     - db.m5.large   (2 vCPU, 8 GB RAM)  -> ~$220/mois
#     - db.r5.large   (2 vCPU, 16 GB RAM) -> ~$450/mois (mémoire opt)
#   = CONSEIL: t3.micro pour apprendre, m5.large pour production

# "--engine mysql" ou "--engine postgres"
#   = Moteur BD: mysql, postgres, mariadb, oracle, sqlserver
#   = [ATTENTION] NE PEUT PAS ÊTRE CHANGÉ APRÈS CRÉATION!

# "--engine-version 8.0.35"
#   = Version exacte du moteur
#   = Omis = AWS choisit la plus récente par défaut
#   = Chercher version stable et récente

# "--master-username admin"
#   = Utilisateur root pour accéder à la DB
#   = Vous l'utiliserez pour connexion initiale
#   = [ATTENTION] NE PEUT PAS ÊTRE CHANGÉ APRÈS CRÉATION!
#   = Conventions: admin, root, postgres (selon moteur)

# "--master-user-password MySecurePassword123!"
#   = Mot de passe root (SÉCURISÉ!)
#   = Minimum 8 caractères, majuscules, chiffres, symboles
#   = [ATTENTION] SAUVEGARDEZ EN SÉCURITÉ! (gestionnaire de mots de passe)
#   = [ATTENTION] NE METTEZ PAS EN DUR DANS LE CODE!

# "--allocated-storage 20"
#   = Taille initiale du disque en GB
#   = MySQL free tier: max 20 GB
#   = Production: 100+ GB courant
#   = [ATTENTION] ON PEUT AUGMENTER, PAS DIMINUER!

# "--storage-type gp3"
#   = Type de stockage:
#     - gp3 = General Purpose (MEILLEUR CHOIX)
#     - gp2 = Plus ancien, compatible
#     - io1 = Haute IOPS (très cher)
#   = CONSEIL: Toujours gp3

# "--iops 3000"
#   = Opérations par seconde (gp3 seulement)
#   = Par défaut: 3000 IOPS
#   = Pour performance: 5000-16000 IOPS
#   = Plus d'IOPS = Plus cher

# "--vpc-security-group-ids sg-0123456789abcdef0"
#   = Pare-feu pour accès à la DB
#   = OBLIGATOIRE!
#   = Doit être créé avant

# "--db-subnet-group-name my-db-subnets"
#   = Subnets VPC où placer la DB
#   = Optionnel si VPC par défaut
#   = Recommandé pour production

# "--backup-retention-period 7"
#   = Combien de jours garder backups auto
#   = 0 = pas de backup auto
#   = 7 = semaine (bon par défaut)
#   = 30-35 = production critique

# "--preferred-backup-window 03:00-04:00"
#   = Quand faire le backup quotidien
#   = Format: HH:MM-HH:MM (UTC)
#   = CONSEIL: Hors heures de pointe

# "--preferred-maintenance-window mon:04:00-mon:05:00"
#   = Quand AWS peut faire maintenance (patches, etc.)
#   = Format: ddd:HH:MM-ddd:HH:MM
#   = CONSEIL: Lundi 04:00 = peu d'utilisateurs

# "--multi-az"
#   = Haute disponibilité (2 zones)
#   = Failover automatique en cas de panne
#   = COÛTE 2x PLUS CHER!
#   = CONSEIL: Production oui, dev non

# "--publicly-accessible"
#   = Accessible depuis Internet (IP publique)
#   = SANS: Seulement depuis EC2/VPC
#   = [ATTENTION] SÉCURITÉ RÉDUITE SI PUBLIC!
#   = CONSEIL: Dev oui, Prod non

# "--enable-cloudwatch-logs-exports error,general,slowquery"
#   = Envoyer logs vers CloudWatch
#   = error = erreurs BD
#   = general = toutes requêtes (lourd!)
#   = slowquery = requêtes lentes (utile!)

# "--deletion-protection"
#   = Protège contre suppression accidentelle
#   = CONSEIL: Pour production


═══════════════════════════════════════════════════════════════════════════════
[OK] LISTER ET VÉRIFIER LES BASES DE DONNÉES
═══════════════════════════════════════════════════════════════════════════════

# Lister toutes les DB instances
aws rds describe-db-instances

# Affichage lisible (tableau)
aws rds describe-db-instances \
  --query 'DBInstances[*].[DBInstanceIdentifier,DBInstanceStatus,Engine,DBInstanceClass,Endpoint.Address]' \
  --output table

# Résultat:
# | DBInstanceIdentifier | DBInstanceStatus | Engine | DBInstanceClass | Endpoint.Address |
# |----------------------|------------------|--------|-----------------|------------------|
# | mydb                 | available        | mysql  | db.t3.micro     | mydb.c9akciq32... |

# Attendre que Status = "available" (5-10 minutes)

# Décrire une DB spécifique
aws rds describe-db-instances --db-instance-identifier mydb

# Récupérer SEULEMENT l'endpoint (URL de connexion)
aws rds describe-db-instances \
  --db-instance-identifier mydb \
  --query 'DBInstances[0].Endpoint.Address' \
  --output text

# Résultat: mydb.c9akciq32.us-east-1.rds.amazonaws.com

# Vérifier en format JSON complet
aws rds describe-db-instances \
  --db-instance-identifier mydb \
  --output json

# Vérifier le port de connexion
aws rds describe-db-instances \
  --db-instance-identifier mydb \
  --query 'DBInstances[0].Endpoint.Port' \
  --output text


═══════════════════════════════════════════════════════════════════════════════
[OK] MODIFIER UNE BASE DE DONNÉES
═══════════════════════════════════════════════════════════════════════════════

# Upgrade classe d'instance
aws rds modify-db-instance \
  --db-instance-identifier mydb \
  --db-instance-class db.t3.small \
  --apply-immediately

# EXPLICATION:
# "db.t3.small" = nouvelle classe (plus puissante)
# "--apply-immediately" = tout de suite (sinon: maintenance window)
# [ATTENTION] ATTENTION: ~1-2 minutes d'indisponibilité!

# Downgrade classe (économiser)
aws rds modify-db-instance \
  --db-instance-identifier mydb \
  --db-instance-class db.t3.micro

# Augmenter stockage (flexible, pas de downtime)
aws rds modify-db-instance \
  --db-instance-identifier mydb \
  --allocated-storage 100 \
  --apply-immediately

# EXPLICATION:
# Augmente tout de suite
# [ATTENTION] ON NE PEUT QUE AUGMENTER!

# Changer mot de passe
aws rds modify-db-instance \
  --db-instance-identifier mydb \
  --master-user-password NewSecurePassword456! \
  --apply-immediately

# EXPLICATION:
# Le nouveau mot de passe s'applique immédiatement
# L'ancien ne fonctionne plus

# Activer Multi-AZ (haute disponibilité)
aws rds modify-db-instance \
  --db-instance-identifier mydb \
  --multi-az \
  --apply-immediately

# EXPLICATION:
# Crée une replica en autre zone
# Failover automatique en cas de panne
# ~3-5 minutes de downtime lors de l'activation

# Désactiver Multi-AZ (économiser)
aws rds modify-db-instance \
  --db-instance-identifier mydb \
  --no-multi-az

# Changer security group
aws rds modify-db-instance \
  --db-instance-identifier mydb \
  --vpc-security-group-ids sg-newgroup0123456789

# Augmenter periode de backup
aws rds modify-db-instance \
  --db-instance-identifier mydb \
  --backup-retention-period 30

# Activer logs CloudWatch
aws rds modify-db-instance \
  --db-instance-identifier mydb \
  --enable-cloudwatch-logs-exports error,slowquery

# Activer Enhanced Monitoring (métriques détaillées)
aws rds modify-db-instance \
  --db-instance-identifier mydb \
  --monitoring-interval 60 \
  --monitoring-role-arn arn:aws:iam::123456789012:role/rds-monitoring

# Activer Performance Insights
aws rds modify-db-instance \
  --db-instance-identifier mydb \
  --enable-performance-insights \
  --performance-insights-retention-period 7


═══════════════════════════════════════════════════════════════════════════════
[OK] ARRÊTER, DÉMARRER, REDÉMARRER, SUPPRIMER
═══════════════════════════════════════════════════════════════════════════════

# ARRÊTER (économiser pendant 7 jours max)
aws rds stop-db-instance --db-instance-identifier mydb

# EXPLICATION:
# La DB s'arrête, données restent intactes
# Vous payez que pour le stockage (~$0.11/GB/mois)
# Plus de charge instance (~$0.02-0.30/h économisé)
# Max 7 jours, puis auto-redémarrage

# QUAND UTILISER:
# - Dev/test (arrêter le soir)
# - Staging (arrêter entre tests)
# - Économiser provisoirement

# DÉMARRER (après arrêt)
aws rds start-db-instance --db-instance-identifier mydb

# EXPLICATION:
# Redémarre une DB arrêtée
# ~30-60 secondes pour être prête

# REDÉMARRER (sans arrêt complet)
aws rds reboot-db-instance --db-instance-identifier mydb

# EXPLICATION:
# Redémarrage complet (comme Ctrl+Alt+Del)
# Connexions fermées, données intactes
# ~1-2 minutes d'indisponibilité

# QUAND UTILISER:
# - Après modification de parameter group
# - Problème connexion
# - Nettoyage mémoire

# SUPPRIMER (avec snapshot final - RECOMMANDÉ)
aws rds delete-db-instance \
  --db-instance-identifier mydb \
  --final-db-snapshot-identifier mydb-final-snapshot

# EXPLICATION:
# La DB est supprimée
# Snapshot créé avant suppression
# Vous pouvez restaurer plus tard si besoin
# [OK] TOUJOURS FAIRE ÇA!

# SUPPRIMER (sans snapshot - [ATTENTION] PERTE DE DONNÉES)
aws rds delete-db-instance \
  --db-instance-identifier mydb \
  --skip-final-snapshot

# [ATTENTION] ATTENTION: IRRÉVERSIBLE! PERTE DÉFINITIVE!
# NE FAITES ÇA QUE SI VOUS ÊTES 100% SÛR!


═══════════════════════════════════════════════════════════════════════════════
[OK] SAUVEGARDES ET SNAPSHOTS
═══════════════════════════════════════════════════════════════════════════════

BACKUPS AUTOMATIQUES vs SNAPSHOTS MANUELS:

Backups Auto:        Snapshots Manuels:
- Quotidien          - À la demande
- 1-35 jours         - Indéfinis
- Gratuit (inclus)   - Coûtent ($)
- PITR possible      - Pour archivage

# Créer snapshot MANUEL
aws rds create-db-snapshot \
  --db-instance-identifier mydb \
  --db-snapshot-identifier mydb-snapshot-$(date +%Y%m%d)

# Résultat: mydb-snapshot-20240115

# QUAND CRÉER:
# - Avant grosse modification
# - Avant suppression
# - Archive long-terme
# - Clone de prod pour test

# Lister TOUS les snapshots
aws rds describe-db-snapshots

# Lister snapshots spécifiques (manuels seulement)
aws rds describe-db-snapshots \
  --filters Name=db-instance-id,Values=mydb

# Format lisible
aws rds describe-db-snapshots \
  --query 'DBSnapshots[*].[DBSnapshotIdentifier,DBInstanceIdentifier,SnapshotCreateTime,AllocatedStorage]' \
  --output table

# Copier snapshot vers autre région (Disaster Recovery)
aws rds copy-db-snapshot \
  --source-db-snapshot-identifier arn:aws:rds:us-east-1:123456789012:snapshot:mydb-snapshot \
  --target-db-snapshot-identifier mydb-snapshot-eu \
  --region eu-west-1

# Restaurer DB à partir de snapshot
aws rds restore-db-instance-from-db-snapshot \
  --db-instance-identifier mydb-restored \
  --db-snapshot-identifier mydb-snapshot-20240115

# EXPLICATION:
# Crée une NOUVELLE DB à partir du snapshot
# Identique aux données du moment du snapshot
# Les deux BD coexistent (pas d'overwrite)

# UTILISATIONS:
# - Cloner prod pour test
# - Récupérer après erreur (avant snapshot)
# - Migrer entre régions

# Restaurer à point dans le temps (PITR)
aws rds restore-db-instance-to-point-in-time \
  --source-db-instance-identifier mydb \
  --target-db-instance-identifier mydb-pitr \
  --restore-time 2024-01-15T10:30:00Z

# EXPLICATION:
# Restaure à un moment PRÉCIS
# Pas besoin de snapshot pour ça
# Les backups auto doivent couvrir cette période

# EXEMPLE:
# À 14h30 vous avez supprimé 1000 clients par erreur
# Vous faites: PITR à 14h25 (avant l'erreur)
# Vous récupérez les 1000 clients!

# Supprimer un snapshot (économiser)
aws rds delete-db-snapshot \
  --db-snapshot-identifier mydb-snapshot-20240115

# [ATTENTION] ATTENTION: IRRÉVERSIBLE! Pas de snapshot = pas de récupération!


═══════════════════════════════════════════════════════════════════════════════
[OK] READ REPLICAS (SCALING EN LECTURE)
═══════════════════════════════════════════════════════════════════════════════

READ REPLICA = Copie en lecture seule (asynchrone)

UTILITÉ:
- Réduire charge sur DB principale
- Rapports lourds sans impact prod
- Géographique failover
- Haute disponibilité

# Créer read replica (même région)
aws rds create-db-instance-read-replica \
  --db-instance-identifier mydb-replica \
  --source-db-instance-identifier mydb \
  --db-instance-class db.t3.micro

# Créer read replica (autre région)
aws rds create-db-instance-read-replica \
  --db-instance-identifier mydb-replica-eu \
  --source-db-instance-identifier mydb \
  --source-region us-east-1 \
  --region eu-west-1 \
  --db-instance-class db.t3.micro

# EXPLICATION:
# La replica est créée dans eu-west-1
# Réplication asynchrone depuis us-east-1
# Décalage: quelques secondes à ms

# Promouvoir replica en DB indépendante
aws rds promote-read-replica \
  --db-instance-identifier mydb-replica

# EXPLICATION:
# La replica devient indépendante
# Plus liée à la source
# Vous pouvez l'écrire maintenant

# UTILITÉ:
# - Avant migration
# - Test sur copie de prod
# - Créer une seconde DB


═══════════════════════════════════════════════════════════════════════════════
[OK] PARAMETER GROUPS (CONFIGURATION AVANCÉE)
═══════════════════════════════════════════════════════════════════════════════

PARAMETER GROUPS = Configuration BD (max_connections, charset, etc.)

# Créer custom parameter group
aws rds create-db-parameter-group \
  --db-parameter-group-name mydb-params \
  --db-parameter-group-family mysql8.0 \
  --description "Custom MySQL 8.0 parameters"

# EXPLICATION:
# "mysql8.0" = Famille (MySQL 8.0 seulement compatible)
# Doit correspondre à votre version moteur

# Modifier paramètres
aws rds modify-db-parameter-group \
  --db-parameter-group-name mydb-params \
  --parameters "ParameterName=max_connections,ParameterValue=500,ApplyMethod=immediate"

# Paramètres courants MySQL:
# max_connections = Max connexions (défaut: 152)
# character_set_server = Charset (utf8mb4)
# slow_query_log = Log requêtes lentes (0/1)
# long_query_time = Seuil requête lente (2 secondes)

# Appliquer parameter group à DB
aws rds modify-db-instance \
  --db-instance-identifier mydb \
  --db-parameter-group-name mydb-params

# Lister parameter groups
aws rds describe-db-parameter-groups

# Voir paramètres d'un group
aws rds describe-db-parameters \
  --db-parameter-group-name mydb-params

# Voir seulement paramètres MODIFIÉS
aws rds describe-db-parameters \
  --db-parameter-group-name mydb-params \
  --filters Name=isModified,Values=true


═══════════════════════════════════════════════════════════════════════════════
[OK] SE CONNECTER À LA BASE DE DONNÉES
═══════════════════════════════════════════════════════════════════════════════

Avant: Obtenir l'endpoint!
aws rds describe-db-instances \
  --db-instance-identifier mydb \
  --query 'DBInstances[0].Endpoint.Address' \
  --output text

# Résultat: mydb.c9akciq32.us-east-1.rds.amazonaws.com

# === MYSQL EN LIGNE DE COMMANDE ===

# Se connecter avec MySQL CLI
mysql -h mydb.c9akciq32.us-east-1.rds.amazonaws.com \
  -P 3306 \
  -u admin \
  -p

# Invite pour mot de passe (plus sécurisé)
# Taper: MySecurePassword123!
# Vous êtes connecté!

# Commandes MySQL basiques:
# SHOW DATABASES;           = Lister bases
# USE mydb;                 = Utiliser base
# CREATE TABLE users (...); = Créer table
# SHOW TABLES;              = Lister tables
# SELECT * FROM users;      = Voir données
# INSERT INTO users ...;    = Ajouter données
# exit;                     = Quitter

# === POSTGRESQL EN LIGNE DE COMMANDE ===

# Se connecter avec psql
psql -h mypgdb.c9akciq32.us-east-1.rds.amazonaws.com \
  -p 5432 \
  -U postgres \
  -d postgres

# Invite pour mot de passe
# Taper: MySecurePassword123!
# Vous êtes connecté!

# Commandes PostgreSQL basiques:
# \l                 = Lister bases
# \c mydb            = Changer base
# \dt                = Lister tables
# \d table_name      = Structure table
# SELECT * FROM ...  = Requête
# \q                 = Quitter

# === DEPUIS PYTHON (MySQL) ===

import pymysql
import os

# Sécurisé: variables d'environnement
host = os.environ.get('DB_HOST')
user = os.environ.get('DB_USER')
password = os.environ.get('DB_PASSWORD')
database = os.environ.get('DB_NAME')

try:
    connection = pymysql.connect(
        host=host,
        user=user,
        password=password,
        database=database,
        port=3306,
        cursorclass=pymysql.cursors.DictCursor
    )
    
    with connection:
        with connection.cursor() as cursor:
            cursor.execute("SELECT * FROM users;")
            result = cursor.fetchall()
            print(result)
            
except pymysql.Error as e:
    print(f"Erreur: {e}")

# Installation:
# pip install pymysql

# === DEPUIS PYTHON (PostgreSQL) ===

import psycopg2
import os

host = os.environ.get('DB_HOST')
user = os.environ.get('DB_USER')
password = os.environ.get('DB_PASSWORD')
database = os.environ.get('DB_NAME')

try:
    connection = psycopg2.connect(
        host=host,
        user=user,
        password=password,
        database=database,
        port=5432
    )
    
    cursor = connection.cursor()
    cursor.execute("SELECT * FROM users;")
    result = cursor.fetchall()
    print(result)
    cursor.close()
    connection.close()
    
except psycopg2.Error as e:
    print(f"Erreur: {e}")

# Installation:
# pip install psycopg2-binary

# === DEPUIS PYTHON (SQLAlchemy - RECOMMANDÉ) ===

from sqlalchemy import create_engine
import os

# MySQL
engine = create_engine(
    f"mysql+pymysql://{os.getenv('DB_USER')}:{os.getenv('DB_PASSWORD')}"
    f"@{os.getenv('DB_HOST')}/{os.getenv('DB_NAME')}"
)

# PostgreSQL
engine = create_engine(
    f"postgresql+psycopg2://{os.getenv('DB_USER')}:{os.getenv('DB_PASSWORD')}"
    f"@{os.getenv('DB_HOST')}/{os.getenv('DB_NAME')}"
)

# Tester connexion
with engine.connect() as conn:
    result = conn.execute("SELECT 1")
    print(result.fetchone())

# Installation:
# pip install sqlalchemy pymysql psycopg2-binary

# [ATTENTION] SÉCURITÉ: Variables d'environnement
# NE METTEZ JAMAIS le mot de passe en dur!
# Créer .env (git ignore) ou utiliser AWS Secrets Manager


═══════════════════════════════════════════════════════════════════════════════
[OK] INTÉGRATION FLASK + RDS - GUIDE COMPLET
═══════════════════════════════════════════════════════════════════════════════

OBJECTIF: Créer une application Flask qui communique avec RDS

ÉTAPES:
1. Préparer RDS (créer DB, endpoint, credentials)
2. Installer dépendances Python
3. Configurer connexion (variables d'env, secrets)
4. Créer modèles de données
5. Écrire routes Flask
6. Tester l'intégration
7. Déployer

ÉTAPE 1: PRÉPARER RDS
════════════════════════════════════════════════════════════════════════════════

# Créer security group (depuis machine locale ou EC2)
SG_ID=$(aws ec2 create-security-group \
  --group-name flask-rds-sg \
  --description "Security group for Flask + RDS" \
  --query 'GroupId' \
  --output text)

echo "Security Group créé: $SG_ID"

# Autoriser depuis votre IP (DEV SEULEMENT!)
YOUR_IP="203.0.113.25"  # Remplacer par VOTRE IP
aws ec2 authorize-security-group-ingress \
  --group-id $SG_ID \
  --protocol tcp \
  --port 3306 \
  --cidr $YOUR_IP/32

# Créer DB MySQL
aws rds create-db-instance \
  --db-instance-identifier flask-app-db \
  --db-instance-class db.t3.micro \
  --engine mysql \
  --engine-version 8.0.35 \
  --master-username admin \
  --master-user-password SecureFlaskPassword123! \
  --allocated-storage 20 \
  --storage-type gp3 \
  --vpc-security-group-ids $SG_ID \
  --backup-retention-period 7 \
  --publicly-accessible

# Attendre que DB soit ready
echo "Attendre ~10 minutes..."
aws rds wait db-instance-available \
  --db-instance-identifier flask-app-db

# Récupérer l'endpoint
ENDPOINT=$(aws rds describe-db-instances \
  --db-instance-identifier flask-app-db \
  --query 'DBInstances[0].Endpoint.Address' \
  --output text)

echo "Endpoint: $ENDPOINT"
echo "Host: $ENDPOINT"
echo "Port: 3306"
echo "User: admin"
echo "Password: SecureFlaskPassword123!"


ÉTAPE 2: INSTALLER DÉPENDANCES PYTHON
════════════════════════════════════════════════════════════════════════════════

# Créer environnement virtuel
python3 -m venv venv
source venv/bin/activate  # Linux/Mac
# venv\Scripts\activate  # Windows

# Créer requirements.txt
cat > requirements.txt << 'EOF'
Flask==2.3.0
Flask-MySQL==1.5.2
python-dotenv==1.0.0
pymysql==1.1.0
SQLAlchemy==2.0.0
Flask-SQLAlchemy==3.0.0
EOF

# Installer dépendances
pip install -r requirements.txt

# EXPLICATIONS:
# Flask              = Framework web
# Flask-MySQL        = Intégration MySQL simple
# python-dotenv      = Charger variables .env
# pymysql            = Driver MySQL
# SQLAlchemy         = ORM (Object Relational Mapping)
# Flask-SQLAlchemy   = Extension Flask pour SQLAlchemy


ÉTAPE 3: CONFIGURER CONNEXION (Variables d'environnement)
════════════════════════════════════════════════════════════════════════════════

# Créer fichier .env (NE PAS COMMIT DANS GIT!)
cat > .env << 'EOF'
# RDS Configuration
DB_HOST=flask-app-db.c9akciq32.us-east-1.rds.amazonaws.com
DB_PORT=3306
DB_USER=admin
DB_PASSWORD=SecureFlaskPassword123!
DB_NAME=flask_app

# Flask
FLASK_ENV=development
FLASK_DEBUG=1
SECRET_KEY=your-super-secret-key-change-this

# AWS (optionnel)
AWS_REGION=us-east-1
EOF

# Ajouter .env à .gitignore
echo ".env" >> .gitignore

# EXPLICATIONS:
# Ces variables seront chargées par python-dotenv
# Jamais exposées en dur dans le code
# Facile à changer par environnement (dev/prod)


ÉTAPE 4: CRÉER FICHIER app.py (PRINCIPALE)
════════════════════════════════════════════════════════════════════════════════

# app.py

import os
from flask import Flask, render_template, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from dotenv import load_dotenv
from datetime import datetime

# Charger variables d'environnement
load_dotenv()

# Initialiser Flask
app = Flask(__name__)

# Configurer SQLAlchemy
app.config['SQLALCHEMY_DATABASE_URI'] = (
    f"mysql+pymysql://{os.getenv('DB_USER')}:{os.getenv('DB_PASSWORD')}"
    f"@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_NAME')}"
)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# EXPLICATIONS:
# SQLALCHEMY_DATABASE_URI = URL de connexion complète
#   Format: mysql+pymysql://user:password@host:port/database
# SQLALCHEMY_TRACK_MODIFICATIONS = Désactiver warning inutile

# Initialiser SQLAlchemy (ORM)
db = SQLAlchemy(app)

# EXPLICATIONS:
# SQLAlchemy = "Langage Python pour bases de données"
# Permet de définir tables comme classes Python
# Pas besoin d'écrire SQL directement!


ÉTAPE 5: DÉFINIR MODÈLES DE DONNÉES (Models)
════════════════════════════════════════════════════════════════════════════════

# Ajouter à app.py (après initialiser db)

# Modèle Utilisateur
class User(db.Model):
    __tablename__ = 'users'  # Nom de la table en BD
    
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password = db.Column(db.String(255), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    # Relation avec Posts
    posts = db.relationship('Post', backref='author', lazy=True, cascade='all, delete-orphan')
    
    def __repr__(self):
        return f'<User {self.username}>'
    
    def to_dict(self):
        return {
            'id': self.id,
            'username': self.username,
            'email': self.email,
            'created_at': self.created_at.isoformat()
        }

# EXPLICATIONS:
# __tablename__         = Nom table en BD (users)
# db.Column             = Colonne de table
# db.Integer            = Type: entier
# db.String(80)         = Type: texte max 80 caractères
# primary_key=True      = Clé primaire
# unique=True           = Valeur unique (pas doublons)
# nullable=False        = Obligatoire (pas NULL)
# default=datetime.utcnow = Valeur par défaut
# onupdate=datetime.utcnow = Auto-mise à jour
# db.relationship       = Lien vers autre table
# cascade='all,delete-orphan' = Supprimer posts si user supprimé
# to_dict()             = Convertir en JSON


# Modèle Article (Post)
class Post(db.Model):
    __tablename__ = 'posts'
    
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    content = db.Column(db.Text, nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    def __repr__(self):
        return f'<Post {self.title}>'
    
    def to_dict(self):
        return {
            'id': self.id,
            'title': self.title,
            'content': self.content,
            'author': self.author.username,
            'created_at': self.created_at.isoformat()
        }

# EXPLICATIONS:
# db.ForeignKey('users.id') = Lien vers table users (contrainte BD)
# Garantit intégrité: user_id doit exister


ÉTAPE 6: ROUTES FLASK (API)
════════════════════════════════════════════════════════════════════════════════

# Ajouter à app.py (après modèles)

# === ROUTE: Créer les tables ===
@app.route('/init-db', methods=['POST'])
def init_db():
    """Créer les tables si elles n'existent pas"""
    try:
        db.create_all()
        return jsonify({
            'status': 'success',
            'message': 'Tables créées avec succès'
        }), 201
    except Exception as e:
        return jsonify({
            'status': 'error',
            'message': str(e)
        }), 500

# EXPLICATION:
# POST /init-db = Crée les tables en BD
# db.create_all() = Crée toutes les tables définies
# À faire UNE SEULE FOIS!

# === ROUTE: Créer utilisateur ===
@app.route('/api/users', methods=['POST'])
def create_user():
    """Créer un nouvel utilisateur"""
    data = request.get_json()
    
    # Validation
    if not data or not all(k in data for k in ['username', 'email', 'password']):
        return jsonify({'error': 'Données manquantes'}), 400
    
    # Vérifier si user existe déjà
    if User.query.filter_by(username=data['username']).first():
        return jsonify({'error': 'Username existant'}), 400
    
    if User.query.filter_by(email=data['email']).first():
        return jsonify({'error': 'Email existant'}), 400
    
    # Créer nouvel utilisateur
    try:
        user = User(
            username=data['username'],
            email=data['email'],
            password=data['password']  # À HASHER en production!
        )
        db.session.add(user)
        db.session.commit()
        
        return jsonify({
            'status': 'success',
            'user': user.to_dict()
        }), 201
    except Exception as e:
        db.session.rollback()
        return jsonify({'error': str(e)}), 500

# EXPLICATION:
# POST /api/users = Crée utilisateur via JSON
# request.get_json() = Récupère données POST
# db.session.add() = Préparer insertion
# db.session.commit() = Confirmer insertion
# db.session.rollback() = Annuler si erreur

# === ROUTE: Lister tous les utilisateurs ===
@app.route('/api/users', methods=['GET'])
def get_users():
    """Récupérer tous les utilisateurs"""
    try:
        users = User.query.all()
        return jsonify({
            'status': 'success',
            'users': [user.to_dict() for user in users]
        }), 200
    except Exception as e:
        return jsonify({'error': str(e)}), 500

# EXPLICATION:
# GET /api/users = Liste tous les users
# User.query.all() = SELECT * FROM users;

# === ROUTE: Récupérer un utilisateur ===
@app.route('/api/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
    """Récupérer un utilisateur par ID"""
    try:
        user = User.query.get(user_id)
        if not user:
            return jsonify({'error': 'Utilisateur non trouvé'}), 404
        
        return jsonify({
            'status': 'success',
            'user': user.to_dict(),
            'posts': [post.to_dict() for post in user.posts]
        }), 200
    except Exception as e:
        return jsonify({'error': str(e)}), 500

# EXPLICATION:
# GET /api/users/1 = Récupère user ID 1
# User.query.get(user_id) = SELECT * FROM users WHERE id = ?

# === ROUTE: Modifier un utilisateur ===
@app.route('/api/users/<int:user_id>', methods=['PUT'])
def update_user(user_id):
    """Modifier un utilisateur"""
    data = request.get_json()
    
    try:
        user = User.query.get(user_id)
        if not user:
            return jsonify({'error': 'Utilisateur non trouvé'}), 404
        
        if 'email' in data:
            user.email = data['email']
        if 'username' in data:
            user.username = data['username']
        
        db.session.commit()
        
        return jsonify({
            'status': 'success',
            'user': user.to_dict()
        }), 200
    except Exception as e:
        db.session.rollback()
        return jsonify({'error': str(e)}), 500

# EXPLICATION:
# PUT /api/users/1 = Modifie user ID 1
# Modifie colonnes spécifiées

# === ROUTE: Supprimer un utilisateur ===
@app.route('/api/users/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
    """Supprimer un utilisateur"""
    try:
        user = User.query.get(user_id)
        if not user:
            return jsonify({'error': 'Utilisateur non trouvé'}), 404
        
        db.session.delete(user)
        db.session.commit()
        
        return jsonify({
            'status': 'success',
            'message': f'Utilisateur {user_id} supprimé'
        }), 200
    except Exception as e:
        db.session.rollback()
        return jsonify({'error': str(e)}), 500

# EXPLICATION:
# DELETE /api/users/1 = Supprime user ID 1
# db.session.delete() = Marquer pour suppression
# CASCADE = Les posts sont aussi supprimés

# === ROUTE: Créer un article ===
@app.route('/api/posts', methods=['POST'])
def create_post():
    """Créer un nouvel article"""
    data = request.get_json()
    
    if not data or not all(k in data for k in ['title', 'content', 'user_id']):
        return jsonify({'error': 'Données manquantes'}), 400
    
    try:
        # Vérifier que l'utilisateur existe
        user = User.query.get(data['user_id'])
        if not user:
            return jsonify({'error': 'Utilisateur non trouvé'}), 404
        
        post = Post(
            title=data['title'],
            content=data['content'],
            user_id=data['user_id']
        )
        db.session.add(post)
        db.session.commit()
        
        return jsonify({
            'status': 'success',
            'post': post.to_dict()
        }), 201
    except Exception as e:
        db.session.rollback()
        return jsonify({'error': str(e)}), 500

# === ROUTE: Récupérer tous les articles ===
@app.route('/api/posts', methods=['GET'])
def get_posts():
    """Récupérer tous les articles"""
    try:
        posts = Post.query.all()
        return jsonify({
            'status': 'success',
            'posts': [post.to_dict() for post in posts]
        }), 200
    except Exception as e:
        return jsonify({'error': str(e)}), 500

# === ROUTE: Health Check ===
@app.route('/health', methods=['GET'])
def health():
    """Vérifier que l'app et BD sont OK"""
    try:
        # Tester connexion BD
        db.session.execute('SELECT 1')
        return jsonify({
            'status': 'healthy',
            'database': 'connected'
        }), 200
    except Exception as e:
        return jsonify({
            'status': 'unhealthy',
            'error': str(e)
        }), 500

# EXPLICATION:
# GET /health = Vérifier l'app
# SELECT 1 = Requête simple pour tester connexion

# === ROUTE: Accueil ===
@app.route('/', methods=['GET'])
def index():
    """Page d'accueil avec docs API"""
    return jsonify({
        'message': 'API Flask + RDS',
        'version': '1.0',
        'endpoints': {
            'POST /init-db': 'Créer les tables',
            'POST /api/users': 'Créer utilisateur',
            'GET /api/users': 'Lister utilisateurs',
            'GET /api/users/<id>': 'Récupérer utilisateur',
            'PUT /api/users/<id>': 'Modifier utilisateur',
            'DELETE /api/users/<id>': 'Supprimer utilisateur',
            'POST /api/posts': 'Créer article',
            'GET /api/posts': 'Lister articles',
            'GET /health': 'Vérifier santé app'
        }
    }), 200


ÉTAPE 7: LANCER L'APPLICATION
════════════════════════════════════════════════════════════════════════════════

# Ajouter à la fin de app.py

if __name__ == '__main__':
    with app.app_context():
        print("Connecté à la base de données...")
        try:
            # Test connexion
            db.session.execute('SELECT 1')
            print("[OK] Connexion OK!")
        except Exception as e:
            print(f"[X] Erreur connexion: {e}")
    
    # Lancer le serveur
    app.run(
        host='0.0.0.0',
        port=5000,
        debug=True
    )

# EXPLICATION:
# host='0.0.0.0' = Accessible depuis n'importe où (DEV SEULEMENT!)
# port=5000 = Port HTTP
# debug=True = Rechargement auto, meilleur error display


ÉTAPE 8: TESTER L'APPLICATION
════════════════════════════════════════════════════════════════════════════════

# Terminal 1: Lancer Flask
python app.py

# Résultat:
# * Running on http://127.0.0.1:5000

# Terminal 2: Tester l'API

# 1. Initialiser la base
curl -X POST http://localhost:5000/init-db

# 2. Vérifier santé
curl http://localhost:5000/health

# 3. Créer un utilisateur
curl -X POST http://localhost:5000/api/users \
  -H "Content-Type: application/json" \
  -d '{
    "username": "alice",
    "email": "alice@example.com",
    "password": "password123"
  }'

# 4. Lister utilisateurs
curl http://localhost:5000/api/users

# 5. Récupérer un utilisateur
curl http://localhost:5000/api/users/1

# 6. Créer un article
curl -X POST http://localhost:5000/api/posts \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Mon premier post",
    "content": "Contenu du post",
    "user_id": 1
  }'

# 7. Récupérer articles
curl http://localhost:5000/api/posts


ÉTAPE 9: FICHIER COMPLET app.py
════════════════════════════════════════════════════════════════════════════════

# app.py (VERSION COMPLÈTE)

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

load_dotenv()

app = Flask(__name__)

# Configuration base de données
app.config['SQLALCHEMY_DATABASE_URI'] = (
    f"mysql+pymysql://{os.getenv('DB_USER')}:{os.getenv('DB_PASSWORD')}"
    f"@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_NAME')}"
)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')

db = SQLAlchemy(app)

# ===== MODÈLES =====

class User(db.Model):
    __tablename__ = 'users'
    
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password = db.Column(db.String(255), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    posts = db.relationship('Post', backref='author', cascade='all, delete-orphan')
    
    def to_dict(self):
        return {
            'id': self.id,
            'username': self.username,
            'email': self.email,
            'created_at': self.created_at.isoformat()
        }

class Post(db.Model):
    __tablename__ = 'posts'
    
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    content = db.Column(db.Text, nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    def to_dict(self):
        return {
            'id': self.id,
            'title': self.title,
            'content': self.content,
            'author': self.author.username,
            'created_at': self.created_at.isoformat()
        }

# ===== ROUTES =====

@app.route('/', methods=['GET'])
def index():
    return jsonify({'message': 'API Flask + RDS'}), 200

@app.route('/init-db', methods=['POST'])
def init_db():
    try:
        db.create_all()
        return jsonify({'status': 'success', 'message': 'Tables créées'}), 201
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/health', methods=['GET'])
def health():
    try:
        db.session.execute('SELECT 1')
        return jsonify({'status': 'healthy', 'database': 'connected'}), 200
    except Exception as e:
        return jsonify({'status': 'unhealthy', 'error': str(e)}), 500

@app.route('/api/users', methods=['POST'])
def create_user():
    data = request.get_json()
    if not data or not all(k in data for k in ['username', 'email', 'password']):
        return jsonify({'error': 'Données manquantes'}), 400
    
    try:
        user = User(username=data['username'], email=data['email'], password=data['password'])
        db.session.add(user)
        db.session.commit()
        return jsonify({'status': 'success', 'user': user.to_dict()}), 201
    except Exception as e:
        db.session.rollback()
        return jsonify({'error': str(e)}), 500

@app.route('/api/users', methods=['GET'])
def get_users():
    try:
        users = User.query.all()
        return jsonify({'users': [u.to_dict() for u in users]}), 200
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
    try:
        user = User.query.get(user_id)
        if not user:
            return jsonify({'error': 'Non trouvé'}), 404
        return jsonify({'user': user.to_dict()}), 200
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/users/<int:user_id>', methods=['PUT'])
def update_user(user_id):
    data = request.get_json()
    try:
        user = User.query.get(user_id)
        if not user:
            return jsonify({'error': 'Non trouvé'}), 404
        if 'email' in data:
            user.email = data['email']
        db.session.commit()
        return jsonify({'user': user.to_dict()}), 200
    except Exception as e:
        db.session.rollback()
        return jsonify({'error': str(e)}), 500

@app.route('/api/users/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
    try:
        user = User.query.get(user_id)
        if not user:
            return jsonify({'error': 'Non trouvé'}), 404
        db.session.delete(user)
        db.session.commit()
        return jsonify({'status': 'supprimé'}), 200
    except Exception as e:
        db.session.rollback()
        return jsonify({'error': str(e)}), 500

@app.route('/api/posts', methods=['POST'])
def create_post():
    data = request.get_json()
    if not all(k in data for k in ['title', 'content', 'user_id']):
        return jsonify({'error': 'Données manquantes'}), 400
    try:
        post = Post(title=data['title'], content=data['content'], user_id=data['user_id'])
        db.session.add(post)
        db.session.commit()
        return jsonify({'post': post.to_dict()}), 201
    except Exception as e:
        db.session.rollback()
        return jsonify({'error': str(e)}), 500

@app.route('/api/posts', methods=['GET'])
def get_posts():
    try:
        posts = Post.query.all()
        return jsonify({'posts': [p.to_dict() for p in posts]}), 200
    except Exception as e:
        return jsonify({'error': str(e)}), 500

if __name__ == '__main__':
    with app.app_context():
        try:
            db.session.execute('SELECT 1')
            print("[OK] Connexion BD OK!")
        except Exception as e:
            print(f"[X] Erreur: {e}")
    app.run(host='0.0.0.0', port=5000, debug=True)



# ============================================================================
# EXEMPLE COMPLET: Application Flask + RDS
# ============================================================================
# Cette application Blog permet:
# - Créer/lire/modifier/supprimer utilisateurs
# - Créer/lire/modifier/supprimer articles
# - Authentification JWT
# - Validation et gestion d'erreurs
# ============================================================================

# === 1. FICHIER: requirements.txt ===
"""
Flask==2.3.0
Flask-SQLAlchemy==3.0.0
Flask-JWT-Extended==4.4.4
Flask-CORS==4.0.0
python-dotenv==1.0.0
pymysql==1.1.0
werkzeug==2.3.0
"""

# === 2. FICHIER: .env ===
"""
# RDS Configuration
DB_HOST=your-db.c9akciq32.us-east-1.rds.amazonaws.com
DB_PORT=3306
DB_USER=admin
DB_PASSWORD=YourSecurePassword123!
DB_NAME=flask_blog

# Flask
FLASK_ENV=development
FLASK_DEBUG=1
SECRET_KEY=change-this-to-something-very-secret-in-production
JWT_SECRET_KEY=jwt-secret-key-change-this-too

# API
API_TITLE=Blog API
API_VERSION=1.0.0
"""

# === 3. FICHIER: app.py (APPLICATION PRINCIPALE) ===

import os
from datetime import datetime
from functools import wraps
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity
from flask_cors import CORS
from werkzeug.security import generate_password_hash, check_password_hash
from dotenv import load_dotenv

# Charger variables d'environnement
load_dotenv()

# ========== INITIALISATION FLASK ==========

app = Flask(__name__)

# Configuration
app.config['SQLALCHEMY_DATABASE_URI'] = (
    f"mysql+pymysql://{os.getenv('DB_USER')}:{os.getenv('DB_PASSWORD')}"
    f"@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_NAME')}"
)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')
app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY')

# Initialiser extensions
db = SQLAlchemy(app)
jwt = JWTManager(app)
CORS(app)

# ========== MODÈLES BD ==========

class User(db.Model):
    """Modèle utilisateur"""
    __tablename__ = 'users'
    
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False, index=True)
    email = db.Column(db.String(120), unique=True, nullable=False, index=True)
    password_hash = db.Column(db.String(255), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    # Relations
    articles = db.relationship('Article', backref='author', lazy=True, cascade='all, delete-orphan')
    
    def set_password(self, password):
        """Hash et stocke le mot de passe"""
        self.password_hash = generate_password_hash(password, method='pbkdf2:sha256')
    
    def check_password(self, password):
        """Vérifie si le mot de passe correspond"""
        return check_password_hash(self.password_hash, password)
    
    def to_dict(self, include_email=False):
        """Convertir en dictionnaire JSON"""
        data = {
            'id': self.id,
            'username': self.username,
            'created_at': self.created_at.isoformat(),
            'articles_count': len(self.articles)
        }
        if include_email:
            data['email'] = self.email
        return data

class Article(db.Model):
    """Modèle article/blog post"""
    __tablename__ = 'articles'
    
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False, index=True)
    content = db.Column(db.Text, nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow, index=True)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    views_count = db.Column(db.Integer, default=0)
    
    def to_dict(self, include_content=False):
        """Convertir en dictionnaire JSON"""
        data = {
            'id': self.id,
            'title': self.title,
            'author': self.author.username,
            'author_id': self.user_id,
            'views_count': self.views_count,
            'created_at': self.created_at.isoformat(),
            'updated_at': self.updated_at.isoformat()
        }
        if include_content:
            data['content'] = self.content
        return data

# ========== ROUTES: UTILISATEURS ==========

@app.route('/api/auth/register', methods=['POST'])
def register():
    """Créer un nouvel utilisateur"""
    data = request.get_json()
    
    # Validation
    if not data or not all(k in data for k in ['username', 'email', 'password']):
        return jsonify({'error': 'username, email et password obligatoires'}), 400
    
    if len(data['username']) < 3 or len(data['username']) > 80:
        return jsonify({'error': 'username: 3-80 caractères'}), 400
    
    if len(data['password']) < 8:
        return jsonify({'error': 'Mot de passe: minimum 8 caractères'}), 400
    
    # Vérifier doublons
    if User.query.filter_by(username=data['username']).first():
        return jsonify({'error': 'Username existant'}), 409
    
    if User.query.filter_by(email=data['email']).first():
        return jsonify({'error': 'Email existant'}), 409
    
    try:
        user = User(username=data['username'], email=data['email'])
        user.set_password(data['password'])
        
        db.session.add(user)
        db.session.commit()
        
        return jsonify({
            'message': 'Utilisateur créé avec succès',
            'user': user.to_dict(include_email=True)
        }), 201
    except Exception as e:
        db.session.rollback()
        return jsonify({'error': f'Erreur création: {str(e)}'}), 500

@app.route('/api/auth/login', methods=['POST'])
def login():
    """Authentification et génération JWT"""
    data = request.get_json()
    
    if not data or not all(k in data for k in ['username', 'password']):
        return jsonify({'error': 'username et password obligatoires'}), 400
    
    user = User.query.filter_by(username=data['username']).first()
    
    if not user or not user.check_password(data['password']):
        return jsonify({'error': 'Identifiants invalides'}), 401
    
    access_token = create_access_token(identity=user.id)
    
    return jsonify({
        'message': 'Connexion réussie',
        'access_token': access_token,
        'user': user.to_dict()
    }), 200

@app.route('/api/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
    """Récupérer profil utilisateur"""
    user = User.query.get(user_id)
    
    if not user:
        return jsonify({'error': 'Utilisateur non trouvé'}), 404
    
    return jsonify({
        'user': user.to_dict(include_email=True),
        'articles': [a.to_dict() for a in user.articles]
    }), 200

@app.route('/api/users', methods=['GET'])
def list_users():
    """Lister tous les utilisateurs (avec pagination)"""
    page = request.args.get('page', 1, type=int)
    per_page = request.args.get('per_page', 10, type=int)
    
    paginated = User.query.paginate(page=page, per_page=per_page)
    
    return jsonify({
        'users': [u.to_dict() for u in paginated.items],
        'pagination': {
            'total': paginated.total,
            'pages': paginated.pages,
            'current_page': page,
            'per_page': per_page
        }
    }), 200

@app.route('/api/users/<int:user_id>', methods=['PUT'])
@jwt_required()
def update_user(user_id):
    """Modifier profil utilisateur (authentifié)"""
    current_user_id = get_jwt_identity()
    
    if current_user_id != user_id:
        return jsonify({'error': 'Non autorisé'}), 403
    
    user = User.query.get(user_id)
    if not user:
        return jsonify({'error': 'Utilisateur non trouvé'}), 404
    
    data = request.get_json()
    
    try:
        if 'email' in data:
            # Vérifier que email n'existe pas
            if User.query.filter_by(email=data['email']).filter(User.id != user_id).first():
                return jsonify({'error': 'Email déjà utilisé'}), 409
            user.email = data['email']
        
        if 'password' in data and len(data['password']) >= 8:
            user.set_password(data['password'])
        
        db.session.commit()
        
        return jsonify({'user': user.to_dict(include_email=True)}), 200
    except Exception as e:
        db.session.rollback()
        return jsonify({'error': str(e)}), 500

@app.route('/api/users/<int:user_id>', methods=['DELETE'])
@jwt_required()
def delete_user(user_id):
    """Supprimer compte utilisateur"""
    current_user_id = get_jwt_identity()
    
    if current_user_id != user_id:
        return jsonify({'error': 'Non autorisé'}), 403
    
    user = User.query.get(user_id)
    if not user:
        return jsonify({'error': 'Utilisateur non trouvé'}), 404
    
    try:
        db.session.delete(user)
        db.session.commit()
        return jsonify({'message': 'Compte supprimé'}), 200
    except Exception as e:
        db.session.rollback()
        return jsonify({'error': str(e)}), 500

# ========== ROUTES: ARTICLES ==========

@app.route('/api/articles', methods=['POST'])
@jwt_required()
def create_article():
    """Créer un nouvel article"""
    user_id = get_jwt_identity()
    data = request.get_json()
    
    if not data or not all(k in data for k in ['title', 'content']):
        return jsonify({'error': 'title et content obligatoires'}), 400
    
    if len(data['title']) < 5 or len(data['title']) > 200:
        return jsonify({'error': 'title: 5-200 caractères'}), 400
    
    try:
        article = Article(
            title=data['title'],
            content=data['content'],
            user_id=user_id
        )
        
        db.session.add(article)
        db.session.commit()
        
        return jsonify({
            'message': 'Article créé',
            'article': article.to_dict(include_content=True)
        }), 201
    except Exception as e:
        db.session.rollback()
        return jsonify({'error': str(e)}), 500

@app.route('/api/articles', methods=['GET'])
def list_articles():
    """Lister les articles (avec pagination et filtrage)"""
    page = request.args.get('page', 1, type=int)
    per_page = request.args.get('per_page', 10, type=int)
    user_id = request.args.get('user_id', type=int)
    
    query = Article.query
    
    if user_id:
        query = query.filter_by(user_id=user_id)
    
    query = query.order_by(Article.created_at.desc())
    paginated = query.paginate(page=page, per_page=per_page)
    
    return jsonify({
        'articles': [a.to_dict() for a in paginated.items],
        'pagination': {
            'total': paginated.total,
            'pages': paginated.pages,
            'current_page': page
        }
    }), 200

@app.route('/api/articles/<int:article_id>', methods=['GET'])
def get_article(article_id):
    """Récupérer un article (augmente compteur de vues)"""
    article = Article.query.get(article_id)
    
    if not article:
        return jsonify({'error': 'Article non trouvé'}), 404
    
    # Incrémenter compteur de vues
    article.views_count += 1
    db.session.commit()
    
    return jsonify({'article': article.to_dict(include_content=True)}), 200

@app.route('/api/articles/<int:article_id>', methods=['PUT'])
@jwt_required()
def update_article(article_id):
    """Modifier un article (seulement l'auteur)"""
    user_id = get_jwt_identity()
    article = Article.query.get(article_id)
    
    if not article:
        return jsonify({'error': 'Article non trouvé'}), 404
    
    if article.user_id != user_id:
        return jsonify({'error': 'Non autorisé'}), 403
    
    data = request.get_json()
    
    try:
        if 'title' in data:
            article.title = data['title']
        if 'content' in data:
            article.content = data['content']
        
        db.session.commit()
        return jsonify({'article': article.to_dict(include_content=True)}), 200
    except Exception as e:
        db.session.rollback()
        return jsonify({'error': str(e)}), 500

@app.route('/api/articles/<int:article_id>', methods=['DELETE'])
@jwt_required()
def delete_article(article_id):
    """Supprimer un article (seulement l'auteur)"""
    user_id = get_jwt_identity()
    article = Article.query.get(article_id)
    
    if not article:
        return jsonify({'error': 'Article non trouvé'}), 404
    
    if article.user_id != user_id:
        return jsonify({'error': 'Non autorisé'}), 403
    
    try:
        db.session.delete(article)
        db.session.commit()
        return jsonify({'message': 'Article supprimé'}), 200
    except Exception as e:
        db.session.rollback()
        return jsonify({'error': str(e)}), 500

# ========== ROUTES: UTILITAIRES ==========

@app.route('/', methods=['GET'])
def index():
    """Documentation API"""
    return jsonify({
        'title': 'Blog API Flask + RDS',
        'version': '1.0',
        'documentation': 'https://your-api.com/docs',
        'endpoints': {
            'auth': {
                'POST /api/auth/register': 'Créer compte',
                'POST /api/auth/login': 'Se connecter (JWT)'
            },
            'users': {
                'GET /api/users': 'Lister utilisateurs',
                'GET /api/users/<id>': 'Profil utilisateur',
                'PUT /api/users/<id>': 'Modifier profil (auth)',
                'DELETE /api/users/<id>': 'Supprimer compte (auth)'
            },
            'articles': {
                'GET /api/articles': 'Lister articles',
                'GET /api/articles/<id>': 'Lire article',
                'POST /api/articles': 'Créer article (auth)',
                'PUT /api/articles/<id>': 'Modifier article (auth)',
                'DELETE /api/articles/<id>': 'Supprimer article (auth)'
            }
        }
    }), 200

@app.route('/health', methods=['GET'])
def health():
    """Vérifier santé app et BD"""
    try:
        db.session.execute('SELECT 1')
        return jsonify({
            'status': 'healthy',
            'database': 'connected',
            'timestamp': datetime.utcnow().isoformat()
        }), 200
    except Exception as e:
        return jsonify({
            'status': 'unhealthy',
            'error': str(e)
        }), 503

@app.route('/api/init', methods=['POST'])
def init_db():
    """Initialiser les tables (RUN ONCE!)"""
    try:
        db.create_all()
        return jsonify({'message': 'Tables créées'}), 201
    except Exception as e:
        return jsonify({'error': str(e)}), 500

# ========== GESTION D'ERREURS ==========

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

@app.errorhandler(500)
def internal_error(error):
    db.session.rollback()
    return jsonify({'error': 'Erreur serveur'}), 500

# ========== POINT D'ENTRÉE ==========

if __name__ == '__main__':
    with app.app_context():
        try:
            db.session.execute('SELECT 1')
            print("[OK] Connexion RDS OK!")
        except Exception as e:
            print(f"[X] Erreur BD: {e}")
            print("Assurez-vous que:")
            print("  1. RDS instance est running")
            print("  2. Variables d'env sont correctes (.env)")
            print("  3. Security group autorise votre IP")
    
    app.run(
        host='0.0.0.0',
        port=5000,
        debug=os.getenv('FLASK_DEBUG', False)
    )


ÉTAPE 10: TESTER L'APPLICATION FLASK
════════════════════════════════════════════════════════════════════════════════

# 1. Initialiser DB (UNE SEULE FOIS!)
curl -X POST http://localhost:5000/api/init

# 2. Vérifier santé
curl http://localhost:5000/health

# Réponse:
# {
#   "status": "healthy",
#   "database": "connected",
#   "timestamp": "2024-01-15T10:30:00.123456"
# }

# 3. Créer un compte utilisateur
curl -X POST http://localhost:5000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "username": "alice",
    "email": "alice@example.com",
    "password": "SecurePassword123!"
  }'

# Réponse:
# {
#   "message": "Utilisateur créé avec succès",
#   "user": {
#     "id": 1,
#     "username": "alice",
#     "email": "alice@example.com",
#     "created_at": "2024-01-15T10:30:00"
#   }
# }

# 4. Se connecter (obtenir JWT token)
curl -X POST http://localhost:5000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "alice",
    "password": "SecurePassword123!"
  }'

# Réponse:
# {
#   "message": "Connexion réussie",
#   "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
#   "user": {...}
# }

# Sauvegarder le token!
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

# 5. Lister utilisateurs
curl http://localhost:5000/api/users

# 6. Récupérer profil utilisateur
curl http://localhost:5000/api/users/1

# 7. Créer un article (nécessite auth)
curl -X POST http://localhost:5000/api/articles \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "title": "Mon premier article",
    "content": "Contenu du premier article..."
  }'

# 8. Lister articles
curl http://localhost:5000/api/articles

# 9. Lire un article
curl http://localhost:5000/api/articles/1

# Note: Le compteur de vues augmente!

# 10. Modifier article (avec token)
curl -X PUT http://localhost:5000/api/articles/1 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "title": "Titre modifié",
    "content": "Contenu modifié..."
  }'

# 11. Supprimer article
curl -X DELETE http://localhost:5000/api/articles/1 \
  -H "Authorization: Bearer $TOKEN"

# 12. Lister articles d'un utilisateur spécifique
curl "http://localhost:5000/api/articles?user_id=1&page=1&per_page=5"

# 13. Lister avec pagination
curl "http://localhost:5000/api/users?page=2&per_page=10"


ÉTAPE 11: SCRIPT DE TEST AUTOMATISÉ (test.sh)
════════════════════════════════════════════════════════════════════════════════

#!/bin/bash

# test.sh - Script de test complet
BASE_URL="http://localhost:5000"
TOKEN=""

echo "[TEST] TESTS API FLASK + RDS"
echo "======================="

# 1. Init DB
echo ""
echo "1⃣ Initialiser base de données..."
curl -s -X POST $BASE_URL/api/init | python -m json.tool

# 2. Health check
echo ""
echo "2⃣ Vérifier santé app..."
curl -s $BASE_URL/health | python -m json.tool

# 3. Register user
echo ""
echo "3⃣ Créer utilisateur alice..."
REGISTER=$(curl -s -X POST $BASE_URL/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "username": "alice",
    "email": "alice@example.com",
    "password": "AlicePass123!"
  }')
echo $REGISTER | python -m json.tool

# 4. Login
echo ""
echo "4⃣ Se connecter..."
LOGIN=$(curl -s -X POST $BASE_URL/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "alice",
    "password": "AlicePass123!"
  }')
echo $LOGIN | python -m json.tool

# Extract token
TOKEN=$(echo $LOGIN | python -c "import sys, json; print(json.load(sys.stdin)['access_token'])")
echo "[OK] Token: $TOKEN"

# 5. Create article
echo ""
echo "5⃣ Créer article..."
curl -s -X POST $BASE_URL/api/articles \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "title": "Mon Premier Article",
    "content": "Ceci est un article de test..."
  }' | python -m json.tool

# 6. List articles
echo ""
echo "6⃣ Lister articles..."
curl -s $BASE_URL/api/articles | python -m json.tool

# 7. List users
echo ""
echo "7⃣ Lister utilisateurs..."
curl -s $BASE_URL/api/users | python -m json.tool

echo ""
echo "[OK] Tests terminés!"

# Utilisation:
# chmod +x test.sh
# ./test.sh


ÉTAPE 12: BONNES PRATIQUES FLASK + RDS
════════════════════════════════════════════════════════════════════════════════

# 1. HASHER LES MOTS DE PASSE (sécurité!)
from werkzeug.security import generate_password_hash, check_password_hash

# Lors de la création:
user = User(
    username=data['username'],
    email=data['email'],
    password=generate_password_hash(data['password'])  # HASH!
)

# Lors de la vérification:
if check_password_hash(user.password, provided_password):
    print("Mot de passe correct!")

# 2. PAGINATION (pour éviter charger 1 million de rows)
@app.route('/api/posts', methods=['GET'])
def get_posts():
    page = request.args.get('page', 1, type=int)
    per_page = 10
    
    posts = Post.query.paginate(page=page, per_page=per_page)
    
    return jsonify({
        'posts': [p.to_dict() for p in posts.items],
        'total': posts.total,
        'pages': posts.pages,
        'current_page': page
    }), 200

# EXPLICATION:
# paginate(page=1, per_page=10) = 10 posts par page
# posts.items = Articles actuels
# posts.total = Nombre total
# posts.pages = Nombre de pages

# 3. VALIDATIONS (ne pas faire confiance à l'utilisateur)
from marshmallow import Schema, fields, validate, ValidationError

class UserSchema(Schema):
    username = fields.String(required=True, validate=validate.Length(min=3, max=80))
    email = fields.Email(required=True)
    password = fields.String(required=True, validate=validate.Length(min=8))

schema = UserSchema()

@app.route('/api/users', methods=['POST'])
def create_user():
    try:
        data = schema.load(request.get_json())
    except ValidationError as err:
        return jsonify({'errors': err.messages}), 400
    # ... reste du code

# 4. GESTION ERREURS GLOBALE
@app.errorhandler(404)
def not_found(error):
    return jsonify({'error': 'Ressource non trouvée'}), 404

@app.errorhandler(500)
def internal_error(error):
    db.session.rollback()
    return jsonify({'error': 'Erreur serveur'}), 500

# 5. LOGGING (pour déboguer)
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@app.route('/api/users', methods=['POST'])
def create_user():
    logger.info(f"Création utilisateur: {request.get_json()}")
    # ...

# 6. LIMITER REQUÊTES (rate limiting)
from flask_limiter import Limiter

limiter = Limiter(app, key_func=lambda: request.remote_addr)

@app.route('/api/users', methods=['POST'])
@limiter.limit("10 per minute")
def create_user():
    # Max 10 requêtes par minute
    pass

# 7. CORS (si frontend séparé)
from flask_cors import CORS

CORS(app, origins="http://localhost:3000")

# 8. AUTHENTIFICATION (JWT)
from flask_jwt_extended import JWTManager, create_access_token, jwt_required

app.config['JWT_SECRET_KEY'] = os.getenv('SECRET_KEY')
jwt = JWTManager(app)

@app.route('/api/login', methods=['POST'])
def login():
    data = request.get_json()
    user = User.query.filter_by(username=data['username']).first()
    
    if not user or not check_password_hash(user.password, data['password']):
        return jsonify({'error': 'Identifiants invalides'}), 401
    
    access_token = create_access_token(identity=user.id)
    return jsonify({'access_token': access_token}), 200

@app.route('/api/protected', methods=['GET'])
@jwt_required()
def protected():
    # Seulement accessible avec JWT valide
    return jsonify({'message': 'Accès autorisé'}), 200


ÉTAPE 11: DÉPLOYER SUR AWS (Elastic Beanstalk)
════════════════════════════════════════════════════════════════════════════════

# Créer .ebextensions/python.config
option_settings:
  aws:autoscaling:launchconfiguration:
    IamInstanceProfile: aws-elasticbeanstalk-ec2-role
  aws:elasticbeanstalk:application:environment:
    PYTHONPATH: /var/app/current:$PYTHONPATH
    DB_HOST: flask-app-db.c9akciq32.us-east-1.rds.amazonaws.com
    DB_USER: admin
    DB_PORT: 3306
    DB_NAME: flask_app

# Créer .ebextensions/iam-policy.json (accès RDS)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "rds:DescribeDBInstances",
        "rds:DescribeDBClusters"
      ],
      "Resource": "*"
    }
  ]
}

# Initialiser Elastic Beanstalk
eb init -p python-3.11 flask-app --region us-east-1

# Créer environnement
eb create flask-app-env

# Déployer
git add .
git commit -m "Ready for deployment"
eb deploy

# Vérifier status
eb status

# Voir logs
eb logs

# SSH sur instance
eb ssh


ÉTAPE 12: CONTAINERISER AVEC DOCKER
════════════════════════════════════════════════════════════════════════════════

# Créer Dockerfile
FROM python:3.11-slim

WORKDIR /app

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

COPY . .

ENV FLASK_APP=app.py

CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

# EXPLICATION:
# FROM python:3.11 = Image Python de base
# WORKDIR /app = Dossier de travail
# COPY requirements.txt = Copier dépendances
# RUN pip install = Installer dépendances
# CMD gunicorn = Serveur production (pas Flask dev!)

# Créer docker-compose.yml
version: '3.8'

services:
  web:
    build: .
    ports:
      - "5000:5000"
    environment:
      DB_HOST: db
      DB_USER: admin
      DB_PASSWORD: password
      DB_NAME: flask_app
    depends_on:
      - db
    volumes:
      - .:/app

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: flask_app
      MYSQL_USER: admin
      MYSQL_PASSWORD: password
    ports:
      - "3306:3306"
    volumes:
      - db_data:/var/lib/mysql

volumes:
  db_data:

# Lancer Docker Compose
docker-compose up

# Accès:
# Flask: http://localhost:5000
# MySQL: localhost:3306


═══════════════════════════════════════════════════════════════════════════════
[OK] MONITORING & ÉVÉNEMENTS
═══════════════════════════════════════════════════════════════════════════════

# Lister événements récents (24h)
aws rds describe-events \
  --duration 1440 \
  --source-type db-instance

# EXPLICATION:
# "--duration 1440" = 1440 minutes = 24 heures
# Affiche: créations, modifications, erreurs, patches

# Événements DB spécifique
aws rds describe-events \
  --source-identifier mydb \
  --source-type db-instance

# Voir en format lisible
aws rds describe-events \
  --source-identifier mydb \
  --query 'Events[*].[EventCategories,Message,SourceArn,SourceType]' \
  --output table

# Voir logs (MySQL)
aws rds describe-db-log-files \
  --db-instance-identifier mydb

# Télécharger log
aws rds download-db-log-file-portion \
  --db-instance-identifier mydb \
  --db-log-file-name error/mysql-error.log \
  --starting-token 0 \
  > error.log

# Logs disponibles (MySQL):
# error        = Erreurs
# general      = Toutes requêtes (lourd!)
# slowquery    = Requêtes lentes
# audit        = Actions (si activé)

# CloudWatch Metrics (via console ou CLI)
aws cloudwatch get-metric-statistics \
  --namespace AWS/RDS \
  --metric-name CPUUtilization \
  --dimensions Name=DBInstanceIdentifier,Value=mydb \
  --start-time 2024-01-15T00:00:00Z \
  --end-time 2024-01-16T00:00:00Z \
  --period 300 \
  --statistics Average

# Métriques courantes:
# CPUUtilization      = CPU %
# DatabaseConnections = Connexions actives
# ReadLatency         = Temps lecture (ms)
# WriteLatency        = Temps écriture (ms)
# DiskQueueDepth      = Files d'attente disque
# FreeableMemory      = Mémoire libre
# NetworkReceiveThroughput = Réseau reçu


═══════════════════════════════════════════════════════════════════════════════
[OK] AURORA (VERSION OPTIMISÉE AWS)
═══════════════════════════════════════════════════════════════════════════════

AURORA = MySQL/PostgreSQL optimisé par AWS
- 5x plus rapide que MySQL
- 3x plus rapide que PostgreSQL
- Réplication 3 zones automatique
- Auto-scaling lectures
- Plus cher mais performance premium

# Créer cluster Aurora MySQL
aws rds create-db-cluster \
  --db-cluster-identifier myaurora \
  --engine aurora-mysql \
  --engine-version 8.0.mysql_aurora.3.04.0 \
  --master-username admin \
  --master-user-password MySecurePassword123! \
  --vpc-security-group-ids sg-0123456789abcdef0 \
  --backup-retention-period 30

# Créer instance Aurora dans cluster
aws rds create-db-instance \
  --db-instance-identifier myaurora-instance-1 \
  --db-instance-class db.r5.large \
  --engine aurora-mysql \
  --db-cluster-identifier myaurora

# Créer Aurora Serverless (auto-scaling)
aws rds create-db-cluster \
  --db-cluster-identifier myaurora-serverless \
  --engine aurora-mysql \
  --engine-mode serverless \
  --master-username admin \
  --master-user-password MySecurePassword123! \
  --scaling-configuration MinCapacity=2,MaxCapacity=16,AutoPause=true

# EXPLICATION:
# "engine-mode serverless" = Sans serveur
# MinCapacity=2 = 2 ACUs minimum
# MaxCapacity=16 = 16 ACUs maximum
# AutoPause=true = Pause si inactif (économie!)

# Lister clusters Aurora
aws rds describe-db-clusters

# Lister instances Aurora
aws rds describe-db-instances \
  --filters Name=engine,Values=aurora-mysql


═══════════════════════════════════════════════════════════════════════════════
[OK] SECURITY GROUPS & RÉSEAU
═══════════════════════════════════════════════════════════════════════════════

# Voir règles actuelles
aws ec2 describe-security-groups \
  --group-ids sg-0123456789abcdef0

# Ajouter règle ingress (entrant)
aws ec2 authorize-security-group-ingress \
  --group-id sg-0123456789abcdef0 \
  --protocol tcp \
  --port 3306 \
  --cidr 203.0.113.25/32

# Ajouter règle depuis autre security group
aws ec2 authorize-security-group-ingress \
  --group-id sg-0123456789abcdef0 \
  --protocol tcp \
  --port 3306 \
  --source-group sg-app-instances

# Révoquer (supprimer) règle
aws ec2 revoke-security-group-ingress \
  --group-id sg-0123456789abcdef0 \
  --protocol tcp \
  --port 3306 \
  --cidr 203.0.113.25/32

# Règles de sortie (optionnel)
aws ec2 authorize-security-group-egress \
  --group-id sg-0123456789abcdef0 \
  --protocol tcp \
  --port 443 \
  --cidr 0.0.0.0/0


═══════════════════════════════════════════════════════════════════════════════
[OK] OPTION GROUPS (FONCTIONNALITÉS ADDITIONNELLES)
═══════════════════════════════════════════════════════════════════════════════

# Créer option group
aws rds create-option-group \
  --option-group-name mydb-options \
  --engine-name mysql \
  --major-engine-version 8.0 \
  --option-group-description "MySQL custom options"

# Ajouter option (exemple: MariaDB Audit Plugin)
aws rds add-option-to-option-group \
  --option-group-name mydb-options \
  --options "OptionName=MARIADB_AUDIT_PLUGIN"

# Appliquer à DB
aws rds modify-db-instance \
  --db-instance-identifier mydb \
  --option-group-name mydb-options


═══════════════════════════════════════════════════════════════════════════════
[OK] BACKUP STRATEGY (STRATÉGIE SAUVEGARDE)
═══════════════════════════════════════════════════════════════════════════════

STRATÉGIE RECOMMANDÉE:

DEV/TEST:
- Backup retention: 7 jours
- Snapshots: 1-2 manuels avant modif
- Multi-AZ: NON (économie)

PRODUCTION:
- Backup retention: 30 jours
- Snapshots: quotidiens (automatisé)
- Multi-AZ: OUI (haute dispo)
- Read Replicas: 1-2 (DR + performance)
- Copy to another region: hebdo

# Automatiser snapshots quotidiens
# Créer fonction Lambda + EventBridge (hors scope ici)

# Script shell (manuel):
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_ID="mydb-backup-$DATE"
aws rds create-db-snapshot \
  --db-instance-identifier mydb \
  --db-snapshot-identifier $BACKUP_ID
echo "Snapshot créé: $BACKUP_ID"

# Ajouter à crontab (Linux):
# 0 2 * * * /home/ubuntu/backup.sh
# Exécute chaque jour à 02:00


═══════════════════════════════════════════════════════════════════════════════
[OK] DÉPANNAGE & SOLUTIONS
═══════════════════════════════════════════════════════════════════════════════

PROBLÈME: "Can't connect to database"
SOLUTIONS:
1. Vérifier endpoint:
   aws rds describe-db-instances --db-instance-identifier mydb \
   --query 'DBInstances[0].Endpoint.Address'

2. Vérifier DB est "available":
   aws rds describe-db-instances --db-instance-identifier mydb \
   --query 'DBInstances[0].DBInstanceStatus'

3. Vérifier sécurité groupe (votre IP autorisée?):
   aws ec2 describe-security-groups --group-ids sg-xxx

4. Tester ping (DNS ok?):
   ping mydb.c9akciq32.us-east-1.rds.amazonaws.com

5. Tester port:
   telnet mydb.c9akciq32.us-east-1.rds.amazonaws.com 3306

6. Vérifier credentials:
   Utilisateur correct? Mot de passe correct?

---

PROBLÈME: "DB is down / unavailable"
SOLUTIONS:
1. Vérifier état:
   aws rds describe-db-instances --db-instance-identifier mydb \
   --query 'DBInstances[0].DBInstanceStatus'

2. Lire événements:
   aws rds describe-events --source-identifier mydb

3. Redémarrer si nécessaire:
   aws rds reboot-db-instance --db-instance-identifier mydb

4. Vérifier Multi-AZ failover:
   Si Multi-AZ = vérifier logs pour basculement auto

---

PROBLÈME: "Slow queries / performance problem"
SOLUTIONS:
1. Activer slow query log:
   aws rds modify-db-instance \
   --db-instance-identifier mydb \
   --enable-cloudwatch-logs-exports slowquery

2. Voir logs:
   aws rds download-db-log-file-portion \
   --db-instance-identifier mydb \
   --db-log-file-name slowquery/mysql-slowquery.log

3. Utiliser Performance Insights:
   aws rds modify-db-instance \
   --db-instance-identifier mydb \
   --enable-performance-insights

4. Vérifier métriques CloudWatch:
   CPU saturé? RAM saturé? Disque plein?

5. Upgrade instance:
   aws rds modify-db-instance \
   --db-instance-identifier mydb \
   --db-instance-class db.m5.large \
   --apply-immediately

6. Créer indexes:
   Sur colonnes WHERE/JOIN

---

PROBLÈME: "Oups, j'ai supprimé des données!"
SOLUTIONS:
1. Point-In-Time Restore (PITR):
   aws rds restore-db-instance-to-point-in-time \
   --source-db-instance-identifier mydb \
   --target-db-instance-identifier mydb-recovered \
   --restore-time 2024-01-15T14:25:00Z

2. Restaurer depuis snapshot:
   aws rds restore-db-instance-from-db-snapshot \
   --db-instance-identifier mydb-recovered \
   --db-snapshot-identifier mydb-snapshot-before-delete

3. Vérifier données récupérées
4. Copier les données vers DB principale

---

PROBLÈME: "Storage full"
SOLUTIONS:
1. Vérifier espace:
   aws rds describe-db-instances --db-instance-identifier mydb \
   --query 'DBInstances[0].AllocatedStorage'

2. Augmenter stockage:
   aws rds modify-db-instance \
   --db-instance-identifier mydb \
   --allocated-storage 200 \
   --apply-immediately

3. Nettoyer anciennes données:
   DELETE FROM table WHERE date < DATE_SUB(NOW(), INTERVAL 1 YEAR);

4. Voir espace disque:
   Via Performance Insights -> "Free Storage Space"

---

PROBLÈME: "Too many connections"
SOLUTIONS:
1. Voir limite actuelle:
   SHOW VARIABLES LIKE 'max_connections';

2. Augmenter limit:
   Créer parameter group, modifier max_connections
   aws rds modify-db-parameter-group \
   --db-parameter-group-name mydb-params \
   --parameters "ParameterName=max_connections,ParameterValue=1000"

3. Utiliser connection pooling dans app:
   PgBouncer (PostgreSQL)
   ProxySQL (MySQL)

4. Fermer connexions inutiles:
   SELECT * FROM INFORMATION_SCHEMA.PROCESSLIST;
   KILL CONNECTION id;


═══════════════════════════════════════════════════════════════════════════════
[OK] CHECKLIST: PREMIÈRE DB EN PRODUCTION
═══════════════════════════════════════════════════════════════════════════════

AVANT:
[ ] Choisir région
[ ] Choisir moteur (MySQL/PostgreSQL)
[ ] Choisir classe instance
[ ] Créer security group
[ ] Créer subnet group (si VPC privée)

CRÉATION:
[ ] Créer DB instance
[ ] Attendre "available" (~10 min)
[ ] Récupérer endpoint
[ ] Tester connexion

SÉCURITÉ:
[ ] Sauvegarder mot de passe (gestionnaire)
[ ] Activer Multi-AZ
[ ] Activer deletion-protection
[ ] Security group restrictif
[ ] Pas --publicly-accessible si possible

BACKUPS:
[ ] Backup retention: 30 jours
[ ] Preferred backup window: 03:00-04:00
[ ] Créer snapshot initial
[ ] Test PITR (restaurer à moment X)
[ ] Copier snapshot vers autre région

MONITORING:
[ ] Activer CloudWatch logs
[ ] Activer Enhanced Monitoring
[ ] Activer Performance Insights
[ ] Configurer CloudWatch alarms
[ ] CPU > 80%, RAM > 90%, Storage > 90%

PERFORMANCE:
[ ] Créer parameter group custom
[ ] Configurer max_connections
[ ] Configurer slow query log
[ ] Créer indexes sur tables principales
[ ] Read replicas pour scaling lecture

DOCUMENTATION:
[ ] Noter: host, port, user, database
[ ] Documenter: schema structure
[ ] Documenter: backup strategy
[ ] Documenter: runbook pour incidents


═══════════════════════════════════════════════════════════════════════════════
[OK] COÛTS & OPTIMISATION
═══════════════════════════════════════════════════════════════════════════════

RÉDUIRE LES COÛTS:

1. UTILISER FREE TIER (~$0/mois 12 mois)
   - db.t3.micro
   - 20 GB stockage
   - 750 heures/mois

2. ARRÊTER INACTIF
   - Dev/test: arrêter le soir (~50% économie)
   - Staging: arrêter entre releases

3. CHOISIR BON MOTEUR
   - MySQL: bon tout-usage, économique
   - PostgreSQL: puissant, même prix
   - Aurora: premium, 2-3x plus cher

4. CHOISIR BON STORAGE
   - gp3: meilleur rapport qualité/prix
   - gp2: compatible, moins cher
   - io1: seulement si besoin IOPS élevés

5. MONITORAGE USAGE
   - CloudWatch metrics
   - Downsize instance si CPU < 10%
   - Augmenter storage progressivement

6. RESERVED INSTANCES
   - Acheter 1/3 ans: 30-50% réduction
   - Si DB stable long-terme

7. NETTOYER SNAPSHOTS
   - Vieux snapshots = argent gaspillé
   - Garder: snapshots importants seulement

8. MULTI-AZ SAGEMENT
   - 2x plus cher
   - Production critique seulement

EXEMPLE: Dev vs Production
DEV (micro + low storage):
  - db.t3.micro: $0
  - 20 GB gp3: $2.20
  - Backup 7j: $0.50
  - Total: ~$2.70/mois (gratuit year 1)

PROD (m5.large + Multi-AZ):
  - 2x db.m5.large: $440
  - 100 GB gp3: $11
  - Backup 30j: $3
  - Total: ~$454/mois


═══════════════════════════════════════════════════════════════════════════════
[OK] COMMANDES ESSENTIELLES (RÉFÉRENCE RAPIDE)
═══════════════════════════════════════════════════════════════════════════════

# CRÉER
aws rds create-db-instance --db-instance-identifier mydb --engine mysql --master-username admin --master-user-password Pwd123!

# LISTER
aws rds describe-db-instances

# ENDPOINT
aws rds describe-db-instances --db-instance-identifier mydb --query 'DBInstances[0].Endpoint.Address' --output text

# STATUS
aws rds describe-db-instances --db-instance-identifier mydb --query 'DBInstances[0].DBInstanceStatus' --output text

# MODIFIER (Upgrade)
aws rds modify-db-instance --db-instance-identifier mydb --db-instance-class db.t3.small --apply-immediately

# SNAPSHOT
aws rds create-db-snapshot --db-instance-identifier mydb --db-snapshot-identifier mydb-backup-$(date +%Y%m%d)

# RESTAURER SNAPSHOT
aws rds restore-db-instance-from-db-snapshot --db-instance-identifier mydb-restored --db-snapshot-identifier mydb-backup

# PITR
aws rds restore-db-instance-to-point-in-time --source-db-instance-identifier mydb --target-db-instance-identifier mydb-pitr --restore-time 2024-01-15T10:30:00Z

# READ REPLICA
aws rds create-db-instance-read-replica --db-instance-identifier mydb-replica --source-db-instance-identifier mydb

# ARRÊTER
aws rds stop-db-instance --db-instance-identifier mydb

# DÉMARRER
aws rds start-db-instance --db-instance-identifier mydb

# REDÉMARRER
aws rds reboot-db-instance --db-instance-identifier mydb

# SUPPRIMER
aws rds delete-db-instance --db-instance-identifier mydb --final-db-snapshot-identifier mydb-final


═══════════════════════════════════════════════════════════════════════════════
[OK] RESSOURCES & LIENS
═══════════════════════════════════════════════════════════════════════════════

AWS Documentation:
https://docs.aws.amazon.com/rds/

RDS User Guide:
https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/

AWS CLI RDS:
https://docs.aws.amazon.com/cli/latest/reference/rds/

Tools Recommandés:
- MySQL Workbench (GUI MySQL/MariaDB)
- pgAdmin (GUI PostgreSQL)
- DBeaver (Universal DB client)
- AWS Database Migration Service (Migrations)

Bonnes Pratiques:
- Backups: Min 7j, Prod 30j
- Multi-AZ: Production critique seulement
- Security Groups: Aussi restrictif que possible
- Parameter Groups: Custom si besoin config
- Monitoring: CloudWatch + Performance Insights
- PITR: Tester régulièrement
- Documentation: Schema, users, runbooks

Performance Tips:
- Indexes sur colonnes WHERE/JOIN/ORDER BY
- Normalisation: Éviter duplication
- Connection pooling: Pour haute concurrence
- Read replicas: Pour rapports/analytics
- Slow query log: Identifier bottlenecks

# ============================================================================
# GUIDE DE DÉPANNAGE: Flask + RDS
# ============================================================================
# Solutions aux problèmes les plus courants


═══════════════════════════════════════════════════════════════════════════════
[X] ERREUR 1: "No module named 'flask'"
═══════════════════════════════════════════════════════════════════════════════

PROBLÈME:
ModuleNotFoundError: No module named 'flask'

CAUSE:
- Dépendances pas installées
- Mauvais environnement virtuel activé

SOLUTION:
1. Créer venv:
   python3 -m venv venv

2. Activer venv:
   # Linux/Mac:
   source venv/bin/activate
   
   # Windows:
   venv\Scripts\activate

3. Installer dépendances:
   pip install -r requirements.txt

4. Vérifier:
   python -c "import flask; print(flask.__version__)"

EXPLICATION:
- Environnement virtuel = "univers Python isolé"
- Chaque projet a ses dépendances
- Activation nécessaire à chaque fois qu'on ouvre le terminal


═══════════════════════════════════════════════════════════════════════════════
[X] ERREUR 2: "(pymysql.err.OperationalError) (2003, 'Can't connect to MySQL'"
═══════════════════════════════════════════════════════════════════════════════

PROBLÈME:
pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on '...'")

CAUSE:
- RDS instance pas running
- Mauvais endpoint
- Security group n'autorise pas votre IP
- Firewall local bloque port 3306

DIAGNOSTIC:

1. Vérifier que RDS existe et est en "available":
   aws rds describe-db-instances --db-instance-identifier flask-app-db \
   --query 'DBInstances[0].DBInstanceStatus'
   
   Résultat devrait être: "available"

2. Vérifier endpoint:
   aws rds describe-db-instances --db-instance-identifier flask-app-db \
   --query 'DBInstances[0].Endpoint.Address'

3. Vérifier dans .env:
   cat .env
   # DB_HOST doit correspondre à l'endpoint

4. Tester connexion directement:
   mysql -h flask-app-db.c9akciq32.us-east-1.rds.amazonaws.com \
   -u admin \
   -p
   # Taper le mot de passe

5. Vérifier security group:
   aws ec2 describe-security-groups --group-ids sg-xxx
   
   Chercher une règle comme:
   - Protocol: tcp
   - Port: 3306
   - CIDR: votre IP/32 OU sg-app-instances

6. Vérifier votre IP:
   curl https://icanhazip.com
   
   Puis ajouter à security group:
   aws ec2 authorize-security-group-ingress \
   --group-id sg-xxx \
   --protocol tcp \
   --port 3306 \
   --cidr YOUR_IP/32

SOLUTION COMPLÈTE (pas à pas):
1. Démarrer RDS si arrêtée:
   aws rds start-db-instance --db-instance-identifier flask-app-db

2. Attendre 2-3 minutes
3. Tester depuis Python:
   python
   >>> import pymysql
   >>> pymysql.connect(host='...', user='admin', password='...', port=3306)
   
   Si connexion OK -> problème dans .env
   Si erreur -> problème connexion réseau


═══════════════════════════════════════════════════════════════════════════════
[X] ERREUR 3: "OperationalError: (1045, 'Access denied for user 'admin'')"
═══════════════════════════════════════════════════════════════════════════════

PROBLÈME:
SQLAlchemy OperationalError: (1045, "Access denied for user 'admin'@'...'")

CAUSE:
- Mot de passe incorrect
- Utilisateur n'existe pas

DIAGNOSTIC:

1. Vérifier mot de passe en .env:
   grep DB_PASSWORD .env

2. Tester connexion manuelle:
   mysql -h endpoint.rds.amazonaws.com -u admin -p
   # Taper le mot de passe de .env

SOLUTION:
1. Si mot de passe correct mais rejeté:
   a. RDS peut nécessiter quelques minutes après création
   b. Attendre 5 minutes puis réessayer

2. Si mot de passe oublié:
   a. Modifier mot de passe RDS:
      aws rds modify-db-instance \
      --db-instance-identifier flask-app-db \
      --master-user-password NewPassword123! \
      --apply-immediately
   
   b. Attendre 1-2 minutes
   c. Mettre à jour .env
   d. Redémarrer app

3. Vérifier qu'il y a pas d'espaces:
   # INCORRECT:
   DB_PASSWORD = MyPassword123!  # Espaces!
   
   # CORRECT:
   DB_PASSWORD=MyPassword123!


═══════════════════════════════════════════════════════════════════════════════
[X] ERREUR 4: "OperationalError: (1049, 'Unknown database'"
═══════════════════════════════════════════════════════════════════════════════

PROBLÈME:
SQLAlchemy OperationalError: (1049, "Unknown database 'flask_app'")

CAUSE:
- Base de données n'existe pas
- Mauvais nom de BD en .env

DIAGNOSTIC:

1. Vérifier que BD existe:
   # Se connecter à RDS d'abord:
   mysql -h endpoint -u admin -p
   
   # Puis dans MySQL:
   SHOW DATABASES;
   # Chercher "flask_app"

2. Vérifier nom en .env:
   grep DB_NAME .env

SOLUTION:

Option 1: Créer la base via MySQL:
   mysql -h endpoint -u admin -p
   > CREATE DATABASE flask_app;
   > exit

Option 2: Créer via AWS CLI (plus simple):
   # D'abord, se connecter à RDS
   mysql -h flask-app-db.c9akciq32.us-east-1.rds.amazonaws.com \
   -u admin -p
   
   # Dans MySQL prompt:
   > CREATE DATABASE flask_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
   > exit

Option 3: Modifier .env pour utiliser "mysql" (BD par défaut):
   DB_NAME=mysql
   
   Puis l'app créera les tables dans "mysql" (pas idéal)


═══════════════════════════════════════════════════════════════════════════════
[X] ERREUR 5: "ProgrammingError: (1146, 'Table 'flask_app.users' doesn't exist')"
═══════════════════════════════════════════════════════════════════════════════

PROBLÈME:
ProgrammingError: (1146, "Table 'flask_app.users' doesn't exist")

CAUSE:
- Tables pas créées
- Endpoint /api/init pas appelé

SOLUTION:

1. Appeler endpoint d'initialisation:
   curl -X POST http://localhost:5000/api/init
   
   Réponse devrait être:
   {"message": "Tables créées"}

2. Vérifier que les tables existent:
   mysql -h endpoint -u admin -p -e "USE flask_app; SHOW TABLES;"
   
   Résultat:
   | Tables_in_flask_app |
   | articles            |
   | users               |

3. Si erreur persiste:
   a. Vérifier que modèles sont définis dans app.py
   b. Vérifier que db.create_all() est appelé
   c. Regarder les logs de la migration

EXPLICATION:
- SQLAlchemy crée pas les tables automatiquement
- db.create_all() crée toutes les tables des modèles
- Doit être fait UNE SEULE FOIS


═══════════════════════════════════════════════════════════════════════════════
[X] ERREUR 6: "IntegrityError: (1062, 'Duplicate entry'"
═══════════════════════════════════════════════════════════════════════════════

PROBLÈME:
IntegrityError: (1062, "Duplicate entry 'alice' for key 'username'")

CAUSE:
- Essai de créer utilisateur avec username existant
- Pas de vérification des doublons

DIAGNOSTIC:

1. Vérifier utilisateurs existants:
   mysql -h endpoint -u admin -p -e "USE flask_app; SELECT * FROM users;"

2. Tester login plutôt que register:
   curl -X POST http://localhost:5000/api/auth/login \
   -d '{"username": "alice", "password": "..."}'

SOLUTION:

Code déjà inclus dans app.py:
```python
if User.query.filter_by(username=data['username']).first():
    return jsonify({'error': 'Username existant'}), 409
```

Si vous n'avez pas cette vérification:
1. Ajouter le code ci-dessus
2. Tester avec autre username

Alternative: Supprimer l'utilisateur:
   mysql -h endpoint -u admin -p
   > USE flask_app;
   > DELETE FROM users WHERE username='alice';
   > exit


═══════════════════════════════════════════════════════════════════════════════
[X] ERREUR 7: "JWTError: Missing authorization header"
═══════════════════════════════════════════════════════════════════════════════

PROBLÈME:
Quand d'appel une route @jwt_required() sans token

CAUSE:
- Token pas envoyé
- Token mal formaté
- Token expiré

DIAGNOSTIC:

1. Vérifier que token est envoyé:
   # INCORRECT (pas de token):
   curl -X POST http://localhost:5000/api/articles \
   -d '{"title": "...", "content": "..."}'
   
   # CORRECT (avec token):
   curl -X POST http://localhost:5000/api/articles \
   -H "Authorization: Bearer $TOKEN" \
   -d '{"title": "...", "content": "..."}'

2. Obtenir un nouveau token:
   curl -X POST http://localhost:5000/api/auth/login \
   -d '{"username": "alice", "password": "..."}'
   
   Copier le "access_token"

SOLUTION:

1. Toujours envoyer le token:
   TOKEN=$(curl -s ... | grep access_token | ...)
   curl -H "Authorization: Bearer $TOKEN" ...

2. Format correct: "Bearer XXXXXXXX"
   Pas "Bearer:" ni "Bearer  " (double espace)

3. Python/requests:
   headers = {"Authorization": f"Bearer {token}"}
   response = requests.post(url, headers=headers)


═══════════════════════════════════════════════════════════════════════════════
[X] ERREUR 8: "Address already in use"
═══════════════════════════════════════════════════════════════════════════════

PROBLÈME:
Address already in use port 5000

CAUSE:
- Application déjà en cours d'exécution sur port 5000
- Autre application utilise le port

DIAGNOSTIC:

1. Voir quelle app utilise port 5000:
   lsof -i :5000  # Linux/Mac
   netstat -ano | findstr :5000  # Windows

2. Voir tous les services:
   netstat -tlnp | grep LISTEN

SOLUTION:

Option 1: Tuer l'app existante:
   pkill -f "python app.py"  # Linux/Mac
   
   Ou ctrl+C dans le terminal Flask

Option 2: Utiliser autre port:
   app.run(port=5001)
   # Puis accéder à http://localhost:5001

Option 3: Nettoyer le port (Linux):
   sudo fuser -k 5000/tcp


═══════════════════════════════════════════════════════════════════════════════
[X] ERREUR 9: "CORS policy: No 'Access-Control-Allow-Origin' header"
═══════════════════════════════════════════════════════════════════════════════

PROBLÈME:
CORS policy: No 'Access-Control-Allow-Origin' header is present

CAUSE:
- Frontend et backend sur domaines/ports différents
- CORS pas activé

DIAGNOSTIC:

1. Où s'exécute le frontend?
   http://localhost:3000 ? http://example.com ?

2. Où s'exécute le backend?
   http://localhost:5000

SOLUTION:

Dans app.py, ajouter:
```python
from flask_cors import CORS

CORS(app, origins="http://localhost:3000")
```

Ou permettre tous (DEV seulement!):
```python
CORS(app)  # Dangereux en production!
```

En production:
```python
CORS(app, origins=[
    "https://yourdomain.com",
    "https://www.yourdomain.com"
])
```

Vérifier que CORS est activé:
Dans navigateur, voir headers réponse:
   Access-Control-Allow-Origin: http://localhost:3000


═══════════════════════════════════════════════════════════════════════════════
[X] ERREUR 10: "Health check fails" / BD disconnect
═══════════════════════════════════════════════════════════════════════════════

PROBLÈME:
GET /health retourne status=unhealthy

CAUSE:
- RDS instance s'est arrêtée
- Connexion BD perdue
- Timeout réseau

DIAGNOSTIC:

1. Vérifier état RDS:
   aws rds describe-db-instances \
   --db-instance-identifier flask-app-db \
   --query 'DBInstances[0].DBInstanceStatus'

2. Consulter événements RDS:
   aws rds describe-events \
   --source-identifier flask-app-db

3. Tester connexion manuelle:
   mysql -h endpoint -u admin -p -e "SELECT 1;"

SOLUTION:

1. Si RDS arrêtée:
   aws rds start-db-instance --db-instance-identifier flask-app-db
   Attendre 1-2 minutes

2. Si "maintenance in progress":
   Attendre que maintenance se termine (peut être longue)

3. Redémarrer Flask app:
   Ctrl+C
   python app.py

4. En production, utiliser connection pooling:
   from sqlalchemy import create_engine
   
   engine = create_engine(
       '...',
       pool_size=10,
       pool_recycle=3600,  # Recycler connexions après 1h
       pool_pre_ping=True   # Tester connexion avant utilisation
   )


═══════════════════════════════════════════════════════════════════════════════
[OK] CHECKLIST DE DÉPANNAGE
═══════════════════════════════════════════════════════════════════════════════

Avant de crier "c'est cassé!":

[WHITE_SQUARE] 1. Python venv activé?
[WHITE_SQUARE] 2. Dépendances installées? (pip install -r requirements.txt)
[WHITE_SQUARE] 3. .env existe et variables correctes?
[WHITE_SQUARE] 4. RDS instance running? (aws rds describe-db-instances)
[WHITE_SQUARE] 5. Base de données créée? (SHOW DATABASES;)
[WHITE_SQUARE] 6. Tables créées? (GET /api/init appelé?)
[WHITE_SQUARE] 7. Security group OK? (IP autorisée?)
[WHITE_SQUARE] 8. Port 5000 disponible?
[WHITE_SQUARE] 9. Vérifier logs Flask (regarde l'erreur exacte!)
[WHITE_SQUARE] 10. Tester connexion BD directement (mysql CLI)


═══════════════════════════════════════════════════════════════════════════════
[NOTE] LOGS UTILES
═══════════════════════════════════════════════════════════════════════════════

# Voir logs Flask (terminal):
# Chercher: ERROR, WARNING, Traceback

# Voir logs RDS:
aws rds download-db-log-file-portion \
  --db-instance-identifier flask-app-db \
  --db-log-file-name error/mysql-error.log

# Voir logs erreur applicatif (Python):
python app.py 2>&1 | tee app.log
# Puis consulter app.log

# Activer DEBUG mode Flask:
FLASK_ENV=development
FLASK_DEBUG=1
# Recharge auto + meilleur error display


═══════════════════════════════════════════════════════════════════════════════
🆘 HELP! Encore pas résolu?
═══════════════════════════════════════════════════════════════════════════════

1. Copier l'erreur exacte (Traceback complet)
2. Essayer chaque diagnostic/solution ci-dessus
3. Vérifier AWS CloudWatch logs
4. Consulter Flask docs: https://flask.palletsprojects.com
5. Consulter SQLAlchemy docs: https://docs.sqlalchemy.org