# 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 collections.abc import Callable from dataclasses import dataclass, field from datetime import UTC, datetime from enum import Enum from typing import Any 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(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", ]