# app/modules/discovery.py """ Module auto-discovery system. Provides automatic discovery of modules from app/modules/*/definition.py. This enables true plug-and-play modules - just create a module directory with a definition.py file and it will be automatically registered. Module Discovery Rules: 1. Module must have app/modules//definition.py 2. definition.py must define a ModuleDefinition instance 3. The instance variable name should be _module (e.g., cms_module) 4. Module tier is determined by is_core and is_internal flags Usage: # Auto-discover all modules (replaces manual imports in registry.py) from app.modules.discovery import discover_modules MODULES = discover_modules() Directory Structure for Auto-Discovery: app/modules/ ├── cms/ │ ├── __init__.py │ ├── definition.py <- Must export ModuleDefinition │ ├── routes/ │ │ ├── api/ │ │ └── pages/ │ ├── services/ │ ├── models/ │ ├── schemas/ │ ├── templates/ │ ├── static/ │ ├── locales/ │ └── tasks/ └── analytics/ └── ... """ import importlib import logging from pathlib import Path from typing import TYPE_CHECKING if TYPE_CHECKING: from app.modules.base import ModuleDefinition logger = logging.getLogger(__name__) # Modules directory MODULES_DIR = Path(__file__).parent def discover_modules() -> dict[str, "ModuleDefinition"]: """ Auto-discover all modules from app/modules/*/definition.py. Scans the modules directory for subdirectories containing definition.py, imports them, and extracts ModuleDefinition instances. Returns: Dictionary mapping module code to ModuleDefinition Example: >>> modules = discover_modules() >>> modules.keys() dict_keys(['cms', 'analytics', 'billing', ...]) """ from app.modules.base import ModuleDefinition modules: dict[str, ModuleDefinition] = {} if not MODULES_DIR.exists(): logger.warning(f"Modules directory not found: {MODULES_DIR}") return modules for module_dir in sorted(MODULES_DIR.iterdir()): # Skip non-directories and special files if not module_dir.is_dir(): continue if module_dir.name.startswith("_") or module_dir.name.startswith("."): continue definition_file = module_dir / "definition.py" if not definition_file.exists(): continue try: # Import the definition module dir_name = module_dir.name import_path = f"app.modules.{dir_name}.definition" definition_module = importlib.import_module(import_path) # Find ModuleDefinition instances in the module for attr_name in dir(definition_module): if attr_name.startswith("_"): continue attr = getattr(definition_module, attr_name) if isinstance(attr, ModuleDefinition): modules[attr.code] = attr logger.debug(f"Discovered module: {attr.code} from {import_path}") break else: logger.warning(f"No ModuleDefinition found in {import_path}") except ImportError as e: logger.error(f"Failed to import {import_path}: {e}") except Exception as e: logger.error(f"Error discovering module in {module_dir}: {e}") # Log summary by tier core = [c for c, m in modules.items() if m.is_core] internal = [c for c, m in modules.items() if m.is_internal] optional = [c for c, m in modules.items() if not m.is_core and not m.is_internal] logger.info( f"[MODULES] Auto-discovered {len(modules)} modules: " f"{len(core)} core ({', '.join(sorted(core))}), " f"{len(optional)} optional ({', '.join(sorted(optional))}), " f"{len(internal)} internal ({', '.join(sorted(internal))})" ) return modules def discover_modules_by_tier() -> dict[str, dict[str, "ModuleDefinition"]]: """ Discover modules and organize by tier. Returns: Dict with keys 'core', 'optional', 'internal' mapping to module dicts Example: >>> by_tier = discover_modules_by_tier() >>> by_tier['core'].keys() dict_keys(['core', 'tenancy', 'cms', 'customers']) """ modules = discover_modules() result = { "core": {}, "optional": {}, "internal": {}, } for code, module in modules.items(): tier = module.get_tier() result[tier][code] = module return result def get_module_codes() -> set[str]: """Get all discovered module codes.""" return set(discover_modules().keys()) def validate_module_discovery() -> list[str]: """ Validate that all module directories have proper structure. Returns: List of validation errors (empty if all valid) """ errors: list[str] = [] if not MODULES_DIR.exists(): return ["Modules directory not found"] for module_dir in sorted(MODULES_DIR.iterdir()): if not module_dir.is_dir(): continue if module_dir.name.startswith("_") or module_dir.name.startswith("."): continue # Check for definition.py definition_file = module_dir / "definition.py" if not definition_file.exists(): # Only warn if it looks like a module (has __init__.py) if (module_dir / "__init__.py").exists(): errors.append( f"Module directory '{module_dir.name}' has __init__.py but no definition.py" ) continue # Check for __init__.py init_file = module_dir / "__init__.py" if not init_file.exists(): errors.append(f"Module '{module_dir.name}' missing __init__.py") return errors __all__ = [ "discover_modules", "discover_modules_by_tier", "get_module_codes", "validate_module_discovery", "MODULES_DIR", ]