1+ from __future__ import annotations
2+
13import json
24import typing as t
35from http import HTTPStatus
1012
1113from .log import get_logger
1214
15+ if t .TYPE_CHECKING :
16+ try :
17+ import pycrdt as y
18+ import jupyter_server_ydoc
19+ from jupyter_ydoc .ynotebook import YNotebook
20+ except ImportError :
21+ # optional dependencies
22+ ...
23+
1324
1425class ExecuteHandler (ExtensionHandlerMixin , APIHandler ):
15- def initialize (self , name : str , * args : t .Any , ** kwargs : t .Any ) -> None :
26+ def initialize (
27+ self ,
28+ name : str ,
29+ ydoc_extension : "jupyter_server_ydoc.app.YDocExtension" | None ,
30+ * args : t .Any ,
31+ ** kwargs : t .Any ,
32+ ) -> None :
1633 super ().initialize (name , * args , ** kwargs )
34+ self ._ydoc = ydoc_extension
1735 self ._outputs = []
36+ self ._ycell : y .Map | None = None
1837
1938 @tornado .web .authenticated
2039 async def post (self , kernel_id : str ) -> None :
@@ -33,8 +52,59 @@ async def post(self, kernel_id: str) -> None:
3352 cell_id (str): to-execute cell identifier
3453 """
3554 body = self .get_json_body ()
36- # FIXME support loading from RTC
37- snippet = body ["code" ]
55+
56+ snippet = body .get ("code" )
57+ # From RTC model
58+ if snippet is None :
59+ document_id = body .get ("document_id" )
60+ cell_id = body .get ("cell_id" )
61+
62+ if document_id is None or cell_id is None :
63+ msg = "Either code or document_id and cell_id must be defined in the request body."
64+ get_logger ().error (msg )
65+ raise tornado .web .HTTPError (
66+ status_code = HTTPStatus .BAD_REQUEST ,
67+ reason = msg ,
68+ )
69+
70+ if self ._ydoc is None :
71+ msg = "jupyter-collaboration extension is not installed on the server."
72+ get_logger ().error (msg )
73+ raise tornado .web .HTTPError (
74+ status_code = HTTPStatus .INTERNAL_SERVER_ERROR , reason = msg
75+ )
76+
77+ notebook : YNotebook = await self ._ydoc .get_document (document_id = document_id , copy = False )
78+
79+ if notebook is None :
80+ msg = f"Document with ID { document_id } not found."
81+ get_logger ().error (msg )
82+ raise tornado .web .HTTPError (status_code = HTTPStatus .NOT_FOUND , reason = msg )
83+
84+ ycells = filter (lambda c : c ["id" ] == cell_id , notebook .ycells )
85+ try :
86+ self ._ycell = next (ycells )
87+ except StopIteration :
88+ msg = f"Cell with ID { cell_id } not found in document { document_id } ."
89+ get_logger ().error (msg )
90+ raise tornado .web .HTTPError (status_code = HTTPStatus .NOT_FOUND , reason = msg ) # noqa: B904
91+ else :
92+ # Check if there is more than one cell
93+ try :
94+ next (ycells )
95+ except StopIteration :
96+ get_logger ().warning ("Multiple cells have the same ID '%s'." , cell_id )
97+
98+ if self ._ycell ["cell_type" ] != "code" :
99+ msg = f"Cell with ID { cell_id } of document { document_id } is not of type code."
100+ get_logger ().error (msg )
101+ raise tornado .web .HTTPError (
102+ status_code = HTTPStatus .BAD_REQUEST ,
103+ reason = msg ,
104+ )
105+
106+ snippet = str (self ._ycell ["source" ])
107+
38108 try :
39109 km = self .kernel_manager .get_kernel (kernel_id )
40110 except KeyError as e :
@@ -44,7 +114,13 @@ async def post(self, kernel_id: str) -> None:
44114
45115 client = km .client ()
46116
117+ if self ._ycell is not None :
118+ # Reset cell
119+ del self ._ycell ["outputs" ][:]
120+ self ._ycell ["execution_count" ] = None
121+
47122 # FIXME set the username of client.session to server user
123+ # FIXME we don't check if the session is consistent (aka the kernel is linked to the document) - should we?
48124 try :
49125 reply = await ensure_async (
50126 client .execute_interactive (
@@ -56,22 +132,37 @@ async def post(self, kernel_id: str) -> None:
56132
57133 reply_content = reply ["content" ]
58134
59- self .finish ({
60- "status" : reply_content ["status" ],
61- "execution_count" : reply_content ["execution_count" ],
62- # FIXME quid for buffers
63- "outputs" : json .dumps (self ._outputs )
64- })
135+ if self ._ycell is not None :
136+ self ._ycell ["execution_count" ] = reply_content ["execution_count" ]
137+
138+ self .finish (
139+ {
140+ "status" : reply_content ["status" ],
141+ "execution_count" : reply_content ["execution_count" ],
142+ # FIXME quid for buffers
143+ "outputs" : json .dumps (self ._outputs ),
144+ }
145+ )
146+
65147 finally :
66148 self ._outputs .clear ()
149+ self ._ycell = None
67150 del client
68151
69152 def _output_hook (self , msg ) -> None :
70153 msg_type = msg ["header" ]["msg_type" ]
71154 if msg_type in ("display_data" , "stream" , "execute_result" , "error" ):
155+ # FIXME support for version
72156 output = nbformat .v4 .output_from_msg (msg )
73157 get_logger ().info ("Got an output. %s" , output )
74158 self ._outputs .append (output )
75159
160+ if self ._ycell is not None :
161+ # FIXME support for 'stream'
162+ outputs = self ._ycell ["outputs" ]
163+ with outputs .doc .transaction ():
164+ outputs .append (output )
165+
166+
76167 def _stdin_hook (self , msg ) -> None :
77168 get_logger ().info ("Code snippet execution is waiting for an input." )
0 commit comments