如何自己开发一个 Jupyter 客户端

本贴最后更新于 879 天前,其中的信息可能已经物是人非

背景

想当年我就是从 Jupyter 入手了块编辑器, 之后遇到思源笔记就立刻喜欢上了思源的块编辑, 因此想方法将思源改造为 Jupyter 的客户端, 这样在思源中可以直接运行代码并将运行结果写入笔记中, 从而避免做笔记时两头切换, 详情请参考: [思源笔记经验分享] Jupyter 模式

Jupyter 管理 API

  • Jupyter 服务, 比如内核管理, 会话管理等等所使用的 API, 这是一组 RESTful 风格 API, 详情请参考:

  • 简单介绍

    • contents: 内容管理

      • /api/contents/{path}

        • GET: 获取文件/目录的信息
        • POST: 新建文件
        • PATCH: 重命名文件/目录
        • PUT: 保存/上传文件
        • DELETE: 删除文件
      • /api/contents/{path}/checkpoints

        • GET: 获取一个文件的检查点列表
        • POST: 创建一个新的文件检查点
      • /api/contents/{path}/checkpoints/{checkpoint_id}

        • POST: 将文件恢复到指定的检查点
        • DELETE: 删除一个检查点
    • sessions: 会话管理

      • /api/sessions

        • GET: 获取当前活动的会话列表
        • POST: 创建一个会话, 如果该会话已存在则返回该会话的信息
      • /api/sessions/{session}

        • GET: 获取一个会话的信息
        • PATCH: 重新设置指定会话的信息
        • DELETE: 删除一个会话
    • kernels: 内核管理

      • /api/kernels

        • GET: 获取当前活动的内核列表
        • POST: 启动一个内核
      • /api/kernels/{kernel_id}

        • GET: 获取一个内核的信息
        • DELETE: 终止一个内核
      • /api/kernels/{kernel_id}/interrupt

        • POST: 中断一个内核的执行
      • /api/kernels/{kernel_id}/restart

        • POST: 重启一个内核
    • kernelspecs: 内核信息

      • /api/kernelspecs

        • GET: 获取可启动的内核信息
    • config: 设置管理

      • /api/config/{section_name}

        • GET: 通过名称获取一个配置项
        • PATCH: 通过名称更新一个配置项
    • terminals: 命令行终端管理

      • /api/terminals

        • GET: 获取活跃的终端列表
        • POST: 创建一个新的终端
      • /api/terminals/{terminal_id}

        • GET: 获取指定的终端会话信息
        • DELETE: 删除指定的终端会话
    • identity: 身份管理

      • /api/me

        • GET: 获取用户身份与权限信息
    • status: 服务状态管理

      • /api/status

        • GET: 获得当前服务的状态
    • api-spec: API 信息

      • /api/spec.yaml

        • GET: 获得当前服务 API 的描述文件
  • 避坑指南

    • kernelspeckernel 的关系类似软件与进程的关系, 一个 kernelspec 可以启动多个独立的 kernel

    • 每一个会话(session)都会关联一个内核(kernel), 且会话名是全局唯一的, 每个会话还有一个路径, 该路径就是使用相对路径访问文件系统时的起点

    • 删除会话时会终止其关联的内核

    • 客户端与每一个会话可以建立多个连接

    • 部分请求需要在请求体头部设置 X-XSRFToken 字段, 该字段的值为 Coolies 中 _xsrf 字段的值

      • 例如 Coolies 为 _xsrf=2|cd719265|8824a1585c9b687f940f1a81da867fc4|1654344109;, 那么请求体头需要设置 X-XSRFToken: 2|cd719265|8824a1585c9b687f940f1a81da867fc4|1654344109

Jupyter 内核交互

与 Jupyter 内核进行交互我没有找到具体的文档, 只找到一个介绍其协议的文档 WebSocket kernel wire protocols, 因此下面的内容是使用浏览器开发者工具查看的信息, 具体方式为 Network -> Filter -> WS -> 选择一个 ws 连接 -> Messages

Jupyter 客户端与内核使用 WebSockets 进行交互, API 格式为 ws(s)://hostname(:port)/api/kernels/{内核ID}/channels?session_id={会话ID}, 其中 内核ID 必须是当前活跃的内核的 ID, 会话ID 使用一个 UUID 不重复即可, 这是因为其并非内核所关联的会话 ID, 而是 WebSocket 连接中发送的第一个会话的 ID。

由于客户端通过 WebSocket 连接发送与接收消息的格式都为一个 JSON 对象字符串, 因此客户端自己构造消息发送后再解析接收到的消息即可, javascript 可以使用内置的 WebSocket - Web API 接口参考 | MDN 方法构造一个 WebSocket 连接对象, 然后分别绑定 onopen, onmessage, onerroronclose 函数句柄即可, 如下所示:

ws = new WebSocket('ws://localhost:8888/api/kernels/39684315-102f-4b77…s?session_id=464f0545-202d-4966-b471-7d41787f6f48')

ws.onopen = e => {}; // 连接建立完成时触发
ws.onerror = e => {}; // 连接出现错误时触发
ws.onmessage = e => {}; // 接收到消息时触发
ws.onclose = e => {}; // 连接关闭时触发

ws.send('{"foo": "bar"}'); // 发送消息

下面是这个对象的内容与格式示例:

客户端发送消息示例

{
    "header": { // 消息头
        "date": "2022-06-01T18:57:33.029Z", // 消息发送时间, 在 js 中可以使用 new Date().toISOString() 生成
        "msg_id": "95de6eb0-04e8-4c11-a687-e20389d95d55", // 消息的 ID, 是一个 UUID, 在 js 中可以使用 crypto.randomUUID() 生成
        "msg_type": "execute_request", // 消息的类型
        "session": "d0105d82-8d30-4ea3-835f-b95bbe26cf5f", // 消息关联的会话
        "username": "", // 用户名
        "version": "5.2" // 客户端版本
    },
    "parent_header": {}, // 如果该消息用于答复某条消息(比如 stdin 答复 stdin 请求), 那么这里设置为所答复消息的 header
    "channel": "shell", // 消息通道类型, 发送主要有 "shell", "control", "stdin"
    "content": { // 消息内容, 主要是待执行代码的描述或控制信息的描述
        "silent": false,
        "store_history": true,
        "user_expressions": {},
        "allow_stdin": true,
        "stop_on_error": true,
        "code": "print(123)" // 待执行的代码
    },
    "metadata": { // 消息一些其他的元信息
        "cellId": "c0f6f1db-3793-42a3-8945-d8bf8248f582"
    },
    "buffers": []
}

接收消息示例

{
    "header": { // 消息头
        "date": "2022-06-01T18:57:33.060094Z",
        "msg_id": "35c6170e-8f5c14b3a4594d857a16d6f6_30400_68",
        "msg_type": "status",
        "username": "username",
        "session": "35c6170e-8f5c14b3a4594d857a16d6f6",
        "version": "5.3"
    },
    "parent_header": { // 所回复消息的消息头
        "date": "2022-06-01T18:57:33.029000Z",
        "msg_id": "95de6eb0-04e8-4c11-a687-e20389d95d55",
        "msg_type": "execute_request",
        "session": "d0105d82-8d30-4ea3-835f-b95bbe26cf5f",
        "username": "",
        "version": "5.2"
    },
    "channel": "iopub", // 消息通道类型, 接收主要有 "iopub", "control", "stdin"
    "content": { // 消息内容
        "execution_state": "idle" // 内核状态
    },
    "metadata": {}, // 元信息
    "msg_id": "35c6170e-8f5c14b3a4594d857a16d6f6_30400_68", // 本条消息的 ID
    "msg_type": "status", // 本条消息的类型
    /**
     * status: 内核状态
     * stream: 输出消息流
     * execute_input: 代码输入信息
     * input_request: 请求一个 stdin 输入
     * execute_result: 运行结果
     * display_data: 需要展示的数据(例如图片)
     * error: 错误信息
     * execute_reply: 响应代码执行请求
     * kernel_info_reply: 响应内核信息请求
     * history_reply: 响应历史请求
     * debug_reply: 响应调试请求
     */
    "buffers": []
}

一次完整交互(以执行代码为例)

[
    { // 发送: 请求执行代码
        "buffers": [],
        "channel": "shell",
        "content": {
            "silent": false,
            "store_history": false,
            "user_expressions": {},
            "allow_stdin": true,
            "stop_on_error": true,
            "code": "print(123)\n456" // 请求执行的代码
        },
        "header": {
            "date": "2022-06-04T14:16:13.750Z",
            "msg_id": "1d1fdc29-56f8-4960-9c7b-85207fcacf02",
            "msg_type": "execute_request",
            "session": "464f0545-202d-4966-b471-7d41787f6f48",
            "username": "",
            "version": "5.3"
        },
        "metadata": {},
        "parent_header": {}
    },
    { // 接收: 内核状态
        "header": {
            "msg_id": "bb71e03b-55479ade6b3428780b8bf212_38156_20",
            "msg_type": "status",
            "username": "username",
            "session": "bb71e03b-55479ade6b3428780b8bf212",
            "date": "2022-06-04T14:16:13.752958Z",
            "version": "5.3"
        },
        "msg_id": "bb71e03b-55479ade6b3428780b8bf212_38156_20",
        "msg_type": "status", // 内核状态
        "parent_header": {
            "date": "2022-06-04T14:16:13.750000Z",
            "msg_id": "1d1fdc29-56f8-4960-9c7b-85207fcacf02",
            "msg_type": "execute_request",
            "session": "464f0545-202d-4966-b471-7d41787f6f48",
            "username": "",
            "version": "5.3"
        },
        "metadata": {},
        "content": {
            "execution_state": "busy" // 内核状态转换为忙碌
        },
        "buffers": [],
        "channel": "iopub"
    },
    { // 接收: 待执行的代码
        "header": {
            "msg_id": "bb71e03b-55479ade6b3428780b8bf212_38156_21",
            "msg_type": "execute_input",
            "username": "username",
            "session": "bb71e03b-55479ade6b3428780b8bf212",
            "date": "2022-06-04T14:16:13.753970Z",
            "version": "5.3"
        },
        "msg_id": "bb71e03b-55479ade6b3428780b8bf212_38156_21",
        "msg_type": "execute_input", // 执行输入(这里是输入代码)
        "parent_header": {
            "date": "2022-06-04T14:16:13.750000Z",
            "msg_id": "1d1fdc29-56f8-4960-9c7b-85207fcacf02",
            "msg_type": "execute_request",
            "session": "464f0545-202d-4966-b471-7d41787f6f48",
            "username": "",
            "version": "5.3"
        },
        "metadata": {},
        "content": {
            "code": "print(123)\n456", // 待执行的代码
            "execution_count": 1
        },
        "buffers": [],
        "channel": "iopub"
    },
    { // 接收: 代码执行时的输出(可能有多个输出)
        "header": {
            "msg_id": "bb71e03b-55479ade6b3428780b8bf212_38156_23",
            "msg_type": "stream",
            "username": "username",
            "session": "bb71e03b-55479ade6b3428780b8bf212",
            "date": "2022-06-04T14:16:13.757970Z",
            "version": "5.3"
        },
        "msg_id": "bb71e03b-55479ade6b3428780b8bf212_38156_23",
        "msg_type": "stream", // 标准输出流
        "parent_header": {
            "date": "2022-06-04T14:16:13.750000Z",
            "msg_id": "1d1fdc29-56f8-4960-9c7b-85207fcacf02",
            "msg_type": "execute_request",
            "session": "464f0545-202d-4966-b471-7d41787f6f48",
            "username": "",
            "version": "5.3"
        },
        "metadata": {},
        "content": {
            "name": "stdout",
            "text": "123\n" // 程序执行时输出的文本信息
        },
        "buffers": [],
        "channel": "iopub"
    },
    { // 接收: 代码执行结果
        "header": {
            "msg_id": "bb71e03b-55479ade6b3428780b8bf212_38156_22",
            "msg_type": "execute_result",
            "username": "username",
            "session": "bb71e03b-55479ade6b3428780b8bf212",
            "date": "2022-06-04T14:16:13.755972Z",
            "version": "5.3"
        },
        "msg_id": "bb71e03b-55479ade6b3428780b8bf212_38156_22",
        "msg_type": "execute_result", // 代码执行结果
        "parent_header": {
            "date": "2022-06-04T14:16:13.750000Z",
            "msg_id": "1d1fdc29-56f8-4960-9c7b-85207fcacf02",
            "msg_type": "execute_request",
            "session": "464f0545-202d-4966-b471-7d41787f6f48",
            "username": "",
            "version": "5.3"
        },
        "metadata": {},
        "content": {
            "data": {
                "text/plain": "456" // 代码执行结果, 这里输出一条文本信息
            },
            "metadata": {},
            "execution_count": 1
        },
        "buffers": [],
        "channel": "iopub"
    },
    { // 接收: 对代码执行请求的响应
        "header": {
            "msg_id": "bb71e03b-55479ade6b3428780b8bf212_38156_24",
            "msg_type": "execute_reply",
            "username": "username",
            "session": "bb71e03b-55479ade6b3428780b8bf212",
            "date": "2022-06-04T14:16:13.767978Z",
            "version": "5.3"
        },
        "msg_id": "bb71e03b-55479ade6b3428780b8bf212_38156_24",
        "msg_type": "execute_reply", // 代码执行请求的响应
        "parent_header": {
            "date": "2022-06-04T14:16:13.750000Z",
            "msg_id": "1d1fdc29-56f8-4960-9c7b-85207fcacf02",
            "msg_type": "execute_request",
            "session": "464f0545-202d-4966-b471-7d41787f6f48",
            "username": "",
            "version": "5.3"
        },
        "metadata": {
            "started": "2022-06-04T14:16:13.753970Z",
            "dependencies_met": true,
            "engine": "c38abf92-fc99-430e-8181-4fbf40b53b12",
            "status": "ok"
        },
        "content": {
            "status": "ok", // 代码执行成功
            "execution_count": 0,
            "user_expressions": {},
            "payload": []
        },
        "buffers": [],
        "channel": "shell"
    },
    { // 接收: 内核状态恢复为空闲
        "header": {
            "msg_id": "bb71e03b-55479ade6b3428780b8bf212_38156_25",
            "msg_type": "status",
            "username": "username",
            "session": "bb71e03b-55479ade6b3428780b8bf212",
            "date": "2022-06-04T14:16:13.769094Z",
            "version": "5.3"
        },
        "msg_id": "bb71e03b-55479ade6b3428780b8bf212_38156_25",
        "msg_type": "status", // 内核状态
        "parent_header": {
            "date": "2022-06-04T14:16:13.750000Z",
            "msg_id": "1d1fdc29-56f8-4960-9c7b-85207fcacf02",
            "msg_type": "execute_request",
            "session": "464f0545-202d-4966-b471-7d41787f6f48",
            "username": "",
            "version": "5.3"
        },
        "metadata": {},
        "content": {
            "execution_state": "idle" // 内核恢复空闲状态
        },
        "buffers": [],
        "channel": "iopub"
    }
]

如果内核返回图片

{
    "header": {
        "msg_id": "36e14ffa-2cb2d3c7c176ede4c528fe3a_27604_42",
        "msg_type": "display_data",
        "username": "username",
        "session": "36e14ffa-2cb2d3c7c176ede4c528fe3a",
        "date": "2022-06-03T01:58:32.090845Z",
        "version": "5.3"
    },
    "parent_header": {
        "date": "2022-06-03T01:58:31.720000Z",
        "msg_id": "78c5f31f-2aa5-4132-b8c2-5abfe2d75f85",
        "msg_type": "execute_request",
        "session": "9bda0e30-8b6d-42f3-95a7-573475777325",
        "username": "",
        "version": "5.2"
    },
    "msg_id": "36e14ffa-2cb2d3c7c176ede4c528fe3a_27604_42",
    "msg_type": "display_data", // 显示数据
    "metadata": {},
    "content": {
        "data": {
            "text/plain": "<Figure size 432x288 with 1 Axes>", // 文本信息
            "image/png": "<base64 编码的图片>" // 图片信息
        },
        "metadata": {
            "needs_background": "light"
        },
        "transient": {}
    },
    "buffers": [],
    "channel": "iopub"
}

如果内核返回错误

[
    {
        "header": {
            "msg_id": "84f9bba0-5b58154ee48d6d35eed0a0c4_21",
            "msg_type": "error",
            "username": "username",
            "session": "84f9bba0-5b58154ee48d6d35eed0a0c4",
            "date": "2022-06-02T17:41:20.639047Z",
            "version": "5.3"
        },
        "msg_id": "84f9bba0-5b58154ee48d6d35eed0a0c4_21",
        "msg_type": "error", // 运行时异常
        "parent_header": {
            "date": "2022-06-02T17:41:19.441000Z",
            "msg_id": "cf0996ff-4e21-407d-8bdf-ea1fbbbf2ab6",
            "msg_type": "execute_request",
            "session": "02de7b11-f395-49d4-a173-d6155b52715d",
            "username": "",
            "version": "5.2"
        },
        "metadata": {},
        "content": {
            "traceback": [ // 异常的堆栈信息
                "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
                "\u001b[1;31mModuleNotFoundError\u001b[0m                       Traceback (most recent call last)",
                "\u001b[1;32m~\\AppData\\Local\\Temp/ipykernel_29476/4173331163.py\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[0;32m      4\u001b[0m \u001b[1;31m# %matplotlib widget\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m      5\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 6\u001b[1;33m \u001b[1;32mimport\u001b[0m \u001b[0mmatplotlib\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mpyplot\u001b[0m \u001b[1;32mas\u001b[0m \u001b[0mplt\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m      7\u001b[0m \u001b[1;31m# import numpy as np\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m      8\u001b[0m \u001b[1;32mimport\u001b[0m \u001b[0mmath\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
                "\u001b[1;31mModuleNotFoundError\u001b[0m: No module named 'matplotlib'"
            ],
            "ename": "ModuleNotFoundError", // 异常名称
            "evalue": "No module named 'matplotlib'" // 异常描述
        },
        "buffers": [],
        "channel": "iopub"
    },
    {
        "header": {
            "msg_id": "84f9bba0-5b58154ee48d6d35eed0a0c4_22",
            "msg_type": "execute_reply",
            "username": "username",
            "session": "84f9bba0-5b58154ee48d6d35eed0a0c4",
            "date": "2022-06-02T17:41:20.644179Z",
            "version": "5.3"
        },
        "msg_id": "84f9bba0-5b58154ee48d6d35eed0a0c4_22",
        "msg_type": "execute_reply", // 代码执行请求的响应
        "parent_header": {
            "date": "2022-06-02T17:41:19.441000Z",
            "msg_id": "cf0996ff-4e21-407d-8bdf-ea1fbbbf2ab6",
            "msg_type": "execute_request",
            "session": "02de7b11-f395-49d4-a173-d6155b52715d",
            "username": "",
            "version": "5.2"
        },
        "metadata": {
            "started": "2022-06-02T17:41:19.443713Z",
            "dependencies_met": true,
            "engine": "0c0534c2-a97b-416f-8731-cf161ea962d2",
            "status": "error"
        },
        "content": {
            "status": "error",
            "traceback": [ // 异常的堆栈信息
                "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
                "\u001b[1;31mModuleNotFoundError\u001b[0m                       Traceback (most recent call last)",
                "\u001b[1;32m~\\AppData\\Local\\Temp/ipykernel_29476/4173331163.py\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[0;32m      4\u001b[0m \u001b[1;31m# %matplotlib widget\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m      5\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 6\u001b[1;33m \u001b[1;32mimport\u001b[0m \u001b[0mmatplotlib\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mpyplot\u001b[0m \u001b[1;32mas\u001b[0m \u001b[0mplt\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m      7\u001b[0m \u001b[1;31m# import numpy as np\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m      8\u001b[0m \u001b[1;32mimport\u001b[0m \u001b[0mmath\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
                "\u001b[1;31mModuleNotFoundError\u001b[0m: No module named 'matplotlib'"
            ],
            "ename": "ModuleNotFoundError", // 异常名称
            "evalue": "No module named 'matplotlib'", // 异常描述
            "engine_info": {
                "engine_uuid": "0c0534c2-a97b-416f-8731-cf161ea962d2",
                "engine_id": -1,
                "method": "execute"
            },
            "execution_count": 1,
            "user_expressions": {},
            "payload": []
        },
        "buffers": [],
        "channel": "shell"
    }
]
  • Jupyter
    5 引用 • 4 回帖
  • WebSocket

    WebSocket 是 HTML5 中定义的一种新协议,它实现了浏览器与服务器之间的全双工通信(full-duplex)。

    48 引用 • 206 回帖 • 346 关注
  • API

    应用程序编程接口(Application Programming Interface)是一些预先定义的函数,目的是提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力,而又无需访问源码,或理解内部工作机制的细节。

    77 引用 • 429 回帖 • 2 关注
  • 前端

    前端技术一般分为前端设计和前端开发,前端设计可以理解为网站的视觉设计,前端开发则是网站的前台代码实现,包括 HTML、CSS 以及 JavaScript 等。

    247 引用 • 1347 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...