runbox-sdk 0.2.2__tar.gz → 0.2.3__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.
- runbox_sdk-0.2.3/PKG-INFO +189 -0
- runbox_sdk-0.2.3/README.md +171 -0
- {runbox_sdk-0.2.2 → runbox_sdk-0.2.3}/pyproject.toml +1 -1
- {runbox_sdk-0.2.2 → runbox_sdk-0.2.3}/src/runbox/__init__.py +1 -1
- runbox_sdk-0.2.3/src/runbox/client.py +551 -0
- runbox_sdk-0.2.3/src/runbox_sdk.egg-info/PKG-INFO +189 -0
- {runbox_sdk-0.2.2 → runbox_sdk-0.2.3}/tests/test_client.py +89 -3
- runbox_sdk-0.2.2/PKG-INFO +0 -40
- runbox_sdk-0.2.2/README.md +0 -22
- runbox_sdk-0.2.2/src/runbox/client.py +0 -321
- runbox_sdk-0.2.2/src/runbox_sdk.egg-info/PKG-INFO +0 -40
- {runbox_sdk-0.2.2 → runbox_sdk-0.2.3}/LICENSE +0 -0
- {runbox_sdk-0.2.2 → runbox_sdk-0.2.3}/setup.cfg +0 -0
- {runbox_sdk-0.2.2 → runbox_sdk-0.2.3}/src/runbox/errors.py +0 -0
- {runbox_sdk-0.2.2 → runbox_sdk-0.2.3}/src/runbox/identity.py +0 -0
- {runbox_sdk-0.2.2 → runbox_sdk-0.2.3}/src/runbox/py.typed +0 -0
- {runbox_sdk-0.2.2 → runbox_sdk-0.2.3}/src/runbox_sdk.egg-info/SOURCES.txt +0 -0
- {runbox_sdk-0.2.2 → runbox_sdk-0.2.3}/src/runbox_sdk.egg-info/dependency_links.txt +0 -0
- {runbox_sdk-0.2.2 → runbox_sdk-0.2.3}/src/runbox_sdk.egg-info/top_level.txt +0 -0
- {runbox_sdk-0.2.2 → runbox_sdk-0.2.3}/tests/test_identity.py +0 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runbox-sdk
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: Python client for Runbox, a lightweight sandbox for agent commands and files
|
|
5
|
+
Author: Runbox contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://runbox.xyun.dev
|
|
8
|
+
Project-URL: Repository, https://github.com/xy200303/runbox
|
|
9
|
+
Project-URL: Documentation, https://github.com/xy200303/runbox/blob/main/docs/api.md
|
|
10
|
+
Keywords: runbox,sandbox,agent,code-execution
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# Runbox Python SDK
|
|
20
|
+
|
|
21
|
+
Python client for [Runbox](https://runbox.xyun.dev), the lightweight sandbox for
|
|
22
|
+
agent commands and files. Standard library only — no `requests`, no build step.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install runbox-sdk
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
import os
|
|
32
|
+
from runbox import Client, Identity
|
|
33
|
+
|
|
34
|
+
client = Client(
|
|
35
|
+
"http://localhost:8099",
|
|
36
|
+
api_key=os.environ["RUNBOX_API_KEY"],
|
|
37
|
+
identity=Identity(namespace="team-a", principal="user-42"),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
result = client.exec('python3 -c "print(6 * 7)"', timeout=30)
|
|
41
|
+
print(result["exit_code"], result["stdout"]) # 0 42
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The client is safe to reuse across threads: bind an identity once, or pass
|
|
45
|
+
`identity=` on any call.
|
|
46
|
+
|
|
47
|
+
## Identity
|
|
48
|
+
|
|
49
|
+
Exactly one identity form per request:
|
|
50
|
+
|
|
51
|
+
| Form | Constructor | Headers sent |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| Opaque token | `Identity(token="user-42")` | `X-Runbox-Identity` |
|
|
54
|
+
| Tenant + user | `Identity(namespace="team-a", principal="user-42")` | `X-Runbox-Namespace` + `X-Runbox-Principal` |
|
|
55
|
+
|
|
56
|
+
Supplying both forms, or half of the scoped form, returns `400` from the
|
|
57
|
+
runner.
|
|
58
|
+
|
|
59
|
+
## Run a command
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
result = client.exec(
|
|
63
|
+
"python3 analyze.py files/input.csv",
|
|
64
|
+
timeout=60,
|
|
65
|
+
files=[FileUpload(name="input.csv", path="input.csv")], # streamed from disk
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
with open("input.csv", "rb") as handle:
|
|
69
|
+
result = client.exec(
|
|
70
|
+
"cat files/input.csv",
|
|
71
|
+
files=[FileUpload(name="input.csv", stream=handle)], # streamed, never closed
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Attachments land in `files/<name>` inside the workspace; the command refers to
|
|
76
|
+
them by that path. `exec` returns the parsed JSON response, including
|
|
77
|
+
`exit_code`, `stdout`, `stderr`, `files` and `duration_ms`.
|
|
78
|
+
|
|
79
|
+
## Files
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
uploaded = client.upload_file("report.xlsx") # -> {"path": "files/report.xlsx", ...}
|
|
83
|
+
client.upload("notes.txt", b"hello")
|
|
84
|
+
client.upload_batch([FileUpload(name="a.csv", path="a.csv"), FileUpload(name="b.csv", path="b.csv")])
|
|
85
|
+
entries = client.list("out")
|
|
86
|
+
client.delete_workspace() # keeps the identity and its UID
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Downloads can go to a path or to any file-like object:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
client.download_path("out/report.csv", "report.csv")
|
|
93
|
+
client.download_path_to("out/report.csv", sink) # sink: anything with .write()
|
|
94
|
+
client.download_archive(["out", "tmp"], "artifacts.tar")
|
|
95
|
+
client.download_archive_to(["out"], sink)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Streaming and forwarding
|
|
99
|
+
|
|
100
|
+
`open_path` returns the raw `http.client.HTTPResponse`, so nothing is buffered.
|
|
101
|
+
`iter_path` yields chunks, which is what a framework response wants:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from fastapi import FastAPI
|
|
105
|
+
from fastapi.responses import StreamingResponse
|
|
106
|
+
|
|
107
|
+
app = FastAPI()
|
|
108
|
+
|
|
109
|
+
@app.get("/files")
|
|
110
|
+
def download(path: str):
|
|
111
|
+
return StreamingResponse(client.iter_path(path), media_type="application/octet-stream")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Forwarding a byte range (video scrubbing, resumable downloads) works the same
|
|
115
|
+
way; the runner answers with `206` and `Content-Range`, and the SDK passes the
|
|
116
|
+
body straight through:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
@app.get("/files")
|
|
120
|
+
def download(path: str, range: str | None = None):
|
|
121
|
+
return StreamingResponse(client.iter_path(path, range_header=range))
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
A whole directory as one tar stream:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
@app.get("/archive")
|
|
128
|
+
def archive():
|
|
129
|
+
return StreamingResponse(
|
|
130
|
+
client.iter_archive(["out", "tmp/step2"]),
|
|
131
|
+
media_type="application/x-tar",
|
|
132
|
+
headers={"Content-Disposition": 'attachment; filename="artifacts.tar"'},
|
|
133
|
+
)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
If you need the response itself — headers, status, manual chunking — use
|
|
137
|
+
`open_path` / `open_archive` and close what you get back:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
response = client.open_path("out/report.pdf", range_header="bytes=0-1023")
|
|
141
|
+
try:
|
|
142
|
+
print(response.status, response.getheader("Content-Length"))
|
|
143
|
+
for chunk in iter(lambda: response.read(65536), b""):
|
|
144
|
+
handle(chunk)
|
|
145
|
+
finally:
|
|
146
|
+
response.close()
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Streaming stops when the client disconnects: the generator's `finally` closes
|
|
150
|
+
the response, and the runner notices the closed connection.
|
|
151
|
+
|
|
152
|
+
## Health and limits
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
client.health()
|
|
156
|
+
client.ready()
|
|
157
|
+
client.capabilities() # limits and detected tooling
|
|
158
|
+
client.wait_ready(timeout=30)
|
|
159
|
+
client.metrics()
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Errors
|
|
163
|
+
|
|
164
|
+
Every non-2xx response raises `RunboxError` with `status`, `code` and
|
|
165
|
+
`message`:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from runbox import RunboxError
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
client.exec("rm -rf out && echo gone")
|
|
172
|
+
except RunboxError as error:
|
|
173
|
+
if error.code in ("busy", "quota_exceeded"):
|
|
174
|
+
... # retry later, or clean the workspace
|
|
175
|
+
elif error.code == "timeout":
|
|
176
|
+
... # raise the timeout
|
|
177
|
+
else:
|
|
178
|
+
raise
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Common codes: `400` bad identity or path, `401` bad bearer token, `404` path
|
|
182
|
+
missing or owned by another identity, `409` `busy` / `quota_exceeded`, `413`
|
|
183
|
+
request too large.
|
|
184
|
+
|
|
185
|
+
## Links
|
|
186
|
+
|
|
187
|
+
- API reference: [docs/api.md](../../docs/api.md)
|
|
188
|
+
- Deployment and hardening: [docs/deployment.md](../../docs/deployment.md)
|
|
189
|
+
- Changelog: [CHANGELOG.md](../../CHANGELOG.md)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Runbox Python SDK
|
|
2
|
+
|
|
3
|
+
Python client for [Runbox](https://runbox.xyun.dev), the lightweight sandbox for
|
|
4
|
+
agent commands and files. Standard library only — no `requests`, no build step.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install runbox-sdk
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Quick start
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
import os
|
|
14
|
+
from runbox import Client, Identity
|
|
15
|
+
|
|
16
|
+
client = Client(
|
|
17
|
+
"http://localhost:8099",
|
|
18
|
+
api_key=os.environ["RUNBOX_API_KEY"],
|
|
19
|
+
identity=Identity(namespace="team-a", principal="user-42"),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
result = client.exec('python3 -c "print(6 * 7)"', timeout=30)
|
|
23
|
+
print(result["exit_code"], result["stdout"]) # 0 42
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The client is safe to reuse across threads: bind an identity once, or pass
|
|
27
|
+
`identity=` on any call.
|
|
28
|
+
|
|
29
|
+
## Identity
|
|
30
|
+
|
|
31
|
+
Exactly one identity form per request:
|
|
32
|
+
|
|
33
|
+
| Form | Constructor | Headers sent |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| Opaque token | `Identity(token="user-42")` | `X-Runbox-Identity` |
|
|
36
|
+
| Tenant + user | `Identity(namespace="team-a", principal="user-42")` | `X-Runbox-Namespace` + `X-Runbox-Principal` |
|
|
37
|
+
|
|
38
|
+
Supplying both forms, or half of the scoped form, returns `400` from the
|
|
39
|
+
runner.
|
|
40
|
+
|
|
41
|
+
## Run a command
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
result = client.exec(
|
|
45
|
+
"python3 analyze.py files/input.csv",
|
|
46
|
+
timeout=60,
|
|
47
|
+
files=[FileUpload(name="input.csv", path="input.csv")], # streamed from disk
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
with open("input.csv", "rb") as handle:
|
|
51
|
+
result = client.exec(
|
|
52
|
+
"cat files/input.csv",
|
|
53
|
+
files=[FileUpload(name="input.csv", stream=handle)], # streamed, never closed
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Attachments land in `files/<name>` inside the workspace; the command refers to
|
|
58
|
+
them by that path. `exec` returns the parsed JSON response, including
|
|
59
|
+
`exit_code`, `stdout`, `stderr`, `files` and `duration_ms`.
|
|
60
|
+
|
|
61
|
+
## Files
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
uploaded = client.upload_file("report.xlsx") # -> {"path": "files/report.xlsx", ...}
|
|
65
|
+
client.upload("notes.txt", b"hello")
|
|
66
|
+
client.upload_batch([FileUpload(name="a.csv", path="a.csv"), FileUpload(name="b.csv", path="b.csv")])
|
|
67
|
+
entries = client.list("out")
|
|
68
|
+
client.delete_workspace() # keeps the identity and its UID
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Downloads can go to a path or to any file-like object:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
client.download_path("out/report.csv", "report.csv")
|
|
75
|
+
client.download_path_to("out/report.csv", sink) # sink: anything with .write()
|
|
76
|
+
client.download_archive(["out", "tmp"], "artifacts.tar")
|
|
77
|
+
client.download_archive_to(["out"], sink)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Streaming and forwarding
|
|
81
|
+
|
|
82
|
+
`open_path` returns the raw `http.client.HTTPResponse`, so nothing is buffered.
|
|
83
|
+
`iter_path` yields chunks, which is what a framework response wants:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from fastapi import FastAPI
|
|
87
|
+
from fastapi.responses import StreamingResponse
|
|
88
|
+
|
|
89
|
+
app = FastAPI()
|
|
90
|
+
|
|
91
|
+
@app.get("/files")
|
|
92
|
+
def download(path: str):
|
|
93
|
+
return StreamingResponse(client.iter_path(path), media_type="application/octet-stream")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Forwarding a byte range (video scrubbing, resumable downloads) works the same
|
|
97
|
+
way; the runner answers with `206` and `Content-Range`, and the SDK passes the
|
|
98
|
+
body straight through:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
@app.get("/files")
|
|
102
|
+
def download(path: str, range: str | None = None):
|
|
103
|
+
return StreamingResponse(client.iter_path(path, range_header=range))
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
A whole directory as one tar stream:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
@app.get("/archive")
|
|
110
|
+
def archive():
|
|
111
|
+
return StreamingResponse(
|
|
112
|
+
client.iter_archive(["out", "tmp/step2"]),
|
|
113
|
+
media_type="application/x-tar",
|
|
114
|
+
headers={"Content-Disposition": 'attachment; filename="artifacts.tar"'},
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
If you need the response itself — headers, status, manual chunking — use
|
|
119
|
+
`open_path` / `open_archive` and close what you get back:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
response = client.open_path("out/report.pdf", range_header="bytes=0-1023")
|
|
123
|
+
try:
|
|
124
|
+
print(response.status, response.getheader("Content-Length"))
|
|
125
|
+
for chunk in iter(lambda: response.read(65536), b""):
|
|
126
|
+
handle(chunk)
|
|
127
|
+
finally:
|
|
128
|
+
response.close()
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Streaming stops when the client disconnects: the generator's `finally` closes
|
|
132
|
+
the response, and the runner notices the closed connection.
|
|
133
|
+
|
|
134
|
+
## Health and limits
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
client.health()
|
|
138
|
+
client.ready()
|
|
139
|
+
client.capabilities() # limits and detected tooling
|
|
140
|
+
client.wait_ready(timeout=30)
|
|
141
|
+
client.metrics()
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Errors
|
|
145
|
+
|
|
146
|
+
Every non-2xx response raises `RunboxError` with `status`, `code` and
|
|
147
|
+
`message`:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from runbox import RunboxError
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
client.exec("rm -rf out && echo gone")
|
|
154
|
+
except RunboxError as error:
|
|
155
|
+
if error.code in ("busy", "quota_exceeded"):
|
|
156
|
+
... # retry later, or clean the workspace
|
|
157
|
+
elif error.code == "timeout":
|
|
158
|
+
... # raise the timeout
|
|
159
|
+
else:
|
|
160
|
+
raise
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Common codes: `400` bad identity or path, `401` bad bearer token, `404` path
|
|
164
|
+
missing or owned by another identity, `409` `busy` / `quota_exceeded`, `413`
|
|
165
|
+
request too large.
|
|
166
|
+
|
|
167
|
+
## Links
|
|
168
|
+
|
|
169
|
+
- API reference: [docs/api.md](../../docs/api.md)
|
|
170
|
+
- Deployment and hardening: [docs/deployment.md](../../docs/deployment.md)
|
|
171
|
+
- Changelog: [CHANGELOG.md](../../CHANGELOG.md)
|