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