背景
想当年我就是从 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 的描述文件
-
-
-
避坑指南
-
kernelspec
与kernel
的关系类似软件与进程的关系, 一个kernelspec
可以启动多个独立的kernel
-
每一个会话(session)都会关联一个内核(kernel), 且会话名是全局唯一的, 每个会话还有一个路径, 该路径就是使用相对路径访问文件系统时的起点
-
删除会话时会终止其关联的内核
-
客户端与每一个会话可以建立多个连接
-
部分请求需要在请求体头部设置
X-XSRFToken
字段, 该字段的值为 Coolies 中_xsrf
字段的值- 例如 Coolies 为
_xsrf=2|cd719265|8824a1585c9b687f940f1a81da867fc4|1654344109;
, 那么请求体头需要设置X-XSRFToken: 2|cd719265|8824a1585c9b687f940f1a81da867fc4|1654344109
- 例如 Coolies 为
-
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
, onerror
与 onclose
函数句柄即可, 如下所示:
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"
}
]
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于