feat: Implement unified configuration management system
Consolidates scattered configuration patterns across TDDAI, Gitea, and MarkiTect into a unified, maintainable system addressing issue #22. Key improvements: - Created centralized config/ module with base classes and utilities - Eliminated duplicate load_dotenv_file() functions - Standardized environment variables with MARKITECT_ prefix - Implemented comprehensive validation with helpful error messages - Maintained full backward compatibility with existing TDDAI config Architecture: - BaseConfig: Abstract base with common functionality - MarkitectConfig: Main configuration class with legacy support - Compatibility layer: TddaiConfigCompat and GiteaConfigCompat wrappers - Unified error handling: ConfigurationError hierarchy All existing tests pass without modification, ensuring seamless transition. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
239
config/manager.py
Normal file
239
config/manager.py
Normal file
@@ -0,0 +1,239 @@
|
||||
"""
|
||||
Unified configuration manager.
|
||||
|
||||
Provides centralized access to all configuration across the MarkiTect project.
|
||||
"""
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional, List, Union
|
||||
|
||||
from .base import BaseConfig
|
||||
from .exceptions import ConfigurationError
|
||||
from .loaders import resolve_path
|
||||
|
||||
|
||||
@dataclass
|
||||
class MarkitectConfig(BaseConfig):
|
||||
"""Main MarkiTect configuration.
|
||||
|
||||
Consolidates configuration for all MarkiTect components with standardized
|
||||
MARKITECT_ environment variable prefix.
|
||||
"""
|
||||
|
||||
# Repository settings
|
||||
gitea_url: str = "http://localhost:3000"
|
||||
repo_owner: str = ""
|
||||
repo_name: str = ""
|
||||
api_token: str = ""
|
||||
|
||||
# Workspace and directory settings
|
||||
workspace_dir: Path = field(default_factory=lambda: Path(".markitect_workspace"))
|
||||
database_path: Path = field(default_factory=lambda: Path.home() / ".markitect" / "markitect.db")
|
||||
cache_dir: Path = field(default_factory=lambda: Path(".ast_cache"))
|
||||
|
||||
# Test settings
|
||||
tests_dir: Path = field(default_factory=lambda: Path("tests"))
|
||||
test_file_pattern: str = "test_issue_{issue_num}_{scenario}.py"
|
||||
|
||||
# AI and command settings
|
||||
claude_code_command: str = "claude"
|
||||
|
||||
# Legacy compatibility
|
||||
_legacy_loaded: bool = field(default=False, init=False)
|
||||
|
||||
def get_env_prefix(self) -> str:
|
||||
"""Get environment variable prefix."""
|
||||
return "MARKITECT_"
|
||||
|
||||
def get_config_files(self) -> List[Union[str, Path]]:
|
||||
"""Get configuration files to load."""
|
||||
return [
|
||||
".markitect.env", # New unified config
|
||||
".env.markitect", # Alternative naming
|
||||
".env.tddai", # Legacy TDDAI config
|
||||
".env" # General environment file
|
||||
]
|
||||
|
||||
def validate(self) -> None:
|
||||
"""Validate configuration."""
|
||||
# Always ensure legacy environment variables are loaded before validation
|
||||
self.load_legacy_environment()
|
||||
self._legacy_loaded = True
|
||||
|
||||
# Validate required fields
|
||||
self.validate_required_fields('gitea_url', 'repo_owner', 'repo_name')
|
||||
|
||||
# Validate URLs
|
||||
self.validate_url('gitea_url')
|
||||
|
||||
# Ensure directories exist or can be created
|
||||
self.validate_path_exists('workspace_dir', create_if_missing=True)
|
||||
self.validate_path_exists('cache_dir', create_if_missing=True)
|
||||
self.validate_path_exists('tests_dir', create_if_missing=True)
|
||||
|
||||
# Ensure database directory exists
|
||||
if self.database_path:
|
||||
self.validate_path_exists(
|
||||
str(self.database_path.parent),
|
||||
create_if_missing=True
|
||||
)
|
||||
|
||||
def get_legacy_mapping(self) -> Dict[str, str]:
|
||||
"""Get legacy environment variable mapping."""
|
||||
return {
|
||||
# TDDAI legacy variables
|
||||
'TDDAI_GITEA_URL': 'gitea_url',
|
||||
'TDDAI_REPO_OWNER': 'repo_owner',
|
||||
'TDDAI_REPO_NAME': 'repo_name',
|
||||
'TDDAI_WORKSPACE_DIR': 'workspace_dir',
|
||||
'TDDAI_TESTS_DIR': 'tests_dir',
|
||||
'TDDAI_TEST_FILE_PATTERN': 'test_file_pattern',
|
||||
'TDDAI_CLAUDE_CODE_COMMAND': 'claude_code_command',
|
||||
|
||||
# Gitea legacy variables
|
||||
'GITEA_API_TOKEN': 'api_token',
|
||||
'GITEA_BASE_URL': 'gitea_url',
|
||||
'GITEA_REPO_OWNER': 'repo_owner',
|
||||
'GITEA_REPO_NAME': 'repo_name',
|
||||
|
||||
# Cache legacy variables
|
||||
'XDG_CACHE_HOME': 'cache_dir',
|
||||
}
|
||||
|
||||
def get_tddai_compatible_dict(self) -> Dict[str, Any]:
|
||||
"""Get configuration in TDDAI-compatible format.
|
||||
|
||||
Returns configuration that can be used to create TddaiConfig
|
||||
objects for backward compatibility.
|
||||
"""
|
||||
return {
|
||||
'workspace_dir': self.workspace_dir,
|
||||
'gitea_url': self.gitea_url,
|
||||
'repo_owner': self.repo_owner,
|
||||
'repo_name': self.repo_name,
|
||||
'tests_dir': self.tests_dir,
|
||||
'test_file_pattern': self.test_file_pattern,
|
||||
'claude_code_command': self.claude_code_command
|
||||
}
|
||||
|
||||
def get_gitea_compatible_dict(self) -> Dict[str, Any]:
|
||||
"""Get configuration in Gitea-compatible format.
|
||||
|
||||
Returns configuration that can be used to create GiteaConfig
|
||||
objects for backward compatibility.
|
||||
"""
|
||||
return {
|
||||
'base_url': self.gitea_url,
|
||||
'repo_owner': self.repo_owner,
|
||||
'repo_name': self.repo_name,
|
||||
'auth_token': self.api_token
|
||||
}
|
||||
|
||||
|
||||
class UnifiedConfigManager:
|
||||
"""Centralized configuration manager.
|
||||
|
||||
Provides singleton access to unified configuration and manages
|
||||
backward compatibility with existing configuration systems.
|
||||
"""
|
||||
|
||||
_instance: Optional['UnifiedConfigManager'] = None
|
||||
_config: Optional[MarkitectConfig] = None
|
||||
|
||||
def __new__(cls) -> 'UnifiedConfigManager':
|
||||
"""Ensure singleton instance."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize configuration manager."""
|
||||
if self._config is None:
|
||||
self._config = MarkitectConfig()
|
||||
|
||||
@property
|
||||
def config(self) -> MarkitectConfig:
|
||||
"""Get the unified configuration."""
|
||||
if self._config is None:
|
||||
self._config = MarkitectConfig()
|
||||
return self._config
|
||||
|
||||
def reload(self) -> None:
|
||||
"""Reload configuration from environment and files."""
|
||||
self._config = MarkitectConfig()
|
||||
|
||||
def validate_all(self) -> None:
|
||||
"""Validate all configuration."""
|
||||
self.config.validate()
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get configuration status for debugging.
|
||||
|
||||
Returns:
|
||||
Dictionary with configuration status information
|
||||
"""
|
||||
try:
|
||||
self.validate_all()
|
||||
valid = True
|
||||
errors = []
|
||||
except ConfigurationError as e:
|
||||
valid = False
|
||||
errors = [str(e)]
|
||||
|
||||
return {
|
||||
'valid': valid,
|
||||
'errors': errors,
|
||||
'config_files_found': [
|
||||
str(f) for f in self.config.get_config_files()
|
||||
if Path(f).exists()
|
||||
],
|
||||
'environment_variables': {
|
||||
key: '***' if 'token' in key.lower() or 'password' in key.lower() else value
|
||||
for key, value in os.environ.items()
|
||||
if key.startswith('MARKITECT_') or key.startswith('TDDAI_') or key.startswith('GITEA_')
|
||||
},
|
||||
'resolved_paths': {
|
||||
'workspace_dir': str(self.config.workspace_dir),
|
||||
'database_path': str(self.config.database_path),
|
||||
'cache_dir': str(self.config.cache_dir),
|
||||
'tests_dir': str(self.config.tests_dir)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Global configuration manager instance
|
||||
_manager: Optional[UnifiedConfigManager] = None
|
||||
|
||||
|
||||
def get_unified_config() -> MarkitectConfig:
|
||||
"""Get the unified configuration instance.
|
||||
|
||||
Returns:
|
||||
The global MarkitectConfig instance
|
||||
|
||||
Raises:
|
||||
ConfigurationError: If configuration is invalid
|
||||
"""
|
||||
global _manager
|
||||
if _manager is None:
|
||||
_manager = UnifiedConfigManager()
|
||||
return _manager.config
|
||||
|
||||
|
||||
def reload_config() -> None:
|
||||
"""Reload configuration from environment and files."""
|
||||
global _manager
|
||||
if _manager is not None:
|
||||
_manager.reload()
|
||||
else:
|
||||
_manager = UnifiedConfigManager()
|
||||
|
||||
|
||||
def get_config_status() -> Dict[str, Any]:
|
||||
"""Get configuration status for debugging."""
|
||||
global _manager
|
||||
if _manager is None:
|
||||
_manager = UnifiedConfigManager()
|
||||
return _manager.get_status()
|
||||
Reference in New Issue
Block a user