boxd 0.2.0.dev38__tar.gz → 0.2.0.dev40__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.2.0.dev38/src/boxd.egg-info → boxd-0.2.0.dev40}/PKG-INFO +222 -57
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/README.md +221 -56
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/pyproject.toml +1 -1
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd/resources/snapshots.py +5 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40/src/boxd.egg-info}/PKG-INFO +222 -57
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/tests/test_namespaces.py +11 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/LICENSE +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/setup.cfg +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd/__init__.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd/_client.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd/_credentials.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd/_generated/__init__.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd/_generated/api_pb2.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd/_generated/api_pb2_grpc.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd/_mappers.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd/_requests.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd/_streaming.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd/_transport.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd/_urls.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd/_version_check.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd/errors.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd/models.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd/resources/__init__.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd/resources/account.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd/resources/credentials.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd/resources/disks.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd/resources/machines.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd/resources/orgs.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd/resources/vars.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd.egg-info/SOURCES.txt +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd.egg-info/dependency_links.txt +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd.egg-info/requires.txt +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/src/boxd.egg-info/top_level.txt +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/tests/test_credentials.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/tests/test_mappers.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/tests/test_requests.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/tests/test_streaming.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/tests/test_transport.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/tests/test_urls.py +0 -0
- {boxd-0.2.0.dev38 → boxd-0.2.0.dev40}/tests/test_version_check.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: boxd
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev40
|
|
4
4
|
Summary: Python SDK for the boxd cloud VM platform
|
|
5
5
|
Author: Azin
|
|
6
6
|
License-Expression: MIT
|
|
@@ -57,6 +57,7 @@ result = boxd.machines.exec(machine.id, "uname -a")
|
|
|
57
57
|
print(result.stdout)
|
|
58
58
|
|
|
59
59
|
boxd.machines.delete(machine.id)
|
|
60
|
+
boxd.close()
|
|
60
61
|
```
|
|
61
62
|
|
|
62
63
|
Everything follows the same shape: **`boxd.<resource>.<verb>(id, ...)`**.
|
|
@@ -70,16 +71,25 @@ Boxd(api_key="bxd_...")
|
|
|
70
71
|
Boxd(base_url="https://boxd.example.com:9443") # any other cluster
|
|
71
72
|
```
|
|
72
73
|
|
|
74
|
+
Every argument is keyword-only.
|
|
75
|
+
|
|
73
76
|
| Argument | Environment variable | Default |
|
|
74
77
|
|---|---|---|
|
|
75
78
|
| `api_key` | `BOXD_API_KEY` | — |
|
|
76
79
|
| `token` | `BOXD_TOKEN` | — |
|
|
77
|
-
| `base_url` | `BOXD_BASE_URL` |
|
|
78
|
-
| `timeout` | — | 60 seconds |
|
|
80
|
+
| `base_url` | `BOXD_BASE_URL` (or the deprecated `BOXD_API_URL`) | `http://boxd.sh:9443` |
|
|
81
|
+
| `timeout` | — | 60.0 seconds |
|
|
79
82
|
| `max_retries` | — | 2 |
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
`base_url` accepts an optional scheme that controls TLS:
|
|
85
|
+
|
|
86
|
+
| Value | Transport |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `http://host:port` | plaintext |
|
|
89
|
+
| `https://host:port` | TLS |
|
|
90
|
+
| bare `host:port` | TLS, except `localhost` / `127.*` |
|
|
91
|
+
|
|
92
|
+
`boxd.base_url` reports the cluster the client settled on.
|
|
83
93
|
|
|
84
94
|
The client holds a connection, so keep one around rather than making a new one
|
|
85
95
|
per call. Close it when you're done — or use it as a context manager:
|
|
@@ -135,6 +145,17 @@ result = await boxd.machines.exec(machine.id, "echo hello")
|
|
|
135
145
|
await boxd.close()
|
|
136
146
|
```
|
|
137
147
|
|
|
148
|
+
`AsyncBoxd` is an async context manager too:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
async with AsyncBoxd(api_key="bxd_...") as boxd:
|
|
152
|
+
...
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Two methods stay un-awaited, because they hand back something to iterate rather
|
|
156
|
+
than a result: `stream_exec` returns the session object directly, and `logs` is
|
|
157
|
+
an async generator you drive with `async for`.
|
|
158
|
+
|
|
138
159
|
Use `AsyncBoxd` when you already have an event loop (FastAPI, asyncio scripts,
|
|
139
160
|
anyio). Use `Boxd` everywhere else — scripts, notebooks, Django views.
|
|
140
161
|
|
|
@@ -149,17 +170,47 @@ machine = boxd.machines.create(
|
|
|
149
170
|
)
|
|
150
171
|
boxd.machines.get("my-machine") # by name or id
|
|
151
172
|
boxd.machines.list() # a plain list
|
|
173
|
+
boxd.machines.list(org="acme") # one organization's machines
|
|
174
|
+
boxd.machines.list(all_contexts=True) # every org you belong to
|
|
152
175
|
boxd.machines.delete("my-machine")
|
|
153
176
|
```
|
|
154
177
|
|
|
178
|
+
Only `name` is positional; everything else is keyword-only. The full set of
|
|
179
|
+
create options:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
boxd.machines.create(
|
|
183
|
+
"builder",
|
|
184
|
+
image="ubuntu:24.04",
|
|
185
|
+
org="acme", # create inside an organization
|
|
186
|
+
shared=True, # and make it visible to every member
|
|
187
|
+
env={"API_URL": "https://example.com"},
|
|
188
|
+
cmd=["/usr/local/bin/start"],
|
|
189
|
+
restart_policy="always", # "always" | "never"
|
|
190
|
+
vcpu=2,
|
|
191
|
+
memory="8G", # "8G", "512M", or a byte count
|
|
192
|
+
disk="100G",
|
|
193
|
+
auto_suspend_timeout=300, # seconds; 0 disables
|
|
194
|
+
auto_destroy_timeout=0,
|
|
195
|
+
ssh=True, # give the machine an SSH port
|
|
196
|
+
proxies=[ProxyEntry(name="api", port=3000)],
|
|
197
|
+
volumes=[VolumeMount(disk_id="d_...", mount_path="/data", read_only=False)],
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
boxd.machines.create() # every option is optional — cluster default image
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`ProxyEntry` and `VolumeMount` are importable from `boxd`. A `ProxyEntry` with
|
|
204
|
+
`port=0` has its port detected inside the machine.
|
|
205
|
+
|
|
155
206
|
State:
|
|
156
207
|
|
|
157
208
|
```python
|
|
158
209
|
boxd.machines.start(id)
|
|
159
210
|
boxd.machines.stop(id)
|
|
160
211
|
boxd.machines.reboot(id)
|
|
161
|
-
boxd.machines.pause(id) # suspend to RAM — fast to resume
|
|
162
|
-
boxd.machines.resume(id)
|
|
212
|
+
boxd.machines.pause(id) # suspend to RAM — fast to resume; PauseResult(suspend_us)
|
|
213
|
+
boxd.machines.resume(id) # ResumeResult(resume_us)
|
|
163
214
|
boxd.machines.hibernate(id) # suspend to disk — cheaper, slower to wake
|
|
164
215
|
boxd.machines.wake(id)
|
|
165
216
|
```
|
|
@@ -168,28 +219,33 @@ Everything else:
|
|
|
168
219
|
|
|
169
220
|
```python
|
|
170
221
|
boxd.machines.fork("my-machine", "my-copy") # live clone
|
|
171
|
-
boxd.machines.
|
|
222
|
+
boxd.machines.fork(id, shared=True, vcpu=8) # same sizing options as `create`
|
|
223
|
+
boxd.machines.rename(id, "new-name") # returns the new name; reboots the machine
|
|
172
224
|
boxd.machines.share(id) # visible to your whole org
|
|
173
225
|
boxd.machines.unshare(id)
|
|
174
226
|
boxd.machines.set_auto_suspend_timeout(id, 300) # seconds idle; 0 disables
|
|
175
227
|
boxd.machines.set_auto_hibernate_timeout(id, 0)
|
|
176
228
|
boxd.machines.wait_until_ready(id)
|
|
229
|
+
boxd.machines.wait_until_ready(id, timeout=180.0, poll_interval=1.0) # seconds
|
|
177
230
|
boxd.machines.suggest_name()
|
|
178
231
|
```
|
|
179
232
|
|
|
233
|
+
A fork inherits the source's sizing for anything you leave unset, and is
|
|
234
|
+
private to you unless you pass `shared=True`.
|
|
235
|
+
|
|
180
236
|
`create` and `fork` return once the machine is scheduled, not once it is
|
|
181
237
|
usable. Call `wait_until_ready` before doing anything that depends on it
|
|
182
238
|
running — especially before forking it again.
|
|
183
239
|
|
|
184
240
|
### The `Machine` record
|
|
185
241
|
|
|
186
|
-
Related fields travel together, so you read one object instead of remembering
|
|
187
|
-
which flat field pairs with which.
|
|
188
|
-
|
|
189
242
|
```python
|
|
190
|
-
machine.id, machine.name, machine.
|
|
243
|
+
machine.id, machine.name, machine.image_ref
|
|
244
|
+
machine.status # "pending" | "starting" | "running" | "suspended" |
|
|
245
|
+
# "hibernated" | "stopped" | "failed" | "destroyed" |
|
|
246
|
+
# "migrating"
|
|
191
247
|
machine.restart_policy # str | None
|
|
192
|
-
machine.created_at # datetime | None — None on
|
|
248
|
+
machine.created_at # datetime | None — None when none is on record
|
|
193
249
|
|
|
194
250
|
machine.resources.vcpu # what the machine actually got, not what you
|
|
195
251
|
machine.resources.memory_bytes # asked for — always concrete
|
|
@@ -229,6 +285,9 @@ set with `shared=False` is normal, not a contradiction.
|
|
|
229
285
|
of where the machine came from, not a live link — **it may not resolve**, and a
|
|
230
286
|
lookup that finds nothing is normal.
|
|
231
287
|
|
|
288
|
+
`MachineStatus` is importable from `boxd` when you want the literal type; a
|
|
289
|
+
status a newer server introduces is passed through as a plain string.
|
|
290
|
+
|
|
232
291
|
### Creating from a snapshot
|
|
233
292
|
|
|
234
293
|
```python
|
|
@@ -236,6 +295,10 @@ boxd.snapshots.create(machine_id, "golden")
|
|
|
236
295
|
machine = boxd.machines.create("from-golden", from_snapshot="golden")
|
|
237
296
|
```
|
|
238
297
|
|
|
298
|
+
Restoring a snapshot replays the machine as it was captured, so `from_snapshot`
|
|
299
|
+
goes with `name`, `org` and the sizing options. Combining it with `image`,
|
|
300
|
+
`env`, `cmd`, `restart_policy` or `shared` raises `ValueError`.
|
|
301
|
+
|
|
239
302
|
### Exec
|
|
240
303
|
|
|
241
304
|
```python
|
|
@@ -253,21 +316,29 @@ boxd.machines.exec(id, "cargo build", timeout=30) # seconds
|
|
|
253
316
|
boxd.machines.exec(id, "top -b -n1", tty=True, cols=120, rows=40)
|
|
254
317
|
```
|
|
255
318
|
|
|
319
|
+
`command` takes a list of argv — shell-quoted for you — or a ready-made command
|
|
320
|
+
line as a string. `timeout` gives up on the call; whatever it started inside the
|
|
321
|
+
machine may well still be running.
|
|
322
|
+
|
|
256
323
|
For anything interactive, `stream_exec` gives you a live session — the one
|
|
257
324
|
handle in the SDK, because a bidirectional stream really is stateful:
|
|
258
325
|
|
|
259
326
|
```python
|
|
260
327
|
with boxd.machines.stream_exec(id, command="bash", tty=True) as stream:
|
|
261
|
-
stream.write(b"ls\n")
|
|
262
|
-
stream.write_eof()
|
|
263
|
-
for chunk in stream:
|
|
328
|
+
stream.write(b"ls\n") # bytes or str
|
|
329
|
+
stream.write_eof() # half-close stdin; the process sees EOF
|
|
330
|
+
for chunk in stream: # bytes — merged output, what a terminal would show
|
|
264
331
|
print(chunk.decode(errors="replace"), end="")
|
|
265
332
|
print("exited", stream.exit_code)
|
|
266
333
|
```
|
|
267
334
|
|
|
268
|
-
`
|
|
269
|
-
|
|
270
|
-
|
|
335
|
+
`stream_exec` takes `command` and the rest as keywords, and hands back the
|
|
336
|
+
session without a round trip. `exit_code` is `None` until the stream is
|
|
337
|
+
exhausted. Leaving the `with` block — or calling `close()` — ends the session.
|
|
338
|
+
|
|
339
|
+
`iter_chunks()` yields `OutputChunk(data, is_stderr)` when you need the two
|
|
340
|
+
streams apart. Under `tty=True` the terminal merges them, so everything arrives
|
|
341
|
+
as stdout — set `tty=False` if you need the split.
|
|
271
342
|
|
|
272
343
|
For a headless one-shot that reads stdin (`jq`, `cat`, `claude -p`), pass
|
|
273
344
|
`close_stdin=True` so it sees end-of-input immediately instead of hanging.
|
|
@@ -297,47 +368,76 @@ for chunk in boxd.machines.logs(id, follow=True): # stays open
|
|
|
297
368
|
### Files
|
|
298
369
|
|
|
299
370
|
```python
|
|
300
|
-
|
|
301
|
-
|
|
371
|
+
from pathlib import Path
|
|
372
|
+
|
|
373
|
+
written = boxd.machines.files.upload(id, "/app/config.json", '{"debug": true}')
|
|
374
|
+
boxd.machines.files.upload(id, "/app/data.bin", Path("local.bin").read_bytes())
|
|
302
375
|
data = boxd.machines.files.download(id, "/app/output.json") # bytes
|
|
303
376
|
```
|
|
304
377
|
|
|
378
|
+
`upload` takes `str` or `bytes`, streams it in chunks so large files are fine,
|
|
379
|
+
and returns the number of bytes the machine confirmed it wrote.
|
|
380
|
+
|
|
305
381
|
### Ports and proxies
|
|
306
382
|
|
|
307
383
|
```python
|
|
308
|
-
boxd.machines.ports.expose(id, 8080)
|
|
309
|
-
boxd.machines.ports.expose(id, 5353, protocol="udp")
|
|
310
|
-
|
|
384
|
+
fwd = boxd.machines.ports.expose(id, 8080) # public TCP forward
|
|
385
|
+
boxd.machines.ports.expose(id, 5353, protocol="udp") # "tcp" | "udp" | "both"
|
|
386
|
+
fwd.dns, fwd.public_port, fwd.machine_port, fwd.protocol
|
|
387
|
+
fwd.machine_id, fwd.machine_name
|
|
388
|
+
boxd.machines.ports.unexpose(id, 8080) # echoes back what it removed
|
|
389
|
+
boxd.machines.ports.list(id) # one machine's forwards
|
|
311
390
|
boxd.machines.ports.list() # every forward you own
|
|
312
391
|
```
|
|
313
392
|
|
|
393
|
+
Connect on `dns:public_port`. Max 3 forwards per machine. Re-exposing a machine
|
|
394
|
+
port keeps its public port and just updates the protocol set; `"both"` shares
|
|
395
|
+
one public port across TCP and UDP.
|
|
396
|
+
|
|
314
397
|
`ports.list()` is account-wide — pass a machine to narrow it, or filter on
|
|
315
398
|
`.machine_id` / `.machine_name`.
|
|
316
399
|
|
|
317
400
|
```python
|
|
318
|
-
boxd.machines.proxies.create("my-machine", "api", 3001) # api.<machine>...
|
|
401
|
+
route = boxd.machines.proxies.create("my-machine", "api", 3001) # api.<machine>...
|
|
402
|
+
route.name, route.port
|
|
319
403
|
routes = boxd.machines.proxies.list("my-machine")
|
|
404
|
+
routes[0].name # str | None — None on the machine's default route
|
|
405
|
+
routes[0].domain # the hostname this route answers on
|
|
320
406
|
routes[0].port # int — where traffic actually goes
|
|
321
407
|
routes[0].port_mode # "locked" (you pinned it) | "auto" (detected for you)
|
|
322
|
-
routes[0].
|
|
408
|
+
routes[0].is_default
|
|
409
|
+
routes[0].machine_id, routes[0].machine_name
|
|
323
410
|
boxd.machines.proxies.set_port("my-machine", 3000, name="api")
|
|
324
411
|
boxd.machines.proxies.set_port("my-machine", "auto") # default route, auto-detected
|
|
325
412
|
boxd.machines.proxies.delete("my-machine", "api")
|
|
326
413
|
```
|
|
327
414
|
|
|
328
|
-
These take an id or a name, like everything else on `machines`.
|
|
415
|
+
These take an id or a name, like everything else on `machines`. `name` is a
|
|
416
|
+
subdomain label: lowercase letters, digits and hyphens, not starting or ending
|
|
417
|
+
with one. `create` answers as soon as the route is accepted, so it confirms the
|
|
418
|
+
subdomain and the port it was pointed at; `list()` reports the full domain and
|
|
419
|
+
the resolved port.
|
|
329
420
|
|
|
330
421
|
### Checkpoints
|
|
331
422
|
|
|
332
423
|
Per-machine captures, restored in place. They are deleted with the machine.
|
|
333
424
|
|
|
334
425
|
```python
|
|
335
|
-
boxd.machines.checkpoints.create(id, "before-upgrade")
|
|
336
|
-
|
|
426
|
+
cp = boxd.machines.checkpoints.create(id, "before-upgrade")
|
|
427
|
+
cp.id, cp.name, cp.status
|
|
428
|
+
saved = boxd.machines.checkpoints.list(id)
|
|
429
|
+
saved[0].size_bytes
|
|
430
|
+
saved[0].created_at # datetime
|
|
431
|
+
saved[0].created_by # str | None
|
|
432
|
+
saved[0].available # restorable right now
|
|
337
433
|
boxd.machines.checkpoints.restore(id, "before-upgrade")
|
|
338
434
|
boxd.machines.checkpoints.delete(id, "before-upgrade")
|
|
339
435
|
```
|
|
340
436
|
|
|
437
|
+
The machine must be running to take a checkpoint. `status` is `"pending"` until
|
|
438
|
+
the artifact lands, then `"ready"` (or `"failed"`); `restore` wants one that is
|
|
439
|
+
`"ready"` and `available`.
|
|
440
|
+
|
|
341
441
|
## Environment variables and secrets
|
|
342
442
|
|
|
343
443
|
Two namespaces with identical methods. The difference is that a secret's value
|
|
@@ -347,6 +447,7 @@ is write-only — the server never returns it, and the `Secret` model has no
|
|
|
347
447
|
```python
|
|
348
448
|
boxd.env.set("MODE", "production", scope="all")
|
|
349
449
|
boxd.env.list() # EnvVar(name, scope, value)
|
|
450
|
+
boxd.env.list(org="acme")
|
|
350
451
|
boxd.env.delete("MODE", scope="all")
|
|
351
452
|
|
|
352
453
|
boxd.secrets.set("API_TOKEN", "s3cr3t", scope="shared")
|
|
@@ -354,6 +455,10 @@ boxd.secrets.list() # Secret(name, scope) — no value
|
|
|
354
455
|
boxd.secrets.delete("API_TOKEN", scope="shared")
|
|
355
456
|
```
|
|
356
457
|
|
|
458
|
+
`scope` defaults to `"shared"` on `set` and `delete`; `list` takes `org` only
|
|
459
|
+
and reports every scope. Pass `org="acme"` to any of these to work in an
|
|
460
|
+
organization instead of your personal scope.
|
|
461
|
+
|
|
357
462
|
`set`, `delete` and `move` each return the server's human-readable
|
|
358
463
|
confirmation of what it did.
|
|
359
464
|
|
|
@@ -380,12 +485,15 @@ the same name moving in, and vice versa.
|
|
|
380
485
|
## Snapshots and disks
|
|
381
486
|
|
|
382
487
|
```python
|
|
383
|
-
boxd.snapshots.create(machine_id, "golden") # re-saving bumps the version
|
|
384
|
-
|
|
488
|
+
snap = boxd.snapshots.create(machine_id, "golden") # re-saving bumps the version
|
|
489
|
+
snap.id, snap.name, snap.version, snap.status
|
|
490
|
+
boxd.snapshots.get("golden") # by name or id
|
|
385
491
|
boxd.snapshots.list()
|
|
386
492
|
boxd.snapshots.delete("golden")
|
|
493
|
+
boxd.snapshots.list(org="acme") # `org` works on get/list/delete too
|
|
387
494
|
|
|
388
|
-
disk = boxd.disks.create("data", "10G")
|
|
495
|
+
disk = boxd.disks.create("data", "10G") # bytes or a human string
|
|
496
|
+
disk.id, disk.name, disk.size_bytes, disk.status
|
|
389
497
|
boxd.disks.attach(disk.id, machine_id, "/mnt/data")
|
|
390
498
|
boxd.disks.attach(disk.id, machine_id, "/mnt/data", read_only=True)
|
|
391
499
|
boxd.disks.detach(disk.id, machine_id)
|
|
@@ -393,27 +501,95 @@ boxd.disks.list()
|
|
|
393
501
|
boxd.disks.delete(disk.id)
|
|
394
502
|
```
|
|
395
503
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
504
|
+
The machine must be running to snapshot it, and `create` answers before the
|
|
505
|
+
artifact lands — `status` is `"pending"` until it does. Snapshots stay inside
|
|
506
|
+
one organization.
|
|
507
|
+
|
|
508
|
+
A disk is always created writable; read-only is chosen per attachment. A disk
|
|
509
|
+
can be attached to only one machine at a time.
|
|
510
|
+
|
|
511
|
+
`create` confirms only what the server can answer immediately; the full records
|
|
512
|
+
come back from `get` and `list`:
|
|
400
513
|
|
|
401
|
-
|
|
514
|
+
```python
|
|
515
|
+
snapshot = boxd.snapshots.get("golden")
|
|
516
|
+
snapshot.id, snapshot.name
|
|
517
|
+
snapshot.version # int | None — latest ready version; None = nothing captured yet
|
|
518
|
+
snapshot.status # "pending" | "ready" | "failed"
|
|
519
|
+
snapshot.size_bytes
|
|
520
|
+
snapshot.created_at # datetime | None — the first capture; it stays put
|
|
521
|
+
snapshot.updated_at # datetime | None — the most recent capture
|
|
522
|
+
snapshot.vcpu # the sizing the machine was captured at
|
|
523
|
+
snapshot.memory_bytes
|
|
524
|
+
snapshot.use_count # machines restored from it so far
|
|
525
|
+
|
|
526
|
+
volume = boxd.disks.list()[0]
|
|
527
|
+
volume.id, volume.name, volume.size_bytes
|
|
528
|
+
volume.status # "creating" | "ready" | "destroyed" — attach once "ready"
|
|
529
|
+
volume.created_at # datetime | None
|
|
530
|
+
volume.attachments # [DiskAttachment(machine_id, machine_name, mount_path, mount_mode)]
|
|
531
|
+
# mount_mode is "ro" or "rw"
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
## Organizations, credentials, account
|
|
402
535
|
|
|
403
536
|
```python
|
|
404
|
-
orgs = boxd.orgs.list() # a plain list
|
|
537
|
+
orgs = boxd.orgs.list() # a plain list
|
|
538
|
+
orgs[0].id
|
|
539
|
+
orgs[0].name # display label — it can repeat across organizations
|
|
540
|
+
orgs[0].slug # the organization's unique key
|
|
541
|
+
orgs[0].is_admin # you administer it
|
|
542
|
+
orgs[0].is_default # where your personal machines are billed
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
Anywhere a call takes `org`, it accepts an organization's name or id.
|
|
405
546
|
|
|
406
|
-
|
|
547
|
+
```python
|
|
548
|
+
key = boxd.api_keys.create(
|
|
549
|
+
"ci",
|
|
550
|
+
org="acme", # the organization the key is fenced to
|
|
551
|
+
kind="member", # "member" (default) acts as you within that org;
|
|
552
|
+
# "org" is a userless service credential, limited
|
|
553
|
+
# to the org's shared fleet, org admin only
|
|
554
|
+
expires_in=60 * 60 * 24 * 30, # seconds; 0 for no expiry
|
|
555
|
+
)
|
|
556
|
+
key.id
|
|
407
557
|
key.api_key # the raw key — shown once, store it now
|
|
408
|
-
|
|
558
|
+
key.expires_at # datetime | None
|
|
559
|
+
|
|
560
|
+
keys = boxd.api_keys.list()
|
|
561
|
+
keys[0].name, keys[0].key_prefix, keys[0].created_at
|
|
562
|
+
keys[0].last_used_at # datetime | None — None = never used
|
|
563
|
+
keys[0].expires_at # datetime | None — None = no expiry
|
|
564
|
+
keys[0].org, keys[0].kind # "member" | "org"
|
|
409
565
|
boxd.api_keys.delete(key.id)
|
|
566
|
+
```
|
|
410
567
|
|
|
568
|
+
Every key is fenced to exactly one organization. Deleting one takes effect
|
|
569
|
+
immediately.
|
|
411
570
|
|
|
571
|
+
```python
|
|
572
|
+
from pathlib import Path
|
|
412
573
|
|
|
413
574
|
me = boxd.account.get()
|
|
414
|
-
me.user_id
|
|
415
|
-
|
|
416
|
-
|
|
575
|
+
me.user_id
|
|
576
|
+
me.display_name # str | None — falls back to `user_id`
|
|
577
|
+
me.pubkey_fingerprints # list[str]
|
|
578
|
+
me.billing.subscription_status # "active", "trialing", … | None
|
|
579
|
+
me.billing.past_due_since # datetime | None
|
|
580
|
+
me.billing.max_vms # effective quota
|
|
581
|
+
me.billing.vcpu, me.billing.memory_bytes
|
|
582
|
+
|
|
583
|
+
pubkey = (Path.home() / ".ssh/id_ed25519.pub").read_text()
|
|
584
|
+
boxd.account.link_ssh_key(pubkey)
|
|
585
|
+
boxd.account.link_ssh_key(
|
|
586
|
+
pubkey,
|
|
587
|
+
device_id="laptop", # one key kept per device — re-linking replaces it
|
|
588
|
+
label="MacBook Pro", # shown wherever the device is listed
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
cfg = boxd.account.config()
|
|
592
|
+
cfg.default_image, cfg.zone
|
|
417
593
|
```
|
|
418
594
|
|
|
419
595
|
## Errors
|
|
@@ -436,8 +612,9 @@ except NotFoundError:
|
|
|
436
612
|
...
|
|
437
613
|
```
|
|
438
614
|
|
|
439
|
-
Every error carries `.message
|
|
440
|
-
`"not_found"`)
|
|
615
|
+
Every error carries `.message`, `.code` (the canonical status name, e.g.
|
|
616
|
+
`"not_found"`) and `.grpc_code`, the numeric
|
|
617
|
+
[status code](https://grpc.github.io/grpc/core/md_doc_statuscodes.html).
|
|
441
618
|
|
|
442
619
|
Connection failures are retried with exponential backoff, `max_retries` times.
|
|
443
620
|
Timeouts are never retried — the server may already have applied the request —
|
|
@@ -455,15 +632,3 @@ A new version of boxd is available (v0.2.0, you have v0.1.9). Update with:
|
|
|
455
632
|
It fires at most once per process and never causes a request to fail.
|
|
456
633
|
|
|
457
634
|
The installed version is available as `boxd.__version__`.
|
|
458
|
-
|
|
459
|
-
## Development
|
|
460
|
-
|
|
461
|
-
```bash
|
|
462
|
-
cd sdk/python
|
|
463
|
-
python -m venv .venv
|
|
464
|
-
source .venv/bin/activate
|
|
465
|
-
pip install -e ".[dev]"
|
|
466
|
-
|
|
467
|
-
pytest # unit tests
|
|
468
|
-
bash scripts/compile_proto.sh # regenerate stubs after an API change
|
|
469
|
-
```
|