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.
@@ -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)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "runbox-sdk"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "Python client for Runbox, a lightweight sandbox for agent commands and files"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -5,4 +5,4 @@ from .errors import IdentityError, RunboxError
5
5
  from .identity import Identity
6
6
 
7
7
  __all__ = ["Client", "FileUpload", "Identity", "IdentityError", "RunboxError"]
8
- __version__ = "0.2.2"
8
+ __version__ = "0.2.3"