Files
orion/app/modules/registry.py
Samir Boulahtit d7a0ff8818 refactor: complete module-driven architecture migration
This commit completes the migration to a fully module-driven architecture:

## Models Migration
- Moved all domain models from models/database/ to their respective modules:
  - tenancy: User, Admin, Vendor, Company, Platform, VendorDomain, etc.
  - cms: MediaFile, VendorTheme
  - messaging: Email, VendorEmailSettings, VendorEmailTemplate
  - core: AdminMenuConfig
- models/database/ now only contains Base and TimestampMixin (infrastructure)

## Schemas Migration
- Moved all domain schemas from models/schema/ to their respective modules:
  - tenancy: company, vendor, admin, team, vendor_domain
  - cms: media, image, vendor_theme
  - messaging: email
- models/schema/ now only contains base.py and auth.py (infrastructure)

## Routes Migration
- Moved admin routes from app/api/v1/admin/ to modules:
  - menu_config.py -> core module
  - modules.py -> tenancy module
  - module_config.py -> tenancy module
- app/api/v1/admin/ now only aggregates auto-discovered module routes

## Menu System
- Implemented module-driven menu system with MenuDiscoveryService
- Extended FrontendType enum: PLATFORM, ADMIN, VENDOR, STOREFRONT
- Added MenuItemDefinition and MenuSectionDefinition dataclasses
- Each module now defines its own menu items in definition.py
- MenuService integrates with MenuDiscoveryService for template rendering

## Documentation
- Updated docs/architecture/models-structure.md
- Updated docs/architecture/menu-management.md
- Updated architecture validation rules for new exceptions

## Architecture Validation
- Updated MOD-019 rule to allow base.py in models/schema/
- Created core module exceptions.py and schemas/ directory
- All validation errors resolved (only warnings remain)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 21:02:56 +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 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 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",
]