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>
239 lines
7.6 KiB
Python
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() |