Skip to content

Commit 255e7f8

Browse files
author
Roy Lin
committed
feat(sdk): add Python and TypeScript E2B clients
1 parent ca7e26b commit 255e7f8

17 files changed

Lines changed: 846 additions & 4 deletions

File tree

.github/workflows/ci.yml

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,39 @@ jobs:
111111
python3 compat/e2b/fixtures/official-clients/run_fixtures.py verify
112112
--rust-server-bin src/target/debug/a3s-box-e2b-fixture-server
113113
114+
# ── Native Python and TypeScript SDK package gate ─────────────
115+
sdk-packages:
116+
name: SDK Packages
117+
runs-on: ubuntu-latest
118+
steps:
119+
- uses: actions/checkout@v5
120+
- uses: actions/setup-python@v6
121+
with:
122+
python-version: '3.12'
123+
- uses: actions/setup-node@v7
124+
with:
125+
node-version: '20'
126+
cache: npm
127+
cache-dependency-path: sdk/typescript/package-lock.json
128+
- name: Build and test Python package
129+
run: |
130+
python -m pip install build
131+
python -m build sdk/python
132+
python -m venv /tmp/a3s-box-python-sdk
133+
/tmp/a3s-box-python-sdk/bin/pip install sdk/python/dist/*.whl
134+
/tmp/a3s-box-python-sdk/bin/python -m unittest discover -s sdk/python/tests
135+
- name: Build and test TypeScript package
136+
working-directory: sdk/typescript
137+
run: |
138+
npm ci
139+
npm run build
140+
npm test
141+
npm pack --dry-run
142+
114143
# ── Build check (compile only, no artifacts) ────────────────────
115144
build-check:
116145
name: Build Check (${{ matrix.target }})
117-
needs: [fmt, clippy, test, e2b-contract, e2b-official-clients]
146+
needs: [fmt, clippy, test, e2b-contract, e2b-official-clients, sdk-packages]
118147
strategy:
119148
matrix:
120149
include:
@@ -160,7 +189,7 @@ jobs:
160189
# ── Windows native WHPX build check ────────────────────────────
161190
build-windows:
162191
name: Build Windows WHPX
163-
needs: [fmt, clippy, test, e2b-contract, e2b-official-clients]
192+
needs: [fmt, clippy, test, e2b-contract, e2b-official-clients, sdk-packages]
164193
runs-on: windows-latest
165194
steps:
166195
- uses: actions/checkout@v5
@@ -202,7 +231,7 @@ jobs:
202231
# Until both are done this job is SKIPPED (inert) and never blocks a PR.
203232
integration-kvm:
204233
name: Integration (real microVM, KVM)
205-
needs: [fmt, clippy, test, e2b-contract, e2b-official-clients]
234+
needs: [fmt, clippy, test, e2b-contract, e2b-official-clients, sdk-packages]
206235
if: vars.KVM_CI == 'true'
207236
runs-on: [self-hosted, linux, kvm]
208237
timeout-minutes: 75

.github/workflows/release.yml

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,41 @@ jobs:
3737
workspaces: src
3838
- run: cd src && cargo test --workspace --lib
3939

40+
# ── Build native SDK release assets ───────────────────────────
41+
sdk:
42+
name: Build SDK packages
43+
needs: test
44+
runs-on: ubuntu-latest
45+
steps:
46+
- uses: actions/checkout@v5
47+
- uses: actions/setup-python@v6
48+
with:
49+
python-version: '3.12'
50+
- uses: actions/setup-node@v7
51+
with:
52+
node-version: '20'
53+
cache: npm
54+
cache-dependency-path: sdk/typescript/package-lock.json
55+
- name: Build and verify Python package
56+
run: |
57+
mkdir -p sdk-artifacts
58+
python -m pip install build
59+
python -m build sdk/python --outdir "$GITHUB_WORKSPACE/sdk-artifacts"
60+
python -m venv /tmp/a3s-box-python-sdk
61+
/tmp/a3s-box-python-sdk/bin/pip install sdk-artifacts/*.whl
62+
/tmp/a3s-box-python-sdk/bin/python -m unittest discover -s sdk/python/tests
63+
- name: Build and verify TypeScript package
64+
working-directory: sdk/typescript
65+
run: |
66+
npm ci
67+
npm run build
68+
npm test
69+
npm pack --pack-destination "$GITHUB_WORKSPACE/sdk-artifacts"
70+
- uses: actions/upload-artifact@v6
71+
with:
72+
name: sdk-packages
73+
path: sdk-artifacts/*
74+
4075
# ── Build matrix ───────────────────────────────────────────────
4176
build:
4277
name: Build (${{ matrix.target }})
@@ -171,7 +206,7 @@ jobs:
171206
# ── Create GitHub Release ──────────────────────────────────────
172207
release:
173208
name: GitHub Release
174-
needs: build
209+
needs: [build, sdk]
175210
runs-on: ubuntu-latest
176211
steps:
177212
- uses: actions/download-artifact@v7
@@ -188,6 +223,8 @@ jobs:
188223
generate_release_notes: true
189224
files: |
190225
artifacts/**/*.tar.gz
226+
artifacts/**/*.whl
227+
artifacts/**/*.tgz
191228
192229
# ── Publish to crates.io ───────────────────────────────────────
193230
publish-crates:

sdk/python/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# A3S Box Python SDK
2+
3+
`a3s-box` is a typed convenience package around the checksum-pinned official
4+
E2B Python clients used by A3S Box compatibility tests. It re-exports the
5+
official `e2b` 2.32.0 API instead of maintaining a fork, so existing E2B code
6+
can keep the same classes and method signatures.
7+
8+
```python
9+
from a3s_box import A3SConnectionConfig, Sandbox
10+
11+
connection = A3SConnectionConfig.from_environment()
12+
13+
with Sandbox.create(
14+
"code-interpreter-v1",
15+
**connection.python_options(),
16+
) as sandbox:
17+
result = sandbox.commands.run("python -c 'print(6 * 7)'")
18+
print(result.stdout)
19+
```
20+
21+
The synchronous and asynchronous Code Interpreter exports are available from
22+
`a3s_box.code_interpreter`.
23+
24+
`A3SConnectionConfig` reads `E2B_API_URL`, `E2B_DOMAIN`, and `E2B_API_KEY`
25+
without changing process-global environment variables. The A3S endpoint still
26+
decides the execution template and isolation policy; the SDK never invokes a
27+
local runtime.

sdk/python/pyproject.toml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[build-system]
2+
requires = ["setuptools>=77,<81"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "a3s-box"
7+
version = "0.1.0"
8+
description = "Typed Python convenience package for the A3S Box E2B-compatible endpoint"
9+
readme = "README.md"
10+
requires-python = ">=3.10"
11+
license = "Apache-2.0"
12+
authors = [{ name = "A3S Lab" }]
13+
classifiers = [
14+
"Development Status :: 3 - Alpha",
15+
"Programming Language :: Python :: 3",
16+
"Programming Language :: Python :: 3 :: Only",
17+
"Typing :: Typed",
18+
]
19+
dependencies = [
20+
"e2b==2.32.0",
21+
"e2b-code-interpreter==2.8.1",
22+
]
23+
24+
[project.urls]
25+
Homepage = "https://github.com/A3S-Lab/Box"
26+
Repository = "https://github.com/A3S-Lab/Box"
27+
Issues = "https://github.com/A3S-Lab/Box/issues"
28+
29+
[tool.setuptools.packages.find]
30+
where = ["src"]
31+
32+
[tool.setuptools.package-data]
33+
a3s_box = ["py.typed"]

sdk/python/src/a3s_box/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""A3S Box SDK with the official E2B Python surface re-exported unchanged."""
2+
3+
from e2b import * # noqa: F403
4+
from e2b import __all__ as _e2b_all
5+
6+
from .connection import A3SConnectionConfig
7+
8+
__all__ = [*_e2b_all, "A3SConnectionConfig"]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Official E2B Code Interpreter exports configured for A3S endpoints."""
2+
3+
from e2b_code_interpreter import * # noqa: F401,F403
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Typed A3S endpoint configuration helpers."""
2+
3+
from __future__ import annotations
4+
5+
import os
6+
from dataclasses import dataclass
7+
from typing import Mapping
8+
9+
10+
@dataclass(frozen=True, slots=True)
11+
class A3SConnectionConfig:
12+
"""Connection values accepted by the pinned official E2B Python SDK."""
13+
14+
api_url: str
15+
domain: str
16+
api_key: str | None = None
17+
18+
def __post_init__(self) -> None:
19+
if not self.api_url.strip():
20+
raise ValueError("api_url cannot be empty")
21+
if not self.domain.strip():
22+
raise ValueError("domain cannot be empty")
23+
if self.api_key is not None and not self.api_key.strip():
24+
raise ValueError("api_key cannot be empty when provided")
25+
26+
@classmethod
27+
def from_environment(
28+
cls,
29+
environment: Mapping[str, str] | None = None,
30+
) -> A3SConnectionConfig:
31+
"""Read standard E2B endpoint variables without mutating the process."""
32+
33+
values = os.environ if environment is None else environment
34+
api_url = values.get("E2B_API_URL")
35+
domain = values.get("E2B_DOMAIN")
36+
if not api_url:
37+
raise ValueError("E2B_API_URL is required")
38+
if not domain:
39+
raise ValueError("E2B_DOMAIN is required")
40+
return cls(
41+
api_url=api_url,
42+
domain=domain,
43+
api_key=values.get("E2B_API_KEY"),
44+
)
45+
46+
def python_options(self) -> dict[str, str]:
47+
"""Return keyword arguments for Python Sandbox create/connect calls."""
48+
49+
options = {"api_url": self.api_url, "domain": self.domain}
50+
if self.api_key is not None:
51+
options["api_key"] = self.api_key
52+
return options

sdk/python/src/a3s_box/py.typed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

sdk/python/tests/test_sdk.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from __future__ import annotations
2+
3+
import unittest
4+
5+
import e2b
6+
import e2b_code_interpreter
7+
8+
import a3s_box
9+
from a3s_box import A3SConnectionConfig
10+
from a3s_box import code_interpreter
11+
12+
13+
class SdkTests(unittest.TestCase):
14+
def test_reexports_pinned_official_clients(self) -> None:
15+
self.assertIs(a3s_box.Sandbox, e2b.Sandbox)
16+
self.assertIs(a3s_box.AsyncSandbox, e2b.AsyncSandbox)
17+
self.assertIs(code_interpreter.Sandbox, e2b_code_interpreter.Sandbox)
18+
self.assertIs(
19+
code_interpreter.AsyncSandbox,
20+
e2b_code_interpreter.AsyncSandbox,
21+
)
22+
23+
def test_connection_options_use_standard_e2b_names(self) -> None:
24+
connection = A3SConnectionConfig.from_environment(
25+
{
26+
"E2B_API_URL": "https://api.box.example.com",
27+
"E2B_DOMAIN": "box.example.com",
28+
"E2B_API_KEY": "e2b_a1b2c3",
29+
}
30+
)
31+
self.assertEqual(
32+
connection.python_options(),
33+
{
34+
"api_url": "https://api.box.example.com",
35+
"domain": "box.example.com",
36+
"api_key": "e2b_a1b2c3",
37+
},
38+
)
39+
40+
41+
if __name__ == "__main__":
42+
unittest.main()

sdk/typescript/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# A3S Box TypeScript SDK
2+
3+
`@a3s-lab/box` re-exports the official `e2b` 2.33.0 TypeScript API and the
4+
pinned `@e2b/code-interpreter` 2.6.1 package. It does not fork or translate the
5+
public protocol.
6+
7+
```typescript
8+
import { A3SConnectionConfig, Sandbox } from '@a3s-lab/box'
9+
10+
const connection = A3SConnectionConfig.fromEnvironment(process.env)
11+
const sandbox = await Sandbox.create('code-interpreter-v1', {
12+
...connection.typescriptOptions(),
13+
timeoutMs: 60_000,
14+
})
15+
16+
try {
17+
const result = await sandbox.commands.run('node -e "console.log(6 * 7)"')
18+
console.log(result.stdout)
19+
} finally {
20+
await sandbox.kill()
21+
}
22+
```
23+
24+
Code Interpreter exports are available from `@a3s-lab/box/code-interpreter`.
25+
The A3S service owns template and isolation selection; this package never
26+
starts a local container or runtime.

0 commit comments

Comments
 (0)