Files
orion/app/modules/events.py
Samir Boulahtit 1a52611438 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>
2026-01-27 22:02:39 +01:00

326 lines
9.0 KiB
Python

# 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",
]