Jupyter Client

repository·main·Indexed 19 days ago

https://github.com/jupyter/jupyter_client

The reference implementation of the Jupyter protocol, providing core APIs for managing and interacting with Jupyter kernels. It includes tools for managing kernelspecs via the `jupyter kernelspec` CLI and supports various execution models through subpackages for asynchronous and blocking interactions, I/O loop utilities, provisioning, and SSH connectivity.

Tokens
40.2K
Snippets
120
Records
172
Agent score
66%

What's inside jupyter_client

  1. Overview of Jupyter Client

    main
    jupyter_client is the reference implementation of the Jupyter protocol. It provides the core APIs for client and kernel management, allowing developers to interact with Jupyter kernels. Additionally, it provides the jupyter kernelspec command-line entrypoint, which is used to install kernelspecs for use with various Jupyter frontends.
  2. Overview of the jupyter_client package structure

    main

    The jupyter_client package provides the core machinery for interacting with Jupyter kernels. It is organized into several subpackages and modules that handle different execution models and connection types:

    Subpackages

    • jupyter_client.asynchronous: For asynchronous kernel interaction (typically using tornado).
    • jupyter_client.blocking: For synchronous, blocking kernel interaction.
    • jupyter_client.ioloop: Utilities related to the I/O loop.
    • jupyter_client.provisioning: Logic for provisioning kernel resources.
    • jupyter_client.ssh: Support for connecting to kernels over SSH.

    Key Modules

    • jupyter_client.client: The primary interface for interacting with a kernel.
    • jupyter_client.channels: Manages the communication channels (stdin, shell, iostreams, control) between the client and the kernel.
    • jupyter_client.manager: Handles the lifecycle and management of kernels.
    • jupyter_client.kernelspec: Utilities for discovering and managing kernel specifications.
    • jupyter_client.launcher: Logic for launching kernel processes.
  3. Implement a custom kernel provisioner

    main

    Kernel Provisioning allows you to manage a kernel's lifecycle within any runtime environment. There are two primary ways to implement a custom provisioner:

    1. Extending LocalProvisioner: Use this if you want to run kernels locally but need to adjust their behavior (e.g., adding authentication or RBAC checks). You subclass LocalProvisioner and override methods like pre_launch. Always ensure you call super().method_name() where appropriate to maintain critical management operations.

    2. Extending KernelProvisionerBase: Use this if you want to launch kernels in remote environments (e.g., Hadoop YARN, Kubernetes). You must implement process-control methods like poll, wait, and send_signal to interact with the remote environment's API.

    Key Concept: kernel_id as a Discovery Mechanism When using KernelProvisionerBase, the kernel_id is available prior to the kernel's launch. You should use this kernel_id as a unique key to discover and control your kernel within resource-managed clusters.

    # Example: Extending LocalProvisioner for RBAC
    class RBACProvisioner(LocalProvisioner):
        role: str = Unicode(config=True)
    
        async def pre_launch(self, **kwargs: Any) -> Dict[str, Any]:
            if not self.user_in_role(self.role):
                raise PermissionError(
                    f"User is not in role {self.role} and cannot launch this kernel."
                )
            return await super().pre_launch(**kwargs)
  4. Understand the Request-Reply Pattern on the Shell Channel

    main

    Communication on the Shell (ROUTER/DEALER) channel typically follows a request-reply pattern:

    1. Request: The client sends an <action>_request (e.g., execute_request) on its shell (DEALER) socket.
    2. Busy Status: The kernel receives the request and publishes status: busy on the IOPub channel.
    3. Processing: The kernel processes the request.
    4. Reply: The kernel sends the corresponding <action>_reply (e.g., execute_reply).
    5. Idle Status: After processing and sending any associated IOPub messages, the kernel publishes status: idle on IOPub. This indicates all IOPub messages related to that request have been received.

    Reply Status Codes

    All reply messages include a status field:

    StatusDescription
    okRequest processed successfully.
    errorRequest failed. Must include ename (str), evalue (str), and traceback (list of str).
    abortedRequest failed with no error info. (Deprecated: use error instead).

    Note: execute_reply messages always include an execution_count field, regardless of status.

  5. Protocol Version 5.2: Unicode cursor_pos handling

    main

    In protocol versions 5.2 and later, the cursor_pos field must be the encoding-independent offset in Unicode codepoints.

    This change resolves issues in older versions (prior to 5.2) where frontends (like JavaScript-based ones) used UTF-16 indices. In UTF-16, 'astral-plane' characters (like certain emojis or mathematical symbols) are represented as surrogate pairs, which caused the cursor position to drift by one for every such character.

    Implementation Requirement:

    • Frontends implementing protocol 5.2 MUST treat cursor_pos as the Unicode character offset.
  6. Use the Jupyter Debugger Protocol (DAP)

    main

    Jupyter supports debugging via the debug_request and debug_reply messages on the control channel. These follow the Debug Adapter Protocol (DAP) v1.39+.

    To check if a kernel supports debugging, look for 'debugger' in the 'supported_features' field of the kernel info reply.

    Key DAP additions in Jupyter include:

    • dumpCell: Submits notebook cell code to the debugger.
    • debugInfo: Retrieves debugger state (breakpoints, etc.).
    • inspectVariables: Retrieves all defined variables.
    • richInspectVariables: Retrieves rich representations of a specific variable.
    • copyToGlobals: Copies a variable from the debugger to the global scope.
  7. Implement Custom Comm (Communication) objects

    main

    The Comm system (introduced in 4.1) allows developers to create custom bidirectional communication channels between the Frontend and the Kernel, commonly used for synchronizing widget states.

    Lifecycle:

    1. Opening: One side sends a comm_open message. The receiver must map the target_name to a constructor to create a corresponding instance.
    2. Communication: Use comm_msg for one-way updates. These are symmetrical: the Kernel listens on the Shell channel, and the Frontend listens on the IOPub channel. There are no expected replies for comm_msg.
    3. Closing: When a Comm is destroyed, the owner must send a comm_close message to notify the other side.

    Important Notes:

    • If a receiver gets a comm_open for an unknown target_name, it should immediately reply with comm_close.
    • Since comm_msg can execute code, handlers should set the parent_header and publish busy/idle status, similar to an execution request.
    // comm_open
    {
      "comm_id": "u-u-i-d",
      "target_name": "my_comm",
      "data": {}
    }
    
    // comm_msg
    {
      "comm_id": "u-u-i-d",
      "data": { "widget_state": "active" }
    }
    
    // comm_close
    {
      "comm_id": "u-u-i-d",
      "data": {}
    }
  8. Handle user input requests via stdin channel

    main

    When a kernel needs to prompt a user for input (e.g., via Python's input() or R's readline()), it sends an input_request message on the stdin (ROUTER/DEALER) channel. This is only possible if the execute_request message had allow_stdin==True.

    Workflow:

    1. Kernel sends input_request.
    2. Frontend displays the prompt and captures user input.
    3. Frontend sends input_reply back to the kernel.

    Requirements:

    • The stdin socket of the client must have the same ZMQ IDENTITY as the client's shell socket so the input_request reaches the frontend.
    • If password is True in the request, the frontend must not echo the input (e.g., by obscuring characters or showing nothing).

    Message Formats:

    • input_request:
      • prompt (str): The text to show.
      • password (bool): Whether to hide input.
    • input_reply:
      • value (str): The text entered by the user.
    // input_request
    {
        "prompt": "Enter your name: ",
        "password": false
    }
    
    // input_reply
    {
        "value": "jovyan"
    }
  9. Understand the Jupyter messaging architecture

    main

    Jupyter communication relies on a kernel connected to one or more frontends via ZeroMQ transport. The kernel maintains five dedicated sockets (channels) to handle different types of communication:

    1. Shell: A ROUTER socket used for request/reply actions (e.g., code execution, object information, prompts). Multiple frontends can connect here.
    2. IOPub: A broadcast channel (XPUB in spec 5.5+) where the kernel publishes side effects like stdout, stderr, and debugging events. This allows all connected clients to see the same information.
    3. stdin: A ROUTER socket used by the kernel to request input from the active frontend (e.g., when raw_input is called). The frontend uses a DEALER socket to act as a 'virtual keyboard'.
    4. Control: A separate channel (identical to Shell) used for management tasks like shutdown, restart, and debugging. It is recommended to run this in a separate thread from the Shell to prevent long-running execution requests from blocking control messages.
    5. Heartbeat: A socket for sending simple bytestring messages to ensure the connection between frontend and kernel is still alive.
  10. How kernel provisioning works

    main

    Kernel Provisioning (introduced in version 7.0) is an abstraction layer that allows third parties to manage the lifecycle of a kernel's runtime environment. Instead of the KernelManager directly managing a local kernel process (like Popen), it delegates this responsibility to a kernel provisioner.

    This allows kernels to reside in remote or managed environments such as Kubernetes, Hadoop YARN, or Slurm. The provisioner is responsible for:

    1. Launching the kernel in the target environment (e.g., a Kubernetes pod).
    2. Communicating connection information back to the application.
    3. Terminating the environment when the kernel is terminated.

    Key relationships:

    • KernelManager vs. KernelProvisioner: The KernelManager has-a KernelProvisioner. While KernelManager is often application-owned, provisioners are agnostic to the application and can be reused across different contexts.
    • KernelClient: All standard kernel interactions still occur through the existing KernelManager and KernelClient APIs, making the underlying provisioning mechanism transparent to the user.
  11. The Jupyter message format specification

    main

    A Jupyter message is a collection of five components: a header, a parent_header, metadata, content, and buffers. While the exact wire serialization is defined by the wire protocol, a logical representation is a dictionary of dictionaries.

    • header: Contains metadata about the message (ID, session, type, version, etc.).
    • parent_header: A copy of the header from the message that triggered this one (e.g., a reply's parent_header is a copy of the request's header). Used by clients to route outputs to the correct UI elements (like a specific cell).
    • metadata: An optional dictionary for extra information (e.g., from extensions).
    • content: The actual body of the message, whose structure depends on the msg_type.
    • buffers: A list of additional binary buffers (used by extensions like ipywidgets or IPython Parallel).
    {
        "header" : {
            "msg_id": "...",
            "msg_type": "...",
            ...
        },
        "parent_header": {},
        "metadata": {},
        "content": {},
        "buffers": [],
    }