# Fichier: python_cheats/cheatsheets/suite_1.txt
# Suite Architecture Trois Tiers - Microservices & GraphQL
# Complément du fichier architecture_trois_tiers.txt


[OK] MICROSERVICES ARCHITECTURE


# === QU'EST-CE QUE LES MICROSERVICES ? ===

Les microservices sont une approche architecturale qui décompose une application
en plusieurs services indépendants, chacun responsable d'une fonctionnalité métier
spécifique. Contrairement à l'architecture monolithique (trois tiers), où toute
la logique métier est dans un seul backend, les microservices séparent chaque
domaine fonctionnel en son propre service autonome.


# ARCHITECTURE MONOLITHIQUE VS MICROSERVICES

┌─────────────────────────────────────────────────────────────────┐
│                    MONOLITHIQUE (3-TIER)                        │
└─────────────────────────────────────────────────────────────────┘

┌─────────────┐
│  Frontend   │
│   (React)   │
└──────┬──────┘
       │ HTTP
       v
┌──────────────────────────────────────┐
│         Backend Unique (Flask)       │
│ ┌──────────┬──────────┬────────────┐ │
│ │   Auth   │ Products │   Orders   │ │
│ │  Module  │  Module  │   Module   │ │
│ └──────────┴──────────┴────────────┘ │
└──────────────┬───────────────────────┘
               │ SQL
               v
┌──────────────────────────┐
│  Database Unique (SQL)   │
└──────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│                        MICROSERVICES                            │
└─────────────────────────────────────────────────────────────────┘

                    ┌─────────────┐
                    │  Frontend   │
                    │   (React)   │
                    └──────┬──────┘
                           │
              ┌────────────┼────────────┐
              │            │            │
              v            v            v
    ┌──────────────┐ ┌──────────┐ ┌──────────┐
    │ Auth Service │ │ Product  │ │  Order   │
    │   (Flask)    │ │ Service  │ │ Service  │
    │              │ │ (Flask)  │ │ (Flask)  │
    └──────┬───────┘ └────┬─────┘ └────┬─────┘
           │              │            │
           v              v            v
    ┌─────────┐    ┌─────────┐  ┌─────────┐
    │ Auth DB │    │Product  │  │Order DB │
    │(Postgres)│   │DB       │  │(Postgres)│
    └─────────┘    │(MongoDB)│  └─────────┘
                   └─────────┘


# === AVANTAGES DES MICROSERVICES ===

[OK] Déploiement indépendant
  - Chaque service peut être déployé sans impacter les autres
  - Permet des releases plus fréquentes et moins risquées

[OK] Scalabilité ciblée
  - Scaler uniquement les services qui en ont besoin
  - Optimisation des ressources et coûts

[OK] Technologie flexible
  - Chaque service peut utiliser la stack la plus adaptée
  - Python pour un service, Node.js pour un autre, Go pour un troisième

[OK] Résilience
  - La panne d'un service n'affecte pas les autres
  - Isolation des erreurs

[OK] Organisation des équipes
  - Chaque équipe possède son service
  - Autonomie et responsabilité claires

[OK] Base de code plus petite
  - Plus facile à comprendre et maintenir
  - Onboarding simplifié


# === INCONVÉNIENTS DES MICROSERVICES ===

[X] Complexité opérationnelle
  - Nombreux services à déployer et monitorer
  - Nécessite orchestration (Kubernetes)

[X] Communication réseau
  - Latence entre services
  - Gestion des erreurs réseau

[X] Cohérence des données
  - Transactions distribuées complexes
  - Éventuelle consistance (eventual consistency)

[X] Debugging difficile
  - Les erreurs traversent plusieurs services
  - Nécessite distributed tracing

[X] Overhead d'infrastructure
  - Plus de serveurs, bases de données
  - Coûts d'infrastructure plus élevés

[X] Courbe d'apprentissage
  - Technologies supplémentaires à maîtriser
  - Patterns distribués (saga, circuit breaker, etc.)


# === QUAND UTILISER LES MICROSERVICES ? ===

# [OK] Utiliser microservices si:
- Application grande et complexe (50+ développeurs)
- Équipes multiples avec domaines distincts
- Besoins de scalabilité différents par fonctionnalité
- Besoin de technologies différentes par service
- Releases fréquentes et indépendantes nécessaires
- Forte croissance prévue

# [X] Ne PAS utiliser microservices si:
- Petite application (<5 développeurs)
- Démarrage d'un projet (MVP, startup early stage)
- Pas de besoins de scalabilité différenciée
- Équipe unique et petite
- Budget infrastructure limité
- Pas d'expertise DevOps/infrastructure

# [IDEE] Règle d'or: Commencer monolithique, migrer vers microservices quand nécessaire


# === ARCHITECTURE MICROSERVICES COMPLÈTE ===

┌─────────────────────────────────────────────────────────────────┐
│                           CLIENT LAYER                          │
│             ┌──────────┐  ┌──────────┐  ┌──────────┐            │
│             │  Web App │  │ Mobile   │  │  Desktop │            │
│             │ (React)  │  │(React N.)│  │ (Electron)            │
│             └────┬─────┘  └────┬─────┘  └────┬─────┘            │
└──────────────────┼─────────────┼─────────────┼──────────────────┘
                   │             │             │
                   └─────────────┼─────────────┘
                                 v
┌────────────────────────────────────────────────────────────────┐
│                      API GATEWAY                               │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │  - Routing                  - Rate Limiting              │  │
│  │  - Authentication           - Load Balancing             │  │
│  │  - Request Aggregation      - Caching                    │  │
│  └──────────────────────────────────────────────────────────┘  │
└───────-┬─────────────┬─────────────┬─────────────┬─────────────┘
         │             │             │             │
         v             v             v             v
    ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐
    │  Auth    │  │ Product  │  │  Order   │  │  User    │
    │ Service  │  │ Service  │  │ Service  │  │ Service  │
    │ (Flask)  │  │ (Flask)  │  │ (Flask)  │  │ (Flask)  │
    └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘
         │             │             │             │
         v             v             v             v
    ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐
    │ Auth DB │  │Product  │  │Order DB │  │ User DB │
    │(Redis)  │  │DB       │  │(Postgres)│ │(Postgres)│
    └─────────┘  │(MongoDB)│  └─────────┘  └─────────┘
                 └─────────┘

┌────────────────────────────────────────────────────────────────┐
│                   INFRASTRUCTURE LAYER                         │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐          │
│  │Message Queue │  │Service       │  │  Monitoring  │          │
│  │  (RabbitMQ)  │  │Discovery     │  │ (Prometheus) │          │
│  └──────────────┘  │  (Consul)    │  └──────────────┘          │
│                    └──────────────┘                            │
└────────────────────────────────────────────────────────────────┘


# === COMPOSANTS D'UNE ARCHITECTURE MICROSERVICES ===

1. API GATEWAY (Kong, AWS API Gateway, NGINX)
   - Point d'entrée unique pour tous les clients
   - Routing vers les services appropriés
   - Authentification centralisée
   - Rate limiting global
   - Transformation de requêtes

2. SERVICE DISCOVERY (Consul, Eureka, etcd)
   - Enregistrement automatique des services
   - Health checks
   - DNS dynamique
   - Load balancing

3. MESSAGE QUEUE (RabbitMQ, Kafka, AWS SQS)
   - Communication asynchrone entre services
   - Découplage des services
   - Garantie de livraison des messages
   - Event sourcing

4. DISTRIBUTED TRACING (Jaeger, Zipkin)
   - Traçabilité des requêtes à travers services
   - Performance monitoring
   - Debugging distribué

5. CENTRALIZED LOGGING (ELK Stack, Loki)
   - Agrégation des logs de tous les services
   - Recherche et analyse centralisées
   - Alerting

6. CONFIGURATION MANAGEMENT (Consul, Spring Cloud Config)
   - Configuration centralisée
   - Changements dynamiques sans redéploiement
   - Secrets management

7. ORCHESTRATION (Kubernetes, Docker Swarm)
   - Déploiement automatisé
   - Scaling automatique
   - Self-healing
   - Rolling updates


# === IMPLÉMENTATION: ARCHITECTURE MICROSERVICES ===


# === STRUCTURE PROJET MICROSERVICES ===

myapp-microservices/
├── services/
│   ├── auth-service/           # Service d'authentification
│   │   ├── app/
│   │   │   ├── __init__.py
│   │   │   ├── models.py
│   │   │   ├── routes.py
│   │   │   └── config.py
│   │   ├── tests/
│   │   ├── Dockerfile
│   │   ├── requirements.txt
│   │   └── run.py
│   │
│   ├── product-service/        # Service produits
│   │   ├── app/
│   │   │   ├── __init__.py
│   │   │   ├── models.py
│   │   │   ├── routes.py
│   │   │   └── config.py
│   │   ├── tests/
│   │   ├── Dockerfile
│   │   ├── requirements.txt
│   │   └── run.py
│   │
│   ├── order-service/          # Service commandes
│   │   ├── app/
│   │   │   ├── __init__.py
│   │   │   ├── models.py
│   │   │   ├── routes.py
│   │   │   ├── events.py       # Event handlers
│   │   │   └── config.py
│   │   ├── tests/
│   │   ├── Dockerfile
│   │   ├── requirements.txt
│   │   └── run.py
│   │
│   ├── user-service/           # Service utilisateurs
│   │   ├── app/
│   │   ├── tests/
│   │   ├── Dockerfile
│   │   └── requirements.txt
│   │
│   └── notification-service/   # Service notifications
│       ├── app/
│       ├── tests/
│       ├── Dockerfile
│       └── requirements.txt
│
├── api-gateway/               # API Gateway
│   ├── kong.yml               # Configuration Kong
│   └── nginx.conf             # Ou configuration NGINX
│
├── shared/                    # Code partagé
│   ├── auth/                  # Utils auth communes
│   │   ├── __init__.py
│   │   └── jwt_utils.py
│   ├── models/                # Modèles partagés
│   │   └── base.py
│   └── messaging/             # Utils messaging
│       ├── __init__.py
│       └── rabbitmq.py
│
├── infrastructure/            # Infrastructure as Code
│   ├── kubernetes/
│   │   ├── auth-deployment.yml
│   │   ├── product-deployment.yml
│   │   ├── order-deployment.yml
│   │   └── ingress.yml
│   ├── terraform/
│   │   ├── main.tf
│   │   └── variables.tf
│   └── docker-compose.yml
│
├── frontend/                  # Frontend (optionnel)
│   └── react-app/
│
└── docs/
    ├── architecture.md
    └── api-documentation.md


# === SERVICE 1: AUTH SERVICE ===

# services/auth-service/app/__init__.py

from flask import Flask
from flask_cors import CORS
from flask_jwt_extended import JWTManager
import redis
import os

def create_app():
    app = Flask(__name__)
    
    # Configuration
    app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY')
    app.config['JWT_ACCESS_TOKEN_EXPIRES'] = 3600
    
    # Redis pour stocker tokens (blacklist)
    redis_client = redis.from_url(os.getenv('REDIS_URL', 'redis://localhost:6379'))
    app.redis = redis_client
    
    # JWT
    jwt = JWTManager(app)
    
    # CORS
    CORS(app)
    
    # Routes
    from app.routes import auth_bp
    app.register_blueprint(auth_bp, url_prefix='/api/auth')
    
    @app.route('/health')
    def health():
        return {'status': 'healthy', 'service': 'auth'}, 200
    
    return app


# services/auth-service/app/routes.py

from flask import Blueprint, request, jsonify, current_app
from flask_jwt_extended import (
    create_access_token, 
    jwt_required, 
    get_jwt_identity,
    get_jwt
)
import requests
import bcrypt

auth_bp = Blueprint('auth', __name__)

# URL du user service (via service discovery ou variable env)
USER_SERVICE_URL = os.getenv('USER_SERVICE_URL', 'http://user-service:5001')

@auth_bp.route('/register', methods=['POST'])
def register():
    """Inscription - délègue au user service"""
    data = request.get_json()
    
    try:
        # Appeler user service pour créer l'utilisateur
        response = requests.post(
            f'{USER_SERVICE_URL}/api/users',
            json=data,
            timeout=5
        )
        
        if response.status_code == 201:
            return response.json(), 201
        else:
            return response.json(), response.status_code
    
    except requests.exceptions.RequestException as e:
        return jsonify({'error': 'User service unavailable'}), 503

@auth_bp.route('/login', methods=['POST'])
def login():
    """Connexion"""
    data = request.get_json()
    email = data.get('email')
    password = data.get('password')
    
    if not email or not password:
        return jsonify({'error': 'Email et password requis'}), 400
    
    try:
        # Récupérer utilisateur depuis user service
        response = requests.get(
            f'{USER_SERVICE_URL}/api/users/by-email/{email}',
            timeout=5
        )
        
        if response.status_code != 200:
            return jsonify({'error': 'Identifiants invalides'}), 401
        
        user = response.json()
        
        # Vérifier mot de passe
        if not bcrypt.checkpw(
            password.encode('utf-8'),
            user['password_hash'].encode('utf-8')
        ):
            return jsonify({'error': 'Identifiants invalides'}), 401
        
        # Créer token JWT
        access_token = create_access_token(
            identity=user['id'],
            additional_claims={'email': user['email']}
        )
        
        # Ne pas retourner le password_hash
        user.pop('password_hash', None)
        
        return jsonify({
            'access_token': access_token,
            'user': user
        }), 200
    
    except requests.exceptions.RequestException:
        return jsonify({'error': 'User service unavailable'}), 503

@auth_bp.route('/logout', methods=['POST'])
@jwt_required()
def logout():
    """Déconnexion - ajoute le token à la blacklist"""
    jti = get_jwt()['jti']  # JWT ID unique
    
    # Ajouter à la blacklist Redis (expire après 1h)
    current_app.redis.setex(f'blacklist:{jti}', 3600, '1')
    
    return jsonify({'message': 'Déconnecté avec succès'}), 200

@auth_bp.route('/verify', methods=['POST'])
@jwt_required()
def verify_token():
    """Vérifie la validité d'un token (pour autres services)"""
    jti = get_jwt()['jti']
    
    # Vérifier si dans blacklist
    if current_app.redis.get(f'blacklist:{jti}'):
        return jsonify({'valid': False, 'error': 'Token révoqué'}), 401
    
    user_id = get_jwt_identity()
    claims = get_jwt()
    
    return jsonify({
        'valid': True,
        'user_id': user_id,
        'email': claims.get('email')
    }), 200

@auth_bp.route('/refresh', methods=['POST'])
@jwt_required()
def refresh():
    """Rafraîchit un token"""
    user_id = get_jwt_identity()
    claims = get_jwt()
    
    new_token = create_access_token(
        identity=user_id,
        additional_claims={'email': claims.get('email')}
    )
    
    return jsonify({'access_token': new_token}), 200


# services/auth-service/Dockerfile

FROM python:3.11-slim

WORKDIR /app

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

COPY . .

EXPOSE 5000

CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0:5000", "run:app"]


# services/auth-service/requirements.txt

Flask==3.0.0
Flask-CORS==4.0.0
Flask-JWT-Extended==4.6.0
redis==5.0.1
requests==2.31.0
bcrypt==4.1.2
gunicorn==21.2.0


# === SERVICE 2: USER SERVICE ===

# services/user-service/app/__init__.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
import os

db = SQLAlchemy()

def create_app():
    app = Flask(__name__)
    
    # Configuration
    app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL')
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    
    # Database
    db.init_app(app)
    
    # CORS
    CORS(app)
    
    # Routes
    from app.routes import users_bp
    app.register_blueprint(users_bp, url_prefix='/api/users')
    
    @app.route('/health')
    def health():
        try:
            db.session.execute('SELECT 1')
            return {'status': 'healthy', 'service': 'user', 'db': 'connected'}, 200
        except:
            return {'status': 'unhealthy', 'service': 'user', 'db': 'disconnected'}, 503
    
    return app


# services/user-service/app/models.py

from app import db
from datetime import datetime

class User(db.Model):
    __tablename__ = 'users'
    
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(50), unique=True, nullable=False)
    email = db.Column(db.String(100), unique=True, nullable=False)
    password_hash = db.Column(db.String(255), nullable=False)
    first_name = db.Column(db.String(50))
    last_name = db.Column(db.String(50))
    phone = db.Column(db.String(20))
    address = db.Column(db.Text)
    is_active = db.Column(db.Boolean, default=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    def to_dict(self, include_sensitive=False):
        """Sérialise l'utilisateur"""
        data = {
            'id': self.id,
            'username': self.username,
            'email': self.email,
            'first_name': self.first_name,
            'last_name': self.last_name,
            'phone': self.phone,
            'address': self.address,
            'is_active': self.is_active,
            'created_at': self.created_at.isoformat()
        }
        
        if include_sensitive:
            data['password_hash'] = self.password_hash
        
        return data


# services/user-service/app/routes.py

from flask import Blueprint, request, jsonify
from app import db
from app.models import User
import bcrypt

users_bp = Blueprint('users', __name__)

@users_bp.route('', methods=['POST'])
def create_user():
    """Crée un utilisateur"""
    data = request.get_json()
    
    # Validation
    required_fields = ['username', 'email', 'password']
    if not all(field in data for field in required_fields):
        return jsonify({'error': 'Champs manquants'}), 400
    
    # Vérifier unicité
    if User.query.filter_by(email=data['email']).first():
        return jsonify({'error': 'Email déjà utilisé'}), 400
    
    if User.query.filter_by(username=data['username']).first():
        return jsonify({'error': 'Username déjà utilisé'}), 400
    
    # Hasher le mot de passe
    password_hash = bcrypt.hashpw(
        data['password'].encode('utf-8'),
        bcrypt.gensalt()
    ).decode('utf-8')
    
    # Créer utilisateur
    user = User(
        username=data['username'],
        email=data['email'],
        password_hash=password_hash,
        first_name=data.get('first_name'),
        last_name=data.get('last_name'),
        phone=data.get('phone'),
        address=data.get('address')
    )
    
    db.session.add(user)
    db.session.commit()
    
    return jsonify(user.to_dict()), 201

@users_bp.route('/<int:user_id>', methods=['GET'])
def get_user(user_id):
    """Récupère un utilisateur par ID"""
    user = User.query.get(user_id)
    
    if not user:
        return jsonify({'error': 'Utilisateur non trouvé'}), 404
    
    return jsonify(user.to_dict()), 200

@users_bp.route('/by-email/<email>', methods=['GET'])
def get_user_by_email(email):
    """Récupère un utilisateur par email (pour auth service)"""
    user = User.query.filter_by(email=email).first()
    
    if not user:
        return jsonify({'error': 'Utilisateur non trouvé'}), 404
    
    # Inclure password_hash pour vérification auth
    return jsonify(user.to_dict(include_sensitive=True)), 200

@users_bp.route('/<int:user_id>', methods=['PUT'])
def update_user(user_id):
    """Met à jour un utilisateur"""
    user = User.query.get(user_id)
    
    if not user:
        return jsonify({'error': 'Utilisateur non trouvé'}), 404
    
    data = request.get_json()
    
    # Mise à jour des champs autorisés
    allowed_fields = ['first_name', 'last_name', 'phone', 'address']
    for field in allowed_fields:
        if field in data:
            setattr(user, field, data[field])
    
    db.session.commit()
    
    return jsonify(user.to_dict()), 200

@users_bp.route('/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
    """Désactive un utilisateur (soft delete)"""
    user = User.query.get(user_id)
    
    if not user:
        return jsonify({'error': 'Utilisateur non trouvé'}), 404
    
    user.is_active = False
    db.session.commit()
    
    return jsonify({'message': 'Utilisateur désactivé'}), 200

@users_bp.route('', methods=['GET'])
def list_users():
    """Liste tous les utilisateurs actifs"""
    page = request.args.get('page', 1, type=int)
    per_page = request.args.get('per_page', 20, type=int)
    
    pagination = User.query.filter_by(is_active=True).paginate(
        page=page,
        per_page=per_page,
        error_out=False
    )
    
    return jsonify({
        'users': [user.to_dict() for user in pagination.items],
        'total': pagination.total,
        'pages': pagination.pages,
        'current_page': page
    }), 200


# === SERVICE 3: PRODUCT SERVICE (avec MongoDB) ===

# services/product-service/app/__init__.py

from flask import Flask
from flask_cors import CORS
from pymongo import MongoClient
import os

mongo_client = None

def create_app():
    global mongo_client
    
    app = Flask(__name__)
    
    # MongoDB
    mongo_uri = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/')
    mongo_client = MongoClient(mongo_uri)
    app.db = mongo_client.product_db
    
    # CORS
    CORS(app)
    
    # Routes
    from app.routes import products_bp
    app.register_blueprint(products_bp, url_prefix='/api/products')
    
    @app.route('/health')
    def health():
        try:
            app.db.command('ping')
            return {'status': 'healthy', 'service': 'product', 'db': 'connected'}, 200
        except:
            return {'status': 'unhealthy', 'service': 'product', 'db': 'disconnected'}, 503
    
    return app


# services/product-service/app/routes.py

from flask import Blueprint, request, jsonify, current_app
from bson import ObjectId
from datetime import datetime

products_bp = Blueprint('products', __name__)

def serialize_product(product):
    """Convertit un document MongoDB en JSON"""
    if product:
        product['id'] = str(product['_id'])
        del product['_id']
    return product

@products_bp.route('', methods=['POST'])
def create_product():
    """Crée un produit"""
    data = request.get_json()
    
    # Validation
    required_fields = ['name', 'price', 'stock']
    if not all(field in data for field in required_fields):
        return jsonify({'error': 'Champs manquants'}), 400
    
    # Document MongoDB
    product = {
        'name': data['name'],
        'description': data.get('description', ''),
        'price': float(data['price']),
        'stock': int(data['stock']),
        'category': data.get('category'),
        'tags': data.get('tags', []),
        'images': data.get('images', []),
        'specifications': data.get('specifications', {}),
        'created_by': data.get('created_by'),
        'created_at': datetime.utcnow(),
        'updated_at': datetime.utcnow()
    }
    
    result = current_app.db.products.insert_one(product)
    product['_id'] = result.inserted_id
    
    return jsonify(serialize_product(product)), 201

@products_bp.route('/<product_id>', methods=['GET'])
def get_product(product_id):
    """Récupère un produit"""
    try:
        product = current_app.db.products.find_one({'_id': ObjectId(product_id)})
        
        if not product:
            return jsonify({'error': 'Produit non trouvé'}), 404
        
        return jsonify(serialize_product(product)), 200
    except:
        return jsonify({'error': 'ID invalide'}), 400

@products_bp.route('', methods=['GET'])
def list_products():
    """Liste les produits avec filtres"""
    page = request.args.get('page', 1, type=int)
    per_page = request.args.get('per_page', 20, type=int)
    category = request.args.get('category')
    min_price = request.args.get('min_price', type=float)
    max_price = request.args.get('max_price', type=float)
    search = request.args.get('search')
    
    # Construction du filtre
    query = {}
    
    if category:
        query['category'] = category
    
    if min_price is not None or max_price is not None:
        query['price'] = {}
        if min_price is not None:
            query['price']['$gte'] = min_price
        if max_price is not None:
            query['price']['$lte'] = max_price
    
    if search:
        query['$or'] = [
            {'name': {'$regex': search, '$options': 'i'}},
            {'description': {'$regex': search, '$options': 'i'}}
        ]
    
    # Pagination
    skip = (page - 1) * per_page
    
    products = list(
        current_app.db.products
        .find(query)
        .skip(skip)
        .limit(per_page)
        .sort('created_at', -1)
    )
    
    total = current_app.db.products.count_documents(query)
    
    return jsonify({
        'products': [serialize_product(p) for p in products],
        'total': total,
        'pages': (total + per_page - 1) // per_page,
        'current_page': page
    }), 200

@products_bp.route('/<product_id>', methods=['PUT'])
def update_product(product_id):
    """Met à jour un produit"""
    try:
        data = request.get_json()
        
        # Champs autorisés
        update_fields = {}
        allowed_fields = ['name', 'description', 'price', 'stock', 'category', 'tags', 'images', 'specifications']
        
        for field in allowed_fields:
            if field in data:
                update_fields[field] = data[field]
        
        if not update_fields:
            return jsonify({'error': 'Aucun champ à mettre à jour'}), 400
        
        update_fields['updated_at'] = datetime.utcnow()
        
        result = current_app.db.products.update_one(
            {'_id': ObjectId(product_id)},
            {'$set': update_fields}
        )
        
        if result.matched_count == 0:
            return jsonify({'error': 'Produit non trouvé'}), 404
        
        product = current_app.db.products.find_one({'_id': ObjectId(product_id)})
        return jsonify(serialize_product(product)), 200
    
    except:
        return jsonify({'error': 'ID invalide'}), 400

@products_bp.route('/<product_id>/stock', methods=['PATCH'])
def update_stock(product_id):
    """Met à jour le stock d'un produit"""
    try:
        data = request.get_json()
        quantity = data.get('quantity')
        
        if quantity is None:
            return jsonify({'error': 'Quantity requis'}), 400
        
        # Mise à jour atomique du stock
        result = current_app.db.products.update_one(
            {'_id': ObjectId(product_id)},
            {
                '$inc': {'stock': quantity},
                '$set': {'updated_at': datetime.utcnow()}
            }
        )
        
        if result.matched_count == 0:
            return jsonify({'error': 'Produit non trouvé'}), 404
        
        product = current_app.db.products.find_one({'_id': ObjectId(product_id)})
        
        # Vérifier si stock négatif
        if product['stock'] < 0:
            current_app.db.products.update_one(
                {'_id': ObjectId(product_id)},
                {'$set': {'stock': 0}}
            )
            return jsonify({'error': 'Stock insuffisant'}), 400
        
        return jsonify(serialize_product(product)), 200
    
    except:
        return jsonify({'error': 'ID invalide'}), 400


# services/product-service/requirements.txt

Flask==3.0.0
Flask-CORS==4.0.0
pymongo==4.6.0
gunicorn==21.2.0


# === SERVICE 4: ORDER SERVICE (avec événements) ===

# services/order-service/app/__init__.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
import os
import pika

db = SQLAlchemy()
rabbitmq_connection = None
rabbitmq_channel = None

def create_app():
    global rabbitmq_connection, rabbitmq_channel
    
    app = Flask(__name__)
    
    # Configuration
    app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL')
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    
    # Database
    db.init_app(app)
    
    # RabbitMQ
    try:
        rabbitmq_url = os.getenv('RABBITMQ_URL', 'amqp://guest:guest@localhost:5672/')
        rabbitmq_connection = pika.BlockingConnection(pika.URLParameters(rabbitmq_url))
        rabbitmq_channel = rabbitmq_connection.channel()
        
        # Déclarer exchanges et queues
        rabbitmq_channel.exchange_declare(exchange='orders', exchange_type='topic', durable=True)
        rabbitmq_channel.queue_declare(queue='order_created', durable=True)
        rabbitmq_channel.queue_declare(queue='order_updated', durable=True)
        
        rabbitmq_channel.queue_bind(exchange='orders', queue='order_created', routing_key='order.created')
        rabbitmq_channel.queue_bind(exchange='orders', queue='order_updated', routing_key='order.updated')
        
        app.rabbitmq = rabbitmq_channel
    except Exception as e:
        print(f"RabbitMQ connection failed: {e}")
        app.rabbitmq = None
    
    # CORS
    CORS(app)
    
    # Routes
    from app.routes import orders_bp
    app.register_blueprint(orders_bp, url_prefix='/api/orders')
    
    @app.route('/health')
    def health():
        db_status = 'connected'
        mq_status = 'connected' if app.rabbitmq else 'disconnected'
        
        try:
            db.session.execute('SELECT 1')
        except:
            db_status = 'disconnected'
        
        healthy = db_status == 'connected'
        
        return {
            'status': 'healthy' if healthy else 'unhealthy',
            'service': 'order',
            'db': db_status,
            'message_queue': mq_status
        }, 200 if healthy else 503
    
    return app


# services/order-service/app/models.py

from app import db
from datetime import datetime

class Order(db.Model):
    __tablename__ = 'orders'
    
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, nullable=False)
    total_amount = db.Column(db.Numeric(10, 2), nullable=False)
    status = db.Column(db.String(20), default='pending')
    payment_method = db.Column(db.String(50))
    payment_status = db.Column(db.String(20), default='pending')
    shipping_address = db.Column(db.Text)
    notes = db.Column(db.Text)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    items = db.relationship('OrderItem', backref='order', lazy=True, cascade='all, delete-orphan')
    
    def to_dict(self):
        return {
            'id': self.id,
            'user_id': self.user_id,
            'total_amount': float(self.total_amount),
            'status': self.status,
            'payment_method': self.payment_method,
            'payment_status': self.payment_status,
            'shipping_address': self.shipping_address,
            'notes': self.notes,
            'items': [item.to_dict() for item in self.items],
            'created_at': self.created_at.isoformat(),
            'updated_at': self.updated_at.isoformat()
        }

class OrderItem(db.Model):
    __tablename__ = 'order_items'
    
    id = db.Column(db.Integer, primary_key=True)
    order_id = db.Column(db.Integer, db.ForeignKey('orders.id'), nullable=False)
    product_id = db.Column(db.String(50), nullable=False)
    product_name = db.Column(db.String(100), nullable=False)
    quantity = db.Column(db.Integer, nullable=False)
    unit_price = db.Column(db.Numeric(10, 2), nullable=False)
    
    def to_dict(self):
        return {
            'id': self.id,
            'product_id': self.product_id,
            'product_name': self.product_name,
            'quantity': self.quantity,
            'unit_price': float(self.unit_price),
            'subtotal': float(self.quantity * self.unit_price)
        }


# services/order-service/app/routes.py

from flask import Blueprint, request, jsonify, current_app
from app import db
from app.models import Order, OrderItem
from app.events import publish_order_event
import requests
from decimal import Decimal

orders_bp = Blueprint('orders', __name__)

# URLs des autres services
PRODUCT_SERVICE_URL = os.getenv('PRODUCT_SERVICE_URL', 'http://product-service:5002')
USER_SERVICE_URL = os.getenv('USER_SERVICE_URL', 'http://user-service:5001')

@orders_bp.route('', methods=['POST'])
def create_order():
    """Crée une nouvelle commande"""
    data = request.get_json()
    
    # Validation
    required_fields = ['user_id', 'items']
    if not all(field in data for field in required_fields):
        return jsonify({'error': 'Champs manquants'}), 400
    
    if not data['items']:
        return jsonify({'error': 'Commande vide'}), 400
    
    try:
        # Vérifier que l'utilisateur existe
        user_response = requests.get(
            f'{USER_SERVICE_URL}/api/users/{data["user_id"]}',
            timeout=5
        )
        
        if user_response.status_code != 200:
            return jsonify({'error': 'Utilisateur non trouvé'}), 404
        
        # Vérifier produits et calculer total
        order_items = []
        total_amount = Decimal('0.00')
        
        for item_data in data['items']:
            product_id = item_data['product_id']
            quantity = item_data['quantity']
            
            # Récupérer infos produit
            product_response = requests.get(
                f'{PRODUCT_SERVICE_URL}/api/products/{product_id}',
                timeout=5
            )
            
            if product_response.status_code != 200:
                return jsonify({'error': f'Produit {product_id} non trouvé'}), 404
            
            product = product_response.json()
            
            # Vérifier stock
            if product['stock'] < quantity:
                return jsonify({'error': f'Stock insuffisant pour {product["name"]}'}), 400
            
            # Calculer sous-total
            unit_price = Decimal(str(product['price']))
            subtotal = unit_price * quantity
            total_amount += subtotal
            
            order_items.append({
                'product_id': product_id,
                'product_name': product['name'],
                'quantity': quantity,
                'unit_price': unit_price
            })
        
        # Créer la commande
        order = Order(
            user_id=data['user_id'],
            total_amount=total_amount,
            payment_method=data.get('payment_method'),
            shipping_address=data.get('shipping_address'),
            notes=data.get('notes')
        )
        
        db.session.add(order)
        db.session.flush()
        
        # Créer les items
        for item_data in order_items:
            order_item = OrderItem(
                order_id=order.id,
                product_id=item_data['product_id'],
                product_name=item_data['product_name'],
                quantity=item_data['quantity'],
                unit_price=item_data['unit_price']
            )
            db.session.add(order_item)
        
        db.session.commit()
        
        # Mettre à jour stock des produits
        for item in order_items:
            requests.patch(
                f'{PRODUCT_SERVICE_URL}/api/products/{item["product_id"]}/stock',
                json={'quantity': -item['quantity']},
                timeout=5
            )
        
        # Publier événement order.created
        publish_order_event('order.created', order.to_dict())
        
        return jsonify(order.to_dict()), 201
    
    except requests.exceptions.RequestException:
        db.session.rollback()
        return jsonify({'error': 'Service externe non disponible'}), 503
    except Exception as e:
        db.session.rollback()
        return jsonify({'error': str(e)}), 500

@orders_bp.route('/<int:order_id>', methods=['GET'])
def get_order(order_id):
    """Récupère une commande"""
    order = Order.query.get(order_id)
    
    if not order:
        return jsonify({'error': 'Commande non trouvée'}), 404
    
    return jsonify(order.to_dict()), 200

@orders_bp.route('/user/<int:user_id>', methods=['GET'])
def get_user_orders(user_id):
    """Récupère les commandes d'un utilisateur"""
    orders = Order.query.filter_by(user_id=user_id).order_by(Order.created_at.desc()).all()
    
    return jsonify([order.to_dict() for order in orders]), 200

@orders_bp.route('/<int:order_id>/status', methods=['PATCH'])
def update_order_status(order_id):
    """Met à jour le statut d'une commande"""
    order = Order.query.get(order_id)
    
    if not order:
        return jsonify({'error': 'Commande non trouvée'}), 404
    
    data = request.get_json()
    new_status = data.get('status')
    
    valid_statuses = ['pending', 'processing', 'shipped', 'delivered', 'cancelled']
    
    if new_status not in valid_statuses:
        return jsonify({'error': f'Statut invalide. Valeurs: {valid_statuses}'}), 400
    
    old_status = order.status
    order.status = new_status
    
    db.session.commit()
    
    # Publier événement
    publish_order_event('order.updated', {
        'order_id': order.id,
        'old_status': old_status,
        'new_status': new_status,
        'order': order.to_dict()
    })
    
    return jsonify(order.to_dict()), 200

@orders_bp.route('/<int:order_id>/cancel', methods=['POST'])
def cancel_order(order_id):
    """Annule une commande"""
    order = Order.query.get(order_id)
    
    if not order:
        return jsonify({'error': 'Commande non trouvée'}), 404
    
    if order.status in ['shipped', 'delivered', 'cancelled']:
        return jsonify({'error': 'Impossible d\'annuler cette commande'}), 400
    
    # Restaurer stock
    for item in order.items:
        try:
            requests.patch(
                f'{PRODUCT_SERVICE_URL}/api/products/{item.product_id}/stock',
                json={'quantity': item.quantity},
                timeout=5
            )
        except:
            pass
    
    order.status = 'cancelled'
    db.session.commit()
    
    # Publier événement
    publish_order_event('order.cancelled', order.to_dict())
    
    return jsonify(order.to_dict()), 200


# services/order-service/app/events.py

from flask import current_app
import json
import pika

def publish_order_event(event_type, data):
    """Publie un événement dans RabbitMQ"""
    if not current_app.rabbitmq:
        print(f"RabbitMQ non disponible, événement non publié: {event_type}")
        return
    
    try:
        message = json.dumps({
            'event_type': event_type,
            'data': data,
            'timestamp': datetime.utcnow().isoformat()
        })
        
        current_app.rabbitmq.basic_publish(
            exchange='orders',
            routing_key=event_type.replace('.', '.'),
            body=message,
            properties=pika.BasicProperties(
                delivery_mode=2,  # Persist message
                content_type='application/json'
            )
        )
        
        print(f"Événement publié: {event_type}")
    
    except Exception as e:
        print(f"Erreur publication événement: {e}")


# services/order-service/requirements.txt

Flask==3.0.0
Flask-SQLAlchemy==3.1.1
Flask-CORS==4.0.0
psycopg2-binary==2.9.9
requests==2.31.0
pika==1.3.2
gunicorn==21.2.0


# === SERVICE 5: NOTIFICATION SERVICE (Consumer) ===

# services/notification-service/app/__init__.py

from flask import Flask
import pika
import json
import threading

def create_app():
    app = Flask(__name__)
    
    @app.route('/health')
    def health():
        return {'status': 'healthy', 'service': 'notification'}, 200
    
    # Démarrer consumer dans un thread séparé
    consumer_thread = threading.Thread(target=start_consumer, daemon=True)
    consumer_thread.start()
    
    return app

def start_consumer():
    """Consomme les événements RabbitMQ"""
    try:
        rabbitmq_url = os.getenv('RABBITMQ_URL', 'amqp://guest:guest@localhost:5672/')
        connection = pika.BlockingConnection(pika.URLParameters(rabbitmq_url))
        channel = connection.channel()
        
        # S'abonner aux événements order
        channel.queue_declare(queue='order_created', durable=True)
        channel.queue_declare(queue='order_updated', durable=True)
        
        def callback(ch, method, properties, body):
            try:
                event = json.loads(body)
                handle_event(event)
                ch.basic_ack(delivery_tag=method.delivery_tag)
            except Exception as e:
                print(f"Erreur traitement événement: {e}")
                ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
        
        channel.basic_consume(queue='order_created', on_message_callback=callback)
        channel.basic_consume(queue='order_updated', on_message_callback=callback)
        
        print("Notification service: consommation événements démarrée")
        channel.start_consuming()
    
    except Exception as e:
        print(f"Erreur consumer: {e}")

def handle_event(event):
    """Traite un événement"""
    event_type = event.get('event_type')
    data = event.get('data')
    
    print(f"\n=== Événement reçu: {event_type} ===")
    
    if event_type == 'order.created':
        send_order_confirmation(data)
    
    elif event_type == 'order.updated':
        send_order_update(data)
    
    elif event_type == 'order.cancelled':
        send_order_cancellation(data)

def send_order_confirmation(order_data):
    """Envoie email de confirmation"""
    print(f"[EMAIL] Envoi email confirmation commande #{order_data['id']}")
    # Logique d'envoi email (SendGrid, Mailgun, etc.)

def send_order_update(order_data):
    """Envoie notification de mise à jour"""
    print(f"[EMAIL] Envoi notification mise à jour commande #{order_data['order_id']}")
    print(f"   Statut: {order_data['old_status']} -> {order_data['new_status']}")

def send_order_cancellation(order_data):
    """Envoie notification d'annulation"""
    print(f"[EMAIL] Envoi notification annulation commande #{order_data['id']}")


# services/notification-service/requirements.txt

Flask==3.0.0
pika==1.3.2
gunicorn==21.2.0


# === API GATEWAY (Kong ou NGINX) ===

# Avec NGINX

# api-gateway/nginx.conf

upstream auth_service {
    server auth-service:5000;
}

upstream user_service {
    server user-service:5001;
}

upstream product_service {
    server product-service:5002;
}

upstream order_service {
    server order-service:5003;
}

# Rate limiting
limit_req_zone $binary_remote_addr zone=general:10m rate=100r/m;
limit_req_zone $binary_remote_addr zone=auth:10m rate=10r/m;

server {
    listen 80;
    server_name api.myapp.com;

    # Logs
    access_log /var/log/nginx/api_access.log;
    error_log /var/log/nginx/api_error.log;

    # CORS Headers
    add_header 'Access-Control-Allow-Origin' '*' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;

    # Handle OPTIONS
    if ($request_method = 'OPTIONS') {
        return 204;
    }

    # Auth endpoints
    location /api/auth {
        limit_req zone=auth burst=5 nodelay;
        proxy_pass http://auth_service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # User endpoints
    location /api/users {
        limit_req zone=general burst=20 nodelay;
        proxy_pass http://user_service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # Product endpoints
    location /api/products {
        limit_req zone=general burst=50 nodelay;
        proxy_pass http://product_service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # Order endpoints
    location /api/orders {
        limit_req zone=general burst=20 nodelay;
        proxy_pass http://order_service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # Health checks
    location /health {
        return 200 "OK\n";
        add_header Content-Type text/plain;
    }
}


# === DOCKER COMPOSE COMPLET ===

# infrastructure/docker-compose.yml

version: '3.8'

services:
  # Databases
  postgres-user:
    image: postgres:15
    environment:
      POSTGRES_USER: userdb
      POSTGRES_PASSWORD: userpass
      POSTGRES_DB: users
    volumes:
      - postgres_user_data:/var/lib/postgresql/data
    networks:
      - microservices

  postgres-order:
    image: postgres:15
    environment:
      POSTGRES_USER: orderdb
      POSTGRES_PASSWORD: orderpass
      POSTGRES_DB: orders
    volumes:
      - postgres_order_data:/var/lib/postgresql/data
    networks:
      - microservices

  mongodb:
    image: mongo:7
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: mongopass
    volumes:
      - mongodb_data:/data/db
    networks:
      - microservices

  redis:
    image: redis:7-alpine
    networks:
      - microservices

  rabbitmq:
    image: rabbitmq:3-management-alpine
    environment:
      RABBITMQ_DEFAULT_USER: guest
      RABBITMQ_DEFAULT_PASS: guest
    ports:
      - "15672:15672"  # Management UI
    networks:
      - microservices

  # Services
  auth-service:
    build: ../services/auth-service
    environment:
      JWT_SECRET_KEY: ${JWT_SECRET_KEY}
      REDIS_URL: redis://redis:6379
      USER_SERVICE_URL: http://user-service:5001
    depends_on:
      - redis
    networks:
      - microservices

  user-service:
    build: ../services/user-service
    environment:
      DATABASE_URL: postgresql://userdb:userpass@postgres-user:5432/users
    depends_on:
      - postgres-user
    networks:
      - microservices

  product-service:
    build: ../services/product-service
    environment:
      MONGODB_URI: mongodb://admin:mongopass@mongodb:27017/
    depends_on:
      - mongodb
    networks:
      - microservices

  order-service:
    build: ../services/order-service
    environment:
      DATABASE_URL: postgresql://orderdb:orderpass@postgres-order:5432/orders
      RABBITMQ_URL: amqp://guest:guest@rabbitmq:5672/
      PRODUCT_SERVICE_URL: http://product-service:5002
      USER_SERVICE_URL: http://user-service:5001
    depends_on:
      - postgres-order
      - rabbitmq
    networks:
      - microservices

  notification-service:
    build: ../services/notification-service
    environment:
      RABBITMQ_URL: amqp://guest:guest@rabbitmq:5672/
    depends_on:
      - rabbitmq
    networks:
      - microservices

  # API Gateway
  nginx:
    image: nginx:alpine
    volumes:
      - ../api-gateway/nginx.conf:/etc/nginx/nginx.conf:ro
    ports:
      - "80:80"
    depends_on:
      - auth-service
      - user-service
      - product-service
      - order-service
    networks:
      - microservices

volumes:
  postgres_user_data:
  postgres_order_data:
  mongodb_data:

networks:
  microservices:
    driver: bridge


# === Lancer tous les services ===

docker-compose up --build


# === COMMUNICATION INTER-SERVICES ===

# PATTERN 1: Communication synchrone (REST)
──────────────────────────────────────────

# Order Service appelle Product Service
response = requests.get(
    f'{PRODUCT_SERVICE_URL}/api/products/{product_id}',
    timeout=5
)

# [OK] Avantages:
- Simple et direct
- Réponse immédiate
- Cohérence forte

# [X] Inconvénients:
- Couplage entre services
- Si Product Service down -> Order Service échoue
- Latence accumulée


# PATTERN 2: Communication asynchrone (Message Queue)
──────────────────────────────────────────────────────

# Order Service publie événement
publish_event('order.created', order_data)

# Notification Service consomme événement
def on_order_created(order_data):
    send_email(order_data)

# [OK] Avantages:
- Découplage total
- Résilience (queues persistent)
- Scalabilité (consumers multiples)

# [X] Inconvénients:
- Complexité accrue
- Cohérence éventuelle
- Debugging difficile


# PATTERN 3: Event Sourcing
────────────────────────────

# Stocker événements plutôt qu'état final
events = [
    {'type': 'order.created', 'data': {...}},
    {'type': 'order.paid', 'data': {...}},
    {'type': 'order.shipped', 'data': {...}}
]

# Reconstruire état en rejouant événements
def get_order_state(order_id):
    events = get_events(order_id)
    state = {}
    for event in events:
        state = apply_event(state, event)
    return state


# PATTERN 4: SAGA Pattern (Transactions distribuées)
──────────────────────────────────────────────────────

# Saga orchestrée: Order Service coordonne
def create_order_saga(order_data):
    # 1. Réserver produits
    product_reservation = reserve_products(order_data['items'])
    if not product_reservation.success:
        return compensate_and_fail()
    
    # 2. Charger utilisateur
    try:
        charge_user(order_data['user_id'], order_data['amount'])
    except PaymentError:
        # Compensation: annuler réservation
        cancel_product_reservation(product_reservation.id)
        return fail()
    
    # 3. Créer commande
    order = create_order(order_data)
    
    # 4. Envoyer notification
    send_notification(order.id)
    
    return order


# === SERVICE DISCOVERY ===

# Sans service discovery: URLs hardcodées
PRODUCT_SERVICE_URL = 'http://product-service:5002'

# Avec service discovery (Consul)
import consul

consul_client = consul.Consul(host='consul', port=8500)

def get_service_url(service_name):
    """Récupère l'URL d'un service via Consul"""
    index, services = consul_client.health.service(service_name, passing=True)
    if services:
        service = services[0]
        return f"http://{service['Service']['Address']}:{service['Service']['Port']}"
    raise ServiceNotFoundError(f"{service_name} not found")

# Utilisation
PRODUCT_SERVICE_URL = get_service_url('product-service')


# === CIRCUIT BREAKER PATTERN ===

# Installer pybreaker
pip install pybreaker

from pybreaker import CircuitBreaker

# Créer circuit breaker
product_service_breaker = CircuitBreaker(
    fail_max=5,           # Ouvrir après 5 échecs
    timeout_duration=60   # Réessayer après 60 secondes
)

@product_service_breaker
def call_product_service(product_id):
    """Appeler product service avec circuit breaker"""
    response = requests.get(
        f'{PRODUCT_SERVICE_URL}/api/products/{product_id}',
        timeout=5
    )
    response.raise_for_status()
    return response.json()

# Utilisation avec fallback
try:
    product = call_product_service(product_id)
except CircuitBreakerError:
    # Circuit ouvert - utiliser cache ou valeur par défaut
    product = get_product_from_cache(product_id)
except Exception as e:
    # Autre erreur
    return jsonify({'error': 'Service unavailable'}), 503


# === MONITORING & OBSERVABILITÉ ===

# DISTRIBUTED TRACING avec OpenTelemetry

# Installer OpenTelemetry
pip install opentelemetry-api opentelemetry-sdk
pip install opentelemetry-instrumentation-flask
pip install opentelemetry-instrumentation-requests
pip install opentelemetry-exporter-jaeger

# Configuration dans chaque service
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.jaeger.thrift import JaegerExporter
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor

def setup_tracing(app, service_name):
    """Configure distributed tracing"""
    
    # Provider
    trace.set_tracer_provider(TracerProvider())
    tracer = trace.get_tracer(__name__)
    
    # Jaeger exporter
    jaeger_exporter = JaegerExporter(
        agent_host_name='jaeger',
        agent_port=6831,
    )
    
    # Span processor
    span_processor = BatchSpanProcessor(jaeger_exporter)
    trace.get_tracer_provider().add_span_processor(span_processor)
    
    # Auto-instrumentation
    FlaskInstrumentor().instrument_app(app)
    RequestsInstrumentor().instrument()
    
    return tracer

# Usage dans routes
@orders_bp.route('', methods=['POST'])
def create_order():
    tracer = trace.get_tracer(__name__)
    
    with tracer.start_as_current_span("create_order") as span:
        span.set_attribute("user_id", data['user_id'])
        
        # Appels aux autres services sont automatiquement tracés
        product = requests.get(f'{PRODUCT_SERVICE_URL}/api/products/{id}')
        
        # ...


# CENTRALIZED LOGGING avec ELK Stack

import logging
import json
from pythonjsonlogger import jsonlogger

def setup_logging(app, service_name):
    """Configure structured logging"""
    
    logHandler = logging.StreamHandler()
    
    formatter = jsonlogger.JsonFormatter(
        '%(asctime)s %(name)s %(levelname)s %(message)s'
    )
    logHandler.setFormatter(formatter)
    
    app.logger.addHandler(logHandler)
    app.logger.setLevel(logging.INFO)
    
    # Ajouter service name à tous les logs
    old_factory = logging.getLogRecordFactory()
    
    def record_factory(*args, **kwargs):
        record = old_factory(*args, **kwargs)
        record.service = service_name
        return record
    
    logging.setLogRecordFactory(record_factory)

# Usage
app.logger.info('Order created', extra={
    'order_id': order.id,
    'user_id': order.user_id,
    'amount': float(order.total_amount)
})


# METRICS avec Prometheus

pip install prometheus-flask-exporter

from prometheus_flask_exporter import PrometheusMetrics

def setup_metrics(app):
    """Configure Prometheus metrics"""
    metrics = PrometheusMetrics(app)
    
    # Métriques custom
    order_counter = metrics.counter(
        'orders_created_total',
        'Total orders created',
        labels={'status': lambda: 'created'}
    )
    
    order_amount = metrics.histogram(
        'order_amount',
        'Order amount distribution'
    )
    
    return metrics

# Endpoint /metrics exposé automatiquement


# === DÉPLOIEMENT KUBERNETES ===

# infrastructure/kubernetes/auth-deployment.yml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: auth-service
  labels:
    app: auth-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: auth-service
  template:
    metadata:
      labels:
        app: auth-service
    spec:
      containers:
      - name: auth-service
        image: myapp/auth-service:latest
        ports:
        - containerPort: 5000
        env:
        - name: JWT_SECRET_KEY
          valueFrom:
            secretKeyRef:
              name: auth-secrets
              key: jwt-secret
        - name: REDIS_URL
          value: redis://redis:6379
        - name: USER_SERVICE_URL
          value: http://user-service:5001
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "200m"
        livenessProbe:
          httpGet:
            path: /health
            port: 5000
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: 5000
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: auth-service
spec:
  selector:
    app: auth-service
  ports:
  - protocol: TCP
    port: 5000
    targetPort: 5000
  type: ClusterIP


# infrastructure/kubernetes/ingress.yml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2
    nginx.ingress.kubernetes.io/rate-limit: "100"
spec:
  ingressClassName: nginx
  rules:
  - host: api.myapp.com
    http:
      paths:
      - path: /api/auth(/|$)(.*)
        pathType: Prefix
        backend:
          service:
            name: auth-service
            port:
              number: 5000
      - path: /api/users(/|$)(.*)
        pathType: Prefix
        backend:
          service:
            name: user-service
            port:
              number: 5001
      - path: /api/products(/|$)(.*)
        pathType: Prefix
        backend:
          service:
            name: product-service
            port:
              number: 5002
      - path: /api/orders(/|$)(.*)
        pathType: Prefix
        backend:
          service:
            name: order-service
            port:
              number: 5003


# infrastructure/kubernetes/hpa.yml (Horizontal Pod Autoscaler)

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: order-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order-service
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80


# Déployer sur Kubernetes
kubectl apply -f infrastructure/kubernetes/


# === MIGRATION MONOLITHE -> MICROSERVICES ===

# STRATÉGIE: Strangler Fig Pattern

┌─────────────────────────────────────────────────────────────────┐
│                        PHASE 1: État Initial                    │
└─────────────────────────────────────────────────────────────────┘

┌─────────────┐
│  Frontend   │
└──────┬──────┘
       │
       v
┌──────────────────────────────────────┐
│       Monolithe (Flask)              │
│  ┌───────┬────────┬─────────────┐   │
│  │ Auth  │Products│   Orders    │   │
│  └───────┴────────┴─────────────┘   │
└──────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│                  PHASE 2: Extraire Auth Service                 │
└─────────────────────────────────────────────────────────────────┘

┌─────────────┐
│  Frontend   │
└──────┬──────┘
       │
       v
┌──────────────┐
│ API Gateway  │
└───┬──────┬───┘
    │      │
    v      v
┌────────┐ ┌──────────────────────────┐
│  Auth  │ │      Monolithe           │
│Service │ │  ┌────────┬─────────┐    │
└────────┘ │  │Products│ Orders  │    │
           │  └────────┴─────────┘    │
           └──────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│              PHASE 3: Extraire Product Service                  │
└─────────────────────────────────────────────────────────────────┘

┌─────────────┐
│  Frontend   │
└──────┬──────┘
       │
       v
┌──────────────┐
│ API Gateway  │
└─┬────┬────┬──┘
  │    │    │
  v    v    v
┌────┐┌────┐┌──────────────┐
│Auth││Prod││  Monolithe   │
│Svc ││Svc ││  ┌────────┐  │
└────┘└────┘│  │ Orders │  │
            │  └────────┘  │
            └──────────────┘


┌─────────────────────────────────────────────────────────────────┐
│              PHASE 4: Extraire Order Service                    │
└─────────────────────────────────────────────────────────────────┘

┌─────────────┐
│  Frontend   │
└──────┬──────┘
       │
       v
┌──────────────┐
│ API Gateway  │
└─┬────┬────┬──┘
  │    │    │
  v    v    v
┌────┐┌────┐┌────┐
│Auth││Prod││Ord │
│Svc ││Svc ││Svc │
└────┘└────┘└────┘

# Monolithe complètement retiré [OK]


# ÉTAPES PRATIQUES DE MIGRATION

# 1. Identifier les domaines métier
- Authentication
- User management
- Product catalog
- Order management
- Payment
- Notification

# 2. Commencer par service sans dépendances
# Exemple: Auth service (lu par tous, écrit par lui-même)

# 3. Implémenter façade dans monolithe
# monolithe/app/routes/auth.py

@auth_bp.route('/login', methods=['POST'])
def login():
    # Rediriger vers nouveau service
    response = requests.post(
        'http://auth-service:5000/api/auth/login',
        json=request.get_json()
    )
    return response.json(), response.status_code

# 4. Déployer service en parallèle

# 5. Basculer progressivement le trafic
# - 10% vers nouveau service
# - 50% vers nouveau service
# - 100% vers nouveau service

# 6. Retirer code du monolithe

# 7. Répéter pour chaque service


[OK] GRAPHQL AU LIEU DE REST


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

GraphQL est un langage de requête pour APIs, développé par Facebook.
Contrairement à REST qui expose plusieurs endpoints, GraphQL expose
un seul endpoint où le client spécifie exactement les données dont il a besoin.


# REST vs GRAPHQL - COMPARAISON

┌─────────────────────────────────────────────────────────────────┐
│                           REST API                              │
└─────────────────────────────────────────────────────────────────┘

# Récupérer utilisateur + ses commandes + produits dans chaque commande

# Requête 1: GET /api/users/1
{
  "id": 1,
  "username": "john",
  "email": "john@example.com"
}

# Requête 2: GET /api/orders?user_id=1
{
  "orders": [
    {"id": 101, "total": 199.99, "status": "shipped"},
    {"id": 102, "total": 49.99, "status": "pending"}
  ]
}

# Requête 3: GET /api/products/5
{"id": 5, "name": "Laptop", "price": 1299.99}

# Requête 4: GET /api/products/8
{"id": 8, "name": "Mouse", "price": 29.99}

# [X] PROBLÈMES REST:
# - 4+ requêtes HTTP (N+1 problem)
# - Over-fetching: reçoit données inutiles
# - Under-fetching: données manquantes


┌─────────────────────────────────────────────────────────────────┐
│                         GRAPHQL API                             │
└─────────────────────────────────────────────────────────────────┘

# UNE SEULE requête: POST /graphql

query {
  user(id: 1) {
    username
    email
    orders {
      id
      total
      status
      items {
        product {
          name
          price
        }
        quantity
      }
    }
  }
}

# Réponse complète en une requête:
{
  "data": {
    "user": {
      "username": "john",
      "email": "john@example.com",
      "orders": [
        {
          "id": 101,
          "total": 199.99,
          "status": "shipped",
          "items": [
            {
              "product": {"name": "Laptop", "price": 1299.99},
              "quantity": 1
            }
          ]
        }
      ]
    }
  }
}

# [OK] AVANTAGES GRAPHQL:
# - UNE seule requête
# - Exactement les données demandées
# - Pas d'over-fetching ni under-fetching


# === AVANTAGES DE GRAPHQL ===

[OK] Requête flexible
  - Client demande exactement ce dont il a besoin
  - Pas de versioning d'API nécessaire

[OK] Performance
  - Une requête au lieu de plusieurs
  - Moins de bande passante

[OK] Typage fort
  - Schéma bien défini
  - Auto-documentation
  - Validation automatique

[OK] Introspection
  - API auto-documentée
  - Outils graphiques (GraphiQL, GraphQL Playground)

[OK] Évolution de l'API
  - Ajout de champs sans breaking changes
  - Dépréciation progressive

[OK] Developer Experience
  - Autocomplétion dans IDE
  - Outils de développement puissants


# === INCONVÉNIENTS DE GRAPHQL ===

[X] Complexité accrue
  - Courbe d'apprentissage plus élevée
  - Backend plus complexe à implémenter

[X] Caching difficile
  - REST utilise cache HTTP natif
  - GraphQL nécessite cache custom

[X] Rate limiting complexe
  - Difficulté à limiter selon coût des requêtes

[X] N+1 queries au backend
  - Nécessite DataLoader pour optimiser

[X] Overhead pour simples APIs
  - REST suffit pour APIs simples


# === IMPLÉMENTATION GRAPHQL AVEC FLASK ===

# Installation
pip install graphene flask-graphql

# backend/app/__init__.py

from flask import Flask
from flask_graphql import GraphQLView
from app.schema import schema

def create_app():
    app = Flask(__name__)
    
    # GraphQL endpoint
    app.add_url_rule(
        '/graphql',
        view_func=GraphQLView.as_view(
            'graphql',
            schema=schema,
            graphiql=True  # Interface graphique
        )
    )
    
    return app


# === SCHÉMA GRAPHQL ===

# backend/app/schema.py

import graphene
from graphene import relay
from graphene_sqlalchemy import SQLAlchemyObjectType, SQLAlchemyConnectionField
from app.models import User as UserModel, Product as ProductModel, Order as OrderModel

# Types GraphQL

class User(SQLAlchemyObjectType):
    class Meta:
        model = UserModel
        interfaces = (relay.Node,)

class Product(SQLAlchemyObjectType):
    class Meta:
        model = ProductModel
        interfaces = (relay.Node,)

class Order(SQLAlchemyObjectType):
    class Meta:
        model = OrderModel
        interfaces = (relay.Node,)


# Queries

class Query(graphene.ObjectType):
    node = relay.Node.Field()
    
    # Lister utilisateurs
    all_users = SQLAlchemyConnectionField(User.connection)
    
    # Utilisateur par ID
    user = graphene.Field(User, id=graphene.Int())
    
    # Lister produits
    all_products = SQLAlchemyConnectionField(Product.connection)
    
    # Produit par ID
    product = graphene.Field(Product, id=graphene.Int())
    
    # Rechercher produits
    search_products = graphene.List(
        Product,
        query=graphene.String(required=True)
    )
    
    # Commandes d'un utilisateur
    user_orders = graphene.List(
        Order,
        user_id=graphene.Int(required=True)
    )
    
    # Resolvers
    
    def resolve_user(self, info, id):
        return UserModel.query.get(id)
    
    def resolve_product(self, info, id):
        return ProductModel.query.get(id)
    
    def resolve_search_products(self, info, query):
        return ProductModel.query.filter(
            ProductModel.name.ilike(f'%{query}%')
        ).all()
    
    def resolve_user_orders(self, info, user_id):
        return OrderModel.query.filter_by(user_id=user_id).all()


# Mutations

class CreateUser(graphene.Mutation):
    class Arguments:
        username = graphene.String(required=True)
        email = graphene.String(required=True)
        password = graphene.String(required=True)
    
    user = graphene.Field(User)
    success = graphene.Boolean()
    errors = graphene.List(graphene.String)
    
    def mutate(self, info, username, email, password):
        # Validation
        if UserModel.query.filter_by(email=email).first():
            return CreateUser(
                user=None,
                success=False,
                errors=['Email already exists']
            )
        
        # Créer utilisateur
        password_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
        user = UserModel(
            username=username,
            email=email,
            password_hash=password_hash
        )
        
        db.session.add(user)
        db.session.commit()
        
        return CreateUser(
            user=user,
            success=True,
            errors=None
        )

class CreateProduct(graphene.Mutation):
    class Arguments:
        name = graphene.String(required=True)
        description = graphene.String()
        price = graphene.Float(required=True)
        stock = graphene.Int(required=True)
    
    product = graphene.Field(Product)
    success = graphene.Boolean()
    
    def mutate(self, info, name, price, stock, description=None):
        product = ProductModel(
            name=name,
            description=description,
            price=price,
            stock=stock
        )
        
        db.session.add(product)
        db.session.commit()
        
        return CreateProduct(product=product, success=True)

class UpdateProduct(graphene.Mutation):
    class Arguments:
        id = graphene.Int(required=True)
        name = graphene.String()
        description = graphene.String()
        price = graphene.Float()
        stock = graphene.Int()
    
    product = graphene.Field(Product)
    success = graphene.Boolean()
    
    def mutate(self, info, id, **kwargs):
        product = ProductModel.query.get(id)
        
        if not product:
            return UpdateProduct(product=None, success=False)
        
        for key, value in kwargs.items():
            if value is not None:
                setattr(product, key, value)
        
        db.session.commit()
        
        return UpdateProduct(product=product, success=True)

class CreateOrder(graphene.Mutation):
    class Arguments:
        user_id = graphene.Int(required=True)
        items = graphene.List(graphene.NonNull(graphene.InputObjectType(
            'OrderItemInput',
            (
                ('product_id', graphene.Int(required=True)),
                ('quantity', graphene.Int(required=True))
            )
        )))
    
    order = graphene.Field(Order)
    success = graphene.Boolean()
    errors = graphene.List(graphene.String)
    
    def mutate(self, info, user_id, items):
        # Vérifier utilisateur
        user = UserModel.query.get(user_id)
        if not user:
            return CreateOrder(
                order=None,
                success=False,
                errors=['User not found']
            )
        
        # Calculer total et vérifier stock
        total_amount = 0
        order_items = []
        errors = []
        
        for item_data in items:
            product = ProductModel.query.get(item_data.product_id)
            
            if not product:
                errors.append(f'Product {item_data.product_id} not found')
                continue
            
            if product.stock < item_data.quantity:
                errors.append(f'Insufficient stock for {product.name}')
                continue
            
            subtotal = product.price * item_data.quantity
            total_amount += subtotal
            
            order_items.append({
                'product': product,
                'quantity': item_data.quantity,
                'unit_price': product.price
            })
        
        if errors:
            return CreateOrder(order=None, success=False, errors=errors)
        
        # Créer commande
        order = OrderModel(
            user_id=user_id,
            total_amount=total_amount
        )
        
        db.session.add(order)
        db.session.flush()
        
        # Créer items et mettre à jour stock
        for item_data in order_items:
            order_item = OrderItemModel(
                order_id=order.id,
                product_id=item_data['product'].id,
                quantity=item_data['quantity'],
                unit_price=item_data['unit_price']
            )
            db.session.add(order_item)
            
            # Réduire stock
            item_data['product'].stock -= item_data['quantity']
        
        db.session.commit()
        
        return CreateOrder(order=order, success=True, errors=None)


class Mutation(graphene.ObjectType):
    create_user = CreateUser.Field()
    create_product = CreateProduct.Field()
    update_product = UpdateProduct.Field()
    create_order = CreateOrder.Field()


# Schéma principal
schema = graphene.Schema(query=Query, mutation=Mutation)


# === REQUÊTES GRAPHQL (CLIENT) ===

# QUERIES

# 1. Récupérer tous les utilisateurs
query {
  allUsers {
    edges {
      node {
        id
        username
        email
        createdAt
      }
    }
  }
}

# 2. Récupérer utilisateur avec ses commandes
query {
  user(id: 1) {
    username
    email
    orders {
      id
      totalAmount
      status
      createdAt
      items {
        product {
          name
          price
        }
        quantity
      }
    }
  }
}

# 3. Rechercher produits
query {
  searchProducts(query: "laptop") {
    id
    name
    description
    price
    stock
  }
}

# 4. Requête avec variables
query GetUser($userId: Int!) {
  user(id: $userId) {
    username
    email
  }
}

# Variables:
{
  "userId": 1
}

# 5. Fragments (réutilisables)
fragment UserInfo on User {
  id
  username
  email
  createdAt
}

query {
  user(id: 1) {
    ...UserInfo
    orders {
      id
      totalAmount
    }
  }
}


# MUTATIONS

# 1. Créer utilisateur
mutation {
  createUser(
    username: "newuser"
    email: "new@example.com"
    password: "securepass123"
  ) {
    user {
      id
      username
      email
    }
    success
    errors
  }
}

# 2. Créer produit
mutation {
  createProduct(
    name: "Gaming Mouse"
    description: "RGB gaming mouse"
    price: 59.99
    stock: 100
  ) {
    product {
      id
      name
      price
    }
    success
  }
}

# 3. Mettre à jour produit
mutation {
  updateProduct(
    id: 5
    price: 49.99
    stock: 150
  ) {
    product {
      id
      name
      price
      stock
    }
    success
  }
}

# 4. Créer commande
mutation {
  createOrder(
    userId: 1
    items: [
      {productId: 5, quantity: 2},
      {productId: 8, quantity: 1}
    ]
  ) {
    order {
      id
      totalAmount
      status
      items {
        product {
          name
        }
        quantity
      }
    }
    success
    errors
  }
}


# === FRONTEND AVEC GRAPHQL ===

# Installation Apollo Client (React)
npm install @apollo/client graphql

# frontend/src/apolloClient.js

import { ApolloClient, InMemoryCache, HttpLink, from } from '@apollo/client';
import { onError } from '@apollo/client/link/error';
import { setContext } from '@apollo/client/link/context';

// Error handling
const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path }) => {
      console.error(`[GraphQL error]: Message: ${message}, Path: ${path}`);
    });
  }
  
  if (networkError) {
    console.error(`[Network error]: ${networkError}`);
  }
});

// Auth link
const authLink = setContext((_, { headers }) => {
  const token = localStorage.getItem('access_token');
  
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : '',
    }
  };
});

// HTTP link
const httpLink = new HttpLink({
  uri: import.meta.env.VITE_GRAPHQL_URL || 'http://localhost:5000/graphql',
});

// Client
const client = new ApolloClient({
  link: from([errorLink, authLink, httpLink]),
  cache: new InMemoryCache(),
});

export default client;


# frontend/src/main.jsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import { ApolloProvider } from '@apollo/client';
import client from './apolloClient';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
  </React.StrictMode>
);


# frontend/src/graphql/queries.js

import { gql } from '@apollo/client';

export const GET_PRODUCTS = gql`
  query GetProducts {
    allProducts {
      edges {
        node {
          id
          name
          description
          price
          stock
        }
      }
    }
  }
`;

export const GET_PRODUCT = gql`
  query GetProduct($id: Int!) {
    product(id: $id) {
      id
      name
      description
      price
      stock
      createdAt
    }
  }
`;

export const SEARCH_PRODUCTS = gql`
  query SearchProducts($query: String!) {
    searchProducts(query: $query) {
      id
      name
      description
      price
      stock
    }
  }
`;

export const GET_USER_WITH_ORDERS = gql`
  query GetUserWithOrders($id: Int!) {
    user(id: $id) {
      id
      username
      email
      orders {
        id
        totalAmount
        status
        createdAt
        items {
          product {
            name
            price
          }
          quantity
        }
      }
    }
  }
`;


# frontend/src/graphql/mutations.js

import { gql } from '@apollo/client';

export const CREATE_USER = gql`
  mutation CreateUser($username: String!, $email: String!, $password: String!) {
    createUser(username: $username, email: $email, password: $password) {
      user {
        id
        username
        email
      }
      success
      errors
    }
  }
`;

export const CREATE_PRODUCT = gql`
  mutation CreateProduct(
    $name: String!
    $description: String
    $price: Float!
    $stock: Int!
  ) {
    createProduct(
      name: $name
      description: $description
      price: $price
      stock: $stock
    ) {
      product {
        id
        name
        price
        stock
      }
      success
    }
  }
`;

export const UPDATE_PRODUCT = gql`
  mutation UpdateProduct(
    $id: Int!
    $name: String
    $price: Float
    $stock: Int
  ) {
    updateProduct(id: $id, name: $name, price: $price, stock: $stock) {
      product {
        id
        name
        price
        stock
      }
      success
    }
  }
`;

export const CREATE_ORDER = gql`
  mutation CreateOrder($userId: Int!, $items: [OrderItemInput!]!) {
    createOrder(userId: $userId, items: $items) {
      order {
        id
        totalAmount
        status
        items {
          product {
            name
            price
          }
          quantity
        }
      }
      success
      errors
    }
  }
`;


# === COMPOSANTS REACT AVEC APOLLO ===

// frontend/src/components/Products/ProductList.jsx

import React, { useState } from 'react';
import { useQuery } from '@apollo/client';
import { GET_PRODUCTS, SEARCH_PRODUCTS } from '../../graphql/queries';
import { Link } from 'react-router-dom';

const ProductList = () => {
  const [searchQuery, setSearchQuery] = useState('');
  const [isSearching, setIsSearching] = useState(false);
  
  // Query principale
  const { loading, error, data, refetch } = useQuery(GET_PRODUCTS);
  
  // Query de recherche (lazy)
  const { 
    loading: searchLoading, 
    data: searchData,
    refetch: searchRefetch 
  } = useQuery(SEARCH_PRODUCTS, {
    variables: { query: searchQuery },
    skip: !isSearching, // Ne lance pas automatiquement
  });
  
  const handleSearch = async (e) => {
    e.preventDefault();
    if (searchQuery.trim()) {
      setIsSearching(true);
      await searchRefetch();
    } else {
      setIsSearching(false);
      refetch();
    }
  };
  
  if (loading || searchLoading) return <div>Chargement...</div>;
  if (error) return <div>Erreur: {error.message}</div>;
  
  const products = isSearching 
    ? searchData?.searchProducts || []
    : data?.allProducts?.edges.map(edge => edge.node) || [];
  
  return (
    <div className="product-list">
      <h2>Produits</h2>
      
      <form onSubmit={handleSearch} className="search-form">
        <input
          type="text"
          placeholder="Rechercher..."
          value={searchQuery}
          onChange={(e) => setSearchQuery(e.target.value)}
        />
        <button type="submit">Rechercher</button>
        {isSearching && (
          <button 
            type="button" 
            onClick={() => {
              setIsSearching(false);
              setSearchQuery('');
              refetch();
            }}
          >
            Réinitialiser
          </button>
        )}
      </form>
      
      <div className="product-grid">
        {products.length === 0 ? (
          <p>Aucun produit trouvé</p>
        ) : (
          products.map(product => (
            <div key={product.id} className="product-card">
              <h3>{product.name}</h3>
              <p className="description">{product.description}</p>
              <p className="price">{product.price} €</p>
              <p className="stock">Stock: {product.stock}</p>
              <Link to={`/products/${product.id}`}>Voir détails</Link>
            </div>
          ))
        )}
      </div>
    </div>
  );
};

export default ProductList;


// frontend/src/components/Products/ProductForm.jsx

import React, { useState } from 'react';
import { useMutation } from '@apollo/client';
import { CREATE_PRODUCT } from '../../graphql/mutations';
import { GET_PRODUCTS } from '../../graphql/queries';
import { useNavigate } from 'react-router-dom';

const ProductForm = () => {
  const navigate = useNavigate();
  const [formData, setFormData] = useState({
    name: '',
    description: '',
    price: '',
    stock: ''
  });
  
  const [createProduct, { loading, error }] = useMutation(CREATE_PRODUCT, {
    // Mettre à jour le cache après création
    refetchQueries: [{ query: GET_PRODUCTS }],
    // Ou update manuel:
    // update(cache, { data: { createProduct } }) {
    //   const existingProducts = cache.readQuery({ query: GET_PRODUCTS });
    //   cache.writeQuery({
    //     query: GET_PRODUCTS,
    //     data: {
    //       allProducts: {
    //         edges: [
    //           ...existingProducts.allProducts.edges,
    //           { node: createProduct.product }
    //         ]
    //       }
    //     }
    //   });
    // }
  });
  
  const handleChange = (e) => {
    setFormData({
      ...formData,
      [e.target.name]: e.target.value
    });
  };
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    try {
      const { data } = await createProduct({
        variables: {
          name: formData.name,
          description: formData.description,
          price: parseFloat(formData.price),
          stock: parseInt(formData.stock)
        }
      });
      
      if (data.createProduct.success) {
        navigate('/products');
      }
    } catch (err) {
      console.error('Erreur création produit:', err);
    }
  };
  
  return (
    <div className="product-form">
      <h2>Nouveau produit</h2>
      
      {error && <div className="error">{error.message}</div>}
      
      <form onSubmit={handleSubmit}>
        <div className="form-group">
          <label htmlFor="name">Nom</label>
          <input
            type="text"
            id="name"
            name="name"
            value={formData.name}
            onChange={handleChange}
            required
          />
        </div>
        
        <div className="form-group">
          <label htmlFor="description">Description</label>
          <textarea
            id="description"
            name="description"
            value={formData.description}
            onChange={handleChange}
            rows="4"
          />
        </div>
        
        <div className="form-group">
          <label htmlFor="price">Prix</label>
          <input
            type="number"
            id="price"
            name="price"
            step="0.01"
            min="0"
            value={formData.price}
            onChange={handleChange}
            required
          />
        </div>
        
        <div className="form-group">
          <label htmlFor="stock">Stock</label>
          <input
            type="number"
            id="stock"
            name="stock"
            min="0"
            value={formData.stock}
            onChange={handleChange}
            required
          />
        </div>
        
        <button type="submit" disabled={loading}>
          {loading ? 'Création...' : 'Créer'}
        </button>
      </form>
    </div>
  );
};

export default ProductForm;


// frontend/src/components/Orders/CreateOrder.jsx

import React, { useState } from 'react';
import { useMutation, useQuery } from '@apollo/client';
import { CREATE_ORDER } from '../../graphql/mutations';
import { GET_PRODUCTS } from '../../graphql/queries';
import { useAuth } from '../../context/AuthContext';

const CreateOrder = () => {
  const { user } = useAuth();
  const [cart, setCart] = useState([]);
  
  const { loading: productsLoading, data: productsData } = useQuery(GET_PRODUCTS);
  
  const [createOrder, { loading: orderLoading }] = useMutation(CREATE_ORDER, {
    onCompleted: (data) => {
      if (data.createOrder.success) {
        alert('Commande créée avec succès!');
        setCart([]);
      } else {
        alert(`Erreurs: ${data.createOrder.errors.join(', ')}`);
      }
    }
  });
  
  const addToCart = (product) => {
    const existing = cart.find(item => item.productId === product.id);
    
    if (existing) {
      setCart(cart.map(item => 
        item.productId === product.id 
          ? { ...item, quantity: item.quantity + 1 }
          : item
      ));
    } else {
      setCart([...cart, { 
        productId: product.id, 
        name: product.name,
        price: product.price,
        quantity: 1 
      }]);
    }
  };
  
  const removeFromCart = (productId) => {
    setCart(cart.filter(item => item.productId !== productId));
  };
  
  const updateQuantity = (productId, quantity) => {
    if (quantity <= 0) {
      removeFromCart(productId);
    } else {
      setCart(cart.map(item =>
        item.productId === productId ? { ...item, quantity } : item
      ));
    }
  };
  
  const calculateTotal = () => {
    return cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
  };
  
  const handleCheckout = async () => {
    if (cart.length === 0) {
      alert('Panier vide');
      return;
    }
    
    const items = cart.map(item => ({
      productId: item.productId,
      quantity: item.quantity
    }));
    
    try {
      await createOrder({
        variables: {
          userId: user.id,
          items
        }
      });
    } catch (err) {
      console.error('Erreur commande:', err);
    }
  };
  
  if (productsLoading) return <div>Chargement...</div>;
  
  const products = productsData?.allProducts?.edges.map(e => e.node) || [];
  
  return (
    <div className="create-order">
      <div className="products-section">
        <h2>Produits disponibles</h2>
        <div className="product-grid">
          {products.map(product => (
            <div key={product.id} className="product-card">
              <h3>{product.name}</h3>
              <p className="price">{product.price} €</p>
              <p>Stock: {product.stock}</p>
              <button 
                onClick={() => addToCart(product)}
                disabled={product.stock === 0}
              >
                Ajouter au panier
              </button>
            </div>
          ))}
        </div>
      </div>
      
      <div className="cart-section">
        <h2>Panier</h2>
        
        {cart.length === 0 ? (
          <p>Panier vide</p>
        ) : (
          <>
            <table className="cart-table">
              <thead>
                <tr>
                  <th>Produit</th>
                  <th>Prix</th>
                  <th>Quantité</th>
                  <th>Sous-total</th>
                  <th></th>
                </tr>
              </thead>
              <tbody>
                {cart.map(item => (
                  <tr key={item.productId}>
                    <td>{item.name}</td>
                    <td>{item.price} €</td>
                    <td>
                      <input
                        type="number"
                        min="1"
                        value={item.quantity}
                        onChange={(e) => 
                          updateQuantity(item.productId, parseInt(e.target.value))
                        }
                      />
                    </td>
                    <td>{(item.price * item.quantity).toFixed(2)} €</td>
                    <td>
                      <button onClick={() => removeFromCart(item.productId)}>
                        Retirer
                      </button>
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
            
            <div className="cart-total">
              <h3>Total: {calculateTotal().toFixed(2)} €</h3>
            </div>
            
            <button 
              className="checkout-btn"
              onClick={handleCheckout}
              disabled={orderLoading}
            >
              {orderLoading ? 'Commande en cours...' : 'Commander'}
            </button>
          </>
        )}
      </div>
    </div>
  );
};

export default CreateOrder;


# === OPTIMISATIONS GRAPHQL ===

# DATALOADER (résout N+1 queries)

pip install aiodataloader

# backend/app/dataloader.py

from aiodataloader import DataLoader
from app.models import Product, User

class ProductLoader(DataLoader):
    async def batch_load_fn(self, product_ids):
        """Charge plusieurs produits en une seule requête"""
        products = Product.query.filter(Product.id.in_(product_ids)).all()
        
        # Créer un dictionnaire pour mapper IDs -> produits
        product_map = {p.id: p for p in products}
        
        # Retourner dans l'ordre des IDs demandés
        return [product_map.get(pid) for pid in product_ids]

class UserLoader(DataLoader):
    async def batch_load_fn(self, user_ids):
        """Charge plusieurs utilisateurs en une seule requête"""
        users = User.query.filter(User.id.in_(user_ids)).all()
        user_map = {u.id: u for u in users}
        return [user_map.get(uid) for uid in user_ids]

# Utilisation dans resolvers
def resolve_order(self, info, id):
    order = Order.query.get(id)
    
    # Au lieu de:
    # user = User.query.get(order.user_id)  # Requête N+1
    
    # Utiliser DataLoader:
    user = info.context['user_loader'].load(order.user_id)
    
    return order


# PAGINATION CURSOR-BASED

class Query(graphene.ObjectType):
    products = relay.ConnectionField(
        Product.connection,
        first=graphene.Int(),
        after=graphene.String()
    )
    
    def resolve_products(self, info, first=10, after=None):
        query = Product.query.order_by(Product.id)
        
        if after:
            # Décoder le cursor
            cursor_id = int(base64.b64decode(after).decode())
            query = query.filter(Product.id > cursor_id)
        
        products = query.limit(first + 1).all()
        
        has_next = len(products) > first
        if has_next:
            products = products[:first]
        
        edges = [
            {
                'node': product,
                'cursor': base64.b64encode(str(product.id).encode()).decode()
            }
            for product in products
        ]
        
        return {
            'edges': edges,
            'pageInfo': {
                'hasNextPage': has_next,
                'endCursor': edges[-1]['cursor'] if edges else None
            }
        }


# QUERY COMPLEXITY ANALYSIS (limiter requêtes coûteuses)

from graphql import GraphQLError

def validate_query_complexity(query, max_complexity=100):
    """Valide la complexité d'une requête"""
    complexity = calculate_complexity(query)
    
    if complexity > max_complexity:
        raise GraphQLError(
            f'Query too complex: {complexity} exceeds max {max_complexity}'
        )

def calculate_complexity(query):
    """Calcule la complexité d'une requête"""
    # Implémentation simplifiée
    # Chaque champ = 1 point
    # Chaque relation = +10 points
    # etc.
    pass


# CACHING avec Redis

from functools import wraps
import json
import hashlib

def graphql_cache(ttl=300):
    """Cache les résultats GraphQL"""
    def decorator(resolver):
        @wraps(resolver)
        def wrapper(self, info, **kwargs):
            # Créer clé de cache basée sur query + args
            cache_key = f"graphql:{resolver.__name__}:{hashlib.md5(json.dumps(kwargs, sort_keys=True).encode()).hexdigest()}"
            
            # Vérifier cache
            cached = redis_client.get(cache_key)
            if cached:
                return json.loads(cached)
            
            # Exécuter resolver
            result = resolver(self, info, **kwargs)
            
            # Mettre en cache
            redis_client.setex(cache_key, ttl, json.dumps(result))
            
            return result
        
        return wrapper
    return decorator

# Utilisation
@graphql_cache(ttl=600)
def resolve_products(self, info):
    return Product.query.all()


# === SUBSCRIPTIONS (Temps réel avec WebSockets) ===

pip install graphene-subscriptions

# backend/app/schema.py

import graphene
from graphene_subscriptions import Subscription

class OrderSubscription(Subscription):
    class Arguments:
        user_id = graphene.Int(required=True)
    
    order_created = graphene.Field(Order)
    order_updated = graphene.Field(Order)
    
    def subscribe(root, info, user_id):
        # S'abonner aux événements pour cet utilisateur
        return info.context['pubsub'].subscribe(f'orders.{user_id}')
    
    def publish(payload, info, user_id):
        # Publier l'événement
        return payload

class Subscription(graphene.ObjectType):
    order_subscription = OrderSubscription.Field()

# Schéma avec subscriptions
schema = graphene.Schema(
    query=Query,
    mutation=Mutation,
    subscription=Subscription
)


# Frontend - S'abonner aux mises à jour

import { useSubscription, gql } from '@apollo/client';

const ORDER_SUBSCRIPTION = gql`
  subscription OnOrderUpdate($userId: Int!) {
    orderSubscription(userId: $userId) {
      orderCreated {
        id
        totalAmount
        status
      }
      orderUpdated {
        id
        status
      }
    }
  }
`;

const OrderNotifications = ({ userId }) => {
  const { data, loading } = useSubscription(ORDER_SUBSCRIPTION, {
    variables: { userId }
  });
  
  if (data?.orderSubscription?.orderCreated) {
    const order = data.orderSubscription.orderCreated;
    showNotification(`Nouvelle commande #${order.id} créée!`);
  }
  
  if (data?.orderSubscription?.orderUpdated) {
    const order = data.orderSubscription.orderUpdated;
    showNotification(`Commande #${order.id} mise à jour: ${order.status}`);
  }
  
  return null;
};


# === REST vs GRAPHQL - QUAND UTILISER QUOI ? ===

┌─────────────────────────────────────────────────────────────────┐
│                     UTILISER REST SI:                            │
└─────────────────────────────────────────────────────────────────┘

[OK] API simple avec peu d'entités
[OK] Besoins de caching HTTP importants
[OK] Endpoints bien définis et stables
[OK] Équipe peu familière avec GraphQL
[OK] Upload de fichiers (REST plus simple)
[OK] API publique avec documentation standard
[OK] Besoin de RESTful maturity (HATEOAS)


┌─────────────────────────────────────────────────────────────────┐
│                    UTILISER GRAPHQL SI:                          │
└─────────────────────────────────────────────────────────────────┘

[OK] Relations complexes entre entités
[OK] Besoins variables selon clients (web, mobile, IoT)
[OK] Évolution fréquente de l'API
[OK] Over-fetching/under-fetching problématiques
[OK] Frontend nécessite flexibilité dans requêtes
[OK] Équipe confortable avec typage fort
[OK] Besoin de temps réel (subscriptions)
[OK] API interne où contrôle sur clients


# === APPROCHE HYBRIDE (REST + GRAPHQL) ===

# Possibilité d'exposer les deux APIs simultanément

from flask import Flask
from flask_graphql import GraphQLView
from app.schema import schema
from app.routes import rest_blueprints

app = Flask(__name__)

# GraphQL endpoint
app.add_url_rule(
    '/graphql',
    view_func=GraphQLView.as_view('graphql', schema=schema, graphiql=True)
)

# REST endpoints
for blueprint in rest_blueprints:
    app.register_blueprint(blueprint)

# Avantages:
# - Clients existants continuent avec REST
# - Nouveaux clients utilisent GraphQL
# - Migration progressive


# === OUTILS GRAPHQL ===

# 1. GRAPHIQL - Interface de test GraphQL
# Accessible à: http://localhost:5000/graphql
# - Autocomplétion
# - Documentation interactive
# - Historique des requêtes

# 2. APOLLO STUDIO
# https://studio.apollographql.com/
# - Monitoring de performances
# - Schema registry
# - Analytics d'usage

# 3. GRAPHQL VOYAGER
# Visualisation du schéma GraphQL
# https://apis.guru/graphql-voyager/

# 4. GRAPHQL CODE GENERATOR
# Génère types TypeScript depuis schéma
npm install -D @graphql-codegen/cli

# 5. ALTAIR GraphQL Client
# Alternative à GraphiQL plus moderne


# === SÉCURITÉ GRAPHQL ===

# 1. AUTHENTIFICATION

from flask_jwt_extended import jwt_required, get_jwt_identity

class Query(graphene.ObjectType):
    user = graphene.Field(User, id=graphene.Int())
    
    @jwt_required()
    def resolve_user(self, info, id):
        current_user_id = get_jwt_identity()
        
        # Utilisateur peut voir seulement son profil
        if id != current_user_id:
            raise GraphQLError('Non autorisé')
        
        return User.query.get(id)


# 2. RATE LIMITING par complexité

from flask_limiter import Limiter

limiter = Limiter(app, key_func=lambda: get_jwt_identity())

@limiter.limit("100 per hour")
@app.route('/graphql', methods=['POST'])
def graphql_view():
    # Vérifier complexité
    complexity = calculate_query_complexity(request.json['query'])
    
    if complexity > 100:
        return jsonify({'error': 'Query too complex'}), 400
    
    # Exécuter query
    return GraphQLView.as_view('graphql', schema=schema)()


# 3. VALIDATION DES INPUTS

class CreateUserInput(graphene.InputObjectType):
    username = graphene.String(required=True)
    email = graphene.String(required=True)
    password = graphene.String(required=True)
    
    @staticmethod
    def validate(data):
        if len(data.password) < 8:
            raise GraphQLError('Password trop court')
        
        if '@' not in data.email:
            raise GraphQLError('Email invalide')


# 4. DEPTH LIMITING (limiter profondeur des requêtes)

from graphql import GraphQLError

def validate_query_depth(query_ast, max_depth=5):
    """Empêche requêtes trop profondes"""
    
    def check_depth(node, current_depth=0):
        if current_depth > max_depth:
            raise GraphQLError(f'Query depth {current_depth} exceeds max {max_depth}')
        
        for field in node.selection_set.selections:
            if hasattr(field, 'selection_set'):
                check_depth(field, current_depth + 1)
    
    check_depth(query_ast)


# === TESTS GRAPHQL ===

# backend/tests/test_graphql.py

import pytest
from app import create_app
from app.schema import schema

@pytest.fixture
def client():
    app = create_app('testing')
    with app.test_client() as client:
        yield client

def test_query_all_products(client):
    """Test requête liste produits"""
    query = '''
        query {
            allProducts {
                edges {
                    node {
                        id
                        name
                        price
                    }
                }
            }
        }
    '''
    
    response = client.post('/graphql', json={'query': query})
    data = response.get_json()
    
    assert response.status_code == 200
    assert 'data' in data
    assert 'allProducts' in data['data']

def test_mutation_create_product(client):
    """Test création produit"""
    mutation = '''
        mutation {
            createProduct(
                name: "Test Product"
                price: 99.99
                stock: 10
            ) {
                product {
                    id
                    name
                    price
                }
                success
            }
        }
    '''
    
    response = client.post('/graphql', json={'query': mutation})
    data = response.get_json()
    
    assert response.status_code == 200
    assert data['data']['createProduct']['success'] == True
    assert data['data']['createProduct']['product']['name'] == 'Test Product'

def test_query_with_variables(client):
    """Test requête avec variables"""
    query = '''
        query GetProduct($id: Int!) {
            product(id: $id) {
                id
                name
                price
            }
        }
    '''
    
    variables = {'id': 1}
    
    response = client.post('/graphql', json={
        'query': query,
        'variables': variables
    })
    
    data = response.get_json()
    assert response.status_code == 200
    assert 'product' in data['data']


# === MIGRATION REST -> GRAPHQL ===

# STRATÉGIE: Wrapper GraphQL autour de REST existant

class Query(graphene.ObjectType):
    products = graphene.List(Product)
    
    def resolve_products(self, info):
        # Appeler API REST existante
        response = requests.get('http://rest-api/api/products')
        products_data = response.json()
        
        # Convertir en objets GraphQL
        return [Product(**p) for p in products_data]

# Permet:
# - Exposer GraphQL sans réécrire backend
# - Migration progressive
# - Garder API REST existante


# === RÉSUMÉ: REST vs GRAPHQL ===

┌─────────────────┬────────────────────────┬────────────────────────┐
│   CRITÈRE       │         REST           │       GRAPHQL          │
├─────────────────┼────────────────────────┼────────────────────────┤
│ Endpoints       │ Multiples (/users,     │ Un seul (/graphql)     │
│                 │ /products, etc.)       │                        │
├─────────────────┼────────────────────────┼────────────────────────┤
│ Requêtes        │ Structure fixe         │ Client définit         │
├─────────────────┼────────────────────────┼────────────────────────┤
│ Over-fetching   │ Fréquent               │ Aucun                  │
├─────────────────┼────────────────────────┼────────────────────────┤
│ Under-fetching  │ Nécessite plusieurs    │ Une requête suffit     │
│                 │ requêtes               │                        │
├─────────────────┼────────────────────────┼────────────────────────┤
│ Versioning      │ /v1, /v2, etc.         │ Pas nécessaire         │
├─────────────────┼────────────────────────┼────────────────────────┤
│ Caching         │ HTTP natif (simple)    │ Custom (complexe)      │
├─────────────────┼────────────────────────┼────────────────────────┤
│ Typage          │ Optionnel (OpenAPI)    │ Fort et obligatoire    │
├─────────────────┼────────────────────────┼────────────────────────┤
│ Documentation   │ Manuelle (Swagger)     │ Auto-générée           │
├─────────────────┼────────────────────────┼────────────────────────┤
│ Courbe          │ Facile                 │ Moyenne                │
│ d'apprentissage │                        │                        │
├─────────────────┼────────────────────────┼────────────────────────┤
│ Temps réel      │ WebSockets séparés     │ Subscriptions natives  │
├─────────────────┼────────────────────────┼────────────────────────┤
│ Upload fichiers │ Simple (multipart)     │ Complexe               │
├─────────────────┼────────────────────────┼────────────────────────┤
│ Maturité        │ Très mature            │ Mature mais plus       │
│                 │                        │ récent                 │
└─────────────────┴────────────────────────┴────────────────────────┘


[OK] CONCLUSION

# MICROSERVICES
- Architecture pour applications complexes et grandes équipes
- Chaque service indépendant avec sa propre DB
- Communication via REST/gRPC/Message Queues
- Nécessite orchestration (Kubernetes)
- Monitoring distribué crucial
- Commencer monolithique, migrer progressivement

# GRAPHQL
- Alternative moderne à REST
- Flexibilité côté client
- Une requête pour données complexes
- Typage fort et auto-documentation
- Meilleur pour relations complexes
- REST reste pertinent pour cas simples

# CHOIX ARCHITECTURAL
Dépend de:
- Taille de l'équipe
- Complexité de l'application
- Besoins de scalabilité
- Expertise technique
- Budget infrastructure

# Recommandation générale:
- Petite application (<5 devs): Monolithe + REST
- Application moyenne (5-20 devs): Monolithe + GraphQL
- Grande application (20+ devs): Microservices + GraphQL/REST
- Application avec besoins temps réel: GraphQL avec subscriptions
- API publique simple: REST avec bonne documentation


# === RESSOURCES COMPLÉMENTAIRES ===

# Microservices
- Livre: "Building Microservices" par Sam Newman
- Patterns: https://microservices.io/patterns/
- Service mesh: Istio, Linkerd
- API Gateway: Kong, Tyk, AWS API Gateway

# GraphQL
- Spécification: https://spec.graphql.org/
- Tutoriel: https://graphql.org/learn/
- Apollo: https://www.apollographql.com/docs/
- Best practices: https://graphql.org/learn/best-practices/

# Outils recommandés
- Docker & Kubernetes pour orchestration
- Prometheus & Grafana pour monitoring
- Jaeger pour distributed tracing
- ELK Stack pour logging centralisé
- Redis pour caching
- RabbitMQ/Kafka pour messaging


# === CHECKLIST ARCHITECTURE MICROSERVICES ===

[WHITE_SQUARE] CONCEPTION
  [WHITE_SQUARE] Domaines métier bien identifiés
  [WHITE_SQUARE] Boundaries entre services claires
  [WHITE_SQUARE] Stratégie de communication définie (sync/async)
  [WHITE_SQUARE] Gestion des transactions distribuées
  [WHITE_SQUARE] Stratégie de données (DB par service ou partagée)

[WHITE_SQUARE] INFRASTRUCTURE
  [WHITE_SQUARE] Container orchestration (Kubernetes)
  [WHITE_SQUARE] Service discovery configuré
  [WHITE_SQUARE] API Gateway déployé
  [WHITE_SQUARE] Message queue setup
  [WHITE_SQUARE] Load balancers configurés

[WHITE_SQUARE] OBSERVABILITÉ
  [WHITE_SQUARE] Distributed tracing implémenté
  [WHITE_SQUARE] Centralized logging configuré
  [WHITE_SQUARE] Metrics collection (Prometheus)
  [WHITE_SQUARE] Alerting configuré
  [WHITE_SQUARE] Dashboards créés (Grafana)

[WHITE_SQUARE] SÉCURITÉ
  [WHITE_SQUARE] Authentication centralisée
  [WHITE_SQUARE] Authorization par service
  [WHITE_SQUARE] Secrets management
  [WHITE_SQUARE] Network policies (Kubernetes)
  [WHITE_SQUARE] Rate limiting

[WHITE_SQUARE] RÉSILIENCE
  [WHITE_SQUARE] Health checks sur tous services
  [WHITE_SQUARE] Circuit breakers implémentés
  [WHITE_SQUARE] Retry logic avec backoff
  [WHITE_SQUARE] Timeouts configurés
  [WHITE_SQUARE] Graceful shutdown

[WHITE_SQUARE] CI/CD
  [WHITE_SQUARE] Pipeline par service
  [WHITE_SQUARE] Tests automatisés
  [WHITE_SQUARE] Build d'images Docker
  [WHITE_SQUARE] Deployment automatisé
  [WHITE_SQUARE] Rollback strategy


# === CHECKLIST GRAPHQL ===

[WHITE_SQUARE] SCHÉMA
  [WHITE_SQUARE] Types bien définis
  [WHITE_SQUARE] Relations entre types claires
  [WHITE_SQUARE] Input types pour mutations
  [WHITE_SQUARE] Descriptions sur tous champs
  [WHITE_SQUARE] Dépréciation des champs obsolètes

[WHITE_SQUARE] PERFORMANCE
  [WHITE_SQUARE] DataLoader implémenté (N+1)
  [WHITE_SQUARE] Pagination cursor-based
  [WHITE_SQUARE] Query complexity analysis
  [WHITE_SQUARE] Depth limiting
  [WHITE_SQUARE] Caching Redis

[WHITE_SQUARE] SÉCURITÉ
  [WHITE_SQUARE] Authentication sur queries sensibles
  [WHITE_SQUARE] Authorization par champ
  [WHITE_SQUARE] Rate limiting
  [WHITE_SQUARE] Input validation
  [WHITE_SQUARE] Query whitelisting (production)

[WHITE_SQUARE] DÉVELOPPEMENT
  [WHITE_SQUARE] GraphiQL ou Playground activé
  [WHITE_SQUARE] Tests unitaires des resolvers
  [WHITE_SQUARE] Tests d'intégration
  [WHITE_SQUARE] Documentation à jour
  [WHITE_SQUARE] TypeScript types générés (frontend)

[WHITE_SQUARE] PRODUCTION
  [WHITE_SQUARE] Monitoring des performances
  [WHITE_SQUARE] Logging des erreurs
  [WHITE_SQUARE] APM (Application Performance Monitoring)
  [WHITE_SQUARE] Cache configuration optimale
  [WHITE_SQUARE] CDN pour queries statiques


# === PATTERNS AVANCÉS ===


# PATTERN: BACKEND FOR FRONTEND (BFF)

┌──────────┐  ┌──────────┐  ┌──────────┐
│ Web App  │  │ Mobile   │  │  Admin   │
└────┬─────┘  └────┬─────┘  └────┬─────┘
     │             │              │
     v             v              v
┌────────────┐ ┌──────────┐ ┌──────────┐
│  Web BFF   │ │Mobile BFF│ │Admin BFF │
│ (GraphQL)  │ │ (REST)   │ │(GraphQL) │
└─────┬──────┘ └─────┬────┘ └─────┬────┘
      │              │             │
      └──────────────┼─────────────┘
                     v
           ┌─────────────────────┐
           │  Microservices      │
           │  (Auth, Products,   │
           │   Orders, Users)    │
           └─────────────────────┘

# Chaque frontend a son propre BFF adapté à ses besoins
# - Web BFF: GraphQL avec toutes les données
# - Mobile BFF: REST optimisé pour mobile (moins de données)
# - Admin BFF: GraphQL avec permissions admin


# PATTERN: API COMPOSITION

# Composer plusieurs microservices dans un seul resolver GraphQL

class Query(graphene.ObjectType):
    enriched_order = graphene.Field(EnrichedOrder, id=graphene.Int())
    
    async def resolve_enriched_order(self, info, id):
        # 1. Récupérer commande
        order_response = await async_get(f'{ORDER_SERVICE}/orders/{id}')
        order = order_response.json()
        
        # 2. Récupérer utilisateur (en parallèle)
        user_task = async_get(f'{USER_SERVICE}/users/{order["user_id"]}')
        
        # 3. Récupérer produits (en parallèle)
        product_ids = [item['product_id'] for item in order['items']]
        product_tasks = [
            async_get(f'{PRODUCT_SERVICE}/products/{pid}')
            for pid in product_ids
        ]
        
        # Attendre toutes les requêtes
        user_response, *product_responses = await asyncio.gather(
            user_task, *product_tasks
        )
        
        # Composer le résultat
        return {
            'order': order,
            'user': user_response.json(),
            'products': [p.json() for p in product_responses]
        }


# PATTERN: GRAPHQL FEDERATION (Apollo)

# Chaque microservice expose son propre schéma GraphQL
# Gateway fédère tous les schémas

# Auth Service
type User @key(fields: "id") {
  id: ID!
  username: String!
  email: String!
}

# Order Service
type Order {
  id: ID!
  user: User!  # Référence vers User
  total: Float!
}

extend type User @key(fields: "id") {
  id: ID! @external
  orders: [Order!]!
}

# Gateway combine automatiquement les schémas


# PATTERN: SAGA CHOREOGRAPHY

# Chaque service émet des événements, autres services réagissent

# 1. Order Service crée commande -> émet "OrderCreated"
publish_event('order.created', {
    'order_id': 123,
    'user_id': 1,
    'items': [...]
})

# 2. Payment Service consomme "OrderCreated" -> traite paiement
def on_order_created(event):
    result = process_payment(event['order_id'])
    if result.success:
        publish_event('payment.succeeded', {...})
    else:
        publish_event('payment.failed', {...})

# 3. Inventory Service consomme "PaymentSucceeded" -> réserve stock
def on_payment_succeeded(event):
    reserve_items(event['order_id'])
    publish_event('inventory.reserved', {...})

# 4. Shipping Service consomme "InventoryReserved" -> crée expédition
def on_inventory_reserved(event):
    create_shipment(event['order_id'])
    publish_event('shipment.created', {...})

# 5. Notification Service consomme tout -> envoie emails
def on_event(event):
    if event['type'] == 'order.created':
        send_order_confirmation()
    elif event['type'] == 'shipment.created':
        send_shipping_notification()


# PATTERN: CQRS (Command Query Responsibility Segregation)

# Séparer les opérations de lecture et d'écriture

# Write Model (Commands)
class OrderCommandService:
    def create_order(self, user_id, items):
        # Logique métier complexe
        order = Order(...)
        db.session.add(order)
        db.session.commit()
        
        # Publier événement
        publish_event('order.created', order.to_dict())
        
        return order

# Read Model (Queries) - Base optimisée pour lectures
class OrderQueryService:
    def get_order(self, order_id):
        # Lecture depuis DB dénormalisée/cache
        return redis.get(f'order:{order_id}')
    
    def get_user_orders(self, user_id):
        # Vue matérialisée optimisée
        return OrderReadModel.query.filter_by(user_id=user_id).all()

# Event Handler maintient Read Model à jour
def on_order_created(event):
    # Mettre à jour Read Model
    redis.set(f'order:{event["id"]}', json.dumps(event))
    OrderReadModel.create_from_event(event)


# PATTERN: EVENT SOURCING

# Stocker tous les changements comme événements

class OrderEventStore:
    def save_event(self, aggregate_id, event_type, event_data):
        event = Event(
            aggregate_id=aggregate_id,
            aggregate_type='Order',
            event_type=event_type,
            event_data=event_data,
            version=self.get_next_version(aggregate_id),
            timestamp=datetime.utcnow()
        )
        db.session.add(event)
        db.session.commit()
    
    def get_events(self, aggregate_id):
        return Event.query.filter_by(aggregate_id=aggregate_id).order_by(Event.version).all()
    
    def rebuild_state(self, aggregate_id):
        events = self.get_events(aggregate_id)
        state = {}
        
        for event in events:
            state = self.apply_event(state, event)
        
        return state
    
    def apply_event(self, state, event):
        if event.event_type == 'OrderCreated':
            return event.event_data
        
        elif event.event_type == 'OrderPaid':
            state['payment_status'] = 'paid'
            state['paid_at'] = event.event_data['paid_at']
        
        elif event.event_type == 'OrderShipped':
            state['status'] = 'shipped'
            state['tracking_number'] = event.event_data['tracking_number']
        
        return state

# Utilisation
store = OrderEventStore()

# Créer commande
store.save_event(123, 'OrderCreated', {
    'user_id': 1,
    'items': [...],
    'total': 199.99
})

# Payer commande
store.save_event(123, 'OrderPaid', {
    'paid_at': '2024-01-15T10:30:00',
    'payment_method': 'card'
})

# Reconstruire état actuel
current_state = store.rebuild_state(123)


# === PERFORMANCE BENCHMARKS ===

# REST vs GraphQL - Tests de performance typiques

┌─────────────────────────────────────────────────────────────────┐
│                    SCÉNARIO: Liste simple                        │
│              GET /api/products vs GraphQL query                  │
└─────────────────────────────────────────────────────────────────┘

REST:
- Requête: 1
- Latence moyenne: 50ms
- Données transférées: 15KB
- Over-fetching: 30%

GraphQL:
- Requête: 1
- Latence moyenne: 55ms (+10%)
- Données transférées: 10KB (-33%)
- Over-fetching: 0%

Verdict: Performance similaire, GraphQL plus efficient en bande passante


┌─────────────────────────────────────────────────────────────────┐
│              SCÉNARIO: Données avec relations                    │
│     User + Orders + Products (3 niveaux d'imbrication)          │
└─────────────────────────────────────────────────────────────────┘

REST:
- Requêtes: 15+ (N+1 problem)
- Latence totale: 750ms
- Données transférées: 45KB
- Over-fetching: 50%

GraphQL (sans DataLoader):
- Requêtes: 15+ (même N+1)
- Latence totale: 800ms (+7%)
- Données transférées: 25KB (-44%)
- Over-fetching: 0%

GraphQL (avec DataLoader):
- Requêtes: 3 (batched)
- Latence totale: 120ms (-84%)
- Données transférées: 25KB
- Over-fetching: 0%

Verdict: GraphQL avec DataLoader nettement supérieur


┌─────────────────────────────────────────────────────────────────┐
│                 SCÉNARIO: Application mobile                     │
│              Connexion lente (3G, 500ms latence)                 │
└─────────────────────────────────────────────────────────────────┘

REST:
- 10 requêtes nécessaires
- Temps total: ~10 secondes
- Expérience utilisateur: Très mauvaise

GraphQL:
- 1 requête
- Temps total: ~1.5 secondes
- Expérience utilisateur: Acceptable

Verdict: GraphQL critique pour mobile


# === COÛTS D'INFRASTRUCTURE ===

# Exemple: Application avec 1M utilisateurs actifs/mois

┌─────────────────────────────────────────────────────────────────┐
│                    ARCHITECTURE MONOLITHIQUE                     │
└─────────────────────────────────────────────────────────────────┘

- Serveurs: 3 instances (load balanced)
- Base de données: 1 instance (réplica pour lecture)
- Cache: 1 Redis instance
- CDN: CloudFlare

Coût mensuel estimé: $500-800


┌─────────────────────────────────────────────────────────────────┐
│                   ARCHITECTURE MICROSERVICES                     │
└─────────────────────────────────────────────────────────────────┘

- Kubernetes cluster: 5-10 nodes
- Services: 5 services × 3 replicas = 15 pods
- Bases de données: 5 instances (une par service)
- Cache: 2 Redis clusters
- Message Queue: RabbitMQ cluster (3 nodes)
- API Gateway: 2 instances
- Monitoring: Prometheus, Grafana, Jaeger
- CDN: CloudFlare

Coût mensuel estimé: $2000-3500

Augmentation: 4-5x plus cher que monolithe

Justifié si:
- Croissance rapide prévue
- Équipes multiples
- Besoin de scalabilité indépendante
- Déploiements fréquents


# === EXEMPLES RÉELS D'ARCHITECTURES ===

# NETFLIX
- 700+ microservices
- API Gateway (Zuul)
- Service discovery (Eureka)
- Circuit breaker (Hystrix)
- Chaos engineering (Chaos Monkey)

# UBER
- 2000+ microservices
- Microservice per city initialement
- Migration vers domaines fonctionnels
- GraphQL pour mobile apps

# AIRBNB
- Monolithe Ruby initial
- Migration progressive vers microservices
- GraphQL pour unifier APIs
- Service mesh avec Istio

# GITHUB
- Monolithe Rails initial
- API REST publique
- Migration partielle vers GraphQL
- GraphQL API v4 lancée en 2016

# SHOPIFY
- Monolithe Rails + microservices
- GraphQL Storefront API
- REST Admin API maintenue
- Approche hybride pragmatique


# === ERREURS COURANTES À ÉVITER ===

# MICROSERVICES

[X] Commencer avec microservices (trop tôt)
[OK] Commencer monolithe, extraire quand nécessaire

[X] Microservices trop petits (fonction par service)
[OK] Services alignés sur domaines métier

[X] Base de données partagée entre services
[OK] Base de données par service (ou domaine)

[X] Communication synchrone partout
[OK] Async quand possible, sync quand nécessaire

[X] Pas de monitoring distribué
[OK] Tracing, logging centralisé obligatoires

[X] Pas de tests end-to-end
[OK] Tests d'intégration entre services essentiels


# GRAPHQL

[X] Exposer directement la base de données
[OK] Layer de service avec logique métier

[X] Pas de DataLoader
[OK] Toujours implémenter DataLoader

[X] Requêtes sans limite
[OK] Depth limiting, complexity analysis

[X] Pas de pagination
[OK] Cursor-based pagination systématique

[X] Mutations sans validation
[OK] Validation stricte des inputs

[X] Pas de caching
[OK] Cache Redis pour requêtes coûteuses


# === CODE COMPLET D'UN SERVICE MICROSERVICE ===

# Structure complète d'un service production-ready

myservice/
├── app/
│   ├── __init__.py
│   ├── config.py           # Configuration
│   ├── models/             # Modèles de données
│   │   ├── __init__.py
│   │   └── order.py
│   ├── repositories/       # Accès données
│   │   ├── __init__.py
│   │   └── order_repository.py
│   ├── services/           # Logique métier
│   │   ├── __init__.py
│   │   └── order_service.py
│   ├── routes/             # API endpoints
│   │   ├── __init__.py
│   │   └── orders.py
│   ├── events/             # Event handlers
│   │   ├── __init__.py
│   │   ├── publisher.py
│   │   └── consumer.py
│   ├── schemas/            # Validation schemas
│   │   ├── __init__.py
│   │   └── order_schema.py
│   ├── utils/              # Utilitaires
│   │   ├── __init__.py
│   │   ├── auth.py
│   │   ├── logging.py
│   │   └── metrics.py
│   └── middleware/         # Middlewares
│       ├── __init__.py
│       ├── auth_middleware.py
│       └── logging_middleware.py
├── tests/
│   ├── unit/
│   ├── integration/
│   └── e2e/
├── migrations/             # DB migrations
├── docs/                   # Documentation
├── kubernetes/             # K8s manifests
│   ├── deployment.yaml
│   ├── service.yaml
│   └── ingress.yaml
├── .github/
│   └── workflows/
│       └── ci-cd.yaml
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── setup.py
└── README.md


# === FIN DU GUIDE ===

Ce guide couvre:

[OK] Architecture Microservices
  - Concepts et patterns
  - Implémentation complète avec Flask
  - Communication inter-services
  - Event-driven architecture
  - Service discovery
  - API Gateway
  - Monitoring et observabilité
  - Déploiement Kubernetes

[OK] GraphQL
  - Concepts et différences avec REST
  - Implémentation complète avec Graphene
  - Queries, Mutations, Subscriptions
  - Frontend avec Apollo Client
  - Optimisations (DataLoader, caching)
  - Sécurité et rate limiting
  - Tests GraphQL

[OK] Comparaisons et Choix
  - Quand utiliser microservices vs monolithe
  - Quand utiliser GraphQL vs REST
  - Coûts et complexité
  - Migration strategies
  - Exemples réels

[OK] Patterns Avancés
  - BFF (Backend for Frontend)
  - API Composition
  - CQRS et Event Sourcing
  - Saga Pattern
  - Circuit Breaker

[OK] Production Ready
  - Monitoring distribué
  - Logging centralisé
  - Sécurité
  - Tests
  - CI/CD
  - Observabilité

Pour approfondir:
- Pratiquer avec des projets réels
- Étudier les architectures de grandes entreprises
- Lire "Building Microservices" de Sam Newman
- Explorer Kubernetes en profondeur
- Maîtriser les patterns distribués
- Contribuer à des projets open source

Bonne chance dans vos architectures! [RAPIDE]