feat: implement three-tier module classification and framework layer
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>
This commit is contained in:
325
app/modules/events.py
Normal file
325
app/modules/events.py
Normal file
@@ -0,0 +1,325 @@
|
||||
# app/modules/events.py
|
||||
"""
|
||||
Module event bus for lifecycle events.
|
||||
|
||||
Provides a way for different parts of the application to subscribe to
|
||||
module lifecycle events (enable, disable, startup, shutdown, config changes).
|
||||
|
||||
Usage:
|
||||
from app.modules.events import module_event_bus, ModuleEvent
|
||||
|
||||
# Subscribe to events
|
||||
@module_event_bus.subscribe(ModuleEvent.ENABLED)
|
||||
def on_module_enabled(event_data: ModuleEventData):
|
||||
print(f"Module {event_data.module_code} enabled for platform {event_data.platform_id}")
|
||||
|
||||
# Or subscribe manually
|
||||
module_event_bus.subscribe(ModuleEvent.DISABLED, my_handler)
|
||||
|
||||
# Emit events (typically done by ModuleService)
|
||||
module_event_bus.emit(ModuleEvent.ENABLED, ModuleEventData(
|
||||
module_code="billing",
|
||||
platform_id=1,
|
||||
user_id=42,
|
||||
))
|
||||
"""
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from typing import Any, Callable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ModuleEvent(str, Enum):
|
||||
"""Module lifecycle events."""
|
||||
|
||||
# Module enablement
|
||||
ENABLED = "enabled" # Module enabled for a platform
|
||||
DISABLED = "disabled" # Module disabled for a platform
|
||||
|
||||
# Application lifecycle
|
||||
STARTUP = "startup" # Application starting, module initializing
|
||||
SHUTDOWN = "shutdown" # Application shutting down
|
||||
|
||||
# Configuration
|
||||
CONFIG_CHANGED = "config_changed" # Module configuration updated
|
||||
|
||||
# Health
|
||||
HEALTH_CHECK = "health_check" # Health check performed
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModuleEventData:
|
||||
"""
|
||||
Data passed with module events.
|
||||
|
||||
Attributes:
|
||||
module_code: The module code (e.g., "billing", "cms")
|
||||
platform_id: Platform ID if event is platform-specific
|
||||
user_id: User ID if action was user-initiated
|
||||
config: Configuration data (for CONFIG_CHANGED)
|
||||
metadata: Additional event-specific data
|
||||
timestamp: When the event occurred
|
||||
"""
|
||||
|
||||
module_code: str
|
||||
platform_id: int | None = None
|
||||
user_id: int | None = None
|
||||
config: dict[str, Any] | None = None
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
|
||||
# Type alias for event handlers
|
||||
EventHandler = Callable[[ModuleEventData], None]
|
||||
|
||||
|
||||
class ModuleEventBus:
|
||||
"""
|
||||
Event bus for module lifecycle events.
|
||||
|
||||
Allows components to subscribe to and emit module events.
|
||||
This enables loose coupling between the module system and other
|
||||
parts of the application that need to react to module changes.
|
||||
|
||||
Example:
|
||||
# In a service that needs to react to module changes
|
||||
@module_event_bus.subscribe(ModuleEvent.ENABLED)
|
||||
def on_billing_enabled(data: ModuleEventData):
|
||||
if data.module_code == "billing":
|
||||
setup_stripe_webhook(data.platform_id)
|
||||
|
||||
# Or subscribe to multiple events
|
||||
@module_event_bus.subscribe(ModuleEvent.ENABLED, ModuleEvent.DISABLED)
|
||||
def on_module_change(data: ModuleEventData):
|
||||
clear_menu_cache(data.platform_id)
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._handlers: dict[ModuleEvent, list[EventHandler]] = {
|
||||
event: [] for event in ModuleEvent
|
||||
}
|
||||
self._global_handlers: list[EventHandler] = []
|
||||
|
||||
def subscribe(
|
||||
self,
|
||||
*events: ModuleEvent,
|
||||
) -> Callable[[EventHandler], EventHandler]:
|
||||
"""
|
||||
Decorator to subscribe a handler to one or more events.
|
||||
|
||||
Args:
|
||||
events: Events to subscribe to. If empty, subscribes to all events.
|
||||
|
||||
Returns:
|
||||
Decorator function
|
||||
|
||||
Example:
|
||||
@module_event_bus.subscribe(ModuleEvent.ENABLED)
|
||||
def on_enabled(data: ModuleEventData):
|
||||
print(f"Module {data.module_code} enabled")
|
||||
"""
|
||||
|
||||
def decorator(handler: EventHandler) -> EventHandler:
|
||||
if not events:
|
||||
# Subscribe to all events
|
||||
self._global_handlers.append(handler)
|
||||
else:
|
||||
for event in events:
|
||||
self._handlers[event].append(handler)
|
||||
return handler
|
||||
|
||||
return decorator
|
||||
|
||||
def subscribe_handler(
|
||||
self,
|
||||
event: ModuleEvent,
|
||||
handler: EventHandler,
|
||||
) -> None:
|
||||
"""
|
||||
Subscribe a handler to a specific event.
|
||||
|
||||
Args:
|
||||
event: Event to subscribe to
|
||||
handler: Handler function to call
|
||||
"""
|
||||
self._handlers[event].append(handler)
|
||||
|
||||
def unsubscribe(
|
||||
self,
|
||||
event: ModuleEvent,
|
||||
handler: EventHandler,
|
||||
) -> bool:
|
||||
"""
|
||||
Unsubscribe a handler from an event.
|
||||
|
||||
Args:
|
||||
event: Event to unsubscribe from
|
||||
handler: Handler function to remove
|
||||
|
||||
Returns:
|
||||
True if handler was found and removed
|
||||
"""
|
||||
try:
|
||||
self._handlers[event].remove(handler)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def emit(
|
||||
self,
|
||||
event: ModuleEvent,
|
||||
data: ModuleEventData,
|
||||
) -> None:
|
||||
"""
|
||||
Emit an event to all subscribed handlers.
|
||||
|
||||
Args:
|
||||
event: Event type to emit
|
||||
data: Event data to pass to handlers
|
||||
|
||||
Note:
|
||||
Handlers are called synchronously in registration order.
|
||||
Exceptions in handlers are logged but don't prevent other
|
||||
handlers from being called.
|
||||
"""
|
||||
logger.debug(
|
||||
f"Emitting {event.value} event for module {data.module_code}"
|
||||
+ (f" on platform {data.platform_id}" if data.platform_id else "")
|
||||
)
|
||||
|
||||
# Call event-specific handlers
|
||||
for handler in self._handlers[event]:
|
||||
try:
|
||||
handler(data)
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
f"Error in {event.value} handler {handler.__name__}: {e}"
|
||||
)
|
||||
|
||||
# Call global handlers
|
||||
for handler in self._global_handlers:
|
||||
try:
|
||||
handler(data)
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
f"Error in global handler {handler.__name__}: {e}"
|
||||
)
|
||||
|
||||
def emit_enabled(
|
||||
self,
|
||||
module_code: str,
|
||||
platform_id: int,
|
||||
user_id: int | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Convenience method to emit an ENABLED event.
|
||||
|
||||
Args:
|
||||
module_code: Module that was enabled
|
||||
platform_id: Platform it was enabled for
|
||||
user_id: User who enabled it
|
||||
"""
|
||||
self.emit(
|
||||
ModuleEvent.ENABLED,
|
||||
ModuleEventData(
|
||||
module_code=module_code,
|
||||
platform_id=platform_id,
|
||||
user_id=user_id,
|
||||
),
|
||||
)
|
||||
|
||||
def emit_disabled(
|
||||
self,
|
||||
module_code: str,
|
||||
platform_id: int,
|
||||
user_id: int | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Convenience method to emit a DISABLED event.
|
||||
|
||||
Args:
|
||||
module_code: Module that was disabled
|
||||
platform_id: Platform it was disabled for
|
||||
user_id: User who disabled it
|
||||
"""
|
||||
self.emit(
|
||||
ModuleEvent.DISABLED,
|
||||
ModuleEventData(
|
||||
module_code=module_code,
|
||||
platform_id=platform_id,
|
||||
user_id=user_id,
|
||||
),
|
||||
)
|
||||
|
||||
def emit_startup(self, module_code: str) -> None:
|
||||
"""
|
||||
Convenience method to emit a STARTUP event.
|
||||
|
||||
Args:
|
||||
module_code: Module that is starting up
|
||||
"""
|
||||
self.emit(
|
||||
ModuleEvent.STARTUP,
|
||||
ModuleEventData(module_code=module_code),
|
||||
)
|
||||
|
||||
def emit_shutdown(self, module_code: str) -> None:
|
||||
"""
|
||||
Convenience method to emit a SHUTDOWN event.
|
||||
|
||||
Args:
|
||||
module_code: Module that is shutting down
|
||||
"""
|
||||
self.emit(
|
||||
ModuleEvent.SHUTDOWN,
|
||||
ModuleEventData(module_code=module_code),
|
||||
)
|
||||
|
||||
def emit_config_changed(
|
||||
self,
|
||||
module_code: str,
|
||||
platform_id: int,
|
||||
config: dict[str, Any],
|
||||
user_id: int | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Convenience method to emit a CONFIG_CHANGED event.
|
||||
|
||||
Args:
|
||||
module_code: Module whose config changed
|
||||
platform_id: Platform the config belongs to
|
||||
config: New configuration values
|
||||
user_id: User who changed the config
|
||||
"""
|
||||
self.emit(
|
||||
ModuleEvent.CONFIG_CHANGED,
|
||||
ModuleEventData(
|
||||
module_code=module_code,
|
||||
platform_id=platform_id,
|
||||
config=config,
|
||||
user_id=user_id,
|
||||
),
|
||||
)
|
||||
|
||||
def clear_handlers(self) -> None:
|
||||
"""Clear all handlers. Useful for testing."""
|
||||
for event in ModuleEvent:
|
||||
self._handlers[event] = []
|
||||
self._global_handlers = []
|
||||
|
||||
|
||||
# Global event bus instance
|
||||
module_event_bus = ModuleEventBus()
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ModuleEvent",
|
||||
"ModuleEventData",
|
||||
"ModuleEventBus",
|
||||
"EventHandler",
|
||||
"module_event_bus",
|
||||
]
|
||||
Reference in New Issue
Block a user