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 ADDED
@@ -0,0 +1,4 @@
1
+ dist/
2
+ *.egg-info/
3
+ __pycache__/
4
+ *.pyc
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()