Files
orion/app/modules/registry.py
Samir Boulahtit 37cf74cbf4 refactor: update registry and main.py for module auto-discovery
- app/modules/registry.py: Use auto-discovery from discovery.py
- main.py: Integrate module route auto-discovery
- app/routes/vendor_pages.py: Remove routes now handled by modules

The registry now dynamically discovers modules from definition.py
files instead of hardcoded imports.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 22:22:08 +01:00

277 lines
8.3 KiB
Python

# 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, company, vendor, admin user management
- cms: Content pages, media library, themes
- customers: Customer database, profiles, segmentation
2. OPTIONAL MODULES - Can be enabled/disabled per platform (default)
- payments: Payment gateway integrations (Stripe, PayPal, etc.)
- billing: Platform subscriptions, vendor invoices (requires: payments)
- inventory: Stock management, locations
- orders: Order management, customer checkout (requires: payments)
- marketplace: Letzshop integration (requires: inventory)
- analytics: Reports, dashboards
- messaging: Messages, notifications
3. INTERNAL MODULES - Admin-only tools, not customer-facing (is_internal=True)
- dev-tools: Component library, icons
- monitoring: Logs, background tasks, Flower link, Grafana dashboards
To add a new module:
1. Create app/modules/<code>/ 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 models.database.admin_menu_config 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 for backward compatibility
# These are computed lazily on first access
def __getattr__(name: str):
"""Lazy module-level attribute access for backward compatibility."""
if name == "MODULES":
return _get_all_modules()
elif name == "CORE_MODULES":
return _get_modules_by_tier()["core"]
elif name == "OPTIONAL_MODULES":
return _get_modules_by_tier()["optional"]
elif 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"
elif code in by_tier["optional"]:
return "optional"
elif 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",
]