Files
journal_organizer/dependency_manager.py
T

341 lines
14 KiB
Python
Raw Normal View History

"""
Dependency Management Module
Handles optional dependencies with graceful degradation and helpful error messages
"""
import logging
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Any, Optional, List, Tuple, Callable
@dataclass
class DependencyInfo:
"""Information about a dependency"""
name: str
import_name: str
install_command: str
description: str
required_for: List[str]
minimum_version: Optional[str] = None
alternative_packages: Optional[List[str]] = None
setup_instructions: Optional[str] = None
class DependencyManager:
"""Manages optional dependencies with graceful degradation"""
def __init__(self):
self.logger = logging.getLogger("DependencyManager")
self._dependency_cache: Dict[str, Any] = {}
self._availability_cache: Dict[str, bool] = {}
# Define known dependencies
self.dependencies = {
'anthropic': DependencyInfo(
name='anthropic',
import_name='anthropic',
install_command='pip install anthropic>=0.25.0',
description='Anthropic Claude API client for AI-powered content analysis',
required_for=['Claude AI analysis', 'Natural language understanding', 'Content transformation'],
minimum_version='0.25.0',
setup_instructions="""
Claude API Setup:
1. Install: pip install anthropic>=0.25.0
2. Get API key from: https://console.anthropic.com/
3. Set environment variable: export ANTHROPIC_API_KEY="sk-ant-your-key-here"
4. Or add to config.yaml: claude.api_key: "${ANTHROPIC_API_KEY}"
"""
),
'aiohttp': DependencyInfo(
name='aiohttp',
import_name='aiohttp',
install_command='pip install aiohttp>=3.9.0',
description='Async HTTP client for Obsidian REST API integration',
required_for=['Obsidian API communication', 'Reading/writing notes', 'File operations'],
minimum_version='3.9.0',
setup_instructions="""
HTTP Client Setup:
1. Install: pip install aiohttp>=3.9.0
2. Used for communicating with Obsidian Local REST API
3. No additional configuration required
"""
),
'yaml': DependencyInfo(
name='PyYAML',
import_name='yaml',
install_command='pip install pyyaml>=6.0',
description='YAML parser for configuration files',
required_for=['Configuration file parsing', 'Settings management'],
minimum_version='6.0',
alternative_packages=['ruamel.yaml'],
setup_instructions="""
YAML Parser Setup:
1. Install: pip install pyyaml>=6.0
2. Alternative: pip install ruamel.yaml
3. Used for parsing config.yaml files
"""
),
'pydantic': DependencyInfo(
name='pydantic',
import_name='pydantic',
install_command='pip install pydantic>=2.0.0',
description='Data validation and settings management',
required_for=['Configuration validation', 'Data model validation', 'Type checking'],
minimum_version='2.0.0',
setup_instructions="""
Data Validation Setup:
1. Install: pip install pydantic>=2.0.0
2. Used for validating configuration files and data models
3. Provides enhanced error messages and type checking
"""
)
}
def is_available(self, dependency_name: str) -> bool:
"""Check if a dependency is available"""
if dependency_name in self._availability_cache:
return self._availability_cache[dependency_name]
if dependency_name not in self.dependencies:
self.logger.warning(f"Unknown dependency: {dependency_name}")
return False
dep_info = self.dependencies[dependency_name]
try:
# Try to import the module
__import__(dep_info.import_name)
self._availability_cache[dependency_name] = True
return True
except ImportError:
self._availability_cache[dependency_name] = False
return False
def get_module(self, dependency_name: str, raise_on_missing: bool = False) -> Optional[Any]:
"""Get a module if available, with optional error raising"""
if dependency_name in self._dependency_cache:
return self._dependency_cache[dependency_name]
if not self.is_available(dependency_name):
if raise_on_missing:
raise ImportError(self._get_missing_dependency_message(dependency_name))
return None
dep_info = self.dependencies[dependency_name]
try:
module = __import__(dep_info.import_name)
self._dependency_cache[dependency_name] = module
return module
except ImportError as e:
if raise_on_missing:
raise ImportError(self._get_missing_dependency_message(dependency_name)) from e
return None
def get_class_from_module(self, dependency_name: str, class_name: str, raise_on_missing: bool = False) -> Optional[Any]:
"""Get a specific class from a module"""
module = self.get_module(dependency_name, raise_on_missing=False)
if module is None:
if raise_on_missing:
raise ImportError(self._get_missing_dependency_message(dependency_name))
return None
try:
return getattr(module, class_name)
except AttributeError as e:
if raise_on_missing:
raise ImportError(f"Class {class_name} not found in {dependency_name}") from e
return None
def require_dependency(self, dependency_name: str) -> Any:
"""Require a dependency, raising detailed error if not available"""
module = self.get_module(dependency_name, raise_on_missing=True)
return module
def check_all_dependencies(self) -> Dict[str, Dict[str, Any]]:
"""Check status of all known dependencies"""
results = {}
for dep_name, dep_info in self.dependencies.items():
is_avail = self.is_available(dep_name)
results[dep_name] = {
'available': is_avail,
'name': dep_info.name,
'description': dep_info.description,
'required_for': dep_info.required_for,
'install_command': dep_info.install_command,
'setup_instructions': dep_info.setup_instructions
}
if is_avail:
# Try to get version info
try:
module = self.get_module(dep_name)
if hasattr(module, '__version__'):
results[dep_name]['version'] = module.__version__
elif hasattr(module, 'version'):
results[dep_name]['version'] = module.version
except:
pass
return results
def get_missing_dependencies(self) -> List[str]:
"""Get list of missing dependencies"""
missing = []
for dep_name in self.dependencies:
if not self.is_available(dep_name):
missing.append(dep_name)
return missing
def get_installation_instructions(self, missing_only: bool = True) -> str:
"""Get installation instructions for dependencies"""
deps_to_show = self.get_missing_dependencies() if missing_only else list(self.dependencies.keys())
if not deps_to_show:
return "✓ All dependencies are available!"
instructions = []
instructions.append("Missing Dependencies Installation Guide:")
instructions.append("=" * 50)
for dep_name in deps_to_show:
dep_info = self.dependencies[dep_name]
instructions.append(f"\n📦 {dep_info.name}")
instructions.append(f" Description: {dep_info.description}")
instructions.append(f" Required for: {', '.join(dep_info.required_for)}")
instructions.append(f" Install: {dep_info.install_command}")
if dep_info.alternative_packages:
instructions.append(f" Alternatives: {', '.join(dep_info.alternative_packages)}")
if dep_info.setup_instructions:
instructions.append(f" Setup:{dep_info.setup_instructions}")
instructions.append("\n" + "=" * 50)
instructions.append("Quick install all missing dependencies:")
install_commands = [self.dependencies[dep].install_command for dep in deps_to_show]
instructions.append(" && ".join(install_commands))
return "\n".join(instructions)
def _get_missing_dependency_message(self, dependency_name: str) -> str:
"""Get detailed error message for missing dependency"""
if dependency_name not in self.dependencies:
return f"Unknown dependency: {dependency_name}"
dep_info = self.dependencies[dependency_name]
message_parts = [
f"Missing required dependency: {dep_info.name}",
f"Description: {dep_info.description}",
f"Required for: {', '.join(dep_info.required_for)}",
"",
f"To install: {dep_info.install_command}",
]
if dep_info.alternative_packages:
message_parts.append(f"Alternatives: {', '.join(dep_info.alternative_packages)}")
if dep_info.setup_instructions:
message_parts.append("")
message_parts.append("Setup Instructions:")
message_parts.append(dep_info.setup_instructions)
return "\n".join(message_parts)
def create_graceful_import_wrapper(self, dependency_name: str, fallback_message: Optional[str] = None):
"""Create a wrapper that provides graceful degradation for missing dependencies"""
def wrapper(func: Callable) -> Callable:
def inner(*args, **kwargs):
if not self.is_available(dependency_name):
error_msg = fallback_message or f"Feature unavailable: {dependency_name} is not installed"
detailed_msg = self._get_missing_dependency_message(dependency_name)
# Log the detailed message
self.logger.error(f"Dependency missing: {detailed_msg}")
# Return a user-friendly error
try:
from agent_core import SkillResult
except ImportError:
# Fallback if agent_core is not available
class SkillResult:
def __init__(self, success, error=None, message=""):
self.success = success
self.error = error
self.message = message
return SkillResult(
success=False,
error=error_msg,
message=f"Please install {dependency_name} to use this feature"
)
return func(*args, **kwargs)
return inner
return wrapper
def get_dependency_status_report(self) -> str:
"""Get a comprehensive dependency status report"""
all_deps = self.check_all_dependencies()
available = [name for name, info in all_deps.items() if info['available']]
missing = [name for name, info in all_deps.items() if not info['available']]
report = []
report.append("Dependency Status Report")
report.append("=" * 30)
if available:
report.append(f"\n✓ Available ({len(available)}):")
for dep_name in available:
info = all_deps[dep_name]
version = info.get('version', 'unknown version')
report.append(f" • {info['name']} ({version})")
if missing:
report.append(f"\n✗ Missing ({len(missing)}):")
for dep_name in missing:
info = all_deps[dep_name]
report.append(f" • {info['name']} - {info['description']}")
report.append(f" Install: {info['install_command']}")
if missing:
report.append(f"\nTo install all missing dependencies:")
install_commands = [all_deps[dep]['install_command'] for dep in missing]
report.append(" && ".join(install_commands))
else:
report.append(f"\n🎉 All dependencies are available!")
return "\n".join(report)
# Global dependency manager instance
dependency_manager = DependencyManager()
def get_dependency_manager() -> DependencyManager:
"""Get the global dependency manager instance"""
return dependency_manager
def check_dependency(dependency_name: str) -> bool:
"""Quick check if a dependency is available"""
return dependency_manager.is_available(dependency_name)
def require_dependency(dependency_name: str) -> Any:
"""Require a dependency, raising detailed error if not available"""
return dependency_manager.require_dependency(dependency_name)
def get_optional_module(dependency_name: str) -> Optional[Any]:
"""Get a module if available, None otherwise"""
return dependency_manager.get_module(dependency_name, raise_on_missing=False)
def graceful_import(dependency_name: str, fallback_message: Optional[str] = None):
"""Decorator for graceful dependency handling"""
return dependency_manager.create_graceful_import_wrapper(dependency_name, fallback_message)