# app/modules/registry.py """ Module registry - AUTO-DISCOVERED. All modules are automatically discovered from app/modules/*/definition.py. No manual imports needed - just create a module directory with definition.py. The module system uses a three-tier classification: 1. CORE MODULES - Always enabled, cannot be disabled (is_core=True) - core: Dashboard, settings, profile - tenancy: Platform, merchant, store, admin user management - cms: Content pages, media library, themes - customers: Customer database, profiles, segmentation - billing: Platform subscriptions, store invoices (requires: payments) - messaging: Messages, notifications, email delivery - payments: Payment gateway integrations (Stripe, PayPal, etc.) - contracts: Cross-module protocols and interfaces 2. OPTIONAL MODULES - Can be enabled/disabled per platform (default) - inventory: Stock management, locations - catalog: Product catalog, translations, media - orders: Order management, customer checkout (requires: payments) - marketplace: Letzshop integration (requires: inventory, catalog) - analytics: Reports, dashboards - cart: Shopping cart (session-based) - checkout: Checkout flow (requires: cart, orders) - loyalty: Loyalty programs, stamps, points, digital wallets 3. INTERNAL MODULES - Admin-only tools, not customer-facing (is_internal=True) - dev_tools: Component library, icons, code quality - monitoring: Logs, background tasks, Flower link, Grafana dashboards To add a new module: 1. Create app/modules// directory 2. Add definition.py with ModuleDefinition instance 3. Set is_core=True or is_internal=True for non-optional modules 4. That's it! Module will be auto-discovered. """ import logging from functools import lru_cache from app.modules.base import ModuleDefinition from app.modules.discovery import discover_modules, discover_modules_by_tier from app.modules.enums import FrontendType logger = logging.getLogger(__name__) # ============================================================================= # Auto-Discovered Module Registry # ============================================================================= @lru_cache(maxsize=1) def _get_all_modules() -> dict[str, ModuleDefinition]: """Get all modules (cached).""" return discover_modules() @lru_cache(maxsize=1) def _get_modules_by_tier() -> dict[str, dict[str, ModuleDefinition]]: """Get modules organized by tier (cached).""" return discover_modules_by_tier() # Expose as module-level variables via lazy loading to avoid circular imports # These are computed on first access using Python's module __getattr__ def __getattr__(name: str): """Lazy module-level attribute access (avoids circular imports at import time).""" if name == "MODULES": return _get_all_modules() if name == "CORE_MODULES": return _get_modules_by_tier()["core"] if name == "OPTIONAL_MODULES": return _get_modules_by_tier()["optional"] if name == "INTERNAL_MODULES": return _get_modules_by_tier()["internal"] raise AttributeError(f"module {__name__!r} has no attribute {name!r}") # ============================================================================= # Helper Functions # ============================================================================= def get_module(code: str) -> ModuleDefinition | None: """Get a module definition by code.""" return _get_all_modules().get(code) def get_core_modules() -> list[ModuleDefinition]: """Get all core modules (cannot be disabled).""" return list(_get_modules_by_tier()["core"].values()) def get_core_module_codes() -> set[str]: """Get codes of all core modules.""" return set(_get_modules_by_tier()["core"].keys()) def get_optional_modules() -> list[ModuleDefinition]: """Get all optional modules (can be enabled/disabled).""" return list(_get_modules_by_tier()["optional"].values()) def get_optional_module_codes() -> set[str]: """Get codes of all optional modules.""" return set(_get_modules_by_tier()["optional"].keys()) def get_internal_modules() -> list[ModuleDefinition]: """Get all internal modules (admin-only tools).""" return list(_get_modules_by_tier()["internal"].values()) def get_internal_module_codes() -> set[str]: """Get codes of all internal modules.""" return set(_get_modules_by_tier()["internal"].keys()) def get_all_module_codes() -> set[str]: """Get all module codes.""" return set(_get_all_modules().keys()) def is_core_module(code: str) -> bool: """Check if a module is a core module.""" return code in _get_modules_by_tier()["core"] def is_internal_module(code: str) -> bool: """Check if a module is an internal module.""" return code in _get_modules_by_tier()["internal"] def get_menu_item_module(menu_item_id: str, frontend_type: FrontendType) -> str | None: """ Find which module provides a specific menu item. Args: menu_item_id: The menu item ID to find frontend_type: The frontend type to search in Returns: Module code if found, None otherwise """ for module in _get_all_modules().values(): if menu_item_id in module.get_menu_items(frontend_type): return module.code return None def get_feature_module(feature_code: str) -> str | None: """ Find which module provides a specific feature. Args: feature_code: The feature code to find Returns: Module code if found, None otherwise """ for module in _get_all_modules().values(): if module.has_feature(feature_code): return module.code return None def validate_module_dependencies() -> list[str]: """ Validate that all module dependencies are valid. Returns: List of error messages for invalid dependencies """ errors = [] all_codes = get_all_module_codes() core_codes = get_core_module_codes() for module in _get_all_modules().values(): for required in module.requires: if required not in all_codes: errors.append( f"Module '{module.code}' requires unknown module '{required}'" ) # Core modules should not depend on optional modules if module.is_core and required not in core_codes: errors.append( f"Core module '{module.code}' depends on optional module '{required}'" ) return errors def get_modules_by_tier() -> dict[str, list[ModuleDefinition]]: """ Get modules organized by tier. Returns: Dict with keys 'core', 'optional', 'internal' mapping to module lists """ by_tier = _get_modules_by_tier() return { "core": list(by_tier["core"].values()), "optional": list(by_tier["optional"].values()), "internal": list(by_tier["internal"].values()), } def get_module_tier(code: str) -> str | None: """ Get the tier classification of a module. Args: code: Module code Returns: 'core', 'optional', 'internal', or None if not found """ by_tier = _get_modules_by_tier() if code in by_tier["core"]: return "core" if code in by_tier["optional"]: return "optional" if code in by_tier["internal"]: return "internal" return None def clear_registry_cache(): """Clear the module registry cache. Useful for testing.""" _get_all_modules.cache_clear() _get_modules_by_tier.cache_clear() # ============================================================================= # Validation on Import (Development Check) # ============================================================================= def _run_validation(): """Run validation checks on module registry.""" _validation_errors = validate_module_dependencies() if _validation_errors: import warnings for error in _validation_errors: warnings.warn(f"Module registry validation: {error}", stacklevel=2) # Run validation on import (can be disabled in production) _run_validation() __all__ = [ # Module dictionaries (lazy-loaded) "MODULES", "CORE_MODULES", "OPTIONAL_MODULES", "INTERNAL_MODULES", # Module retrieval "get_module", "get_core_modules", "get_core_module_codes", "get_optional_modules", "get_optional_module_codes", "get_internal_modules", "get_internal_module_codes", "get_all_module_codes", # Module classification "is_core_module", "is_internal_module", "get_modules_by_tier", "get_module_tier", # Menu and feature lookup "get_menu_item_module", "get_feature_module", # Validation "validate_module_dependencies", # Cache management "clear_registry_cache", ]