Files
orion/app/modules/discovery.py
Samir Boulahtit 7fefab1508 feat: add detailed logging for module loading and context providers
Add INFO-level logging to help diagnose module loading issues:

- discovery.py: Log summary of discovered modules by tier (core/optional/internal)
- service.py: Log which modules are enabled for each platform (DEBUG level)
- page_context.py: Log context building with platform info and which
  modules contributed context with key counts

Example log output:
  [MODULES] Auto-discovered 18 modules: 5 core, 11 optional, 2 internal
  [CONTEXT] Building PLATFORM context for platform 'main' with 5 enabled modules
  [CONTEXT] Context providers called: cms(3 keys), billing(3 keys)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 19:32:32 +01:00

201 lines
6.0 KiB
Python

# 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/<code>/definition.py
2. definition.py must define a ModuleDefinition instance
3. The instance variable name should be <code>_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",
]