CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-jupyterhub

A multi-user server for Jupyter notebooks that provides authentication, spawning, and proxying for multiple users simultaneously

Pending
Overview
Eval results
Files

spawners.mddocs/

Spawner System

The JupyterHub spawner system is responsible for creating, managing, and stopping single-user Jupyter notebook servers. It provides a pluggable architecture supporting local processes, containers, cloud platforms, and custom deployment scenarios.

Capabilities

Base Spawner Class

The foundation class for all spawner implementations in JupyterHub.

class Spawner(LoggingConfigurable):
    """
    Base class for spawning single-user notebook servers.
    
    Subclass this to implement custom spawning mechanisms.
    """
    
    # User and server information
    user: User  # The user this spawner belongs to
    name: str  # The name of the server (empty string for default server)
    server: Server  # The server object from the database
    
    # Configuration attributes
    start_timeout: int  # Timeout for server startup (seconds)
    http_timeout: int  # Timeout for HTTP requests to server
    poll_interval: int  # Interval for polling server status
    
    # Server connection info
    ip: str  # IP address server should bind to
    port: int  # Port server should bind to  
    url: str  # Full URL to the server
    
    # Environment and resources
    environment: Dict[str, str]  # Environment variables
    cmd: List[str]  # Command to start server
    args: List[str]  # Arguments to the command
    
    async def start(self):
        """
        Start the single-user server.
        
        Returns:
            (ip, port) tuple of where the server is listening,
            or just port if ip is unchanged
        """
    
    async def stop(self, now=False):
        """
        Stop the single-user server.
        
        Args:
            now: If True, force immediate stop without graceful shutdown
        """
    
    async def poll(self):
        """
        Poll the spawned process to see if it is still running.
        
        Returns:
            None if still running, integer exit code if stopped
        """
    
    def get_state(self):
        """
        Get the current state of the spawner.
        
        Returns:
            Dictionary of state information for persistence
        """
    
    def load_state(self, state):
        """
        Load state from a previous session.
        
        Args:
            state: Dictionary of state information
        """
    
    def clear_state(self):
        """
        Clear any stored state about the server.
        """
    
    def get_env(self):
        """
        Get the complete environment for the server.
        
        Returns:
            Dictionary of environment variables
        """
    
    def get_args(self):
        """
        Get the complete argument list for starting the server.
        
        Returns:
            List of command arguments
        """

Local Process Spawners

Spawners that run single-user servers as local system processes.

class LocalProcessSpawner(Spawner):
    """
    Spawner that runs single-user servers as local processes.
    
    The default spawner implementation for JupyterHub.
    """
    
    # Process management
    proc: subprocess.Popen  # The subprocess object
    pid: int  # Process ID of the spawned server
    
    # Local user management  
    create_system_users: bool  # Whether to create system users
    shell_cmd: List[str]  # Shell command for process execution
    
    async def start(self):
        """
        Start server as local subprocess.
        
        Returns:
            (ip, port) tuple where server is listening
        """
    
    async def stop(self, now=False):
        """
        Stop the local process.
        
        Args:
            now: If True, send SIGKILL instead of SIGTERM
        """
    
    async def poll(self):
        """
        Poll the local process.
        
        Returns:
            None if running, exit code if stopped
        """
    
    def make_preexec_fn(self, name):
        """
        Create preexec function for subprocess.
        
        Args:
            name: Username to switch to
            
        Returns:
            Function to call before exec in subprocess
        """

class SimpleLocalProcessSpawner(LocalProcessSpawner):
    """
    Simplified local process spawner.
    
    Doesn't switch users or create system users. Suitable for
    single-user or development deployments.
    """
    
    # Simplified configuration
    create_system_users: bool = False
    shell_cmd: List[str] = []  # No shell wrapper

Usage Examples

Basic Local Process Spawner

# jupyterhub_config.py
c = get_config()

# Use local process spawner (default)
c.JupyterHub.spawner_class = 'localprocess'

# Configure spawner settings
c.Spawner.start_timeout = 60
c.Spawner.http_timeout = 30

# Set notebook directory
c.Spawner.notebook_dir = '/home/{username}/notebooks'

# Configure environment variables
c.Spawner.environment = {
    'JUPYTER_ENABLE_LAB': 'yes',
    'JUPYTERHUB_SINGLEUSER_APP': 'jupyter_server.serverapp.ServerApp'
}

Custom Spawner Implementation

from jupyterhub.spawner import LocalProcessSpawner
import docker

class DockerSpawner(LocalProcessSpawner):
    """Example Docker-based spawner"""
    
    # Docker configuration
    image: str = 'jupyter/base-notebook'
    remove: bool = True
    
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.client = docker.from_env()
        self.container = None
    
    async def start(self):
        """Start server in Docker container"""
        # Docker container startup logic
        self.container = self.client.containers.run(
            image=self.image,
            command=self.get_args(),
            environment=self.get_env(),
            ports={'8888/tcp': None},
            detach=True,
            remove=self.remove,
            name=f'jupyter-{self.user.name}-{self.name}'
        )
        
        # Get container port mapping
        port_info = self.container.attrs['NetworkSettings']['Ports']['8888/tcp'][0]
        return (self.ip, int(port_info['HostPort']))
    
    async def stop(self, now=False):
        """Stop Docker container"""
        if self.container:
            self.container.stop()
            self.container = None
    
    async def poll(self):
        """Poll container status"""
        if not self.container:
            return 1
        
        self.container.reload()
        status = self.container.status
        
        if status == 'running':
            return None
        else:
            return 1

# Register the spawner
c.JupyterHub.spawner_class = DockerSpawner
c.DockerSpawner.image = 'jupyter/scipy-notebook'

Resource Management

class ResourceManagedSpawner(LocalProcessSpawner):
    """Spawner with resource limits"""
    
    mem_limit: str = '1G'  # Memory limit
    cpu_limit: float = 1.0  # CPU limit
    
    def get_env(self):
        """Add resource limits to environment"""
        env = super().get_env()
        env.update({
            'MEM_LIMIT': self.mem_limit,
            'CPU_LIMIT': str(self.cpu_limit)
        })
        return env
    
    async def start(self):
        """Start with resource constraints"""
        # Set resource limits before starting
        self.set_resource_limits()
        return await super().start()
    
    def set_resource_limits(self):
        """Apply resource limits to the process"""
        # Implementation depends on deployment method
        pass

# Configuration with resource management
c.JupyterHub.spawner_class = ResourceManagedSpawner
c.ResourceManagedSpawner.mem_limit = '2G'
c.ResourceManagedSpawner.cpu_limit = 2.0

Profile-based Spawning

class ProfileSpawner(LocalProcessSpawner):
    """Spawner with selectable profiles"""
    
    profiles = [
        {
            'display_name': 'Basic Python',
            'description': 'Basic Python environment',
            'default': True,
            'kubespawner_override': {
                'image': 'jupyter/base-notebook',
                'mem_limit': '1G',
                'cpu_limit': 1
            }
        },
        {
            'display_name': 'Data Science',
            'description': 'Full data science stack',
            'kubespawner_override': {
                'image': 'jupyter/datascience-notebook',
                'mem_limit': '4G',
                'cpu_limit': 2
            }
        }
    ]
    
    def options_from_form(self, formdata):
        """Process spawner options from form"""
        profile = formdata.get('profile', [None])[0]
        return {'profile': profile}
    
    def load_user_options(self, user_options):
        """Load user-selected options"""
        profile_name = user_options.get('profile')
        if profile_name:
            # Apply profile configuration
            profile = next(p for p in self.profiles if p['display_name'] == profile_name)
            for key, value in profile.get('kubespawner_override', {}).items():
                setattr(self, key, value)

Advanced Patterns

State Persistence

class StatefulSpawner(LocalProcessSpawner):
    """Spawner that persists state across restarts"""
    
    def get_state(self):
        """Get spawner state for persistence"""
        state = super().get_state()
        state.update({
            'custom_setting': self.custom_setting,
            'last_activity': self.last_activity.isoformat()
        })
        return state
    
    def load_state(self, state):
        """Load persisted state"""
        super().load_state(state)
        self.custom_setting = state.get('custom_setting')
        if 'last_activity' in state:
            self.last_activity = datetime.fromisoformat(state['last_activity'])

Health Monitoring

class MonitoredSpawner(LocalProcessSpawner):
    """Spawner with health monitoring"""
    
    async def poll(self):
        """Enhanced polling with health checks"""
        # Check process status
        status = await super().poll()
        if status is not None:
            return status
        
        # Additional health checks
        if not await self.health_check():
            # Server is running but unhealthy
            await self.restart_unhealthy_server()
        
        return None
    
    async def health_check(self):
        """Check if server is healthy"""
        try:
            # Make HTTP request to server
            async with aiohttp.ClientSession() as session:
                async with session.get(f'{self.url}/api/status') as resp:
                    return resp.status == 200
        except:
            return False
    
    async def restart_unhealthy_server(self):
        """Restart an unhealthy server"""
        await self.stop()
        await self.start()

Pre/Post Hooks

class HookedSpawner(LocalProcessSpawner):
    """Spawner with pre/post hooks"""
    
    async def start(self):
        """Start with pre/post hooks"""
        await self.pre_spawn_hook()
        
        try:
            result = await super().start()
            await self.post_spawn_hook()
            return result
        except Exception as e:
            await self.spawn_error_hook(e)
            raise
    
    async def pre_spawn_hook(self):
        """Hook called before spawning"""
        # Setup user workspace, check resources, etc.
        pass
    
    async def post_spawn_hook(self):
        """Hook called after successful spawn"""
        # Register with external systems, send notifications, etc.
        pass
    
    async def spawn_error_hook(self, error):
        """Hook called on spawn error"""
        # Log error, cleanup resources, send alerts, etc.
        pass

Install with Tessl CLI

npx tessl i tessl/pypi-jupyterhub

docs

authentication.md

configuration-utilities.md

core-application.md

database-models.md

index.md

monitoring-metrics.md

rbac-permissions.md

rest-api.md

services-oauth.md

singleuser-integration.md

spawners.md

tile.json