boxd 0.1.9.dev37__tar.gz → 0.2.0.dev38__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/PKG-INFO +469 -0
- boxd-0.2.0.dev38/README.md +437 -0
- {boxd-0.1.9.dev37 → boxd-0.2.0.dev38}/pyproject.toml +8 -6
- boxd-0.2.0.dev38/src/boxd/__init__.py +132 -0
- boxd-0.2.0.dev38/src/boxd/_client.py +148 -0
- boxd-0.2.0.dev38/src/boxd/_credentials.py +304 -0
- boxd-0.2.0.dev38/src/boxd/_generated/api_pb2.py +358 -0
- {boxd-0.1.9.dev37 → boxd-0.2.0.dev38}/src/boxd/_generated/api_pb2_grpc.py +458 -102
- boxd-0.2.0.dev38/src/boxd/_mappers.py +331 -0
- boxd-0.2.0.dev38/src/boxd/_requests.py +246 -0
- boxd-0.2.0.dev38/src/boxd/_streaming.py +195 -0
- boxd-0.2.0.dev38/src/boxd/_transport.py +208 -0
- boxd-0.2.0.dev38/src/boxd/_urls.py +101 -0
- {boxd-0.1.9.dev37 → boxd-0.2.0.dev38}/src/boxd/_version_check.py +31 -71
- boxd-0.2.0.dev38/src/boxd/errors.py +97 -0
- boxd-0.2.0.dev38/src/boxd/models.py +402 -0
- boxd-0.2.0.dev38/src/boxd/resources/__init__.py +15 -0
- boxd-0.2.0.dev38/src/boxd/resources/account.py +79 -0
- boxd-0.2.0.dev38/src/boxd/resources/credentials.py +97 -0
- boxd-0.2.0.dev38/src/boxd/resources/disks.py +112 -0
- boxd-0.2.0.dev38/src/boxd/resources/machines.py +1046 -0
- boxd-0.2.0.dev38/src/boxd/resources/orgs.py +27 -0
- boxd-0.2.0.dev38/src/boxd/resources/snapshots.py +78 -0
- boxd-0.2.0.dev38/src/boxd/resources/vars.py +249 -0
- boxd-0.2.0.dev38/src/boxd.egg-info/PKG-INFO +469 -0
- boxd-0.2.0.dev38/src/boxd.egg-info/SOURCES.txt +38 -0
- {boxd-0.1.9.dev37 → boxd-0.2.0.dev38}/src/boxd.egg-info/requires.txt +1 -0
- boxd-0.2.0.dev38/tests/test_credentials.py +305 -0
- boxd-0.2.0.dev38/tests/test_mappers.py +453 -0
- boxd-0.2.0.dev38/tests/test_namespaces.py +832 -0
- boxd-0.2.0.dev38/tests/test_requests.py +203 -0
- boxd-0.2.0.dev38/tests/test_streaming.py +132 -0
- boxd-0.2.0.dev38/tests/test_transport.py +298 -0
- boxd-0.2.0.dev38/tests/test_urls.py +139 -0
- boxd-0.2.0.dev38/tests/test_version_check.py +121 -0
- boxd-0.1.9.dev37/PKG-INFO +0 -499
- boxd-0.1.9.dev37/README.md +0 -468
- boxd-0.1.9.dev37/src/boxd/__init__.py +0 -131
- boxd-0.1.9.dev37/src/boxd/_generated/api_pb2.py +0 -310
- boxd-0.1.9.dev37/src/boxd/_sync.py +0 -547
- boxd-0.1.9.dev37/src/boxd/_utils.py +0 -73
- boxd-0.1.9.dev37/src/boxd/aio.py +0 -98
- boxd-0.1.9.dev37/src/boxd/auth.py +0 -118
- boxd-0.1.9.dev37/src/boxd/billing.py +0 -128
- boxd-0.1.9.dev37/src/boxd/box.py +0 -424
- boxd-0.1.9.dev37/src/boxd/boxes.py +0 -192
- boxd-0.1.9.dev37/src/boxd/client.py +0 -176
- boxd-0.1.9.dev37/src/boxd/disks.py +0 -111
- boxd-0.1.9.dev37/src/boxd/domains.py +0 -54
- boxd-0.1.9.dev37/src/boxd/errors.py +0 -62
- boxd-0.1.9.dev37/src/boxd/exec.py +0 -149
- boxd-0.1.9.dev37/src/boxd/networks.py +0 -43
- boxd-0.1.9.dev37/src/boxd/templates.py +0 -113
- boxd-0.1.9.dev37/src/boxd/tokens.py +0 -51
- boxd-0.1.9.dev37/src/boxd/types.py +0 -151
- boxd-0.1.9.dev37/src/boxd.egg-info/PKG-INFO +0 -499
- boxd-0.1.9.dev37/src/boxd.egg-info/SOURCES.txt +0 -41
- boxd-0.1.9.dev37/tests/test_auth.py +0 -33
- boxd-0.1.9.dev37/tests/test_boxes.py +0 -6
- boxd-0.1.9.dev37/tests/test_e2e.py +0 -324
- boxd-0.1.9.dev37/tests/test_e2e_v2.py +0 -93
- boxd-0.1.9.dev37/tests/test_exec.py +0 -139
- boxd-0.1.9.dev37/tests/test_files.py +0 -21
- boxd-0.1.9.dev37/tests/test_lifecycle.py +0 -21
- boxd-0.1.9.dev37/tests/test_no_shape.py +0 -67
- boxd-0.1.9.dev37/tests/test_proxies.py +0 -33
- boxd-0.1.9.dev37/tests/test_utils.py +0 -47
- boxd-0.1.9.dev37/tests/test_v2.py +0 -62
- boxd-0.1.9.dev37/tests/test_version_check.py +0 -178
- {boxd-0.1.9.dev37 → boxd-0.2.0.dev38}/LICENSE +0 -0
- {boxd-0.1.9.dev37 → boxd-0.2.0.dev38}/setup.cfg +0 -0
- {boxd-0.1.9.dev37 → boxd-0.2.0.dev38}/src/boxd/_generated/__init__.py +0 -0
- {boxd-0.1.9.dev37 → boxd-0.2.0.dev38}/src/boxd.egg-info/dependency_links.txt +0 -0
- {boxd-0.1.9.dev37 → boxd-0.2.0.dev38}/src/boxd.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: boxd
|
|
3
|
+
Version: 0.2.0.dev38
|
|
4
|
+
Summary: Python SDK for the boxd cloud VM platform
|
|
5
|
+
Author: Azin
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://boxd.sh
|
|
8
|
+
Keywords: boxd,vm,microvm,sandbox,compute,grpc,sdk
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: grpcio>=1.60
|
|
22
|
+
Requires-Dist: protobuf>=4.25
|
|
23
|
+
Requires-Dist: pydantic>=2
|
|
24
|
+
Requires-Dist: httpx<1,>=0.27
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: grpcio-tools>=1.60; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
29
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
30
|
+
Requires-Dist: twine>=4.0; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# boxd Python SDK
|
|
34
|
+
|
|
35
|
+
Python SDK for the [boxd](https://boxd.sh) cloud machine platform. Create
|
|
36
|
+
machines, run commands in them, move files, and manage everything around them.
|
|
37
|
+
|
|
38
|
+
Requires Python 3.10+.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install boxd
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from boxd import Boxd
|
|
50
|
+
|
|
51
|
+
boxd = Boxd(api_key="bxd_...")
|
|
52
|
+
|
|
53
|
+
machine = boxd.machines.create("my-machine")
|
|
54
|
+
boxd.machines.wait_until_ready(machine.id)
|
|
55
|
+
|
|
56
|
+
result = boxd.machines.exec(machine.id, "uname -a")
|
|
57
|
+
print(result.stdout)
|
|
58
|
+
|
|
59
|
+
boxd.machines.delete(machine.id)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Everything follows the same shape: **`boxd.<resource>.<verb>(id, ...)`**.
|
|
63
|
+
Resources return plain data — a `Machine` has fields, not methods.
|
|
64
|
+
|
|
65
|
+
## Client
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
Boxd() # production
|
|
69
|
+
Boxd(api_key="bxd_...")
|
|
70
|
+
Boxd(base_url="https://boxd.example.com:9443") # any other cluster
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
| Argument | Environment variable | Default |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| `api_key` | `BOXD_API_KEY` | — |
|
|
76
|
+
| `token` | `BOXD_TOKEN` | — |
|
|
77
|
+
| `base_url` | `BOXD_BASE_URL` | production |
|
|
78
|
+
| `timeout` | — | 60 seconds |
|
|
79
|
+
| `max_retries` | — | 2 |
|
|
80
|
+
|
|
81
|
+
There is **no `environment` argument**. One `base_url` selects a cluster and
|
|
82
|
+
everything else follows from it.
|
|
83
|
+
|
|
84
|
+
The client holds a connection, so keep one around rather than making a new one
|
|
85
|
+
per call. Close it when you're done — or use it as a context manager:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
with Boxd(api_key="bxd_...") as boxd:
|
|
89
|
+
...
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Authentication
|
|
93
|
+
|
|
94
|
+
The first of these that is present wins:
|
|
95
|
+
|
|
96
|
+
1. `token=` — used as given
|
|
97
|
+
2. `api_key=` — exchanged for a short-lived credential and kept fresh for you
|
|
98
|
+
3. `BOXD_TOKEN`, then `BOXD_API_KEY`
|
|
99
|
+
4. running inside a boxd machine — see below
|
|
100
|
+
5. otherwise `AuthenticationError`
|
|
101
|
+
|
|
102
|
+
If your key is revoked mid-session, the SDK fails fast with
|
|
103
|
+
`AuthenticationError` rather than retrying.
|
|
104
|
+
|
|
105
|
+
### Inside a machine
|
|
106
|
+
|
|
107
|
+
Inside a boxd machine, `Boxd()` authenticates automatically — no API key
|
|
108
|
+
needed, and it talks to that machine's own cluster unless you pass `base_url`.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from boxd import Boxd
|
|
112
|
+
|
|
113
|
+
boxd = Boxd()
|
|
114
|
+
for machine in boxd.machines.list():
|
|
115
|
+
print(machine.name, machine.status)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
One limit: inside a **shared** machine the automatic credential can manage the
|
|
119
|
+
organization's shared machines, but cannot read environment variables or
|
|
120
|
+
secrets, and cannot reach private machines. Pass an API key for those.
|
|
121
|
+
|
|
122
|
+
## Sync and async
|
|
123
|
+
|
|
124
|
+
`Boxd` and `AsyncBoxd` are the same surface — same namespaces, same method
|
|
125
|
+
names, same arguments, same return types. Switching is `await` and an import,
|
|
126
|
+
not a rewrite.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from boxd import AsyncBoxd
|
|
130
|
+
|
|
131
|
+
boxd = AsyncBoxd(api_key="bxd_...")
|
|
132
|
+
|
|
133
|
+
machine = await boxd.machines.create("my-machine")
|
|
134
|
+
result = await boxd.machines.exec(machine.id, "echo hello")
|
|
135
|
+
await boxd.close()
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Use `AsyncBoxd` when you already have an event loop (FastAPI, asyncio scripts,
|
|
139
|
+
anyio). Use `Boxd` everywhere else — scripts, notebooks, Django views.
|
|
140
|
+
|
|
141
|
+
## Machines
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
machine = boxd.machines.create(
|
|
145
|
+
"my-machine",
|
|
146
|
+
vcpu=4,
|
|
147
|
+
memory="16G",
|
|
148
|
+
env={"MODE": "production"},
|
|
149
|
+
)
|
|
150
|
+
boxd.machines.get("my-machine") # by name or id
|
|
151
|
+
boxd.machines.list() # a plain list
|
|
152
|
+
boxd.machines.delete("my-machine")
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
State:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
boxd.machines.start(id)
|
|
159
|
+
boxd.machines.stop(id)
|
|
160
|
+
boxd.machines.reboot(id)
|
|
161
|
+
boxd.machines.pause(id) # suspend to RAM — fast to resume
|
|
162
|
+
boxd.machines.resume(id)
|
|
163
|
+
boxd.machines.hibernate(id) # suspend to disk — cheaper, slower to wake
|
|
164
|
+
boxd.machines.wake(id)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Everything else:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
boxd.machines.fork("my-machine", "my-copy") # live clone
|
|
171
|
+
boxd.machines.rename(id, "new-name") # reboots the machine
|
|
172
|
+
boxd.machines.share(id) # visible to your whole org
|
|
173
|
+
boxd.machines.unshare(id)
|
|
174
|
+
boxd.machines.set_auto_suspend_timeout(id, 300) # seconds idle; 0 disables
|
|
175
|
+
boxd.machines.set_auto_hibernate_timeout(id, 0)
|
|
176
|
+
boxd.machines.wait_until_ready(id)
|
|
177
|
+
boxd.machines.suggest_name()
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`create` and `fork` return once the machine is scheduled, not once it is
|
|
181
|
+
usable. Call `wait_until_ready` before doing anything that depends on it
|
|
182
|
+
running — especially before forking it again.
|
|
183
|
+
|
|
184
|
+
### The `Machine` record
|
|
185
|
+
|
|
186
|
+
Related fields travel together, so you read one object instead of remembering
|
|
187
|
+
which flat field pairs with which.
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
machine.id, machine.name, machine.status, machine.image_ref
|
|
191
|
+
machine.restart_policy # str | None
|
|
192
|
+
machine.created_at # datetime | None — None on older machines
|
|
193
|
+
|
|
194
|
+
machine.resources.vcpu # what the machine actually got, not what you
|
|
195
|
+
machine.resources.memory_bytes # asked for — always concrete
|
|
196
|
+
machine.resources.disk_bytes
|
|
197
|
+
|
|
198
|
+
machine.org # OrgRef(id, name) | None — None = personal quota
|
|
199
|
+
machine.shared # shared with that org, or private to you
|
|
200
|
+
|
|
201
|
+
machine.access.ssh_port # int | None — None until allocated
|
|
202
|
+
machine.access.domain
|
|
203
|
+
machine.access.url # https://<name>.<domain>
|
|
204
|
+
|
|
205
|
+
machine.idle.suspend_after # seconds; 0 = that timer is disabled
|
|
206
|
+
machine.idle.hibernate_after
|
|
207
|
+
machine.idle.destroy_after
|
|
208
|
+
|
|
209
|
+
machine.source # MachineSource | None — None = booted from an image
|
|
210
|
+
machine.source.kind # "fork" | "snapshot"
|
|
211
|
+
machine.source.name # source machine, or snapshot name
|
|
212
|
+
machine.source.version # int | None — snapshots only; a fork has none
|
|
213
|
+
machine.source.id # str | None — provenance; may not resolve
|
|
214
|
+
|
|
215
|
+
machine.hibernated_at # datetime | None — None = not hibernated
|
|
216
|
+
machine.last_connected_at # datetime | None — None = never connected
|
|
217
|
+
machine.boot_time_ms # int | None — last boot; None = never booted
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`None` always means "not set": a port that was never allocated, a boot that
|
|
221
|
+
never happened, an org you do not have. Where `0` is a real answer — a disabled
|
|
222
|
+
idle timer — it stays `0`.
|
|
223
|
+
|
|
224
|
+
`org` is the org the machine belongs to and is billed to; `shared` says whether
|
|
225
|
+
your teammates can see it. A private machine can still be org-billed, so `org`
|
|
226
|
+
set with `shared=False` is normal, not a contradiction.
|
|
227
|
+
|
|
228
|
+
`source.id` points at the machine or snapshot this one came from. It is a record
|
|
229
|
+
of where the machine came from, not a live link — **it may not resolve**, and a
|
|
230
|
+
lookup that finds nothing is normal.
|
|
231
|
+
|
|
232
|
+
### Creating from a snapshot
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
boxd.snapshots.create(machine_id, "golden")
|
|
236
|
+
machine = boxd.machines.create("from-golden", from_snapshot="golden")
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Exec
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
result = boxd.machines.exec(id, "cargo build")
|
|
243
|
+
result.stdout # str
|
|
244
|
+
result.stderr # str — populated for non-PTY execs
|
|
245
|
+
result.exit_code # int
|
|
246
|
+
result.success # bool
|
|
247
|
+
|
|
248
|
+
boxd.machines.exec(id, ["echo", "a b"]) # a list is quoted for you
|
|
249
|
+
boxd.machines.exec(id, "env", env={"FOO": "bar"})
|
|
250
|
+
boxd.machines.exec(id, "cargo build", timeout=30) # seconds
|
|
251
|
+
|
|
252
|
+
# Under a PTY, stderr merges into stdout and `stderr` comes back empty.
|
|
253
|
+
boxd.machines.exec(id, "top -b -n1", tty=True, cols=120, rows=40)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
For anything interactive, `stream_exec` gives you a live session — the one
|
|
257
|
+
handle in the SDK, because a bidirectional stream really is stateful:
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
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:
|
|
264
|
+
print(chunk.decode(errors="replace"), end="")
|
|
265
|
+
print("exited", stream.exit_code)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
`iter_chunks()` tags each slice with `is_stderr` when you need the two streams
|
|
269
|
+
apart. Under `tty=True` the terminal merges them, so everything arrives as
|
|
270
|
+
stdout — set `tty=False` if you need the split.
|
|
271
|
+
|
|
272
|
+
For a headless one-shot that reads stdin (`jq`, `cat`, `claude -p`), pass
|
|
273
|
+
`close_stdin=True` so it sees end-of-input immediately instead of hanging.
|
|
274
|
+
Combining it with `tty=True` raises `ValueError` — a shell needs stdin open.
|
|
275
|
+
|
|
276
|
+
Set the terminal size with `cols`/`rows`, and call `stream.resize(cols, rows)`
|
|
277
|
+
when the local terminal changes size:
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
import shutil, signal
|
|
281
|
+
|
|
282
|
+
cols, rows = shutil.get_terminal_size()
|
|
283
|
+
stream = boxd.machines.stream_exec(id, command="htop", tty=True, cols=cols, rows=rows)
|
|
284
|
+
signal.signal(signal.SIGWINCH, lambda *_: stream.resize(*shutil.get_terminal_size()))
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Logs
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
for chunk in boxd.machines.logs(id):
|
|
291
|
+
print(chunk.decode(errors="replace"), end="")
|
|
292
|
+
|
|
293
|
+
for chunk in boxd.machines.logs(id, follow=True): # stays open
|
|
294
|
+
...
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Files
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
boxd.machines.files.upload(id, "/app/config.json", '{"debug": true}')
|
|
301
|
+
boxd.machines.files.upload(id, "/app/data.bin", open("local.bin", "rb").read())
|
|
302
|
+
data = boxd.machines.files.download(id, "/app/output.json") # bytes
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Ports and proxies
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
boxd.machines.ports.expose(id, 8080) # public TCP forward
|
|
309
|
+
boxd.machines.ports.expose(id, 5353, protocol="udp")
|
|
310
|
+
boxd.machines.ports.unexpose(id, 8080)
|
|
311
|
+
boxd.machines.ports.list() # every forward you own
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
`ports.list()` is account-wide — pass a machine to narrow it, or filter on
|
|
315
|
+
`.machine_id` / `.machine_name`.
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
boxd.machines.proxies.create("my-machine", "api", 3001) # api.<machine>...
|
|
319
|
+
routes = boxd.machines.proxies.list("my-machine")
|
|
320
|
+
routes[0].port # int — where traffic actually goes
|
|
321
|
+
routes[0].port_mode # "locked" (you pinned it) | "auto" (detected for you)
|
|
322
|
+
routes[0].machine_id
|
|
323
|
+
boxd.machines.proxies.set_port("my-machine", 3000, name="api")
|
|
324
|
+
boxd.machines.proxies.set_port("my-machine", "auto") # default route, auto-detected
|
|
325
|
+
boxd.machines.proxies.delete("my-machine", "api")
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
These take an id or a name, like everything else on `machines`.
|
|
329
|
+
|
|
330
|
+
### Checkpoints
|
|
331
|
+
|
|
332
|
+
Per-machine captures, restored in place. They are deleted with the machine.
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
boxd.machines.checkpoints.create(id, "before-upgrade")
|
|
336
|
+
boxd.machines.checkpoints.list(id)
|
|
337
|
+
boxd.machines.checkpoints.restore(id, "before-upgrade")
|
|
338
|
+
boxd.machines.checkpoints.delete(id, "before-upgrade")
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Environment variables and secrets
|
|
342
|
+
|
|
343
|
+
Two namespaces with identical methods. The difference is that a secret's value
|
|
344
|
+
is write-only — the server never returns it, and the `Secret` model has no
|
|
345
|
+
`value` field at all.
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
boxd.env.set("MODE", "production", scope="all")
|
|
349
|
+
boxd.env.list() # EnvVar(name, scope, value)
|
|
350
|
+
boxd.env.delete("MODE", scope="all")
|
|
351
|
+
|
|
352
|
+
boxd.secrets.set("API_TOKEN", "s3cr3t", scope="shared")
|
|
353
|
+
boxd.secrets.list() # Secret(name, scope) — no value
|
|
354
|
+
boxd.secrets.delete("API_TOKEN", scope="shared")
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
`set`, `delete` and `move` each return the server's human-readable
|
|
358
|
+
confirmation of what it did.
|
|
359
|
+
|
|
360
|
+
Scope decides which machines a name applies to:
|
|
361
|
+
|
|
362
|
+
| Scope | Applies to |
|
|
363
|
+
|---|---|
|
|
364
|
+
| `private` | only your machines in that organization |
|
|
365
|
+
| `shared` | the organization's shared machines |
|
|
366
|
+
| `all` | every machine in the organization |
|
|
367
|
+
|
|
368
|
+
Scope is part of a name's identity — the same name can exist in several scopes
|
|
369
|
+
at once — so changing it is a `move` between two addresses, and both ends are
|
|
370
|
+
required:
|
|
371
|
+
|
|
372
|
+
```python
|
|
373
|
+
boxd.secrets.move("API_TOKEN", from_scope="private", to_scope="shared")
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Calling it twice fails the second time. Environment variables and secrets share
|
|
377
|
+
one namespace within a scope, so an environment variable can block a secret of
|
|
378
|
+
the same name moving in, and vice versa.
|
|
379
|
+
|
|
380
|
+
## Snapshots and disks
|
|
381
|
+
|
|
382
|
+
```python
|
|
383
|
+
boxd.snapshots.create(machine_id, "golden") # re-saving bumps the version
|
|
384
|
+
boxd.snapshots.get("golden")
|
|
385
|
+
boxd.snapshots.list()
|
|
386
|
+
boxd.snapshots.delete("golden")
|
|
387
|
+
|
|
388
|
+
disk = boxd.disks.create("data", "10G")
|
|
389
|
+
boxd.disks.attach(disk.id, machine_id, "/mnt/data")
|
|
390
|
+
boxd.disks.attach(disk.id, machine_id, "/mnt/data", read_only=True)
|
|
391
|
+
boxd.disks.detach(disk.id, machine_id)
|
|
392
|
+
boxd.disks.list()
|
|
393
|
+
boxd.disks.delete(disk.id)
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
A `Snapshot` carries both `created_at` (the first capture) and `updated_at` (the
|
|
397
|
+
most recent one — re-saving under the same name bumps the version). A `Disk`
|
|
398
|
+
carries `created_at` and a `status` of `"creating"`, `"ready"` or `"destroyed"`;
|
|
399
|
+
it can only be attached once it is `"ready"`.
|
|
400
|
+
|
|
401
|
+
## Organizations, credentials, billing, account
|
|
402
|
+
|
|
403
|
+
```python
|
|
404
|
+
orgs = boxd.orgs.list() # a plain list; each org has `is_default`
|
|
405
|
+
|
|
406
|
+
key = boxd.api_keys.create("ci", org="acme")
|
|
407
|
+
key.api_key # the raw key — shown once, store it now
|
|
408
|
+
boxd.api_keys.list()
|
|
409
|
+
boxd.api_keys.delete(key.id)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
me = boxd.account.get()
|
|
414
|
+
me.user_id, me.display_name, me.pubkey_fingerprints
|
|
415
|
+
boxd.account.link_ssh_key(open("~/.ssh/id_ed25519.pub").read())
|
|
416
|
+
boxd.account.config() # default image, cluster zone
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## Errors
|
|
420
|
+
|
|
421
|
+
```python
|
|
422
|
+
from boxd import (
|
|
423
|
+
BoxdError, # base class — catch this to catch everything
|
|
424
|
+
AuthenticationError, # no usable credential, or it was rejected
|
|
425
|
+
PermissionDeniedError, # authenticated, but not allowed
|
|
426
|
+
NotFoundError,
|
|
427
|
+
ConflictError, # already exists, or fights the current state
|
|
428
|
+
RateLimitError, # rate limit or quota
|
|
429
|
+
APIStatusError, # any other error from the server
|
|
430
|
+
APIConnectionError, # could not reach the server
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
boxd.machines.get("nope")
|
|
435
|
+
except NotFoundError:
|
|
436
|
+
...
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Every error carries `.message` and `.code` (the canonical status name, e.g.
|
|
440
|
+
`"not_found"`).
|
|
441
|
+
|
|
442
|
+
Connection failures are retried with exponential backoff, `max_retries` times.
|
|
443
|
+
Timeouts are never retried — the server may already have applied the request —
|
|
444
|
+
and neither is `AuthenticationError`.
|
|
445
|
+
|
|
446
|
+
## Update notices
|
|
447
|
+
|
|
448
|
+
The SDK prints a one-time note to stderr if the server reports a newer release:
|
|
449
|
+
|
|
450
|
+
```
|
|
451
|
+
A new version of boxd is available (v0.2.0, you have v0.1.9). Update with:
|
|
452
|
+
pip install --upgrade boxd
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
It fires at most once per process and never causes a request to fail.
|
|
456
|
+
|
|
457
|
+
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
|
+
```
|