boxd 0.1.1.dev7__tar.gz → 0.1.2__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.
- {boxd-0.1.1.dev7/src/boxd.egg-info → boxd-0.1.2}/PKG-INFO +123 -23
- {boxd-0.1.1.dev7 → boxd-0.1.2}/README.md +122 -20
- {boxd-0.1.1.dev7 → boxd-0.1.2}/pyproject.toml +1 -3
- {boxd-0.1.1.dev7 → boxd-0.1.2/src/boxd.egg-info}/PKG-INFO +123 -23
- {boxd-0.1.1.dev7 → boxd-0.1.2}/LICENSE +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/setup.cfg +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/__init__.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/_generated/__init__.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/_generated/api_pb2.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/_generated/api_pb2_grpc.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/_sync.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/_utils.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/_version_check.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/aio.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/auth.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/box.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/boxes.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/client.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/disks.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/domains.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/errors.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/exec.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/networks.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/templates.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/tokens.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/types.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd.egg-info/SOURCES.txt +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd.egg-info/dependency_links.txt +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd.egg-info/requires.txt +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd.egg-info/top_level.txt +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/tests/test_auth.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/tests/test_boxes.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/tests/test_e2e.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/tests/test_e2e_v2.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/tests/test_exec.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/tests/test_files.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/tests/test_lifecycle.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/tests/test_proxies.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/tests/test_utils.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/tests/test_v2.py +0 -0
- {boxd-0.1.1.dev7 → boxd-0.1.2}/tests/test_version_check.py +0 -0
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: boxd
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Python SDK for the boxd cloud VM platform
|
|
5
5
|
Author: Azin
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://boxd.sh
|
|
8
|
-
Project-URL: Repository, https://github.com/azin-tech/boxd
|
|
9
|
-
Project-URL: Issues, https://github.com/azin-tech/boxd/issues
|
|
10
8
|
Keywords: boxd,vm,microvm,sandbox,compute,grpc,sdk
|
|
11
9
|
Classifier: Development Status :: 4 - Beta
|
|
12
10
|
Classifier: Intended Audience :: Developers
|
|
@@ -84,9 +82,17 @@ Compute(
|
|
|
84
82
|
)
|
|
85
83
|
```
|
|
86
84
|
|
|
87
|
-
|
|
85
|
+
### Environment variables
|
|
88
86
|
|
|
89
|
-
|
|
87
|
+
All `Compute` arguments can be supplied via env vars. Constructor args win over env vars; env vars win over the `environment` preset.
|
|
88
|
+
|
|
89
|
+
| Variable | Sets | Default |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `BOXD_API_KEY` | API key (long-lived, recommended) | — |
|
|
92
|
+
| `BOXD_TOKEN` | Direct JWT (short-lived) | — |
|
|
93
|
+
| `BOXD_ENVIRONMENT` | Preset name (`production` or `staging`) | `production` |
|
|
94
|
+
| `BOXD_API_URL` | gRPC endpoint, overrides preset | `http://boxd.sh:9443` |
|
|
95
|
+
| `BOXD_EXCHANGE_URL` | Token-exchange URL, overrides preset | `https://boxd.sh/api/v1/auth/token` |
|
|
90
96
|
|
|
91
97
|
`api_url` accepts an optional URL scheme that controls TLS:
|
|
92
98
|
|
|
@@ -114,7 +120,39 @@ s = box.suspend() # SuspendResult
|
|
|
114
120
|
r = box.resume() # ResumeResult
|
|
115
121
|
```
|
|
116
122
|
|
|
117
|
-
|
|
123
|
+
### Box fields
|
|
124
|
+
|
|
125
|
+
`Box` always carries server-returned fields, but which ones are populated depends on how it was obtained:
|
|
126
|
+
|
|
127
|
+
| Field | `create` | `fork` | `list` | `get` |
|
|
128
|
+
|---|---|---|---|---|
|
|
129
|
+
| `id`, `name`, `image`, `public_ip`, `status` | ✓ | ✓ | ✓ | ✓ |
|
|
130
|
+
| `url`, `boot_time_ms` | ✓ | ✓ | `None` | `None` |
|
|
131
|
+
| `forked_from` | `None` | ✓ | `None` | `None` |
|
|
132
|
+
| `restart_policy`, `disk_bytes`, `auto_suspend_timeout_secs` | `None` | `None` | `None` | ✓ |
|
|
133
|
+
|
|
134
|
+
If you need the URL or boot time after a `list` / `get` round-trip, the `https://<name>.boxd.sh` form is stable, or call `box.proxies()` for the full set. If you need the lifecycle fields off a `Box` from `list` / `create` / `fork`, re-fetch via `c.box.get(box.name)`.
|
|
135
|
+
|
|
136
|
+
### BoxConfig
|
|
137
|
+
|
|
138
|
+
`create`, `fork`, and `template.create_vm` all take an optional `config`:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from boxd import BoxConfig, LifecycleConfig
|
|
142
|
+
|
|
143
|
+
config = BoxConfig(
|
|
144
|
+
vcpu=2, # default 2
|
|
145
|
+
memory="4G", # default "8G"
|
|
146
|
+
env={"API_KEY": "secret"}, # env vars exposed to the VM
|
|
147
|
+
restart_policy="always", # "always" | "never"
|
|
148
|
+
lifecycle=LifecycleConfig(
|
|
149
|
+
auto_suspend_timeout=300, # idle network secs; 0 disables
|
|
150
|
+
auto_destroy_timeout=0, # total lifetime secs; 0 disables
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
box = c.box.create(name="my-vm", config=config)
|
|
155
|
+
```
|
|
118
156
|
|
|
119
157
|
## Exec
|
|
120
158
|
|
|
@@ -124,19 +162,25 @@ r = box.exec("python", "script.py")
|
|
|
124
162
|
r.stdout # str
|
|
125
163
|
r.stderr # str
|
|
126
164
|
r.exit_code # int
|
|
127
|
-
r.success # bool
|
|
165
|
+
r.success # bool — exit_code == 0
|
|
128
166
|
|
|
129
167
|
# With env vars and timeout
|
|
130
168
|
box.exec("sh", "-c", "echo $FOO", env={"FOO": "bar"}, timeout=30)
|
|
131
169
|
|
|
132
|
-
# Streaming
|
|
170
|
+
# Streaming — proc is an ExecProcess. Use iter_stdout / iter_stderr
|
|
171
|
+
# (sync generators that block until the next chunk arrives), then wait()
|
|
172
|
+
# for the exit code. close() force-terminates the stream.
|
|
133
173
|
proc = box.exec("tail", "-f", "/var/log/syslog", stream=True)
|
|
134
174
|
for chunk in proc.iter_stdout():
|
|
135
175
|
print(chunk.decode(), end="")
|
|
136
176
|
exit_code = proc.wait()
|
|
177
|
+
proc.close() # idempotent
|
|
137
178
|
|
|
138
|
-
# Interactive (PTY + stdin)
|
|
139
|
-
sh = box.exec("bash", interactive=True)
|
|
179
|
+
# Interactive (PTY + stdin) — write to proc.stdin, end with write_eof
|
|
180
|
+
sh = box.exec("bash", interactive=True) # interactive implies pty
|
|
181
|
+
sh.stdin.write(b"echo hello\n")
|
|
182
|
+
sh.stdin.write_eof()
|
|
183
|
+
print(sh.wait())
|
|
140
184
|
```
|
|
141
185
|
|
|
142
186
|
## Files
|
|
@@ -174,6 +218,8 @@ for chunk in box.stream_logs(follow=True):
|
|
|
174
218
|
|
|
175
219
|
## Templates
|
|
176
220
|
|
|
221
|
+
Reusable image + `BoxConfig` frozen together.
|
|
222
|
+
|
|
177
223
|
```python
|
|
178
224
|
from boxd import BoxConfig
|
|
179
225
|
|
|
@@ -183,7 +229,16 @@ t = c.template.create(
|
|
|
183
229
|
config=BoxConfig(vcpu=2, memory="4G"),
|
|
184
230
|
)
|
|
185
231
|
c.template.list()
|
|
232
|
+
|
|
233
|
+
# create_vm accepts a Template object OR a template ID string. Pass an
|
|
234
|
+
# optional `config` to override the template's defaults (e.g. bump
|
|
235
|
+
# memory for one specific VM).
|
|
186
236
|
box = c.template.create_vm(template=t, name="from-t")
|
|
237
|
+
big = c.template.create_vm(
|
|
238
|
+
template=t.id,
|
|
239
|
+
name="from-t-big",
|
|
240
|
+
config=BoxConfig(memory="16G"),
|
|
241
|
+
)
|
|
187
242
|
c.template.delete(t.id)
|
|
188
243
|
```
|
|
189
244
|
|
|
@@ -191,10 +246,18 @@ c.template.delete(t.id)
|
|
|
191
246
|
|
|
192
247
|
```python
|
|
193
248
|
d = c.disk.create("data", size="10G")
|
|
249
|
+
d.id; d.name; d.size_bytes; d.status
|
|
250
|
+
|
|
251
|
+
# attach / detach take a Box instance OR a name/id string
|
|
194
252
|
d.attach(box, mount_path="/mnt/data")
|
|
195
|
-
d.attach(
|
|
196
|
-
d.detach(
|
|
253
|
+
d.attach("my-vm", mount_path="/mnt/data", read_only=True)
|
|
254
|
+
d.detach("my-vm")
|
|
255
|
+
|
|
197
256
|
d.destroy()
|
|
257
|
+
|
|
258
|
+
# list returns DiskHandle instances — same methods as above
|
|
259
|
+
for d in c.disk.list():
|
|
260
|
+
print(d.name, d.status)
|
|
198
261
|
```
|
|
199
262
|
|
|
200
263
|
## Domains
|
|
@@ -212,8 +275,11 @@ c.domain.unbind("app.example.com")
|
|
|
212
275
|
## Networks
|
|
213
276
|
|
|
214
277
|
```python
|
|
215
|
-
n = c.network.create()
|
|
278
|
+
n = c.network.create() # server assigns id
|
|
216
279
|
named = c.network.create(name="staging")
|
|
280
|
+
|
|
281
|
+
# `create` returns the new network's id only — `subnet` and `status` come
|
|
282
|
+
# back populated once provisioning settles. Re-fetch via `list` to read them.
|
|
217
283
|
for net in c.network.list():
|
|
218
284
|
print(net.id, net.subnet, net.status)
|
|
219
285
|
```
|
|
@@ -223,13 +289,18 @@ for net in c.network.list():
|
|
|
223
289
|
Issue scoped JWTs for delegated access. The raw token string is only returned at creation — store it then.
|
|
224
290
|
|
|
225
291
|
```python
|
|
226
|
-
t = c.token.create(expires_in=3600)
|
|
227
|
-
t.token # "eyJ..."
|
|
228
|
-
t.expires_at # unix seconds
|
|
292
|
+
t = c.token.create(expires_in=3600) # 0 = server default
|
|
293
|
+
t.token # str — "eyJ..." save this; list() will not return it again
|
|
294
|
+
t.expires_at # int — unix seconds
|
|
229
295
|
|
|
296
|
+
# list() returns TokenInfo (no raw token; listing-safe metadata).
|
|
297
|
+
# The `jti` field here is what revoke() takes — there's no jti on
|
|
298
|
+
# the freshly-created Token, so revoke goes through list().
|
|
230
299
|
for info in c.token.list():
|
|
231
|
-
|
|
232
|
-
|
|
300
|
+
info.jti # str — used by revoke()
|
|
301
|
+
info.created_at # int — unix seconds
|
|
302
|
+
info.expires_at # int — unix seconds
|
|
303
|
+
c.token.revoke(info.jti)
|
|
233
304
|
|
|
234
305
|
# Use the token to authenticate a new client
|
|
235
306
|
c2 = Compute(token=t.token)
|
|
@@ -248,6 +319,13 @@ cfg.default_image # "ubuntu:latest"
|
|
|
248
319
|
cfg.zone # "boxd.sh"
|
|
249
320
|
```
|
|
250
321
|
|
|
322
|
+
The package also exposes its installed version:
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
import boxd
|
|
326
|
+
print("on", boxd.__version__)
|
|
327
|
+
```
|
|
328
|
+
|
|
251
329
|
## Errors
|
|
252
330
|
|
|
253
331
|
```python
|
|
@@ -278,17 +356,39 @@ except NotFoundError:
|
|
|
278
356
|
| `ConnectionError` | `UNAVAILABLE` |
|
|
279
357
|
| `InternalError` | `INTERNAL`, `UNKNOWN` |
|
|
280
358
|
|
|
281
|
-
Each error carries the underlying `grpc_code` for finer-grained handling
|
|
359
|
+
Each error carries the underlying `grpc_code` (numeric gRPC status — see [grpc.StatusCode](https://grpc.github.io/grpc/core/md_doc_statuscodes.html)) for finer-grained handling:
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
import grpc
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
c.box.create(name="my-vm")
|
|
366
|
+
except BoxdError as e:
|
|
367
|
+
if e.grpc_code == grpc.StatusCode.RESOURCE_EXHAUSTED.value[0]:
|
|
368
|
+
... # hit per-user quota
|
|
369
|
+
raise
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Update notifications
|
|
373
|
+
|
|
374
|
+
Every gRPC response carries an `x-boxd-py-sdk-latest` header set by the boxd proxy. The SDK's interceptor compares it to the installed version and prints a one-time `sys.stderr` line if a newer release is available:
|
|
375
|
+
|
|
376
|
+
```
|
|
377
|
+
A new version of boxd is available (v0.1.2, you have v0.1.1). Update with:
|
|
378
|
+
pip install --upgrade boxd
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
The notice fires at most once per process, never causes a request to fail, and is silent if the proxy isn't advertising a newer version. Compares as PEP 440-ish (numeric prefix, then per-component compare on `.devN` suffixes).
|
|
282
382
|
|
|
283
383
|
## Sync vs Async
|
|
284
384
|
|
|
285
|
-
The default
|
|
385
|
+
The default `boxd.Compute` is the **sync API** — fully blocking, safe for scripts, REPLs, notebooks, Django views, anywhere you don't already have an event loop. It wraps the async implementation behind a dedicated background loop, so you don't pay for `asyncio` setup yourself.
|
|
286
386
|
|
|
287
387
|
```python
|
|
288
|
-
from boxd import Compute # sync — recommended
|
|
388
|
+
from boxd import Compute # sync — recommended default
|
|
289
389
|
```
|
|
290
390
|
|
|
291
|
-
|
|
391
|
+
`boxd.aio.Compute` is the **async API** — use it from inside an existing event loop (FastAPI, asyncio scripts, Quart, anyio):
|
|
292
392
|
|
|
293
393
|
```python
|
|
294
394
|
from boxd.aio import Compute
|
|
@@ -298,7 +398,7 @@ async with Compute(api_key="bxk_...") as c:
|
|
|
298
398
|
result = await box.exec("echo", "hello")
|
|
299
399
|
```
|
|
300
400
|
|
|
301
|
-
The two
|
|
401
|
+
The two are surface-equivalent: same method names, same arguments, same return types. The only differences are `with` vs `async with` and the `await` keyword on every call. **Pick `boxd` unless you already have an event loop.**
|
|
302
402
|
|
|
303
403
|
## Development
|
|
304
404
|
|
|
@@ -51,9 +51,17 @@ Compute(
|
|
|
51
51
|
)
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
### Environment variables
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
All `Compute` arguments can be supplied via env vars. Constructor args win over env vars; env vars win over the `environment` preset.
|
|
57
|
+
|
|
58
|
+
| Variable | Sets | Default |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `BOXD_API_KEY` | API key (long-lived, recommended) | — |
|
|
61
|
+
| `BOXD_TOKEN` | Direct JWT (short-lived) | — |
|
|
62
|
+
| `BOXD_ENVIRONMENT` | Preset name (`production` or `staging`) | `production` |
|
|
63
|
+
| `BOXD_API_URL` | gRPC endpoint, overrides preset | `http://boxd.sh:9443` |
|
|
64
|
+
| `BOXD_EXCHANGE_URL` | Token-exchange URL, overrides preset | `https://boxd.sh/api/v1/auth/token` |
|
|
57
65
|
|
|
58
66
|
`api_url` accepts an optional URL scheme that controls TLS:
|
|
59
67
|
|
|
@@ -81,7 +89,39 @@ s = box.suspend() # SuspendResult
|
|
|
81
89
|
r = box.resume() # ResumeResult
|
|
82
90
|
```
|
|
83
91
|
|
|
84
|
-
|
|
92
|
+
### Box fields
|
|
93
|
+
|
|
94
|
+
`Box` always carries server-returned fields, but which ones are populated depends on how it was obtained:
|
|
95
|
+
|
|
96
|
+
| Field | `create` | `fork` | `list` | `get` |
|
|
97
|
+
|---|---|---|---|---|
|
|
98
|
+
| `id`, `name`, `image`, `public_ip`, `status` | ✓ | ✓ | ✓ | ✓ |
|
|
99
|
+
| `url`, `boot_time_ms` | ✓ | ✓ | `None` | `None` |
|
|
100
|
+
| `forked_from` | `None` | ✓ | `None` | `None` |
|
|
101
|
+
| `restart_policy`, `disk_bytes`, `auto_suspend_timeout_secs` | `None` | `None` | `None` | ✓ |
|
|
102
|
+
|
|
103
|
+
If you need the URL or boot time after a `list` / `get` round-trip, the `https://<name>.boxd.sh` form is stable, or call `box.proxies()` for the full set. If you need the lifecycle fields off a `Box` from `list` / `create` / `fork`, re-fetch via `c.box.get(box.name)`.
|
|
104
|
+
|
|
105
|
+
### BoxConfig
|
|
106
|
+
|
|
107
|
+
`create`, `fork`, and `template.create_vm` all take an optional `config`:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from boxd import BoxConfig, LifecycleConfig
|
|
111
|
+
|
|
112
|
+
config = BoxConfig(
|
|
113
|
+
vcpu=2, # default 2
|
|
114
|
+
memory="4G", # default "8G"
|
|
115
|
+
env={"API_KEY": "secret"}, # env vars exposed to the VM
|
|
116
|
+
restart_policy="always", # "always" | "never"
|
|
117
|
+
lifecycle=LifecycleConfig(
|
|
118
|
+
auto_suspend_timeout=300, # idle network secs; 0 disables
|
|
119
|
+
auto_destroy_timeout=0, # total lifetime secs; 0 disables
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
box = c.box.create(name="my-vm", config=config)
|
|
124
|
+
```
|
|
85
125
|
|
|
86
126
|
## Exec
|
|
87
127
|
|
|
@@ -91,19 +131,25 @@ r = box.exec("python", "script.py")
|
|
|
91
131
|
r.stdout # str
|
|
92
132
|
r.stderr # str
|
|
93
133
|
r.exit_code # int
|
|
94
|
-
r.success # bool
|
|
134
|
+
r.success # bool — exit_code == 0
|
|
95
135
|
|
|
96
136
|
# With env vars and timeout
|
|
97
137
|
box.exec("sh", "-c", "echo $FOO", env={"FOO": "bar"}, timeout=30)
|
|
98
138
|
|
|
99
|
-
# Streaming
|
|
139
|
+
# Streaming — proc is an ExecProcess. Use iter_stdout / iter_stderr
|
|
140
|
+
# (sync generators that block until the next chunk arrives), then wait()
|
|
141
|
+
# for the exit code. close() force-terminates the stream.
|
|
100
142
|
proc = box.exec("tail", "-f", "/var/log/syslog", stream=True)
|
|
101
143
|
for chunk in proc.iter_stdout():
|
|
102
144
|
print(chunk.decode(), end="")
|
|
103
145
|
exit_code = proc.wait()
|
|
146
|
+
proc.close() # idempotent
|
|
104
147
|
|
|
105
|
-
# Interactive (PTY + stdin)
|
|
106
|
-
sh = box.exec("bash", interactive=True)
|
|
148
|
+
# Interactive (PTY + stdin) — write to proc.stdin, end with write_eof
|
|
149
|
+
sh = box.exec("bash", interactive=True) # interactive implies pty
|
|
150
|
+
sh.stdin.write(b"echo hello\n")
|
|
151
|
+
sh.stdin.write_eof()
|
|
152
|
+
print(sh.wait())
|
|
107
153
|
```
|
|
108
154
|
|
|
109
155
|
## Files
|
|
@@ -141,6 +187,8 @@ for chunk in box.stream_logs(follow=True):
|
|
|
141
187
|
|
|
142
188
|
## Templates
|
|
143
189
|
|
|
190
|
+
Reusable image + `BoxConfig` frozen together.
|
|
191
|
+
|
|
144
192
|
```python
|
|
145
193
|
from boxd import BoxConfig
|
|
146
194
|
|
|
@@ -150,7 +198,16 @@ t = c.template.create(
|
|
|
150
198
|
config=BoxConfig(vcpu=2, memory="4G"),
|
|
151
199
|
)
|
|
152
200
|
c.template.list()
|
|
201
|
+
|
|
202
|
+
# create_vm accepts a Template object OR a template ID string. Pass an
|
|
203
|
+
# optional `config` to override the template's defaults (e.g. bump
|
|
204
|
+
# memory for one specific VM).
|
|
153
205
|
box = c.template.create_vm(template=t, name="from-t")
|
|
206
|
+
big = c.template.create_vm(
|
|
207
|
+
template=t.id,
|
|
208
|
+
name="from-t-big",
|
|
209
|
+
config=BoxConfig(memory="16G"),
|
|
210
|
+
)
|
|
154
211
|
c.template.delete(t.id)
|
|
155
212
|
```
|
|
156
213
|
|
|
@@ -158,10 +215,18 @@ c.template.delete(t.id)
|
|
|
158
215
|
|
|
159
216
|
```python
|
|
160
217
|
d = c.disk.create("data", size="10G")
|
|
218
|
+
d.id; d.name; d.size_bytes; d.status
|
|
219
|
+
|
|
220
|
+
# attach / detach take a Box instance OR a name/id string
|
|
161
221
|
d.attach(box, mount_path="/mnt/data")
|
|
162
|
-
d.attach(
|
|
163
|
-
d.detach(
|
|
222
|
+
d.attach("my-vm", mount_path="/mnt/data", read_only=True)
|
|
223
|
+
d.detach("my-vm")
|
|
224
|
+
|
|
164
225
|
d.destroy()
|
|
226
|
+
|
|
227
|
+
# list returns DiskHandle instances — same methods as above
|
|
228
|
+
for d in c.disk.list():
|
|
229
|
+
print(d.name, d.status)
|
|
165
230
|
```
|
|
166
231
|
|
|
167
232
|
## Domains
|
|
@@ -179,8 +244,11 @@ c.domain.unbind("app.example.com")
|
|
|
179
244
|
## Networks
|
|
180
245
|
|
|
181
246
|
```python
|
|
182
|
-
n = c.network.create()
|
|
247
|
+
n = c.network.create() # server assigns id
|
|
183
248
|
named = c.network.create(name="staging")
|
|
249
|
+
|
|
250
|
+
# `create` returns the new network's id only — `subnet` and `status` come
|
|
251
|
+
# back populated once provisioning settles. Re-fetch via `list` to read them.
|
|
184
252
|
for net in c.network.list():
|
|
185
253
|
print(net.id, net.subnet, net.status)
|
|
186
254
|
```
|
|
@@ -190,13 +258,18 @@ for net in c.network.list():
|
|
|
190
258
|
Issue scoped JWTs for delegated access. The raw token string is only returned at creation — store it then.
|
|
191
259
|
|
|
192
260
|
```python
|
|
193
|
-
t = c.token.create(expires_in=3600)
|
|
194
|
-
t.token # "eyJ..."
|
|
195
|
-
t.expires_at # unix seconds
|
|
261
|
+
t = c.token.create(expires_in=3600) # 0 = server default
|
|
262
|
+
t.token # str — "eyJ..." save this; list() will not return it again
|
|
263
|
+
t.expires_at # int — unix seconds
|
|
196
264
|
|
|
265
|
+
# list() returns TokenInfo (no raw token; listing-safe metadata).
|
|
266
|
+
# The `jti` field here is what revoke() takes — there's no jti on
|
|
267
|
+
# the freshly-created Token, so revoke goes through list().
|
|
197
268
|
for info in c.token.list():
|
|
198
|
-
|
|
199
|
-
|
|
269
|
+
info.jti # str — used by revoke()
|
|
270
|
+
info.created_at # int — unix seconds
|
|
271
|
+
info.expires_at # int — unix seconds
|
|
272
|
+
c.token.revoke(info.jti)
|
|
200
273
|
|
|
201
274
|
# Use the token to authenticate a new client
|
|
202
275
|
c2 = Compute(token=t.token)
|
|
@@ -215,6 +288,13 @@ cfg.default_image # "ubuntu:latest"
|
|
|
215
288
|
cfg.zone # "boxd.sh"
|
|
216
289
|
```
|
|
217
290
|
|
|
291
|
+
The package also exposes its installed version:
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
import boxd
|
|
295
|
+
print("on", boxd.__version__)
|
|
296
|
+
```
|
|
297
|
+
|
|
218
298
|
## Errors
|
|
219
299
|
|
|
220
300
|
```python
|
|
@@ -245,17 +325,39 @@ except NotFoundError:
|
|
|
245
325
|
| `ConnectionError` | `UNAVAILABLE` |
|
|
246
326
|
| `InternalError` | `INTERNAL`, `UNKNOWN` |
|
|
247
327
|
|
|
248
|
-
Each error carries the underlying `grpc_code` for finer-grained handling
|
|
328
|
+
Each error carries the underlying `grpc_code` (numeric gRPC status — see [grpc.StatusCode](https://grpc.github.io/grpc/core/md_doc_statuscodes.html)) for finer-grained handling:
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
import grpc
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
c.box.create(name="my-vm")
|
|
335
|
+
except BoxdError as e:
|
|
336
|
+
if e.grpc_code == grpc.StatusCode.RESOURCE_EXHAUSTED.value[0]:
|
|
337
|
+
... # hit per-user quota
|
|
338
|
+
raise
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Update notifications
|
|
342
|
+
|
|
343
|
+
Every gRPC response carries an `x-boxd-py-sdk-latest` header set by the boxd proxy. The SDK's interceptor compares it to the installed version and prints a one-time `sys.stderr` line if a newer release is available:
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
A new version of boxd is available (v0.1.2, you have v0.1.1). Update with:
|
|
347
|
+
pip install --upgrade boxd
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
The notice fires at most once per process, never causes a request to fail, and is silent if the proxy isn't advertising a newer version. Compares as PEP 440-ish (numeric prefix, then per-component compare on `.devN` suffixes).
|
|
249
351
|
|
|
250
352
|
## Sync vs Async
|
|
251
353
|
|
|
252
|
-
The default
|
|
354
|
+
The default `boxd.Compute` is the **sync API** — fully blocking, safe for scripts, REPLs, notebooks, Django views, anywhere you don't already have an event loop. It wraps the async implementation behind a dedicated background loop, so you don't pay for `asyncio` setup yourself.
|
|
253
355
|
|
|
254
356
|
```python
|
|
255
|
-
from boxd import Compute # sync — recommended
|
|
357
|
+
from boxd import Compute # sync — recommended default
|
|
256
358
|
```
|
|
257
359
|
|
|
258
|
-
|
|
360
|
+
`boxd.aio.Compute` is the **async API** — use it from inside an existing event loop (FastAPI, asyncio scripts, Quart, anyio):
|
|
259
361
|
|
|
260
362
|
```python
|
|
261
363
|
from boxd.aio import Compute
|
|
@@ -265,7 +367,7 @@ async with Compute(api_key="bxk_...") as c:
|
|
|
265
367
|
result = await box.exec("echo", "hello")
|
|
266
368
|
```
|
|
267
369
|
|
|
268
|
-
The two
|
|
370
|
+
The two are surface-equivalent: same method names, same arguments, same return types. The only differences are `with` vs `async with` and the `await` keyword on every call. **Pick `boxd` unless you already have an event loop.**
|
|
269
371
|
|
|
270
372
|
## Development
|
|
271
373
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "boxd"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.2"
|
|
4
4
|
description = "Python SDK for the boxd cloud VM platform"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -33,8 +33,6 @@ dependencies = [
|
|
|
33
33
|
|
|
34
34
|
[project.urls]
|
|
35
35
|
Homepage = "https://boxd.sh"
|
|
36
|
-
Repository = "https://github.com/azin-tech/boxd"
|
|
37
|
-
Issues = "https://github.com/azin-tech/boxd/issues"
|
|
38
36
|
|
|
39
37
|
[project.optional-dependencies]
|
|
40
38
|
dev = [
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: boxd
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Python SDK for the boxd cloud VM platform
|
|
5
5
|
Author: Azin
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://boxd.sh
|
|
8
|
-
Project-URL: Repository, https://github.com/azin-tech/boxd
|
|
9
|
-
Project-URL: Issues, https://github.com/azin-tech/boxd/issues
|
|
10
8
|
Keywords: boxd,vm,microvm,sandbox,compute,grpc,sdk
|
|
11
9
|
Classifier: Development Status :: 4 - Beta
|
|
12
10
|
Classifier: Intended Audience :: Developers
|
|
@@ -84,9 +82,17 @@ Compute(
|
|
|
84
82
|
)
|
|
85
83
|
```
|
|
86
84
|
|
|
87
|
-
|
|
85
|
+
### Environment variables
|
|
88
86
|
|
|
89
|
-
|
|
87
|
+
All `Compute` arguments can be supplied via env vars. Constructor args win over env vars; env vars win over the `environment` preset.
|
|
88
|
+
|
|
89
|
+
| Variable | Sets | Default |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `BOXD_API_KEY` | API key (long-lived, recommended) | — |
|
|
92
|
+
| `BOXD_TOKEN` | Direct JWT (short-lived) | — |
|
|
93
|
+
| `BOXD_ENVIRONMENT` | Preset name (`production` or `staging`) | `production` |
|
|
94
|
+
| `BOXD_API_URL` | gRPC endpoint, overrides preset | `http://boxd.sh:9443` |
|
|
95
|
+
| `BOXD_EXCHANGE_URL` | Token-exchange URL, overrides preset | `https://boxd.sh/api/v1/auth/token` |
|
|
90
96
|
|
|
91
97
|
`api_url` accepts an optional URL scheme that controls TLS:
|
|
92
98
|
|
|
@@ -114,7 +120,39 @@ s = box.suspend() # SuspendResult
|
|
|
114
120
|
r = box.resume() # ResumeResult
|
|
115
121
|
```
|
|
116
122
|
|
|
117
|
-
|
|
123
|
+
### Box fields
|
|
124
|
+
|
|
125
|
+
`Box` always carries server-returned fields, but which ones are populated depends on how it was obtained:
|
|
126
|
+
|
|
127
|
+
| Field | `create` | `fork` | `list` | `get` |
|
|
128
|
+
|---|---|---|---|---|
|
|
129
|
+
| `id`, `name`, `image`, `public_ip`, `status` | ✓ | ✓ | ✓ | ✓ |
|
|
130
|
+
| `url`, `boot_time_ms` | ✓ | ✓ | `None` | `None` |
|
|
131
|
+
| `forked_from` | `None` | ✓ | `None` | `None` |
|
|
132
|
+
| `restart_policy`, `disk_bytes`, `auto_suspend_timeout_secs` | `None` | `None` | `None` | ✓ |
|
|
133
|
+
|
|
134
|
+
If you need the URL or boot time after a `list` / `get` round-trip, the `https://<name>.boxd.sh` form is stable, or call `box.proxies()` for the full set. If you need the lifecycle fields off a `Box` from `list` / `create` / `fork`, re-fetch via `c.box.get(box.name)`.
|
|
135
|
+
|
|
136
|
+
### BoxConfig
|
|
137
|
+
|
|
138
|
+
`create`, `fork`, and `template.create_vm` all take an optional `config`:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from boxd import BoxConfig, LifecycleConfig
|
|
142
|
+
|
|
143
|
+
config = BoxConfig(
|
|
144
|
+
vcpu=2, # default 2
|
|
145
|
+
memory="4G", # default "8G"
|
|
146
|
+
env={"API_KEY": "secret"}, # env vars exposed to the VM
|
|
147
|
+
restart_policy="always", # "always" | "never"
|
|
148
|
+
lifecycle=LifecycleConfig(
|
|
149
|
+
auto_suspend_timeout=300, # idle network secs; 0 disables
|
|
150
|
+
auto_destroy_timeout=0, # total lifetime secs; 0 disables
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
box = c.box.create(name="my-vm", config=config)
|
|
155
|
+
```
|
|
118
156
|
|
|
119
157
|
## Exec
|
|
120
158
|
|
|
@@ -124,19 +162,25 @@ r = box.exec("python", "script.py")
|
|
|
124
162
|
r.stdout # str
|
|
125
163
|
r.stderr # str
|
|
126
164
|
r.exit_code # int
|
|
127
|
-
r.success # bool
|
|
165
|
+
r.success # bool — exit_code == 0
|
|
128
166
|
|
|
129
167
|
# With env vars and timeout
|
|
130
168
|
box.exec("sh", "-c", "echo $FOO", env={"FOO": "bar"}, timeout=30)
|
|
131
169
|
|
|
132
|
-
# Streaming
|
|
170
|
+
# Streaming — proc is an ExecProcess. Use iter_stdout / iter_stderr
|
|
171
|
+
# (sync generators that block until the next chunk arrives), then wait()
|
|
172
|
+
# for the exit code. close() force-terminates the stream.
|
|
133
173
|
proc = box.exec("tail", "-f", "/var/log/syslog", stream=True)
|
|
134
174
|
for chunk in proc.iter_stdout():
|
|
135
175
|
print(chunk.decode(), end="")
|
|
136
176
|
exit_code = proc.wait()
|
|
177
|
+
proc.close() # idempotent
|
|
137
178
|
|
|
138
|
-
# Interactive (PTY + stdin)
|
|
139
|
-
sh = box.exec("bash", interactive=True)
|
|
179
|
+
# Interactive (PTY + stdin) — write to proc.stdin, end with write_eof
|
|
180
|
+
sh = box.exec("bash", interactive=True) # interactive implies pty
|
|
181
|
+
sh.stdin.write(b"echo hello\n")
|
|
182
|
+
sh.stdin.write_eof()
|
|
183
|
+
print(sh.wait())
|
|
140
184
|
```
|
|
141
185
|
|
|
142
186
|
## Files
|
|
@@ -174,6 +218,8 @@ for chunk in box.stream_logs(follow=True):
|
|
|
174
218
|
|
|
175
219
|
## Templates
|
|
176
220
|
|
|
221
|
+
Reusable image + `BoxConfig` frozen together.
|
|
222
|
+
|
|
177
223
|
```python
|
|
178
224
|
from boxd import BoxConfig
|
|
179
225
|
|
|
@@ -183,7 +229,16 @@ t = c.template.create(
|
|
|
183
229
|
config=BoxConfig(vcpu=2, memory="4G"),
|
|
184
230
|
)
|
|
185
231
|
c.template.list()
|
|
232
|
+
|
|
233
|
+
# create_vm accepts a Template object OR a template ID string. Pass an
|
|
234
|
+
# optional `config` to override the template's defaults (e.g. bump
|
|
235
|
+
# memory for one specific VM).
|
|
186
236
|
box = c.template.create_vm(template=t, name="from-t")
|
|
237
|
+
big = c.template.create_vm(
|
|
238
|
+
template=t.id,
|
|
239
|
+
name="from-t-big",
|
|
240
|
+
config=BoxConfig(memory="16G"),
|
|
241
|
+
)
|
|
187
242
|
c.template.delete(t.id)
|
|
188
243
|
```
|
|
189
244
|
|
|
@@ -191,10 +246,18 @@ c.template.delete(t.id)
|
|
|
191
246
|
|
|
192
247
|
```python
|
|
193
248
|
d = c.disk.create("data", size="10G")
|
|
249
|
+
d.id; d.name; d.size_bytes; d.status
|
|
250
|
+
|
|
251
|
+
# attach / detach take a Box instance OR a name/id string
|
|
194
252
|
d.attach(box, mount_path="/mnt/data")
|
|
195
|
-
d.attach(
|
|
196
|
-
d.detach(
|
|
253
|
+
d.attach("my-vm", mount_path="/mnt/data", read_only=True)
|
|
254
|
+
d.detach("my-vm")
|
|
255
|
+
|
|
197
256
|
d.destroy()
|
|
257
|
+
|
|
258
|
+
# list returns DiskHandle instances — same methods as above
|
|
259
|
+
for d in c.disk.list():
|
|
260
|
+
print(d.name, d.status)
|
|
198
261
|
```
|
|
199
262
|
|
|
200
263
|
## Domains
|
|
@@ -212,8 +275,11 @@ c.domain.unbind("app.example.com")
|
|
|
212
275
|
## Networks
|
|
213
276
|
|
|
214
277
|
```python
|
|
215
|
-
n = c.network.create()
|
|
278
|
+
n = c.network.create() # server assigns id
|
|
216
279
|
named = c.network.create(name="staging")
|
|
280
|
+
|
|
281
|
+
# `create` returns the new network's id only — `subnet` and `status` come
|
|
282
|
+
# back populated once provisioning settles. Re-fetch via `list` to read them.
|
|
217
283
|
for net in c.network.list():
|
|
218
284
|
print(net.id, net.subnet, net.status)
|
|
219
285
|
```
|
|
@@ -223,13 +289,18 @@ for net in c.network.list():
|
|
|
223
289
|
Issue scoped JWTs for delegated access. The raw token string is only returned at creation — store it then.
|
|
224
290
|
|
|
225
291
|
```python
|
|
226
|
-
t = c.token.create(expires_in=3600)
|
|
227
|
-
t.token # "eyJ..."
|
|
228
|
-
t.expires_at # unix seconds
|
|
292
|
+
t = c.token.create(expires_in=3600) # 0 = server default
|
|
293
|
+
t.token # str — "eyJ..." save this; list() will not return it again
|
|
294
|
+
t.expires_at # int — unix seconds
|
|
229
295
|
|
|
296
|
+
# list() returns TokenInfo (no raw token; listing-safe metadata).
|
|
297
|
+
# The `jti` field here is what revoke() takes — there's no jti on
|
|
298
|
+
# the freshly-created Token, so revoke goes through list().
|
|
230
299
|
for info in c.token.list():
|
|
231
|
-
|
|
232
|
-
|
|
300
|
+
info.jti # str — used by revoke()
|
|
301
|
+
info.created_at # int — unix seconds
|
|
302
|
+
info.expires_at # int — unix seconds
|
|
303
|
+
c.token.revoke(info.jti)
|
|
233
304
|
|
|
234
305
|
# Use the token to authenticate a new client
|
|
235
306
|
c2 = Compute(token=t.token)
|
|
@@ -248,6 +319,13 @@ cfg.default_image # "ubuntu:latest"
|
|
|
248
319
|
cfg.zone # "boxd.sh"
|
|
249
320
|
```
|
|
250
321
|
|
|
322
|
+
The package also exposes its installed version:
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
import boxd
|
|
326
|
+
print("on", boxd.__version__)
|
|
327
|
+
```
|
|
328
|
+
|
|
251
329
|
## Errors
|
|
252
330
|
|
|
253
331
|
```python
|
|
@@ -278,17 +356,39 @@ except NotFoundError:
|
|
|
278
356
|
| `ConnectionError` | `UNAVAILABLE` |
|
|
279
357
|
| `InternalError` | `INTERNAL`, `UNKNOWN` |
|
|
280
358
|
|
|
281
|
-
Each error carries the underlying `grpc_code` for finer-grained handling
|
|
359
|
+
Each error carries the underlying `grpc_code` (numeric gRPC status — see [grpc.StatusCode](https://grpc.github.io/grpc/core/md_doc_statuscodes.html)) for finer-grained handling:
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
import grpc
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
c.box.create(name="my-vm")
|
|
366
|
+
except BoxdError as e:
|
|
367
|
+
if e.grpc_code == grpc.StatusCode.RESOURCE_EXHAUSTED.value[0]:
|
|
368
|
+
... # hit per-user quota
|
|
369
|
+
raise
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Update notifications
|
|
373
|
+
|
|
374
|
+
Every gRPC response carries an `x-boxd-py-sdk-latest` header set by the boxd proxy. The SDK's interceptor compares it to the installed version and prints a one-time `sys.stderr` line if a newer release is available:
|
|
375
|
+
|
|
376
|
+
```
|
|
377
|
+
A new version of boxd is available (v0.1.2, you have v0.1.1). Update with:
|
|
378
|
+
pip install --upgrade boxd
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
The notice fires at most once per process, never causes a request to fail, and is silent if the proxy isn't advertising a newer version. Compares as PEP 440-ish (numeric prefix, then per-component compare on `.devN` suffixes).
|
|
282
382
|
|
|
283
383
|
## Sync vs Async
|
|
284
384
|
|
|
285
|
-
The default
|
|
385
|
+
The default `boxd.Compute` is the **sync API** — fully blocking, safe for scripts, REPLs, notebooks, Django views, anywhere you don't already have an event loop. It wraps the async implementation behind a dedicated background loop, so you don't pay for `asyncio` setup yourself.
|
|
286
386
|
|
|
287
387
|
```python
|
|
288
|
-
from boxd import Compute # sync — recommended
|
|
388
|
+
from boxd import Compute # sync — recommended default
|
|
289
389
|
```
|
|
290
390
|
|
|
291
|
-
|
|
391
|
+
`boxd.aio.Compute` is the **async API** — use it from inside an existing event loop (FastAPI, asyncio scripts, Quart, anyio):
|
|
292
392
|
|
|
293
393
|
```python
|
|
294
394
|
from boxd.aio import Compute
|
|
@@ -298,7 +398,7 @@ async with Compute(api_key="bxk_...") as c:
|
|
|
298
398
|
result = await box.exec("echo", "hello")
|
|
299
399
|
```
|
|
300
400
|
|
|
301
|
-
The two
|
|
401
|
+
The two are surface-equivalent: same method names, same arguments, same return types. The only differences are `with` vs `async with` and the `await` keyword on every call. **Pick `boxd` unless you already have an event loop.**
|
|
302
402
|
|
|
303
403
|
## Development
|
|
304
404
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|