1
+ from __future__ import annotations
2
+
1
3
import json
2
4
import typing as t
3
5
from http import HTTPStatus
10
12
11
13
from .log import get_logger
12
14
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
+
13
24
14
25
class 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 :
16
33
super ().initialize (name , * args , ** kwargs )
34
+ self ._ydoc = ydoc_extension
17
35
self ._outputs = []
36
+ self ._ycell : y .Map | None = None
18
37
19
38
@tornado .web .authenticated
20
39
async def post (self , kernel_id : str ) -> None :
@@ -33,8 +52,59 @@ async def post(self, kernel_id: str) -> None:
33
52
cell_id (str): to-execute cell identifier
34
53
"""
35
54
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
+
38
108
try :
39
109
km = self .kernel_manager .get_kernel (kernel_id )
40
110
except KeyError as e :
@@ -44,7 +114,13 @@ async def post(self, kernel_id: str) -> None:
44
114
45
115
client = km .client ()
46
116
117
+ if self ._ycell is not None :
118
+ # Reset cell
119
+ del self ._ycell ["outputs" ][:]
120
+ self ._ycell ["execution_count" ] = None
121
+
47
122
# 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?
48
124
try :
49
125
reply = await ensure_async (
50
126
client .execute_interactive (
@@ -56,22 +132,37 @@ async def post(self, kernel_id: str) -> None:
56
132
57
133
reply_content = reply ["content" ]
58
134
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
+
65
147
finally :
66
148
self ._outputs .clear ()
149
+ self ._ycell = None
67
150
del client
68
151
69
152
def _output_hook (self , msg ) -> None :
70
153
msg_type = msg ["header" ]["msg_type" ]
71
154
if msg_type in ("display_data" , "stream" , "execute_result" , "error" ):
155
+ # FIXME support for version
72
156
output = nbformat .v4 .output_from_msg (msg )
73
157
get_logger ().info ("Got an output. %s" , output )
74
158
self ._outputs .append (output )
75
159
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
+
76
167
def _stdin_hook (self , msg ) -> None :
77
168
get_logger ().info ("Code snippet execution is waiting for an input." )
0 commit comments