from django.conf import settings from django.core.cache import cache from django.db.models import Q from .models import FeatureFlag, FeatureFlagOverride from .registry import FeatureRegistry class FeatureService: """Evaluates feature flags with caching and multi-tenant support.""" def is_enabled( self, slug: str, *, user=None, tenant_id: str | None = None, default: bool | None = None, ttl_seconds: int = 60, ) -> bool: config = getattr(settings, "INFRASYNTH_FEATURES", {}) ttl_seconds = ttl_seconds or config.get("CACHE_TTL_SECONDS", 60) if user and user.is_authenticated: override = self._get_user_override(slug, user) if override is not None: return override if user and user.is_authenticated: override = self._get_group_override(slug, user) if override is not None: return override if tenant_id: flag = self._get_flag(slug, tenant_id, ttl_seconds) else: flag = self._get_flag_global(slug, ttl_seconds) if flag: return flag.is_active registry_default = FeatureRegistry.get_all().get(slug) if registry_default: return registry_default.default return default if default is not None else False def get_active_flags(self, *, user=None, tenant_id: str | None = None) -> dict[str, bool]: all_slugs = set(FeatureRegistry.get_all().keys()) db_flags = set( FeatureFlag.objects.filter(Q(tenant_id=tenant_id) | Q(tenant_id__isnull=True)).values_list( "slug", flat=True ) ) all_slugs.update(db_flags) return {slug: self.is_enabled(slug, user=user, tenant_id=tenant_id) for slug in sorted(all_slugs)} def _get_user_override(self, slug: str, user) -> bool | None: override = FeatureFlagOverride.objects.filter(flag__slug=slug, user=user).first() if override: return override.is_enabled return None def _get_group_override(self, slug: str, user) -> bool | None: groups = list(user.groups.all()) if not groups: return None override = FeatureFlagOverride.objects.filter(flag__slug=slug, group__in=groups).first() if override: return override.is_enabled return None def _get_flag(self, slug: str, tenant_id: str, ttl_seconds: int) -> FeatureFlag | None: cache_key = f"features:{slug}:{tenant_id}" cached = cache.get(cache_key) if cached is not None: return cached if isinstance(cached, FeatureFlag) else None flag = FeatureFlag.objects.filter(slug=slug, tenant_id=tenant_id).first() cache.set(cache_key, flag, ttl_seconds) return flag def _get_flag_global(self, slug: str, ttl_seconds: int) -> FeatureFlag | None: cache_key = f"features:{slug}:global" cached = cache.get(cache_key) if cached is not None: return cached if isinstance(cached, FeatureFlag) else None flag = FeatureFlag.objects.filter(slug=slug, tenant_id__isnull=True).first() cache.set(cache_key, flag, ttl_seconds) return flag