e2b-code-interpreter 0.0.1a0__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.
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.1
2
+ Name: e2b-code-interpreter
3
+ Version: 0.0.1a0
4
+ Summary: E2B Code Interpreter - Stateful code execution
5
+ Home-page: https://e2b.dev/
6
+ License: Apache-2.0
7
+ Author: e2b
8
+ Author-email: hello@e2b.dev
9
+ Requires-Python: >=3.8,<4.0
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Requires-Dist: e2b (>=0.14.11)
17
+ Requires-Dist: pydantic (>1,<3)
18
+ Requires-Dist: websocket-client (>=1.7.0,<2.0.0)
19
+ Project-URL: Bug Tracker, https://github.com/e2b-dev/code-interpreter/issues
20
+ Project-URL: Repository, https://github.com/e2b-dev/e2b-code-interpreter/tree/python
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Code interpreter extension for Python
24
+
25
+ The repository contains a template and modules for the code interpreter sandbox. It is based on the Jupyter server and implements the Jupyter Kernel messaging protocol. This allows for sharing context between code executions and improves support for plotting charts and other display-able data.
26
+
27
+ ## Key Features
28
+
29
+ - **Stateful Execution**: Unlike traditional sandboxes that treat each code execution independently, this package maintains context across executions.
30
+ - **Displaying Graph & Data**: Implements parts of the [Jupyter Kernel messaging protocol](https://jupyter-client.readthedocs.io/en/latest/messaging.html), which support for interactive features like plotting charts, rendering DataFrames, etc.
31
+
32
+ ## Installation
33
+
34
+ ```sh
35
+ pip install e2b-code-interpreter
36
+ ```
37
+
38
+ ## Examples
39
+
40
+ ### Minimal example with the sharing context
41
+
42
+ ```python
43
+ from e2b_code_interpreter import CodeInterpreter
44
+
45
+ with CodeInterpreter() as sandbox:
46
+ sandbox.exec_cell("x = 1")
47
+
48
+ result = sandbox.exec_cell("x += 1; x")
49
+ print(result.text) # outputs 2
50
+
51
+ ```
52
+
53
+ ### Get charts and any display-able data
54
+
55
+ ```python
56
+ import base64
57
+ import io
58
+
59
+ from matplotlib import image as mpimg, pyplot as plt
60
+
61
+ from e2b_code_interpreter import CodeInterpreter
62
+
63
+ code = """
64
+ import matplotlib.pyplot as plt
65
+ import numpy as np
66
+
67
+ x = np.linspace(0, 20, 100)
68
+ y = np.sin(x)
69
+
70
+ plt.plot(x, y)
71
+ plt.show()
72
+ """
73
+
74
+ with CodeInterpreter() as sandbox:
75
+ # you can install dependencies in "jupyter notebook style"
76
+ sandbox.exec_cell("!pip install matplotlib")
77
+
78
+ # plot random graph
79
+ result = sandbox.exec_cell(code)
80
+
81
+ # there's your image
82
+ image = result.display_data[0]["image/png"]
83
+
84
+ # example how to show the image / prove it works
85
+ i = base64.b64decode(image)
86
+ i = io.BytesIO(i)
87
+ i = mpimg.imread(i, format='PNG')
88
+
89
+ plt.imshow(i, interpolation='nearest')
90
+ plt.show()
91
+ ```
92
+
93
+ ### Streaming code output
94
+
95
+ ```python
96
+ from e2b_code_interpreter import CodeInterpreter
97
+
98
+ code = """
99
+ import time
100
+
101
+ print("hello")
102
+ time.sleep(5)
103
+ print("world")
104
+ """
105
+
106
+ with CodeInterpreter() as sandbox:
107
+ sandbox.exec_cell(code, on_stdout=print, on_stderr=print)
108
+ ```
109
+
110
+ ### Pre-installed Python packages inside the sandbox
111
+
112
+ The full and always up-to-date list can be found in the [`requirements.txt`](https://github.com/e2b-dev/E2B/blob/stateful-code-interpreter/sandboxes/code-interpreter-stateful/requirements.txt) file.
113
+
114
+ ```text
115
+ # Jupyter server requirements
116
+ jupyter-server==2.13.0
117
+ ipykernel==6.29.3
118
+ ipython==8.22.2
119
+
120
+ # Other packages
121
+ aiohttp==3.9.3
122
+ beautifulsoup4==4.12.3
123
+ bokeh==3.3.4
124
+ gensim==4.3.2
125
+ imageio==2.34.0
126
+ joblib==1.3.2
127
+ librosa==0.10.1
128
+ matplotlib==3.8.3
129
+ nltk==3.8.1
130
+ numpy==1.26.4
131
+ opencv-python==4.9.0.80
132
+ openpyxl==3.1.2
133
+ pandas==1.5.3
134
+ plotly==5.19.0
135
+ pytest==8.1.0
136
+ python-docx==1.1.0
137
+ pytz==2024.1
138
+ requests==2.26.0
139
+ scikit-image==0.22.0
140
+ scikit-learn==1.4.1.post1
141
+ scipy==1.12.0
142
+ seaborn==0.13.2
143
+ soundfile==0.12.1
144
+ spacy==3.7.4
145
+ textblob==0.18.0
146
+ tornado==6.4
147
+ urllib3==1.26.7
148
+ xarray==2024.2.0
149
+ xlrd==2.0.1
150
+ ```
151
+
@@ -0,0 +1,128 @@
1
+ # Code interpreter extension for Python
2
+
3
+ The repository contains a template and modules for the code interpreter sandbox. It is based on the Jupyter server and implements the Jupyter Kernel messaging protocol. This allows for sharing context between code executions and improves support for plotting charts and other display-able data.
4
+
5
+ ## Key Features
6
+
7
+ - **Stateful Execution**: Unlike traditional sandboxes that treat each code execution independently, this package maintains context across executions.
8
+ - **Displaying Graph & Data**: Implements parts of the [Jupyter Kernel messaging protocol](https://jupyter-client.readthedocs.io/en/latest/messaging.html), which support for interactive features like plotting charts, rendering DataFrames, etc.
9
+
10
+ ## Installation
11
+
12
+ ```sh
13
+ pip install e2b-code-interpreter
14
+ ```
15
+
16
+ ## Examples
17
+
18
+ ### Minimal example with the sharing context
19
+
20
+ ```python
21
+ from e2b_code_interpreter import CodeInterpreter
22
+
23
+ with CodeInterpreter() as sandbox:
24
+ sandbox.exec_cell("x = 1")
25
+
26
+ result = sandbox.exec_cell("x += 1; x")
27
+ print(result.text) # outputs 2
28
+
29
+ ```
30
+
31
+ ### Get charts and any display-able data
32
+
33
+ ```python
34
+ import base64
35
+ import io
36
+
37
+ from matplotlib import image as mpimg, pyplot as plt
38
+
39
+ from e2b_code_interpreter import CodeInterpreter
40
+
41
+ code = """
42
+ import matplotlib.pyplot as plt
43
+ import numpy as np
44
+
45
+ x = np.linspace(0, 20, 100)
46
+ y = np.sin(x)
47
+
48
+ plt.plot(x, y)
49
+ plt.show()
50
+ """
51
+
52
+ with CodeInterpreter() as sandbox:
53
+ # you can install dependencies in "jupyter notebook style"
54
+ sandbox.exec_cell("!pip install matplotlib")
55
+
56
+ # plot random graph
57
+ result = sandbox.exec_cell(code)
58
+
59
+ # there's your image
60
+ image = result.display_data[0]["image/png"]
61
+
62
+ # example how to show the image / prove it works
63
+ i = base64.b64decode(image)
64
+ i = io.BytesIO(i)
65
+ i = mpimg.imread(i, format='PNG')
66
+
67
+ plt.imshow(i, interpolation='nearest')
68
+ plt.show()
69
+ ```
70
+
71
+ ### Streaming code output
72
+
73
+ ```python
74
+ from e2b_code_interpreter import CodeInterpreter
75
+
76
+ code = """
77
+ import time
78
+
79
+ print("hello")
80
+ time.sleep(5)
81
+ print("world")
82
+ """
83
+
84
+ with CodeInterpreter() as sandbox:
85
+ sandbox.exec_cell(code, on_stdout=print, on_stderr=print)
86
+ ```
87
+
88
+ ### Pre-installed Python packages inside the sandbox
89
+
90
+ The full and always up-to-date list can be found in the [`requirements.txt`](https://github.com/e2b-dev/E2B/blob/stateful-code-interpreter/sandboxes/code-interpreter-stateful/requirements.txt) file.
91
+
92
+ ```text
93
+ # Jupyter server requirements
94
+ jupyter-server==2.13.0
95
+ ipykernel==6.29.3
96
+ ipython==8.22.2
97
+
98
+ # Other packages
99
+ aiohttp==3.9.3
100
+ beautifulsoup4==4.12.3
101
+ bokeh==3.3.4
102
+ gensim==4.3.2
103
+ imageio==2.34.0
104
+ joblib==1.3.2
105
+ librosa==0.10.1
106
+ matplotlib==3.8.3
107
+ nltk==3.8.1
108
+ numpy==1.26.4
109
+ opencv-python==4.9.0.80
110
+ openpyxl==3.1.2
111
+ pandas==1.5.3
112
+ plotly==5.19.0
113
+ pytest==8.1.0
114
+ python-docx==1.1.0
115
+ pytz==2024.1
116
+ requests==2.26.0
117
+ scikit-image==0.22.0
118
+ scikit-learn==1.4.1.post1
119
+ scipy==1.12.0
120
+ seaborn==0.13.2
121
+ soundfile==0.12.1
122
+ spacy==3.7.4
123
+ textblob==0.18.0
124
+ tornado==6.4
125
+ urllib3==1.26.7
126
+ xarray==2024.2.0
127
+ xlrd==2.0.1
128
+ ```
@@ -0,0 +1,2 @@
1
+ from .main import CodeInterpreter, JupyterExtension
2
+ from .models import Result, Error, Data, KernelException
@@ -0,0 +1,289 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import threading
5
+ from concurrent.futures import Future
6
+ from typing import Any, Callable, List, Optional, Dict
7
+
8
+ import requests
9
+ from e2b import EnvVars, ProcessMessage, Sandbox
10
+ from e2b.constants import TIMEOUT
11
+
12
+ from e2b_code_interpreter.messaging import JupyterKernelWebSocket
13
+ from e2b_code_interpreter.models import KernelException, Result
14
+
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
+ _default_kernel_id: Optional[str] = None
56
+ _connected_kernels: Dict[str, Future[JupyterKernelWebSocket]] = {}
57
+
58
+ def __init__(self, sandbox: CodeInterpreter, timeout: Optional[float] = TIMEOUT):
59
+ self._sandbox = sandbox
60
+ self._kernel_id_set = Future()
61
+ self._start_connecting_to_default_kernel(timeout=timeout)
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_display_data: Optional[Callable[[Dict[str, Any]], Any]] = None,
70
+ timeout: Optional[float] = TIMEOUT,
71
+ ) -> Result:
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_display_data: A callback function to handle display data messages from 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_display_data)
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 = {"cwd": 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
+ logger.debug(f"Default kernel id: {kernel_id}")
286
+ self._connect_to_kernel_ws(kernel_id, timeout=timeout)
287
+ self._kernel_id_set.set_result(kernel_id)
288
+
289
+ threading.Thread(target=setup_default_kernel).start()
@@ -0,0 +1,253 @@
1
+ import json
2
+ import logging
3
+ import threading
4
+ import time
5
+ import uuid
6
+ from concurrent.futures import Future
7
+ from queue import Queue
8
+ from typing import Callable, Dict, List, Any, Optional
9
+
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
+ from pydantic import ConfigDict, PrivateAttr, BaseModel
16
+
17
+ from e2b_code_interpreter.models import Result, Data, Error, MIMEType
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class CellExecution:
23
+ """
24
+ Represents the execution of a cell in the Jupyter kernel.
25
+ It's an internal class used by JupyterKernelWebSocket.
26
+ """
27
+
28
+ input_accepted: bool = False
29
+ on_stdout: Optional[Callable[[ProcessMessage], None]] = None
30
+ on_stderr: Optional[Callable[[ProcessMessage], None]] = None
31
+ on_display_data: Optional[Callable[[Dict[MIMEType, str]], None]] = None
32
+
33
+ def __init__(
34
+ self,
35
+ on_stdout: Optional[Callable[[ProcessMessage], None]] = None,
36
+ on_stderr: Optional[Callable[[ProcessMessage], None]] = None,
37
+ on_display_data: Optional[Callable[[Dict[MIMEType, str]], None]] = None,
38
+ ):
39
+ self.partial_result = Result()
40
+ self.result = Future()
41
+ self.on_stdout = on_stdout
42
+ self.on_stderr = on_stderr
43
+ self.on_display_data = on_display_data
44
+
45
+
46
+ class JupyterKernelWebSocket(BaseModel):
47
+ model_config = ConfigDict(arbitrary_types_allowed=True)
48
+
49
+ url: str
50
+
51
+ _cells: Dict[str, CellExecution] = {}
52
+ _waiting_for_replies: Dict[str, DeferredFuture] = PrivateAttr(default_factory=dict)
53
+ _queue_in: Queue = PrivateAttr(default_factory=Queue)
54
+ _queue_out: Queue = PrivateAttr(default_factory=Queue)
55
+ _process_cleanup: List[Callable[[], Any]] = PrivateAttr(default_factory=list)
56
+ _closed: bool = PrivateAttr(default=False)
57
+
58
+ def process_messages(self):
59
+ while True:
60
+ data = self._queue_out.get()
61
+
62
+ logger.debug(f"WebSocket received message: {data}".strip())
63
+ self._receive_message(json.loads(data))
64
+ self._queue_out.task_done()
65
+
66
+ def connect(self, timeout: float = TIMEOUT):
67
+ started = threading.Event()
68
+ stopped = threading.Event()
69
+ self._process_cleanup.append(stopped.set)
70
+
71
+ threading.Thread(
72
+ target=self.process_messages, daemon=True, name="e2b-process-messages"
73
+ ).start()
74
+
75
+ threading.Thread(
76
+ target=WebSocket(
77
+ url=self.url,
78
+ queue_in=self._queue_in,
79
+ queue_out=self._queue_out,
80
+ started=started,
81
+ stopped=stopped,
82
+ ).run,
83
+ daemon=True,
84
+ name="e2b-code-interpreter-websocket",
85
+ ).start()
86
+
87
+ logger.debug("WebSocket waiting to start")
88
+
89
+ try:
90
+ start_time = time.time()
91
+ while (
92
+ not started.is_set()
93
+ and time.time() - start_time < timeout
94
+ and not self._closed
95
+ ):
96
+ time.sleep(0.1)
97
+
98
+ if not started.is_set():
99
+ raise TimeoutException("WebSocket failed to start")
100
+ except BaseException as e:
101
+ self.close()
102
+ raise Exception(f"WebSocket failed to start: {e}") from e
103
+
104
+ logger.debug("WebSocket started")
105
+
106
+ @staticmethod
107
+ def _get_execute_request(msg_id: str, code: str) -> str:
108
+ return json.dumps(
109
+ {
110
+ "header": {
111
+ "msg_id": msg_id,
112
+ "username": "e2b",
113
+ "session": str(uuid.uuid4()),
114
+ "msg_type": "execute_request",
115
+ "version": "5.3",
116
+ },
117
+ "parent_header": {},
118
+ "metadata": {},
119
+ "content": {
120
+ "code": code,
121
+ "silent": False,
122
+ "store_history": False,
123
+ "user_expressions": {},
124
+ "allow_stdin": False,
125
+ },
126
+ }
127
+ )
128
+
129
+ def send_execution_message(
130
+ self,
131
+ code: str,
132
+ on_stdout: Optional[Callable[[ProcessMessage], None]] = None,
133
+ on_stderr: Optional[Callable[[ProcessMessage], None]] = None,
134
+ on_display_data: Optional[Callable[[Dict[MIMEType, str]], None]] = None,
135
+ ) -> str:
136
+ message_id = str(uuid.uuid4())
137
+ logger.debug(f"Sending execution message: {message_id}")
138
+
139
+ self._cells[message_id] = CellExecution(
140
+ on_stdout=on_stdout,
141
+ on_stderr=on_stderr,
142
+ on_display_data=on_display_data,
143
+ )
144
+ request = self._get_execute_request(message_id, code)
145
+ self._queue_in.put(request)
146
+ return message_id
147
+
148
+ def get_result(self, message_id: str, timeout: Optional[float] = TIMEOUT) -> Result:
149
+ result = self._cells[message_id].result.result(timeout=timeout)
150
+ logger.debug(f"Got result for message: {message_id}")
151
+ del self._cells[message_id]
152
+ return result
153
+
154
+ def _receive_message(self, data: dict):
155
+ """
156
+ Process messages from the WebSocket
157
+
158
+ Message types:
159
+ https://jupyter-client.readthedocs.io/en/stable/messaging.html
160
+
161
+ :param data: The message data
162
+ """
163
+ parent_msg_ig = data["parent_header"]["msg_id"]
164
+ logger.debug(f"Received message {data['msg_type']} for {parent_msg_ig}")
165
+
166
+ cell = self._cells.get(parent_msg_ig)
167
+ if not cell:
168
+ return
169
+
170
+ result = cell.partial_result
171
+
172
+ if data["msg_type"] == "error":
173
+ logger.debug(f"Cell {parent_msg_ig} finished execution with error")
174
+ result.error = Error(
175
+ name=data["content"]["ename"],
176
+ value=data["content"]["evalue"],
177
+ traceback_raw=data["content"]["traceback"],
178
+ )
179
+
180
+ elif data["msg_type"] == "stream":
181
+ if data["content"]["name"] == "stdout":
182
+ result.logs.stdout.append(data["content"]["text"])
183
+ if cell.on_stdout:
184
+ cell.on_stdout(
185
+ ProcessMessage(
186
+ line=data["content"]["text"],
187
+ timestamp=time.time_ns(),
188
+ )
189
+ )
190
+
191
+ elif data["content"]["name"] == "stderr":
192
+ result.logs.stderr.append(data["content"]["text"])
193
+ if cell.on_stderr:
194
+ cell.on_stderr(
195
+ ProcessMessage(
196
+ line=data["content"]["text"],
197
+ error=True,
198
+ timestamp=time.time_ns(),
199
+ )
200
+ )
201
+
202
+ elif data["msg_type"] in "display_data":
203
+ result.data.append(Data(is_main_result=False, data=data["content"]["data"]))
204
+ if cell.on_display_data:
205
+ cell.on_display_data(data["content"]["data"])
206
+ elif data["msg_type"] == "execute_result":
207
+ result.data.append(Data(is_main_result=True, data=data["content"]["data"]))
208
+ elif data["msg_type"] == "status":
209
+ if data["content"]["execution_state"] == "idle":
210
+ if cell.input_accepted:
211
+ logger.debug(f"Cell {parent_msg_ig} finished execution")
212
+ cell.result.set_result(result)
213
+
214
+ elif data["content"]["execution_state"] == "error":
215
+ logger.debug(f"Cell {parent_msg_ig} finished execution with error")
216
+ result.error = Error(
217
+ name=data["content"]["ename"],
218
+ value=data["content"]["evalue"],
219
+ traceback_raw=data["content"]["traceback"],
220
+ )
221
+ cell.result.set_result(result)
222
+
223
+ elif data["msg_type"] == "execute_reply":
224
+ if data["content"]["status"] == "error":
225
+ logger.debug(f"Cell {parent_msg_ig} finished execution with error")
226
+ result.error = Error(
227
+ name=data["content"]["ename"],
228
+ value=data["content"]["evalue"],
229
+ traceback_raw=data["content"]["traceback"],
230
+ )
231
+ elif data["content"]["status"] == "ok":
232
+ pass
233
+
234
+ elif data["msg_type"] == "execute_input":
235
+ logger.debug(f"Input accepted for {parent_msg_ig}")
236
+ cell.input_accepted = True
237
+ else:
238
+ logger.error(f"[UNHANDLED MESSAGE TYPE]: {data['msg_type']}")
239
+ print("[UNHANDLED MESSAGE TYPE]:", data["msg_type"])
240
+
241
+ def close(self):
242
+ logger.debug("Closing WebSocket")
243
+ self._closed = True
244
+
245
+ for cancel in self._process_cleanup:
246
+ cancel()
247
+
248
+ self._process_cleanup.clear()
249
+
250
+ for handler in self._waiting_for_replies.values():
251
+ logger.debug(f"Cancelling waiting for execution result for {handler}")
252
+ handler.cancel()
253
+ del handler
@@ -0,0 +1,215 @@
1
+ import copy
2
+ from typing import List, Optional, Iterable, Dict
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class Error(BaseModel):
7
+ """
8
+ Represents an error that occurred during the execution of a cell.
9
+ The error contains the name of the error, the value of the error, and the traceback.
10
+ """
11
+
12
+ name: str
13
+ "Name of the exception."
14
+ value: str
15
+ "Value of the exception."
16
+ traceback_raw: List[str]
17
+ "List of strings representing the traceback."
18
+
19
+ @property
20
+ def traceback(self) -> str:
21
+ """
22
+ Returns the traceback as a single string.
23
+
24
+ :return: The traceback as a single string.
25
+ """
26
+ return "\n".join(self.traceback_raw)
27
+
28
+
29
+ class MIMEType(str):
30
+ """
31
+ Represents a MIME type.
32
+ """
33
+
34
+
35
+ class Data:
36
+ """
37
+ Represents the data to be displayed as a result of executing a cell in a Jupyter notebook.
38
+ This is result returned by ipython kernel: https://ipython.readthedocs.io/en/stable/development/execution.html#execution-semantics
39
+
40
+
41
+ The result can contain multiple types of data, such as text, images, plots, etc. Each type of data is represented
42
+ as a string, and the result can contain multiple types of data. The text representation is always present, and
43
+ the other representations are optional.
44
+
45
+ The class also provides methods to display the data in a Jupyter notebook.
46
+ """
47
+ text: str
48
+ "Text representation of the data. Always present."
49
+ html: Optional[str] = None
50
+ markdown: Optional[str] = None
51
+ svg: Optional[str] = None
52
+ png: Optional[str] = None
53
+ jpeg: Optional[str] = None
54
+ pdf: Optional[str] = None
55
+ latex: Optional[str] = None
56
+ json: Optional[dict] = None
57
+ javascript: Optional[str] = None
58
+ extra: Optional[dict] = None
59
+ "Extra data that can be included. Not part of the standard types."
60
+
61
+ is_main_result: bool
62
+ "Whether this data is the result of the cell. Data can be produced by display calls of which can be multiple in a cell."
63
+
64
+ raw: Dict[MIMEType, str]
65
+ "Dictionary that maps MIME types to their corresponding string representations of the data."
66
+
67
+ def __init__(self, is_main_result: bool, data: [MIMEType, str]):
68
+ self.is_main_result = is_main_result
69
+ self.raw = copy.deepcopy(data)
70
+
71
+ self.text = data.pop("text/plain")
72
+ self.html = data.pop("text/html", None)
73
+ self.markdown = data.pop("text/markdown", None)
74
+ self.svg = data.pop("image/svg+xml", None)
75
+ self.png = data.pop("image/png", None)
76
+ self.jpeg = data.pop("image/jpeg", None)
77
+ self.pdf = data.pop("application/pdf", None)
78
+ self.latex = data.pop("text/latex", None)
79
+ self.json = data.pop("application/json", None)
80
+ self.javascript = data.pop("application/javascript", None)
81
+ self.extra = data
82
+
83
+ def keys(self) -> Iterable[str]:
84
+ """
85
+ Returns the MIME types of the data.
86
+
87
+ :return: The MIME types of the data.
88
+ """
89
+ return self.raw.keys()
90
+
91
+ def __str__(self) -> str:
92
+ """
93
+ Returns the text representation of the data.
94
+
95
+ :return: The text representation of the data.
96
+ """
97
+ return self.text
98
+
99
+ def _repr_html_(self) -> str:
100
+ """
101
+ Returns the HTML representation of the data.
102
+
103
+ :return: The HTML representation of the data.
104
+ """
105
+ return self.html
106
+
107
+ def _repr_markdown_(self) -> str:
108
+ """
109
+ Returns the Markdown representation of the data.
110
+
111
+ :return: The Markdown representation of the data.
112
+ """
113
+ return self.markdown
114
+
115
+ def _repr_svg_(self) -> str:
116
+ """
117
+ Returns the SVG representation of the data.
118
+
119
+ :return: The SVG representation of the data.
120
+ """
121
+ return self.svg
122
+
123
+ def _repr_png_(self) -> str:
124
+ """
125
+ Returns the base64 representation of the PNG data.
126
+
127
+ :return: The base64 representation of the PNG data.
128
+ """
129
+ return self.png
130
+
131
+ def _repr_jpeg_(self) -> str:
132
+ """
133
+ Returns the base64 representation of the JPEG data.
134
+
135
+ :return: The base64 representation of the JPEG data.
136
+ """
137
+ return self.jpeg
138
+
139
+ def _repr_pdf_(self) -> str:
140
+ """
141
+ Returns the PDF representation of the data.
142
+
143
+ :return: The PDF representation of the data.
144
+ """
145
+ return self.pdf
146
+
147
+ def _repr_latex_(self) -> str:
148
+ """
149
+ Returns the LaTeX representation of the data.
150
+
151
+ :return: The LaTeX representation of the data.
152
+ """
153
+ return self.latex
154
+
155
+ def _repr_json_(self) -> dict:
156
+ """
157
+ Returns the JSON representation of the data.
158
+
159
+ :return: The JSON representation of the data.
160
+ """
161
+ return self.json
162
+
163
+ def _repr_javascript_(self) -> str:
164
+ """
165
+ Returns the JavaScript representation of the data.
166
+
167
+ :return: The JavaScript representation of the data.
168
+ """
169
+ return self.javascript
170
+
171
+
172
+ class Logs(BaseModel):
173
+ """
174
+ Data printed to stdout and stderr during execution, usually by print statements, logs, warnings, subprocesses, etc.
175
+ """
176
+
177
+ stdout: List[str] = []
178
+ "List of strings printed to stdout by prints, subprocesses, etc."
179
+ stderr: List[str] = []
180
+ "List of strings printed to stderr by prints, subprocesses, etc."
181
+
182
+
183
+ class Result(BaseModel):
184
+ """
185
+ Represents the result of a cell execution.
186
+ """
187
+
188
+ class Config:
189
+ arbitrary_types_allowed = True
190
+
191
+ data: List[Data] = []
192
+ "List of result of the cell (interactively interpreted last line), display calls, e.g. matplotlib plots."
193
+ logs: Logs = Logs()
194
+ "Logs printed to stdout and stderr during execution."
195
+ error: Optional[Error] = None
196
+ "Error object if an error occurred, None otherwise."
197
+
198
+ @property
199
+ def text(self) -> Optional[str]:
200
+ """
201
+ Returns the text representation of the result.
202
+
203
+ :return: The text representation of the result.
204
+ """
205
+ for d in self.data:
206
+ if d.is_main_result:
207
+ return d.text
208
+
209
+
210
+ class KernelException(Exception):
211
+ """
212
+ Exception raised when a kernel operation fails.
213
+ """
214
+
215
+ pass
@@ -0,0 +1,31 @@
1
+ [tool.poetry]
2
+ name = "e2b-code-interpreter"
3
+ version = "0.0.1a0"
4
+ description = "E2B Code Interpreter - Stateful code execution"
5
+ authors = ["e2b <hello@e2b.dev>"]
6
+ license = "Apache-2.0"
7
+ readme = "README.md"
8
+ homepage = "https://e2b.dev/"
9
+ repository = "https://github.com/e2b-dev/e2b-code-interpreter/tree/python"
10
+ packages = [{ include = "e2b_code_interpreter" }]
11
+
12
+ [tool.poetry.dependencies]
13
+ python = "^3.8"
14
+
15
+ e2b = ">=0.14.11"
16
+ pydantic = ">1, <3"
17
+ websocket-client = "^1.7.0"
18
+
19
+ [tool.poetry.group.dev.dependencies]
20
+ black = "^24.3.0"
21
+ pytest = "^7.4.0"
22
+ python-dotenv = "^1.0.0"
23
+ pytest-dotenv = "^0.5.2"
24
+
25
+
26
+ [build-system]
27
+ requires = ["poetry-core"]
28
+ build-backend = "poetry.core.masonry.api"
29
+
30
+ [tool.poetry.urls]
31
+ "Bug Tracker" = "https://github.com/e2b-dev/code-interpreter/issues"