""" EU-Utility Premium - Event Bus =============================== High-performance async event system for inter-plugin communication. Features: - Async/await support - Event prioritization - Event filtering and routing - Buffered events for high-throughput scenarios - Type-safe event definitions Example: from premium.core.event_bus import EventBus, Event, EventPriority bus = EventBus() # Subscribe to events @bus.on('game.loot') async def handle_loot(event: Event): print(f"Got loot: {event.data}") # Emit events bus.emit('game.loot', {'item': 'Angel Scales', 'value': 150.50}) """ from __future__ import annotations import asyncio import inspect import logging import time import traceback import weakref from abc import ABC, abstractmethod from collections import defaultdict, deque from dataclasses import dataclass, field from enum import Enum, auto from typing import ( Any, Callable, Coroutine, Dict, List, Optional, Set, Type, TypeVar, Union, Generic, Protocol, runtime_checkable ) # ============================================================================= # EVENT PRIORITY # ============================================================================= class EventPriority(Enum): """Priority levels for event handlers. Higher priority handlers are called first. """ CRITICAL = 100 # System-critical events (error handling, shutdown) HIGH = 75 # Important game events (loot, globals) NORMAL = 50 # Standard events LOW = 25 # Background tasks BACKGROUND = 10 # Analytics, logging # ============================================================================= # EVENT CLASS # ============================================================================= @dataclass class Event: """Represents an event in the system. Attributes: type: Event type/category data: Event payload data source: Source of the event (plugin_id or system) timestamp: When the event was created priority: Event priority id: Unique event identifier """ type: str data: Dict[str, Any] = field(default_factory=dict) source: str = "system" timestamp: float = field(default_factory=time.time) priority: EventPriority = EventPriority.NORMAL id: str = field(default_factory=lambda: f"evt_{int(time.time()*1000)}_{id(Event) % 10000}") def get(self, key: str, default: Any = None) -> Any: """Get a value from event data.""" return self.data.get(key, default) def to_dict(self) -> Dict[str, Any]: """Convert event to dictionary.""" return { 'id': self.id, 'type': self.type, 'data': self.data, 'source': self.source, 'timestamp': self.timestamp, 'priority': self.priority.name, } @classmethod def create( cls, event_type: str, data: Dict[str, Any], source: str = "system", priority: EventPriority = EventPriority.NORMAL ) -> Event: """Create a new event.""" return cls( type=event_type, data=data, source=source, priority=priority ) # ============================================================================= # EVENT FILTER # ============================================================================= @dataclass class EventFilter: """Filter for event subscription. Allows subscribers to receive only events matching specific criteria. Example: filter = EventFilter( event_types=['game.loot', 'game.skill'], sources=['game_client'], min_priority=EventPriority.HIGH ) """ event_types: Optional[Set[str]] = None sources: Optional[Set[str]] = None min_priority: Optional[EventPriority] = None data_filter: Optional[Callable[[Dict[str, Any]], bool]] = None def matches(self, event: Event) -> bool: """Check if an event matches this filter.""" if self.event_types and event.type not in self.event_types: return False if self.sources and event.source not in self.sources: return False if self.min_priority and event.priority.value < self.min_priority.value: return False if self.data_filter and not self.data_filter(event.data): return False return True # ============================================================================= # SUBSCRIPTION # ============================================================================= @dataclass class Subscription: """Represents an event subscription.""" id: str event_types: Set[str] callback: Callable[[Event], Any] filter: Optional[EventFilter] priority: EventPriority once: bool def __hash__(self) -> int: """Make subscription hashable (use id only).""" return hash(self.id) def __eq__(self, other) -> bool: """Compare subscriptions by id.""" if not isinstance(other, Subscription): return False return self.id == other.id def matches(self, event: Event) -> bool: """Check if this subscription matches an event.""" if self.event_types and event.type not in self.event_types: return False if self.filter and not self.filter.matches(event): return False return True # ============================================================================= # EVENT BUS # ============================================================================= HandlerType = Callable[[Event], Any] class EventBus: """High-performance async event bus. Features: - Async and sync handler support - Priority-based execution order - Event filtering - Buffered event queues - Wildcard subscriptions Example: bus = EventBus() # Sync handler @bus.on('game.loot') def handle_loot(event): print(f"Loot: {event.data}") # Async handler @bus.on('game.global', priority=EventPriority.HIGH) async def handle_global(event): await notify_discord(event.data) # Emit event bus.emit('game.loot', {'item': 'Uber Item', 'value': 1000}) """ def __init__(self, max_queue_size: int = 10000): """Initialize event bus. Args: max_queue_size: Maximum events in queue before dropping """ self._subscribers: Dict[str, Set[Subscription]] = defaultdict(set) self._wildcard_subscribers: Set[Subscription] = set() self._queue: deque = deque(maxlen=max_queue_size) self._logger = logging.getLogger("EventBus") self._sub_counter = 0 self._running = False self._lock = asyncio.Lock() self._event_count = 0 self._dropped_count = 0 # ========== Subscription Methods ========== def on( self, event_type: Union[str, List[str]], priority: EventPriority = EventPriority.NORMAL, filter: Optional[EventFilter] = None, once: bool = False ) -> Callable[[HandlerType], HandlerType]: """Decorator to subscribe to events. Args: event_type: Event type(s) to subscribe to priority: Handler priority filter: Optional event filter once: Remove after first matching event Example: @bus.on('game.loot') def handle_loot(event): print(event.data) """ def decorator(handler: HandlerType) -> HandlerType: self.subscribe(event_type, handler, priority, filter, once) return handler return decorator def subscribe( self, event_type: Union[str, List[str]], handler: HandlerType, priority: EventPriority = EventPriority.NORMAL, filter: Optional[EventFilter] = None, once: bool = False ) -> str: """Subscribe to events. Args: event_type: Event type(s) to subscribe to, or '*' for all handler: Callback function priority: Handler priority filter: Optional event filter once: Remove after first matching event Returns: Subscription ID """ self._sub_counter += 1 sub_id = f"sub_{self._sub_counter}" # Handle wildcard subscription if event_type == '*': sub = Subscription( id=sub_id, event_types=set(), callback=handler, filter=filter, priority=priority, once=once ) self._wildcard_subscribers.add(sub) return sub_id # Handle single or multiple event types if isinstance(event_type, str): event_types = {event_type} else: event_types = set(event_type) sub = Subscription( id=sub_id, event_types=event_types, callback=handler, filter=filter, priority=priority, once=once ) for et in event_types: self._subscribers[et].add(sub) return sub_id def unsubscribe(self, subscription_id: str) -> bool: """Unsubscribe from events. Args: subscription_id: ID returned by subscribe() Returns: True if subscription was found and removed """ # Check wildcard subscribers for sub in self._wildcard_subscribers: if sub.id == subscription_id: self._wildcard_subscribers.remove(sub) return True # Check typed subscribers for event_type, subs in self._subscribers.items(): for sub in list(subs): if sub.id == subscription_id: subs.remove(sub) return True return False def once( self, event_type: str, handler: HandlerType, priority: EventPriority = EventPriority.NORMAL, timeout: Optional[float] = None ) -> asyncio.Future: """Subscribe to a single event. Args: event_type: Event type to wait for handler: Optional handler callback priority: Handler priority timeout: Optional timeout in seconds Returns: Future that resolves with the event """ future = asyncio.Future() def wrapper(event: Event): if handler: handler(event) if not future.done(): future.set_result(event) self.subscribe(event_type, wrapper, priority, once=True) if timeout: async def timeout_handler(): await asyncio.sleep(timeout) if not future.done(): future.set_exception(TimeoutError(f"Event {event_type} timed out")) asyncio.create_task(timeout_handler()) return future # ========== Event Emission ========== def emit( self, event_type: str, data: Dict[str, Any], source: str = "system", priority: EventPriority = EventPriority.NORMAL ) -> Event: """Emit an event. Args: event_type: Type of event data: Event data source: Event source priority: Event priority Returns: The created event """ event = Event.create( event_type=event_type, data=data, source=source, priority=priority ) self._queue.append(event) self._event_count += 1 # Process immediately in async context (thread-safe) try: loop = asyncio.get_running_loop() if loop.is_running(): asyncio.create_task(self._process_event(event)) except RuntimeError: # No event loop running in this thread - queue for later processing pass return event async def emit_async( self, event_type: str, data: Dict[str, Any], source: str = "system", priority: EventPriority = EventPriority.NORMAL ) -> Event: """Emit an event asynchronously.""" event = Event.create( event_type=event_type, data=data, source=source, priority=priority ) await self._process_event(event) return event async def _process_event(self, event: Event) -> None: """Process a single event.""" # Collect all matching handlers handlers: List[tuple] = [] # Get handlers for specific event type for sub in self._subscribers.get(event.type, set()): if sub.matches(event): handlers.append((sub.priority.value, sub)) # Get wildcard handlers for sub in self._wildcard_subscribers: if sub.filter is None or sub.filter.matches(event): handlers.append((sub.priority.value, sub)) # Sort by priority (highest first) handlers.sort(key=lambda x: -x[0]) # Execute handlers once_subs = [] for _, sub in handlers: try: if inspect.iscoroutinefunction(sub.callback): await sub.callback(event) else: sub.callback(event) if sub.once: once_subs.append(sub) except Exception as e: self._logger.error(f"Error in event handler: {e}") traceback.print_exc() # Remove once subscriptions for sub in once_subs: self.unsubscribe(sub.id) # ========== Utility Methods ========== def clear(self) -> None: """Clear all subscriptions and queued events.""" self._subscribers.clear() self._wildcard_subscribers.clear() self._queue.clear() def get_stats(self) -> Dict[str, Any]: """Get event bus statistics.""" return { 'total_events': self._event_count, 'dropped_events': self._dropped_count, 'queue_size': len(self._queue), 'subscription_count': sum( len(subs) for subs in self._subscribers.values() ), 'wildcard_subscriptions': len(self._wildcard_subscribers), } def wait_for( self, event_type: str, condition: Optional[Callable[[Event], bool]] = None, timeout: Optional[float] = None ) -> asyncio.Future: """Wait for a specific event. Args: event_type: Event type to wait for condition: Optional condition function timeout: Optional timeout in seconds Returns: Future that resolves with the matching event """ future = asyncio.Future() def handler(event: Event): if condition and not condition(event): return if not future.done(): future.set_result(event) return True # Unsubscribe self.subscribe(event_type, handler, once=True) if timeout: async def timeout_coro(): await asyncio.sleep(timeout) if not future.done(): future.set_exception(TimeoutError(f"Timeout waiting for {event_type}")) asyncio.create_task(timeout_coro()) return future # ============================================================================= # TYPED EVENTS # ============================================================================= class TypedEventBus(EventBus): """Type-safe event bus with predefined event types.""" # Game events GAME_CONNECTED = "game.connected" GAME_DISCONNECTED = "game.disconnected" GAME_FOCUS_CHANGED = "game.focus_changed" # Loot events LOOT_RECEIVED = "game.loot" GLOBAL_HOF = "game.global" # Skill events SKILL_GAIN = "game.skill" PROFESSION_GAIN = "game.profession" # Chat events CHAT_MESSAGE = "game.chat" # System events PLUGIN_LOADED = "system.plugin.loaded" PLUGIN_UNLOADED = "system.plugin.unloaded" ERROR_OCCURRED = "system.error" def emit_game_connected(self, character_name: str) -> Event: """Emit game connected event.""" return self.emit(self.GAME_CONNECTED, { 'character_name': character_name }) def emit_loot(self, item: str, value: float, quantity: int = 1) -> Event: """Emit loot received event.""" return self.emit(self.LOOT_RECEIVED, { 'item': item, 'value': value, 'quantity': quantity, 'timestamp': time.time(), }) def emit_skill(self, skill_name: str, value: float, gain: float) -> Event: """Emit skill gain event.""" return self.emit(self.SKILL_GAIN, { 'skill': skill_name, 'value': value, 'gain': gain, }) def emit_global(self, mob_name: str, value: float, player: str) -> Event: """Emit global/HoF event.""" return self.emit(self.GLOBAL_HOF, { 'mob': mob_name, 'value': value, 'player': player, 'is_hof': value >= 1000, }) # ============================================================================= # EXPORTS # ============================================================================= __all__ = [ 'EventPriority', 'Event', 'EventFilter', 'Subscription', 'EventBus', 'TypedEventBus', ]