e2b-code-interpreter 0.0.7__tar.gz → 0.0.8a0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: e2b-code-interpreter
3
- Version: 0.0.7
3
+ Version: 0.0.8a0
4
4
  Summary: E2B Code Interpreter - Stateful code execution
5
5
  Home-page: https://e2b.dev/
6
6
  License: Apache-2.0
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.9
14
14
  Classifier: Programming Language :: Python :: 3.10
15
15
  Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
- Requires-Dist: e2b (>=0.17.0)
17
+ Requires-Dist: e2b (>=0.17.1)
18
18
  Requires-Dist: pydantic (>1,<3)
19
19
  Requires-Dist: websocket-client (>=1.7.0,<2.0.0)
20
20
  Project-URL: Bug Tracker, https://github.com/e2b-dev/code-interpreter/issues
@@ -0,0 +1,379 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import threading
6
+ import uuid
7
+
8
+ import requests
9
+
10
+ from concurrent.futures import Future
11
+ from typing import Any, Callable, List, Optional, Dict
12
+
13
+ from e2b import EnvVars, ProcessMessage, Sandbox
14
+ from e2b.constants import TIMEOUT
15
+
16
+ from e2b_code_interpreter.messaging import JupyterKernelWebSocket
17
+ from e2b_code_interpreter.models import KernelException, Execution, Result
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class CodeInterpreter(Sandbox):
23
+ """
24
+ E2B code interpreter sandbox extension.
25
+ """
26
+
27
+ template = "code-interpreter-stateful"
28
+
29
+ def __init__(
30
+ self,
31
+ template: Optional[str] = None,
32
+ api_key: Optional[str] = None,
33
+ cwd: Optional[str] = None,
34
+ env_vars: Optional[EnvVars] = None,
35
+ timeout: Optional[float] = TIMEOUT,
36
+ on_stdout: Optional[Callable[[ProcessMessage], Any]] = None,
37
+ on_stderr: Optional[Callable[[ProcessMessage], Any]] = None,
38
+ on_exit: Optional[Callable[[int], Any]] = None,
39
+ **kwargs,
40
+ ):
41
+ super().__init__(
42
+ template=template or self.template,
43
+ api_key=api_key,
44
+ cwd=cwd,
45
+ env_vars=env_vars,
46
+ timeout=timeout,
47
+ on_stdout=on_stdout,
48
+ on_stderr=on_stderr,
49
+ on_exit=on_exit,
50
+ **kwargs,
51
+ )
52
+ self.notebook = JupyterExtension(self, timeout=timeout)
53
+ # Close all the websocket connections when the interpreter is closed
54
+ self._process_cleanup.append(self.notebook.close)
55
+
56
+
57
+ class JupyterExtension:
58
+
59
+ def __init__(self, sandbox: CodeInterpreter, timeout: Optional[float] = TIMEOUT):
60
+ self._sandbox = sandbox
61
+ self._kernel_id_set = Future()
62
+ self._start_connecting_to_default_kernel(timeout=timeout)
63
+ self._connected_kernels: Dict[str, Future[JupyterKernelWebSocket]] = {}
64
+ self._default_kernel_id: Optional[str] = None
65
+
66
+ def get_default_url(self) -> str:
67
+ return f"https://{self._sandbox.get_hostname(8888)}/doc/tree/RTC:default.ipynb"
68
+
69
+ def exec_cell(
70
+ self,
71
+ code: str,
72
+ kernel_id: Optional[str] = None,
73
+ on_stdout: Optional[Callable[[ProcessMessage], Any]] = None,
74
+ on_stderr: Optional[Callable[[ProcessMessage], Any]] = None,
75
+ on_result: Optional[Callable[[Result], Any]] = None,
76
+ timeout: Optional[float] = TIMEOUT,
77
+ ) -> Execution:
78
+ """
79
+ Execute code in a notebook cell.
80
+
81
+ :param code: Code to execute
82
+ :param kernel_id: The ID of the kernel to execute the code on. If not provided, the default kernel is used.
83
+ :param on_stdout: A callback function to handle standard output messages from the code execution.
84
+ :param on_stderr: A callback function to handle standard error messages from the code execution.
85
+ :param on_result: A callback function to handle the result and display calls of the code execution.
86
+ :param timeout: Timeout for the call
87
+
88
+ :return: Result of the execution
89
+ """
90
+ kernel_id = kernel_id or self.default_kernel_id
91
+ ws_future = self._connected_kernels.get(kernel_id)
92
+
93
+ logger.debug(f"Executing code in kernel {kernel_id}")
94
+
95
+ if ws_future:
96
+ logger.debug(f"Using existing websocket connection to kernel {kernel_id}")
97
+ ws = ws_future.result(timeout=timeout)
98
+ else:
99
+ logger.debug(f"Creating new websocket connection to kernel {kernel_id}")
100
+ ws = self._connect_to_kernel_ws(kernel_id, None, timeout=timeout)
101
+
102
+ message_id = ws.send_execution_message(code, on_stdout, on_stderr, on_result)
103
+ logger.debug(
104
+ f"Sent execution message to kernel {kernel_id}, message_id: {message_id}"
105
+ )
106
+
107
+ result = ws.get_result(message_id, timeout=timeout)
108
+ logger.debug(
109
+ f"Received result from kernel {kernel_id}, message_id: {message_id}, result: {result}"
110
+ )
111
+
112
+ nb = self._sandbox.filesystem.read(f"/home/user/default.ipynb", timeout=timeout)
113
+ nb_parsed = json.loads(nb)
114
+ cell = {
115
+ "cell_type": "code",
116
+ "metadata": {},
117
+ "source": code,
118
+ }
119
+
120
+ outputs = []
121
+ if result.logs.stdout:
122
+ outputs.append(
123
+ {"output_type": "stream", "name": "stdout", "text": result.logs.stdout}
124
+ )
125
+ if result.logs.stderr:
126
+ outputs.append(
127
+ {"output_type": "stream", "name": "stderr", "text": result.logs.stderr}
128
+ )
129
+
130
+ if result.results:
131
+ outputs = [
132
+ {
133
+ "output_type": (
134
+ "execute_result" if r.is_main_result else "display_data"
135
+ ),
136
+ "data": r.raw,
137
+ "metadata": {},
138
+ }
139
+ for r in result.results
140
+ ]
141
+
142
+ cell["execution_count"] = result.execution_count
143
+ cell["outputs"] = outputs
144
+
145
+ if nb_parsed["cells"] and not nb_parsed["cells"][-1]["source"]:
146
+ nb_parsed["cells"][-1] = cell
147
+ else:
148
+ nb_parsed["cells"].append(cell)
149
+
150
+ self._sandbox.filesystem.write(
151
+ f"/home/user/default.ipynb", json.dumps(nb_parsed), timeout=timeout
152
+ )
153
+ return result
154
+
155
+ @property
156
+ def default_kernel_id(self) -> str:
157
+ """
158
+ Get the default kernel id
159
+
160
+ :return: Default kernel id
161
+ """
162
+ if not self._default_kernel_id:
163
+ logger.debug("Waiting for default kernel id")
164
+ self._default_kernel_id = self._kernel_id_set.result()
165
+
166
+ return self._default_kernel_id
167
+ #
168
+ # def create_kernel(
169
+ # self,
170
+ # name: str,
171
+ # path: str = "/home/user",
172
+ # kernel_name: str = "python3",
173
+ # timeout: Optional[float] = TIMEOUT,
174
+ # ) -> str:
175
+ # """
176
+ # Creates a new kernel, this can be useful if you want to have multiple independent code execution environments.
177
+ #
178
+ # The kernel can be optionally configured to start in a specific working directory and/or
179
+ # with a specific kernel name. If no kernel name is provided, the default kernel will be used.
180
+ # Once the kernel is created, this method establishes a WebSocket connection to the new kernel for
181
+ # real-time communication.
182
+ #
183
+ # :param name: Name of the kernel
184
+ # :param path: Sets the current working directory for the kernel. Defaults to "/home/user".
185
+ # :param kernel_name: Specifies which kernel should be used, useful if you have multiple kernel types.
186
+ # :param timeout: Timeout for the kernel creation request.
187
+ # :return: Kernel id of the created kernel
188
+ # """
189
+ #
190
+ # x = {
191
+ # "metadata": {
192
+ # "signature": "hex-digest",
193
+ # "kernel_info": {"name": kernel_name},
194
+ # "language_info": {
195
+ # "name": kernel_name,
196
+ # "version": "3.10.14",
197
+ # }, # TODO: get version
198
+ # },
199
+ # "nbformat": 4,
200
+ # "nbformat_minor": 0,
201
+ # "cells": [],
202
+ # }
203
+ #
204
+ # self._sandbox.filesystem.write(
205
+ # f"/home/user/default.ipynb", json.dumps(x), timeout=timeout
206
+ # )
207
+ # self._sandbox.process.start("chmod 777 /home/user/default.ipynb")
208
+ #
209
+ # data = {
210
+ # "name": name,
211
+ # "kernel": {"name": kernel_name},
212
+ # "notebook": {"name": "default.ipynb"},
213
+ # "path": path,
214
+ # "type": "notebook",
215
+ # }
216
+ #
217
+ # logger.debug(f"Creating kernel with data: {data}")
218
+ #
219
+ # response = requests.post(
220
+ # f"{self._sandbox.get_protocol()}://{self._sandbox.get_hostname(8888)}/api/sessions",
221
+ # json=data,
222
+ # timeout=timeout,
223
+ # )
224
+ # if not response.ok:
225
+ # raise KernelException(f"Failed to create kernel: {response.text}")
226
+ #
227
+ # response_data = response.json()
228
+ # kernel_id = response_data["kernel"]["id"]
229
+ # session_id = response_data["id"]
230
+ #
231
+ # logger.debug(f"Created kernel {kernel_id}, session {session_id}")
232
+ #
233
+ # threading.Thread(
234
+ # target=self._connect_to_kernel_ws, args=(kernel_id, session_id, timeout)
235
+ # ).start()
236
+ #
237
+ # return kernel_id
238
+
239
+ def restart_kernel(
240
+ self, kernel_id: Optional[str] = None, timeout: Optional[float] = TIMEOUT
241
+ ) -> None:
242
+ """
243
+ Restarts an existing Jupyter kernel. This can be useful to reset the kernel's state or to recover from errors.
244
+
245
+ :param kernel_id: The unique identifier of the kernel to restart. If not provided, the default kernel is restarted.
246
+ :param timeout: The timeout in milliseconds for the kernel restart request.
247
+ """
248
+ kernel_id = kernel_id or self.default_kernel_id
249
+ logger.debug(f"Restarting kernel {kernel_id}")
250
+
251
+ self._connected_kernels[kernel_id].result().close()
252
+ del self._connected_kernels[kernel_id]
253
+ logger.debug(f"Closed websocket connection to kernel {kernel_id}")
254
+
255
+ response = requests.post(
256
+ f"{self._sandbox.get_protocol()}://{self._sandbox.get_hostname(8888)}/api/kernels/{kernel_id}/restart",
257
+ timeout=timeout,
258
+ )
259
+ if not response.ok:
260
+ raise KernelException(f"Failed to restart kernel {kernel_id}")
261
+
262
+ logger.debug(f"Restarted kernel {kernel_id}")
263
+
264
+ threading.Thread(
265
+ target=self._connect_to_kernel_ws, args=(kernel_id, None, timeout)
266
+ ).start()
267
+ #
268
+ # def shutdown_kernel(
269
+ # self, kernel_id: Optional[str] = None, timeout: Optional[float] = TIMEOUT
270
+ # ) -> None:
271
+ # """
272
+ # Shuts down an existing Jupyter kernel. This method is used to gracefully terminate a kernel's process.
273
+ #
274
+ # :param kernel_id: The unique identifier of the kernel to shutdown. If not provided, the default kernel is shutdown.
275
+ # :param timeout: The timeout for the kernel shutdown request.
276
+ # """
277
+ # kernel_id = kernel_id or self.default_kernel_id
278
+ # logger.debug(f"Shutting down kernel {kernel_id}")
279
+ #
280
+ # self._connected_kernels[kernel_id].result().close()
281
+ # del self._connected_kernels[kernel_id]
282
+ # logger.debug(f"Closed websocket connection to kernel {kernel_id}")
283
+ #
284
+ # response = requests.delete(
285
+ # f"{self._sandbox.get_protocol()}://{self._sandbox.get_hostname(8888)}/api/kernels/{kernel_id}",
286
+ # timeout=timeout,
287
+ # )
288
+ # if not response.ok:
289
+ # raise KernelException(f"Failed to shutdown kernel {kernel_id}")
290
+ #
291
+ # logger.debug(f"Shutdown kernel {kernel_id}")
292
+ #
293
+ # def list_kernels(self, timeout: Optional[float] = TIMEOUT) -> List[str]:
294
+ # """
295
+ # Lists all available Jupyter kernels.
296
+ #
297
+ # This method fetches a list of all currently available Jupyter kernels from the server. It can be used
298
+ # to retrieve the IDs of all kernels that are currently running or available for connection.
299
+ #
300
+ # :param timeout: The timeout for the kernel list request.
301
+ # :return: List of kernel ids
302
+ # """
303
+ # response = requests.get(
304
+ # f"{self._sandbox.get_protocol()}://{self._sandbox.get_hostname(8888)}/api/kernels",
305
+ # timeout=timeout,
306
+ # )
307
+ #
308
+ # if not response.ok:
309
+ # raise KernelException(f"Failed to list kernels: {response.text}")
310
+ #
311
+ # return [kernel["id"] for kernel in response.json()]
312
+
313
+ def close(self):
314
+ """
315
+ Close all the websocket connections to the kernels. It doesn't shutdown the kernels.
316
+ """
317
+ logger.debug("Closing all websocket connections")
318
+ for ws in self._connected_kernels.values():
319
+ ws.result().close()
320
+
321
+ def _connect_to_kernel_ws(
322
+ self,
323
+ kernel_id: str,
324
+ session_id: Optional[str],
325
+ timeout: Optional[float] = TIMEOUT,
326
+ ) -> JupyterKernelWebSocket:
327
+ """
328
+ Establishes a WebSocket connection to a specified Jupyter kernel.
329
+
330
+ :param kernel_id: Kernel id
331
+ :param session_id: Session id
332
+ :param timeout: The timeout for the kernel connection request.
333
+
334
+ :return: Websocket connection
335
+ """
336
+ if not session_id:
337
+ session_id = uuid.uuid4()
338
+
339
+ logger.debug(f"Connecting to kernel's ({kernel_id}) websocket")
340
+ future = Future()
341
+ self._connected_kernels[kernel_id] = future
342
+
343
+ session_id = session_id or str(uuid.uuid4())
344
+ ws = JupyterKernelWebSocket(
345
+ url=f"{self._sandbox.get_protocol('ws')}://{self._sandbox.get_hostname(8888)}/api/kernels/{kernel_id}/channels",
346
+ session_id=session_id,
347
+ )
348
+
349
+ ws.connect(timeout=timeout)
350
+ logger.debug(f"Connected to kernel's ({kernel_id}) websocket.")
351
+
352
+ future.set_result(ws)
353
+ return ws
354
+
355
+ def _start_connecting_to_default_kernel(
356
+ self, timeout: Optional[float] = TIMEOUT
357
+ ) -> None:
358
+ """
359
+ Start connecting to the default kernel in a separate thread to avoid blocking the main thread.
360
+ :param timeout: Timeout for the call
361
+ """
362
+ logger.debug("Starting to connect to the default kernel")
363
+
364
+ def setup_default_kernel():
365
+ session_info = self._sandbox.filesystem.read(
366
+ "/root/.jupyter/.session_info", timeout=timeout
367
+ )
368
+
369
+ if session_info is None and not self._sandbox.is_open:
370
+ return
371
+
372
+ data = json.loads(session_info)
373
+ kernel_id = data["kernel"]["id"]
374
+ session_id = data["id"]
375
+ logger.debug(f"Default kernel id: {kernel_id}")
376
+ self._connect_to_kernel_ws(kernel_id, session_id, timeout=timeout)
377
+ self._kernel_id_set.set_result(kernel_id)
378
+
379
+ threading.Thread(target=setup_default_kernel).start()
@@ -25,6 +25,7 @@ class CellExecution:
25
25
  """
26
26
 
27
27
  input_accepted: bool = False
28
+
28
29
  on_stdout: Optional[Callable[[ProcessMessage], Any]] = None
29
30
  on_stderr: Optional[Callable[[ProcessMessage], Any]] = None
30
31
  on_result: Optional[Callable[[Result], Any]] = None
@@ -44,8 +45,9 @@ class CellExecution:
44
45
 
45
46
  class JupyterKernelWebSocket:
46
47
 
47
- def __init__(self, url: str):
48
+ def __init__(self, url: str, session_id: str):
48
49
  self.url = url
50
+ self.session_id = session_id
49
51
  self._cells: Dict[str, CellExecution] = {}
50
52
  self._waiting_for_replies: Dict[str, DeferredFuture] = {}
51
53
  self._queue_in = Queue()
@@ -101,14 +103,13 @@ class JupyterKernelWebSocket:
101
103
 
102
104
  logger.debug("WebSocket started")
103
105
 
104
- @staticmethod
105
- def _get_execute_request(msg_id: str, code: str) -> str:
106
+ def _get_execute_request(self, msg_id: str, code: str) -> str:
106
107
  return json.dumps(
107
108
  {
108
109
  "header": {
109
110
  "msg_id": msg_id,
110
111
  "username": "e2b",
111
- "session": str(uuid.uuid4()),
112
+ "session": self.session_id,
112
113
  "msg_type": "execute_request",
113
114
  "version": "5.3",
114
115
  },
@@ -117,7 +118,7 @@ class JupyterKernelWebSocket:
117
118
  "content": {
118
119
  "code": code,
119
120
  "silent": False,
120
- "store_history": False,
121
+ "store_history": True,
121
122
  "user_expressions": {},
122
123
  "allow_stdin": False,
123
124
  },
@@ -237,6 +238,7 @@ class JupyterKernelWebSocket:
237
238
 
238
239
  elif data["msg_type"] == "execute_input":
239
240
  logger.debug(f"Input accepted for {parent_msg_ig}")
241
+ cell.partial_result.execution_count = data["content"]["execution_count"]
240
242
  cell.input_accepted = True
241
243
  else:
242
244
  logger.warning(f"[UNHANDLED MESSAGE TYPE]: {data['msg_type']}")
@@ -222,6 +222,8 @@ class Execution(BaseModel):
222
222
  "Logs printed to stdout and stderr during execution."
223
223
  error: Optional[Error] = None
224
224
  "Error object if an error occurred, None otherwise."
225
+ execution_count: Optional[int] = None
226
+ "Execution count of the cell."
225
227
 
226
228
  @property
227
229
  def text(self) -> Optional[str]:
@@ -249,7 +251,7 @@ class Execution(BaseModel):
249
251
  serialized = []
250
252
  for result in results:
251
253
  serialized_dict = {key: result[key] for key in result.formats()}
252
- serialized_dict['text'] = result.text
254
+ serialized_dict["text"] = result.text
253
255
  serialized.append(serialized_dict)
254
256
  return serialized
255
257
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "e2b-code-interpreter"
3
- version = "0.0.7"
3
+ version = "0.0.8a0"
4
4
  description = "E2B Code Interpreter - Stateful code execution"
5
5
  authors = ["e2b <hello@e2b.dev>"]
6
6
  license = "Apache-2.0"
@@ -14,7 +14,7 @@ python = "^3.8"
14
14
 
15
15
  pydantic = ">1, <3"
16
16
  websocket-client = "^1.7.0"
17
- e2b = ">=0.17.0"
17
+ e2b = ">=0.17.1"
18
18
 
19
19
  [tool.poetry.group.dev.dependencies]
20
20
  black = "^24.3.0"
@@ -1,290 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import logging
4
- import threading
5
- import requests
6
-
7
- from concurrent.futures import Future
8
- from typing import Any, Callable, List, Optional, Dict
9
-
10
- from e2b import EnvVars, ProcessMessage, Sandbox
11
- from e2b.constants import TIMEOUT
12
-
13
- from e2b_code_interpreter.messaging import JupyterKernelWebSocket
14
- from e2b_code_interpreter.models import KernelException, Execution, Result
15
-
16
- logger = logging.getLogger(__name__)
17
-
18
-
19
- class CodeInterpreter(Sandbox):
20
- """
21
- E2B code interpreter sandbox extension.
22
- """
23
-
24
- template = "code-interpreter-stateful"
25
-
26
- def __init__(
27
- self,
28
- template: Optional[str] = None,
29
- api_key: Optional[str] = None,
30
- cwd: Optional[str] = None,
31
- env_vars: Optional[EnvVars] = None,
32
- timeout: Optional[float] = TIMEOUT,
33
- on_stdout: Optional[Callable[[ProcessMessage], Any]] = None,
34
- on_stderr: Optional[Callable[[ProcessMessage], Any]] = None,
35
- on_exit: Optional[Callable[[int], Any]] = None,
36
- **kwargs,
37
- ):
38
- super().__init__(
39
- template=template or self.template,
40
- api_key=api_key,
41
- cwd=cwd,
42
- env_vars=env_vars,
43
- timeout=timeout,
44
- on_stdout=on_stdout,
45
- on_stderr=on_stderr,
46
- on_exit=on_exit,
47
- **kwargs,
48
- )
49
- self.notebook = JupyterExtension(self, timeout=timeout)
50
- # Close all the websocket connections when the interpreter is closed
51
- self._process_cleanup.append(self.notebook.close)
52
-
53
-
54
- class JupyterExtension:
55
-
56
- def __init__(self, sandbox: CodeInterpreter, timeout: Optional[float] = TIMEOUT):
57
- self._sandbox = sandbox
58
- self._kernel_id_set = Future()
59
- self._start_connecting_to_default_kernel(timeout=timeout)
60
- self._connected_kernels: Dict[str, Future[JupyterKernelWebSocket]] = {}
61
- self._default_kernel_id: Optional[str] = None
62
-
63
- def exec_cell(
64
- self,
65
- code: str,
66
- kernel_id: Optional[str] = None,
67
- on_stdout: Optional[Callable[[ProcessMessage], Any]] = None,
68
- on_stderr: Optional[Callable[[ProcessMessage], Any]] = None,
69
- on_result: Optional[Callable[[Result], Any]] = None,
70
- timeout: Optional[float] = TIMEOUT,
71
- ) -> Execution:
72
- """
73
- Execute code in a notebook cell.
74
-
75
- :param code: Code to execute
76
- :param kernel_id: The ID of the kernel to execute the code on. If not provided, the default kernel is used.
77
- :param on_stdout: A callback function to handle standard output messages from the code execution.
78
- :param on_stderr: A callback function to handle standard error messages from the code execution.
79
- :param on_result: A callback function to handle the result and display calls of the code execution.
80
- :param timeout: Timeout for the call
81
-
82
- :return: Result of the execution
83
- """
84
- kernel_id = kernel_id or self.default_kernel_id
85
- ws_future = self._connected_kernels.get(kernel_id)
86
-
87
- logger.debug(f"Executing code in kernel {kernel_id}")
88
-
89
- if ws_future:
90
- logger.debug(f"Using existing websocket connection to kernel {kernel_id}")
91
- ws = ws_future.result(timeout=timeout)
92
- else:
93
- logger.debug(f"Creating new websocket connection to kernel {kernel_id}")
94
- ws = self._connect_to_kernel_ws(kernel_id, timeout=timeout)
95
-
96
- session_id = ws.send_execution_message(code, on_stdout, on_stderr, on_result)
97
- logger.debug(
98
- f"Sent execution message to kernel {kernel_id}, session_id: {session_id}"
99
- )
100
-
101
- result = ws.get_result(session_id, timeout=timeout)
102
- logger.debug(
103
- f"Received result from kernel {kernel_id}, session_id: {session_id}, result: {result}"
104
- )
105
-
106
- return result
107
-
108
- @property
109
- def default_kernel_id(self) -> str:
110
- """
111
- Get the default kernel id
112
-
113
- :return: Default kernel id
114
- """
115
- if not self._default_kernel_id:
116
- logger.debug("Waiting for default kernel id")
117
- self._default_kernel_id = self._kernel_id_set.result()
118
-
119
- return self._default_kernel_id
120
-
121
- def create_kernel(
122
- self,
123
- cwd: str = "/home/user",
124
- kernel_name: Optional[str] = None,
125
- timeout: Optional[float] = TIMEOUT,
126
- ) -> str:
127
- """
128
- Creates a new kernel, this can be useful if you want to have multiple independent code execution environments.
129
-
130
- The kernel can be optionally configured to start in a specific working directory and/or
131
- with a specific kernel name. If no kernel name is provided, the default kernel will be used.
132
- Once the kernel is created, this method establishes a WebSocket connection to the new kernel for
133
- real-time communication.
134
-
135
- :param cwd: Sets the current working directory for the kernel. Defaults to "/home/user".
136
- :param kernel_name:
137
- Specifies which kernel should be used, useful if you have multiple kernel types.
138
- If not provided, the default kernel will be used.
139
- :param timeout: Timeout for the kernel creation request.
140
- :return: Kernel id of the created kernel
141
- """
142
- data = {"path": cwd}
143
- if kernel_name:
144
- data["kernel_name"] = kernel_name
145
- logger.debug(f"Creating kernel with data: {data}")
146
-
147
- response = requests.post(
148
- f"{self._sandbox.get_protocol()}://{self._sandbox.get_hostname(8888)}/api/kernels",
149
- json=data,
150
- timeout=timeout,
151
- )
152
- if not response.ok:
153
- raise KernelException(f"Failed to create kernel: {response.text}")
154
-
155
- kernel_id = response.json()["id"]
156
- logger.debug(f"Created kernel {kernel_id}")
157
-
158
- threading.Thread(
159
- target=self._connect_to_kernel_ws, args=(kernel_id, timeout)
160
- ).start()
161
- return kernel_id
162
-
163
- def restart_kernel(
164
- self, kernel_id: Optional[str] = None, timeout: Optional[float] = TIMEOUT
165
- ) -> None:
166
- """
167
- Restarts an existing Jupyter kernel. This can be useful to reset the kernel's state or to recover from errors.
168
-
169
- :param kernel_id: The unique identifier of the kernel to restart. If not provided, the default kernel is restarted.
170
- :param timeout: The timeout in milliseconds for the kernel restart request.
171
- """
172
- kernel_id = kernel_id or self.default_kernel_id
173
- logger.debug(f"Restarting kernel {kernel_id}")
174
-
175
- self._connected_kernels[kernel_id].result().close()
176
- del self._connected_kernels[kernel_id]
177
- logger.debug(f"Closed websocket connection to kernel {kernel_id}")
178
-
179
- response = requests.post(
180
- f"{self._sandbox.get_protocol()}://{self._sandbox.get_hostname(8888)}/api/kernels/{kernel_id}/restart",
181
- timeout=timeout,
182
- )
183
- if not response.ok:
184
- raise KernelException(f"Failed to restart kernel {kernel_id}")
185
-
186
- logger.debug(f"Restarted kernel {kernel_id}")
187
-
188
- threading.Thread(
189
- target=self._connect_to_kernel_ws, args=(kernel_id, timeout)
190
- ).start()
191
-
192
- def shutdown_kernel(
193
- self, kernel_id: Optional[str] = None, timeout: Optional[float] = TIMEOUT
194
- ) -> None:
195
- """
196
- Shuts down an existing Jupyter kernel. This method is used to gracefully terminate a kernel's process.
197
-
198
- :param kernel_id: The unique identifier of the kernel to shutdown. If not provided, the default kernel is shutdown.
199
- :param timeout: The timeout for the kernel shutdown request.
200
- """
201
- kernel_id = kernel_id or self.default_kernel_id
202
- logger.debug(f"Shutting down kernel {kernel_id}")
203
-
204
- self._connected_kernels[kernel_id].result().close()
205
- del self._connected_kernels[kernel_id]
206
- logger.debug(f"Closed websocket connection to kernel {kernel_id}")
207
-
208
- response = requests.delete(
209
- f"{self._sandbox.get_protocol()}://{self._sandbox.get_hostname(8888)}/api/kernels/{kernel_id}",
210
- timeout=timeout,
211
- )
212
- if not response.ok:
213
- raise KernelException(f"Failed to shutdown kernel {kernel_id}")
214
-
215
- logger.debug(f"Shutdown kernel {kernel_id}")
216
-
217
- def list_kernels(self, timeout: Optional[float] = TIMEOUT) -> List[str]:
218
- """
219
- Lists all available Jupyter kernels.
220
-
221
- This method fetches a list of all currently available Jupyter kernels from the server. It can be used
222
- to retrieve the IDs of all kernels that are currently running or available for connection.
223
-
224
- :param timeout: The timeout for the kernel list request.
225
- :return: List of kernel ids
226
- """
227
- response = requests.get(
228
- f"{self._sandbox.get_protocol()}://{self._sandbox.get_hostname(8888)}/api/kernels",
229
- timeout=timeout,
230
- )
231
-
232
- if not response.ok:
233
- raise KernelException(f"Failed to list kernels: {response.text}")
234
-
235
- return [kernel["id"] for kernel in response.json()]
236
-
237
- def close(self):
238
- """
239
- Close all the websocket connections to the kernels. It doesn't shutdown the kernels.
240
- """
241
- logger.debug("Closing all websocket connections")
242
- for ws in self._connected_kernels.values():
243
- ws.result().close()
244
-
245
- def _connect_to_kernel_ws(
246
- self, kernel_id: str, timeout: Optional[float] = TIMEOUT
247
- ) -> JupyterKernelWebSocket:
248
- """
249
- Establishes a WebSocket connection to a specified Jupyter kernel.
250
-
251
- :param kernel_id: Kernel id
252
- :param timeout: The timeout for the kernel connection request.
253
-
254
- :return: Websocket connection
255
- """
256
- logger.debug(f"Connecting to kernel's ({kernel_id}) websocket")
257
- future = Future()
258
- self._connected_kernels[kernel_id] = future
259
-
260
- ws = JupyterKernelWebSocket(
261
- url=f"{self._sandbox.get_protocol('ws')}://{self._sandbox.get_hostname(8888)}/api/kernels/{kernel_id}/channels",
262
- )
263
- ws.connect(timeout=timeout)
264
- logger.debug(f"Connected to kernel's ({kernel_id}) websocket.")
265
-
266
- future.set_result(ws)
267
- return ws
268
-
269
- def _start_connecting_to_default_kernel(
270
- self, timeout: Optional[float] = TIMEOUT
271
- ) -> None:
272
- """
273
- Start connecting to the default kernel in a separate thread to avoid blocking the main thread.
274
- :param timeout: Timeout for the call
275
- """
276
- logger.debug("Starting to connect to the default kernel")
277
-
278
- def setup_default_kernel():
279
- kernel_id = self._sandbox.filesystem.read(
280
- "/root/.jupyter/kernel_id", timeout=timeout
281
- )
282
- if kernel_id is None and not self._sandbox.is_open:
283
- return
284
-
285
- kernel_id = kernel_id.strip()
286
- logger.debug(f"Default kernel id: {kernel_id}")
287
- self._connect_to_kernel_ws(kernel_id, timeout=timeout)
288
- self._kernel_id_set.set_result(kernel_id)
289
-
290
- threading.Thread(target=setup_default_kernel).start()