vega-framework 0.1.29__py3-none-any.whl → 0.1.31__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
vega/events/bus.py ADDED
@@ -0,0 +1,382 @@
1
+ """Event Bus implementation for pub/sub pattern"""
2
+ import asyncio
3
+ import logging
4
+ from typing import Type, Callable, List, Dict, Any, Optional
5
+ from collections import defaultdict
6
+
7
+ from vega.events.event import Event
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class EventBus:
13
+ """
14
+ Event Bus for publishing and subscribing to domain events.
15
+
16
+ Supports:
17
+ - Async event handlers
18
+ - Multiple subscribers per event
19
+ - Event inheritance (handlers for base events receive derived events)
20
+ - Priority ordering
21
+ - Error handling with retries
22
+ - Middleware support
23
+
24
+ Example:
25
+ from vega.events import EventBus, Event
26
+
27
+ bus = EventBus()
28
+
29
+ # Subscribe to event
30
+ @bus.subscribe(UserCreated)
31
+ async def send_welcome_email(event: UserCreated):
32
+ await email_service.send(event.email, "Welcome!")
33
+
34
+ # Publish event
35
+ await bus.publish(UserCreated(user_id="123", email="test@test.com"))
36
+ """
37
+
38
+ def __init__(self):
39
+ """Initialize event bus"""
40
+ self._subscribers: Dict[Type[Event], List[Dict[str, Any]]] = defaultdict(list)
41
+ self._middleware: List[Any] = []
42
+ self._error_handlers: List[Callable] = []
43
+
44
+ def subscribe(
45
+ self,
46
+ event_type: Type[Event],
47
+ handler: Optional[Callable] = None,
48
+ priority: int = 0,
49
+ retry_on_error: bool = False,
50
+ max_retries: int = 3
51
+ ):
52
+ """
53
+ Subscribe a handler to an event type.
54
+
55
+ Can be used as a decorator or called directly.
56
+
57
+ Args:
58
+ event_type: Type of event to subscribe to
59
+ handler: Handler function (if not used as decorator)
60
+ priority: Handler priority (higher runs first)
61
+ retry_on_error: Whether to retry on failure
62
+ max_retries: Maximum retry attempts
63
+
64
+ Returns:
65
+ Handler function (for decorator usage) or None
66
+
67
+ Example:
68
+ # As decorator
69
+ @bus.subscribe(UserCreated)
70
+ async def handle_user_created(event: UserCreated):
71
+ ...
72
+
73
+ # Direct call
74
+ bus.subscribe(UserCreated, handle_user_created)
75
+
76
+ # With options
77
+ @bus.subscribe(UserCreated, priority=10, retry_on_error=True)
78
+ async def important_handler(event: UserCreated):
79
+ ...
80
+ """
81
+ def decorator(func: Callable) -> Callable:
82
+ subscriber_info = {
83
+ 'handler': func,
84
+ 'priority': priority,
85
+ 'retry_on_error': retry_on_error,
86
+ 'max_retries': max_retries,
87
+ }
88
+
89
+ # Add to subscribers list
90
+ self._subscribers[event_type].append(subscriber_info)
91
+
92
+ # Sort by priority (higher priority first)
93
+ self._subscribers[event_type].sort(
94
+ key=lambda x: x['priority'],
95
+ reverse=True
96
+ )
97
+
98
+ logger.debug(
99
+ f"Registered handler '{func.__name__}' for event '{event_type.__name__}' "
100
+ f"(priority={priority})"
101
+ )
102
+
103
+ return func
104
+
105
+ # If handler is provided, apply decorator immediately
106
+ if handler is not None:
107
+ return decorator(handler)
108
+
109
+ # Otherwise return decorator for @subscribe usage
110
+ return decorator
111
+
112
+ def unsubscribe(self, event_type: Type[Event], handler: Callable) -> bool:
113
+ """
114
+ Unsubscribe a handler from an event type.
115
+
116
+ Args:
117
+ event_type: Type of event
118
+ handler: Handler function to remove
119
+
120
+ Returns:
121
+ True if handler was found and removed, False otherwise
122
+ """
123
+ if event_type not in self._subscribers:
124
+ return False
125
+
126
+ initial_count = len(self._subscribers[event_type])
127
+ self._subscribers[event_type] = [
128
+ sub for sub in self._subscribers[event_type]
129
+ if sub['handler'] != handler
130
+ ]
131
+
132
+ removed = initial_count > len(self._subscribers[event_type])
133
+ if removed:
134
+ logger.debug(f"Unsubscribed handler '{handler.__name__}' from '{event_type.__name__}'")
135
+
136
+ return removed
137
+
138
+ def add_middleware(self, middleware: 'EventMiddleware') -> None:
139
+ """
140
+ Add middleware to the event bus.
141
+
142
+ Middleware is executed for all events in the order added.
143
+
144
+ Args:
145
+ middleware: Middleware instance
146
+ """
147
+ self._middleware.append(middleware)
148
+ logger.debug(f"Added middleware: {middleware.__class__.__name__}")
149
+
150
+ def on_error(self, handler: Callable) -> Callable:
151
+ """
152
+ Register error handler for failed event processing.
153
+
154
+ Error handlers receive (event, exception, handler_name) and should not raise.
155
+
156
+ Example:
157
+ @bus.on_error
158
+ async def log_errors(event, exception, handler_name):
159
+ logger.error(f"Handler {handler_name} failed: {exception}")
160
+ """
161
+ self._error_handlers.append(handler)
162
+ return handler
163
+
164
+ async def publish(self, event: Event) -> None:
165
+ """
166
+ Publish an event to all subscribers.
167
+
168
+ Executes all handlers in priority order. If a handler fails and has
169
+ retry enabled, it will be retried up to max_retries times.
170
+
171
+ Args:
172
+ event: Event instance to publish
173
+
174
+ Example:
175
+ await bus.publish(UserCreated(user_id="123", email="test@test.com"))
176
+ """
177
+ logger.debug(f"Publishing event: {event.event_name} (id={event.event_id})")
178
+
179
+ # Execute middleware (before)
180
+ for middleware in self._middleware:
181
+ await middleware.before_publish(event)
182
+
183
+ # Get handlers for this event type and all parent event types
184
+ handlers = self._get_handlers_for_event(event)
185
+
186
+ if not handlers:
187
+ logger.debug(f"No subscribers for event: {event.event_name}")
188
+ # Execute middleware (after) even if no handlers
189
+ for middleware in reversed(self._middleware):
190
+ await middleware.after_publish(event)
191
+ return
192
+
193
+ logger.debug(f"Found {len(handlers)} handler(s) for event: {event.event_name}")
194
+
195
+ # Execute all handlers
196
+ tasks = []
197
+ for subscriber_info in handlers:
198
+ task = self._execute_handler(event, subscriber_info)
199
+ tasks.append(task)
200
+
201
+ # Wait for all handlers to complete
202
+ results = await asyncio.gather(*tasks, return_exceptions=True)
203
+
204
+ # Check for errors
205
+ for idx, result in enumerate(results):
206
+ if isinstance(result, Exception):
207
+ handler_name = handlers[idx]['handler'].__name__
208
+ logger.error(
209
+ f"Handler '{handler_name}' failed for event '{event.event_name}': {result}"
210
+ )
211
+
212
+ # Execute middleware (after)
213
+ for middleware in reversed(self._middleware):
214
+ await middleware.after_publish(event)
215
+
216
+ logger.debug(f"Event published: {event.event_name} (id={event.event_id})")
217
+
218
+ async def publish_many(self, events: List[Event]) -> None:
219
+ """
220
+ Publish multiple events.
221
+
222
+ Events are published sequentially in order.
223
+
224
+ Args:
225
+ events: List of events to publish
226
+ """
227
+ for event in events:
228
+ await self.publish(event)
229
+
230
+ def _get_handlers_for_event(self, event: Event) -> List[Dict[str, Any]]:
231
+ """
232
+ Get all handlers that should receive this event.
233
+
234
+ Includes handlers for the exact event type and all parent event types.
235
+ """
236
+ handlers = []
237
+ event_type = type(event)
238
+
239
+ # Get handlers for exact type
240
+ if event_type in self._subscribers:
241
+ handlers.extend(self._subscribers[event_type])
242
+
243
+ # Get handlers for parent types (event inheritance)
244
+ for base_type in event_type.__mro__[1:]:
245
+ if base_type == Event or not issubclass(base_type, Event):
246
+ continue
247
+ if base_type in self._subscribers:
248
+ handlers.extend(self._subscribers[base_type])
249
+
250
+ return handlers
251
+
252
+ async def _execute_handler(
253
+ self,
254
+ event: Event,
255
+ subscriber_info: Dict[str, Any]
256
+ ) -> None:
257
+ """
258
+ Execute a single event handler with retry logic.
259
+
260
+ Args:
261
+ event: Event to handle
262
+ subscriber_info: Subscriber configuration
263
+ """
264
+ handler = subscriber_info['handler']
265
+ retry_on_error = subscriber_info['retry_on_error']
266
+ max_retries = subscriber_info['max_retries']
267
+
268
+ attempt = 0
269
+ last_exception = None
270
+
271
+ while attempt <= (max_retries if retry_on_error else 0):
272
+ try:
273
+ # Execute handler
274
+ result = handler(event)
275
+
276
+ # Await if coroutine
277
+ if asyncio.iscoroutine(result):
278
+ await result
279
+
280
+ # Success - exit retry loop
281
+ return
282
+
283
+ except Exception as e:
284
+ last_exception = e
285
+ attempt += 1
286
+
287
+ if attempt <= max_retries and retry_on_error:
288
+ logger.warning(
289
+ f"Handler '{handler.__name__}' failed (attempt {attempt}/{max_retries}): {e}"
290
+ )
291
+ # Exponential backoff
292
+ await asyncio.sleep(0.1 * (2 ** (attempt - 1)))
293
+ else:
294
+ # All retries exhausted or retry disabled
295
+ logger.error(
296
+ f"Handler '{handler.__name__}' failed for event '{event.event_name}': {e}",
297
+ exc_info=True
298
+ )
299
+
300
+ # Call error handlers
301
+ for error_handler in self._error_handlers:
302
+ try:
303
+ result = error_handler(event, e, handler.__name__)
304
+ if asyncio.iscoroutine(result):
305
+ await result
306
+ except Exception as err_ex:
307
+ logger.error(
308
+ f"Error handler failed: {err_ex}",
309
+ exc_info=True
310
+ )
311
+
312
+ # Re-raise exception
313
+ raise last_exception
314
+
315
+ def clear_subscribers(self, event_type: Optional[Type[Event]] = None) -> None:
316
+ """
317
+ Clear all subscribers.
318
+
319
+ Args:
320
+ event_type: If provided, only clear subscribers for this event type.
321
+ If None, clear all subscribers.
322
+ """
323
+ if event_type is None:
324
+ self._subscribers.clear()
325
+ logger.debug("Cleared all subscribers")
326
+ elif event_type in self._subscribers:
327
+ del self._subscribers[event_type]
328
+ logger.debug(f"Cleared subscribers for: {event_type.__name__}")
329
+
330
+ def get_subscriber_count(self, event_type: Type[Event]) -> int:
331
+ """
332
+ Get the number of subscribers for an event type.
333
+
334
+ Args:
335
+ event_type: Event type
336
+
337
+ Returns:
338
+ Number of subscribers
339
+ """
340
+ return len(self._subscribers.get(event_type, []))
341
+
342
+
343
+ # Global event bus instance
344
+ _global_event_bus: Optional[EventBus] = None
345
+
346
+
347
+ def get_event_bus() -> EventBus:
348
+ """
349
+ Get the global event bus instance.
350
+
351
+ Creates the instance on first call (singleton pattern).
352
+
353
+ Returns:
354
+ Global EventBus instance
355
+
356
+ Example:
357
+ from vega.events import get_event_bus
358
+
359
+ bus = get_event_bus()
360
+ await bus.publish(MyEvent())
361
+ """
362
+ global _global_event_bus
363
+
364
+ if _global_event_bus is None:
365
+ _global_event_bus = EventBus()
366
+ logger.debug("Created global event bus")
367
+
368
+ return _global_event_bus
369
+
370
+
371
+ def set_event_bus(bus: EventBus) -> None:
372
+ """
373
+ Set a custom event bus as the global instance.
374
+
375
+ Useful for testing or custom configurations.
376
+
377
+ Args:
378
+ bus: EventBus instance to use globally
379
+ """
380
+ global _global_event_bus
381
+ _global_event_bus = bus
382
+ logger.debug("Set custom global event bus")
@@ -0,0 +1,181 @@
1
+ """Decorators for event handling"""
2
+ from typing import Type, Callable, Optional
3
+ from vega.events.event import Event
4
+ from vega.events.bus import get_event_bus
5
+
6
+
7
+ def subscribe(
8
+ event_type: Type[Event],
9
+ priority: int = 0,
10
+ retry_on_error: bool = False,
11
+ max_retries: int = 3
12
+ ):
13
+ """
14
+ Decorator to subscribe a function to an event on the global event bus.
15
+
16
+ This is a convenience decorator that uses the global event bus instance.
17
+
18
+ Args:
19
+ event_type: Type of event to subscribe to
20
+ priority: Handler priority (higher runs first)
21
+ retry_on_error: Whether to retry on failure
22
+ max_retries: Maximum retry attempts
23
+
24
+ Returns:
25
+ Decorated function
26
+
27
+ Example:
28
+ from vega.events import subscribe, Event
29
+ from dataclasses import dataclass
30
+
31
+ @dataclass(frozen=True)
32
+ class UserCreated(Event):
33
+ user_id: str
34
+ email: str
35
+
36
+ @subscribe(UserCreated)
37
+ async def send_welcome_email(event: UserCreated):
38
+ print(f"Sending welcome email to {event.email}")
39
+
40
+ @subscribe(UserCreated, priority=10)
41
+ async def critical_handler(event: UserCreated):
42
+ # This runs first due to higher priority
43
+ print("Critical handler")
44
+
45
+ @subscribe(UserCreated, retry_on_error=True, max_retries=5)
46
+ async def retry_handler(event: UserCreated):
47
+ # Will retry up to 5 times on failure
48
+ await external_api.call()
49
+ """
50
+ def decorator(func: Callable) -> Callable:
51
+ bus = get_event_bus()
52
+ bus.subscribe(
53
+ event_type=event_type,
54
+ handler=func,
55
+ priority=priority,
56
+ retry_on_error=retry_on_error,
57
+ max_retries=max_retries
58
+ )
59
+ return func
60
+
61
+ return decorator
62
+
63
+
64
+ def event_handler(
65
+ event_type: Type[Event],
66
+ bus: Optional['EventBus'] = None,
67
+ priority: int = 0,
68
+ retry_on_error: bool = False,
69
+ max_retries: int = 3
70
+ ):
71
+ """
72
+ Decorator to mark a method as an event handler.
73
+
74
+ Similar to @subscribe but allows specifying a custom event bus.
75
+ Useful for class-based handlers or testing.
76
+
77
+ Args:
78
+ event_type: Type of event to subscribe to
79
+ bus: Custom event bus instance (uses global if None)
80
+ priority: Handler priority
81
+ retry_on_error: Whether to retry on failure
82
+ max_retries: Maximum retry attempts
83
+
84
+ Returns:
85
+ Decorated function
86
+
87
+ Example:
88
+ from vega.events import event_handler, Event, EventBus
89
+
90
+ class UserEventHandlers:
91
+ def __init__(self, email_service):
92
+ self.email_service = email_service
93
+
94
+ @event_handler(UserCreated)
95
+ async def handle_user_created(self, event: UserCreated):
96
+ await self.email_service.send_welcome(event.email)
97
+
98
+ # With custom bus
99
+ custom_bus = EventBus()
100
+
101
+ class CustomHandlers:
102
+ @event_handler(UserCreated, bus=custom_bus)
103
+ async def handle(self, event: UserCreated):
104
+ pass
105
+ """
106
+ def decorator(func: Callable) -> Callable:
107
+ event_bus = bus or get_event_bus()
108
+ event_bus.subscribe(
109
+ event_type=event_type,
110
+ handler=func,
111
+ priority=priority,
112
+ retry_on_error=retry_on_error,
113
+ max_retries=max_retries
114
+ )
115
+ # Mark function as event handler for introspection
116
+ func._is_event_handler = True
117
+ func._event_type = event_type
118
+ return func
119
+
120
+ return decorator
121
+
122
+
123
+ def trigger(event_class: Type[Event]):
124
+ """
125
+ Decorator for Interactor classes to automatically trigger an event after call() completes.
126
+
127
+ The event is constructed with the return value of call() method and auto-published.
128
+ This is perfect for domain events that should be triggered after a use case completes.
129
+
130
+ Args:
131
+ event_class: The event class to trigger (must accept call() result in constructor)
132
+
133
+ Example:
134
+ from vega.patterns import Interactor
135
+ from vega.events import trigger
136
+ from vega.di import bind
137
+ from dataclasses import dataclass
138
+
139
+ @dataclass(frozen=True)
140
+ class UserCreated(Event):
141
+ user_id: str
142
+ email: str
143
+ name: str
144
+
145
+ def __post_init__(self):
146
+ super().__init__()
147
+
148
+ @trigger(UserCreated)
149
+ class CreateUser(Interactor[dict]):
150
+ def __init__(self, name: str, email: str):
151
+ self.name = name
152
+ self.email = email
153
+
154
+ @bind
155
+ async def call(self, repository: UserRepository) -> dict:
156
+ user = await repository.create(name=self.name, email=self.email)
157
+ # Return dict that matches UserCreated constructor
158
+ return {
159
+ "user_id": user.id,
160
+ "email": user.email,
161
+ "name": user.name
162
+ }
163
+
164
+ # Usage
165
+ result = await CreateUser(name="John", email="john@test.com")
166
+ # After call() completes:
167
+ # 1. Returns result to caller
168
+ # 2. Automatically publishes UserCreated event with result as input
169
+ # 3. All @subscribe(UserCreated) handlers are triggered
170
+
171
+ Note:
172
+ - The event class constructor must accept the call() result
173
+ - If call() returns a dict, event(**result) is called
174
+ - If call() returns an object, event is called with the object as first arg
175
+ - Works seamlessly with auto-publish (events publish themselves)
176
+ """
177
+ def decorator(cls):
178
+ # Store the event class on the Interactor class
179
+ cls._trigger_event = event_class
180
+ return cls
181
+ return decorator