@@ -37,6 +37,7 @@ class ExecutionStack:
37
37
"""
38
38
39
39
def __init__ (self ):
40
+ self .__pending_inputs : dict [str , dict ] = {}
40
41
self .__tasks : dict [str , asyncio .Task ] = {}
41
42
42
43
def __del__ (self ):
@@ -55,11 +56,12 @@ def cancel(self, uid: str) -> None:
55
56
56
57
self .__tasks [uid ].cancel ()
57
58
58
- def get (self , uid : str ) -> t .Any :
59
+ def get (self , kernel_id : str , uid : str ) -> t .Any :
59
60
"""Get the request ``uid`` results or None.
60
61
61
62
Args:
62
- uid (str): Request index
63
+ kernel_id : Kernel identifier
64
+ uid : Request index
63
65
64
66
Returns:
65
67
Any: None if the request is pending else its result
@@ -71,13 +73,16 @@ def get(self, uid: str) -> t.Any:
71
73
if uid not in self .__tasks :
72
74
raise ValueError (f"Request { uid } does not exists." )
73
75
76
+ if kernel_id in self .__pending_inputs :
77
+ return self .__pending_inputs .pop (kernel_id )
78
+
74
79
if self .__tasks [uid ].done ():
75
80
task = self .__tasks .pop (uid )
76
81
return task .result ()
77
82
else :
78
83
return None
79
84
80
- def put (self , task : t . Awaitable , * args ) -> str :
85
+ def put (self , km : jupyter_client . manager . KernelManager , snippet : str , ycell : y . Map ) -> str :
81
86
"""Add a asynchronous execution request.
82
87
83
88
Args:
@@ -87,36 +92,57 @@ def put(self, task: t.Awaitable, *args) -> str:
87
92
Returns:
88
93
Request identifier
89
94
"""
90
- uid = uuid .uuid4 ()
95
+ uid = str ( uuid .uuid4 () )
91
96
92
- async def execute_task (uid , f , * args ) -> t .Any :
93
- try :
94
- get_logger ().debug (f"Will execute request { uid } ." )
95
- result = await f (* args )
96
- except asyncio .CancelledError :
97
- raise
98
- except Exception as e :
99
- exception_type , _ , tb = sys .exc_info ()
100
- result = {
101
- "type" : exception_type .__qualname__ ,
102
- "error" : str (e ),
103
- "message" : repr (e ),
104
- "traceback" : traceback .format_tb (tb ),
105
- }
106
- get_logger ().error ("Error for request %s." , result )
107
- else :
108
- get_logger ().debug (f"Has executed request { uid } ." )
97
+ self .__tasks [uid ] = asyncio .create_task (
98
+ execute_task (uid , km , snippet , ycell , partial (self ._stdin_hook , km .kernel_id ))
99
+ )
100
+ return uid
109
101
110
- return result
102
+ def _stdin_hook (self , kernel_id , msg ) -> None :
103
+ get_logger ().info (f"Execution request { kernel_id } received a input request { msg !s} " )
104
+ if kernel_id in self .__pending_inputs :
105
+ get_logger ().error (f"Execution request { kernel_id } received a input request while waiting for an input.\n { msg } " )
106
+
107
+ header = msg ["header" ].copy ()
108
+ header ["date" ] = header ["date" ].isoformat ()
109
+ self .__pending_inputs [kernel_id ] = {"parent_header" : header , "input_request" : msg ["content" ]}
111
110
112
- self .__tasks [uid ] = asyncio .create_task (execute_task (uid , task , * args ))
113
- return uid
111
+
112
+ async def execute_task (
113
+ uid , km : jupyter_client .manager .KernelManager , snippet : str , ycell : y .Map , stdin_hook
114
+ ) -> t .Any :
115
+ try :
116
+ get_logger ().debug (f"Will execute request { uid } ." )
117
+ result = await _execute_snippet (uid , km , snippet , ycell , stdin_hook )
118
+ except asyncio .CancelledError :
119
+ raise
120
+ except Exception as e :
121
+ exception_type , _ , tb = sys .exc_info ()
122
+ result = {
123
+ "type" : exception_type .__qualname__ ,
124
+ "error" : str (e ),
125
+ "message" : repr (e ),
126
+ "traceback" : traceback .format_tb (tb ),
127
+ }
128
+ get_logger ().error ("Error for request %s." , result )
129
+ else :
130
+ get_logger ().debug (f"Has executed request { uid } ." )
131
+
132
+ return result
114
133
115
134
116
- async def execute_snippet (
117
- km : jupyter_client .manager .KernelManager , snippet : str , ycell : y .Map
135
+ async def _execute_snippet (
136
+ uid : str ,
137
+ km : jupyter_client .client .KernelClient ,
138
+ snippet : str ,
139
+ ycell : y .Map ,
140
+ stdin_hook ,
118
141
) -> dict [str , t .Any ]:
119
142
client = km .client ()
143
+ client .session .session = uid
144
+ # FIXME
145
+ # client.session.username = username
120
146
121
147
if ycell is not None :
122
148
# Reset cell
@@ -125,15 +151,14 @@ async def execute_snippet(
125
151
126
152
outputs = []
127
153
128
- # FIXME set the username of client.session to server user
129
154
# FIXME we don't check if the session is consistent (aka the kernel is linked to the document)
130
155
# - should we?
131
156
try :
132
157
reply = await ensure_async (
133
158
client .execute_interactive (
134
159
snippet ,
135
160
output_hook = partial (_output_hook , ycell , outputs ),
136
- stdin_hook = _stdin_hook if client .allow_stdin else None ,
161
+ stdin_hook = stdin_hook if client .allow_stdin else None ,
137
162
)
138
163
)
139
164
@@ -191,9 +216,6 @@ def _output_hook(ycell, outputs, msg) -> None:
191
216
# FIXME
192
217
...
193
218
194
- def _stdin_hook (msg ) -> None :
195
- get_logger ().info ("Code snippet execution is waiting for an input." )
196
-
197
219
198
220
class ExecuteHandler (ExtensionHandlerMixin , APIHandler ):
199
221
"""Handle request for snippet execution."""
@@ -288,13 +310,38 @@ async def post(self, kernel_id: str) -> None:
288
310
get_logger ().error (msg , exc_info = e )
289
311
raise tornado .web .HTTPError (status_code = HTTPStatus .NOT_FOUND , reason = msg ) from e
290
312
291
- uid = self ._execution_stack .put (execute_snippet , km , snippet , ycell )
313
+ uid = self ._execution_stack .put (km , snippet , ycell )
292
314
293
315
self .set_status (HTTPStatus .ACCEPTED )
294
316
self .set_header ("Location" , f"/api/kernels/{ kernel_id } /requests/{ uid } " )
295
317
self .finish ("{}" )
296
318
297
319
320
+ class InputHandler (ExtensionHandlerMixin , APIHandler ):
321
+ """Handle request for input reply."""
322
+
323
+ @tornado .web .authenticated
324
+ async def post (self , kernel_id : str ) -> None :
325
+ body = self .get_json_body ()
326
+
327
+ try :
328
+ km = self .kernel_manager .get_kernel (kernel_id )
329
+ except KeyError as e :
330
+ msg = f"Unknown kernel with id: { kernel_id } "
331
+ get_logger ().error (msg , exc_info = e )
332
+ raise tornado .web .HTTPError (status_code = HTTPStatus .NOT_FOUND , reason = msg ) from e
333
+
334
+ client = km .client ()
335
+
336
+ try :
337
+ # only send stdin reply if there *was not* another request
338
+ # or execution finished while we were reading.
339
+ if not (await client .stdin_channel .msg_ready () or await client .shell_channel .msg_ready ()):
340
+ client .input (body ["input" ])
341
+ finally :
342
+ del client
343
+
344
+
298
345
class RequestHandler (ExtensionHandlerMixin , APIHandler ):
299
346
"""Handler for /api/kernels/<kernel_id>/requests/<request_id>"""
300
347
@@ -305,14 +352,15 @@ def initialize(
305
352
self ._stack = execution_stack
306
353
307
354
@tornado .web .authenticated
308
- def get (self , kernel_id : str , uid : str ) -> None :
355
+ def get (self , kernel_id : str , request_id : str ) -> None :
309
356
"""`GET /api/kernels/<kernel_id>/requests/<id>` Returns the request ``uid`` status.
310
357
311
358
Status are:
312
359
313
- * 200: Task result is returned
314
- * 202: Task is pending
315
- * 500: Task ends with errors
360
+ * 200: Request result is returned
361
+ * 202: Request is pending
362
+ * 300: Request has a pending input
363
+ * 500: Request ends with errors
316
364
317
365
Args:
318
366
index: Request identifier
@@ -321,7 +369,7 @@ def get(self, kernel_id: str, uid: str) -> None:
321
369
404 if request ``uid`` does not exist
322
370
"""
323
371
try :
324
- r = self ._stack .get (uid )
372
+ r = self ._stack .get (kernel_id , request_id )
325
373
except ValueError as err :
326
374
raise tornado .web .HTTPError (404 , reason = str (err )) from err
327
375
else :
@@ -332,12 +380,15 @@ def get(self, kernel_id: str, uid: str) -> None:
332
380
if "error" in r :
333
381
self .set_status (500 )
334
382
self .log .debug (f"{ r } " )
383
+ elif "input_request" in r :
384
+ self .set_status (300 )
385
+ self .set_header ("Location" , f"/api/kernels/{ kernel_id } /input" )
335
386
else :
336
387
self .set_status (200 )
337
388
self .finish (json .dumps (r ))
338
389
339
390
@tornado .web .authenticated
340
- def delete (self , kernel_id : str , uid : str ) -> None :
391
+ def delete (self , kernel_id : str , request_id : str ) -> None :
341
392
"""`DELETE /api/kernels/<kernel_id>/requests/<id>` cancels the request ``uid``.
342
393
343
394
Status are:
@@ -350,7 +401,7 @@ def delete(self, kernel_id: str, uid: str) -> None:
350
401
404 if request ``uid`` does not exist
351
402
"""
352
403
try :
353
- self ._stack .cancel (int ( uid ) )
404
+ self ._stack .cancel (request_id )
354
405
except ValueError as err :
355
406
raise tornado .web .HTTPError (404 , reason = str (err )) from err
356
407
else :
0 commit comments