""" 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 på 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 på 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()