Files
markitect-main/config/manager.py
tegwick a7a7960ef6 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>
2025-09-26 17:45:56 +02:00

239 lines
7.6 KiB
Python

"""
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()