88from dataclasses import asdict , dataclass
99from functools import partial
1010from http import HTTPStatus
11+ from datetime import datetime , timezone
1112
1213import jupyter_server
1314import jupyter_server .services
1819from jupyter_core .utils import ensure_async
1920from jupyter_server .base .handlers import APIHandler
2021from jupyter_server .extension .handler import ExtensionHandlerMixin
22+ from jupyter_events import EventLogger
2123
2224from .log import get_logger
25+ from .event_logger import event_logger
2326
2427if t .TYPE_CHECKING :
2528 import jupyter_client
@@ -123,7 +126,6 @@ async def _get_ycell(
123126 raise KeyError (
124127 msg ,
125128 )
126-
127129 return ycell
128130
129131
@@ -199,6 +201,13 @@ def _stdin_hook(kernel_id: str, request_id: str, pending_input: PendingInput, ms
199201 parent_header = header , input_request = InputRequest (** msg ["content" ])
200202 )
201203
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+
202211
203212async def _execute_snippet (
204213 client : jupyter_client .asynchronous .client .AsyncKernelClient ,
@@ -219,15 +228,34 @@ async def _execute_snippet(
219228 The execution status and outputs.
220229 """
221230 ycell = None
231+ time_info = {}
222232 if metadata is not None :
223233 ycell = await _get_ycell (ydoc , metadata )
224234 if ycell is not None :
235+ execution_start_time = datetime .now (timezone .utc ).isoformat ()[:- 6 ]
225236 # Reset cell
226237 with ycell .doc .transaction ():
227238 del ycell ["outputs" ][:]
228239 ycell ["execution_count" ] = None
229240 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+ )
231259 outputs = []
232260
233261 # 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(
244272 reply_content = reply ["content" ]
245273
246274 if ycell is not None :
275+ execution_end_time = datetime .now (timezone .utc ).isoformat ()[:- 6 ]
247276 with ycell .doc .transaction ():
248277 ycell ["execution_count" ] = reply_content .get ("execution_count" )
249278 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+ )
251297 return {
252298 "status" : reply_content ["status" ],
253299 "execution_count" : reply_content .get ("execution_count" ),
@@ -524,9 +570,7 @@ async def post(self, kernel_id: str) -> None:
524570 msg = f"Unknown kernel with id: { kernel_id } "
525571 get_logger ().error (msg )
526572 raise tornado .web .HTTPError (status_code = HTTPStatus .NOT_FOUND , reason = msg )
527-
528573 uid = self ._execution_stack .put (kernel_id , snippet , metadata )
529-
530574 self .set_status (HTTPStatus .ACCEPTED )
531575 self .set_header ("Location" , f"/api/kernels/{ kernel_id } /requests/{ uid } " )
532576 self .finish ("{}" )
0 commit comments