infrasynth-backend-kit/infrasynth/features/services.py
2026-08-28 14:38:47 -05:00

89 lines
3.2 KiB
Python

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