bmc_hub/app/core/module_loader.py

293 lines
11 KiB
Python
Raw Permalink Normal View History

"""
Module Loader
Dynamisk loading af moduler med hot-reload support
"""
import json
import logging
import importlib
import importlib.util
from pathlib import Path
from typing import Dict, List, Optional
from dataclasses import dataclass
from fastapi import FastAPI, HTTPException
logger = logging.getLogger(__name__)
@dataclass
class ModuleMetadata:
"""Metadata for et modul"""
name: str
version: str
description: str
enabled: bool
author: str
dependencies: List[str]
table_prefix: str
api_prefix: str
tags: List[str]
module_path: Path
class ModuleLoader:
"""
Dynamisk modul loader med hot-reload support
Moduler ligger i app/modules/{module_name}/
Hver modul har en module.json med metadata
"""
def __init__(self, modules_dir: str = "app/modules"):
self.modules_dir = Path(modules_dir)
self.loaded_modules: Dict[str, ModuleMetadata] = {}
self.module_routers: Dict[str, tuple] = {} # name -> (api_router, frontend_router)
def discover_modules(self) -> List[ModuleMetadata]:
"""
Find alle moduler i modules directory
Returns:
Liste af ModuleMetadata objekter
"""
modules = []
if not self.modules_dir.exists():
logger.warning(f"⚠️ Modules directory ikke fundet: {self.modules_dir}")
return modules
for module_dir in self.modules_dir.iterdir():
if not module_dir.is_dir() or module_dir.name.startswith("_"):
continue
manifest_path = module_dir / "module.json"
if not manifest_path.exists():
logger.warning(f"⚠️ Ingen module.json i {module_dir.name}")
continue
try:
with open(manifest_path, 'r', encoding='utf-8') as f:
manifest = json.load(f)
metadata = ModuleMetadata(
name=manifest.get("name", module_dir.name),
version=manifest.get("version", "1.0.0"),
description=manifest.get("description", ""),
enabled=manifest.get("enabled", False),
author=manifest.get("author", "Unknown"),
dependencies=manifest.get("dependencies", []),
table_prefix=manifest.get("table_prefix", f"{module_dir.name}_"),
api_prefix=manifest.get("api_prefix", f"/api/v1/{module_dir.name}"),
tags=manifest.get("tags", [module_dir.name.title()]),
module_path=module_dir
)
modules.append(metadata)
logger.info(f"📦 Fundet modul: {metadata.name} v{metadata.version} (enabled={metadata.enabled})")
except Exception as e:
logger.error(f"❌ Kunne ikke læse manifest for {module_dir.name}: {e}")
return modules
def load_module(self, metadata: ModuleMetadata) -> Optional[tuple]:
"""
Load et enkelt modul (backend + frontend routers)
Args:
metadata: Modul metadata
Returns:
Tuple af (api_router, frontend_router) eller None ved fejl
"""
if not metadata.enabled:
logger.info(f"⏭️ Springer over disabled modul: {metadata.name}")
return None
try:
# Check dependencies
for dep in metadata.dependencies:
if dep not in self.loaded_modules:
logger.warning(f"⚠️ Modul {metadata.name} kræver {dep} (mangler)")
return None
# Import backend router
backend_path = metadata.module_path / "backend" / "router.py"
api_router = None
if backend_path.exists():
spec = importlib.util.spec_from_file_location(
f"app.modules.{metadata.name}.backend.router",
backend_path
)
if spec and spec.loader:
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
if hasattr(module, 'router'):
api_router = module.router
logger.info(f"✅ Loaded API router for {metadata.name}")
# Import frontend views
frontend_path = metadata.module_path / "frontend" / "views.py"
frontend_router = None
if frontend_path.exists():
spec = importlib.util.spec_from_file_location(
f"app.modules.{metadata.name}.frontend.views",
frontend_path
)
if spec and spec.loader:
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
if hasattr(module, 'router'):
frontend_router = module.router
logger.info(f"✅ Loaded frontend router for {metadata.name}")
if api_router is None and frontend_router is None:
logger.warning(f"⚠️ Ingen routers fundet for {metadata.name}")
return None
self.loaded_modules[metadata.name] = metadata
self.module_routers[metadata.name] = (api_router, frontend_router)
logger.info(f"🎉 Modul {metadata.name} loaded successfully")
return (api_router, frontend_router)
except Exception as e:
logger.error(f"❌ Kunne ikke loade modul {metadata.name}: {e}", exc_info=True)
return None
def register_modules(self, app: FastAPI):
"""
Registrer alle enabled moduler i FastAPI app
Args:
app: FastAPI application instance
"""
modules = self.discover_modules()
for metadata in modules:
if not metadata.enabled:
continue
routers = self.load_module(metadata)
if routers is None:
continue
api_router, frontend_router = routers
# Registrer API router
if api_router:
try:
app.include_router(
api_router,
prefix=metadata.api_prefix,
tags=list(metadata.tags) # type: ignore
)
logger.info(f"🔌 Registered API: {metadata.api_prefix}")
except Exception as e:
logger.error(f"❌ Kunne ikke registrere API router for {metadata.name}: {e}")
# Registrer frontend router
if frontend_router:
try:
from typing import cast, List
frontend_tags: List[str] = ["Frontend"] + list(metadata.tags)
app.include_router(
frontend_router,
tags=frontend_tags # type: ignore
)
logger.info(f"🔌 Registered Frontend for {metadata.name}")
except Exception as e:
logger.error(f"❌ Kunne ikke registrere frontend router for {metadata.name}: {e}")
def get_module_status(self) -> Dict[str, dict]:
"""
Hent status for alle loaded moduler
Returns:
Dict med modul navn -> status info
"""
status = {}
for name, metadata in self.loaded_modules.items():
status[name] = {
"name": metadata.name,
"version": metadata.version,
"description": metadata.description,
"enabled": metadata.enabled,
"author": metadata.author,
"table_prefix": metadata.table_prefix,
"api_prefix": metadata.api_prefix,
"has_api": self.module_routers[name][0] is not None,
"has_frontend": self.module_routers[name][1] is not None
}
return status
def enable_module(self, module_name: str) -> bool:
"""
Aktiver et modul (kræver app restart)
Args:
module_name: Navn modul
Returns:
True hvis success
"""
module_dir = self.modules_dir / module_name
manifest_path = module_dir / "module.json"
if not manifest_path.exists():
raise HTTPException(status_code=404, detail=f"Modul {module_name} ikke fundet")
try:
with open(manifest_path, 'r', encoding='utf-8') as f:
manifest = json.load(f)
manifest["enabled"] = True
with open(manifest_path, 'w', encoding='utf-8') as f:
json.dump(manifest, f, indent=2, ensure_ascii=False)
logger.info(f"✅ Modul {module_name} enabled (restart required)")
return True
except Exception as e:
logger.error(f"❌ Kunne ikke enable {module_name}: {e}")
raise HTTPException(status_code=500, detail=str(e))
def disable_module(self, module_name: str) -> bool:
"""
Deaktiver et modul (kræver app restart)
Args:
module_name: Navn modul
Returns:
True hvis success
"""
module_dir = self.modules_dir / module_name
manifest_path = module_dir / "module.json"
if not manifest_path.exists():
raise HTTPException(status_code=404, detail=f"Modul {module_name} ikke fundet")
try:
with open(manifest_path, 'r', encoding='utf-8') as f:
manifest = json.load(f)
manifest["enabled"] = False
with open(manifest_path, 'w', encoding='utf-8') as f:
json.dump(manifest, f, indent=2, ensure_ascii=False)
logger.info(f"⏸️ Modul {module_name} disabled (restart required)")
return True
except Exception as e:
logger.error(f"❌ Kunne ikke disable {module_name}: {e}")
raise HTTPException(status_code=500, detail=str(e))
# Global module loader instance
module_loader = ModuleLoader()