boxfab-code-interpreter 0.1.0a1__py3-none-any.whl

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,416 @@
1
+ import logging
2
+ import httpx
3
+
4
+ from typing import Optional, Dict, overload, Union, List
5
+ from httpx import AsyncClient
6
+
7
+ from boxfab import (
8
+ AsyncSandbox as BaseAsyncSandbox,
9
+ InvalidArgumentException,
10
+ )
11
+ from boxfab.api.client_async import get_transport
12
+
13
+ from boxfab_code_interpreter.constants import (
14
+ DEFAULT_TEMPLATE,
15
+ JUPYTER_PORT,
16
+ DEFAULT_TIMEOUT,
17
+ )
18
+ from boxfab_code_interpreter.models import (
19
+ Execution,
20
+ ExecutionError,
21
+ Context,
22
+ RunCodeLanguage,
23
+ Result,
24
+ aextract_exception,
25
+ OutputHandlerWithAsync,
26
+ async_parse_output,
27
+ OutputMessage,
28
+ )
29
+ from boxfab_code_interpreter.exceptions import (
30
+ format_execution_timeout_error,
31
+ format_request_timeout_error,
32
+ format_sandbox_killed_error,
33
+ )
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ class AsyncSandbox(BaseAsyncSandbox):
39
+ """
40
+ BoxFab cloud sandbox is a secure and isolated cloud environment.
41
+
42
+ The sandbox allows you to:
43
+ - Access Linux OS
44
+ - Create, list, and delete files and directories
45
+ - Run commands
46
+ - Run isolated code
47
+ - Access the internet
48
+
49
+ Check docs [here](https://boxfab.dev/docs).
50
+
51
+ Use the `AsyncSandbox.create()` to create a new sandbox.
52
+
53
+ Example:
54
+ ```python
55
+ from boxfab_code_interpreter import AsyncSandbox
56
+ sandbox = await AsyncSandbox.create()
57
+ ```
58
+ """
59
+
60
+ default_template = DEFAULT_TEMPLATE
61
+
62
+ @property
63
+ def _jupyter_url(self) -> str:
64
+ return self.connection_config.get_sandbox_url(
65
+ self.sandbox_id, self.sandbox_domain, port=JUPYTER_PORT
66
+ )
67
+
68
+ @property
69
+ def _client(self) -> AsyncClient:
70
+ # TODO: Remove later
71
+ # Use a dedicated HTTP/1.1 transport for Jupyter requests.
72
+ #
73
+ # The base SDK's shared transport now defaults to HTTP/2. With
74
+ # HTTP/2, multiple requests are multiplexed over a single TCP
75
+ # connection, so when a client cancels a request (e.g. the caller
76
+ # disconnects from the streaming `/execute` endpoint) the server
77
+ # may not detect the disconnect: only the HTTP/2 stream is
78
+ # cancelled, the underlying TCP connection stays open.
79
+ #
80
+ # Forcing HTTP/1.1 here keeps the 1:1 mapping between TCP
81
+ # connection and request, so client disconnects propagate to the
82
+ # server as a TCP close and long-running executions can be
83
+ # cancelled reliably. The helper also caches the transport
84
+ # per-event-loop for async.
85
+ return AsyncClient(
86
+ transport=get_transport(self.connection_config, http_version="1.1"),
87
+ )
88
+
89
+ async def _handle_connection_error(self, err: Exception) -> None:
90
+ """
91
+ Raises a descriptive exception if the connection error was caused by
92
+ the sandbox being killed mid-request. If the sandbox is still running
93
+ (or its state can't be determined), returns so the caller can re-raise
94
+ the original error.
95
+ """
96
+ try:
97
+ running = await self.is_running()
98
+ except Exception:
99
+ # The state check itself failed, so we can't tell whether the
100
+ # sandbox was killed — let the caller re-raise the original error
101
+ # instead of wrongly claiming the sandbox is gone.
102
+ return
103
+ if not running:
104
+ raise format_sandbox_killed_error() from err
105
+
106
+ @overload
107
+ async def run_code(
108
+ self,
109
+ code: str,
110
+ language: Optional[RunCodeLanguage] = None,
111
+ on_stdout: Optional[OutputHandlerWithAsync[OutputMessage]] = None,
112
+ on_stderr: Optional[OutputHandlerWithAsync[OutputMessage]] = None,
113
+ on_result: Optional[OutputHandlerWithAsync[Result]] = None,
114
+ on_error: Optional[OutputHandlerWithAsync[ExecutionError]] = None,
115
+ envs: Optional[Dict[str, str]] = None,
116
+ timeout: Optional[float] = None,
117
+ request_timeout: Optional[float] = None,
118
+ ) -> Execution:
119
+ """
120
+ Runs the code for the specified language.
121
+
122
+ Specify the `language` or `context` option to run the code as a different language or in a different `Context`.
123
+ If no language is specified, Python is used.
124
+
125
+ You can reference previously defined variables, imports, and functions in the code.
126
+
127
+ :param code: Code to execute
128
+ :param language: Language to use for code execution. If not defined, the default Python context is used.
129
+ :param on_stdout: Callback for stdout messages
130
+ :param on_stderr: Callback for stderr messages
131
+ :param on_result: Callback for the `Result` object
132
+ :param on_error: Callback for the `ExecutionError` object
133
+ :param envs: Custom environment variables
134
+ :param timeout: Timeout for the code execution in **seconds**
135
+ :param request_timeout: Timeout for the request in **seconds**
136
+
137
+ :return: `Execution` result object
138
+ """
139
+ ...
140
+
141
+ @overload
142
+ async def run_code(
143
+ self,
144
+ code: str,
145
+ context: Optional[Context] = None,
146
+ on_stdout: Optional[OutputHandlerWithAsync[OutputMessage]] = None,
147
+ on_stderr: Optional[OutputHandlerWithAsync[OutputMessage]] = None,
148
+ on_result: Optional[OutputHandlerWithAsync[Result]] = None,
149
+ on_error: Optional[OutputHandlerWithAsync[ExecutionError]] = None,
150
+ envs: Optional[Dict[str, str]] = None,
151
+ timeout: Optional[float] = None,
152
+ request_timeout: Optional[float] = None,
153
+ ) -> Execution:
154
+ """
155
+ Runs the code in the specified context, if not specified, the default context is used.
156
+
157
+ Specify the `language` or `context` option to run the code as a different language or in a different `Context`.
158
+
159
+ You can reference previously defined variables, imports, and functions in the code.
160
+
161
+ :param code: Code to execute
162
+ :param context: Concrete context to run the code in. If not specified, the default context for the language is used. It's mutually exclusive with the language.
163
+ :param on_stdout: Callback for stdout messages
164
+ :param on_stderr: Callback for stderr messages
165
+ :param on_result: Callback for the `Result` object
166
+ :param on_error: Callback for the `ExecutionError` object
167
+ :param envs: Custom environment variables
168
+ :param timeout: Timeout for the code execution in **seconds**
169
+ :param request_timeout: Timeout for the request in **seconds**
170
+
171
+ :return: `Execution` result object
172
+ """
173
+ ...
174
+
175
+ async def run_code(
176
+ self,
177
+ code: str,
178
+ language: Optional[str] = None,
179
+ context: Optional[Context] = None,
180
+ on_stdout: Optional[OutputHandlerWithAsync[OutputMessage]] = None,
181
+ on_stderr: Optional[OutputHandlerWithAsync[OutputMessage]] = None,
182
+ on_result: Optional[OutputHandlerWithAsync[Result]] = None,
183
+ on_error: Optional[OutputHandlerWithAsync[ExecutionError]] = None,
184
+ envs: Optional[Dict[str, str]] = None,
185
+ timeout: Optional[float] = None,
186
+ request_timeout: Optional[float] = None,
187
+ ) -> Execution:
188
+ logger.debug(f"Executing code {code}")
189
+
190
+ if context and language:
191
+ raise InvalidArgumentException(
192
+ "You can provide context or language, but not both at the same time."
193
+ )
194
+
195
+ timeout = None if timeout == 0 else (timeout or DEFAULT_TIMEOUT)
196
+ request_timeout = request_timeout or self.connection_config.request_timeout
197
+ context_id = context.id if context else None
198
+ try:
199
+ headers = {
200
+ "Content-Type": "application/json",
201
+ "E2b-Sandbox-Id": self.sandbox_id,
202
+ "E2b-Sandbox-Port": str(JUPYTER_PORT),
203
+ }
204
+ if self._envd_access_token:
205
+ headers["X-Access-Token"] = self._envd_access_token
206
+ if self.traffic_access_token:
207
+ headers["E2B-Traffic-Access-Token"] = self.traffic_access_token
208
+
209
+ async with self._client.stream(
210
+ "POST",
211
+ f"{self._jupyter_url}/execute",
212
+ json={
213
+ "code": code,
214
+ "context_id": context_id,
215
+ "language": language,
216
+ "env_vars": envs,
217
+ },
218
+ headers=headers,
219
+ # `timeout` bounds the execution, `request_timeout` only the
220
+ # connect. Every non-connect phase must carry `timeout`: the
221
+ # SDK's pyqwest-backed transport collapses the per-phase
222
+ # timeouts into a single whole-request deadline and takes the
223
+ # longest of them, so leaving `request_timeout` on the write
224
+ # and pool phases would raise the floor to
225
+ # `max(timeout, request_timeout)` and silently ignore any
226
+ # `timeout` shorter than it. This matches the JS SDK, which
227
+ # aborts the execution on a `timeout`-long timer.
228
+ # `timeout=0` disables the deadline entirely; the transport
229
+ # still bounds connect on its own.
230
+ timeout=(
231
+ httpx.Timeout(timeout, connect=request_timeout)
232
+ if timeout is not None
233
+ else httpx.Timeout(None)
234
+ ),
235
+ ) as response:
236
+ err = await aextract_exception(response)
237
+ if err:
238
+ raise err
239
+
240
+ execution = Execution()
241
+
242
+ async for line in response.aiter_lines():
243
+ await async_parse_output(
244
+ execution,
245
+ line,
246
+ on_stdout=on_stdout,
247
+ on_stderr=on_stderr,
248
+ on_result=on_result,
249
+ on_error=on_error,
250
+ )
251
+
252
+ return execution
253
+ except httpx.ReadTimeout:
254
+ raise format_execution_timeout_error()
255
+ except httpx.TimeoutException:
256
+ raise format_request_timeout_error()
257
+ except (httpx.ReadError, httpx.RemoteProtocolError) as err:
258
+ await self._handle_connection_error(err)
259
+ raise
260
+
261
+ async def create_code_context(
262
+ self,
263
+ cwd: Optional[str] = None,
264
+ language: Optional[RunCodeLanguage] = None,
265
+ request_timeout: Optional[float] = None,
266
+ ) -> Context:
267
+ """
268
+ Creates a new context to run code in.
269
+
270
+ :param cwd: Set the current working directory for the context, defaults to `/home/user`
271
+ :param language: Language of the context. If not specified, defaults to Python
272
+ :param request_timeout: Timeout for the request in **milliseconds**
273
+
274
+ :return: Context object
275
+ """
276
+ logger.debug(f"Creating new {language} context")
277
+
278
+ data = {}
279
+ if language:
280
+ data["language"] = language
281
+ if cwd:
282
+ data["cwd"] = cwd
283
+
284
+ try:
285
+ headers = {
286
+ "Content-Type": "application/json",
287
+ "E2b-Sandbox-Id": self.sandbox_id,
288
+ "E2b-Sandbox-Port": str(JUPYTER_PORT),
289
+ }
290
+ if self.traffic_access_token:
291
+ headers["E2B-Traffic-Access-Token"] = self.traffic_access_token
292
+
293
+ response = await self._client.post(
294
+ f"{self._jupyter_url}/contexts",
295
+ headers=headers,
296
+ json=data,
297
+ timeout=request_timeout or self.connection_config.request_timeout,
298
+ )
299
+
300
+ err = await aextract_exception(response)
301
+ if err:
302
+ raise err
303
+
304
+ data = response.json()
305
+ return Context.from_json(data)
306
+ except httpx.TimeoutException:
307
+ raise format_request_timeout_error()
308
+ except (httpx.ReadError, httpx.RemoteProtocolError) as err:
309
+ await self._handle_connection_error(err)
310
+ raise
311
+
312
+ async def remove_code_context(
313
+ self,
314
+ context: Union[Context, str],
315
+ ) -> None:
316
+ """
317
+ Removes a context.
318
+
319
+ :param context: Context to remove. Can be a Context object or a context ID string.
320
+
321
+ :return: None
322
+ """
323
+ context_id = context.id if isinstance(context, Context) else context
324
+
325
+ try:
326
+ headers = {
327
+ "Content-Type": "application/json",
328
+ "E2b-Sandbox-Id": self.sandbox_id,
329
+ "E2b-Sandbox-Port": str(JUPYTER_PORT),
330
+ }
331
+ if self.traffic_access_token:
332
+ headers["E2B-Traffic-Access-Token"] = self.traffic_access_token
333
+
334
+ response = await self._client.delete(
335
+ f"{self._jupyter_url}/contexts/{context_id}",
336
+ headers=headers,
337
+ timeout=self.connection_config.request_timeout,
338
+ )
339
+
340
+ err = await aextract_exception(response)
341
+ if err:
342
+ raise err
343
+ except httpx.TimeoutException:
344
+ raise format_request_timeout_error()
345
+ except (httpx.ReadError, httpx.RemoteProtocolError) as err:
346
+ await self._handle_connection_error(err)
347
+ raise
348
+
349
+ async def list_code_contexts(self) -> List[Context]:
350
+ """
351
+ List all contexts.
352
+
353
+ :return: List of contexts.
354
+ """
355
+ try:
356
+ headers = {
357
+ "Content-Type": "application/json",
358
+ "E2b-Sandbox-Id": self.sandbox_id,
359
+ "E2b-Sandbox-Port": str(JUPYTER_PORT),
360
+ }
361
+ if self.traffic_access_token:
362
+ headers["E2B-Traffic-Access-Token"] = self.traffic_access_token
363
+
364
+ response = await self._client.get(
365
+ f"{self._jupyter_url}/contexts",
366
+ headers=headers,
367
+ timeout=self.connection_config.request_timeout,
368
+ )
369
+
370
+ err = await aextract_exception(response)
371
+ if err:
372
+ raise err
373
+
374
+ data = response.json()
375
+ return [Context.from_json(context_data) for context_data in data]
376
+ except httpx.TimeoutException:
377
+ raise format_request_timeout_error()
378
+ except (httpx.ReadError, httpx.RemoteProtocolError) as err:
379
+ await self._handle_connection_error(err)
380
+ raise
381
+
382
+ async def restart_code_context(
383
+ self,
384
+ context: Union[Context, str],
385
+ ) -> None:
386
+ """
387
+ Restart a context.
388
+
389
+ :param context: Context to restart. Can be a Context object or a context ID string.
390
+
391
+ :return: None
392
+ """
393
+ context_id = context.id if isinstance(context, Context) else context
394
+ try:
395
+ headers = {
396
+ "Content-Type": "application/json",
397
+ "E2b-Sandbox-Id": self.sandbox_id,
398
+ "E2b-Sandbox-Port": str(JUPYTER_PORT),
399
+ }
400
+ if self.traffic_access_token:
401
+ headers["E2B-Traffic-Access-Token"] = self.traffic_access_token
402
+
403
+ response = await self._client.post(
404
+ f"{self._jupyter_url}/contexts/{context_id}/restart",
405
+ headers=headers,
406
+ timeout=self.connection_config.request_timeout,
407
+ )
408
+
409
+ err = await aextract_exception(response)
410
+ if err:
411
+ raise err
412
+ except httpx.TimeoutException:
413
+ raise format_request_timeout_error()
414
+ except (httpx.ReadError, httpx.RemoteProtocolError) as err:
415
+ await self._handle_connection_error(err)
416
+ raise