e2b-code-interpreter 0.0.10a0__tar.gz → 0.0.11a0__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.

Potentially problematic release.


This version of e2b-code-interpreter might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: e2b-code-interpreter
3
- Version: 0.0.10a0
3
+ Version: 0.0.11a0
4
4
  Summary: E2B Code Interpreter - Stateful code execution
5
5
  Home-page: https://e2b.dev/
6
6
  License: Apache-2.0
@@ -14,9 +14,11 @@ 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.1)
18
- Requires-Dist: pydantic (>1,<3)
17
+ Requires-Dist: e2b (==0.17.2a20)
18
+ Requires-Dist: pydantic
19
+ Requires-Dist: requests (>=2.32.3,<3.0.0)
19
20
  Requires-Dist: websocket-client (>=1.7.0,<2.0.0)
21
+ Requires-Dist: websockets (>=12.0,<13.0)
20
22
  Project-URL: Bug Tracker, https://github.com/e2b-dev/code-interpreter/issues
21
23
  Project-URL: Repository, https://github.com/e2b-dev/e2b-code-interpreter/tree/python
22
24
  Description-Content-Type: text/markdown
@@ -2,3 +2,4 @@ from e2b import *
2
2
 
3
3
  from .main import CodeInterpreter, JupyterExtension
4
4
  from .models import Execution, Error, Result, KernelException, MIMEType, Logs
5
+ from .messaging import CellMessage
@@ -0,0 +1,46 @@
1
+ import asyncio
2
+
3
+ from asyncio import AbstractEventLoop
4
+ from concurrent.futures import Future, CancelledError
5
+ from typing import Any, Awaitable, Callable, Generic, List, Optional, TypeVar
6
+
7
+ T = TypeVar("T")
8
+
9
+
10
+ # Check if using event is not better for most use cases
11
+ class DeferredFuture(Generic[T]):
12
+ def __init__(self, cleanup_list: Optional[List[Callable[[], Any]]] = None):
13
+ self._future = Future()
14
+ if cleanup_list is not None:
15
+ cleanup_list.append(self.cancel)
16
+
17
+ def __call__(self, result: T):
18
+ if not self._future.done():
19
+ self._future.set_result(result)
20
+
21
+ def result(self, timeout: Optional[float] = None) -> T:
22
+ try:
23
+ return self._future.result(timeout=timeout)
24
+ # The thread was interrupted while waiting, it's okay to release the thread
25
+ except CancelledError:
26
+ pass
27
+
28
+ def done(self) -> bool:
29
+ return self._future.done()
30
+
31
+ def cancel(self):
32
+ if not self._future.done():
33
+ self._future.cancel()
34
+
35
+ def reject(self, reason: Exception):
36
+ if not self._future.done():
37
+ self._future.set_exception(reason)
38
+
39
+
40
+ def run_async_func_in_loop(loop: AbstractEventLoop, coro: Awaitable):
41
+ asyncio.set_event_loop(loop)
42
+
43
+ try:
44
+ return loop.run_until_complete(coro)
45
+ finally:
46
+ loop.close()
@@ -3,16 +3,14 @@ from __future__ import annotations
3
3
  import logging
4
4
  import threading
5
5
  import uuid
6
-
7
6
  import requests
8
7
 
9
8
  from concurrent.futures import Future
10
9
  from typing import Any, Callable, List, Optional, Dict
10
+ from e2b import Sandbox
11
11
 
12
- from e2b import EnvVars, ProcessMessage, Sandbox
13
- from e2b.constants import TIMEOUT
14
-
15
- from e2b_code_interpreter.messaging import JupyterKernelWebSocket
12
+ from e2b_code_interpreter.constants import TIMEOUT
13
+ from e2b_code_interpreter.messaging import CellMessage, JupyterKernelWebSocket
16
14
  from e2b_code_interpreter.models import KernelException, Execution, Result
17
15
 
18
16
  logger = logging.getLogger(__name__)
@@ -23,42 +21,56 @@ class CodeInterpreter(Sandbox):
23
21
  E2B code interpreter sandbox extension.
24
22
  """
25
23
 
26
- template = "code-interpreter-multikernel"
24
+ default_template = "code-interpreter-stateful"
27
25
 
28
26
  def __init__(
29
27
  self,
30
28
  template: Optional[str] = None,
29
+ timeout: Optional[int] = None,
30
+ metadata: Optional[Dict[str, str]] = None,
31
31
  api_key: Optional[str] = None,
32
- cwd: Optional[str] = None,
33
- env_vars: Optional[EnvVars] = None,
34
- timeout: Optional[float] = TIMEOUT,
35
- on_stdout: Optional[Callable[[ProcessMessage], Any]] = None,
36
- on_stderr: Optional[Callable[[ProcessMessage], Any]] = None,
37
- on_exit: Optional[Callable[[int], Any]] = None,
38
- **kwargs,
32
+ domain: Optional[str] = None,
33
+ debug: Optional[bool] = None,
34
+ sandbox_id: Optional[str] = None,
35
+ request_timeout: float = TIMEOUT,
39
36
  ):
40
37
  super().__init__(
41
- template=template or self.template,
42
- api_key=api_key,
43
- cwd=cwd,
44
- env_vars=env_vars,
38
+ template=template,
45
39
  timeout=timeout,
46
- on_stdout=on_stdout,
47
- on_stderr=on_stderr,
48
- on_exit=on_exit,
49
- **kwargs,
40
+ metadata=metadata,
41
+ api_key=api_key,
42
+ domain=domain,
43
+ debug=debug,
44
+ sandbox_id=sandbox_id,
45
+ request_timeout=request_timeout,
50
46
  )
51
- self.notebook = JupyterExtension(self, timeout=timeout)
52
- # Close all the websocket connections when the interpreter is closed
53
- self._process_cleanup.append(self.notebook.close)
47
+ self.notebook = JupyterExtension(self, request_timeout=request_timeout)
54
48
 
49
+ def close(self):
50
+ # Close all the websocket connections to the kernels
51
+ self.notebook.close()
52
+
53
+ def get_protocol(self, base_protocol: str = "http") -> str:
54
+ """
55
+ The function decides whether to use the secure or insecure protocol.
56
+
57
+ :param base_protocol: Specify the specific protocol you want to use. Do not include the `s` in `https` or `wss`.
58
+ :param secure: Specify whether you want to use the secure protocol or not.
59
+
60
+ :return: Protocol for the connection to the sandbox
61
+ """
62
+ return base_protocol if self._connection_config.debug else f"{base_protocol}s"
63
+
64
+ def __exit__(self, exc_type, exc_value, traceback):
65
+ self.close()
66
+ super().__exit__(exc_type, exc_value, traceback)
55
67
 
56
- class JupyterExtension:
57
68
 
58
- def __init__(self, sandbox: CodeInterpreter, timeout: Optional[float] = TIMEOUT):
69
+ class JupyterExtension:
70
+ def __init__(self, sandbox: CodeInterpreter, request_timeout: float = TIMEOUT):
59
71
  self._sandbox = sandbox
60
72
  self._kernel_id_set = Future()
61
- self._start_connecting_to_default_kernel(timeout=timeout)
73
+ self._start_connecting_to_default_kernel(request_timeout=request_timeout)
62
74
  self._connected_kernels: Dict[str, Future[JupyterKernelWebSocket]] = {}
63
75
  self._default_kernel_id: Optional[str] = None
64
76
 
@@ -66,10 +78,11 @@ class JupyterExtension:
66
78
  self,
67
79
  code: str,
68
80
  kernel_id: Optional[str] = None,
69
- on_stdout: Optional[Callable[[ProcessMessage], Any]] = None,
70
- on_stderr: Optional[Callable[[ProcessMessage], Any]] = None,
81
+ on_stdout: Optional[Callable[[CellMessage], Any]] = None,
82
+ on_stderr: Optional[Callable[[CellMessage], Any]] = None,
71
83
  on_result: Optional[Callable[[Result], Any]] = None,
72
- timeout: Optional[float] = TIMEOUT,
84
+ timeout: float = TIMEOUT,
85
+ request_timeout: float = TIMEOUT,
73
86
  ) -> Execution:
74
87
  """
75
88
  Execute code in a notebook cell.
@@ -93,7 +106,11 @@ class JupyterExtension:
93
106
  ws = ws_future.result(timeout=timeout)
94
107
  else:
95
108
  logger.debug(f"Creating new websocket connection to kernel {kernel_id}")
96
- ws = self._connect_to_kernel_ws(kernel_id, None, timeout=timeout)
109
+ ws = self._connect_to_kernel_ws(
110
+ kernel_id,
111
+ None,
112
+ request_timeout=request_timeout,
113
+ )
97
114
 
98
115
  message_id = ws.send_execution_message(code, on_stdout, on_stderr, on_result)
99
116
  logger.debug(
@@ -124,7 +141,7 @@ class JupyterExtension:
124
141
  self,
125
142
  cwd: str = "/home/user",
126
143
  kernel_name: Optional[str] = None,
127
- timeout: Optional[float] = TIMEOUT,
144
+ request_timeout: Optional[float] = TIMEOUT,
128
145
  ) -> str:
129
146
  """
130
147
  Creates a new kernel, this can be useful if you want to have multiple independent code execution environments.
@@ -138,18 +155,23 @@ class JupyterExtension:
138
155
  :param kernel_name:
139
156
  Specifies which kernel should be used, useful if you have multiple kernel types.
140
157
  If not provided, the default kernel will be used.
141
- :param timeout: Timeout for the kernel creation request.
158
+ :param request_timeout: Timeout for the kernel creation request.
142
159
  :return: Kernel id of the created kernel
143
160
  """
144
161
  kernel_name = kernel_name or "python3"
145
162
 
146
- data = {"path": str(uuid.uuid4()), "kernel": {"name": kernel_name}, "type": "notebook", "name": str(uuid.uuid4())}
163
+ data = {
164
+ "path": str(uuid.uuid4()),
165
+ "kernel": {"name": kernel_name},
166
+ "type": "notebook",
167
+ "name": str(uuid.uuid4()),
168
+ }
147
169
  logger.debug(f"Creating kernel with data: {data}")
148
170
 
149
171
  response = requests.post(
150
- f"{self._sandbox.get_protocol()}://{self._sandbox.get_hostname(8888)}/api/sessions",
172
+ f"{self._sandbox.get_protocol()}://{self._sandbox.get_host(8888)}/api/sessions",
151
173
  json=data,
152
- timeout=timeout,
174
+ timeout=request_timeout,
153
175
  )
154
176
  if not response.ok:
155
177
  raise KernelException(f"Failed to create kernel: {response.text}")
@@ -159,9 +181,9 @@ class JupyterExtension:
159
181
  kernel_id = session_data["kernel"]["id"]
160
182
 
161
183
  response = requests.patch(
162
- f"{self._sandbox.get_protocol()}://{self._sandbox.get_hostname(8888)}/api/sessions/{session_id}",
184
+ f"{self._sandbox.get_protocol()}://{self._sandbox.get_host(8888)}/api/sessions/{session_id}",
163
185
  json={"path": cwd},
164
- timeout=timeout,
186
+ timeout=request_timeout,
165
187
  )
166
188
  if not response.ok:
167
189
  raise KernelException(f"Failed to create kernel: {response.text}")
@@ -169,12 +191,15 @@ class JupyterExtension:
169
191
  logger.debug(f"Created kernel {kernel_id}")
170
192
 
171
193
  threading.Thread(
172
- target=self._connect_to_kernel_ws, args=(kernel_id, session_id, timeout)
194
+ target=self._connect_to_kernel_ws,
195
+ args=(kernel_id, session_id, request_timeout),
173
196
  ).start()
174
197
  return kernel_id
175
198
 
176
199
  def restart_kernel(
177
- self, kernel_id: Optional[str] = None, timeout: Optional[float] = TIMEOUT
200
+ self,
201
+ kernel_id: Optional[str] = None,
202
+ request_timeout: Optional[float] = TIMEOUT,
178
203
  ) -> None:
179
204
  """
180
205
  Restarts an existing Jupyter kernel. This can be useful to reset the kernel's state or to recover from errors.
@@ -190,8 +215,8 @@ class JupyterExtension:
190
215
  logger.debug(f"Closed websocket connection to kernel {kernel_id}")
191
216
 
192
217
  response = requests.post(
193
- f"{self._sandbox.get_protocol()}://{self._sandbox.get_hostname(8888)}/api/kernels/{kernel_id}/restart",
194
- timeout=timeout,
218
+ f"{self._sandbox.get_protocol()}://{self._sandbox.get_host(8888)}/api/kernels/{kernel_id}/restart",
219
+ timeout=request_timeout,
195
220
  )
196
221
  if not response.ok:
197
222
  raise KernelException(f"Failed to restart kernel {kernel_id}")
@@ -199,11 +224,13 @@ class JupyterExtension:
199
224
  logger.debug(f"Restarted kernel {kernel_id}")
200
225
 
201
226
  threading.Thread(
202
- target=self._connect_to_kernel_ws, args=(kernel_id, None, timeout)
227
+ target=self._connect_to_kernel_ws, args=(kernel_id, None, request_timeout)
203
228
  ).start()
204
229
 
205
230
  def shutdown_kernel(
206
- self, kernel_id: Optional[str] = None, timeout: Optional[float] = TIMEOUT
231
+ self,
232
+ kernel_id: Optional[str] = None,
233
+ request_timeout: Optional[float] = TIMEOUT,
207
234
  ) -> None:
208
235
  """
209
236
  Shuts down an existing Jupyter kernel. This method is used to gracefully terminate a kernel's process.
@@ -219,15 +246,15 @@ class JupyterExtension:
219
246
  logger.debug(f"Closed websocket connection to kernel {kernel_id}")
220
247
 
221
248
  response = requests.delete(
222
- f"{self._sandbox.get_protocol()}://{self._sandbox.get_hostname(8888)}/api/kernels/{kernel_id}",
223
- timeout=timeout,
249
+ f"{self._sandbox.get_protocol()}://{self._sandbox.get_host(8888)}/api/kernels/{kernel_id}",
250
+ timeout=request_timeout,
224
251
  )
225
252
  if not response.ok:
226
253
  raise KernelException(f"Failed to shutdown kernel {kernel_id}")
227
254
 
228
255
  logger.debug(f"Shutdown kernel {kernel_id}")
229
256
 
230
- def list_kernels(self, timeout: Optional[float] = TIMEOUT) -> List[str]:
257
+ def list_kernels(self, request_timeout: Optional[float] = TIMEOUT) -> List[str]:
231
258
  """
232
259
  Lists all available Jupyter kernels.
233
260
 
@@ -238,8 +265,8 @@ class JupyterExtension:
238
265
  :return: List of kernel ids
239
266
  """
240
267
  response = requests.get(
241
- f"{self._sandbox.get_protocol()}://{self._sandbox.get_hostname(8888)}/api/kernels",
242
- timeout=timeout,
268
+ f"{self._sandbox.get_protocol()}://{self._sandbox.get_host(8888)}/api/kernels",
269
+ timeout=request_timeout,
243
270
  )
244
271
 
245
272
  if not response.ok:
@@ -256,7 +283,10 @@ class JupyterExtension:
256
283
  ws.result().close()
257
284
 
258
285
  def _connect_to_kernel_ws(
259
- self, kernel_id: str, session_id: Optional[str], timeout: Optional[float] = TIMEOUT
286
+ self,
287
+ kernel_id: str,
288
+ session_id: Optional[str],
289
+ request_timeout: float = TIMEOUT,
260
290
  ) -> JupyterKernelWebSocket:
261
291
  """
262
292
  Establishes a WebSocket connection to a specified Jupyter kernel.
@@ -272,18 +302,18 @@ class JupyterExtension:
272
302
 
273
303
  session_id = session_id or str(uuid.uuid4())
274
304
  ws = JupyterKernelWebSocket(
275
- url=f"{self._sandbox.get_protocol('ws')}://{self._sandbox.get_hostname(8888)}/api/kernels/{kernel_id}/channels",
276
- session_id=session_id
305
+ url=f"{self._sandbox.get_protocol('ws')}://{self._sandbox.get_host(8888)}/api/kernels/{kernel_id}/channels",
306
+ session_id=session_id,
277
307
  )
278
308
 
279
- ws.connect(timeout=timeout)
309
+ ws.connect(timeout=request_timeout)
280
310
  logger.debug(f"Connected to kernel's ({kernel_id}) websocket.")
281
311
 
282
312
  future.set_result(ws)
283
313
  return ws
284
314
 
285
315
  def _start_connecting_to_default_kernel(
286
- self, timeout: Optional[float] = TIMEOUT
316
+ self, request_timeout: float = TIMEOUT
287
317
  ) -> None:
288
318
  """
289
319
  Start connecting to the default kernel in a separate thread to avoid blocking the main thread.
@@ -292,17 +322,19 @@ class JupyterExtension:
292
322
  logger.debug("Starting to connect to the default kernel")
293
323
 
294
324
  def setup_default_kernel():
295
- kernel_id = self._sandbox.filesystem.read(
296
- "/root/.jupyter/kernel_id", timeout=timeout
325
+ kernel_id = self._sandbox.files.read(
326
+ "/root/.jupyter/kernel_id", request_timeout=request_timeout
297
327
  )
298
328
 
299
- if kernel_id is None and not self._sandbox.is_open:
329
+ if kernel_id is None and not self._sandbox.is_running(
330
+ request_timeout=request_timeout
331
+ ):
300
332
  return
301
333
 
302
334
  kernel_id = kernel_id.strip()
303
335
 
304
336
  logger.debug(f"Default kernel id: {kernel_id}")
305
- self._connect_to_kernel_ws(kernel_id, None, timeout=timeout)
337
+ self._connect_to_kernel_ws(kernel_id, None, request_timeout=request_timeout)
306
338
  self._kernel_id_set.set_result(kernel_id)
307
339
 
308
340
  threading.Thread(target=setup_default_kernel).start()
@@ -3,21 +3,38 @@ import logging
3
3
  import threading
4
4
  import time
5
5
  import uuid
6
+
6
7
  from concurrent.futures import Future
7
8
  from queue import Queue
8
9
  from typing import Callable, Dict, Any, Optional
10
+ from pydantic import BaseModel
11
+ from e2b import TimeoutException
9
12
 
10
- from e2b import ProcessMessage
11
- from e2b.constants import TIMEOUT
12
- from e2b.sandbox import TimeoutException
13
- from e2b.sandbox.websocket_client import WebSocket
14
- from e2b.utils.future import DeferredFuture
15
-
13
+ from e2b_code_interpreter.websocket_client import WebSocket
14
+ from e2b_code_interpreter.future import DeferredFuture
16
15
  from e2b_code_interpreter.models import Execution, Result, Error
16
+ from e2b_code_interpreter.constants import TIMEOUT
17
+
17
18
 
18
19
  logger = logging.getLogger(__name__)
19
20
 
20
21
 
22
+ class CellMessage(BaseModel):
23
+ """
24
+ A message from a process.
25
+ """
26
+
27
+ line: str
28
+ error: bool = False
29
+ timestamp: int
30
+ """
31
+ Unix epoch in nanoseconds
32
+ """
33
+
34
+ def __str__(self):
35
+ return self.line
36
+
37
+
21
38
  class CellExecution:
22
39
  """
23
40
  Represents the execution of a cell in the Jupyter kernel.
@@ -26,14 +43,14 @@ class CellExecution:
26
43
 
27
44
  input_accepted: bool = False
28
45
 
29
- on_stdout: Optional[Callable[[ProcessMessage], Any]] = None
30
- on_stderr: Optional[Callable[[ProcessMessage], Any]] = None
46
+ on_stdout: Optional[Callable[[CellMessage], Any]] = None
47
+ on_stderr: Optional[Callable[[CellMessage], Any]] = None
31
48
  on_result: Optional[Callable[[Result], Any]] = None
32
49
 
33
50
  def __init__(
34
51
  self,
35
- on_stdout: Optional[Callable[[ProcessMessage], Any]] = None,
36
- on_stderr: Optional[Callable[[ProcessMessage], Any]] = None,
52
+ on_stdout: Optional[Callable[[CellMessage], Any]] = None,
53
+ on_stderr: Optional[Callable[[CellMessage], Any]] = None,
37
54
  on_result: Optional[Callable[[Result], Any]] = None,
38
55
  ):
39
56
  self.partial_result = Execution()
@@ -44,7 +61,6 @@ class CellExecution:
44
61
 
45
62
 
46
63
  class JupyterKernelWebSocket:
47
-
48
64
  def __init__(self, url: str, session_id: str):
49
65
  self.url = url
50
66
  self.session_id = session_id
@@ -78,7 +94,7 @@ class JupyterKernelWebSocket:
78
94
  queue_in=self._queue_in,
79
95
  queue_out=self._queue_out,
80
96
  started=started,
81
- stopped=self._stopped
97
+ stopped=self._stopped,
82
98
  ).run,
83
99
  daemon=True,
84
100
  name="e2b-code-interpreter-websocket",
@@ -128,8 +144,8 @@ class JupyterKernelWebSocket:
128
144
  def send_execution_message(
129
145
  self,
130
146
  code: str,
131
- on_stdout: Optional[Callable[[ProcessMessage], Any]] = None,
132
- on_stderr: Optional[Callable[[ProcessMessage], Any]] = None,
147
+ on_stdout: Optional[Callable[[CellMessage], Any]] = None,
148
+ on_stderr: Optional[Callable[[CellMessage], Any]] = None,
133
149
  on_result: Optional[Callable[[Result], Any]] = None,
134
150
  ) -> str:
135
151
  message_id = str(uuid.uuid4())
@@ -187,7 +203,7 @@ class JupyterKernelWebSocket:
187
203
  execution.logs.stdout.append(data["content"]["text"])
188
204
  if cell.on_stdout:
189
205
  cell.on_stdout(
190
- ProcessMessage(
206
+ CellMessage(
191
207
  line=data["content"]["text"],
192
208
  timestamp=time.time_ns(),
193
209
  )
@@ -197,7 +213,7 @@ class JupyterKernelWebSocket:
197
213
  execution.logs.stderr.append(data["content"]["text"])
198
214
  if cell.on_stderr:
199
215
  cell.on_stderr(
200
- ProcessMessage(
216
+ CellMessage(
201
217
  line=data["content"]["text"],
202
218
  error=True,
203
219
  timestamp=time.time_ns(),
@@ -1,6 +1,6 @@
1
1
  import copy
2
2
  from typing import List, Optional, Iterable, Dict
3
- from pydantic import BaseModel, field_serializer
3
+ from pydantic import BaseModel
4
4
 
5
5
 
6
6
  class Error(BaseModel):
@@ -211,6 +211,19 @@ class Logs(BaseModel):
211
211
  "List of strings printed to stderr by prints, subprocesses, etc."
212
212
 
213
213
 
214
+ def serialize_results(results: List[Result]) -> List[Dict[str, str]]:
215
+ """
216
+ Serializes the results to JSON.
217
+ This method is used by the Pydantic JSON encoder.
218
+ """
219
+ serialized = []
220
+ for result in results:
221
+ serialized_dict = {key: result[key] for key in result.formats()}
222
+ serialized_dict["text"] = result.text
223
+ serialized.append(serialized_dict)
224
+ return serialized
225
+
226
+
214
227
  class Execution(BaseModel):
215
228
  """
216
229
  Represents the result of a cell execution.
@@ -218,6 +231,9 @@ class Execution(BaseModel):
218
231
 
219
232
  class Config:
220
233
  arbitrary_types_allowed = True
234
+ json_encoders = {
235
+ List[Result]: serialize_results,
236
+ }
221
237
 
222
238
  results: List[Result] = []
223
239
  "List of the result of the cell (interactively interpreted last line), display calls (e.g. matplotlib plots)."
@@ -245,19 +261,6 @@ class Execution(BaseModel):
245
261
  """
246
262
  return self.model_dump_json(exclude_none=True)
247
263
 
248
- @field_serializer("results", when_used="json")
249
- def serialize_results(results: List[Result]) -> List[Dict[str, str]]:
250
- """
251
- Serializes the results to JSON.
252
- This method is used by the Pydantic JSON encoder.
253
- """
254
- serialized = []
255
- for result in results:
256
- serialized_dict = {key: result[key] for key in result.formats()}
257
- serialized_dict["text"] = result.text
258
- serialized.append(serialized_dict)
259
- return serialized
260
-
261
264
 
262
265
  class KernelException(Exception):
263
266
  """
@@ -0,0 +1,163 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import random
6
+
7
+ from queue import Queue
8
+ from threading import Event
9
+ from typing import Any, Callable, List, Optional, AsyncIterator
10
+
11
+ from websockets.legacy.client import WebSocketClientProtocol, Connect
12
+ from websockets.exceptions import ConnectionClosed
13
+ from websockets.typing import Data
14
+
15
+ from e2b import SandboxException
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class WebSocket:
21
+ def __init__(
22
+ self,
23
+ url: str,
24
+ started: Event,
25
+ stopped: Event,
26
+ queue_in: Queue[dict],
27
+ queue_out: Queue[Data],
28
+ ):
29
+ self._ws: Optional[WebSocketClientProtocol] = None
30
+ self.url = url
31
+ self.started = started
32
+ self.stopped = stopped
33
+ self._process_cleanup: List[Callable[[], Any]] = []
34
+ self._queue_in = queue_in
35
+ self._queue_out = queue_out
36
+
37
+ def run(self):
38
+ loop = asyncio.new_event_loop()
39
+ asyncio.set_event_loop(loop)
40
+
41
+ loop.run_until_complete(self.async_run())
42
+
43
+ async def async_run(self):
44
+ await self._connect()
45
+ await self.close()
46
+
47
+ async def _send_message(self):
48
+ logger.debug("WebSocket starting to send messages")
49
+ while True:
50
+ if self._queue_in.empty():
51
+ await asyncio.sleep(0)
52
+ continue
53
+ message = self._queue_in.get()
54
+ logger.debug(f"WebSocket message to send: {message}")
55
+ if self._ws:
56
+ await self._ws.send(message)
57
+ logger.debug(f"WebSocket message sent: {message}")
58
+ self._queue_in.task_done()
59
+ else:
60
+ logger.error("No WebSocket connection")
61
+
62
+ async def _receive_message(self):
63
+ try:
64
+ if not self._ws:
65
+ logger.error("No WebSocket connection")
66
+ return
67
+ async for message in self._ws:
68
+ logger.debug(f"WebSocket received message: {message}".strip())
69
+ self._queue_out.put(message)
70
+ except Exception as e:
71
+ logger.error(f"WebSocket received error while receiving messages: {e}")
72
+
73
+ async def _connect(self):
74
+ logger.debug(f"WebSocket connecting to {self.url}")
75
+
76
+ ws_logger = logger.getChild("websockets.client")
77
+ ws_logger.setLevel(logging.ERROR)
78
+
79
+ websocket_connector = E2BConnect(
80
+ self.url,
81
+ max_size=None,
82
+ max_queue=None,
83
+ logger=ws_logger,
84
+ )
85
+
86
+ websocket_connector.BACKOFF_MIN = 1
87
+ websocket_connector.BACKOFF_FACTOR = 1
88
+ websocket_connector.BACKOFF_INITIAL = 0.2 # type: ignore
89
+
90
+ async for websocket in websocket_connector:
91
+ try:
92
+ self._ws = websocket
93
+ self.started.set()
94
+ logger.info(f"WebSocket connected to {self.url}")
95
+
96
+ send_task = asyncio.create_task(
97
+ self._send_message(), name="send_message"
98
+ )
99
+ self._process_cleanup.append(send_task.cancel)
100
+
101
+ receive_task = asyncio.create_task(
102
+ self._receive_message(), name="receive_message"
103
+ )
104
+ self._process_cleanup.append(receive_task.cancel)
105
+
106
+ while not self.stopped.is_set():
107
+ await asyncio.sleep(0)
108
+
109
+ logger.info("WebSocket stopped")
110
+ break
111
+ except ConnectionClosed:
112
+ logger.warning("WebSocket disconnected, it will try to reconnect")
113
+ if self.stopped.is_set():
114
+ break
115
+
116
+ async def close(self):
117
+ for cancel in self._process_cleanup:
118
+ cancel()
119
+
120
+ self._process_cleanup.clear()
121
+
122
+ if self._ws:
123
+ await self._ws.close()
124
+
125
+
126
+ class E2BConnect(Connect):
127
+ async def __aiter__(self) -> AsyncIterator[WebSocketClientProtocol]:
128
+ retries = 0
129
+ max_retries = 12
130
+ backoff_delay = 0.1
131
+ while True:
132
+ try:
133
+ async with self as protocol:
134
+ yield protocol
135
+ except Exception:
136
+ retries += 1
137
+ if retries >= max_retries:
138
+ raise SandboxException("Failed to connect to the server")
139
+ # Add a random initial delay between 0 and 5 seconds.
140
+ # See 7.2.3. Recovering from Abnormal Closure in RFC 6544.
141
+ if backoff_delay == 0.1:
142
+ initial_delay = random.random()
143
+ self.logger.info(
144
+ "! connect failed; reconnecting in %.1f seconds",
145
+ initial_delay,
146
+ exc_info=True,
147
+ )
148
+ await asyncio.sleep(initial_delay)
149
+ else:
150
+ self.logger.info(
151
+ "! connect failed again; retrying in %d seconds",
152
+ int(backoff_delay),
153
+ exc_info=True,
154
+ )
155
+ await asyncio.sleep(int(backoff_delay))
156
+ # Increase delay with truncated exponential backoff.
157
+ if retries > 4:
158
+ backoff_delay = backoff_delay * 1.2
159
+ backoff_delay = min(backoff_delay, 10)
160
+ continue
161
+ else:
162
+ # Connection succeeded - reset backoff delay
163
+ backoff_delay = 0.1
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "e2b-code-interpreter"
3
- version = "0.0.10a0"
3
+ version = "0.0.11a0"
4
4
  description = "E2B Code Interpreter - Stateful code execution"
5
5
  authors = ["e2b <hello@e2b.dev>"]
6
6
  license = "Apache-2.0"
@@ -12,9 +12,11 @@ packages = [{ include = "e2b_code_interpreter" }]
12
12
  [tool.poetry.dependencies]
13
13
  python = "^3.8"
14
14
 
15
- pydantic = ">1, <3"
15
+ pydantic = "*"
16
16
  websocket-client = "^1.7.0"
17
- e2b = ">=0.17.1"
17
+ websockets = "^12.0"
18
+ e2b = "0.17.2a20"
19
+ requests = "^2.32.3"
18
20
 
19
21
  [tool.poetry.group.dev.dependencies]
20
22
  black = "^24.3.0"