Skip to content

Commit 6552b5d

Browse files
committed
split python code
1 parent 58f7f02 commit 6552b5d

File tree

8 files changed

+603
-536
lines changed

8 files changed

+603
-536
lines changed

jupyter_server_nbmodel/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
# in editable mode with pip. It is highly recommended to install
99
# the package from a stable release or in editable mode: https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs
1010
import warnings
11-
1211
warnings.warn("Importing 'jupyter_server_nbmodel' outside a proper installation.", stacklevel=1)
1312
__version__ = "dev"
13+
1414
from .extension import Extension
1515

1616

jupyter_server_nbmodel/actions.py

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
# Copyright (c) 2024-2025 Datalayer, Inc.
2+
# Distributed under the terms of the Modified BSD License.
3+
4+
from __future__ import annotations
5+
6+
import asyncio
7+
import json
8+
import os
9+
import typing as t
10+
11+
from functools import partial
12+
from datetime import datetime, timezone
13+
14+
import nbformat
15+
16+
from jupyter_core.utils import ensure_async
17+
18+
from jupyter_server_nbmodel.models import (
19+
PendingInput,
20+
InputDescription,
21+
InputRequest,
22+
)
23+
from jupyter_server_nbmodel.log import get_logger
24+
from jupyter_server_nbmodel.event_logger import event_logger
25+
26+
27+
if t.TYPE_CHECKING:
28+
import jupyter_client
29+
from nbformat import NotebookNode
30+
try:
31+
import jupyter_server_ydoc
32+
import pycrdt as y
33+
from jupyter_ydoc.ynotebook import YNotebook
34+
except ImportError:
35+
# optional dependencies
36+
...
37+
38+
39+
# FIXME should we use caching to retrieve faster at least the document
40+
async def _get_ycell(
41+
ydoc: jupyter_server_ydoc.app.YDocExtension | None,
42+
metadata: dict | None,
43+
) -> y.Map | None:
44+
"""Get the cell from which the execution was triggered.
45+
46+
Args:
47+
ydoc: The YDoc jupyter server extension
48+
metadata: Execution context
49+
Returns:
50+
The cell
51+
"""
52+
if ydoc is None:
53+
msg = "jupyter-collaboration extension is not installed on the server. Outputs won't be written within the document." # noqa: E501
54+
get_logger().warning(msg)
55+
return None
56+
57+
document_id = metadata.get("document_id")
58+
cell_id = metadata.get("cell_id")
59+
60+
if document_id is None or cell_id is None:
61+
msg = (
62+
"document_id and cell_id not defined. The outputs won't be written within the document."
63+
)
64+
get_logger().debug(msg)
65+
return None
66+
67+
notebook: YNotebook | None = await ydoc.get_document(room_id=document_id, copy=False)
68+
69+
if notebook is None:
70+
msg = f"Document with ID {document_id} not found."
71+
get_logger().warning(msg)
72+
return None
73+
74+
ycells = filter(lambda c: c["id"] == cell_id, notebook.ycells)
75+
76+
ycell = next(ycells, None)
77+
if ycell is None:
78+
msg = f"Cell with ID {cell_id} not found in document {document_id}."
79+
get_logger().warning(msg)
80+
return None
81+
else:
82+
# Check if there is more than one cell
83+
if next(ycells, None) is not None:
84+
get_logger().warning("Multiple cells have the same ID '%s'.", cell_id)
85+
86+
if ycell["cell_type"] != "code":
87+
msg = f"Cell with ID {cell_id} of document {document_id} is not of type code."
88+
get_logger().error(msg)
89+
raise KeyError(
90+
msg,
91+
)
92+
return ycell
93+
94+
95+
def _output_hook(outputs: list[NotebookNode], ycell: y.Map | None, msg: dict) -> None:
96+
"""Callback on execution request when an output is emitted.
97+
98+
Args:
99+
outputs: A list of previously emitted outputs
100+
ycell: The cell being executed
101+
msg: The output message
102+
"""
103+
msg_type = msg["header"]["msg_type"]
104+
if msg_type in ("display_data", "stream", "execute_result", "error"):
105+
# FIXME support for version
106+
output = nbformat.v4.output_from_msg(msg)
107+
outputs.append(output)
108+
109+
if ycell is not None:
110+
cell_outputs = ycell["outputs"]
111+
if msg_type == "stream":
112+
with cell_outputs.doc.transaction():
113+
text = output["text"]
114+
115+
# FIXME Logic is quite complex at https://github.com/jupyterlab/jupyterlab/blob/7ae2d436fc410b0cff51042a3350ba71f54f4445/packages/outputarea/src/model.ts#L518
116+
if text.endswith((os.linesep, "\n")):
117+
text = text[:-1]
118+
119+
if (not cell_outputs) or (cell_outputs[-1]["name"] != output["name"]):
120+
output["text"] = [text]
121+
cell_outputs.append(output)
122+
else:
123+
last_output = cell_outputs[-1]
124+
last_output["text"].append(text)
125+
cell_outputs[-1] = last_output
126+
else:
127+
with cell_outputs.doc.transaction():
128+
cell_outputs.append(output)
129+
130+
elif msg_type == "clear_output":
131+
# FIXME msg.content.wait - if true should clear at the next message
132+
outputs.clear()
133+
134+
if ycell is not None:
135+
del ycell["outputs"][:]
136+
137+
elif msg_type == "update_display_data":
138+
# FIXME
139+
...
140+
141+
142+
def _stdin_hook(kernel_id: str, request_id: str, pending_input: PendingInput, msg: dict) -> None:
143+
"""Callback on stdin message.
144+
145+
It will register the pending input as temporary answer to the execution request.
146+
147+
Args:
148+
kernel_id: The Kernel ID
149+
request_id: The request ID that triggers the input request
150+
pending_input: The pending input description.
151+
This object will be mutated with useful information from ``msg``.
152+
msg: The stdin msg
153+
"""
154+
get_logger().debug(f"Execution request {kernel_id} received a input request.")
155+
if PendingInput.request_id is not None:
156+
get_logger().error(
157+
f"Execution request {kernel_id} received a input request while waiting for an input.\n{msg}" # noqa: E501
158+
)
159+
160+
header = msg["header"].copy()
161+
header["date"] = header["date"].isoformat()
162+
pending_input.request_id = request_id
163+
pending_input.content = InputDescription(
164+
parent_header=header, input_request=InputRequest(**msg["content"])
165+
)
166+
167+
168+
def _get_error(outputs):
169+
return "\n".join(
170+
f"{output['ename']}: {output['evalue']}"
171+
for output in outputs
172+
if output.get("output_type") == "error"
173+
)
174+
175+
176+
async def _execute_snippet(
177+
client: jupyter_client.asynchronous.client.AsyncKernelClient,
178+
ydoc: jupyter_server_ydoc.app.YDocExtension | None,
179+
snippet: str,
180+
metadata: dict | None,
181+
stdin_hook: t.Callable[[dict], None] | None,
182+
) -> dict[str, t.Any]:
183+
"""Snippet executor
184+
185+
Args:
186+
client: Kernel client
187+
ydoc: Jupyter server YDoc extension
188+
snippet: The code snippet to execute
189+
metadata: The code snippet metadata; e.g. to define the snippet context
190+
stdin_hook: The stdin message callback
191+
Returns:
192+
The execution status and outputs.
193+
"""
194+
ycell = None
195+
time_info = {}
196+
if metadata is not None:
197+
ycell = await _get_ycell(ydoc, metadata)
198+
if ycell is not None:
199+
execution_start_time = datetime.now(timezone.utc).isoformat()[:-6]
200+
# Reset cell
201+
with ycell.doc.transaction():
202+
del ycell["outputs"][:]
203+
ycell["execution_count"] = None
204+
ycell["execution_state"] = "running"
205+
if "execution" in ycell["metadata"]:
206+
del ycell["metadata"]["execution"]
207+
if metadata.get("record_timing", False):
208+
time_info = ycell["metadata"].get("execution", {})
209+
time_info["shell.execute_reply.started"] = execution_start_time
210+
# for compatibility with jupyterlab-execute-time also set:
211+
time_info["iopub.execute_input"] = execution_start_time
212+
ycell["metadata"]["execution"] = time_info
213+
# Emit cell execution start event
214+
event_logger.emit(
215+
schema_id="https://events.jupyter.org/jupyter_server_nbmodel/cell_execution/v1",
216+
data={
217+
"event_type": "execution_start",
218+
"cell_id": metadata["cell_id"],
219+
"document_id": metadata["document_id"],
220+
"timestamp": execution_start_time
221+
}
222+
)
223+
outputs = []
224+
225+
# FIXME we don't check if the session is consistent (aka the kernel is linked to the document)
226+
# - should we?
227+
reply = await ensure_async(
228+
client.execute_interactive(
229+
snippet,
230+
# FIXME stream partial results
231+
output_hook=partial(_output_hook, outputs, ycell),
232+
stdin_hook=stdin_hook if client.allow_stdin else None,
233+
)
234+
)
235+
236+
reply_content = reply["content"]
237+
238+
if ycell is not None:
239+
execution_end_time = datetime.now(timezone.utc).isoformat()[:-6]
240+
with ycell.doc.transaction():
241+
ycell["execution_count"] = reply_content.get("execution_count")
242+
ycell["execution_state"] = "idle"
243+
if metadata and metadata.get("record_timing", False):
244+
if reply_content["status"] == "ok":
245+
time_info["shell.execute_reply"] = execution_end_time
246+
else:
247+
time_info["execution_failed"] = execution_end_time
248+
ycell["metadata"]["execution"] = time_info
249+
# Emit cell execution end event
250+
event_logger.emit(
251+
schema_id="https://events.jupyter.org/jupyter_server_nbmodel/cell_execution/v1",
252+
data={
253+
"event_type": "execution_end",
254+
"cell_id": metadata["cell_id"],
255+
"document_id": metadata["document_id"],
256+
"success": reply_content["status"]=="ok",
257+
"kernel_error": _get_error(outputs),
258+
"timestamp": execution_end_time
259+
}
260+
)
261+
return {
262+
"status": reply_content["status"],
263+
"execution_count": reply_content.get("execution_count"),
264+
# FIXME quid for buffers
265+
"outputs": json.dumps(outputs),
266+
}
267+
268+
269+
async def kernel_worker(
270+
kernel_id: str,
271+
client: jupyter_client.asynchronous.client.AsyncKernelClient,
272+
ydoc: jupyter_server_ydoc.app.YDocExtension | None,
273+
queue: asyncio.Queue,
274+
results: dict,
275+
pending_input: PendingInput,
276+
) -> None:
277+
"""Process execution request in order for a kernel."""
278+
get_logger().debug(f"Starting worker to process execution requests of kernel {kernel_id}…")
279+
to_raise = None
280+
while True:
281+
try:
282+
uid, snippet, metadata = await queue.get()
283+
get_logger().debug(f"Processing execution request {uid} for kernel {kernel_id}…")
284+
get_logger().debug("%s %s %s", uid, snippet, metadata)
285+
client.session.session = uid
286+
# FIXME
287+
# client.session.username = username
288+
results[uid] = await _execute_snippet(
289+
client, ydoc, snippet, metadata, partial(_stdin_hook, kernel_id, uid, pending_input)
290+
)
291+
292+
queue.task_done()
293+
get_logger().debug(f"Execution request {uid} processed for kernel {kernel_id}.")
294+
except (asyncio.CancelledError, KeyboardInterrupt, RuntimeError) as e:
295+
get_logger().debug(
296+
f"Stopping execution requests worker for kernel {kernel_id}…", exc_info=e
297+
)
298+
# Empty the queue
299+
while not queue.empty():
300+
queue.task_done()
301+
to_raise = e
302+
break
303+
except BaseException as e:
304+
get_logger().error(
305+
f"Failed to process execution request {uid} for kernel {kernel_id}.", exc_info=e
306+
)
307+
if not queue.empty():
308+
queue.task_done()
309+
310+
if to_raise is not None:
311+
raise to_raise

jupyter_server_nbmodel/event_logger.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# Copyright (c) 2024-2025 Datalayer, Inc.
22
# Distributed under the terms of the Modified BSD License.
33

4-
from jupyter_events import EventLogger
54
import pathlib
65

6+
from jupyter_events import EventLogger
7+
8+
79
_JUPYTER_SERVER_EVENTS_URI = "https://events.jupyter.org/jupyter_server_nbmodel"
810

911
_DEFAULT_EVENTS_SCHEMA_PATH = pathlib.Path(__file__).parent / "event_schemas"

jupyter_server_nbmodel/event_schemas/cell_execution/v1.yaml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Copyright (c) 2024-2025 Datalayer, Inc.
2+
# Distributed under the terms of the Modified BSD License.
3+
14
"$id": https://events.jupyter.org/jupyter_server_nbmodel/cell_execution/v1
25
version: "1"
36
title: Cell Execution activities
@@ -15,27 +18,22 @@ properties:
1518
enum:
1619
- execution_start
1720
- execution_end
18-
1921
cell_id:
2022
type: string
2123
description: |
2224
Cell id.
23-
2425
document_id:
2526
type: string
2627
description: |
2728
Document id.
28-
2929
success:
3030
type: boolean
3131
description: |
3232
Whether the cell execution was successful or not.
33-
3433
kernel_error:
3534
type: string
3635
description: |
3736
Error message from the kernel.
38-
3937
timestamp:
4038
type: string
4139
description: |

0 commit comments

Comments
 (0)