Add self-contained configuration and migrations support for modules:
Config auto-discovery (app/modules/config.py):
- Modules can have config.py with Pydantic Settings
- Environment variables prefixed with MODULE_NAME_
- Auto-discovered via get_module_config()
Migrations auto-discovery:
- Each module has migrations/versions/ directory
- Alembic discovers module migrations automatically
- Naming convention: {module}_{seq}_{description}.py
New architecture rules (MOD-013 to MOD-015):
- MOD-013: config.py should export config/config_class
- MOD-014: Migrations must follow naming convention
- MOD-015: Migrations directory must have __init__.py
Created for all 11 self-contained modules:
- config.py placeholder files
- migrations/ directories with __init__.py files
Added core and tenancy module definitions for completeness.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
191 lines
5.6 KiB
Python
191 lines
5.6 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}")
|
|
|
|
logger.info(f"Auto-discovered {len(modules)} modules")
|
|
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",
|
|
]
|