- Auto-fixed 4,496 lint issues (import sorting, modern syntax, etc.) - Added ignore rules for patterns intentional in this codebase: E402 (late imports), E712 (SQLAlchemy filters), B904 (raise from), SIM108/SIM105/SIM117 (readability preferences) - Added per-file ignores for tests and scripts - Excluded broken scripts/rename_terminology.py (has curly quotes) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
327 lines
9.0 KiB
Python
327 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 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",
|
|
]
|