mega changes

This commit is contained in:
2025-11-20 11:11:18 +01:00
parent e070c95190
commit 841d79f26b
138 changed files with 21499 additions and 8844 deletions

View File

@@ -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()