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/README.md ADDED
@@ -0,0 +1,564 @@
1
+ # Vega Events - Domain Events & Event Bus
2
+
3
+ The Vega Events module provides a powerful event-driven architecture for your applications, enabling loose coupling and reactive programming patterns.
4
+
5
+ ## Features
6
+
7
+ - ✅ **Event Bus** - Publish/subscribe pattern for domain events
8
+ - ✅ **Async Support** - Full async/await support for event handlers
9
+ - ✅ **Auto-Publish** - Ultra-clean syntax with metaclass-powered instant publishing
10
+ - ✅ **Priority Ordering** - Control handler execution order
11
+ - ✅ **Retry Logic** - Automatic retries for failed handlers
12
+ - ✅ **Middleware** - Cross-cutting concerns (logging, metrics, validation)
13
+ - ✅ **Event Inheritance** - Handlers for base events receive derived events
14
+ - ✅ **Type-Safe** - Full type hints support
15
+
16
+ ## Quick Start
17
+
18
+ ### 1. Define an Event
19
+
20
+ Events are immutable data classes that represent something that happened:
21
+
22
+ ```python
23
+ from dataclasses import dataclass
24
+ from vega.events import Event
25
+
26
+ @dataclass(frozen=True)
27
+ class UserCreated(Event):
28
+ user_id: str
29
+ email: str
30
+ name: str
31
+
32
+ def __post_init__(self):
33
+ super().__init__()
34
+ ```
35
+
36
+ ### 2. Subscribe to Events
37
+
38
+ Use the `@subscribe` decorator to register event handlers:
39
+
40
+ ```python
41
+ from vega.events import subscribe
42
+
43
+ @subscribe(UserCreated)
44
+ async def send_welcome_email(event: UserCreated):
45
+ """Send welcome email when user is created"""
46
+ print(f"Sending welcome email to {event.email}")
47
+ await email_service.send(
48
+ to=event.email,
49
+ subject="Welcome!",
50
+ body=f"Hello {event.name}, welcome to our platform!"
51
+ )
52
+
53
+ @subscribe(UserCreated)
54
+ async def create_audit_log(event: UserCreated):
55
+ """Log user creation for audit trail"""
56
+ await audit_service.log(f"User {event.user_id} created at {event.timestamp}")
57
+ ```
58
+
59
+ ### 3. Publish Events
60
+
61
+ **Simple Syntax (Recommended)** - Just call `.publish()` on the event:
62
+
63
+ ```python
64
+ async def create_user(name: str, email: str):
65
+ # Create user logic...
66
+ user = User(id="123", name=name, email=email)
67
+
68
+ # Publish event - Simple and clean!
69
+ event = UserCreated(
70
+ user_id=user.id,
71
+ email=user.email,
72
+ name=user.name
73
+ )
74
+ await event.publish() # That's it!
75
+
76
+ return user
77
+ ```
78
+
79
+ **Alternative Syntax** - Using the event bus directly:
80
+
81
+ ```python
82
+ from vega.events import get_event_bus
83
+
84
+ async def create_user(name: str, email: str):
85
+ user = User(id="123", name=name, email=email)
86
+
87
+ # Publish via event bus
88
+ bus = get_event_bus()
89
+ await bus.publish(UserCreated(
90
+ user_id=user.id,
91
+ email=user.email,
92
+ name=user.name
93
+ ))
94
+
95
+ return user
96
+ ```
97
+
98
+ ## Advanced Usage
99
+
100
+ ### Priority and Ordering
101
+
102
+ Control handler execution order with priorities:
103
+
104
+ ```python
105
+ @subscribe(UserCreated, priority=100)
106
+ async def critical_handler(event: UserCreated):
107
+ """Runs first due to higher priority"""
108
+ pass
109
+
110
+ @subscribe(UserCreated, priority=0)
111
+ async def normal_handler(event: UserCreated):
112
+ """Runs after critical handlers"""
113
+ pass
114
+ ```
115
+
116
+ ### Retry on Failure
117
+
118
+ Automatically retry handlers that fail:
119
+
120
+ ```python
121
+ @subscribe(UserCreated, retry_on_error=True, max_retries=5)
122
+ async def unreliable_handler(event: UserCreated):
123
+ """Will retry up to 5 times on failure"""
124
+ response = await external_api.call()
125
+ if not response.ok:
126
+ raise Exception("API call failed")
127
+ ```
128
+
129
+ ### Event Inheritance
130
+
131
+ Handlers for base events automatically receive derived events:
132
+
133
+ ```python
134
+ @dataclass(frozen=True)
135
+ class UserEvent(Event):
136
+ user_id: str
137
+
138
+ @dataclass(frozen=True)
139
+ class UserCreated(UserEvent):
140
+ email: str
141
+
142
+ @dataclass(frozen=True)
143
+ class UserUpdated(UserEvent):
144
+ changes: dict
145
+
146
+ # This handler receives ALL UserEvent subtypes
147
+ @subscribe(UserEvent)
148
+ async def handle_any_user_event(event: UserEvent):
149
+ print(f"User event: {event.event_name}")
150
+ ```
151
+
152
+ ### Error Handling
153
+
154
+ Register global error handlers:
155
+
156
+ ```python
157
+ from vega.events import get_event_bus
158
+
159
+ bus = get_event_bus()
160
+
161
+ @bus.on_error
162
+ async def log_event_errors(event, exception, handler_name):
163
+ """Called when any handler fails"""
164
+ logger.error(
165
+ f"Handler '{handler_name}' failed for event '{event.event_name}': {exception}"
166
+ )
167
+ # Send to error tracking service
168
+ await sentry.capture_exception(exception)
169
+ ```
170
+
171
+ ### Middleware
172
+
173
+ Add cross-cutting concerns with middleware:
174
+
175
+ ```python
176
+ from vega.events import get_event_bus, LoggingEventMiddleware
177
+
178
+ bus = get_event_bus()
179
+
180
+ # Built-in logging middleware
181
+ bus.add_middleware(LoggingEventMiddleware())
182
+
183
+ # Custom middleware
184
+ from vega.events import EventMiddleware
185
+
186
+ class TenantContextMiddleware(EventMiddleware):
187
+ async def before_publish(self, event: Event):
188
+ # Add tenant context to all events
189
+ tenant_id = get_current_tenant()
190
+ event.add_metadata('tenant_id', tenant_id)
191
+
192
+ async def after_publish(self, event: Event):
193
+ # Cleanup after event is processed
194
+ pass
195
+
196
+ bus.add_middleware(TenantContextMiddleware())
197
+ ```
198
+
199
+ ### Metrics Collection
200
+
201
+ Track event processing with built-in metrics middleware:
202
+
203
+ ```python
204
+ from vega.events import get_event_bus
205
+ from vega.events.middleware import MetricsEventMiddleware
206
+
207
+ bus = get_event_bus()
208
+ metrics = MetricsEventMiddleware()
209
+ bus.add_middleware(metrics)
210
+
211
+ # Later, get metrics
212
+ stats = metrics.get_metrics()
213
+ print(stats)
214
+ # {
215
+ # 'UserCreated': {
216
+ # 'count': 150,
217
+ # 'avg_duration_ms': 45.2,
218
+ # 'min_duration_ms': 12.1,
219
+ # 'max_duration_ms': 234.5,
220
+ # }
221
+ # }
222
+ ```
223
+
224
+ ### Validation
225
+
226
+ Validate events before publishing:
227
+
228
+ ```python
229
+ from vega.events.middleware import ValidationEventMiddleware
230
+
231
+ def validate_user_created(event):
232
+ if not event.email or '@' not in event.email:
233
+ raise ValueError("Invalid email address")
234
+ if not event.user_id:
235
+ raise ValueError("user_id is required")
236
+
237
+ validation = ValidationEventMiddleware()
238
+ validation.add_validator(UserCreated, validate_user_created)
239
+
240
+ bus.add_middleware(validation)
241
+ ```
242
+
243
+ ### Event Enrichment
244
+
245
+ Automatically add metadata to events:
246
+
247
+ ```python
248
+ from vega.events.middleware import EnrichmentEventMiddleware
249
+
250
+ enrichment = EnrichmentEventMiddleware()
251
+
252
+ # Add correlation ID
253
+ enrichment.add_enricher(
254
+ lambda event: event.add_metadata('correlation_id', get_correlation_id())
255
+ )
256
+
257
+ # Add user context
258
+ enrichment.add_enricher(
259
+ lambda event: event.add_metadata('triggered_by', get_current_user_id())
260
+ )
261
+
262
+ bus.add_middleware(enrichment)
263
+ ```
264
+
265
+ ## Integration with Vega Patterns
266
+
267
+ ### In Interactors
268
+
269
+ ```python
270
+ from vega.patterns import Interactor
271
+ from vega.di import bind
272
+ from vega.events import get_event_bus
273
+
274
+ class CreateUser(Interactor[User]):
275
+ def __init__(self, name: str, email: str):
276
+ self.name = name
277
+ self.email = email
278
+
279
+ @bind
280
+ async def call(self, repository: UserRepository) -> User:
281
+ # Domain logic
282
+ user = User(name=self.name, email=self.email)
283
+ user = await repository.save(user)
284
+
285
+ # Publish domain event
286
+ bus = get_event_bus()
287
+ await bus.publish(UserCreated(
288
+ user_id=user.id,
289
+ email=user.email,
290
+ name=user.name
291
+ ))
292
+
293
+ return user
294
+ ```
295
+
296
+ ### In Mediators
297
+
298
+ ```python
299
+ from vega.patterns import Mediator
300
+ from vega.events import get_event_bus
301
+
302
+ class UserRegistrationWorkflow(Mediator[User]):
303
+ def __init__(self, name: str, email: str, password: str):
304
+ self.name = name
305
+ self.email = email
306
+ self.password = password
307
+
308
+ async def call(self) -> User:
309
+ # Create user
310
+ user = await CreateUser(self.name, self.email)
311
+
312
+ # Set password
313
+ await SetUserPassword(user.id, self.password)
314
+
315
+ # Publish workflow completion event
316
+ bus = get_event_bus()
317
+ await bus.publish(UserRegistrationCompleted(
318
+ user_id=user.id,
319
+ email=user.email
320
+ ))
321
+
322
+ return user
323
+ ```
324
+
325
+ ## Event Naming Conventions
326
+
327
+ Events should be named in **past tense** to indicate something that happened:
328
+
329
+ ✅ **Good**:
330
+ - `UserCreated`
331
+ - `OrderPlaced`
332
+ - `PaymentProcessed`
333
+ - `EmailSent`
334
+ - `InventoryUpdated`
335
+
336
+ ❌ **Bad**:
337
+ - `CreateUser` (this is a command/action, not an event)
338
+ - `PlaceOrder` (command)
339
+ - `SendEmail` (command)
340
+
341
+ ## Best Practices
342
+
343
+ ### 1. Keep Events Immutable
344
+
345
+ Use `@dataclass(frozen=True)` to ensure events cannot be modified:
346
+
347
+ ```python
348
+ @dataclass(frozen=True) # ✅ Immutable
349
+ class UserCreated(Event):
350
+ user_id: str
351
+ email: str
352
+ ```
353
+
354
+ ### 2. Include All Relevant Data
355
+
356
+ Events should contain all data needed by handlers:
357
+
358
+ ```python
359
+ # ✅ Good - includes all relevant data
360
+ @dataclass(frozen=True)
361
+ class OrderPlaced(Event):
362
+ order_id: str
363
+ customer_id: str
364
+ items: List[OrderItem]
365
+ total_amount: Decimal
366
+ currency: str
367
+
368
+ # ❌ Bad - handlers need to fetch additional data
369
+ @dataclass(frozen=True)
370
+ class OrderPlaced(Event):
371
+ order_id: str # Handlers must fetch order details
372
+ ```
373
+
374
+ ### 3. One Event Per Domain Action
375
+
376
+ Create specific events for specific domain actions:
377
+
378
+ ```python
379
+ # ✅ Good - specific events
380
+ @dataclass(frozen=True)
381
+ class UserEmailChanged(Event):
382
+ user_id: str
383
+ old_email: str
384
+ new_email: str
385
+
386
+ @dataclass(frozen=True)
387
+ class UserPasswordChanged(Event):
388
+ user_id: str
389
+ changed_at: datetime
390
+
391
+ # ❌ Bad - generic event
392
+ @dataclass(frozen=True)
393
+ class UserUpdated(Event):
394
+ user_id: str
395
+ changes: dict # Too generic
396
+ ```
397
+
398
+ ### 4. Handlers Should Be Idempotent
399
+
400
+ Handlers may be called multiple times (retries), so make them idempotent:
401
+
402
+ ```python
403
+ @subscribe(UserCreated)
404
+ async def send_welcome_email(event: UserCreated):
405
+ # ✅ Check if email already sent
406
+ if await email_log.has_sent(event.user_id, 'welcome'):
407
+ return
408
+
409
+ await email_service.send(event.email, "Welcome!")
410
+ await email_log.record(event.user_id, 'welcome')
411
+ ```
412
+
413
+ ### 5. Don't Publish Events in Handlers
414
+
415
+ Avoid publishing events from within event handlers to prevent cascading complexity:
416
+
417
+ ```python
418
+ # ❌ Bad - publishing from handler
419
+ @subscribe(UserCreated)
420
+ async def on_user_created(event: UserCreated):
421
+ bus = get_event_bus()
422
+ await bus.publish(WelcomeEmailSent(...)) # ❌ Cascading events
423
+
424
+ # ✅ Good - publish from domain logic
425
+ class CreateUser(Interactor[User]):
426
+ async def call(self, ...):
427
+ user = await repository.save(user)
428
+
429
+ # Publish all related events here
430
+ bus = get_event_bus()
431
+ await bus.publish(UserCreated(...))
432
+ await bus.publish(WelcomeEmailScheduled(...))
433
+ ```
434
+
435
+ ## Testing
436
+
437
+ ### Testing Event Handlers
438
+
439
+ ```python
440
+ import pytest
441
+ from vega.events import EventBus, set_event_bus
442
+
443
+ @pytest.fixture
444
+ def event_bus():
445
+ """Create isolated event bus for tests"""
446
+ bus = EventBus()
447
+ set_event_bus(bus)
448
+ yield bus
449
+ # Cleanup
450
+ bus.clear_subscribers()
451
+
452
+ async def test_user_created_handler(event_bus):
453
+ """Test that welcome email is sent"""
454
+ sent_emails = []
455
+
456
+ @event_bus.subscribe(UserCreated)
457
+ async def track_emails(event: UserCreated):
458
+ sent_emails.append(event.email)
459
+
460
+ # Publish event
461
+ await event_bus.publish(UserCreated(
462
+ user_id="123",
463
+ email="test@test.com",
464
+ name="Test User"
465
+ ))
466
+
467
+ # Assert
468
+ assert "test@test.com" in sent_emails
469
+ ```
470
+
471
+ ### Mocking Event Bus
472
+
473
+ ```python
474
+ from unittest.mock import AsyncMock
475
+
476
+ async def test_interactor_publishes_event():
477
+ """Test that interactor publishes event"""
478
+ mock_bus = AsyncMock()
479
+
480
+ # Inject mock bus
481
+ set_event_bus(mock_bus)
482
+
483
+ # Execute interactor
484
+ user = await CreateUser(name="Test", email="test@test.com")
485
+
486
+ # Assert event was published
487
+ mock_bus.publish.assert_called_once()
488
+ event = mock_bus.publish.call_args[0][0]
489
+ assert isinstance(event, UserCreated)
490
+ assert event.email == "test@test.com"
491
+ ```
492
+
493
+ ## API Reference
494
+
495
+ ### Event
496
+
497
+ Base class for all events.
498
+
499
+ **Properties**:
500
+ - `event_id: str` - Unique identifier (auto-generated UUID)
501
+ - `timestamp: datetime` - When event was created (auto-generated)
502
+ - `event_name: str` - Event class name
503
+ - `metadata: Dict[str, Any]` - Additional metadata
504
+
505
+ **Methods**:
506
+ - `add_metadata(key: str, value: Any)` - Add metadata to event
507
+
508
+ ### EventBus
509
+
510
+ Central event bus for pub/sub.
511
+
512
+ **Methods**:
513
+ - `subscribe(event_type, handler, priority=0, retry_on_error=False, max_retries=3)` - Subscribe handler
514
+ - `unsubscribe(event_type, handler)` - Unsubscribe handler
515
+ - `publish(event)` - Publish event to all subscribers
516
+ - `publish_many(events)` - Publish multiple events
517
+ - `add_middleware(middleware)` - Add middleware
518
+ - `on_error(handler)` - Register error handler
519
+ - `clear_subscribers(event_type=None)` - Clear subscribers
520
+ - `get_subscriber_count(event_type)` - Get subscriber count
521
+
522
+ ### Decorators
523
+
524
+ **`@subscribe(event_type, priority=0, retry_on_error=False, max_retries=3)`**
525
+
526
+ Subscribe function to event on global bus.
527
+
528
+ **`@event_handler(event_type, bus=None, priority=0, retry_on_error=False, max_retries=3)`**
529
+
530
+ Mark method as event handler with optional custom bus.
531
+
532
+ ### Middleware
533
+
534
+ **Built-in Middleware**:
535
+ - `LoggingEventMiddleware` - Log all events
536
+ - `MetricsEventMiddleware` - Collect event metrics
537
+ - `ValidationEventMiddleware` - Validate events before publishing
538
+ - `EnrichmentEventMiddleware` - Auto-add metadata to events
539
+
540
+ **Custom Middleware**:
541
+
542
+ Extend `EventMiddleware` and implement:
543
+ - `async def before_publish(event: Event)` - Called before event is published
544
+ - `async def after_publish(event: Event)` - Called after handlers complete
545
+
546
+ ## Performance Considerations
547
+
548
+ - **Async Handlers**: All handlers run concurrently (not sequentially)
549
+ - **Error Isolation**: Failed handlers don't block other handlers
550
+ - **Retry Overhead**: Use retry sparingly - exponential backoff adds delay
551
+ - **Middleware Order**: Middleware runs in order added - keep it lightweight
552
+
553
+ ## Examples
554
+
555
+ See the [examples directory](../../examples/events/) for complete examples:
556
+ - Basic event publishing
557
+ - Event handlers with DI
558
+ - Middleware usage
559
+ - Testing strategies
560
+ - Integration with Interactors/Mediators
561
+
562
+ ## License
563
+
564
+ Part of the Vega Framework - See LICENSE for details.