8
8
from dataclasses import asdict , dataclass
9
9
from functools import partial
10
10
from http import HTTPStatus
11
+ from datetime import datetime , timezone
11
12
12
13
import jupyter_server
13
14
import jupyter_server .services
18
19
from jupyter_core .utils import ensure_async
19
20
from jupyter_server .base .handlers import APIHandler
20
21
from jupyter_server .extension .handler import ExtensionHandlerMixin
22
+ from jupyter_events import EventLogger
21
23
22
24
from .log import get_logger
25
+ from .event_logger import event_logger
23
26
24
27
if t .TYPE_CHECKING :
25
28
import jupyter_client
@@ -123,7 +126,6 @@ async def _get_ycell(
123
126
raise KeyError (
124
127
msg ,
125
128
)
126
-
127
129
return ycell
128
130
129
131
@@ -199,6 +201,13 @@ def _stdin_hook(kernel_id: str, request_id: str, pending_input: PendingInput, ms
199
201
parent_header = header , input_request = InputRequest (** msg ["content" ])
200
202
)
201
203
204
+ def _get_error (outputs ):
205
+ return "\n " .join (
206
+ f"{ output ['ename' ]} : { output ['evalue' ]} "
207
+ for output in outputs
208
+ if output .get ("output_type" ) == "error"
209
+ )
210
+
202
211
203
212
async def _execute_snippet (
204
213
client : jupyter_client .asynchronous .client .AsyncKernelClient ,
@@ -219,15 +228,34 @@ async def _execute_snippet(
219
228
The execution status and outputs.
220
229
"""
221
230
ycell = None
231
+ time_info = {}
222
232
if metadata is not None :
223
233
ycell = await _get_ycell (ydoc , metadata )
224
234
if ycell is not None :
235
+ execution_start_time = datetime .now (timezone .utc ).isoformat ()[:- 6 ]
225
236
# Reset cell
226
237
with ycell .doc .transaction ():
227
238
del ycell ["outputs" ][:]
228
239
ycell ["execution_count" ] = None
229
240
ycell ["execution_state" ] = "running"
230
-
241
+ if "execution" in ycell ["metadata" ]:
242
+ del ycell ["metadata" ]["execution" ]
243
+ if metadata .get ("record_timing" , False ):
244
+ time_info = ycell ["metadata" ].get ("execution" , {})
245
+ time_info ["shell.execute_reply.started" ] = execution_start_time
246
+ # for compatibility with jupyterlab-execute-time also set:
247
+ time_info ["iopub.execute_input" ] = execution_start_time
248
+ ycell ["metadata" ]["execution" ] = time_info
249
+ # Emit cell execution start event
250
+ event_logger .emit (
251
+ schema_id = "https://events.jupyter.org/jupyter_server_nbmodel/cell_execution/v1" ,
252
+ data = {
253
+ "event_type" : "execution_start" ,
254
+ "cell_id" : metadata ["cell_id" ],
255
+ "document_id" : metadata ["document_id" ],
256
+ "timestamp" : execution_start_time
257
+ }
258
+ )
231
259
outputs = []
232
260
233
261
# FIXME we don't check if the session is consistent (aka the kernel is linked to the document)
@@ -244,10 +272,28 @@ async def _execute_snippet(
244
272
reply_content = reply ["content" ]
245
273
246
274
if ycell is not None :
275
+ execution_end_time = datetime .now (timezone .utc ).isoformat ()[:- 6 ]
247
276
with ycell .doc .transaction ():
248
277
ycell ["execution_count" ] = reply_content .get ("execution_count" )
249
278
ycell ["execution_state" ] = "idle"
250
-
279
+ if metadata and metadata .get ("record_timing" , False ):
280
+ if reply_content ["status" ] == "ok" :
281
+ time_info ["shell.execute_reply" ] = execution_end_time
282
+ else :
283
+ time_info ["execution_failed" ] = execution_end_time
284
+ ycell ["metadata" ]["execution" ] = time_info
285
+ # Emit cell execution end event
286
+ event_logger .emit (
287
+ schema_id = "https://events.jupyter.org/jupyter_server_nbmodel/cell_execution/v1" ,
288
+ data = {
289
+ "event_type" : "execution_end" ,
290
+ "cell_id" : metadata ["cell_id" ],
291
+ "document_id" : metadata ["document_id" ],
292
+ "success" : reply_content ["status" ]== "ok" ,
293
+ "kernel_error" : _get_error (outputs ),
294
+ "timestamp" : execution_end_time
295
+ }
296
+ )
251
297
return {
252
298
"status" : reply_content ["status" ],
253
299
"execution_count" : reply_content .get ("execution_count" ),
@@ -524,9 +570,7 @@ async def post(self, kernel_id: str) -> None:
524
570
msg = f"Unknown kernel with id: { kernel_id } "
525
571
get_logger ().error (msg )
526
572
raise tornado .web .HTTPError (status_code = HTTPStatus .NOT_FOUND , reason = msg )
527
-
528
573
uid = self ._execution_stack .put (kernel_id , snippet , metadata )
529
-
530
574
self .set_status (HTTPStatus .ACCEPTED )
531
575
self .set_header ("Location" , f"/api/kernels/{ kernel_id } /requests/{ uid } " )
532
576
self .finish ("{}" )
0 commit comments