""" Plugin manager for issue backends. This module handles discovery, loading, and configuration of issue management backends. """ import importlib import yaml from pathlib import Path from typing import Dict, Any, Type, Optional from .base import IssueBackend from .exceptions import PluginNotFoundError, ConfigurationError class IssuePluginManager: """Manages issue backend plugins and configuration.""" def __init__(self, config_path: Optional[str] = None): """ Initialize plugin manager. Args: config_path: Optional path to configuration file """ self.config = self._load_config(config_path) self.plugins = self._discover_plugins() def get_backend(self, backend_name: Optional[str] = None) -> IssueBackend: """ Get configured backend instance. Args: backend_name: Backend name to use, or None for default Returns: IssueBackend instance Raises: PluginNotFoundError: If backend not found """ backend_name = backend_name or self.config.get('default_backend', 'gitea') plugin_class = self.plugins.get(backend_name) if not plugin_class: raise PluginNotFoundError(f"Unknown backend: {backend_name}") backend_config = self.config.get('backends', {}).get(backend_name, {}) return plugin_class(backend_config) def _load_config(self, config_path: Optional[str] = None) -> Dict[str, Any]: """ Load configuration from file or return defaults. Args: config_path: Path to configuration file Returns: Configuration dictionary """ from config.manager import UnifiedConfigManager # Get main markitect configuration to extract API token and settings try: config_manager = UnifiedConfigManager() markitect_config = config_manager.get_config() main_config = markitect_config.__dict__ if hasattr(markitect_config, '__dict__') else {} except Exception: main_config = {} if config_path is None: config_path = Path('.markitect/config/issues.yml') else: config_path = Path(config_path) # Extract configuration from main markitect config gitea_url = main_config.get('gitea_url', 'http://92.205.130.254:32166') api_token = main_config.get('api_token', '') repo_owner = main_config.get('repo_owner', 'coulomb') repo_name = main_config.get('repo_name', 'markitect_project') # Default configuration using main config values default_config = { 'default_backend': 'gitea', 'backends': { 'gitea': { 'url': gitea_url, 'token': api_token, 'repo_owner': repo_owner, 'repo_name': repo_name, 'repo': f'{repo_owner}/{repo_name}' }, 'local': { 'directory': '.markitect/issues', 'auto_git': True } } } if not config_path.exists(): return default_config try: with open(config_path, 'r') as f: config = yaml.safe_load(f) or {} # Merge with defaults merged_config = default_config.copy() merged_config.update(config) # Ensure gitea backend has token from main config if not overridden if 'backends' in merged_config and 'gitea' in merged_config['backends']: gitea_config = merged_config['backends']['gitea'] if not gitea_config.get('token'): gitea_config['token'] = api_token if not gitea_config.get('repo_owner'): gitea_config['repo_owner'] = repo_owner if not gitea_config.get('repo_name'): gitea_config['repo_name'] = repo_name return merged_config except Exception: # Return defaults if config loading fails return default_config def _discover_plugins(self) -> Dict[str, Type[IssueBackend]]: """ Discover available backend plugins. Returns: Dictionary mapping backend names to plugin classes """ plugins = {} # Try to import known plugins plugin_modules = [ ('gitea', 'markitect.issues.plugins.gitea', 'GiteaPlugin'), ('local', 'markitect.issues.plugins.local', 'LocalPlugin'), ] for name, module_path, class_name in plugin_modules: try: module = importlib.import_module(module_path) plugin_class = getattr(module, class_name) if issubclass(plugin_class, IssueBackend): plugins[name] = plugin_class except (ImportError, AttributeError): # Plugin not available, skip continue return plugins