jupyterpack 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +2 -0
  2. package/lib/document/widgetFactory.js +4 -1
  3. package/lib/pythonServer/common/generatedPythonFiles.d.ts +2 -0
  4. package/lib/pythonServer/common/generatedPythonFiles.js +72 -0
  5. package/lib/pythonServer/dash/dashServer.d.ts +24 -0
  6. package/lib/pythonServer/dash/dashServer.js +39 -0
  7. package/lib/pythonServer/dash/generatedPythonFiles.d.ts +2 -0
  8. package/lib/pythonServer/dash/generatedPythonFiles.js +31 -0
  9. package/lib/pythonServer/index.d.ts +5 -0
  10. package/lib/pythonServer/index.js +9 -0
  11. package/lib/pythonServer/kernelExecutor.d.ts +66 -0
  12. package/lib/pythonServer/kernelExecutor.js +133 -0
  13. package/lib/pythonServer/streamlit/generatedPythonFiles.d.ts +2 -0
  14. package/lib/pythonServer/streamlit/generatedPythonFiles.js +147 -0
  15. package/lib/pythonServer/streamlit/streamlitServer.d.ts +33 -0
  16. package/lib/pythonServer/streamlit/streamlitServer.js +55 -0
  17. package/lib/pythonServer/tornado/generatedPythonFiles.d.ts +3 -0
  18. package/lib/pythonServer/tornado/generatedPythonFiles.js +456 -0
  19. package/lib/pythonServer/tornado/tornadoServer.d.ts +32 -0
  20. package/lib/pythonServer/tornado/tornadoServer.js +51 -0
  21. package/lib/pythonWidget/pythonWidget.d.ts +1 -0
  22. package/lib/pythonWidget/pythonWidget.js +9 -3
  23. package/lib/pythonWidget/pythonWidgetModel.d.ts +12 -3
  24. package/lib/pythonWidget/pythonWidgetModel.js +32 -10
  25. package/lib/swConnection/index.js +2 -2
  26. package/lib/{pythonWidget/connectionManager.d.ts → swConnection/mainConnectionManager.d.ts} +10 -0
  27. package/lib/swConnection/mainConnectionManager.js +93 -0
  28. package/lib/swConnection/sw.js +1 -1
  29. package/lib/swConnection/swCommManager.d.ts +11 -0
  30. package/lib/swConnection/{comm_manager.js → swCommManager.js} +5 -0
  31. package/lib/tools.d.ts +4 -0
  32. package/lib/tools.js +58 -0
  33. package/lib/type.d.ts +37 -2
  34. package/lib/type.js +2 -0
  35. package/lib/websocket/websocket.d.ts +0 -0
  36. package/lib/websocket/websocket.js +152 -0
  37. package/package.json +8 -5
  38. package/src/document/widgetFactory.ts +4 -1
  39. package/src/global.d.ts +4 -0
  40. package/src/pythonServer/common/generatedPythonFiles.ts +73 -0
  41. package/src/pythonServer/dash/dashServer.ts +57 -0
  42. package/src/pythonServer/dash/generatedPythonFiles.ts +32 -0
  43. package/src/pythonServer/index.ts +18 -0
  44. package/src/pythonServer/kernelExecutor.ts +229 -0
  45. package/src/pythonServer/streamlit/generatedPythonFiles.ts +148 -0
  46. package/src/pythonServer/streamlit/streamlitServer.ts +87 -0
  47. package/src/pythonServer/tornado/generatedPythonFiles.ts +457 -0
  48. package/src/pythonServer/tornado/tornadoServer.ts +80 -0
  49. package/src/pythonWidget/pythonWidget.ts +20 -3
  50. package/src/pythonWidget/pythonWidgetModel.ts +53 -19
  51. package/src/swConnection/index.ts +5 -2
  52. package/src/swConnection/mainConnectionManager.ts +121 -0
  53. package/src/swConnection/sw.ts +1 -1
  54. package/src/swConnection/{comm_manager.ts → swCommManager.ts} +6 -0
  55. package/src/tools.ts +69 -0
  56. package/src/type.ts +47 -3
  57. package/src/websocket/websocket.ts +216 -0
  58. package/lib/pythonWidget/connectionManager.js +0 -27
  59. package/lib/pythonWidget/kernelExecutor.d.ts +0 -27
  60. package/lib/pythonWidget/kernelExecutor.js +0 -104
  61. package/lib/swConnection/comm_manager.d.ts +0 -6
  62. package/lib/swConnection/connection_manager.d.ts +0 -18
  63. package/lib/swConnection/connection_manager.js +0 -27
  64. package/src/pythonWidget/connectionManager.ts +0 -43
  65. package/src/pythonWidget/kernelExecutor.ts +0 -140
  66. package/src/swConnection/connection_manager.ts +0 -43
@@ -0,0 +1,457 @@
1
+ // Auto-generated TypeScript file from Python files
2
+
3
+ export const bootstrap = `
4
+ import sys
5
+
6
+ if "tornado.gen" in sys.modules:
7
+ del sys.modules["tornado.gen"]
8
+
9
+ tornado = __jupyterpack_import_from_path(
10
+ "tornado", "/lib/python3.13/site-packages/tornado/__init__.py"
11
+ )
12
+ `;
13
+ export const tornadoBridge = `
14
+ import asyncio
15
+ import base64
16
+ import json
17
+ import logging
18
+ import tornado
19
+
20
+ from tornado.http1connection import HTTP1Connection, HTTP1ConnectionParameters
21
+ from tornado.iostream import BaseIOStream, IOStream
22
+ from tornado.httputil import HTTPHeaders, RequestStartLine, HTTPServerRequest
23
+ from tornado.websocket import WebSocketHandler
24
+ import tornado.escape
25
+ import pyjs
26
+ from typing import Any, Dict, List, Optional, Tuple
27
+
28
+
29
+ logger = logging.getLogger(__name__)
30
+ logger.setLevel(logging.WARN)
31
+
32
+
33
+ def convert_headers(
34
+ headers: List[Tuple[str, str]],
35
+ ) -> HTTPHeaders:
36
+ tornado_headers = HTTPHeaders()
37
+ for k, v in headers:
38
+ tornado_headers.add(k, v)
39
+ return tornado_headers
40
+
41
+
42
+ def encode_broadcast_message(
43
+ kernel_client_id: str,
44
+ ws_url: str,
45
+ msg: str | bytes,
46
+ action: str = "backend_message",
47
+ ):
48
+ if isinstance(msg, bytes):
49
+ is_binary = True
50
+ b64_msg = base64.b64encode(msg).decode("ascii")
51
+ elif isinstance(msg, str):
52
+ is_binary = False
53
+ b64_msg = msg
54
+
55
+ return json.dumps(
56
+ {
57
+ "action": action,
58
+ "dest": kernel_client_id,
59
+ "wsUrl": ws_url,
60
+ "payload": {"isBinary": is_binary, "data": b64_msg},
61
+ }
62
+ )
63
+
64
+
65
+ def decode_broadcast_message(payload_message: str):
66
+ msg_object = json.loads(payload_message)
67
+ is_binary = msg_object["isBinary"]
68
+ data = msg_object["data"]
69
+ if is_binary:
70
+ return base64.b64decode(data)
71
+ else:
72
+ return data
73
+
74
+
75
+ class DumpStream(BaseIOStream):
76
+ max_buffer_size = 1048576000
77
+
78
+ def close_fd(*args, **kwargs):
79
+ pass
80
+
81
+ def write_to_fd(self, buf):
82
+ raise NotImplementedError("Not supported!")
83
+
84
+
85
+ class ConnectionState:
86
+ def __init__(self):
87
+ self._reply_body = b""
88
+ self._finish_future = asyncio.Future()
89
+ self._reply_headers = []
90
+ self._status: Optional[int] = None
91
+
92
+ @property
93
+ def reply_body(self):
94
+ return base64.b64encode(self._reply_body).decode("ascii")
95
+
96
+ @property
97
+ def reply_headers(self):
98
+ reply_headers = json.dumps(dict(self._reply_headers)).encode("utf-8")
99
+ return base64.b64encode(reply_headers).decode("ascii")
100
+
101
+ @property
102
+ def status(self):
103
+ return self._status
104
+
105
+ @property
106
+ def finish_future(self):
107
+ return self._finish_future
108
+
109
+ def append_reply_body(self, chunk: bytes):
110
+ self._reply_body += chunk
111
+
112
+ def append_reply_header(self, headers: List[Tuple[str, str]]):
113
+ self._reply_headers.extend(headers)
114
+
115
+ def set_status(self, status: int):
116
+ self._status = status
117
+
118
+ def finish(self):
119
+ self._finish_future.set_result(None)
120
+
121
+
122
+ class PatchedConnection(HTTP1Connection):
123
+ def __init__(
124
+ self,
125
+ stream: IOStream,
126
+ is_client: bool,
127
+ params: Optional[HTTP1ConnectionParameters] = None,
128
+ context: Optional[object] = None,
129
+ initial_request_data: Optional[Dict] = None,
130
+ ) -> None:
131
+ super().__init__(stream, is_client, params, context)
132
+ self._connection_state = ConnectionState()
133
+ if initial_request_data is not None:
134
+ self._request_start_line = RequestStartLine(
135
+ initial_request_data.get("request_method"),
136
+ initial_request_data.get("request_url"),
137
+ "HTTP/1.1",
138
+ )
139
+
140
+ @property
141
+ def connection_state(self):
142
+ return self._connection_state
143
+
144
+ def write(self, chunk: bytes):
145
+ self._connection_state.append_reply_body(chunk)
146
+ f = asyncio.Future()
147
+ f.set_result(None)
148
+ return f
149
+
150
+ def write_headers(self, start_line, headers, chunk=None):
151
+ self._connection_state.set_status(int(start_line.code))
152
+ self._connection_state.append_reply_header(headers.get_all())
153
+ if chunk is not None:
154
+ self._connection_state.append_reply_body(chunk)
155
+ f = asyncio.Future()
156
+ f.set_result(None)
157
+ return f
158
+
159
+ def finish(self):
160
+ self._connection_state.finish()
161
+
162
+
163
+ class WSConnection:
164
+ def __init__(
165
+ self,
166
+ instance_id: str,
167
+ kernel_client_id: str,
168
+ ws_url: str,
169
+ broadcast_channel: Any,
170
+ ):
171
+ self.instance_id = instance_id
172
+ self.kernel_client_id = kernel_client_id
173
+ self.ws_url = ws_url
174
+ self.broadcast_channel = broadcast_channel
175
+
176
+ client_terminated = False
177
+
178
+ def is_closing(self):
179
+ return False
180
+
181
+ def write_message(self, msg, binary=False):
182
+ if isinstance(msg, dict):
183
+ msg = tornado.escape.json_encode(msg)
184
+ self.broadcast_channel.postMessage(
185
+ encode_broadcast_message(self.kernel_client_id, self.ws_url, msg)
186
+ )
187
+ f = asyncio.Future()
188
+ f.set_result(None)
189
+ return f
190
+
191
+ def write_ping(self, data):
192
+ pass
193
+
194
+ def close(self, code, reason=None):
195
+ pass
196
+
197
+
198
+ class TornadoBridge:
199
+ """
200
+ Encapsulates Tornado app lifecycle, request/response bridging, and WebSocket handling.
201
+ """
202
+
203
+ def __init__(self, tornado_app: tornado.web.Application, base_url: str):
204
+ self.base_url = base_url
205
+ self.tornado_app = tornado_app
206
+ self.websocket_handlers: Dict[str, tornado.websocket.WebSocketHandler] = {}
207
+ self._patched = False
208
+ self._ws_handlers = {}
209
+ self._ws_broadcast_channels = {}
210
+
211
+ # ---------------------------
212
+ # Fetch Handling
213
+ # ---------------------------
214
+
215
+ async def fetch(self, request: Dict):
216
+ """
217
+ request: {body: Optional[bytes], headers: List[Tuple[str, str]], method: str, url: str}
218
+ """
219
+
220
+ request_headers = convert_headers(request.get("headers", []))
221
+ request_method = request.get("method", "GET").upper()
222
+ request_url: str = request.get("url", "/")
223
+
224
+ request_body = request.get("body", None)
225
+ stream = DumpStream()
226
+
227
+ connection = PatchedConnection(
228
+ stream,
229
+ is_client=False,
230
+ params=None,
231
+ context=None,
232
+ initial_request_data={
233
+ "request_method": request_method,
234
+ "request_url": request_url,
235
+ },
236
+ )
237
+
238
+ request = HTTPServerRequest(
239
+ method=request_method,
240
+ uri=request_url,
241
+ headers=request_headers,
242
+ body=request_body,
243
+ connection=connection,
244
+ )
245
+ try:
246
+ handler = self.tornado_app.find_handler(request)
247
+ handler.execute()
248
+ connection_state = connection.connection_state
249
+ await connection_state.finish_future
250
+
251
+ return {
252
+ "content": connection_state.reply_body,
253
+ "headers": connection_state.reply_headers,
254
+ "status_code": connection_state.status,
255
+ }
256
+ except Exception as e:
257
+ return {
258
+ "headers": "e30=", # {}
259
+ "content": base64.b64encode(str(e).encode("utf-8")).decode("ascii"),
260
+ "status_code": 500,
261
+ }
262
+
263
+ # ---------------------------
264
+ # Websocket Handling
265
+ # ---------------------------
266
+ async def open_ws(
267
+ self,
268
+ instance_id: str,
269
+ kernel_client_id: str,
270
+ ws_url: str,
271
+ protocols_str: str | None,
272
+ ):
273
+ handler_key = f"{instance_id}@{kernel_client_id}@{ws_url}"
274
+ broadcast_channel = self._ws_broadcast_channels.get(handler_key, None)
275
+
276
+ if broadcast_channel is None:
277
+ broadcast_channel = pyjs.js.BroadcastChannel.new(
278
+ f"/jupyterpack/ws/{instance_id}"
279
+ )
280
+ self._ws_broadcast_channels[handler_key] = broadcast_channel
281
+
282
+ headers = convert_headers(
283
+ [
284
+ ("X-Sec-WebSocket-Protocol", protocols_str),
285
+ ("Upgrade", "websocket"),
286
+ ("Connection", "Upgrade"),
287
+ ("Sec-WebSocket-Key", ""),
288
+ ("Sec-WebSocket-Version", "13"),
289
+ ]
290
+ )
291
+ protocols = []
292
+ if protocols_str is not None:
293
+ protocols = protocols_str.split(",")
294
+
295
+ stream = DumpStream()
296
+ ws_connection = WSConnection(
297
+ instance_id, kernel_client_id, ws_url, broadcast_channel
298
+ )
299
+ connection = PatchedConnection(
300
+ stream,
301
+ is_client=False,
302
+ params=None,
303
+ context=None,
304
+ initial_request_data={
305
+ "request_method": "GET",
306
+ "request_url": ws_url,
307
+ },
308
+ )
309
+
310
+ request = tornado.httputil.HTTPServerRequest(
311
+ method="GET",
312
+ uri=ws_url,
313
+ headers=headers,
314
+ body=None,
315
+ connection=connection,
316
+ )
317
+
318
+ handler = self.tornado_app.find_handler(request)
319
+ ret = handler.execute()
320
+ if ret is not None:
321
+ await ret
322
+ connection_state = connection.connection_state
323
+ await connection_state.finish_future
324
+
325
+ if isinstance(handler.handler, WebSocketHandler):
326
+ handler.handler.select_subprotocol(protocols)
327
+ handler.handler.ws_connection = ws_connection
328
+ ret = handler.handler.open(
329
+ *handler.handler.open_args, **handler.handler.open_kwargs
330
+ )
331
+ if ret is not None:
332
+ try:
333
+ await ret
334
+ except Exception:
335
+ raise ("Failed to open websocket")
336
+
337
+ self._ws_handlers[handler_key] = handler
338
+
339
+ self.send_ws_message_to_js(
340
+ instance_id, kernel_client_id, ws_url, "", "connected"
341
+ )
342
+
343
+ def send_ws_message_to_js(
344
+ self,
345
+ instance_id: str,
346
+ kernel_client_id: str,
347
+ ws_url: str,
348
+ msg: str | bytes,
349
+ action: str = "backend_message",
350
+ ):
351
+ handler_key = f"{instance_id}@{kernel_client_id}@{ws_url}"
352
+ broadcast_channel = self._ws_broadcast_channels.get(handler_key, None)
353
+ if broadcast_channel is not None:
354
+ broadcast_channel.postMessage(
355
+ encode_broadcast_message(kernel_client_id, ws_url, msg, action)
356
+ )
357
+
358
+ async def receive_ws_message_from_js(
359
+ self, instance_id: str, kernel_client_id: str, ws_url: str, payload_message: str
360
+ ):
361
+ handler_key = f"{instance_id}@{kernel_client_id}@{ws_url}"
362
+ handler = self._ws_handlers.get(handler_key, None)
363
+ if handler is not None:
364
+ data = decode_broadcast_message(payload_message)
365
+ future = handler.handler.on_message(data)
366
+ if future is not None:
367
+ await future
368
+ `;
369
+ export const tornadoLoader = `
370
+ import base64
371
+ import json
372
+
373
+ try:
374
+ # Check if __jupyterpack_tornado_instance defined from previous run
375
+ __jupyterpack_tornado_instance
376
+ except NameError:
377
+ __jupyterpack_tornado_instance = {
378
+ "tornado_bridge": None,
379
+ "tornado_server": None,
380
+ }
381
+
382
+ def __jupyterpack_tornado_dispose():
383
+ global __jupyterpack_tornado_instance
384
+ __jupyterpack_tornado_instance = {
385
+ "tornado_bridge": None,
386
+ "tornado_server": None,
387
+ }
388
+
389
+
390
+
391
+ async def __jupyterpack_tornado_open_ws(
392
+ instance_id: str, kernel_client_id: str, ws_url: str, protocols_str: str | None
393
+ ):
394
+ tornado_bridge = __jupyterpack_tornado_instance["tornado_bridge"]
395
+ if tornado_bridge is None:
396
+ raise Exception("Missing tornado instance")
397
+ await tornado_bridge.open_ws(instance_id, kernel_client_id, ws_url, protocols_str)
398
+
399
+
400
+ async def __jupyterpack_tornado_receive_ws_message(
401
+ instance_id: str, kernel_client_id: str, ws_url: str, payload_message: str
402
+ ):
403
+ tornado_bridge = __jupyterpack_tornado_instance["tornado_bridge"]
404
+ if tornado_bridge is None:
405
+ raise Exception("Missing tornado instance")
406
+ await tornado_bridge.receive_ws_message_from_js(
407
+ instance_id, kernel_client_id, ws_url, payload_message
408
+ )
409
+
410
+
411
+ async def __jupyterpack_tornado_get_response(
412
+ method, url, headers, content=None, params=None
413
+ ):
414
+ global __jupyterpack_tornado_instance
415
+ if __jupyterpack_tornado_instance["tornado_server"] is None:
416
+ __jupyterpack_tornado_instance["tornado_server"] = app # noqa
417
+ __jupyterpack_tornado_instance["tornado_bridge"] = TornadoBridge( # noqa
418
+ app, "{{base_url}}" # noqa
419
+ )
420
+
421
+ tornado_bridge = __jupyterpack_tornado_instance["tornado_bridge"]
422
+ req_dict = {
423
+ "method": method,
424
+ "url": url,
425
+ "headers": list(headers.items()),
426
+ "body": content,
427
+ }
428
+
429
+ try:
430
+ res_body, res_headers, res_status = await tornado_bridge.fetch(req_dict)
431
+ response = {
432
+ "headers": dict(res_headers),
433
+ "content": res_body,
434
+ "status_code": res_status,
435
+ "original_request": {
436
+ "method": method,
437
+ "url": url,
438
+ "params": params,
439
+ "headers": headers,
440
+ },
441
+ }
442
+ except Exception as e:
443
+ response = {
444
+ "headers": {},
445
+ "content": str(e),
446
+ "status_code": 500,
447
+ "original_request": {
448
+ "method": method,
449
+ "url": url,
450
+ "params": params,
451
+ "headers": headers,
452
+ },
453
+ }
454
+ json_str = json.dumps(response)
455
+ b64_str = base64.b64encode(json_str.encode("utf-8")).decode("utf-8")
456
+ return b64_str
457
+ `;
@@ -0,0 +1,80 @@
1
+ import { stringOrNone } from '../../tools';
2
+ import { IDict, JupyterPackFramework } from '../../type';
3
+ import { tools } from '../common/generatedPythonFiles';
4
+ import { KernelExecutor } from '../kernelExecutor';
5
+ import {
6
+ bootstrap,
7
+ tornadoBridge,
8
+ tornadoLoader
9
+ } from './generatedPythonFiles';
10
+ export class TornadoServer extends KernelExecutor {
11
+ async init(options: {
12
+ initCode?: string;
13
+ instanceId: string;
14
+ kernelClientId: string;
15
+ }) {
16
+ await super.init(options);
17
+ const { initCode, instanceId, kernelClientId } = options;
18
+
19
+ const baseURL = this.buildBaseURL({
20
+ instanceId,
21
+ kernelClientId,
22
+ framework: JupyterPackFramework.TORNADO
23
+ });
24
+ await this.executeCode({ code: tools.replaceAll('{{base_url}}', baseURL) });
25
+ await this.executeCode({ code: bootstrap });
26
+ await this.executeCode({ code: tornadoBridge });
27
+ if (initCode) {
28
+ const initCodeWithUrl = initCode.replaceAll('{{base_url}}', baseURL);
29
+ await this.executeCode({ code: initCodeWithUrl });
30
+ const torCode = tornadoLoader.replaceAll('{{base_url}}', baseURL);
31
+ await this.executeCode({ code: torCode });
32
+ }
33
+ }
34
+
35
+ getResponseFunctionFactory(options: {
36
+ urlPath: string;
37
+ method: string;
38
+ headers: IDict;
39
+ params?: string;
40
+ content?: string;
41
+ }) {
42
+ const { method, urlPath, headers, params, content } = options;
43
+ const code = `await ${this._GET_RESPONSE_FUNCTION}("${method}", "${urlPath}", headers=${JSON.stringify(headers)} , content=${stringOrNone(content)}, params=${stringOrNone(params)})`;
44
+ return code;
45
+ }
46
+
47
+ openWebsocketFunctionFactory(options: {
48
+ instanceId: string;
49
+ kernelId: string;
50
+ wsUrl: string;
51
+ protocol?: string;
52
+ }): string {
53
+ const { instanceId, kernelId, wsUrl, protocol } = options;
54
+
55
+ const code = `await ${this._OPEN_WEBSOCKET_FUNCTION}("${instanceId}", "${kernelId}", "${wsUrl}", ${stringOrNone(protocol)})`;
56
+ return code;
57
+ }
58
+
59
+ sendWebsocketMessageFunctionFactory(options: {
60
+ instanceId: string;
61
+ kernelId: string;
62
+ wsUrl: string;
63
+ message: string;
64
+ }): string {
65
+ const { instanceId, kernelId, wsUrl, message } = options;
66
+ const code = `await ${this._SEND_WEBSOCKET_FUNCTION}("${instanceId}", "${kernelId}", "${wsUrl}", '''${message}''')`;
67
+ return code;
68
+ }
69
+
70
+ async disposePythonServer(): Promise<void> {
71
+ await this.executeCode({
72
+ code: '__jupyterpack_tornado_dispose()'
73
+ });
74
+ }
75
+
76
+ private _GET_RESPONSE_FUNCTION = '__jupyterpack_tornado_get_response';
77
+ private _OPEN_WEBSOCKET_FUNCTION = '__jupyterpack_tornado_open_ws';
78
+
79
+ private _SEND_WEBSOCKET_FUNCTION = '__jupyterpack_tornado_receive_ws_message';
80
+ }
@@ -1,5 +1,5 @@
1
1
  import { PythonWidgetModel } from './pythonWidgetModel';
2
- import { PageConfig } from '@jupyterlab/coreutils';
2
+ import { PageConfig, URLExt } from '@jupyterlab/coreutils';
3
3
  import { IFramePanel } from '../document/iframePanel';
4
4
 
5
5
  export class PythonWidget extends IFramePanel {
@@ -7,12 +7,25 @@ export class PythonWidget extends IFramePanel {
7
7
  super();
8
8
  this._model = options.model;
9
9
  this._model.initialize().then(connectionData => {
10
- if (!connectionData) {
10
+ if (!connectionData.success) {
11
+ this.toggleSpinner(false);
12
+ this._iframe.contentDocument!.body.innerText = `Failed to start server: ${connectionData.error}`;
11
13
  return;
12
14
  }
13
15
  const iframe = this._iframe;
14
16
  const fullLabextensionsUrl = PageConfig.getOption('fullLabextensionsUrl');
15
- iframe.src = `${fullLabextensionsUrl}/jupyterpack/static/${connectionData.instanceId}/dash/${connectionData.kernelClientId}/`;
17
+
18
+ const iframeUrl = URLExt.join(
19
+ fullLabextensionsUrl,
20
+ 'jupyterpack/static',
21
+ connectionData.instanceId,
22
+ connectionData.framework,
23
+ connectionData.kernelClientId,
24
+ connectionData.rootUrl
25
+ );
26
+
27
+ iframe.src = iframeUrl;
28
+
16
29
  iframe.addEventListener('load', () => {
17
30
  this.toggleSpinner(false);
18
31
  });
@@ -23,6 +36,10 @@ export class PythonWidget extends IFramePanel {
23
36
  return this._model;
24
37
  }
25
38
 
39
+ dispose(): void {
40
+ this._model.dispose();
41
+ }
42
+
26
43
  private _model: PythonWidgetModel;
27
44
  }
28
45