EU-Utility/core/plugin_dependency_manager.py

186 lines
6.1 KiB
Python

"""
EU-Utility - Plugin Dependency Manager
Manages plugin dependencies and auto-installs them when needed.
"""
import sys
import subprocess
import importlib
from typing import Dict, List, Tuple, Optional, Any
from dataclasses import dataclass
from pathlib import Path
import json
@dataclass
class DependencyCheck:
"""Result of a dependency check."""
name: str
required: bool
installed: bool
version: Optional[str] = None
error: Optional[str] = None
class PluginDependencyManager:
"""Manages plugin dependencies with auto-install capability."""
def __init__(self, plugin_manager=None):
self.plugin_manager = plugin_manager
self.install_status_file = Path("config/dependency_installs.json")
self._install_status = self._load_install_status()
def _load_install_status(self) -> Dict:
"""Load status of previously installed dependencies."""
if self.install_status_file.exists():
try:
with open(self.install_status_file, 'r') as f:
return json.load(f)
except:
pass
return {}
def _save_install_status(self):
"""Save status of installed dependencies."""
try:
self.install_status_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.install_status_file, 'w') as f:
json.dump(self._install_status, f, indent=2)
except Exception as e:
print(f"[DependencyManager] Failed to save status: {e}")
def check_dependency(self, package_name: str) -> DependencyCheck:
"""Check if a pip package is installed."""
# Handle version specifiers (e.g., "package>=1.0")
base_name = package_name.split('[')[0].split('>')[0].split('<')[0].split('=')[0].strip()
try:
module = importlib.import_module(base_name.replace('-', '_'))
version = getattr(module, '__version__', 'unknown')
return DependencyCheck(
name=package_name,
required=True,
installed=True,
version=version
)
except ImportError:
return DependencyCheck(
name=package_name,
required=True,
installed=False
)
except Exception as e:
return DependencyCheck(
name=package_name,
required=True,
installed=False,
error=str(e)
)
def check_all_dependencies(self, plugin_class) -> Tuple[List[DependencyCheck], List[DependencyCheck]]:
"""Check all dependencies for a plugin.
Returns:
Tuple of (installed_deps, missing_deps)
"""
deps = getattr(plugin_class, 'dependencies', {})
pip_deps = deps.get('pip', [])
installed = []
missing = []
for dep in pip_deps:
check = self.check_dependency(dep)
if check.installed:
installed.append(check)
else:
missing.append(check)
return installed, missing
def install_dependency(self, package_name: str) -> Tuple[bool, str]:
"""Install a pip package.
Returns:
(success, message)
"""
try:
print(f"[DependencyManager] Installing {package_name}...")
result = subprocess.run(
[sys.executable, '-m', 'pip', 'install', package_name],
capture_output=True,
text=True,
timeout=300
)
if result.returncode == 0:
self._install_status[package_name] = {
'installed': True,
'output': result.stdout[-500:] if len(result.stdout) > 500 else result.stdout
}
self._save_install_status()
return True, f"Successfully installed {package_name}"
else:
return False, f"Failed to install {package_name}: {result.stderr}"
except subprocess.TimeoutExpired:
return False, f"Installation of {package_name} timed out"
except Exception as e:
return False, f"Error installing {package_name}: {e}"
def install_all_missing(self, plugin_class, progress_callback=None) -> Tuple[bool, List[str]]:
"""Install all missing dependencies for a plugin.
Returns:
(all_successful, messages)
"""
_, missing = self.check_all_dependencies(plugin_class)
if not missing:
return True, ["All dependencies already installed"]
messages = []
all_success = True
for dep in missing:
if progress_callback:
progress_callback(f"Installing {dep.name}...")
success, msg = self.install_dependency(dep.name)
messages.append(msg)
if not success:
all_success = False
return all_success, messages
def get_missing_dependencies_text(self, plugin_class) -> str:
"""Get a formatted text of missing dependencies."""
_, missing = self.check_all_dependencies(plugin_class)
if not missing:
return "All dependencies are installed."
lines = [f"Missing dependencies ({len(missing)}):"]
for dep in missing:
lines.append(f"{dep.name}")
return "\n".join(lines)
def has_dependencies(self, plugin_class) -> bool:
"""Check if a plugin has declared dependencies."""
deps = getattr(plugin_class, 'dependencies', {})
return bool(deps.get('pip', []))
# Singleton instance
_dep_manager = None
def get_dependency_manager(plugin_manager=None) -> PluginDependencyManager:
"""Get the plugin dependency manager singleton."""
global _dep_manager
if _dep_manager is None:
_dep_manager = PluginDependencyManager(plugin_manager)
return _dep_manager