Arch changes and feature flags
This commit is contained in:
@@ -60,7 +60,9 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = mysql+pymysql://membership_user:SecureMembershipPass2024!@mysql:3306/membership_db
|
||||
# Database URL - will be overridden by environment variables in production
|
||||
# sqlalchemy.url = mysql+pymysql://username:password@host:port/database
|
||||
sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from logging.config import fileConfig
|
||||
import os
|
||||
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
@@ -19,6 +20,25 @@ if config.config_file_name is not None:
|
||||
from app.models.models import Base
|
||||
target_metadata = Base.metadata
|
||||
|
||||
# Set database URL from environment variables if available
|
||||
def get_database_url():
|
||||
"""Get database URL from environment variables or config"""
|
||||
# Try to get from environment variables first
|
||||
db_host = os.getenv("DATABASE_HOST")
|
||||
db_port = os.getenv("DATABASE_PORT", "3306")
|
||||
db_user = os.getenv("DATABASE_USER")
|
||||
db_password = os.getenv("DATABASE_PASSWORD")
|
||||
db_name = os.getenv("DATABASE_NAME")
|
||||
|
||||
if all([db_host, db_user, db_password, db_name]):
|
||||
return f"mysql+pymysql://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}"
|
||||
|
||||
# Fallback to config file
|
||||
return config.get_main_option("sqlalchemy.url")
|
||||
|
||||
# Set the database URL
|
||||
config.set_main_option("sqlalchemy.url", get_database_url())
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
@@ -37,7 +57,7 @@ def run_migrations_offline() -> None:
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
url = get_database_url()
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
|
||||
@@ -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"])
|
||||
|
||||
47
backend/app/api/v1/feature_flags.py
Normal file
47
backend/app/api/v1/feature_flags.py
Normal 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"}
|
||||
15
backend/app/schemas/feature_flags.py
Normal file
15
backend/app/schemas/feature_flags.py
Normal 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
|
||||
80
backend/app/services/feature_flag_service.py
Normal file
80
backend/app/services/feature_flag_service.py
Normal 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()
|
||||
Reference in New Issue
Block a user