ai2in 0.1.0__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.
- ai2in-0.1.0/.gitignore +4 -0
- ai2in-0.1.0/LICENSE +21 -0
- ai2in-0.1.0/PKG-INFO +110 -0
- ai2in-0.1.0/README.md +90 -0
- ai2in-0.1.0/pyproject.toml +31 -0
- ai2in-0.1.0/src/ai2in/__init__.py +296 -0
ai2in-0.1.0/.gitignore
ADDED
ai2in-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AI2IN.dev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ai2in-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ai2in
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for AI2IN — secure AI code sandboxes, hosted in India.
|
|
5
|
+
Project-URL: Homepage, https://ai2in.dev
|
|
6
|
+
Project-URL: Repository, https://github.com/AI2IN-DEV/AI2IN
|
|
7
|
+
Project-URL: Documentation, https://ai2in.dev
|
|
8
|
+
Author: AI2IN.dev
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,ai,code-interpreter,e2b,india,sandbox
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# ai2in — Python SDK
|
|
22
|
+
|
|
23
|
+
The developer client for [AI2IN.dev](https://ai2in.dev) — secure AI code
|
|
24
|
+
sandboxes, hosted in India. Spin up an isolated Linux sandbox, run untrusted /
|
|
25
|
+
AI‑generated Python in a stateful kernel, and get back rich, typed results
|
|
26
|
+
(text, HTML tables, charts, JSON) — the E2B code‑interpreter model, in‑region.
|
|
27
|
+
|
|
28
|
+
Stdlib‑only, zero dependencies.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install ai2in # once published
|
|
34
|
+
# or, from this repo:
|
|
35
|
+
pip install -e sdk/python
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quickstart
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from ai2in import Sandbox
|
|
42
|
+
|
|
43
|
+
with Sandbox(api_key="ai2in_live_…", base_url="https://api.ai2in.dev") as sbx:
|
|
44
|
+
execution = sbx.run_code(
|
|
45
|
+
"import pandas as pd; pd.DataFrame({'gst_lakh_cr': [1.87, 1.73, 1.95]})"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
print(execution.text) # the main result's plain text
|
|
49
|
+
print(execution.logs.stdout) # ['…']
|
|
50
|
+
|
|
51
|
+
for r in execution.results: # rich, typed outputs
|
|
52
|
+
if r.html: # e.g. a DataFrame → HTML table
|
|
53
|
+
save(r.html)
|
|
54
|
+
if r.png: # e.g. a matplotlib figure → base64 PNG
|
|
55
|
+
save_image(r.png)
|
|
56
|
+
|
|
57
|
+
if execution.error: # structured error for self-correction
|
|
58
|
+
print(execution.error.name, execution.error.value)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
State persists across `run_code` calls in the same sandbox (like a notebook):
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
sbx.run_code("x = 41")
|
|
65
|
+
sbx.run_code("x + 1").text # "42"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Streaming
|
|
69
|
+
|
|
70
|
+
Pass callbacks to stream output **live** as the kernel produces it (long loops,
|
|
71
|
+
training logs, incremental prints). The events are still accumulated into the
|
|
72
|
+
returned `Execution`, so the return value is identical whether or not you stream:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
execution = sbx.run_code(
|
|
76
|
+
"for i in range(5):\n print('step', i)\n import time; time.sleep(1)",
|
|
77
|
+
on_stdout=lambda t: print(t, end="", flush=True), # arrives chunk-by-chunk
|
|
78
|
+
on_result=lambda r: render(r), # display() calls, figures, the value
|
|
79
|
+
on_error=lambda e: print(e.value),
|
|
80
|
+
)
|
|
81
|
+
print(execution.execution_ms) # still get the full Execution
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Result model
|
|
85
|
+
|
|
86
|
+
`run_code()` returns an `Execution`:
|
|
87
|
+
|
|
88
|
+
| Field | Type | Notes |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `results` | `list[Result]` | rich outputs; the cell's value has `is_main_result=True` |
|
|
91
|
+
| `logs` | `Logs` | `.stdout` / `.stderr` (lists of str) |
|
|
92
|
+
| `error` | `ExecutionError \| None` | `.name`, `.value`, `.traceback` |
|
|
93
|
+
| `text` | `str \| None` | convenience: the main result's text |
|
|
94
|
+
|
|
95
|
+
Each `Result` may carry any of: `text`, `html`, `markdown`, `svg`, `png`,
|
|
96
|
+
`jpeg`, `pdf`, `latex`, `json`, `javascript`. Call `r.formats()` for what's
|
|
97
|
+
present.
|
|
98
|
+
|
|
99
|
+
## Local dev
|
|
100
|
+
|
|
101
|
+
Point at a local engine (default `http://localhost:4000`):
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
with Sandbox() as sbx: # talks to localhost:4000
|
|
105
|
+
print(sbx.run_code("print(1 + 1)").logs.stdout)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
© 2026 AI2IN.dev — see the repository LICENSE.
|
ai2in-0.1.0/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# ai2in — Python SDK
|
|
2
|
+
|
|
3
|
+
The developer client for [AI2IN.dev](https://ai2in.dev) — secure AI code
|
|
4
|
+
sandboxes, hosted in India. Spin up an isolated Linux sandbox, run untrusted /
|
|
5
|
+
AI‑generated Python in a stateful kernel, and get back rich, typed results
|
|
6
|
+
(text, HTML tables, charts, JSON) — the E2B code‑interpreter model, in‑region.
|
|
7
|
+
|
|
8
|
+
Stdlib‑only, zero dependencies.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install ai2in # once published
|
|
14
|
+
# or, from this repo:
|
|
15
|
+
pip install -e sdk/python
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quickstart
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from ai2in import Sandbox
|
|
22
|
+
|
|
23
|
+
with Sandbox(api_key="ai2in_live_…", base_url="https://api.ai2in.dev") as sbx:
|
|
24
|
+
execution = sbx.run_code(
|
|
25
|
+
"import pandas as pd; pd.DataFrame({'gst_lakh_cr': [1.87, 1.73, 1.95]})"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
print(execution.text) # the main result's plain text
|
|
29
|
+
print(execution.logs.stdout) # ['…']
|
|
30
|
+
|
|
31
|
+
for r in execution.results: # rich, typed outputs
|
|
32
|
+
if r.html: # e.g. a DataFrame → HTML table
|
|
33
|
+
save(r.html)
|
|
34
|
+
if r.png: # e.g. a matplotlib figure → base64 PNG
|
|
35
|
+
save_image(r.png)
|
|
36
|
+
|
|
37
|
+
if execution.error: # structured error for self-correction
|
|
38
|
+
print(execution.error.name, execution.error.value)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
State persists across `run_code` calls in the same sandbox (like a notebook):
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
sbx.run_code("x = 41")
|
|
45
|
+
sbx.run_code("x + 1").text # "42"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Streaming
|
|
49
|
+
|
|
50
|
+
Pass callbacks to stream output **live** as the kernel produces it (long loops,
|
|
51
|
+
training logs, incremental prints). The events are still accumulated into the
|
|
52
|
+
returned `Execution`, so the return value is identical whether or not you stream:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
execution = sbx.run_code(
|
|
56
|
+
"for i in range(5):\n print('step', i)\n import time; time.sleep(1)",
|
|
57
|
+
on_stdout=lambda t: print(t, end="", flush=True), # arrives chunk-by-chunk
|
|
58
|
+
on_result=lambda r: render(r), # display() calls, figures, the value
|
|
59
|
+
on_error=lambda e: print(e.value),
|
|
60
|
+
)
|
|
61
|
+
print(execution.execution_ms) # still get the full Execution
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Result model
|
|
65
|
+
|
|
66
|
+
`run_code()` returns an `Execution`:
|
|
67
|
+
|
|
68
|
+
| Field | Type | Notes |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `results` | `list[Result]` | rich outputs; the cell's value has `is_main_result=True` |
|
|
71
|
+
| `logs` | `Logs` | `.stdout` / `.stderr` (lists of str) |
|
|
72
|
+
| `error` | `ExecutionError \| None` | `.name`, `.value`, `.traceback` |
|
|
73
|
+
| `text` | `str \| None` | convenience: the main result's text |
|
|
74
|
+
|
|
75
|
+
Each `Result` may carry any of: `text`, `html`, `markdown`, `svg`, `png`,
|
|
76
|
+
`jpeg`, `pdf`, `latex`, `json`, `javascript`. Call `r.formats()` for what's
|
|
77
|
+
present.
|
|
78
|
+
|
|
79
|
+
## Local dev
|
|
80
|
+
|
|
81
|
+
Point at a local engine (default `http://localhost:4000`):
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
with Sandbox() as sbx: # talks to localhost:4000
|
|
85
|
+
print(sbx.run_code("print(1 + 1)").logs.stdout)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
© 2026 AI2IN.dev — see the repository LICENSE.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ai2in"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for AI2IN — secure AI code sandboxes, hosted in India."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "AI2IN.dev" }]
|
|
13
|
+
keywords = ["ai", "agents", "sandbox", "code-interpreter", "india", "e2b"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
|
+
]
|
|
22
|
+
# Stdlib-only — zero third-party dependencies.
|
|
23
|
+
dependencies = []
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://ai2in.dev"
|
|
27
|
+
Repository = "https://github.com/AI2IN-DEV/AI2IN"
|
|
28
|
+
Documentation = "https://ai2in.dev"
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
packages = ["src/ai2in"]
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI2IN Python SDK — the developer-facing client for sandboxes.
|
|
3
|
+
|
|
4
|
+
from ai2in import Sandbox
|
|
5
|
+
|
|
6
|
+
with Sandbox(api_key="ai2in_live_…") as sbx:
|
|
7
|
+
execution = sbx.run_code("import pandas as pd; pd.DataFrame({'x': [1, 2]})")
|
|
8
|
+
print(execution.text) # the main result's text
|
|
9
|
+
for r in execution.results: # rich, typed results (html, png, …)
|
|
10
|
+
if r.png:
|
|
11
|
+
...
|
|
12
|
+
|
|
13
|
+
The code-interpreter result model mirrors E2B's: each cell yields `results`
|
|
14
|
+
(any of text/html/markdown/svg/png/jpeg/latex/json), structured `logs`, and a
|
|
15
|
+
structured `error`. Talks to the AI2IN Sandbox Service (default
|
|
16
|
+
http://localhost:4000; pass base_url="https://api.ai2in.dev" in production).
|
|
17
|
+
Pass api_key=… for a bearer token when auth is on.
|
|
18
|
+
|
|
19
|
+
Stdlib-only — no third-party dependencies.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import urllib.request
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from typing import Any, Callable, Optional
|
|
27
|
+
|
|
28
|
+
__version__ = "0.1.0"
|
|
29
|
+
__all__ = ["Sandbox", "Execution", "Result", "Logs", "ExecutionError", "CommandHandle", "Watcher"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Result:
|
|
34
|
+
"""One rich output. Any MIME field may be set; is_main_result marks the
|
|
35
|
+
cell's return value."""
|
|
36
|
+
text: Optional[str] = None
|
|
37
|
+
html: Optional[str] = None
|
|
38
|
+
markdown: Optional[str] = None
|
|
39
|
+
svg: Optional[str] = None
|
|
40
|
+
png: Optional[str] = None # base64
|
|
41
|
+
jpeg: Optional[str] = None # base64
|
|
42
|
+
pdf: Optional[str] = None # base64
|
|
43
|
+
latex: Optional[str] = None
|
|
44
|
+
json: Optional[Any] = None
|
|
45
|
+
javascript: Optional[str] = None
|
|
46
|
+
is_main_result: bool = False
|
|
47
|
+
|
|
48
|
+
def formats(self) -> list[str]:
|
|
49
|
+
return [k for k in ("text", "html", "markdown", "svg", "png", "jpeg",
|
|
50
|
+
"pdf", "latex", "json", "javascript")
|
|
51
|
+
if getattr(self, k) is not None]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class Logs:
|
|
56
|
+
stdout: list[str] = field(default_factory=list)
|
|
57
|
+
stderr: list[str] = field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class ExecutionError:
|
|
62
|
+
name: str
|
|
63
|
+
value: str
|
|
64
|
+
traceback: str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class Execution:
|
|
69
|
+
results: list[Result] = field(default_factory=list)
|
|
70
|
+
logs: Logs = field(default_factory=Logs)
|
|
71
|
+
error: Optional[ExecutionError] = None
|
|
72
|
+
execution_ms: Optional[int] = None
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def text(self) -> Optional[str]:
|
|
76
|
+
for r in self.results:
|
|
77
|
+
if r.is_main_result:
|
|
78
|
+
return r.text
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _result_from(item: dict) -> Result:
|
|
83
|
+
return Result(
|
|
84
|
+
text=item.get("text"), html=item.get("html"), markdown=item.get("markdown"),
|
|
85
|
+
svg=item.get("svg"), png=item.get("png"), jpeg=item.get("jpeg"),
|
|
86
|
+
pdf=item.get("pdf"), latex=item.get("latex"), json=item.get("json"),
|
|
87
|
+
javascript=item.get("javascript"), is_main_result=bool(item.get("is_main_result")),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _parse_execution(r: dict) -> Execution:
|
|
92
|
+
results = [_result_from(item) for item in (r.get("results") or [])]
|
|
93
|
+
logs_in = r.get("logs") or {}
|
|
94
|
+
logs = Logs(stdout=list(logs_in.get("stdout") or []), stderr=list(logs_in.get("stderr") or []))
|
|
95
|
+
err = r.get("error")
|
|
96
|
+
error = ExecutionError(err["name"], err.get("value", ""), err.get("traceback", "")) if err else None
|
|
97
|
+
return Execution(results=results, logs=logs, error=error, execution_ms=r.get("execution_ms"))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class CommandHandle:
|
|
101
|
+
"""Handle to a background command — poll ``logs()`` or ``kill()`` it."""
|
|
102
|
+
|
|
103
|
+
def __init__(self, sandbox: "Sandbox", command_id: str, pid: int):
|
|
104
|
+
self._sb = sandbox
|
|
105
|
+
self.id = command_id
|
|
106
|
+
self.pid = pid
|
|
107
|
+
|
|
108
|
+
def logs(self) -> dict:
|
|
109
|
+
"""Current buffered output + status: {stdout, stderr, status, exit_code}."""
|
|
110
|
+
return self._sb._req("POST", f"/v1/sandboxes/{self._sb.id}/commands/{self.id}/logs")
|
|
111
|
+
|
|
112
|
+
def kill(self) -> dict:
|
|
113
|
+
"""Terminate the command (SIGTERM to its process group)."""
|
|
114
|
+
return self._sb._req("POST", f"/v1/sandboxes/{self._sb.id}/commands/{self.id}/kill")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class _Commands:
|
|
118
|
+
"""Long-running commands: ``run`` blocks; ``start`` backgrounds and returns a
|
|
119
|
+
handle. Pair ``start`` with ``get_host`` to run a server and preview it."""
|
|
120
|
+
|
|
121
|
+
def __init__(self, sandbox: "Sandbox"):
|
|
122
|
+
self._sb = sandbox
|
|
123
|
+
|
|
124
|
+
def run(self, cmd: str) -> dict:
|
|
125
|
+
r = self._sb._req("POST", f"/v1/sandboxes/{self._sb.id}/shell", {"cmd": cmd})
|
|
126
|
+
return {"stdout": r.get("stdout", ""), "stderr": r.get("stderr", ""), "exit_code": r.get("exit_code", 0)}
|
|
127
|
+
|
|
128
|
+
def start(self, cmd: str) -> CommandHandle:
|
|
129
|
+
r = self._sb._req("POST", f"/v1/sandboxes/{self._sb.id}/commands", {"cmd": cmd})
|
|
130
|
+
return CommandHandle(self._sb, r["command_id"], r["pid"])
|
|
131
|
+
|
|
132
|
+
def list(self) -> list:
|
|
133
|
+
return self._sb._req("GET", f"/v1/sandboxes/{self._sb.id}/commands").get("commands", [])
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class Watcher:
|
|
137
|
+
"""A filesystem watcher. ``poll()`` returns events since the last poll
|
|
138
|
+
(create/modify/remove); ``stop()`` ends it."""
|
|
139
|
+
|
|
140
|
+
def __init__(self, sandbox: "Sandbox", watch_id: str):
|
|
141
|
+
self._sb = sandbox
|
|
142
|
+
self.id = watch_id
|
|
143
|
+
|
|
144
|
+
def poll(self) -> list:
|
|
145
|
+
return self._sb._req("POST", f"/v1/sandboxes/{self._sb.id}/fs/watch/{self.id}/poll").get("events", [])
|
|
146
|
+
|
|
147
|
+
def stop(self) -> dict:
|
|
148
|
+
return self._sb._req("POST", f"/v1/sandboxes/{self._sb.id}/fs/watch/{self.id}/stop")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class _Files:
|
|
152
|
+
"""Filesystem access inside the sandbox (rooted at the workspace)."""
|
|
153
|
+
|
|
154
|
+
def __init__(self, sandbox: "Sandbox"):
|
|
155
|
+
self._sb = sandbox
|
|
156
|
+
|
|
157
|
+
def list(self, path: str = ".") -> list:
|
|
158
|
+
return self._sb._req("POST", f"/v1/sandboxes/{self._sb.id}/fs/list", {"path": path}).get("entries", [])
|
|
159
|
+
|
|
160
|
+
def read(self, path: str) -> str:
|
|
161
|
+
return self._sb._req("POST", f"/v1/sandboxes/{self._sb.id}/fs/read", {"path": path}).get("content", "")
|
|
162
|
+
|
|
163
|
+
def write(self, path: str, content: str) -> dict:
|
|
164
|
+
return self._sb._req("POST", f"/v1/sandboxes/{self._sb.id}/fs/write", {"path": path, "content": content})
|
|
165
|
+
|
|
166
|
+
def watch(self, path: str = ".", recursive: bool = True) -> Watcher:
|
|
167
|
+
r = self._sb._req("POST", f"/v1/sandboxes/{self._sb.id}/fs/watch", {"path": path, "recursive": recursive})
|
|
168
|
+
return Watcher(self._sb, r["watch_id"])
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class Sandbox:
|
|
172
|
+
"""A live, isolated sandbox. Created on construction; kill() (or the context
|
|
173
|
+
manager) tears it down."""
|
|
174
|
+
|
|
175
|
+
def __init__(self, base_url: str = "http://localhost:4000", api_key: Optional[str] = None,
|
|
176
|
+
region: str = "ap-south-1"):
|
|
177
|
+
self.base_url = base_url.rstrip("/")
|
|
178
|
+
self.api_key = api_key
|
|
179
|
+
self.region = region
|
|
180
|
+
self.id: Optional[str] = None
|
|
181
|
+
self.commands = _Commands(self)
|
|
182
|
+
self.files = _Files(self)
|
|
183
|
+
self._create()
|
|
184
|
+
|
|
185
|
+
def _req(self, method: str, path: str, body: Optional[dict] = None) -> dict:
|
|
186
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
187
|
+
req = urllib.request.Request(self.base_url + path, data=data, method=method)
|
|
188
|
+
req.add_header("Content-Type", "application/json")
|
|
189
|
+
if self.api_key:
|
|
190
|
+
req.add_header("Authorization", f"Bearer {self.api_key}")
|
|
191
|
+
with urllib.request.urlopen(req) as resp:
|
|
192
|
+
return json.loads(resp.read().decode())
|
|
193
|
+
|
|
194
|
+
def _create(self) -> None:
|
|
195
|
+
self.id = self._req("POST", "/v1/sandboxes")["id"]
|
|
196
|
+
|
|
197
|
+
def run_code(
|
|
198
|
+
self,
|
|
199
|
+
code: str,
|
|
200
|
+
on_stdout: Optional[Callable[[str], None]] = None,
|
|
201
|
+
on_stderr: Optional[Callable[[str], None]] = None,
|
|
202
|
+
on_result: Optional[Callable[[Result], None]] = None,
|
|
203
|
+
on_error: Optional[Callable[[ExecutionError], None]] = None,
|
|
204
|
+
) -> Execution:
|
|
205
|
+
"""Run a Python cell in the sandbox's stateful kernel; return the rich
|
|
206
|
+
Execution (results / logs / error).
|
|
207
|
+
|
|
208
|
+
Pass any of on_stdout/on_stderr/on_result/on_error to stream output live
|
|
209
|
+
as it's produced. The events are still accumulated into the returned
|
|
210
|
+
Execution, so the return value is identical whether or not you stream.
|
|
211
|
+
"""
|
|
212
|
+
if not (on_stdout or on_stderr or on_result or on_error):
|
|
213
|
+
return _parse_execution(self._req("POST", f"/v1/sandboxes/{self.id}/run", {"code": code}))
|
|
214
|
+
return self._run_stream(code, on_stdout, on_stderr, on_result, on_error)
|
|
215
|
+
|
|
216
|
+
def _run_stream(self, code, on_stdout, on_stderr, on_result, on_error) -> Execution:
|
|
217
|
+
data = json.dumps({"code": code}).encode()
|
|
218
|
+
req = urllib.request.Request(
|
|
219
|
+
self.base_url + f"/v1/sandboxes/{self.id}/run/stream", data=data, method="POST")
|
|
220
|
+
req.add_header("Content-Type", "application/json")
|
|
221
|
+
if self.api_key:
|
|
222
|
+
req.add_header("Authorization", f"Bearer {self.api_key}")
|
|
223
|
+
|
|
224
|
+
results: list[Result] = []
|
|
225
|
+
logs = Logs()
|
|
226
|
+
error: Optional[ExecutionError] = None
|
|
227
|
+
execution_ms: Optional[int] = None
|
|
228
|
+
|
|
229
|
+
with urllib.request.urlopen(req) as resp:
|
|
230
|
+
buf = ""
|
|
231
|
+
for raw in resp: # iterate the SSE stream line-by-line, live
|
|
232
|
+
buf += raw.decode("utf-8")
|
|
233
|
+
while "\n\n" in buf:
|
|
234
|
+
frame, buf = buf.split("\n\n", 1)
|
|
235
|
+
line = frame.strip()
|
|
236
|
+
if line.startswith("data:"):
|
|
237
|
+
line = line[5:].strip()
|
|
238
|
+
if not line:
|
|
239
|
+
continue
|
|
240
|
+
ev = json.loads(line)
|
|
241
|
+
kind = ev.get("type")
|
|
242
|
+
if kind == "stdout":
|
|
243
|
+
logs.stdout.append(ev["text"])
|
|
244
|
+
if on_stdout:
|
|
245
|
+
on_stdout(ev["text"])
|
|
246
|
+
elif kind == "stderr":
|
|
247
|
+
logs.stderr.append(ev["text"])
|
|
248
|
+
if on_stderr:
|
|
249
|
+
on_stderr(ev["text"])
|
|
250
|
+
elif kind == "result":
|
|
251
|
+
r = _result_from(ev["result"])
|
|
252
|
+
results.append(r)
|
|
253
|
+
if on_result:
|
|
254
|
+
on_result(r)
|
|
255
|
+
elif kind == "error":
|
|
256
|
+
e = ev["error"]
|
|
257
|
+
error = ExecutionError(e["name"], e.get("value", ""), e.get("traceback", ""))
|
|
258
|
+
if on_error:
|
|
259
|
+
on_error(error)
|
|
260
|
+
elif kind == "end":
|
|
261
|
+
execution_ms = ev.get("execution_ms")
|
|
262
|
+
|
|
263
|
+
return Execution(results=results, logs=logs, error=error, execution_ms=execution_ms)
|
|
264
|
+
|
|
265
|
+
def pause(self) -> dict:
|
|
266
|
+
"""Pause the sandbox: its container is stopped (no CPU/RAM) while the
|
|
267
|
+
full filesystem is preserved, so a long-running agent session can be
|
|
268
|
+
parked cheaply and ``resume()``-d later. Returns the sandbox view."""
|
|
269
|
+
return self._req("POST", f"/v1/sandboxes/{self.id}/pause")
|
|
270
|
+
|
|
271
|
+
def resume(self) -> dict:
|
|
272
|
+
"""Resume a paused sandbox: the container is started again (fresh memory,
|
|
273
|
+
the filesystem exactly as left) and reopened for work."""
|
|
274
|
+
return self._req("POST", f"/v1/sandboxes/{self.id}/resume")
|
|
275
|
+
|
|
276
|
+
def get_metrics(self) -> dict:
|
|
277
|
+
"""Live resource usage: cpu_pct, mem_used_mb, mem_pct, pids, net_*,
|
|
278
|
+
uptime_s (E2B ``get_metrics`` parity)."""
|
|
279
|
+
return self._req("GET", f"/v1/sandboxes/{self.id}/metrics")
|
|
280
|
+
|
|
281
|
+
def get_host(self, port: int) -> str:
|
|
282
|
+
"""Expose a port the sandbox binds internally (e.g. a dev server on
|
|
283
|
+
3000) at a public HTTPS URL and return its host (E2B ``get_host``
|
|
284
|
+
parity). Build the URL as ``f"https://{sbx.get_host(3000)}"``."""
|
|
285
|
+
return self._req("POST", f"/v1/sandboxes/{self.id}/expose", {"port": port})["host"]
|
|
286
|
+
|
|
287
|
+
def kill(self) -> None:
|
|
288
|
+
if self.id:
|
|
289
|
+
self._req("DELETE", f"/v1/sandboxes/{self.id}")
|
|
290
|
+
self.id = None
|
|
291
|
+
|
|
292
|
+
def __enter__(self) -> "Sandbox":
|
|
293
|
+
return self
|
|
294
|
+
|
|
295
|
+
def __exit__(self, *exc) -> None:
|
|
296
|
+
self.kill()
|