0
# Event System
1
2
Extensible event system for hooking into request/response lifecycle, enabling custom authentication, logging, monitoring, and request modification. The event system provides a hierarchical emitter that supports wildcard matching and event aliasing for maximum flexibility in customizing AWS API interactions.
3
4
## Capabilities
5
6
### Hierarchical Event Emitter
7
8
Core event emission system with hierarchical event names and wildcard support.
9
10
```python { .api }
11
class HierarchicalEmitter:
12
def __init__(self):
13
"""
14
Initialize hierarchical event emitter.
15
16
Provides event registration, emission, and handler management
17
with support for hierarchical event names and wildcard matching.
18
"""
19
20
def emit(self, event_name: str, **kwargs) -> List[Tuple[callable, Any]]:
21
"""
22
Emit an event by name with arguments passed as keyword args.
23
24
Args:
25
event_name: Dot-separated event name (e.g., 'before-call.s3.GetObject')
26
**kwargs: Arguments to pass to event handlers
27
28
Returns:
29
List of (handler, response) tuples from all processed handlers
30
31
Example:
32
>>> responses = emitter.emit(
33
... 'my-event.service.operation', arg1='one', arg2='two')
34
"""
35
36
def emit_until_response(
37
self,
38
event_name: str,
39
**kwargs
40
) -> Tuple[callable, Any]:
41
"""
42
Emit event until first non-None response is received.
43
44
Prevents subsequent handlers from being invoked after receiving
45
a non-None response, useful for short-circuiting event processing.
46
47
Args:
48
event_name: Dot-separated event name
49
**kwargs: Arguments to pass to event handlers
50
51
Returns:
52
First (handler, response) tuple with non-None response,
53
otherwise (None, None)
54
55
Example:
56
>>> handler, response = emitter.emit_until_response(
57
... 'my-event.service.operation', arg1='one', arg2='two')
58
"""
59
60
def register(
61
self,
62
event_name: str,
63
handler: callable,
64
unique_id: str = None,
65
unique_id_uses_count: bool = False
66
) -> None:
67
"""
68
Register an event handler for a given event.
69
70
Handlers are called in order: register_first() → register() → register_last()
71
72
Args:
73
event_name: Event name to register handler for
74
handler: Callable event handler that accepts **kwargs
75
unique_id: Unique identifier to prevent duplicate registrations
76
unique_id_uses_count: Whether unique_id uses reference counting
77
78
Raises:
79
ValueError: If handler is not callable or doesn't accept **kwargs
80
"""
81
82
def register_first(
83
self,
84
event_name: str,
85
handler: callable,
86
unique_id: str = None,
87
unique_id_uses_count: bool = False
88
) -> None:
89
"""
90
Register event handler to be called first for an event.
91
92
All handlers registered with register_first() are called before
93
handlers registered with register() and register_last().
94
95
Args:
96
event_name: Event name to register handler for
97
handler: Callable event handler that accepts **kwargs
98
unique_id: Unique identifier to prevent duplicate registrations
99
unique_id_uses_count: Whether unique_id uses reference counting
100
"""
101
102
def register_last(
103
self,
104
event_name: str,
105
handler: callable,
106
unique_id: str = None,
107
unique_id_uses_count: bool = False
108
) -> None:
109
"""
110
Register event handler to be called last for an event.
111
112
All handlers registered with register_last() are called after
113
handlers registered with register_first() and register().
114
115
Args:
116
event_name: Event name to register handler for
117
handler: Callable event handler that accepts **kwargs
118
unique_id: Unique identifier to prevent duplicate registrations
119
unique_id_uses_count: Whether unique_id uses reference counting
120
"""
121
122
def unregister(
123
self,
124
event_name: str,
125
handler: callable = None,
126
unique_id: str = None,
127
unique_id_uses_count: bool = False
128
) -> None:
129
"""
130
Unregister an event handler for a given event.
131
132
Args:
133
event_name: Event name to unregister handler from
134
handler: Handler to unregister (if no unique_id specified)
135
unique_id: Unique identifier of handler to unregister
136
unique_id_uses_count: Whether unique_id uses reference counting
137
"""
138
```
139
140
### Event Aliasing
141
142
Event name aliasing system for backward compatibility and event name transformation.
143
144
```python { .api }
145
class EventAliaser:
146
def __init__(
147
self,
148
event_emitter: HierarchicalEmitter,
149
event_aliases: dict = None
150
):
151
"""
152
Initialize event aliaser with underlying emitter and alias mappings.
153
154
Args:
155
event_emitter: Underlying hierarchical emitter
156
event_aliases: Mapping of old event names to new event names
157
"""
158
159
def emit(self, event_name: str, **kwargs) -> List[Tuple[callable, Any]]:
160
"""Emit event with automatic name aliasing."""
161
162
def emit_until_response(
163
self,
164
event_name: str,
165
**kwargs
166
) -> Tuple[callable, Any]:
167
"""Emit event until response with automatic name aliasing."""
168
169
def register(
170
self,
171
event_name: str,
172
handler: callable,
173
unique_id: str = None,
174
unique_id_uses_count: bool = False
175
) -> None:
176
"""Register handler with automatic event name aliasing."""
177
178
def unregister(
179
self,
180
event_name: str,
181
handler: callable = None,
182
unique_id: str = None,
183
unique_id_uses_count: bool = False
184
) -> None:
185
"""Unregister handler with automatic event name aliasing."""
186
```
187
188
### Utility Functions
189
190
Helper functions for working with event responses.
191
192
```python { .api }
193
def first_non_none_response(
194
responses: List[Tuple[callable, Any]],
195
default: Any = None
196
) -> Any:
197
"""
198
Find first non-None response in a list of handler response tuples.
199
200
Args:
201
responses: List of (handler, response) tuples from emit()
202
default: Default value if no non-None responses found
203
204
Returns:
205
First non-None response or default value
206
207
Example:
208
>>> responses = [(func1, None), (func2, 'foo'), (func3, 'bar')]
209
>>> first_non_none_response(responses)
210
'foo'
211
"""
212
```
213
214
## Event Lifecycle
215
216
### AWS Client Request Lifecycle
217
218
The botocore event system provides hooks throughout the AWS API request lifecycle:
219
220
**Event Flow Order:**
221
1. `provide-client-params.*.*` - Provide additional client parameters
222
2. `before-parameter-build.*.*` - Modify parameters before request building
223
3. `before-call.*.*` - Modify request before sending
224
4. `before-sign.*.*` - Modify request before signing
225
5. `response-received.*.*` - Process response after receiving
226
6. `after-call.*.*` - Process final response
227
7. `needs-retry.*.*` - Determine if request should be retried
228
229
### Common Event Types
230
231
**Parameter Building Events:**
232
- `before-parameter-build.{service}.{operation}` - Modify operation parameters
233
- `provide-client-params.{service}.{operation}` - Add client-level parameters
234
235
**Request Processing Events:**
236
- `before-call.{service}.{operation}` - Modify request before sending
237
- `before-sign.{service}.{operation}` - Modify request before signing
238
239
**Response Processing Events:**
240
- `response-received.{service}.{operation}` - Process HTTP response
241
- `after-call.{service}.{operation}` - Process parsed response
242
243
**Retry Events:**
244
- `needs-retry.{service}.{operation}` - Determine retry behavior
245
246
**Global Events:**
247
- `before-call.*.*` - Called for all service operations
248
- `after-call.*.*` - Called for all service operations
249
- `needs-retry.*.*` - Called for all retry decisions
250
251
## Usage Examples
252
253
### Basic Event Registration
254
255
```python
256
from botocore.session import get_session
257
258
# Create session and client
259
session = get_session()
260
client = session.create_client('s3', region_name='us-east-1')
261
262
# Access the event system
263
events = client.meta.events
264
265
# Register a simple event handler
266
def log_request(**kwargs):
267
print(f"Making request: {kwargs.get('event_name')}")
268
269
events.register('before-call.s3.*', log_request)
270
271
# Make a request - handler will be called
272
response = client.list_buckets()
273
```
274
275
### Custom Authentication Handler
276
277
```python
278
def custom_auth_handler(request, **kwargs):
279
"""Add custom authentication headers to requests."""
280
request.headers['X-Custom-Auth'] = 'my-auth-token'
281
request.headers['X-Request-ID'] = str(uuid.uuid4())
282
283
# Register for all S3 operations
284
events.register('before-sign.s3.*', custom_auth_handler)
285
286
# Register for specific operation only
287
events.register('before-sign.s3.GetObject', custom_auth_handler)
288
```
289
290
### Request/Response Logging
291
292
```python
293
import json
294
import logging
295
296
logger = logging.getLogger(__name__)
297
298
def log_request_params(params, **kwargs):
299
"""Log request parameters before API call."""
300
operation = kwargs.get('event_name', '').split('.')[-1]
301
logger.info(f"Calling {operation} with params: {json.dumps(params, default=str)}")
302
303
def log_response_data(parsed, **kwargs):
304
"""Log response data after API call."""
305
operation = kwargs.get('event_name', '').split('.')[-1]
306
logger.info(f"Response from {operation}: {json.dumps(parsed, default=str)}")
307
308
# Register logging handlers
309
events.register('before-parameter-build.*.*', log_request_params)
310
events.register('after-call.*.*', log_response_data)
311
```
312
313
### Performance Monitoring
314
315
```python
316
import time
317
318
class PerformanceMonitor:
319
def __init__(self):
320
self.timings = {}
321
322
def start_timer(self, **kwargs):
323
"""Record request start time."""
324
event_name = kwargs.get('event_name')
325
self.timings[event_name] = time.time()
326
327
def end_timer(self, **kwargs):
328
"""Calculate and log request duration."""
329
event_name = kwargs.get('event_name')
330
if event_name in self.timings:
331
duration = time.time() - self.timings[event_name]
332
print(f"Request {event_name} took {duration:.3f} seconds")
333
del self.timings[event_name]
334
335
monitor = PerformanceMonitor()
336
337
# Register monitoring handlers
338
events.register('before-call.*.*', monitor.start_timer)
339
events.register('after-call.*.*', monitor.end_timer)
340
```
341
342
### Response Modification
343
344
```python
345
def modify_s3_response(parsed, **kwargs):
346
"""Add custom metadata to S3 responses."""
347
if 'ResponseMetadata' in parsed:
348
parsed['ResponseMetadata']['CustomProcessed'] = True
349
parsed['ResponseMetadata']['ProcessedAt'] = time.time()
350
351
events.register('after-call.s3.*', modify_s3_response)
352
```
353
354
### Error Handling and Retry Logic
355
356
```python
357
from botocore.exceptions import ClientError
358
359
def custom_retry_handler(response, operation, **kwargs):
360
"""Implement custom retry logic."""
361
if response and response.get('Error', {}).get('Code') == 'SlowDown':
362
# Implement custom backoff for S3 SlowDown errors
363
return {
364
'retry': True,
365
'retry_delay': 5.0 # 5 second delay
366
}
367
return None
368
369
def error_notification_handler(parsed, **kwargs):
370
"""Send notifications for specific errors."""
371
if 'Error' in parsed:
372
error_code = parsed['Error']['Code']
373
if error_code in ['AccessDenied', 'InvalidAccessKeyId']:
374
# Send alert for authentication issues
375
print(f"Authentication error detected: {error_code}")
376
377
events.register('needs-retry.s3.*', custom_retry_handler)
378
events.register('after-call.*.*', error_notification_handler)
379
```
380
381
### Session-Level Event Handling
382
383
```python
384
def global_request_logger(**kwargs):
385
"""Log all AWS API requests across all services."""
386
event_parts = kwargs.get('event_name', '').split('.')
387
if len(event_parts) >= 3:
388
service = event_parts[1]
389
operation = event_parts[2]
390
print(f"AWS API Call: {service}.{operation}")
391
392
# Register at session level for all clients
393
session_events = session.get_component('event_emitter')
394
session_events.register('before-call.*.*', global_request_logger)
395
```
396
397
### Event Handler Priorities
398
399
```python
400
def first_handler(**kwargs):
401
print("Called first")
402
403
def middle_handler(**kwargs):
404
print("Called middle")
405
406
def last_handler(**kwargs):
407
print("Called last")
408
409
# Register handlers with different priorities
410
events.register_first('before-call.s3.ListBuckets', first_handler)
411
events.register('before-call.s3.ListBuckets', middle_handler)
412
events.register_last('before-call.s3.ListBuckets', last_handler)
413
414
# When ListBuckets is called, output will be:
415
# Called first
416
# Called middle
417
# Called last
418
```
419
420
### Conditional Event Processing
421
422
```python
423
def conditional_handler(request, **kwargs):
424
"""Only process requests to specific buckets."""
425
params = kwargs.get('params', {})
426
bucket_name = params.get('Bucket', '')
427
428
if bucket_name.startswith('sensitive-'):
429
# Add extra security headers for sensitive buckets
430
request.headers['X-Extra-Security'] = 'enabled'
431
print(f"Enhanced security applied to {bucket_name}")
432
433
events.register('before-sign.s3.*', conditional_handler)
434
```
435
436
### Unique Event Handler Registration
437
438
```python
439
def singleton_handler(**kwargs):
440
"""Handler that should only be registered once."""
441
print("Singleton handler called")
442
443
# Register with unique ID to prevent duplicates
444
events.register(
445
'before-call.s3.*',
446
singleton_handler,
447
unique_id='singleton-handler'
448
)
449
450
# Subsequent registrations with same unique_id are ignored
451
events.register(
452
'before-call.s3.*',
453
singleton_handler,
454
unique_id='singleton-handler' # This will be ignored
455
)
456
```
457
458
### Event Unregistration
459
460
```python
461
def temporary_handler(**kwargs):
462
print("Temporary handler")
463
464
# Register handler
465
events.register('before-call.s3.ListBuckets', temporary_handler)
466
467
# Use it for some operations
468
client.list_buckets() # Handler called
469
470
# Unregister when no longer needed
471
events.unregister('before-call.s3.ListBuckets', temporary_handler)
472
473
# Handler no longer called
474
client.list_buckets() # Handler not called
475
```
476
477
## Integration Patterns
478
479
### Custom Client Configuration
480
481
```python
482
from botocore.client import BaseClient
483
from botocore.config import Config
484
485
def setup_enhanced_s3_client():
486
"""Create S3 client with enhanced event handling."""
487
488
# Create client with custom configuration
489
config = Config(
490
retries={'max_attempts': 3},
491
read_timeout=30
492
)
493
494
client = session.create_client('s3', config=config)
495
events = client.meta.events
496
497
# Add custom event handlers
498
events.register('before-call.s3.*', add_request_metadata)
499
events.register('after-call.s3.*', log_response_metadata)
500
events.register('needs-retry.s3.*', custom_retry_strategy)
501
502
return client
503
504
def add_request_metadata(request, **kwargs):
505
"""Add metadata to all S3 requests."""
506
request.headers['X-Client-Version'] = '1.0'
507
request.headers['X-Request-Source'] = 'enhanced-client'
508
509
def log_response_metadata(parsed, **kwargs):
510
"""Log S3 response metadata."""
511
if 'ResponseMetadata' in parsed:
512
request_id = parsed['ResponseMetadata'].get('RequestId')
513
print(f"S3 Request ID: {request_id}")
514
515
def custom_retry_strategy(response, **kwargs):
516
"""Implement enhanced retry strategy."""
517
if response and 'Error' in response:
518
error_code = response['Error']['Code']
519
if error_code == 'ServiceUnavailable':
520
return {'retry': True, 'retry_delay': 2.0}
521
return None
522
523
# Use enhanced client
524
s3_client = setup_enhanced_s3_client()
525
```
526
527
### Multi-Service Event Coordination
528
529
```python
530
class AWSOperationTracker:
531
"""Track operations across multiple AWS services."""
532
533
def __init__(self):
534
self.active_operations = {}
535
self.completed_operations = []
536
537
def start_operation(self, **kwargs):
538
"""Track when an operation starts."""
539
event_name = kwargs.get('event_name', '')
540
operation_id = f"{event_name}-{time.time()}"
541
542
self.active_operations[operation_id] = {
543
'event': event_name,
544
'start_time': time.time(),
545
'params': kwargs.get('params', {})
546
}
547
548
print(f"Started operation: {operation_id}")
549
550
def complete_operation(self, **kwargs):
551
"""Track when an operation completes."""
552
event_name = kwargs.get('event_name', '')
553
554
# Find matching active operation
555
for op_id, op_data in list(self.active_operations.items()):
556
if op_data['event'] == event_name:
557
duration = time.time() - op_data['start_time']
558
559
self.completed_operations.append({
560
'id': op_id,
561
'event': event_name,
562
'duration': duration
563
})
564
565
del self.active_operations[op_id]
566
print(f"Completed operation: {op_id} ({duration:.3f}s)")
567
break
568
569
# Create tracker and register across multiple clients
570
tracker = AWSOperationTracker()
571
572
# Register for multiple services
573
for service in ['s3', 'ec2', 'dynamodb']:
574
client = session.create_client(service)
575
events = client.meta.events
576
577
events.register(f'before-call.{service}.*', tracker.start_operation)
578
events.register(f'after-call.{service}.*', tracker.complete_operation)
579
```
580
581
## Best Practices
582
583
### Event Handler Design
584
585
```python
586
# Good: Handler accepts **kwargs and handles missing parameters gracefully
587
def robust_handler(**kwargs):
588
event_name = kwargs.get('event_name', 'unknown')
589
params = kwargs.get('params', {})
590
591
# Process event safely
592
if 'Bucket' in params:
593
print(f"Processing bucket: {params['Bucket']}")
594
595
# Bad: Handler has specific parameter signature
596
def fragile_handler(event_name, params): # Will fail if parameters change
597
print(f"Event: {event_name}, Bucket: {params['Bucket']}")
598
599
# Good: Register with appropriate specificity
600
events.register('before-call.s3.GetObject', specific_handler) # Specific operation
601
events.register('before-call.s3.*', service_handler) # All S3 operations
602
events.register('before-call.*.*', global_handler) # All operations
603
604
# Good: Use unique IDs for singleton handlers
605
events.register(
606
'before-call.s3.*',
607
auth_handler,
608
unique_id='custom-auth' # Prevents duplicate registration
609
)
610
```
611
612
### Error Handling in Event Handlers
613
614
```python
615
def safe_event_handler(**kwargs):
616
"""Event handler with proper error handling."""
617
try:
618
# Handler logic here
619
request = kwargs.get('request')
620
if request:
621
request.headers['X-Custom-Header'] = 'value'
622
except Exception as e:
623
# Log error but don't raise to avoid breaking the request
624
logger.error(f"Event handler error: {e}")
625
# Optionally re-raise for critical handlers
626
# raise
627
628
def critical_event_handler(**kwargs):
629
"""Handler where errors should stop request processing."""
630
try:
631
# Critical validation or security logic
632
validate_request_security(kwargs.get('request'))
633
except SecurityError:
634
# Re-raise to stop request processing
635
raise
636
except Exception as e:
637
logger.error(f"Critical handler error: {e}")
638
raise # Re-raise all errors for critical handlers
639
```
640
641
### Performance Considerations
642
643
```python
644
# Good: Efficient event handler
645
def efficient_handler(**kwargs):
646
# Quick checks first
647
if 'request' not in kwargs:
648
return
649
650
request = kwargs['request']
651
652
# Avoid expensive operations in handlers
653
if should_process_request(request):
654
process_request_efficiently(request)
655
656
# Good: Conditional registration
657
if enable_detailed_logging:
658
events.register('before-call.*.*', detailed_logging_handler)
659
else:
660
events.register('before-call.*.*', basic_logging_handler)
661
662
# Good: Unregister when no longer needed
663
def temporary_debugging():
664
def debug_handler(**kwargs):
665
print(f"Debug: {kwargs}")
666
667
# Register temporarily
668
events.register('before-call.s3.*', debug_handler)
669
670
try:
671
# Perform operations with debugging
672
client.list_buckets()
673
finally:
674
# Clean up
675
events.unregister('before-call.s3.*', debug_handler)
676
```
677
678
The event system provides powerful hooks for customizing AWS API interactions while maintaining clean separation of concerns and extensible architecture patterns.