boxd 0.1.0.dev2__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 (39) hide show
  1. boxd-0.1.0.dev2/LICENSE +21 -0
  2. boxd-0.1.0.dev2/PKG-INFO +329 -0
  3. boxd-0.1.0.dev2/README.md +296 -0
  4. boxd-0.1.0.dev2/pyproject.toml +58 -0
  5. boxd-0.1.0.dev2/setup.cfg +4 -0
  6. boxd-0.1.0.dev2/src/boxd/__init__.py +117 -0
  7. boxd-0.1.0.dev2/src/boxd/_generated/__init__.py +0 -0
  8. boxd-0.1.0.dev2/src/boxd/_generated/api_pb2.py +222 -0
  9. boxd-0.1.0.dev2/src/boxd/_generated/api_pb2_grpc.py +1776 -0
  10. boxd-0.1.0.dev2/src/boxd/_sync.py +448 -0
  11. boxd-0.1.0.dev2/src/boxd/_utils.py +73 -0
  12. boxd-0.1.0.dev2/src/boxd/aio.py +96 -0
  13. boxd-0.1.0.dev2/src/boxd/auth.py +118 -0
  14. boxd-0.1.0.dev2/src/boxd/box.py +320 -0
  15. boxd-0.1.0.dev2/src/boxd/boxes.py +164 -0
  16. boxd-0.1.0.dev2/src/boxd/client.py +141 -0
  17. boxd-0.1.0.dev2/src/boxd/disks.py +111 -0
  18. boxd-0.1.0.dev2/src/boxd/domains.py +54 -0
  19. boxd-0.1.0.dev2/src/boxd/errors.py +62 -0
  20. boxd-0.1.0.dev2/src/boxd/exec.py +122 -0
  21. boxd-0.1.0.dev2/src/boxd/networks.py +43 -0
  22. boxd-0.1.0.dev2/src/boxd/templates.py +113 -0
  23. boxd-0.1.0.dev2/src/boxd/tokens.py +51 -0
  24. boxd-0.1.0.dev2/src/boxd/types.py +142 -0
  25. boxd-0.1.0.dev2/src/boxd.egg-info/PKG-INFO +329 -0
  26. boxd-0.1.0.dev2/src/boxd.egg-info/SOURCES.txt +37 -0
  27. boxd-0.1.0.dev2/src/boxd.egg-info/dependency_links.txt +1 -0
  28. boxd-0.1.0.dev2/src/boxd.egg-info/requires.txt +10 -0
  29. boxd-0.1.0.dev2/src/boxd.egg-info/top_level.txt +1 -0
  30. boxd-0.1.0.dev2/tests/test_auth.py +33 -0
  31. boxd-0.1.0.dev2/tests/test_boxes.py +6 -0
  32. boxd-0.1.0.dev2/tests/test_e2e.py +318 -0
  33. boxd-0.1.0.dev2/tests/test_e2e_v2.py +97 -0
  34. boxd-0.1.0.dev2/tests/test_exec.py +21 -0
  35. boxd-0.1.0.dev2/tests/test_files.py +21 -0
  36. boxd-0.1.0.dev2/tests/test_lifecycle.py +21 -0
  37. boxd-0.1.0.dev2/tests/test_proxies.py +33 -0
  38. boxd-0.1.0.dev2/tests/test_utils.py +47 -0
  39. boxd-0.1.0.dev2/tests/test_v2.py +60 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Azin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,329 @@
1
+ Metadata-Version: 2.4
2
+ Name: boxd
3
+ Version: 0.1.0.dev2
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
+ Project-URL: Repository, https://github.com/azin-tech/boxd
9
+ Project-URL: Issues, https://github.com/azin-tech/boxd/issues
10
+ Keywords: boxd,vm,microvm,sandbox,compute,grpc,sdk
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: System :: Distributed Computing
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: grpcio>=1.60
24
+ Requires-Dist: protobuf>=4.25
25
+ Requires-Dist: httpx>=0.27
26
+ Provides-Extra: dev
27
+ Requires-Dist: grpcio-tools>=1.60; extra == "dev"
28
+ Requires-Dist: pytest>=8; extra == "dev"
29
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
30
+ Requires-Dist: build>=1.0; extra == "dev"
31
+ Requires-Dist: twine>=4.0; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # boxd Python SDK
35
+
36
+ Python SDK for the [boxd](https://boxd.sh) cloud VM platform. Sync-first API with full async support.
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 Compute
50
+
51
+ with Compute(api_key="bxk_...") as c:
52
+ box = c.box.create(name="my-vm")
53
+ result = box.exec("echo", "hello")
54
+ print(result.stdout)
55
+ box.destroy()
56
+ ```
57
+
58
+ ## Authentication
59
+
60
+ ```python
61
+ Compute(api_key="bxk_...") # API key (recommended)
62
+ Compute(token="eyJ...") # direct JWT
63
+ Compute() # reads BOXD_API_KEY or BOXD_TOKEN
64
+ ```
65
+
66
+ ## Configuration
67
+
68
+ The SDK reads its endpoint configuration from constructor arguments or env vars:
69
+
70
+ ```python
71
+ Compute(
72
+ api_key="bxk_...",
73
+ api_url="http://boxd.sh:9443", # default
74
+ exchange_url="https://boxd.sh/api/v1/auth/token", # default
75
+ )
76
+ ```
77
+
78
+ Equivalent env vars: `BOXD_API_KEY`, `BOXD_TOKEN`, `BOXD_API_URL`, `BOXD_EXCHANGE_URL`.
79
+
80
+ `api_url` accepts an optional URL scheme that controls TLS:
81
+
82
+ | `api_url` value | Transport |
83
+ |---|---|
84
+ | `http://host:port` | plaintext (scheme stripped before connecting) |
85
+ | `https://host:port` | TLS (scheme stripped before connecting) |
86
+ | bare `host:port` | TLS, except `localhost` / `127.*` which stay plaintext |
87
+
88
+ The default `http://boxd.sh:9443` matches production. Self-hosted clusters can pass `api_url="http://my-cluster:9443"` to opt into plaintext.
89
+
90
+ ## VM Lifecycle
91
+
92
+ ```python
93
+ box = c.box.create(name="my-vm")
94
+ boxes = c.box.list()
95
+ found = c.box.get("my-vm") # by name or id
96
+ forked = c.box.fork("my-vm", name="f1")
97
+
98
+ box.start()
99
+ box.stop()
100
+ box.reboot()
101
+ box.destroy()
102
+ s = box.suspend() # SuspendResult
103
+ r = box.resume() # ResumeResult
104
+ ```
105
+
106
+ `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`.
107
+
108
+ ## Exec
109
+
110
+ ```python
111
+ # Simple — collect all output
112
+ r = box.exec("python", "script.py")
113
+ r.stdout # str
114
+ r.stderr # str
115
+ r.exit_code # int
116
+ r.success # bool
117
+
118
+ # With env vars and timeout
119
+ box.exec("sh", "-c", "echo $FOO", env={"FOO": "bar"}, timeout=30)
120
+
121
+ # Streaming
122
+ proc = box.exec("tail", "-f", "/var/log/syslog", stream=True)
123
+ for chunk in proc.iter_stdout():
124
+ print(chunk.decode(), end="")
125
+ exit_code = proc.wait()
126
+
127
+ # Interactive (PTY + stdin)
128
+ sh = box.exec("bash", interactive=True) # interactive implies pty
129
+ ```
130
+
131
+ ## Files
132
+
133
+ ```python
134
+ from pathlib import Path
135
+
136
+ box.write_file(b"binary content", "/app/file.bin")
137
+ box.write_file("text content", "/app/file.txt")
138
+ box.write_file(Path("local/file.py"), "/app/file.py")
139
+ data = box.read_file("/app/output.json") # bytes
140
+ ```
141
+
142
+ ## Proxies
143
+
144
+ ```python
145
+ box.proxies() # list[Proxy]
146
+ proxy = box.create_proxy("api", port=3001) # api.<vm>.boxd.sh -> port 3001
147
+ box.set_proxy_port(port=3000) # change default proxy port
148
+ box.set_proxy_port(port=3001, name="api") # change a named proxy
149
+ box.delete_proxy("api")
150
+ ```
151
+
152
+ ## Logs
153
+
154
+ ```python
155
+ # Snapshot of available console output
156
+ for chunk in box.stream_logs():
157
+ print(chunk.decode(errors="replace"), end="")
158
+
159
+ # Follow (keeps the stream open for new chunks)
160
+ for chunk in box.stream_logs(follow=True):
161
+ print(chunk.decode(errors="replace"), end="")
162
+ ```
163
+
164
+ ## Templates
165
+
166
+ ```python
167
+ from boxd import BoxConfig
168
+
169
+ t = c.template.create(
170
+ name="t1",
171
+ image="ghcr.io/org/img:tag",
172
+ config=BoxConfig(vcpu=2, memory="4G"),
173
+ )
174
+ c.template.list()
175
+ box = c.template.create_vm(template=t, name="from-t")
176
+ c.template.delete(t.id)
177
+ ```
178
+
179
+ ## Disks
180
+
181
+ ```python
182
+ d = c.disk.create("data", size="10G")
183
+ d.attach(box, mount_path="/mnt/data")
184
+ d.attach(box, mount_path="/mnt/data", read_only=True)
185
+ d.detach(box)
186
+ d.destroy()
187
+ ```
188
+
189
+ ## Domains
190
+
191
+ Bind an external domain (DNS must already point at the boxd proxy).
192
+
193
+ ```python
194
+ c.domain.bind("app.example.com", box) # accepts a Box, name, or id
195
+ c.domain.bind("app.example.com", "my-vm")
196
+ for d in c.domain.list():
197
+ print(d.domain, "->", d.vm_id)
198
+ c.domain.unbind("app.example.com")
199
+ ```
200
+
201
+ ## Networks
202
+
203
+ ```python
204
+ n = c.network.create() # server assigns id
205
+ named = c.network.create(name="staging")
206
+ for net in c.network.list():
207
+ print(net.id, net.subnet, net.status)
208
+ ```
209
+
210
+ ## Tokens
211
+
212
+ Issue scoped JWTs for delegated access. The raw token string is only returned at creation — store it then.
213
+
214
+ ```python
215
+ t = c.token.create(expires_in=3600) # 0 = server default
216
+ t.token # "eyJ..." — save this; list() will not return it
217
+ t.expires_at # unix seconds
218
+
219
+ for info in c.token.list():
220
+ print(info.jti, info.created_at, info.expires_at)
221
+ c.token.revoke(info.jti)
222
+
223
+ # Use the token to authenticate a new client
224
+ c2 = Compute(token=t.token)
225
+ ```
226
+
227
+ ## Identity
228
+
229
+ ```python
230
+ me = c.whoami()
231
+ me.user_id # "gh-username"
232
+ me.fingerprints # ["SHA256:..."]
233
+ me.default_network_id # "net-..."
234
+
235
+ cfg = c.config()
236
+ cfg.default_image # "ubuntu:latest"
237
+ cfg.zone # "boxd.sh"
238
+ ```
239
+
240
+ ## Errors
241
+
242
+ ```python
243
+ from boxd import (
244
+ BoxdError, # base class
245
+ AuthenticationError,
246
+ NotFoundError,
247
+ QuotaExceededError,
248
+ InvalidArgumentError,
249
+ TimeoutError,
250
+ ConnectionError,
251
+ InternalError,
252
+ )
253
+
254
+ try:
255
+ box = c.box.get("nope")
256
+ except NotFoundError:
257
+ ...
258
+ ```
259
+
260
+ | Class | gRPC status |
261
+ |---|---|
262
+ | `AuthenticationError` | `UNAUTHENTICATED`, `PERMISSION_DENIED` |
263
+ | `NotFoundError` | `NOT_FOUND` |
264
+ | `QuotaExceededError` | `RESOURCE_EXHAUSTED` |
265
+ | `InvalidArgumentError` | `INVALID_ARGUMENT`, `ALREADY_EXISTS` |
266
+ | `TimeoutError` | `DEADLINE_EXCEEDED` |
267
+ | `ConnectionError` | `UNAVAILABLE` |
268
+ | `InternalError` | `INTERNAL`, `UNKNOWN` |
269
+
270
+ Each error carries the underlying `grpc_code` for finer-grained handling.
271
+
272
+ ## Sync vs Async
273
+
274
+ The default import is the **sync API**, which wraps the async implementation using a dedicated event loop:
275
+
276
+ ```python
277
+ from boxd import Compute # sync — recommended for scripts and notebooks
278
+ ```
279
+
280
+ For async code, import from `boxd.aio`:
281
+
282
+ ```python
283
+ from boxd.aio import Compute
284
+
285
+ async with Compute(api_key="bxk_...") as c:
286
+ box = await c.box.create(name="my-vm")
287
+ result = await box.exec("echo", "hello")
288
+ ```
289
+
290
+ The two APIs are surface-equivalent — only the call style (sync vs `await`) differs.
291
+
292
+ ## Development
293
+
294
+ ```bash
295
+ cd sdk/python
296
+ python -m venv .venv
297
+ source .venv/bin/activate
298
+ pip install -e ".[dev]"
299
+
300
+ pytest tests/ # unit tests (e2e marker excluded by default)
301
+ pytest tests/ -m e2e # e2e tests (creates/destroys VMs)
302
+ pytest tests/ -m "" # everything
303
+ bash scripts/compile_proto.sh # regenerate _generated/ after changing api.proto
304
+ ```
305
+
306
+ ## Architecture
307
+
308
+ ```
309
+ sdk/python/
310
+ ├── src/boxd/
311
+ │ ├── __init__.py # public sync API exports (default import)
312
+ │ ├── aio.py # public async API exports
313
+ │ ├── _sync.py # sync wrappers (run_until_complete)
314
+ │ ├── client.py # async Compute (entry point) + auth/transport
315
+ │ ├── auth.py # API key → JWT exchange + refresh
316
+ │ ├── boxes.py # async BoxService (create/list/get/fork)
317
+ │ ├── box.py # async Box (lifecycle/exec/files/proxies/logs)
318
+ │ ├── exec.py # ExecResult, ExecProcess, stream readers/writers
319
+ │ ├── templates.py # async TemplateService
320
+ │ ├── disks.py # async DiskService + DiskHandle
321
+ │ ├── domains.py # async DomainService
322
+ │ ├── networks.py # async NetworkService
323
+ │ ├── tokens.py # async TokenService
324
+ │ ├── types.py # public dataclasses (BoxConfig, Proxy, etc.)
325
+ │ ├── errors.py # BoxdError hierarchy + gRPC mapping
326
+ │ ├── _utils.py # GrpcCaller mixin, parse_size, resolve_endpoint
327
+ │ └── _generated/ # protoc-grpc-python output (committed)
328
+ └── tests/ # pytest unit + gated e2e
329
+ ```
@@ -0,0 +1,296 @@
1
+ # boxd Python SDK
2
+
3
+ Python SDK for the [boxd](https://boxd.sh) cloud VM platform. Sync-first API with full async support.
4
+
5
+ Requires Python 3.10+.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install boxd
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```python
16
+ from boxd import Compute
17
+
18
+ with Compute(api_key="bxk_...") as c:
19
+ box = c.box.create(name="my-vm")
20
+ result = box.exec("echo", "hello")
21
+ print(result.stdout)
22
+ box.destroy()
23
+ ```
24
+
25
+ ## Authentication
26
+
27
+ ```python
28
+ Compute(api_key="bxk_...") # API key (recommended)
29
+ Compute(token="eyJ...") # direct JWT
30
+ Compute() # reads BOXD_API_KEY or BOXD_TOKEN
31
+ ```
32
+
33
+ ## Configuration
34
+
35
+ The SDK reads its endpoint configuration from constructor arguments or env vars:
36
+
37
+ ```python
38
+ Compute(
39
+ api_key="bxk_...",
40
+ api_url="http://boxd.sh:9443", # default
41
+ exchange_url="https://boxd.sh/api/v1/auth/token", # default
42
+ )
43
+ ```
44
+
45
+ Equivalent env vars: `BOXD_API_KEY`, `BOXD_TOKEN`, `BOXD_API_URL`, `BOXD_EXCHANGE_URL`.
46
+
47
+ `api_url` accepts an optional URL scheme that controls TLS:
48
+
49
+ | `api_url` value | Transport |
50
+ |---|---|
51
+ | `http://host:port` | plaintext (scheme stripped before connecting) |
52
+ | `https://host:port` | TLS (scheme stripped before connecting) |
53
+ | bare `host:port` | TLS, except `localhost` / `127.*` which stay plaintext |
54
+
55
+ The default `http://boxd.sh:9443` matches production. Self-hosted clusters can pass `api_url="http://my-cluster:9443"` to opt into plaintext.
56
+
57
+ ## VM Lifecycle
58
+
59
+ ```python
60
+ box = c.box.create(name="my-vm")
61
+ boxes = c.box.list()
62
+ found = c.box.get("my-vm") # by name or id
63
+ forked = c.box.fork("my-vm", name="f1")
64
+
65
+ box.start()
66
+ box.stop()
67
+ box.reboot()
68
+ box.destroy()
69
+ s = box.suspend() # SuspendResult
70
+ r = box.resume() # ResumeResult
71
+ ```
72
+
73
+ `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`.
74
+
75
+ ## Exec
76
+
77
+ ```python
78
+ # Simple — collect all output
79
+ r = box.exec("python", "script.py")
80
+ r.stdout # str
81
+ r.stderr # str
82
+ r.exit_code # int
83
+ r.success # bool
84
+
85
+ # With env vars and timeout
86
+ box.exec("sh", "-c", "echo $FOO", env={"FOO": "bar"}, timeout=30)
87
+
88
+ # Streaming
89
+ proc = box.exec("tail", "-f", "/var/log/syslog", stream=True)
90
+ for chunk in proc.iter_stdout():
91
+ print(chunk.decode(), end="")
92
+ exit_code = proc.wait()
93
+
94
+ # Interactive (PTY + stdin)
95
+ sh = box.exec("bash", interactive=True) # interactive implies pty
96
+ ```
97
+
98
+ ## Files
99
+
100
+ ```python
101
+ from pathlib import Path
102
+
103
+ box.write_file(b"binary content", "/app/file.bin")
104
+ box.write_file("text content", "/app/file.txt")
105
+ box.write_file(Path("local/file.py"), "/app/file.py")
106
+ data = box.read_file("/app/output.json") # bytes
107
+ ```
108
+
109
+ ## Proxies
110
+
111
+ ```python
112
+ box.proxies() # list[Proxy]
113
+ proxy = box.create_proxy("api", port=3001) # api.<vm>.boxd.sh -> port 3001
114
+ box.set_proxy_port(port=3000) # change default proxy port
115
+ box.set_proxy_port(port=3001, name="api") # change a named proxy
116
+ box.delete_proxy("api")
117
+ ```
118
+
119
+ ## Logs
120
+
121
+ ```python
122
+ # Snapshot of available console output
123
+ for chunk in box.stream_logs():
124
+ print(chunk.decode(errors="replace"), end="")
125
+
126
+ # Follow (keeps the stream open for new chunks)
127
+ for chunk in box.stream_logs(follow=True):
128
+ print(chunk.decode(errors="replace"), end="")
129
+ ```
130
+
131
+ ## Templates
132
+
133
+ ```python
134
+ from boxd import BoxConfig
135
+
136
+ t = c.template.create(
137
+ name="t1",
138
+ image="ghcr.io/org/img:tag",
139
+ config=BoxConfig(vcpu=2, memory="4G"),
140
+ )
141
+ c.template.list()
142
+ box = c.template.create_vm(template=t, name="from-t")
143
+ c.template.delete(t.id)
144
+ ```
145
+
146
+ ## Disks
147
+
148
+ ```python
149
+ d = c.disk.create("data", size="10G")
150
+ d.attach(box, mount_path="/mnt/data")
151
+ d.attach(box, mount_path="/mnt/data", read_only=True)
152
+ d.detach(box)
153
+ d.destroy()
154
+ ```
155
+
156
+ ## Domains
157
+
158
+ Bind an external domain (DNS must already point at the boxd proxy).
159
+
160
+ ```python
161
+ c.domain.bind("app.example.com", box) # accepts a Box, name, or id
162
+ c.domain.bind("app.example.com", "my-vm")
163
+ for d in c.domain.list():
164
+ print(d.domain, "->", d.vm_id)
165
+ c.domain.unbind("app.example.com")
166
+ ```
167
+
168
+ ## Networks
169
+
170
+ ```python
171
+ n = c.network.create() # server assigns id
172
+ named = c.network.create(name="staging")
173
+ for net in c.network.list():
174
+ print(net.id, net.subnet, net.status)
175
+ ```
176
+
177
+ ## Tokens
178
+
179
+ Issue scoped JWTs for delegated access. The raw token string is only returned at creation — store it then.
180
+
181
+ ```python
182
+ t = c.token.create(expires_in=3600) # 0 = server default
183
+ t.token # "eyJ..." — save this; list() will not return it
184
+ t.expires_at # unix seconds
185
+
186
+ for info in c.token.list():
187
+ print(info.jti, info.created_at, info.expires_at)
188
+ c.token.revoke(info.jti)
189
+
190
+ # Use the token to authenticate a new client
191
+ c2 = Compute(token=t.token)
192
+ ```
193
+
194
+ ## Identity
195
+
196
+ ```python
197
+ me = c.whoami()
198
+ me.user_id # "gh-username"
199
+ me.fingerprints # ["SHA256:..."]
200
+ me.default_network_id # "net-..."
201
+
202
+ cfg = c.config()
203
+ cfg.default_image # "ubuntu:latest"
204
+ cfg.zone # "boxd.sh"
205
+ ```
206
+
207
+ ## Errors
208
+
209
+ ```python
210
+ from boxd import (
211
+ BoxdError, # base class
212
+ AuthenticationError,
213
+ NotFoundError,
214
+ QuotaExceededError,
215
+ InvalidArgumentError,
216
+ TimeoutError,
217
+ ConnectionError,
218
+ InternalError,
219
+ )
220
+
221
+ try:
222
+ box = c.box.get("nope")
223
+ except NotFoundError:
224
+ ...
225
+ ```
226
+
227
+ | Class | gRPC status |
228
+ |---|---|
229
+ | `AuthenticationError` | `UNAUTHENTICATED`, `PERMISSION_DENIED` |
230
+ | `NotFoundError` | `NOT_FOUND` |
231
+ | `QuotaExceededError` | `RESOURCE_EXHAUSTED` |
232
+ | `InvalidArgumentError` | `INVALID_ARGUMENT`, `ALREADY_EXISTS` |
233
+ | `TimeoutError` | `DEADLINE_EXCEEDED` |
234
+ | `ConnectionError` | `UNAVAILABLE` |
235
+ | `InternalError` | `INTERNAL`, `UNKNOWN` |
236
+
237
+ Each error carries the underlying `grpc_code` for finer-grained handling.
238
+
239
+ ## Sync vs Async
240
+
241
+ The default import is the **sync API**, which wraps the async implementation using a dedicated event loop:
242
+
243
+ ```python
244
+ from boxd import Compute # sync — recommended for scripts and notebooks
245
+ ```
246
+
247
+ For async code, import from `boxd.aio`:
248
+
249
+ ```python
250
+ from boxd.aio import Compute
251
+
252
+ async with Compute(api_key="bxk_...") as c:
253
+ box = await c.box.create(name="my-vm")
254
+ result = await box.exec("echo", "hello")
255
+ ```
256
+
257
+ The two APIs are surface-equivalent — only the call style (sync vs `await`) differs.
258
+
259
+ ## Development
260
+
261
+ ```bash
262
+ cd sdk/python
263
+ python -m venv .venv
264
+ source .venv/bin/activate
265
+ pip install -e ".[dev]"
266
+
267
+ pytest tests/ # unit tests (e2e marker excluded by default)
268
+ pytest tests/ -m e2e # e2e tests (creates/destroys VMs)
269
+ pytest tests/ -m "" # everything
270
+ bash scripts/compile_proto.sh # regenerate _generated/ after changing api.proto
271
+ ```
272
+
273
+ ## Architecture
274
+
275
+ ```
276
+ sdk/python/
277
+ ├── src/boxd/
278
+ │ ├── __init__.py # public sync API exports (default import)
279
+ │ ├── aio.py # public async API exports
280
+ │ ├── _sync.py # sync wrappers (run_until_complete)
281
+ │ ├── client.py # async Compute (entry point) + auth/transport
282
+ │ ├── auth.py # API key → JWT exchange + refresh
283
+ │ ├── boxes.py # async BoxService (create/list/get/fork)
284
+ │ ├── box.py # async Box (lifecycle/exec/files/proxies/logs)
285
+ │ ├── exec.py # ExecResult, ExecProcess, stream readers/writers
286
+ │ ├── templates.py # async TemplateService
287
+ │ ├── disks.py # async DiskService + DiskHandle
288
+ │ ├── domains.py # async DomainService
289
+ │ ├── networks.py # async NetworkService
290
+ │ ├── tokens.py # async TokenService
291
+ │ ├── types.py # public dataclasses (BoxConfig, Proxy, etc.)
292
+ │ ├── errors.py # BoxdError hierarchy + gRPC mapping
293
+ │ ├── _utils.py # GrpcCaller mixin, parse_size, resolve_endpoint
294
+ │ └── _generated/ # protoc-grpc-python output (committed)
295
+ └── tests/ # pytest unit + gated e2e
296
+ ```
@@ -0,0 +1,58 @@
1
+ [project]
2
+ name = "boxd"
3
+ version = "0.1.0.dev2"
4
+ description = "Python SDK for the boxd cloud VM platform"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [{ name = "Azin" }]
9
+ requires-python = ">=3.10"
10
+ keywords = ["boxd", "vm", "microvm", "sandbox", "compute", "grpc", "sdk"]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Intended Audience :: Developers",
14
+ "Operating System :: OS Independent",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Topic :: Software Development :: Libraries :: Python Modules",
20
+ "Topic :: System :: Distributed Computing",
21
+ ]
22
+ dependencies = [
23
+ "grpcio>=1.60",
24
+ "protobuf>=4.25",
25
+ "httpx>=0.27",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://boxd.sh"
30
+ Repository = "https://github.com/azin-tech/boxd"
31
+ Issues = "https://github.com/azin-tech/boxd/issues"
32
+
33
+ [project.optional-dependencies]
34
+ dev = [
35
+ "grpcio-tools>=1.60",
36
+ "pytest>=8",
37
+ "pytest-asyncio>=0.23",
38
+ "build>=1.0",
39
+ "twine>=4.0",
40
+ ]
41
+
42
+ [build-system]
43
+ requires = ["setuptools>=68"]
44
+ build-backend = "setuptools.build_meta"
45
+
46
+ [tool.setuptools.packages.find]
47
+ where = ["src"]
48
+
49
+ [tool.pytest.ini_options]
50
+ asyncio_mode = "auto"
51
+ asyncio_default_fixture_loop_scope = "session"
52
+ asyncio_default_test_loop_scope = "session"
53
+ markers = ["e2e: end-to-end tests that create/destroy VMs (slow)"]
54
+ addopts = "-m 'not e2e'"
55
+ filterwarnings = [
56
+ "error",
57
+ "ignore::ResourceWarning",
58
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+