Files
ALwrity/backend/alwrity_utils/feature_profiles.py
ajaysi cd06c6aaa8 Consolidate feature flags to ALWRITY_ENABLED_FEATURES
Backend:
- Add get_enabled_features() returning set from ALWRITY_ENABLED_FEATURES
- Update router registry to use 'features' instead of 'profiles'
- Support feature names: podcast, blog-writer, youtube, story-writer, etc
- Update bootstrap gating to use enabled features
- Update PODCAST_ONLY_DEMO_MODE to check new flag first
- Add backwards compatibility with legacy env vars

Frontend:
- Update demoMode.ts to use REACT_APP_ENABLED_FEATURES
- Add getEnabledFeatures() and isFeatureEnabled() utilities

Usage:
  ALWRITY_ENABLED_FEATURES=all          # All features (default)
  ALWRITY_ENABLED_FEATURES=podcast      # Podcast only
  ALWRITY_ENABLED_FEATURES=podcast,core # Podcast + core features
2026-03-31 18:40:54 +05:30

89 lines
2.9 KiB
Python

"""Feature profile parsing and expansion logic."""
from __future__ import annotations
import os
from dataclasses import dataclass
from typing import Iterable, Tuple
from .feature_registry import FEATURE_GROUPS, PROFILE_GROUP_MAP
# Consolidated env var - supports both old and new format
ENV_FEATURE_PROFILE = "ALWRITY_ENABLED_FEATURES"
ENV_FEATURE_PROFILE_LEGACY = "ALWRITY_FEATURE_TO_ENABLE"
DEFAULT_PROFILE = "all"
@dataclass(frozen=True)
class ExpandedFeatureProfile:
"""Expanded profile data used by runtime helpers."""
profiles: Tuple[str, ...]
groups: Tuple[str, ...]
class UnknownFeatureProfileError(ValueError):
"""Raised when ALWRITY_ENABLED_FEATURES contains unknown profile values."""
def _get_env_value() -> str:
"""Get the feature profile value from environment - new var takes precedence."""
return os.getenv(ENV_FEATURE_PROFILE) or os.getenv(ENV_FEATURE_PROFILE_LEGACY) or DEFAULT_PROFILE
def _normalize_values(raw_value: str | None) -> Tuple[str, ...]:
if not raw_value or not raw_value.strip():
return (DEFAULT_PROFILE,)
normalized = tuple(
value.strip().lower()
for value in raw_value.split(",")
if value.strip()
)
return normalized or (DEFAULT_PROFILE,)
def parse_feature_profiles(raw_value: str | None = None) -> Tuple[str, ...]:
"""Parse and validate profile names from env/raw input.
Supports comma-separated profile names, e.g. `core,podcast`.
Raises UnknownFeatureProfileError when any profile is not registered.
"""
selected_profiles = _normalize_values(raw_value if raw_value is not None else _get_env_value())
unknown = sorted({profile for profile in selected_profiles if profile not in PROFILE_GROUP_MAP and profile not in FEATURE_GROUPS})
if unknown:
supported = ", ".join(sorted(set(PROFILE_GROUP_MAP.keys()) | set(FEATURE_GROUPS.keys())))
unknown_display = ", ".join(unknown)
raise UnknownFeatureProfileError(
f"Unknown {ENV_FEATURE_PROFILE} value(s): {unknown_display}. Supported profiles: {supported}."
)
return selected_profiles
def _dedupe_stable(items: Iterable[str]) -> Tuple[str, ...]:
return tuple(dict.fromkeys(items))
def expand_profiles(profiles: Tuple[str, ...]) -> ExpandedFeatureProfile:
"""Expand profile names into a deduplicated group list."""
# Handle "all" specially - include all groups
if "all" in profiles:
return ExpandedFeatureProfile(profiles=("all",), groups=tuple(FEATURE_GROUPS.keys()))
# Otherwise expand via PROFILE_GROUP_MAP
groups = _dedupe_stable(
group
for profile in profiles
for group in PROFILE_GROUP_MAP.get(profile, (profile,))
)
# Include FEATURE_GROUPS keys directly
all_groups = _dedupe_stable(list(groups) + [g for g in groups if g in FEATURE_GROUPS])
return ExpandedFeatureProfile(profiles=profiles, groups=all_groups)