or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

client.mdconfig.mdcredentials.mdevents.mdexceptions.mdindex.mdmodels.mdpagination.mdresponse.mdsession.mdtesting.mdwaiters.md

events.mddocs/

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.