mirror of
https://github.com/aljazceru/enclava.git
synced 2025-12-18 16:04:28 +01:00
mega changes
This commit is contained in:
@@ -18,6 +18,7 @@ logger = get_logger(__name__)
|
||||
@dataclass
|
||||
class ModuleManifest:
|
||||
"""Module manifest loaded from module.yaml"""
|
||||
|
||||
name: str
|
||||
version: str
|
||||
description: str
|
||||
@@ -37,7 +38,7 @@ class ModuleManifest:
|
||||
health_checks: List[Dict] = None
|
||||
ui_config: Dict = None
|
||||
documentation: Dict = None
|
||||
|
||||
|
||||
def __post_init__(self):
|
||||
if self.dependencies is None:
|
||||
self.dependencies = []
|
||||
@@ -63,27 +64,29 @@ class ModuleManifest:
|
||||
|
||||
class ModuleConfigManager:
|
||||
"""Manages module configurations and JSON schema validation"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.manifests: Dict[str, ModuleManifest] = {}
|
||||
self.schemas: Dict[str, Dict] = {}
|
||||
self.configs: Dict[str, Dict] = {}
|
||||
|
||||
async def discover_modules(self, modules_path: str = "modules") -> Dict[str, ModuleManifest]:
|
||||
|
||||
async def discover_modules(
|
||||
self, modules_path: str = "modules"
|
||||
) -> Dict[str, ModuleManifest]:
|
||||
"""Discover modules from filesystem using module.yaml manifests"""
|
||||
discovered_modules = {}
|
||||
|
||||
|
||||
modules_dir = Path(modules_path)
|
||||
if not modules_dir.exists():
|
||||
logger.warning(f"Modules directory not found: {modules_path}")
|
||||
return discovered_modules
|
||||
|
||||
|
||||
logger.info(f"Discovering modules in: {modules_dir.absolute()}")
|
||||
|
||||
|
||||
for module_dir in modules_dir.iterdir():
|
||||
if not module_dir.is_dir():
|
||||
continue
|
||||
|
||||
|
||||
manifest_path = module_dir / "module.yaml"
|
||||
if not manifest_path.exists():
|
||||
# Try module.yml as fallback
|
||||
@@ -91,44 +94,48 @@ class ModuleConfigManager:
|
||||
if not manifest_path.exists():
|
||||
# Check if it's a legacy module (has main.py but no manifest)
|
||||
if (module_dir / "main.py").exists():
|
||||
logger.info(f"Legacy module found (no manifest): {module_dir.name}")
|
||||
logger.info(
|
||||
f"Legacy module found (no manifest): {module_dir.name}"
|
||||
)
|
||||
# Create a basic manifest for legacy modules
|
||||
manifest = ModuleManifest(
|
||||
name=module_dir.name,
|
||||
version="1.0.0",
|
||||
description=f"Legacy {module_dir.name} module",
|
||||
author="System",
|
||||
category="legacy"
|
||||
category="legacy",
|
||||
)
|
||||
discovered_modules[manifest.name] = manifest
|
||||
continue
|
||||
|
||||
|
||||
try:
|
||||
manifest = await self._load_module_manifest(manifest_path)
|
||||
discovered_modules[manifest.name] = manifest
|
||||
logger.info(f"Discovered module: {manifest.name} v{manifest.version}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load manifest for {module_dir.name}: {e}")
|
||||
continue
|
||||
|
||||
|
||||
self.manifests = discovered_modules
|
||||
return discovered_modules
|
||||
|
||||
|
||||
async def _load_module_manifest(self, manifest_path: Path) -> ModuleManifest:
|
||||
"""Load and validate a module manifest file"""
|
||||
try:
|
||||
with open(manifest_path, 'r', encoding='utf-8') as f:
|
||||
with open(manifest_path, "r", encoding="utf-8") as f:
|
||||
manifest_data = yaml.safe_load(f)
|
||||
|
||||
|
||||
# Validate required fields
|
||||
required_fields = ['name', 'version', 'description', 'author']
|
||||
required_fields = ["name", "version", "description", "author"]
|
||||
for field in required_fields:
|
||||
if field not in manifest_data:
|
||||
raise ConfigurationError(f"Missing required field '{field}' in {manifest_path}")
|
||||
|
||||
raise ConfigurationError(
|
||||
f"Missing required field '{field}' in {manifest_path}"
|
||||
)
|
||||
|
||||
manifest = ModuleManifest(**manifest_data)
|
||||
|
||||
|
||||
# Load configuration schema if specified
|
||||
if manifest.config_schema:
|
||||
schema_path = manifest_path.parent / manifest.config_schema
|
||||
@@ -136,161 +143,170 @@ class ModuleConfigManager:
|
||||
await self._load_module_schema(manifest.name, schema_path)
|
||||
else:
|
||||
logger.warning(f"Config schema not found: {schema_path}")
|
||||
|
||||
|
||||
return manifest
|
||||
|
||||
|
||||
except yaml.YAMLError as e:
|
||||
raise ConfigurationError(f"Invalid YAML in {manifest_path}: {e}")
|
||||
except Exception as e:
|
||||
raise ConfigurationError(f"Failed to load manifest {manifest_path}: {e}")
|
||||
|
||||
|
||||
async def _load_module_schema(self, module_name: str, schema_path: Path):
|
||||
"""Load JSON schema for module configuration"""
|
||||
try:
|
||||
with open(schema_path, 'r', encoding='utf-8') as f:
|
||||
with open(schema_path, "r", encoding="utf-8") as f:
|
||||
schema = json.load(f)
|
||||
|
||||
|
||||
self.schemas[module_name] = schema
|
||||
logger.info(f"Loaded configuration schema for module: {module_name}")
|
||||
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
raise ConfigurationError(f"Invalid JSON schema in {schema_path}: {e}")
|
||||
except Exception as e:
|
||||
raise ConfigurationError(f"Failed to load schema {schema_path}: {e}")
|
||||
|
||||
|
||||
def get_module_manifest(self, module_name: str) -> Optional[ModuleManifest]:
|
||||
"""Get module manifest by name"""
|
||||
return self.manifests.get(module_name)
|
||||
|
||||
|
||||
def get_module_schema(self, module_name: str) -> Optional[Dict]:
|
||||
"""Get configuration schema for a module"""
|
||||
return self.schemas.get(module_name)
|
||||
|
||||
|
||||
def get_module_config(self, module_name: str) -> Dict:
|
||||
"""Get current configuration for a module"""
|
||||
return self.configs.get(module_name, {})
|
||||
|
||||
|
||||
async def validate_config(self, module_name: str, config: Dict) -> Dict:
|
||||
"""Validate module configuration against its schema"""
|
||||
schema = self.schemas.get(module_name)
|
||||
if not schema:
|
||||
logger.info(f"No schema found for module {module_name}, skipping validation")
|
||||
logger.info(
|
||||
f"No schema found for module {module_name}, skipping validation"
|
||||
)
|
||||
return {"valid": True, "errors": []}
|
||||
|
||||
|
||||
try:
|
||||
validate(instance=config, schema=schema, format_checker=draft7_format_checker)
|
||||
validate(
|
||||
instance=config, schema=schema, format_checker=draft7_format_checker
|
||||
)
|
||||
return {"valid": True, "errors": []}
|
||||
|
||||
|
||||
except ValidationError as e:
|
||||
return {
|
||||
"valid": False,
|
||||
"errors": [{
|
||||
"path": list(e.path),
|
||||
"message": e.message,
|
||||
"invalid_value": e.instance
|
||||
}]
|
||||
"errors": [
|
||||
{
|
||||
"path": list(e.path),
|
||||
"message": e.message,
|
||||
"invalid_value": e.instance,
|
||||
}
|
||||
],
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"valid": False,
|
||||
"errors": [{"message": f"Schema validation failed: {str(e)}"}]
|
||||
"errors": [{"message": f"Schema validation failed: {str(e)}"}],
|
||||
}
|
||||
|
||||
|
||||
async def save_module_config(self, module_name: str, config: Dict) -> bool:
|
||||
"""Save module configuration"""
|
||||
# Validate configuration first
|
||||
validation_result = await self.validate_config(module_name, config)
|
||||
if not validation_result["valid"]:
|
||||
error_messages = [error["message"] for error in validation_result["errors"]]
|
||||
raise ConfigurationError(f"Invalid configuration for {module_name}: {', '.join(error_messages)}")
|
||||
|
||||
raise ConfigurationError(
|
||||
f"Invalid configuration for {module_name}: {', '.join(error_messages)}"
|
||||
)
|
||||
|
||||
# Save configuration
|
||||
self.configs[module_name] = config
|
||||
|
||||
|
||||
# In production, this would persist to database
|
||||
# For now, we'll save to a local JSON file
|
||||
config_dir = Path("backend/storage/module_configs")
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
config_file = config_dir / f"{module_name}.json"
|
||||
try:
|
||||
with open(config_file, 'w', encoding='utf-8') as f:
|
||||
with open(config_file, "w", encoding="utf-8") as f:
|
||||
json.dump(config, f, indent=2)
|
||||
|
||||
|
||||
logger.info(f"Saved configuration for module: {module_name}")
|
||||
return True
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save config for {module_name}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def load_saved_configs(self):
|
||||
"""Load previously saved module configurations"""
|
||||
config_dir = Path("backend/storage/module_configs")
|
||||
if not config_dir.exists():
|
||||
return
|
||||
|
||||
|
||||
for config_file in config_dir.glob("*.json"):
|
||||
module_name = config_file.stem
|
||||
try:
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
with open(config_file, "r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
|
||||
|
||||
self.configs[module_name] = config
|
||||
logger.info(f"Loaded saved configuration for module: {module_name}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load saved config for {module_name}: {e}")
|
||||
|
||||
|
||||
def list_available_modules(self) -> List[Dict]:
|
||||
"""List all discovered modules with their metadata"""
|
||||
modules = []
|
||||
for name, manifest in self.manifests.items():
|
||||
modules.append({
|
||||
"name": manifest.name,
|
||||
"version": manifest.version,
|
||||
"description": manifest.description,
|
||||
"author": manifest.author,
|
||||
"category": manifest.category,
|
||||
"enabled": manifest.enabled,
|
||||
"dependencies": manifest.dependencies,
|
||||
"provides": manifest.provides,
|
||||
"consumes": manifest.consumes,
|
||||
"has_schema": name in self.schemas,
|
||||
"has_config": name in self.configs,
|
||||
"ui_config": manifest.ui_config
|
||||
})
|
||||
|
||||
modules.append(
|
||||
{
|
||||
"name": manifest.name,
|
||||
"version": manifest.version,
|
||||
"description": manifest.description,
|
||||
"author": manifest.author,
|
||||
"category": manifest.category,
|
||||
"enabled": manifest.enabled,
|
||||
"dependencies": manifest.dependencies,
|
||||
"provides": manifest.provides,
|
||||
"consumes": manifest.consumes,
|
||||
"has_schema": name in self.schemas,
|
||||
"has_config": name in self.configs,
|
||||
"ui_config": manifest.ui_config,
|
||||
}
|
||||
)
|
||||
|
||||
return modules
|
||||
|
||||
|
||||
|
||||
async def update_module_status(self, module_name: str, enabled: bool) -> bool:
|
||||
"""Update module enabled status"""
|
||||
manifest = self.manifests.get(module_name)
|
||||
if not manifest:
|
||||
return False
|
||||
|
||||
|
||||
manifest.enabled = enabled
|
||||
|
||||
|
||||
# Update the manifest file
|
||||
modules_dir = Path("modules")
|
||||
manifest_path = modules_dir / module_name / "module.yaml"
|
||||
|
||||
|
||||
if manifest_path.exists():
|
||||
try:
|
||||
manifest_dict = asdict(manifest)
|
||||
with open(manifest_path, 'w', encoding='utf-8') as f:
|
||||
with open(manifest_path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(manifest_dict, f, default_flow_style=False)
|
||||
|
||||
|
||||
logger.info(f"Updated module status: {module_name} enabled={enabled}")
|
||||
return True
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update manifest for {module_name}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# Global module config manager instance
|
||||
module_config_manager = ModuleConfigManager()
|
||||
module_config_manager = ModuleConfigManager()
|
||||
|
||||
Reference in New Issue
Block a user