""" Module Registry for NCAkit Handles automatic discovery and registration of feature modules. """ import importlib import pkgutil import logging from pathlib import Path from typing import List, Dict, Any, Callable, Optional from fastapi import FastAPI logger = logging.getLogger(__name__) class ModuleInfo: """Information about a registered module""" def __init__( self, name: str, prefix: str, description: str = "", register_fn: Callable = None ): self.name = name self.prefix = prefix self.description = description self.register_fn = register_fn class ModuleRegistry: """ Centralized registry for all NCAkit modules. Each module must have an __init__.py with: - MODULE_NAME: str - MODULE_PREFIX: str - MODULE_DESCRIPTION: str (optional) - register(app, config): function """ def __init__(self): self._modules: Dict[str, ModuleInfo] = {} self._initialized: bool = False def discover_modules(self, modules_package: str = "modules") -> List[str]: """ Discover all available modules in the modules package. Returns list of module names. """ discovered = [] try: logger.info(f"Discovering modules in package: {modules_package}") package = importlib.import_module(modules_package) package_path = Path(package.__file__).parent logger.info(f"Module package path: {package_path}") for finder, name, is_pkg in pkgutil.iter_modules([str(package_path)]): logger.info(f"Found: {name} (is_package={is_pkg})") # Skip private/template modules if name.startswith('_'): logger.info(f"Skipping private module: {name}") continue if is_pkg: discovered.append(name) logger.info(f"Discovered module: {name}") except Exception as e: logger.error(f"Error discovering modules: {e}") import traceback logger.error(traceback.format_exc()) logger.info(f"Total discovered: {discovered}") return discovered def load_module(self, module_name: str, modules_package: str = "modules") -> Optional[ModuleInfo]: """Load a single module and return its info""" logger.info(f"[STEP 4] Loading module: {module_name}") try: full_module_name = f"{modules_package}.{module_name}" logger.info(f"[STEP 4.1] Importing: {full_module_name}") module = importlib.import_module(full_module_name) logger.info(f"[STEP 4.2] Import successful: {full_module_name}") # Check required attributes if not hasattr(module, 'register'): logger.warning(f"[STEP 4.3] Module {module_name} has no register function, skipping") return None logger.info(f"[STEP 4.3] register() function found") # Get module metadata name = getattr(module, 'MODULE_NAME', module_name) prefix = getattr(module, 'MODULE_PREFIX', f"/api/{module_name}") description = getattr(module, 'MODULE_DESCRIPTION', "") logger.info(f"[STEP 4.4] Module metadata: name={name}, prefix={prefix}") info = ModuleInfo( name=name, prefix=prefix, description=description, register_fn=module.register ) self._modules[name] = info logger.info(f"[STEP 4.5] Loaded module: {name} (prefix: {prefix})") return info except Exception as e: logger.error(f"[STEP 4.ERROR] Failed to load module {module_name}: {e}") import traceback logger.error(f"[STEP 4.TRACEBACK]\n{traceback.format_exc()}") return None def register_all(self, app: FastAPI, config: Any) -> int: """ Register all discovered modules with the FastAPI app. Returns number of successfully registered modules. """ if self._initialized: logger.warning("Modules already initialized") return len(self._modules) # Discover modules module_names = self.discover_modules() registered = 0 for name in module_names: info = self.load_module(name) if info and info.register_fn: try: info.register_fn(app, config) registered += 1 logger.info(f"Registered module: {info.name}") except Exception as e: logger.error(f"Failed to register module {name}: {e}") self._initialized = True logger.info(f"Registered {registered}/{len(module_names)} modules") return registered def get_module(self, name: str) -> Optional[ModuleInfo]: """Get info about a specific module""" return self._modules.get(name) def list_modules(self) -> List[Dict[str, str]]: """List all registered modules""" return [ { "name": info.name, "prefix": info.prefix, "description": info.description } for info in self._modules.values() ] # Global registry instance registry = ModuleRegistry()