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.
- {e2b_code_interpreter-0.0.10a0 → e2b_code_interpreter-0.0.11a0}/PKG-INFO +5 -3
- {e2b_code_interpreter-0.0.10a0 → e2b_code_interpreter-0.0.11a0}/e2b_code_interpreter/__init__.py +1 -0
- e2b_code_interpreter-0.0.11a0/e2b_code_interpreter/constants.py +1 -0
- e2b_code_interpreter-0.0.11a0/e2b_code_interpreter/future.py +46 -0
- {e2b_code_interpreter-0.0.10a0 → e2b_code_interpreter-0.0.11a0}/e2b_code_interpreter/main.py +90 -58
- {e2b_code_interpreter-0.0.10a0 → e2b_code_interpreter-0.0.11a0}/e2b_code_interpreter/messaging.py +32 -16
- {e2b_code_interpreter-0.0.10a0 → e2b_code_interpreter-0.0.11a0}/e2b_code_interpreter/models.py +17 -14
- e2b_code_interpreter-0.0.11a0/e2b_code_interpreter/websocket_client.py +163 -0
- {e2b_code_interpreter-0.0.10a0 → e2b_code_interpreter-0.0.11a0}/pyproject.toml +5 -3
- {e2b_code_interpreter-0.0.10a0 → e2b_code_interpreter-0.0.11a0}/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: e2b-code-interpreter
|
|
3
|
-
Version: 0.0.
|
|
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 (
|
|
18
|
-
Requires-Dist: pydantic
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
TIMEOUT = 60
|
|
@@ -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()
|
{e2b_code_interpreter-0.0.10a0 → e2b_code_interpreter-0.0.11a0}/e2b_code_interpreter/main.py
RENAMED
|
@@ -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
|
|
13
|
-
from
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
42
|
-
api_key=api_key,
|
|
43
|
-
cwd=cwd,
|
|
44
|
-
env_vars=env_vars,
|
|
38
|
+
template=template,
|
|
45
39
|
timeout=timeout,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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[[
|
|
70
|
-
on_stderr: Optional[Callable[[
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
|
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 = {
|
|
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.
|
|
172
|
+
f"{self._sandbox.get_protocol()}://{self._sandbox.get_host(8888)}/api/sessions",
|
|
151
173
|
json=data,
|
|
152
|
-
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.
|
|
184
|
+
f"{self._sandbox.get_protocol()}://{self._sandbox.get_host(8888)}/api/sessions/{session_id}",
|
|
163
185
|
json={"path": cwd},
|
|
164
|
-
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,
|
|
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,
|
|
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.
|
|
194
|
-
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,
|
|
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,
|
|
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.
|
|
223
|
-
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,
|
|
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.
|
|
242
|
-
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,
|
|
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.
|
|
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=
|
|
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,
|
|
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.
|
|
296
|
-
"/root/.jupyter/kernel_id",
|
|
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.
|
|
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,
|
|
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()
|
{e2b_code_interpreter-0.0.10a0 → e2b_code_interpreter-0.0.11a0}/e2b_code_interpreter/messaging.py
RENAMED
|
@@ -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
|
|
11
|
-
from
|
|
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[[
|
|
30
|
-
on_stderr: Optional[Callable[[
|
|
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[[
|
|
36
|
-
on_stderr: Optional[Callable[[
|
|
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[[
|
|
132
|
-
on_stderr: Optional[Callable[[
|
|
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
|
-
|
|
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
|
-
|
|
216
|
+
CellMessage(
|
|
201
217
|
line=data["content"]["text"],
|
|
202
218
|
error=True,
|
|
203
219
|
timestamp=time.time_ns(),
|
{e2b_code_interpreter-0.0.10a0 → e2b_code_interpreter-0.0.11a0}/e2b_code_interpreter/models.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import copy
|
|
2
2
|
from typing import List, Optional, Iterable, Dict
|
|
3
|
-
from pydantic import BaseModel
|
|
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.
|
|
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 = "
|
|
15
|
+
pydantic = "*"
|
|
16
16
|
websocket-client = "^1.7.0"
|
|
17
|
-
|
|
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"
|
|
File without changes
|