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