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.
Files changed (41) hide show
  1. {boxd-0.1.1.dev7/src/boxd.egg-info → boxd-0.1.2}/PKG-INFO +123 -23
  2. {boxd-0.1.1.dev7 → boxd-0.1.2}/README.md +122 -20
  3. {boxd-0.1.1.dev7 → boxd-0.1.2}/pyproject.toml +1 -3
  4. {boxd-0.1.1.dev7 → boxd-0.1.2/src/boxd.egg-info}/PKG-INFO +123 -23
  5. {boxd-0.1.1.dev7 → boxd-0.1.2}/LICENSE +0 -0
  6. {boxd-0.1.1.dev7 → boxd-0.1.2}/setup.cfg +0 -0
  7. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/__init__.py +0 -0
  8. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/_generated/__init__.py +0 -0
  9. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/_generated/api_pb2.py +0 -0
  10. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/_generated/api_pb2_grpc.py +0 -0
  11. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/_sync.py +0 -0
  12. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/_utils.py +0 -0
  13. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/_version_check.py +0 -0
  14. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/aio.py +0 -0
  15. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/auth.py +0 -0
  16. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/box.py +0 -0
  17. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/boxes.py +0 -0
  18. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/client.py +0 -0
  19. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/disks.py +0 -0
  20. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/domains.py +0 -0
  21. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/errors.py +0 -0
  22. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/exec.py +0 -0
  23. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/networks.py +0 -0
  24. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/templates.py +0 -0
  25. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/tokens.py +0 -0
  26. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd/types.py +0 -0
  27. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd.egg-info/SOURCES.txt +0 -0
  28. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd.egg-info/dependency_links.txt +0 -0
  29. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd.egg-info/requires.txt +0 -0
  30. {boxd-0.1.1.dev7 → boxd-0.1.2}/src/boxd.egg-info/top_level.txt +0 -0
  31. {boxd-0.1.1.dev7 → boxd-0.1.2}/tests/test_auth.py +0 -0
  32. {boxd-0.1.1.dev7 → boxd-0.1.2}/tests/test_boxes.py +0 -0
  33. {boxd-0.1.1.dev7 → boxd-0.1.2}/tests/test_e2e.py +0 -0
  34. {boxd-0.1.1.dev7 → boxd-0.1.2}/tests/test_e2e_v2.py +0 -0
  35. {boxd-0.1.1.dev7 → boxd-0.1.2}/tests/test_exec.py +0 -0
  36. {boxd-0.1.1.dev7 → boxd-0.1.2}/tests/test_files.py +0 -0
  37. {boxd-0.1.1.dev7 → boxd-0.1.2}/tests/test_lifecycle.py +0 -0
  38. {boxd-0.1.1.dev7 → boxd-0.1.2}/tests/test_proxies.py +0 -0
  39. {boxd-0.1.1.dev7 → boxd-0.1.2}/tests/test_utils.py +0 -0
  40. {boxd-0.1.1.dev7 → boxd-0.1.2}/tests/test_v2.py +0 -0
  41. {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.1.dev7
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
- Env vars `BOXD_API_URL` / `BOXD_EXCHANGE_URL` override the preset too.
85
+ ### Environment variables
88
86
 
89
- Equivalent env vars: `BOXD_API_KEY`, `BOXD_TOKEN`, `BOXD_API_URL`, `BOXD_EXCHANGE_URL`.
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
- `Box` exposes the server-returned fields: `id`, `name`, `image`, `public_ip`, `status`, `url`, `boot_time_ms`. Forked VMs additionally carry `forked_from`. VMs returned by `c.box.get(...)` also expose `restart_policy`, `disk_bytes`, and `auto_suspend_timeout_secs`.
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) # interactive implies pty
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(box, mount_path="/mnt/data", read_only=True)
196
- d.detach(box)
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() # server assigns id
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) # 0 = server default
227
- t.token # "eyJ..."save this; list() will not return it
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
- print(info.jti, info.created_at, info.expires_at)
232
- c.token.revoke(info.jti)
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 import is the **sync API**, which wraps the async implementation using a dedicated event loop:
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 for scripts and notebooks
388
+ from boxd import Compute # sync — recommended default
289
389
  ```
290
390
 
291
- For async code, import from `boxd.aio`:
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 APIs are surface-equivalent only the call style (sync vs `await`) differs.
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
- Env vars `BOXD_API_URL` / `BOXD_EXCHANGE_URL` override the preset too.
54
+ ### Environment variables
55
55
 
56
- Equivalent env vars: `BOXD_API_KEY`, `BOXD_TOKEN`, `BOXD_API_URL`, `BOXD_EXCHANGE_URL`.
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
- `Box` exposes the server-returned fields: `id`, `name`, `image`, `public_ip`, `status`, `url`, `boot_time_ms`. Forked VMs additionally carry `forked_from`. VMs returned by `c.box.get(...)` also expose `restart_policy`, `disk_bytes`, and `auto_suspend_timeout_secs`.
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) # interactive implies pty
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(box, mount_path="/mnt/data", read_only=True)
163
- d.detach(box)
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() # server assigns id
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) # 0 = server default
194
- t.token # "eyJ..."save this; list() will not return it
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
- print(info.jti, info.created_at, info.expires_at)
199
- c.token.revoke(info.jti)
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 import is the **sync API**, which wraps the async implementation using a dedicated event loop:
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 for scripts and notebooks
357
+ from boxd import Compute # sync — recommended default
256
358
  ```
257
359
 
258
- For async code, import from `boxd.aio`:
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 APIs are surface-equivalent only the call style (sync vs `await`) differs.
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.1.dev7"
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.1.dev7
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
- Env vars `BOXD_API_URL` / `BOXD_EXCHANGE_URL` override the preset too.
85
+ ### Environment variables
88
86
 
89
- Equivalent env vars: `BOXD_API_KEY`, `BOXD_TOKEN`, `BOXD_API_URL`, `BOXD_EXCHANGE_URL`.
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
- `Box` exposes the server-returned fields: `id`, `name`, `image`, `public_ip`, `status`, `url`, `boot_time_ms`. Forked VMs additionally carry `forked_from`. VMs returned by `c.box.get(...)` also expose `restart_policy`, `disk_bytes`, and `auto_suspend_timeout_secs`.
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) # interactive implies pty
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(box, mount_path="/mnt/data", read_only=True)
196
- d.detach(box)
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() # server assigns id
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) # 0 = server default
227
- t.token # "eyJ..."save this; list() will not return it
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
- print(info.jti, info.created_at, info.expires_at)
232
- c.token.revoke(info.jti)
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 import is the **sync API**, which wraps the async implementation using a dedicated event loop:
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 for scripts and notebooks
388
+ from boxd import Compute # sync — recommended default
289
389
  ```
290
390
 
291
- For async code, import from `boxd.aio`:
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 APIs are surface-equivalent only the call style (sync vs `await`) differs.
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