Arch changes and feature flags

This commit is contained in:
James Pattinson
2025-11-23 15:46:51 +00:00
parent 6f1d09cd77
commit e1659c07ea
22 changed files with 577 additions and 119 deletions

View File

@@ -1,5 +1,5 @@
from fastapi import APIRouter
from . import auth, users, tiers, memberships, payments, email, email_templates, events
from . import auth, users, tiers, memberships, payments, email, email_templates, events, feature_flags
api_router = APIRouter()
@@ -11,3 +11,4 @@ api_router.include_router(payments.router, prefix="/payments", tags=["payments"]
api_router.include_router(email.router, prefix="/email", tags=["email"])
api_router.include_router(email_templates.router, prefix="/email-templates", tags=["email-templates"])
api_router.include_router(events.router, prefix="/events", tags=["events"])
api_router.include_router(feature_flags.router, prefix="/feature-flags", tags=["feature-flags"])

View File

@@ -0,0 +1,47 @@
from fastapi import APIRouter, Depends
from typing import Dict, Any
from app.services.feature_flag_service import feature_flags
from app.schemas.feature_flags import FeatureFlagsResponse, FeatureFlagResponse
router = APIRouter()
@router.get("/flags", response_model=FeatureFlagsResponse)
async def get_all_feature_flags() -> FeatureFlagsResponse:
"""
Get all feature flags for the frontend
This endpoint is public as it only returns feature configuration
"""
all_flags = feature_flags.get_all_flags()
enabled_flags = feature_flags.get_enabled_flags()
return FeatureFlagsResponse(
flags=all_flags,
enabled_flags=enabled_flags
)
@router.get("/flags/{flag_name}", response_model=FeatureFlagResponse)
async def get_feature_flag(flag_name: str) -> FeatureFlagResponse:
"""
Get a specific feature flag value
"""
flag_name_upper = flag_name.upper()
enabled = feature_flags.is_enabled(flag_name_upper)
value = feature_flags.get_flag_value(flag_name_upper)
return FeatureFlagResponse(
name=flag_name_upper,
enabled=enabled,
value=value
)
@router.post("/flags/reload")
async def reload_feature_flags():
"""
Reload feature flags from environment variables
This could be protected with admin permissions in production
"""
feature_flags.reload_flags()
return {"message": "Feature flags reloaded successfully"}

View File

@@ -0,0 +1,15 @@
from pydantic import BaseModel
from typing import Dict, Any, List
class FeatureFlagsResponse(BaseModel):
"""Response model for feature flags"""
flags: Dict[str, Any]
enabled_flags: List[str]
class FeatureFlagResponse(BaseModel):
"""Response model for a single feature flag"""
name: str
enabled: bool
value: Any

View File

@@ -0,0 +1,80 @@
"""
Feature Flag Service for managing application features
"""
import os
from typing import Dict, List, Any
from app.core.config import settings
class FeatureFlagService:
"""Service for managing feature flags"""
def __init__(self):
"""Initialize feature flags from environment variables"""
self._flags = self._load_flags_from_env()
def _load_flags_from_env(self) -> Dict[str, Any]:
"""Load feature flags from environment variables"""
# Get the FEATURE_FLAGS environment variable (comma-separated list)
feature_flags_env = os.getenv("FEATURE_FLAGS", "")
# Default feature flags - these can be overridden by environment
default_flags = {
"CASH_PAYMENT_ENABLED": True,
"EMAIL_NOTIFICATIONS_ENABLED": True,
"EVENT_MANAGEMENT_ENABLED": True,
"AUTO_RENEWAL_ENABLED": False,
"MEMBERSHIP_TRANSFERS_ENABLED": False,
"BULK_OPERATIONS_ENABLED": False,
"ADVANCED_REPORTING_ENABLED": False,
"API_RATE_LIMITING_ENABLED": True,
}
# Parse environment variable
flags = default_flags.copy()
if feature_flags_env:
# Parse comma-separated key=value pairs
for flag_pair in feature_flags_env.split(","):
flag_pair = flag_pair.strip()
if "=" in flag_pair:
key, value = flag_pair.split("=", 1)
key = key.strip().upper()
value = value.strip().lower()
# Convert string to boolean
if value in ("true", "1", "yes", "on"):
flags[key] = True
elif value in ("false", "0", "no", "off"):
flags[key] = False
else:
# For non-boolean values, keep as string
flags[key] = value
return flags
def is_enabled(self, flag_name: str) -> bool:
"""Check if a feature flag is enabled"""
flag_name = flag_name.upper()
return bool(self._flags.get(flag_name, False))
def get_flag_value(self, flag_name: str, default: Any = None) -> Any:
"""Get the value of a feature flag"""
flag_name = flag_name.upper()
return self._flags.get(flag_name, default)
def get_all_flags(self) -> Dict[str, Any]:
"""Get all feature flags"""
return self._flags.copy()
def get_enabled_flags(self) -> List[str]:
"""Get list of enabled feature flag names"""
return [name for name, value in self._flags.items() if value is True]
def reload_flags(self) -> None:
"""Reload feature flags from environment (useful for runtime updates)"""
self._flags = self._load_flags_from_env()
# Global instance
feature_flags = FeatureFlagService()