Module Classification: - Core (4): core, tenancy, cms, customers - always enabled - Optional (7): payments, billing, inventory, orders, marketplace, analytics, messaging - Internal (2): dev-tools, monitoring - admin-only Key Changes: - Rename platform-admin module to tenancy - Promote CMS and Customers to core modules - Create new payments module (gateway abstractions) - Add billing→payments and orders→payments dependencies - Mark dev-tools and monitoring as internal modules New Infrastructure: - app/modules/events.py: Module event bus (ENABLED, DISABLED, STARTUP, SHUTDOWN) - app/modules/migrations.py: Module-specific migration discovery - app/core/observability.py: Health checks, Prometheus metrics, Sentry integration Enhanced ModuleDefinition: - version, is_internal, permissions - config_schema, default_config - migrations_path - Lifecycle hooks: on_enable, on_disable, on_startup, health_check New Registry Functions: - get_optional_module_codes(), get_internal_module_codes() - is_core_module(), is_internal_module() - get_modules_by_tier(), get_module_tier() Migrations: - zc*: Rename platform-admin to tenancy - zd*: Ensure CMS and Customers enabled for all platforms Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
254 lines
7.0 KiB
Python
254 lines
7.0 KiB
Python
# app/modules/migrations.py
|
|
"""
|
|
Module migration discovery utility.
|
|
|
|
Provides utilities for discovering and managing module-specific migrations.
|
|
Each self-contained module can have its own migrations directory that will
|
|
be included in Alembic's version locations.
|
|
|
|
Module Migration Structure:
|
|
app/modules/<code>/
|
|
└── migrations/
|
|
└── versions/
|
|
├── <module>_001_initial.py
|
|
├── <module>_002_add_field.py
|
|
└── ...
|
|
|
|
Migration Naming Convention:
|
|
{module_code}_{sequence}_{description}.py
|
|
Example: cms_001_create_content_pages.py
|
|
|
|
This ensures no collision between modules and makes it clear which
|
|
module owns each migration.
|
|
|
|
Usage:
|
|
# Get all migration paths for Alembic
|
|
from app.modules.migrations import get_all_migration_paths
|
|
|
|
paths = get_all_migration_paths()
|
|
# Returns: [Path("alembic/versions"), Path("app/modules/cms/migrations/versions"), ...]
|
|
|
|
# In alembic/env.py
|
|
context.configure(
|
|
version_locations=[str(p) for p in get_all_migration_paths()],
|
|
...
|
|
)
|
|
"""
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from app.modules.base import ModuleDefinition
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def get_core_migrations_path() -> Path:
|
|
"""
|
|
Get the path to the core (non-module) migrations directory.
|
|
|
|
Returns:
|
|
Path to alembic/versions/
|
|
"""
|
|
# Navigate from app/modules/migrations.py to project root
|
|
project_root = Path(__file__).parent.parent.parent
|
|
return project_root / "alembic" / "versions"
|
|
|
|
|
|
def get_module_migrations_path(module: "ModuleDefinition") -> Path | None:
|
|
"""
|
|
Get the migrations path for a specific module.
|
|
|
|
Args:
|
|
module: Module definition to get migrations path for
|
|
|
|
Returns:
|
|
Path to module's migrations/versions/ directory, or None if not configured
|
|
"""
|
|
migrations_dir = module.get_migrations_dir()
|
|
if migrations_dir is None:
|
|
return None
|
|
|
|
versions_dir = migrations_dir / "versions"
|
|
return versions_dir
|
|
|
|
|
|
def discover_module_migrations() -> list[Path]:
|
|
"""
|
|
Discover all module migration directories.
|
|
|
|
Scans all registered modules for those with migrations_path configured
|
|
and returns paths to their versions directories.
|
|
|
|
Returns:
|
|
List of paths to module migration version directories
|
|
"""
|
|
# Import here to avoid circular imports
|
|
from app.modules.registry import MODULES
|
|
|
|
paths: list[Path] = []
|
|
|
|
for module in MODULES.values():
|
|
if not module.migrations_path:
|
|
continue
|
|
|
|
versions_path = get_module_migrations_path(module)
|
|
if versions_path is None:
|
|
continue
|
|
|
|
if versions_path.exists():
|
|
paths.append(versions_path)
|
|
logger.debug(f"Found migrations for module {module.code}: {versions_path}")
|
|
else:
|
|
logger.debug(
|
|
f"Module {module.code} has migrations_path but no versions directory"
|
|
)
|
|
|
|
return sorted(paths) # Sort for deterministic ordering
|
|
|
|
|
|
def get_all_migration_paths() -> list[Path]:
|
|
"""
|
|
Get all migration paths including core and module migrations.
|
|
|
|
Returns:
|
|
List of paths starting with core migrations, followed by module migrations
|
|
in alphabetical order by module code.
|
|
|
|
Example:
|
|
[
|
|
Path("alembic/versions"),
|
|
Path("app/modules/billing/migrations/versions"),
|
|
Path("app/modules/cms/migrations/versions"),
|
|
]
|
|
"""
|
|
paths = [get_core_migrations_path()]
|
|
paths.extend(discover_module_migrations())
|
|
return paths
|
|
|
|
|
|
def get_migration_order() -> list[str]:
|
|
"""
|
|
Get the order in which module migrations should be applied.
|
|
|
|
Returns migrations in dependency order - modules with no dependencies first,
|
|
then modules that depend on them, etc.
|
|
|
|
Returns:
|
|
List of module codes in migration order
|
|
"""
|
|
# Import here to avoid circular imports
|
|
from app.modules.registry import MODULES
|
|
|
|
# Build dependency graph
|
|
modules_with_migrations = [
|
|
m for m in MODULES.values() if m.migrations_path
|
|
]
|
|
|
|
if not modules_with_migrations:
|
|
return []
|
|
|
|
# Topological sort based on dependencies
|
|
ordered: list[str] = []
|
|
visited: set[str] = set()
|
|
temp_visited: set[str] = set()
|
|
|
|
def visit(code: str) -> None:
|
|
if code in visited:
|
|
return
|
|
if code in temp_visited:
|
|
raise ValueError(f"Circular dependency detected involving {code}")
|
|
|
|
module = MODULES.get(code)
|
|
if module is None:
|
|
return
|
|
|
|
temp_visited.add(code)
|
|
|
|
# Visit dependencies first
|
|
for dep in module.requires:
|
|
if dep in {m.code for m in modules_with_migrations}:
|
|
visit(dep)
|
|
|
|
temp_visited.remove(code)
|
|
visited.add(code)
|
|
|
|
if module.migrations_path:
|
|
ordered.append(code)
|
|
|
|
for module in modules_with_migrations:
|
|
visit(module.code)
|
|
|
|
return ordered
|
|
|
|
|
|
def validate_migration_names() -> list[str]:
|
|
"""
|
|
Validate that all module migrations follow the naming convention.
|
|
|
|
Returns:
|
|
List of validation errors (empty if all valid)
|
|
"""
|
|
errors: list[str] = []
|
|
|
|
for path in discover_module_migrations():
|
|
# Extract module code from path (e.g., app/modules/cms/migrations/versions -> cms)
|
|
module_dir = path.parent.parent # migrations/versions -> migrations -> module
|
|
module_code = module_dir.name.replace("_", "-") # cms or dev_tools -> dev-tools
|
|
|
|
for migration_file in path.glob("*.py"):
|
|
name = migration_file.stem
|
|
if name == "__pycache__":
|
|
continue
|
|
|
|
# Check prefix matches module code
|
|
expected_prefix = module_code.replace("-", "_")
|
|
if not name.startswith(f"{expected_prefix}_"):
|
|
errors.append(
|
|
f"Migration {migration_file} should start with '{expected_prefix}_'"
|
|
)
|
|
|
|
return errors
|
|
|
|
|
|
def create_module_migrations_dir(module: "ModuleDefinition") -> Path:
|
|
"""
|
|
Create the migrations directory structure for a module.
|
|
|
|
Args:
|
|
module: Module to create migrations for
|
|
|
|
Returns:
|
|
Path to the created versions directory
|
|
"""
|
|
module_dir = module.get_module_dir()
|
|
migrations_dir = module_dir / "migrations"
|
|
versions_dir = migrations_dir / "versions"
|
|
|
|
versions_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Create __init__.py files
|
|
init_files = [
|
|
migrations_dir / "__init__.py",
|
|
versions_dir / "__init__.py",
|
|
]
|
|
for init_file in init_files:
|
|
if not init_file.exists():
|
|
init_file.write_text('"""Module migrations."""\n')
|
|
|
|
logger.info(f"Created migrations directory for module {module.code}")
|
|
return versions_dir
|
|
|
|
|
|
__all__ = [
|
|
"get_core_migrations_path",
|
|
"get_module_migrations_path",
|
|
"discover_module_migrations",
|
|
"get_all_migration_paths",
|
|
"get_migration_order",
|
|
"validate_migration_names",
|
|
"create_module_migrations_dir",
|
|
]
|