# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ Feature Registry for Plug&Play Feature Container Loading. Dynamically discovers and loads feature containers from the features directory. """ import os import glob import importlib import logging from typing import List, Dict, Any from fastapi import FastAPI logger = logging.getLogger(__name__) # Path to the features directory FEATURES_DIR = os.path.dirname(os.path.abspath(__file__)) def discoverFeatureContainers() -> List[str]: """ Discover all feature container directories by filename pattern. A valid feature container has a routeFeature*.py file. """ containers = [] pattern = os.path.join(FEATURES_DIR, "*", "routeFeature*.py") for filepath in glob.glob(pattern): featureDir = os.path.basename(os.path.dirname(filepath)) if featureDir not in containers and not featureDir.startswith("_"): containers.append(featureDir) return sorted(containers) def loadFeatureRouters(app: FastAPI) -> Dict[str, Any]: """ Dynamically load and register routers from all discovered feature containers. Also registers feature template roles and AccessRules in the database. """ results = {} pattern = os.path.join(FEATURES_DIR, "*", "routeFeature*.py") for filepath in glob.glob(pattern): featureDir = os.path.basename(os.path.dirname(filepath)) routerFile = os.path.basename(filepath)[:-3] # Remove .py if featureDir.startswith("_"): continue try: modulePath = f"modules.features.{featureDir}.{routerFile}" module = importlib.import_module(modulePath) if hasattr(module, "router"): app.include_router(module.router) logger.info(f"Loaded router: {featureDir}") results[featureDir] = {"status": "loaded", "module": modulePath} else: logger.warning(f"No 'router' in {modulePath}") results[featureDir] = {"status": "no_router_object"} except Exception as e: logger.error(f"Failed to load router from {featureDir}: {e}") results[featureDir] = {"status": "error", "error": str(e)} # Register features in RBAC catalog and sync template roles to database from modules.security.rbacCatalog import getCatalogService catalogService = getCatalogService() registrationResults = registerAllFeaturesInCatalog(catalogService) for featureName, success in registrationResults.items(): if featureName in results: results[featureName]["rbac_registered"] = success return results def loadFeatureMainModules() -> Dict[str, Any]: """ Dynamically load main modules from all discovered feature containers. """ mainModules = {} pattern = os.path.join(FEATURES_DIR, "*", "main*.py") for filepath in glob.glob(pattern): filename = os.path.basename(filepath) if filename == "__init__.py": continue featureDir = os.path.basename(os.path.dirname(filepath)) if featureDir.startswith("_"): continue mainFile = filename[:-3] # Remove .py try: modulePath = f"modules.features.{featureDir}.{mainFile}" module = importlib.import_module(modulePath) mainModules[featureDir] = module logger.debug(f"Loaded main module: {featureDir}") except Exception as e: logger.error(f"Failed to load main module from {featureDir}: {e}") return mainModules def registerAllFeaturesInCatalog(catalogService) -> Dict[str, bool]: """ Register all features' RBAC objects in the catalog. """ mainModules = loadFeatureMainModules() results = {} for featureName, module in mainModules.items(): if hasattr(module, "registerFeature"): try: success = module.registerFeature(catalogService) results[featureName] = success if success: logger.info(f"Registered RBAC objects: {featureName}") except Exception as e: logger.error(f"Error registering {featureName}: {e}") results[featureName] = False return results