Files
orion/app/modules/registry.py
Samir Boulahtit aad18c27ab
Some checks failed
CI / ruff (push) Successful in 11s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has started running
refactor: remove all backward compatibility code across 70 files
Clean up 28 backward compatibility instances identified in the codebase.
The app is not live, so all shims are replaced with the target architecture:

- Remove legacy Inventory.location column (use bin_location exclusively)
- Remove dashboard _extract_metric_value helper (use flat metrics dict)
- Remove legacy stat field duplicates (total_stores, total_imports, etc.)
- Remove 13 re-export shims and class aliases across modules
- Remove module-enabling JSON fallback (use PlatformModule junction table)
- Remove menu_to_legacy_format() conversion (return dataclasses directly)
- Remove title/description from MarketplaceProductBase schema
- Clean billing convenience method docstrings
- Clean test fixtures and backward-compat comments
- Add PlatformModule seeding to init_production.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 13:20:29 +01:00

282 lines
8.7 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, 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/<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 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",
]