0
# Configuration and Utilities
1
2
JupyterHub provides a comprehensive configuration system using traitlets along with utility functions for common operations including token management, URL handling, SSL setup, and asynchronous programming patterns.
3
4
## Capabilities
5
6
### Configuration System (Traitlets)
7
8
Custom traitlet types for JupyterHub configuration validation and processing.
9
10
```python { .api }
11
class Command(TraitType):
12
"""
13
Traitlet for command-line commands with shell and string list support.
14
15
Validates and processes command specifications for spawners and services.
16
"""
17
18
def validate(self, obj, value):
19
"""
20
Validate command specification.
21
22
Args:
23
obj: Object being configured
24
value: Command value (string, list, or callable)
25
26
Returns:
27
Validated command as list of strings
28
29
Raises:
30
TraitError: If command format is invalid
31
"""
32
33
class URLPrefix(TraitType):
34
"""
35
Traitlet for URL prefix validation and normalization.
36
37
Ensures URL prefixes are properly formatted with leading/trailing slashes.
38
"""
39
40
def validate(self, obj, value):
41
"""
42
Validate and normalize URL prefix.
43
44
Args:
45
obj: Object being configured
46
value: URL prefix string
47
48
Returns:
49
Normalized URL prefix with proper slash handling
50
"""
51
52
class ByteSpecification(TraitType):
53
"""
54
Traitlet for memory/storage size specifications with unit support.
55
56
Accepts values like '1G', '512M', '2048K' and converts to bytes.
57
"""
58
59
UNIT_MAP = {
60
'K': 1024,
61
'M': 1024**2,
62
'G': 1024**3,
63
'T': 1024**4
64
}
65
66
def validate(self, obj, value):
67
"""
68
Validate and convert byte specification to integer bytes.
69
70
Args:
71
obj: Object being configured
72
value: Size specification (int, string with units)
73
74
Returns:
75
Size in bytes as integer
76
"""
77
78
class Callable(TraitType):
79
"""
80
Traitlet for callable objects with import string support.
81
82
Accepts callable objects or import strings that resolve to callables.
83
"""
84
85
def validate(self, obj, value):
86
"""
87
Validate and resolve callable specification.
88
89
Args:
90
obj: Object being configured
91
value: Callable or import string
92
93
Returns:
94
Resolved callable object
95
"""
96
97
class EntryPointType(TraitType):
98
"""
99
Traitlet for loading Python entry points for plugin systems.
100
101
Supports authenticators, spawners, proxies, and other plugin types.
102
"""
103
104
def __init__(self, entry_point_group, **kwargs):
105
"""
106
Initialize entry point loader.
107
108
Args:
109
entry_point_group: Entry point group name (e.g., 'jupyterhub.authenticators')
110
**kwargs: Additional traitlet options
111
"""
112
self.entry_point_group = entry_point_group
113
super().__init__(**kwargs)
114
115
def validate(self, obj, value):
116
"""
117
Load plugin from entry point specification.
118
119
Args:
120
obj: Object being configured
121
value: Entry point name or class
122
123
Returns:
124
Loaded plugin class
125
"""
126
```
127
128
### Token Management Utilities
129
130
Secure token generation, hashing, and validation functions.
131
132
```python { .api }
133
def new_token(length: int = 32, entropy: int = None) -> str:
134
"""
135
Generate a new random API token.
136
137
Args:
138
length: Token length in characters (default: 32)
139
entropy: Additional entropy source (optional)
140
141
Returns:
142
Random token string (URL-safe base64)
143
"""
144
145
def hash_token(token: str, salt: bytes = None, rounds: int = 16384) -> str:
146
"""
147
Hash a token for secure storage using PBKDF2.
148
149
Args:
150
token: Token string to hash
151
salt: Salt bytes (generated if None)
152
rounds: PBKDF2 iteration count
153
154
Returns:
155
Base64-encoded hash suitable for database storage
156
"""
157
158
def compare_token(token: str, hashed: str) -> bool:
159
"""
160
Compare a token against its stored hash.
161
162
Args:
163
token: Plain token string
164
hashed: Stored hash from hash_token()
165
166
Returns:
167
True if token matches hash
168
"""
169
170
def token_authenticated(method):
171
"""
172
Decorator for methods that require token authentication.
173
174
Args:
175
method: Method to protect with token auth
176
177
Returns:
178
Decorated method that checks for valid token
179
"""
180
```
181
182
### URL and Path Utilities
183
184
Functions for safe URL and path manipulation in web contexts.
185
186
```python { .api }
187
def url_path_join(*pieces) -> str:
188
"""
189
Join URL path components with proper slash handling.
190
191
Args:
192
*pieces: URL path components to join
193
194
Returns:
195
Properly joined URL path with normalized slashes
196
"""
197
198
def url_escape_path(path: str, safe: str = '@!:$&\'()*+,;=') -> str:
199
"""
200
URL-encode path components while preserving safe characters.
201
202
Args:
203
path: Path string to encode
204
safe: Characters to leave unencoded
205
206
Returns:
207
URL-encoded path string
208
"""
209
210
def url_unescape_path(path: str) -> str:
211
"""
212
URL-decode path components.
213
214
Args:
215
path: URL-encoded path string
216
217
Returns:
218
Decoded path string
219
"""
220
221
def guess_base_url(url: str) -> str:
222
"""
223
Guess the base URL from a full URL.
224
225
Args:
226
url: Full URL string
227
228
Returns:
229
Base URL (protocol + host + port)
230
"""
231
```
232
233
### Async and Concurrency Utilities
234
235
Helper functions for asynchronous programming patterns in JupyterHub.
236
237
```python { .api }
238
def maybe_future(value):
239
"""
240
Convert value to Future if it's not already awaitable.
241
242
Args:
243
value: Value that may or may not be awaitable
244
245
Returns:
246
Future/coroutine that can be awaited
247
"""
248
249
def exponential_backoff(func,
250
max_retries: int = 5,
251
initial_delay: float = 1.0,
252
max_delay: float = 60.0,
253
backoff_factor: float = 2.0):
254
"""
255
Decorator for exponential backoff retry logic.
256
257
Args:
258
func: Function to wrap with retry logic
259
max_retries: Maximum number of retry attempts
260
initial_delay: Initial delay between retries (seconds)
261
max_delay: Maximum delay between retries (seconds)
262
backoff_factor: Multiplier for delay on each retry
263
264
Returns:
265
Decorated function with retry logic
266
"""
267
268
async def cancel_tasks(tasks):
269
"""
270
Cancel a collection of asyncio tasks gracefully.
271
272
Args:
273
tasks: Iterable of asyncio tasks to cancel
274
"""
275
276
def run_sync(async_func):
277
"""
278
Run an async function synchronously.
279
280
Args:
281
async_func: Async function to run
282
283
Returns:
284
Result of the async function
285
"""
286
```
287
288
### Network and SSL Utilities
289
290
Functions for network operations and SSL context management.
291
292
```python { .api }
293
def make_ssl_context(keyfile: str = None,
294
certfile: str = None,
295
cafile: str = None,
296
verify_mode: ssl.VerifyMode = None,
297
check_hostname: bool = None,
298
**kwargs) -> ssl.SSLContext:
299
"""
300
Create SSL context with common JupyterHub defaults.
301
302
Args:
303
keyfile: Path to SSL private key file
304
certfile: Path to SSL certificate file
305
cafile: Path to CA certificate file
306
verify_mode: SSL verification mode
307
check_hostname: Whether to verify hostname
308
**kwargs: Additional SSL context options
309
310
Returns:
311
Configured SSL context
312
"""
313
314
def random_port() -> int:
315
"""
316
Find a random available port.
317
318
Returns:
319
Available port number
320
"""
321
322
def is_valid_ip(ip: str) -> bool:
323
"""
324
Check if string is a valid IP address.
325
326
Args:
327
ip: IP address string to validate
328
329
Returns:
330
True if valid IPv4 or IPv6 address
331
"""
332
333
def get_server_info(url: str) -> Dict[str, Any]:
334
"""
335
Get server information from URL.
336
337
Args:
338
url: Server URL to analyze
339
340
Returns:
341
Dictionary with host, port, protocol info
342
"""
343
```
344
345
### Database and ORM Utilities
346
347
Utilities for database operations and schema management.
348
349
```python { .api }
350
def mysql_large_prefix_check():
351
"""
352
Check MySQL configuration for large index prefix support.
353
354
Raises:
355
DatabaseError: If MySQL doesn't support required index sizes
356
"""
357
358
def upgrade_if_needed(db_url: str, log=None):
359
"""
360
Upgrade database schema if needed.
361
362
Args:
363
db_url: Database connection URL
364
log: Logger instance for output
365
"""
366
367
def get_schema_version(db_url: str) -> str:
368
"""
369
Get current database schema version.
370
371
Args:
372
db_url: Database connection URL
373
374
Returns:
375
Schema version string
376
"""
377
```
378
379
## Usage Examples
380
381
### Configuration with Custom Traitlets
382
383
```python
384
# jupyterhub_config.py using custom traitlets
385
c = get_config()
386
387
# Memory limits using ByteSpecification
388
c.Spawner.mem_limit = '2G' # Converted to 2147483648 bytes
389
c.Spawner.mem_guarantee = '512M' # Converted to 536870912 bytes
390
391
# URL prefix configuration
392
c.JupyterHub.base_url = '/hub/' # Normalized to '/hub/'
393
394
# Command configuration
395
c.Spawner.cmd = ['jupyterhub-singleuser'] # Validated as command list
396
c.LocalProcessSpawner.shell_cmd = ['/bin/bash', '-l', '-c']
397
398
# Plugin loading via entry points
399
c.JupyterHub.authenticator_class = 'pam' # Loads PAMAuthenticator
400
c.JupyterHub.spawner_class = 'localprocess' # Loads LocalProcessSpawner
401
```
402
403
### Token Management
404
405
```python
406
from jupyterhub.utils import new_token, hash_token, compare_token
407
from jupyterhub.orm import APIToken
408
409
# Generate API token for user
410
user = db.query(User).filter(User.name == 'alice').first()
411
token = new_token(length=32)
412
hashed = hash_token(token)
413
414
# Store in database
415
api_token = APIToken(
416
user=user,
417
hashed=hashed,
418
prefix=token[:4], # Store prefix for identification
419
note='CLI access token'
420
)
421
db.add(api_token)
422
db.commit()
423
424
# Validate token later
425
def validate_api_token(provided_token):
426
"""Validate API token against database"""
427
prefix = provided_token[:4]
428
token_record = db.query(APIToken).filter(
429
APIToken.prefix == prefix
430
).first()
431
432
if token_record and compare_token(provided_token, token_record.hashed):
433
return token_record.user
434
return None
435
```
436
437
### URL Handling
438
439
```python
440
from jupyterhub.utils import url_path_join, url_escape_path
441
442
# Safe URL path joining
443
base_url = '/hub'
444
user_name = 'alice@example.com'
445
server_name = 'my-server'
446
447
# Build user server URL safely
448
server_url = url_path_join(
449
base_url,
450
'user',
451
url_escape_path(user_name),
452
url_escape_path(server_name)
453
)
454
# Result: '/hub/user/alice%40example.com/my-server'
455
456
# Build API endpoint URLs
457
api_url = url_path_join(base_url, 'api', 'users', url_escape_path(user_name))
458
# Result: '/hub/api/users/alice%40example.com'
459
```
460
461
### Async Utilities
462
463
```python
464
from jupyterhub.utils import maybe_future, exponential_backoff
465
import asyncio
466
467
# Convert sync/async values consistently
468
async def handle_result(result):
469
"""Handle potentially async result"""
470
# maybe_future ensures we can always await
471
final_result = await maybe_future(result)
472
return final_result
473
474
# Retry with exponential backoff
475
@exponential_backoff(max_retries=3, initial_delay=1.0)
476
async def unreliable_operation():
477
"""Operation that might fail temporarily"""
478
# Simulate operation that might fail
479
if random.random() < 0.5:
480
raise Exception("Temporary failure")
481
return "Success"
482
483
# Usage
484
try:
485
result = await unreliable_operation()
486
print(f"Operation succeeded: {result}")
487
except Exception as e:
488
print(f"Operation failed after retries: {e}")
489
```
490
491
### SSL Configuration
492
493
```python
494
from jupyterhub.utils import make_ssl_context
495
496
# Create SSL context for HTTPS
497
ssl_context = make_ssl_context(
498
keyfile='/path/to/private.key',
499
certfile='/path/to/certificate.crt',
500
cafile='/path/to/ca-bundle.crt'
501
)
502
503
# Use in JupyterHub configuration
504
c.JupyterHub.ssl_key = '/path/to/private.key'
505
c.JupyterHub.ssl_cert = '/path/to/certificate.crt'
506
507
# Or programmatically
508
app = JupyterHub()
509
app.ssl_context = ssl_context
510
```
511
512
### Custom Configuration Classes
513
514
```python
515
from traitlets import Unicode, Integer, Bool
516
from traitlets.config import Configurable
517
from jupyterhub.traitlets import ByteSpecification, Command
518
519
class CustomSpawner(Spawner):
520
"""Custom spawner with additional configuration"""
521
522
# Memory configuration using ByteSpecification
523
memory_limit = ByteSpecification(
524
config=True,
525
help="""
526
Memory limit for user servers.
527
Specify with units like '1G', '512M', etc.
528
"""
529
)
530
531
# Custom command configuration
532
setup_command = Command(
533
config=True,
534
help="""
535
Command to run before starting notebook server.
536
Can be string or list of strings.
537
"""
538
)
539
540
# Container image with validation
541
image = Unicode(
542
'jupyter/base-notebook',
543
config=True,
544
help="Docker image to use for user servers"
545
)
546
547
# Advanced configuration
548
privileged = Bool(
549
False,
550
config=True,
551
help="Whether to run containers in privileged mode"
552
)
553
554
async def start(self):
555
"""Start server with custom configuration"""
556
# Use configured values
557
print(f"Memory limit: {self.memory_limit} bytes")
558
print(f"Setup command: {self.setup_command}")
559
print(f"Image: {self.image}")
560
561
# Custom startup logic here
562
return await super().start()
563
564
# Usage in config
565
c.JupyterHub.spawner_class = CustomSpawner
566
c.CustomSpawner.memory_limit = '4G'
567
c.CustomSpawner.setup_command = ['conda', 'activate', 'myenv']
568
c.CustomSpawner.image = 'myregistry/custom-notebook:latest'
569
```
570
571
### Database Utilities
572
573
```python
574
from jupyterhub.dbutil import upgrade_if_needed
575
576
# Automatic database upgrades
577
def initialize_database(db_url):
578
"""Initialize database with schema upgrades"""
579
try:
580
upgrade_if_needed(db_url, log=app.log)
581
app.log.info("Database schema is up to date")
582
except Exception as e:
583
app.log.error(f"Database upgrade failed: {e}")
584
raise
585
586
# Use in application startup
587
app = JupyterHub()
588
initialize_database(app.db_url)
589
app.start()
590
```
591
592
## Advanced Configuration Patterns
593
594
### Environment-Based Configuration
595
596
```python
597
import os
598
from jupyterhub.utils import url_path_join
599
600
# Environment-aware configuration
601
def get_config():
602
"""Get configuration based on environment"""
603
c = super().get_config()
604
605
# Database URL from environment
606
c.JupyterHub.db_url = os.environ.get(
607
'JUPYTERHUB_DB_URL',
608
'sqlite:///jupyterhub.sqlite'
609
)
610
611
# Base URL handling
612
base_url = os.environ.get('JUPYTERHUB_BASE_URL', '/')
613
c.JupyterHub.base_url = base_url
614
615
# SSL in production
616
if os.environ.get('JUPYTERHUB_ENV') == 'production':
617
c.JupyterHub.ssl_key = os.environ['SSL_KEY_PATH']
618
c.JupyterHub.ssl_cert = os.environ['SSL_CERT_PATH']
619
c.JupyterHub.port = 443
620
else:
621
c.JupyterHub.port = 8000
622
623
return c
624
```
625
626
### Dynamic Configuration Updates
627
628
```python
629
class DynamicConfig(Configurable):
630
"""Configuration that can be updated at runtime"""
631
632
def __init__(self, **kwargs):
633
super().__init__(**kwargs)
634
self.config_watchers = []
635
636
def watch_config_changes(self, callback):
637
"""Register callback for configuration changes"""
638
self.config_watchers.append(callback)
639
640
def update_config(self, **kwargs):
641
"""Update configuration and notify watchers"""
642
for key, value in kwargs.items():
643
if hasattr(self, key):
644
setattr(self, key, value)
645
646
# Notify watchers
647
for callback in self.config_watchers:
648
callback(kwargs)
649
650
# Usage
651
config = DynamicConfig()
652
config.watch_config_changes(lambda changes: print(f"Config updated: {changes}"))
653
config.update_config(memory_limit='8G', image='new-image:latest')
654
```