#!/usr/bin/env python3
"""
Configuration Manager
Provides unified configuration management with support for environment variables,
YAML files, and database storage.
"""
import os
import yaml
from pathlib import Path
from typing import Any, Dict, List, Optional, Union, Type, TypeVar
from dataclasses import dataclass, field, asdict
from contextlib import contextmanager
from ..utils import get_logger, get_env_bool, get_env_int, get_env_float, get_env_list
from ..database.models import Configuration
from ..database.manager import DatabaseManager
T = TypeVar('T')
logger = get_logger(__name__)
@dataclass
class ServerConfig:
"""Server configuration."""
host: str = '127.0.0.1'
port: int = 8000
debug: bool = False
reload: bool = False
workers: int = 1
max_connections: int = 100
timeout: int = 30
@classmethod
def from_env(cls) -> 'ServerConfig':
"""Create from environment variables."""
return cls(
host=os.getenv('SERVER_HOST', '127.0.0.1'),
port=get_env_int('SERVER_PORT', 8000),
debug=get_env_bool('DEBUG', False),
reload=get_env_bool('RELOAD', False),
workers=get_env_int('WORKERS', 1),
max_connections=get_env_int('MAX_CONNECTIONS', 100),
timeout=get_env_int('TIMEOUT', 30),
)
@dataclass
class DatabaseConfig:
"""Database configuration."""
type: str = 'sqlite'
url: str = 'sqlite:///anydocs.db'
pool_size: int = 5
max_overflow: int = 10
pool_timeout: int = 30
pool_recycle: int = 3600
echo: bool = False
@classmethod
def from_env(cls) -> 'DatabaseConfig':
"""Create from environment variables."""
return cls(
type=os.getenv('DATABASE_TYPE', 'sqlite'),
url=os.getenv('DATABASE_URL', 'sqlite:///anydocs.db'),
pool_size=get_env_int('DATABASE_POOL_SIZE', 5),
max_overflow=get_env_int('DATABASE_MAX_OVERFLOW', 10),
pool_timeout=get_env_int('DATABASE_POOL_TIMEOUT', 30),
pool_recycle=get_env_int('DATABASE_POOL_RECYCLE', 3600),
echo=get_env_bool('DATABASE_ECHO', False),
)
@dataclass
class AuthConfig:
"""Authentication configuration."""
methods: List[str] = field(default_factory=lambda: ['api_key'])
jwt_secret: str = ''
jwt_algorithm: str = 'HS256'
jwt_expiration: int = 3600
api_key_prefix: str = 'ak'
oauth2_providers: Dict[str, Dict[str, str]] = field(default_factory=dict)
@classmethod
def from_env(cls) -> 'AuthConfig':
"""Create from environment variables."""
return cls(
methods=get_env_list('AUTH_METHODS', default=['api_key']),
jwt_secret=os.getenv('JWT_SECRET', ''),
jwt_algorithm=os.getenv('JWT_ALGORITHM', 'HS256'),
jwt_expiration=get_env_int('JWT_EXPIRATION', 3600),
api_key_prefix=os.getenv('API_KEY_PREFIX', 'ak'),
oauth2_providers={}, # Load from config file
)
@dataclass
class CacheConfig:
"""Cache configuration."""
enabled: bool = True
type: str = 'memory'
ttl: int = 3600
max_size: int = 1000
redis_url: str = ''
@classmethod
def from_env(cls) -> 'CacheConfig':
"""Create from environment variables."""
return cls(
enabled=get_env_bool('CACHE_ENABLED', True),
type=os.getenv('CACHE_TYPE', 'memory'),
ttl=get_env_int('CACHE_TTL', 3600),
max_size=get_env_int('CACHE_MAX_SIZE', 1000),
redis_url=os.getenv('REDIS_URL', ''),
)
@dataclass
class LoggingConfig:
"""Logging configuration."""
level: str = 'INFO'
format: str = '%(asctime)s [%(levelname)8s] %(name)s: %(message)s'
file: str = ''
max_size: int = 10 * 1024 * 1024 # 10MB
backup_count: int = 5
json_logs: bool = False
@classmethod
def from_env(cls) -> 'LoggingConfig':
"""Create from environment variables."""
return cls(
level=os.getenv('LOG_LEVEL', 'INFO'),
format=os.getenv('LOG_FORMAT', '%(asctime)s [%(levelname)8s] %(name)s: %(message)s'),
file=os.getenv('LOG_FILE', ''),
max_size=get_env_int('LOG_MAX_SIZE', 10 * 1024 * 1024),
backup_count=get_env_int('LOG_BACKUP_COUNT', 5),
json_logs=get_env_bool('JSON_LOGS', False),
)
@dataclass
class ContentConfig:
"""Content processing configuration."""
max_file_size: int = 50 * 1024 * 1024 # 50MB
supported_formats: List[str] = field(default_factory=lambda: ['markdown', 'html', 'text'])
image_max_size: int = 5 * 1024 * 1024 # 5MB
image_formats: List[str] = field(default_factory=lambda: ['jpg', 'jpeg', 'png', 'gif', 'webp'])
enable_syntax_highlighting: bool = True
@classmethod
def from_env(cls) -> 'ContentConfig':
"""Create from environment variables."""
return cls(
max_file_size=get_env_int('CONTENT_MAX_FILE_SIZE', 50 * 1024 * 1024),
supported_formats=get_env_list('CONTENT_SUPPORTED_FORMATS', default=['markdown', 'html', 'text']),
image_max_size=get_env_int('CONTENT_IMAGE_MAX_SIZE', 5 * 1024 * 1024),
image_formats=get_env_list('CONTENT_IMAGE_FORMATS', default=['jpg', 'jpeg', 'png', 'gif', 'webp']),
enable_syntax_highlighting=get_env_bool('CONTENT_ENABLE_SYNTAX_HIGHLIGHTING', True),
)
@dataclass
class MCPConfig:
"""MCP server configuration."""
name: str = 'AnyDocs MCP Server'
version: str = '1.0.0'
description: str = 'Transform documentation into MCP-compatible server'
max_resources: int = 1000
max_tools: int = 50
enable_discovery: bool = True
@classmethod
def from_env(cls) -> 'MCPConfig':
"""Create from environment variables."""
return cls(
name=os.getenv('MCP_NAME', 'AnyDocs MCP Server'),
version=os.getenv('MCP_VERSION', '1.0.0'),
description=os.getenv('MCP_DESCRIPTION', 'Transform documentation into MCP-compatible server'),
max_resources=get_env_int('MCP_MAX_RESOURCES', 1000),
max_tools=get_env_int('MCP_MAX_TOOLS', 50),
enable_discovery=get_env_bool('MCP_ENABLE_DISCOVERY', True),
)
@dataclass
class AppConfig:
"""Main application configuration."""
server: ServerConfig = field(default_factory=ServerConfig)
database: DatabaseConfig = field(default_factory=DatabaseConfig)
auth: AuthConfig = field(default_factory=AuthConfig)
cache: CacheConfig = field(default_factory=CacheConfig)
logging: LoggingConfig = field(default_factory=LoggingConfig)
content: ContentConfig = field(default_factory=ContentConfig)
mcp: MCPConfig = field(default_factory=MCPConfig)
@classmethod
def from_env(cls) -> 'AppConfig':
"""Create from environment variables."""
return cls(
server=ServerConfig.from_env(),
database=DatabaseConfig.from_env(),
auth=AuthConfig.from_env(),
cache=CacheConfig.from_env(),
logging=LoggingConfig.from_env(),
content=ContentConfig.from_env(),
mcp=MCPConfig.from_env(),
)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return asdict(self)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'AppConfig':
"""Create from dictionary."""
return cls(
server=ServerConfig(**data.get('server', {})),
database=DatabaseConfig(**data.get('database', {})),
auth=AuthConfig(**data.get('auth', {})),
cache=CacheConfig(**data.get('cache', {})),
logging=LoggingConfig(**data.get('logging', {})),
content=ContentConfig(**data.get('content', {})),
mcp=MCPConfig(**data.get('mcp', {})),
)
class ConfigManager:
"""Configuration manager with multiple sources."""
def __init__(
self,
config_file: Optional[str] = None,
db_manager: Optional[DatabaseManager] = None,
auto_reload: bool = False
):
"""
Initialize configuration manager.
Args:
config_file: Path to YAML configuration file
db_manager: Database manager for persistent config
auto_reload: Enable automatic config reloading
"""
self.config_file = config_file
self.db_manager = db_manager
self.auto_reload = auto_reload
self._config: Optional[AppConfig] = None
self._file_mtime: Optional[float] = None
logger.info("Configuration manager initialized", config_file=config_file)
def load_config(self, force_reload: bool = False) -> AppConfig:
"""Load configuration from all sources.
Args:
force_reload: Force reload even if cached
Returns:
Application configuration
"""
if self._config is not None and not force_reload and not self._should_reload():
return self._config
logger.info("Loading configuration")
# Start with environment variables
config = AppConfig.from_env()
# Override with file configuration
if self.config_file:
file_config = self._load_file_config()
if file_config:
config = self._merge_configs(config, file_config)
# Override with database configuration
if self.db_manager:
db_config = self._load_db_config()
if db_config:
config = self._merge_configs(config, db_config)
self._config = config
self._update_file_mtime()
logger.info("Configuration loaded successfully")
return config
def save_config(self, config: AppConfig, target: str = 'file') -> bool:
"""Save configuration to target.
Args:
config: Configuration to save
target: Target ('file' or 'database')
Returns:
True if successful
"""
try:
if target == 'file' and self.config_file:
return self._save_file_config(config)
elif target == 'database' and self.db_manager:
return self._save_db_config(config)
else:
logger.error("Invalid save target or missing manager", target=target)
return False
except Exception as e:
logger.error("Failed to save configuration", target=target, error=str(e))
return False
def get_config(self) -> AppConfig:
"""Get current configuration.
Returns:
Current application configuration
"""
return self.load_config()
def update_config(self, updates: Dict[str, Any], save_to: Optional[str] = None) -> bool:
"""Update configuration with partial updates.
Args:
updates: Configuration updates
save_to: Target to save to ('file', 'database', or None)
Returns:
True if successful
"""
try:
current_config = self.get_config()
config_dict = current_config.to_dict()
# Apply updates
self._deep_update(config_dict, updates)
# Create new config
new_config = AppConfig.from_dict(config_dict)
# Save if requested
if save_to:
if not self.save_config(new_config, save_to):
return False
# Update cached config
self._config = new_config
logger.info("Configuration updated", updates=updates)
return True
except Exception as e:
logger.error("Failed to update configuration", error=str(e))
return False
def get_setting(self, key: str, default: Any = None) -> Any:
"""Get specific setting value.
Args:
key: Setting key (dot notation supported)
default: Default value if not found
Returns:
Setting value
"""
config = self.get_config()
config_dict = config.to_dict()
# Navigate through nested keys
keys = key.split('.')
value = config_dict
for k in keys:
if isinstance(value, dict) and k in value:
value = value[k]
else:
return default
return value
def set_setting(self, key: str, value: Any, save_to: Optional[str] = None) -> bool:
"""Set specific setting value.
Args:
key: Setting key (dot notation supported)
value: Setting value
save_to: Target to save to
Returns:
True if successful
"""
keys = key.split('.')
updates = {}
# Build nested update dictionary
current = updates
for k in keys[:-1]:
current[k] = {}
current = current[k]
current[keys[-1]] = value
return self.update_config(updates, save_to)
def _load_file_config(self) -> Optional[AppConfig]:
"""Load configuration from YAML file."""
if not self.config_file or not Path(self.config_file).exists():
return None
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
if not data:
return None
return AppConfig.from_dict(data)
except Exception as e:
logger.error("Failed to load file configuration", file=self.config_file, error=str(e))
return None
def _save_file_config(self, config: AppConfig) -> bool:
"""Save configuration to YAML file."""
if not self.config_file:
return False
try:
# Ensure directory exists
Path(self.config_file).parent.mkdir(parents=True, exist_ok=True)
with open(self.config_file, 'w', encoding='utf-8') as f:
yaml.dump(config.to_dict(), f, default_flow_style=False, indent=2)
self._update_file_mtime()
logger.info("Configuration saved to file", file=self.config_file)
return True
except Exception as e:
logger.error("Failed to save file configuration", file=self.config_file, error=str(e))
return False
def _load_db_config(self) -> Optional[AppConfig]:
"""Load configuration from database."""
if not self.db_manager:
return None
try:
with self.db_manager.get_session() as session:
configs = session.query(Configuration).all()
if not configs:
return None
# Build config dictionary
config_dict = {}
for config in configs:
keys = config.key.split('.')
current = config_dict
for key in keys[:-1]:
if key not in current:
current[key] = {}
current = current[key]
# Parse value based on type
if config.value_type == 'int':
current[keys[-1]] = int(config.value)
elif config.value_type == 'float':
current[keys[-1]] = float(config.value)
elif config.value_type == 'bool':
current[keys[-1]] = config.value.lower() == 'true'
elif config.value_type == 'list':
current[keys[-1]] = config.value.split(',')
else:
current[keys[-1]] = config.value
return AppConfig.from_dict(config_dict)
except Exception as e:
logger.error("Failed to load database configuration", error=str(e))
return None
def _save_db_config(self, config: AppConfig) -> bool:
"""Save configuration to database."""
if not self.db_manager:
return False
try:
with self.db_manager.get_session() as session:
# Clear existing configuration
session.query(Configuration).delete()
# Flatten config dictionary
flat_config = self._flatten_dict(config.to_dict())
# Save each setting
for key, value in flat_config.items():
value_type = 'str'
if isinstance(value, int):
value_type = 'int'
elif isinstance(value, float):
value_type = 'float'
elif isinstance(value, bool):
value_type = 'bool'
elif isinstance(value, list):
value_type = 'list'
value = ','.join(str(v) for v in value)
config_obj = Configuration(
key=key,
value=str(value),
value_type=value_type,
description=f"Configuration setting: {key}"
)
session.add(config_obj)
session.commit()
logger.info("Configuration saved to database")
return True
except Exception as e:
logger.error("Failed to save database configuration", error=str(e))
return False
def _merge_configs(self, base: AppConfig, override: AppConfig) -> AppConfig:
"""Merge two configurations."""
base_dict = base.to_dict()
override_dict = override.to_dict()
self._deep_update(base_dict, override_dict)
return AppConfig.from_dict(base_dict)
def _deep_update(self, base: Dict[str, Any], updates: Dict[str, Any]) -> None:
"""Deep update dictionary."""
for key, value in updates.items():
if key in base and isinstance(base[key], dict) and isinstance(value, dict):
self._deep_update(base[key], value)
else:
base[key] = value
def _flatten_dict(self, d: Dict[str, Any], parent_key: str = '', sep: str = '.') -> Dict[str, Any]:
"""Flatten nested dictionary."""
items = []
for k, v in d.items():
new_key = f"{parent_key}{sep}{k}" if parent_key else k
if isinstance(v, dict):
items.extend(self._flatten_dict(v, new_key, sep=sep).items())
else:
items.append((new_key, v))
return dict(items)
def _should_reload(self) -> bool:
"""Check if configuration should be reloaded."""
if not self.auto_reload or not self.config_file:
return False
try:
current_mtime = Path(self.config_file).stat().st_mtime
return self._file_mtime is None or current_mtime > self._file_mtime
except Exception:
return False
def _update_file_mtime(self) -> None:
"""Update file modification time."""
if self.config_file:
try:
self._file_mtime = Path(self.config_file).stat().st_mtime
except Exception:
self._file_mtime = None
@contextmanager
def config_context(self, **overrides):
"""Context manager for temporary configuration overrides."""
original_config = self._config
try:
if overrides:
current_config = self.get_config()
config_dict = current_config.to_dict()
self._deep_update(config_dict, overrides)
self._config = AppConfig.from_dict(config_dict)
yield self._config
finally:
self._config = original_config
# Global configuration manager instance
_config_manager: Optional[ConfigManager] = None
def init_config_manager(
config_file: Optional[str] = None,
db_manager: Optional[DatabaseManager] = None,
auto_reload: bool = False
) -> ConfigManager:
"""Initialize global configuration manager.
Args:
config_file: Path to configuration file
db_manager: Database manager instance
auto_reload: Enable automatic reloading
Returns:
ConfigManager instance
"""
global _config_manager
_config_manager = ConfigManager(config_file, db_manager, auto_reload)
return _config_manager
def get_config_manager() -> Optional[ConfigManager]:
"""Get global configuration manager.
Returns:
ConfigManager instance or None
"""
return _config_manager
def get_config() -> AppConfig:
"""Get current application configuration.
Returns:
Application configuration
"""
if _config_manager:
return _config_manager.get_config()
else:
# Fallback to environment-only config
return AppConfig.from_env()
def get_setting(key: str, default: Any = None) -> Any:
"""Get configuration setting.
Args:
key: Setting key
default: Default value
Returns:
Setting value
"""
if _config_manager:
return _config_manager.get_setting(key, default)
else:
# Fallback to environment variables
env_key = key.upper().replace('.', '_')
return os.getenv(env_key, default)