A multi-user server for Jupyter notebooks that provides authentication, spawning, and proxying for multiple users simultaneously
—
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.
Custom traitlet types for JupyterHub configuration validation and processing.
class Command(TraitType):
"""
Traitlet for command-line commands with shell and string list support.
Validates and processes command specifications for spawners and services.
"""
def validate(self, obj, value):
"""
Validate command specification.
Args:
obj: Object being configured
value: Command value (string, list, or callable)
Returns:
Validated command as list of strings
Raises:
TraitError: If command format is invalid
"""
class URLPrefix(TraitType):
"""
Traitlet for URL prefix validation and normalization.
Ensures URL prefixes are properly formatted with leading/trailing slashes.
"""
def validate(self, obj, value):
"""
Validate and normalize URL prefix.
Args:
obj: Object being configured
value: URL prefix string
Returns:
Normalized URL prefix with proper slash handling
"""
class ByteSpecification(TraitType):
"""
Traitlet for memory/storage size specifications with unit support.
Accepts values like '1G', '512M', '2048K' and converts to bytes.
"""
UNIT_MAP = {
'K': 1024,
'M': 1024**2,
'G': 1024**3,
'T': 1024**4
}
def validate(self, obj, value):
"""
Validate and convert byte specification to integer bytes.
Args:
obj: Object being configured
value: Size specification (int, string with units)
Returns:
Size in bytes as integer
"""
class Callable(TraitType):
"""
Traitlet for callable objects with import string support.
Accepts callable objects or import strings that resolve to callables.
"""
def validate(self, obj, value):
"""
Validate and resolve callable specification.
Args:
obj: Object being configured
value: Callable or import string
Returns:
Resolved callable object
"""
class EntryPointType(TraitType):
"""
Traitlet for loading Python entry points for plugin systems.
Supports authenticators, spawners, proxies, and other plugin types.
"""
def __init__(self, entry_point_group, **kwargs):
"""
Initialize entry point loader.
Args:
entry_point_group: Entry point group name (e.g., 'jupyterhub.authenticators')
**kwargs: Additional traitlet options
"""
self.entry_point_group = entry_point_group
super().__init__(**kwargs)
def validate(self, obj, value):
"""
Load plugin from entry point specification.
Args:
obj: Object being configured
value: Entry point name or class
Returns:
Loaded plugin class
"""Secure token generation, hashing, and validation functions.
def new_token(length: int = 32, entropy: int = None) -> str:
"""
Generate a new random API token.
Args:
length: Token length in characters (default: 32)
entropy: Additional entropy source (optional)
Returns:
Random token string (URL-safe base64)
"""
def hash_token(token: str, salt: bytes = None, rounds: int = 16384) -> str:
"""
Hash a token for secure storage using PBKDF2.
Args:
token: Token string to hash
salt: Salt bytes (generated if None)
rounds: PBKDF2 iteration count
Returns:
Base64-encoded hash suitable for database storage
"""
def compare_token(token: str, hashed: str) -> bool:
"""
Compare a token against its stored hash.
Args:
token: Plain token string
hashed: Stored hash from hash_token()
Returns:
True if token matches hash
"""
def token_authenticated(method):
"""
Decorator for methods that require token authentication.
Args:
method: Method to protect with token auth
Returns:
Decorated method that checks for valid token
"""Functions for safe URL and path manipulation in web contexts.
def url_path_join(*pieces) -> str:
"""
Join URL path components with proper slash handling.
Args:
*pieces: URL path components to join
Returns:
Properly joined URL path with normalized slashes
"""
def url_escape_path(path: str, safe: str = '@!:$&\'()*+,;=') -> str:
"""
URL-encode path components while preserving safe characters.
Args:
path: Path string to encode
safe: Characters to leave unencoded
Returns:
URL-encoded path string
"""
def url_unescape_path(path: str) -> str:
"""
URL-decode path components.
Args:
path: URL-encoded path string
Returns:
Decoded path string
"""
def guess_base_url(url: str) -> str:
"""
Guess the base URL from a full URL.
Args:
url: Full URL string
Returns:
Base URL (protocol + host + port)
"""Helper functions for asynchronous programming patterns in JupyterHub.
def maybe_future(value):
"""
Convert value to Future if it's not already awaitable.
Args:
value: Value that may or may not be awaitable
Returns:
Future/coroutine that can be awaited
"""
def exponential_backoff(func,
max_retries: int = 5,
initial_delay: float = 1.0,
max_delay: float = 60.0,
backoff_factor: float = 2.0):
"""
Decorator for exponential backoff retry logic.
Args:
func: Function to wrap with retry logic
max_retries: Maximum number of retry attempts
initial_delay: Initial delay between retries (seconds)
max_delay: Maximum delay between retries (seconds)
backoff_factor: Multiplier for delay on each retry
Returns:
Decorated function with retry logic
"""
async def cancel_tasks(tasks):
"""
Cancel a collection of asyncio tasks gracefully.
Args:
tasks: Iterable of asyncio tasks to cancel
"""
def run_sync(async_func):
"""
Run an async function synchronously.
Args:
async_func: Async function to run
Returns:
Result of the async function
"""Functions for network operations and SSL context management.
def make_ssl_context(keyfile: str = None,
certfile: str = None,
cafile: str = None,
verify_mode: ssl.VerifyMode = None,
check_hostname: bool = None,
**kwargs) -> ssl.SSLContext:
"""
Create SSL context with common JupyterHub defaults.
Args:
keyfile: Path to SSL private key file
certfile: Path to SSL certificate file
cafile: Path to CA certificate file
verify_mode: SSL verification mode
check_hostname: Whether to verify hostname
**kwargs: Additional SSL context options
Returns:
Configured SSL context
"""
def random_port() -> int:
"""
Find a random available port.
Returns:
Available port number
"""
def is_valid_ip(ip: str) -> bool:
"""
Check if string is a valid IP address.
Args:
ip: IP address string to validate
Returns:
True if valid IPv4 or IPv6 address
"""
def get_server_info(url: str) -> Dict[str, Any]:
"""
Get server information from URL.
Args:
url: Server URL to analyze
Returns:
Dictionary with host, port, protocol info
"""Utilities for database operations and schema management.
def mysql_large_prefix_check():
"""
Check MySQL configuration for large index prefix support.
Raises:
DatabaseError: If MySQL doesn't support required index sizes
"""
def upgrade_if_needed(db_url: str, log=None):
"""
Upgrade database schema if needed.
Args:
db_url: Database connection URL
log: Logger instance for output
"""
def get_schema_version(db_url: str) -> str:
"""
Get current database schema version.
Args:
db_url: Database connection URL
Returns:
Schema version string
"""# jupyterhub_config.py using custom traitlets
c = get_config()
# Memory limits using ByteSpecification
c.Spawner.mem_limit = '2G' # Converted to 2147483648 bytes
c.Spawner.mem_guarantee = '512M' # Converted to 536870912 bytes
# URL prefix configuration
c.JupyterHub.base_url = '/hub/' # Normalized to '/hub/'
# Command configuration
c.Spawner.cmd = ['jupyterhub-singleuser'] # Validated as command list
c.LocalProcessSpawner.shell_cmd = ['/bin/bash', '-l', '-c']
# Plugin loading via entry points
c.JupyterHub.authenticator_class = 'pam' # Loads PAMAuthenticator
c.JupyterHub.spawner_class = 'localprocess' # Loads LocalProcessSpawnerfrom jupyterhub.utils import new_token, hash_token, compare_token
from jupyterhub.orm import APIToken
# Generate API token for user
user = db.query(User).filter(User.name == 'alice').first()
token = new_token(length=32)
hashed = hash_token(token)
# Store in database
api_token = APIToken(
user=user,
hashed=hashed,
prefix=token[:4], # Store prefix for identification
note='CLI access token'
)
db.add(api_token)
db.commit()
# Validate token later
def validate_api_token(provided_token):
"""Validate API token against database"""
prefix = provided_token[:4]
token_record = db.query(APIToken).filter(
APIToken.prefix == prefix
).first()
if token_record and compare_token(provided_token, token_record.hashed):
return token_record.user
return Nonefrom jupyterhub.utils import url_path_join, url_escape_path
# Safe URL path joining
base_url = '/hub'
user_name = 'alice@example.com'
server_name = 'my-server'
# Build user server URL safely
server_url = url_path_join(
base_url,
'user',
url_escape_path(user_name),
url_escape_path(server_name)
)
# Result: '/hub/user/alice%40example.com/my-server'
# Build API endpoint URLs
api_url = url_path_join(base_url, 'api', 'users', url_escape_path(user_name))
# Result: '/hub/api/users/alice%40example.com'from jupyterhub.utils import maybe_future, exponential_backoff
import asyncio
# Convert sync/async values consistently
async def handle_result(result):
"""Handle potentially async result"""
# maybe_future ensures we can always await
final_result = await maybe_future(result)
return final_result
# Retry with exponential backoff
@exponential_backoff(max_retries=3, initial_delay=1.0)
async def unreliable_operation():
"""Operation that might fail temporarily"""
# Simulate operation that might fail
if random.random() < 0.5:
raise Exception("Temporary failure")
return "Success"
# Usage
try:
result = await unreliable_operation()
print(f"Operation succeeded: {result}")
except Exception as e:
print(f"Operation failed after retries: {e}")from jupyterhub.utils import make_ssl_context
# Create SSL context for HTTPS
ssl_context = make_ssl_context(
keyfile='/path/to/private.key',
certfile='/path/to/certificate.crt',
cafile='/path/to/ca-bundle.crt'
)
# Use in JupyterHub configuration
c.JupyterHub.ssl_key = '/path/to/private.key'
c.JupyterHub.ssl_cert = '/path/to/certificate.crt'
# Or programmatically
app = JupyterHub()
app.ssl_context = ssl_contextfrom traitlets import Unicode, Integer, Bool
from traitlets.config import Configurable
from jupyterhub.traitlets import ByteSpecification, Command
class CustomSpawner(Spawner):
"""Custom spawner with additional configuration"""
# Memory configuration using ByteSpecification
memory_limit = ByteSpecification(
config=True,
help="""
Memory limit for user servers.
Specify with units like '1G', '512M', etc.
"""
)
# Custom command configuration
setup_command = Command(
config=True,
help="""
Command to run before starting notebook server.
Can be string or list of strings.
"""
)
# Container image with validation
image = Unicode(
'jupyter/base-notebook',
config=True,
help="Docker image to use for user servers"
)
# Advanced configuration
privileged = Bool(
False,
config=True,
help="Whether to run containers in privileged mode"
)
async def start(self):
"""Start server with custom configuration"""
# Use configured values
print(f"Memory limit: {self.memory_limit} bytes")
print(f"Setup command: {self.setup_command}")
print(f"Image: {self.image}")
# Custom startup logic here
return await super().start()
# Usage in config
c.JupyterHub.spawner_class = CustomSpawner
c.CustomSpawner.memory_limit = '4G'
c.CustomSpawner.setup_command = ['conda', 'activate', 'myenv']
c.CustomSpawner.image = 'myregistry/custom-notebook:latest'from jupyterhub.dbutil import upgrade_if_needed
# Automatic database upgrades
def initialize_database(db_url):
"""Initialize database with schema upgrades"""
try:
upgrade_if_needed(db_url, log=app.log)
app.log.info("Database schema is up to date")
except Exception as e:
app.log.error(f"Database upgrade failed: {e}")
raise
# Use in application startup
app = JupyterHub()
initialize_database(app.db_url)
app.start()import os
from jupyterhub.utils import url_path_join
# Environment-aware configuration
def get_config():
"""Get configuration based on environment"""
c = super().get_config()
# Database URL from environment
c.JupyterHub.db_url = os.environ.get(
'JUPYTERHUB_DB_URL',
'sqlite:///jupyterhub.sqlite'
)
# Base URL handling
base_url = os.environ.get('JUPYTERHUB_BASE_URL', '/')
c.JupyterHub.base_url = base_url
# SSL in production
if os.environ.get('JUPYTERHUB_ENV') == 'production':
c.JupyterHub.ssl_key = os.environ['SSL_KEY_PATH']
c.JupyterHub.ssl_cert = os.environ['SSL_CERT_PATH']
c.JupyterHub.port = 443
else:
c.JupyterHub.port = 8000
return cclass DynamicConfig(Configurable):
"""Configuration that can be updated at runtime"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.config_watchers = []
def watch_config_changes(self, callback):
"""Register callback for configuration changes"""
self.config_watchers.append(callback)
def update_config(self, **kwargs):
"""Update configuration and notify watchers"""
for key, value in kwargs.items():
if hasattr(self, key):
setattr(self, key, value)
# Notify watchers
for callback in self.config_watchers:
callback(kwargs)
# Usage
config = DynamicConfig()
config.watch_config_changes(lambda changes: print(f"Config updated: {changes}"))
config.update_config(memory_limit='8G', image='new-image:latest')Install with Tessl CLI
npx tessl i tessl/pypi-jupyterhub