0
# Signal System
1
2
Event-driven hooks for application lifecycle, request/response processing, WebSocket handling, error management, and template rendering with comprehensive signal support for monitoring and extending application behavior.
3
4
## Capabilities
5
6
### Application Lifecycle Signals
7
8
Signals fired during application context creation, destruction, and teardown phases.
9
10
```python { .api }
11
# Application context signals
12
appcontext_pushed: Namespace
13
"""
14
Signal sent when application context is pushed.
15
16
Sender: The application instance
17
Arguments: None
18
19
Usage:
20
@appcontext_pushed.connect
21
def on_app_context_pushed(sender, **extra):
22
# Application context is now available
23
pass
24
"""
25
26
appcontext_popped: Namespace
27
"""
28
Signal sent when application context is popped.
29
30
Sender: The application instance
31
Arguments: None
32
33
Usage:
34
@appcontext_popped.connect
35
def on_app_context_popped(sender, **extra):
36
# Application context is being removed
37
pass
38
"""
39
40
appcontext_tearing_down: Namespace
41
"""
42
Signal sent when application context is tearing down.
43
44
Sender: The application instance
45
Arguments:
46
exc: Exception that caused teardown (None for normal teardown)
47
48
Usage:
49
@appcontext_tearing_down.connect
50
def on_app_context_teardown(sender, exc=None, **extra):
51
# Clean up app context resources
52
if exc:
53
# Handle teardown due to exception
54
pass
55
"""
56
```
57
58
### Request Lifecycle Signals
59
60
Signals for HTTP request processing lifecycle events.
61
62
```python { .api }
63
# Request processing signals
64
request_started: Namespace
65
"""
66
Signal sent when request processing starts.
67
68
Sender: The application instance
69
Arguments: None
70
71
Usage:
72
@request_started.connect
73
def on_request_started(sender, **extra):
74
# Request processing has begun
75
g.start_time = time.time()
76
"""
77
78
request_finished: Namespace
79
"""
80
Signal sent when request processing finishes successfully.
81
82
Sender: The application instance
83
Arguments:
84
response: The response object
85
86
Usage:
87
@request_finished.connect
88
def on_request_finished(sender, response, **extra):
89
# Request completed successfully
90
duration = time.time() - g.start_time
91
log_request(request, response, duration)
92
"""
93
94
request_tearing_down: Namespace
95
"""
96
Signal sent when request context is tearing down.
97
98
Sender: The application instance
99
Arguments:
100
exc: Exception that caused teardown (None for normal teardown)
101
102
Usage:
103
@request_tearing_down.connect
104
def on_request_teardown(sender, exc=None, **extra):
105
# Clean up request resources
106
if hasattr(g, 'db_connection'):
107
g.db_connection.close()
108
"""
109
```
110
111
### WebSocket Lifecycle Signals
112
113
Signals for WebSocket connection lifecycle events.
114
115
```python { .api }
116
# WebSocket processing signals
117
websocket_started: Namespace
118
"""
119
Signal sent when WebSocket processing starts.
120
121
Sender: The application instance
122
Arguments: None
123
124
Usage:
125
@websocket_started.connect
126
def on_websocket_started(sender, **extra):
127
# WebSocket connection has begun
128
active_connections.add(websocket)
129
"""
130
131
websocket_finished: Namespace
132
"""
133
Signal sent when WebSocket processing finishes.
134
135
Sender: The application instance
136
Arguments: None
137
138
Usage:
139
@websocket_finished.connect
140
def on_websocket_finished(sender, **extra):
141
# WebSocket connection ended
142
active_connections.discard(websocket)
143
"""
144
145
websocket_tearing_down: Namespace
146
"""
147
Signal sent when WebSocket context is tearing down.
148
149
Sender: The application instance
150
Arguments:
151
exc: Exception that caused teardown (None for normal teardown)
152
153
Usage:
154
@websocket_tearing_down.connect
155
def on_websocket_teardown(sender, exc=None, **extra):
156
# Clean up WebSocket resources
157
cleanup_websocket_resources()
158
"""
159
```
160
161
### Template Signals
162
163
Signals fired during template rendering operations.
164
165
```python { .api }
166
# Template rendering signals
167
before_render_template: Namespace
168
"""
169
Signal sent before template rendering begins.
170
171
Sender: The application instance
172
Arguments:
173
template: The template object
174
context: Template context dictionary
175
176
Usage:
177
@before_render_template.connect
178
def on_before_render(sender, template, context, **extra):
179
# Modify context or log template access
180
context['render_start_time'] = time.time()
181
"""
182
183
template_rendered: Namespace
184
"""
185
Signal sent after template has been rendered.
186
187
Sender: The application instance
188
Arguments:
189
template: The template object
190
context: Template context dictionary
191
192
Usage:
193
@template_rendered.connect
194
def on_template_rendered(sender, template, context, **extra):
195
# Log template rendering or clean up
196
duration = time.time() - context.get('render_start_time', 0)
197
log_template_render(template.name, duration)
198
"""
199
```
200
201
### Exception Signals
202
203
Signals for error handling and exception management.
204
205
```python { .api }
206
# Exception handling signals
207
got_request_exception: Namespace
208
"""
209
Signal sent when request processing encounters an exception.
210
211
Sender: The application instance
212
Arguments:
213
exception: The exception object
214
215
Usage:
216
@got_request_exception.connect
217
def on_request_exception(sender, exception, **extra):
218
# Log or handle request exceptions
219
error_tracker.report_exception(exception, request)
220
"""
221
222
got_websocket_exception: Namespace
223
"""
224
Signal sent when WebSocket processing encounters an exception.
225
226
Sender: The application instance
227
Arguments:
228
exception: The exception object
229
230
Usage:
231
@got_websocket_exception.connect
232
def on_websocket_exception(sender, exception, **extra):
233
# Log or handle WebSocket exceptions
234
error_tracker.report_websocket_exception(exception, websocket)
235
"""
236
237
got_background_exception: Namespace
238
"""
239
Signal sent when background task encounters an exception.
240
241
Sender: The application instance
242
Arguments:
243
exception: The exception object
244
245
Usage:
246
@got_background_exception.connect
247
def on_background_exception(sender, exception, **extra):
248
# Handle background task exceptions
249
background_error_handler.handle(exception)
250
"""
251
252
got_serving_exception: Namespace
253
"""
254
Signal sent when server encounters an exception during serving.
255
256
Sender: The application instance
257
Arguments:
258
exception: The exception object
259
260
Usage:
261
@got_serving_exception.connect
262
def on_serving_exception(sender, exception, **extra):
263
# Handle server-level exceptions
264
server_monitor.report_exception(exception)
265
"""
266
```
267
268
### Flash Message Signals
269
270
Signals for flash message system events.
271
272
```python { .api }
273
# Flash messaging signals
274
message_flashed: Namespace
275
"""
276
Signal sent when a flash message is added.
277
278
Sender: The application instance
279
Arguments:
280
message: The flash message text
281
category: The message category
282
283
Usage:
284
@message_flashed.connect
285
def on_message_flashed(sender, message, category, **extra):
286
# Log or process flash messages
287
audit_log.record_flash_message(message, category)
288
"""
289
```
290
291
### Signal Availability
292
293
Constant indicating whether signal support is available.
294
295
```python { .api }
296
signals_available: bool
297
"""
298
Boolean indicating whether signal support is available.
299
300
Value: True if blinker is installed, False otherwise
301
302
Usage:
303
if signals_available:
304
# Use signal-based features
305
setup_signal_handlers()
306
else:
307
# Fallback to non-signal approach
308
setup_polling_monitor()
309
"""
310
```
311
312
### Usage Examples
313
314
#### Application Monitoring
315
316
```python
317
from quart import Quart, g
318
from quart.signals import (
319
request_started, request_finished, request_tearing_down,
320
got_request_exception, appcontext_pushed, appcontext_popped
321
)
322
import time
323
324
app = Quart(__name__)
325
326
# Request timing and monitoring
327
@request_started.connect_via(app)
328
def on_request_start(sender, **extra):
329
g.request_start_time = time.time()
330
g.request_id = generate_request_id()
331
332
@request_finished.connect_via(app)
333
def on_request_finish(sender, response, **extra):
334
duration = time.time() - g.request_start_time
335
336
# Log request metrics
337
metrics.record_request_duration(duration)
338
metrics.increment(f'requests.status.{response.status_code}')
339
340
# Add timing header
341
response.headers['X-Response-Time'] = f'{duration:.3f}s'
342
response.headers['X-Request-ID'] = g.request_id
343
344
@got_request_exception.connect_via(app)
345
def on_request_exception(sender, exception, **extra):
346
# Log exception with context
347
error_logger.error(
348
f"Request {g.request_id} failed: {exception}",
349
extra={
350
'request_id': g.request_id,
351
'path': request.path,
352
'method': request.method,
353
'user_agent': request.headers.get('User-Agent'),
354
'duration': time.time() - g.request_start_time
355
}
356
)
357
358
# Report to error tracking service
359
error_tracker.capture_exception(exception, {
360
'request_id': g.request_id,
361
'request': {
362
'url': request.url,
363
'method': request.method,
364
'headers': dict(request.headers)
365
}
366
})
367
368
@request_tearing_down.connect_via(app)
369
def cleanup_request(sender, exc=None, **extra):
370
# Clean up request-specific resources
371
if hasattr(g, 'database_connection'):
372
g.database_connection.close()
373
374
if hasattr(g, 'cache_client'):
375
g.cache_client.disconnect()
376
```
377
378
#### WebSocket Connection Management
379
380
```python
381
from quart import Quart, websocket
382
from quart.signals import (
383
websocket_started, websocket_finished, websocket_tearing_down,
384
got_websocket_exception
385
)
386
387
app = Quart(__name__)
388
389
# Track active WebSocket connections
390
active_websockets = set()
391
websocket_stats = {'total_connections': 0, 'active_connections': 0}
392
393
@websocket_started.connect_via(app)
394
def on_websocket_start(sender, **extra):
395
active_websockets.add(websocket)
396
websocket_stats['total_connections'] += 1
397
websocket_stats['active_connections'] = len(active_websockets)
398
399
# Log connection details
400
app.logger.info(f"WebSocket connected from {websocket.remote_addr}, "
401
f"total active: {len(active_websockets)}")
402
403
@websocket_finished.connect_via(app)
404
def on_websocket_finish(sender, **extra):
405
active_websockets.discard(websocket)
406
websocket_stats['active_connections'] = len(active_websockets)
407
408
app.logger.info(f"WebSocket disconnected, "
409
f"remaining active: {len(active_websockets)}")
410
411
@websocket_tearing_down.connect_via(app)
412
def cleanup_websocket(sender, exc=None, **extra):
413
# Clean up WebSocket-specific resources
414
if hasattr(g, 'websocket_subscriptions'):
415
for subscription in g.websocket_subscriptions:
416
subscription.unsubscribe()
417
418
@got_websocket_exception.connect_via(app)
419
def on_websocket_exception(sender, exception, **extra):
420
app.logger.error(f"WebSocket error: {exception}")
421
422
# Attempt graceful cleanup
423
try:
424
await websocket.close(code=1011, reason="Internal server error")
425
except:
426
pass
427
428
@app.websocket('/ws/stats')
429
async def websocket_stats_endpoint():
430
await websocket.accept()
431
432
try:
433
while True:
434
await websocket.send_json(websocket_stats)
435
await asyncio.sleep(5)
436
except ConnectionClosed:
437
pass
438
```
439
440
#### Template Rendering Analytics
441
442
```python
443
from quart import Quart, render_template
444
from quart.signals import before_render_template, template_rendered
445
import time
446
from collections import defaultdict, Counter
447
448
app = Quart(__name__)
449
450
# Template rendering statistics
451
template_stats = {
452
'render_counts': Counter(),
453
'render_times': defaultdict(list),
454
'context_sizes': defaultdict(list)
455
}
456
457
@before_render_template.connect_via(app)
458
def on_before_render(sender, template, context, **extra):
459
# Record template access
460
template_stats['render_counts'][template.name] += 1
461
462
# Store render start time in context
463
context['_render_start'] = time.time()
464
465
# Record context size
466
context_size = len(str(context))
467
template_stats['context_sizes'][template.name].append(context_size)
468
469
# Add common template variables
470
context['app_version'] = app.config.get('VERSION', '1.0.0')
471
context['render_timestamp'] = time.time()
472
473
@template_rendered.connect_via(app)
474
def on_template_rendered(sender, template, context, **extra):
475
# Calculate render time
476
if '_render_start' in context:
477
render_time = time.time() - context['_render_start']
478
template_stats['render_times'][template.name].append(render_time)
479
480
# Log slow templates
481
if render_time > 0.1: # 100ms threshold
482
app.logger.warning(f"Slow template render: {template.name} "
483
f"took {render_time:.3f}s")
484
485
@app.route('/template-stats')
486
async def template_statistics():
487
# Calculate statistics
488
stats = {}
489
for template_name in template_stats['render_counts']:
490
times = template_stats['render_times'][template_name]
491
sizes = template_stats['context_sizes'][template_name]
492
493
stats[template_name] = {
494
'render_count': template_stats['render_counts'][template_name],
495
'avg_render_time': sum(times) / len(times) if times else 0,
496
'max_render_time': max(times) if times else 0,
497
'avg_context_size': sum(sizes) / len(sizes) if sizes else 0
498
}
499
500
return await render_template('template_stats.html', stats=stats)
501
```
502
503
#### Error Tracking and Alerting
504
505
```python
506
from quart import Quart, request
507
from quart.signals import (
508
got_request_exception, got_websocket_exception,
509
got_background_exception, got_serving_exception
510
)
511
from collections import deque
512
import asyncio
513
514
app = Quart(__name__)
515
516
# Error tracking
517
recent_errors = deque(maxlen=100) # Keep last 100 errors
518
error_counts = Counter()
519
520
async def send_alert(error_type, exception, context=None):
521
"""Send alert for critical errors."""
522
if app.config.get('ALERTS_ENABLED'):
523
alert_data = {
524
'type': error_type,
525
'exception': str(exception),
526
'timestamp': time.time(),
527
'context': context or {}
528
}
529
530
# Send to monitoring service
531
await monitoring_service.send_alert(alert_data)
532
533
@got_request_exception.connect_via(app)
534
def on_request_error(sender, exception, **extra):
535
error_type = type(exception).__name__
536
error_counts[error_type] += 1
537
538
error_data = {
539
'type': 'request_error',
540
'exception': str(exception),
541
'exception_type': error_type,
542
'path': request.path,
543
'method': request.method,
544
'timestamp': time.time()
545
}
546
547
recent_errors.append(error_data)
548
549
# Send alert for critical errors
550
if error_type in ['DatabaseError', 'ExternalServiceError']:
551
asyncio.create_task(send_alert('request_error', exception, {
552
'path': request.path,
553
'method': request.method
554
}))
555
556
@got_websocket_exception.connect_via(app)
557
def on_websocket_error(sender, exception, **extra):
558
error_type = type(exception).__name__
559
error_counts[f'websocket_{error_type}'] += 1
560
561
error_data = {
562
'type': 'websocket_error',
563
'exception': str(exception),
564
'exception_type': error_type,
565
'path': websocket.path,
566
'timestamp': time.time()
567
}
568
569
recent_errors.append(error_data)
570
571
@got_background_exception.connect_via(app)
572
def on_background_error(sender, exception, **extra):
573
error_type = type(exception).__name__
574
error_counts[f'background_{error_type}'] += 1
575
576
# Background errors are often critical
577
asyncio.create_task(send_alert('background_error', exception))
578
579
@app.route('/error-dashboard')
580
async def error_dashboard():
581
return await render_template('error_dashboard.html',
582
recent_errors=list(recent_errors),
583
error_counts=dict(error_counts))
584
```
585
586
#### Signal-Based Plugin System
587
588
```python
589
from quart import Quart
590
from quart.signals import appcontext_pushed
591
from blinker import Namespace
592
593
app = Quart(__name__)
594
595
# Custom application signals
596
app_signals = Namespace()
597
user_logged_in = app_signals.signal('user-logged-in')
598
data_updated = app_signals.signal('data-updated')
599
600
# Plugin registration system
601
@appcontext_pushed.connect_via(app)
602
def load_plugins(sender, **extra):
603
if not hasattr(g, 'plugins_loaded'):
604
# Load and initialize plugins
605
for plugin_name in app.config.get('ENABLED_PLUGINS', []):
606
plugin = load_plugin(plugin_name)
607
plugin.initialize(app)
608
609
g.plugins_loaded = True
610
611
# Plugin example: Audit logging
612
class AuditLogPlugin:
613
def initialize(self, app):
614
user_logged_in.connect(self.log_user_login, app)
615
data_updated.connect(self.log_data_update, app)
616
617
def log_user_login(self, sender, user, **extra):
618
audit_log.info(f"User {user.username} logged in from {request.remote_addr}")
619
620
def log_data_update(self, sender, model, action, **extra):
621
audit_log.info(f"Data update: {model.__class__.__name__} {action}")
622
623
# Usage in application code
624
@app.route('/login', methods=['POST'])
625
async def login():
626
user = await authenticate_user(request.form)
627
if user:
628
session['user_id'] = user.id
629
630
# Emit custom signal
631
user_logged_in.send(app, user=user)
632
633
return redirect(url_for('dashboard'))
634
635
@app.route('/api/data', methods=['POST'])
636
async def update_data():
637
data = await request.get_json()
638
model = await update_model(data)
639
640
# Emit custom signal
641
data_updated.send(app, model=model, action='update')
642
643
return jsonify({'status': 'updated'})
644
```