""" Système de Feature Flags pour Migration Progressive (JOUR 1-2) Ce module implémente un système de feature flags robust pour permettre l'activation/désactivation contrôlée des nouvelles fonctionnalités pendant la migration vers l'architecture refactorisée. Architecture: - Enum typé pour toutes les feature flags - Configuration centralisée avec validation - Support pour rollback instantané - Logging automatique des changements d'état Utilisé pour la migration progressive selon MIGRATION_PROGRESSIVE.md """ import os from enum import Enum from typing import Dict, Any, Optional from dataclasses import dataclass from datetime import datetime import logging logger = logging.getLogger(__name__) class FeatureFlag(Enum): """ Énumération de tous les feature flags disponibles. Conventions de nommage: - USE_NEW_ pour les migrations de services - ENABLE_ pour les nouvelles fonctionnalités """ # === MIGRATION PROGRESSIVE SERVICES === # JOUR 3-4: Migration Services Core USE_STRATEGY_PATTERN = "use_strategy_pattern" USE_REFACTORED_ASSESSMENT = "use_refactored_assessment" # JOUR 5-6: Services Avancés USE_NEW_STUDENT_SCORE_CALCULATOR = "use_new_student_score_calculator" USE_NEW_ASSESSMENT_STATISTICS_SERVICE = "use_new_assessment_statistics_service" # === FONCTIONNALITÉS AVANCÉES === # Performance et monitoring ENABLE_PERFORMANCE_MONITORING = "enable_performance_monitoring" ENABLE_QUERY_OPTIMIZATION = "enable_query_optimization" # Interface utilisateur ENABLE_BULK_OPERATIONS = "enable_bulk_operations" ENABLE_ADVANCED_FILTERS = "enable_advanced_filters" @dataclass class FeatureFlagConfig: """Configuration d'un feature flag avec métadonnées.""" enabled: bool description: str migration_day: Optional[int] = None # Jour de migration selon le plan (1-7) rollback_safe: bool = True # Peut être désactivé sans risque created_at: datetime = None updated_at: datetime = None def __post_init__(self): if self.created_at is None: self.created_at = datetime.utcnow() if self.updated_at is None: self.updated_at = datetime.utcnow() class FeatureFlagManager: """ Gestionnaire centralisé des feature flags. Fonctionnalités: - Configuration via variables d'environnement - Fallback vers configuration par défaut - Logging des changements d'état - Validation des flags - Support pour tests unitaires """ def __init__(self): self._flags: Dict[FeatureFlag, FeatureFlagConfig] = {} self._initialize_defaults() self._load_from_environment() def _initialize_defaults(self) -> None: """Initialise la configuration par défaut des feature flags.""" # Configuration par défaut - TOUT DÉSACTIVÉ pour sécurité maximale default_configs = { # MIGRATION PROGRESSIVE - JOUR 3-4 FeatureFlag.USE_STRATEGY_PATTERN: FeatureFlagConfig( enabled=False, description="Utilise les nouvelles stratégies de notation (Pattern Strategy)", migration_day=3, rollback_safe=True ), FeatureFlag.USE_REFACTORED_ASSESSMENT: FeatureFlagConfig( enabled=False, description="Utilise le nouveau service de calcul de progression", migration_day=4, rollback_safe=True ), # MIGRATION PROGRESSIVE - JOUR 5-6 FeatureFlag.USE_NEW_STUDENT_SCORE_CALCULATOR: FeatureFlagConfig( enabled=False, description="Utilise le nouveau calculateur de scores étudiants", migration_day=5, rollback_safe=True ), FeatureFlag.USE_NEW_ASSESSMENT_STATISTICS_SERVICE: FeatureFlagConfig( enabled=False, description="Utilise le nouveau service de statistiques d'évaluation", migration_day=6, rollback_safe=True ), # FONCTIONNALITÉS AVANCÉES FeatureFlag.ENABLE_PERFORMANCE_MONITORING: FeatureFlagConfig( enabled=False, description="Active le monitoring des performances", rollback_safe=True ), FeatureFlag.ENABLE_QUERY_OPTIMIZATION: FeatureFlagConfig( enabled=False, description="Active les optimisations de requêtes", rollback_safe=True ), FeatureFlag.ENABLE_BULK_OPERATIONS: FeatureFlagConfig( enabled=False, description="Active les opérations en masse", rollback_safe=True ), FeatureFlag.ENABLE_ADVANCED_FILTERS: FeatureFlagConfig( enabled=False, description="Active les filtres avancés", rollback_safe=True ), } self._flags.update(default_configs) logger.info("Feature flags initialisés avec configuration par défaut") def _load_from_environment(self) -> None: """Charge la configuration depuis les variables d'environnement.""" for flag in FeatureFlag: env_var = f"FEATURE_FLAG_{flag.value.upper()}" env_value = os.environ.get(env_var) if env_value is not None: # Parse boolean depuis l'environnement enabled = env_value.lower() in ('true', '1', 'yes', 'on', 'enabled') if flag in self._flags: old_state = self._flags[flag].enabled self._flags[flag].enabled = enabled self._flags[flag].updated_at = datetime.utcnow() if old_state != enabled: logger.info( f"Feature flag {flag.value} modifié par env: {old_state} -> {enabled}", extra={ 'event_type': 'feature_flag_changed', 'flag_name': flag.value, 'old_value': old_state, 'new_value': enabled, 'source': 'environment' } ) def is_enabled(self, flag: FeatureFlag) -> bool: """ Vérifie si un feature flag est activé. Args: flag: Le feature flag à vérifier Returns: bool: True si le flag est activé, False sinon """ if flag not in self._flags: logger.warning( f"Feature flag inconnu: {flag.value}. Retour False par défaut.", extra={'event_type': 'unknown_feature_flag', 'flag_name': flag.value} ) return False return self._flags[flag].enabled def enable(self, flag: FeatureFlag, reason: str = "") -> bool: """ Active un feature flag. Args: flag: Le feature flag à activer reason: Raison de l'activation (pour logs) Returns: bool: True si l'activation a réussi """ if flag not in self._flags: logger.error(f"Impossible d'activer un feature flag inconnu: {flag.value}") return False old_state = self._flags[flag].enabled self._flags[flag].enabled = True self._flags[flag].updated_at = datetime.utcnow() logger.info( f"Feature flag {flag.value} activé. Raison: {reason}", extra={ 'event_type': 'feature_flag_enabled', 'flag_name': flag.value, 'old_value': old_state, 'new_value': True, 'reason': reason, 'migration_day': self._flags[flag].migration_day } ) return True def disable(self, flag: FeatureFlag, reason: str = "") -> bool: """ Désactive un feature flag. Args: flag: Le feature flag à désactiver reason: Raison de la désactivation (pour logs) Returns: bool: True si la désactivation a réussi """ if flag not in self._flags: logger.error(f"Impossible de désactiver un feature flag inconnu: {flag.value}") return False if not self._flags[flag].rollback_safe: logger.warning( f"Désactivation d'un flag non-rollback-safe: {flag.value}", extra={'event_type': 'unsafe_rollback_attempt', 'flag_name': flag.value} ) old_state = self._flags[flag].enabled self._flags[flag].enabled = False self._flags[flag].updated_at = datetime.utcnow() logger.info( f"Feature flag {flag.value} désactivé. Raison: {reason}", extra={ 'event_type': 'feature_flag_disabled', 'flag_name': flag.value, 'old_value': old_state, 'new_value': False, 'reason': reason, 'rollback_safe': self._flags[flag].rollback_safe } ) return True def get_config(self, flag: FeatureFlag) -> Optional[FeatureFlagConfig]: """Récupère la configuration complète d'un feature flag.""" return self._flags.get(flag) def get_status_summary(self) -> Dict[str, Any]: """ Retourne un résumé de l'état de tous les feature flags. Returns: Dict contenant le statut de chaque flag avec métadonnées """ summary = { 'flags': {}, 'migration_status': { 'day_3_ready': False, 'day_4_ready': False, 'day_5_ready': False, 'day_6_ready': False }, 'total_enabled': 0, 'last_updated': None } latest_update = None enabled_count = 0 for flag, config in self._flags.items(): summary['flags'][flag.value] = { 'enabled': config.enabled, 'description': config.description, 'migration_day': config.migration_day, 'rollback_safe': config.rollback_safe, 'updated_at': config.updated_at.isoformat() if config.updated_at else None } if config.enabled: enabled_count += 1 if latest_update is None or (config.updated_at and config.updated_at > latest_update): latest_update = config.updated_at # Calcul du statut de migration par jour day_3_flags = [FeatureFlag.USE_STRATEGY_PATTERN] day_4_flags = [FeatureFlag.USE_REFACTORED_ASSESSMENT] day_5_flags = [FeatureFlag.USE_NEW_STUDENT_SCORE_CALCULATOR] day_6_flags = [FeatureFlag.USE_NEW_ASSESSMENT_STATISTICS_SERVICE] summary['migration_status']['day_3_ready'] = all(self.is_enabled(flag) for flag in day_3_flags) summary['migration_status']['day_4_ready'] = all(self.is_enabled(flag) for flag in day_4_flags) summary['migration_status']['day_5_ready'] = all(self.is_enabled(flag) for flag in day_5_flags) summary['migration_status']['day_6_ready'] = all(self.is_enabled(flag) for flag in day_6_flags) summary['total_enabled'] = enabled_count summary['last_updated'] = latest_update.isoformat() if latest_update else None return summary def enable_migration_day(self, day: int, reason: str = "") -> Dict[str, bool]: """ Active tous les feature flags pour un jour de migration donné. Args: day: Numéro du jour de migration (3-6) reason: Raison de l'activation Returns: Dict[flag_name, success] indiquant quels flags ont été activés """ day_flags_map = { 3: [FeatureFlag.USE_STRATEGY_PATTERN], 4: [FeatureFlag.USE_REFACTORED_ASSESSMENT], 5: [FeatureFlag.USE_NEW_STUDENT_SCORE_CALCULATOR], 6: [FeatureFlag.USE_NEW_ASSESSMENT_STATISTICS_SERVICE] } if day not in day_flags_map: logger.error(f"Jour de migration invalide: {day}. Jours supportés: 3-6") return {} results = {} migration_reason = f"Migration Jour {day}: {reason}" if reason else f"Migration Jour {day}" for flag in day_flags_map[day]: success = self.enable(flag, migration_reason) results[flag.value] = success logger.info( f"Activation des flags pour le jour {day} terminée", extra={ 'event_type': 'migration_day_activation', 'migration_day': day, 'results': results, 'reason': reason } ) return results # Instance globale du gestionnaire de feature flags feature_flags = FeatureFlagManager() def is_feature_enabled(flag: FeatureFlag) -> bool: """ Fonction utilitaire pour vérifier l'état d'un feature flag. Usage dans le code: from config.feature_flags import is_feature_enabled, FeatureFlag if is_feature_enabled(FeatureFlag.USE_NEW_GRADING_STRATEGIES): # Utiliser la nouvelle implémentation result = new_grading_service.calculate() else: # Utiliser l'ancienne implémentation result = old_grading_method() """ return feature_flags.is_enabled(flag)