capsule-sdk 0.1.0__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.
- capsule_sdk-0.1.0/.gitignore +101 -0
- capsule_sdk-0.1.0/PKG-INFO +222 -0
- capsule_sdk-0.1.0/README.md +204 -0
- capsule_sdk-0.1.0/pyproject.toml +49 -0
- capsule_sdk-0.1.0/src/capsule_sdk/__init__.py +60 -0
- capsule_sdk-0.1.0/src/capsule_sdk/_config.py +66 -0
- capsule_sdk-0.1.0/src/capsule_sdk/_errors.py +142 -0
- capsule_sdk-0.1.0/src/capsule_sdk/_http.py +461 -0
- capsule_sdk-0.1.0/src/capsule_sdk/_http_async.py +429 -0
- capsule_sdk-0.1.0/src/capsule_sdk/_shell.py +118 -0
- capsule_sdk-0.1.0/src/capsule_sdk/_shell_async.py +127 -0
- capsule_sdk-0.1.0/src/capsule_sdk/_snapshot_commands.py +32 -0
- capsule_sdk-0.1.0/src/capsule_sdk/_version.py +1 -0
- capsule_sdk-0.1.0/src/capsule_sdk/async_client.py +47 -0
- capsule_sdk-0.1.0/src/capsule_sdk/async_runner_config.py +18 -0
- capsule_sdk-0.1.0/src/capsule_sdk/async_runner_session.py +207 -0
- capsule_sdk-0.1.0/src/capsule_sdk/client.py +47 -0
- capsule_sdk-0.1.0/src/capsule_sdk/models/__init__.py +74 -0
- capsule_sdk-0.1.0/src/capsule_sdk/models/common.py +31 -0
- capsule_sdk-0.1.0/src/capsule_sdk/models/file.py +53 -0
- capsule_sdk-0.1.0/src/capsule_sdk/models/layered_config.py +130 -0
- capsule_sdk-0.1.0/src/capsule_sdk/models/runner.py +134 -0
- capsule_sdk-0.1.0/src/capsule_sdk/models/snapshot.py +37 -0
- capsule_sdk-0.1.0/src/capsule_sdk/models/workload.py +24 -0
- capsule_sdk-0.1.0/src/capsule_sdk/py.typed +0 -0
- capsule_sdk-0.1.0/src/capsule_sdk/resources/__init__.py +8 -0
- capsule_sdk-0.1.0/src/capsule_sdk/resources/async_layered_configs.py +192 -0
- capsule_sdk-0.1.0/src/capsule_sdk/resources/async_runners.py +655 -0
- capsule_sdk-0.1.0/src/capsule_sdk/resources/async_snapshots.py +15 -0
- capsule_sdk-0.1.0/src/capsule_sdk/resources/async_workloads.py +270 -0
- capsule_sdk-0.1.0/src/capsule_sdk/resources/layered_configs.py +200 -0
- capsule_sdk-0.1.0/src/capsule_sdk/resources/runners.py +692 -0
- capsule_sdk-0.1.0/src/capsule_sdk/resources/snapshots.py +15 -0
- capsule_sdk-0.1.0/src/capsule_sdk/resources/workloads.py +276 -0
- capsule_sdk-0.1.0/src/capsule_sdk/runner_config.py +170 -0
- capsule_sdk-0.1.0/src/capsule_sdk/runner_session.py +254 -0
- capsule_sdk-0.1.0/tests/conftest.py +11 -0
- capsule_sdk-0.1.0/tests/e2e_live.py +186 -0
- capsule_sdk-0.1.0/tests/e2e_live_async.py +188 -0
- capsule_sdk-0.1.0/tests/test_async_client_surface.py +26 -0
- capsule_sdk-0.1.0/tests/test_async_http.py +233 -0
- capsule_sdk-0.1.0/tests/test_async_runner_session.py +130 -0
- capsule_sdk-0.1.0/tests/test_async_runners.py +216 -0
- capsule_sdk-0.1.0/tests/test_async_shell.py +133 -0
- capsule_sdk-0.1.0/tests/test_client_surface.py +24 -0
- capsule_sdk-0.1.0/tests/test_config.py +56 -0
- capsule_sdk-0.1.0/tests/test_contract.py +96 -0
- capsule_sdk-0.1.0/tests/test_errors.py +83 -0
- capsule_sdk-0.1.0/tests/test_files.py +171 -0
- capsule_sdk-0.1.0/tests/test_http_retries.py +149 -0
- capsule_sdk-0.1.0/tests/test_layered_configs.py +147 -0
- capsule_sdk-0.1.0/tests/test_runner_config.py +157 -0
- capsule_sdk-0.1.0/tests/test_runner_session.py +241 -0
- capsule_sdk-0.1.0/tests/test_runners.py +252 -0
- capsule_sdk-0.1.0/tests/test_shell.py +198 -0
- capsule_sdk-0.1.0/tests/test_snapshots.py +79 -0
- capsule_sdk-0.1.0/tests/test_workloads.py +146 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Pre-built binaries (use `make build` instead)
|
|
2
|
+
bin/
|
|
3
|
+
*.exe
|
|
4
|
+
*.dll
|
|
5
|
+
*.so
|
|
6
|
+
*.dylib
|
|
7
|
+
|
|
8
|
+
# Go build output (matches cmd/ directory names)
|
|
9
|
+
/capsule-control-plane
|
|
10
|
+
/data-snapshot-builder
|
|
11
|
+
/capsule-manager
|
|
12
|
+
/git-cache-builder
|
|
13
|
+
/git-cache-freshness
|
|
14
|
+
/onboard
|
|
15
|
+
/snapshot-builder
|
|
16
|
+
/snapshot-converter
|
|
17
|
+
/capsule-thaw-agent
|
|
18
|
+
/derive-snapshot
|
|
19
|
+
|
|
20
|
+
# Build outputs (use `make build-rootfs`)
|
|
21
|
+
images/microvm/output/
|
|
22
|
+
*.img
|
|
23
|
+
*.bin
|
|
24
|
+
|
|
25
|
+
# Terraform
|
|
26
|
+
deploy/terraform/**/.terraform/
|
|
27
|
+
deploy/terraform/**/.terraform.lock.hcl
|
|
28
|
+
deploy/terraform/**/*.tfplan
|
|
29
|
+
deploy/terraform/**/*.tfstate
|
|
30
|
+
deploy/terraform/**/*.tfstate.*
|
|
31
|
+
deploy/terraform/**/*.tfvars
|
|
32
|
+
!deploy/terraform/**/*.tfvars.example
|
|
33
|
+
|
|
34
|
+
# Packer
|
|
35
|
+
deploy/packer/*.pkr.hcl.bak
|
|
36
|
+
*.box
|
|
37
|
+
|
|
38
|
+
# Kubernetes secrets (generated)
|
|
39
|
+
deploy/kubernetes/*-secrets.yaml
|
|
40
|
+
deploy/kubernetes/*-secret.yaml
|
|
41
|
+
|
|
42
|
+
# Helm
|
|
43
|
+
deploy/helm/**/charts/
|
|
44
|
+
deploy/helm/**/*.tgz
|
|
45
|
+
|
|
46
|
+
# IDE
|
|
47
|
+
.idea/
|
|
48
|
+
.vscode/
|
|
49
|
+
*.swp
|
|
50
|
+
*.swo
|
|
51
|
+
*~
|
|
52
|
+
|
|
53
|
+
# OS
|
|
54
|
+
.DS_Store
|
|
55
|
+
Thumbs.db
|
|
56
|
+
|
|
57
|
+
# Secrets and credentials
|
|
58
|
+
*.pem
|
|
59
|
+
*.key
|
|
60
|
+
*.crt
|
|
61
|
+
*.p12
|
|
62
|
+
*.pfx
|
|
63
|
+
secrets/
|
|
64
|
+
.env
|
|
65
|
+
.env.*
|
|
66
|
+
!.env.example
|
|
67
|
+
|
|
68
|
+
# Logs
|
|
69
|
+
*.log
|
|
70
|
+
logs/
|
|
71
|
+
|
|
72
|
+
# Go
|
|
73
|
+
vendor/
|
|
74
|
+
go.work
|
|
75
|
+
go.work.sum
|
|
76
|
+
|
|
77
|
+
# Test artifacts
|
|
78
|
+
coverage.out
|
|
79
|
+
coverage.html
|
|
80
|
+
*.test
|
|
81
|
+
*.prof
|
|
82
|
+
|
|
83
|
+
# Temporary files
|
|
84
|
+
tmp/
|
|
85
|
+
temp/
|
|
86
|
+
*.tmp
|
|
87
|
+
*.bak
|
|
88
|
+
|
|
89
|
+
# Docker
|
|
90
|
+
.docker/
|
|
91
|
+
|
|
92
|
+
# Cache directories
|
|
93
|
+
.cache/
|
|
94
|
+
__pycache__/
|
|
95
|
+
|
|
96
|
+
/deploy/terraform/tfplan
|
|
97
|
+
|
|
98
|
+
.claude/
|
|
99
|
+
|
|
100
|
+
# Terraform provider cache (non-glob match for root terraform dir)
|
|
101
|
+
deploy/terraform/.terraform/
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: capsule-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Capsule
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
8
|
+
Requires-Dist: pydantic<3.0,>=2.0
|
|
9
|
+
Requires-Dist: pyyaml>=6.0
|
|
10
|
+
Requires-Dist: websockets<15.0,>=12.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pyright>=1.1; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
16
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Capsule SDK
|
|
20
|
+
|
|
21
|
+
The Capsule SDK is the recommended client surface for registering workloads,
|
|
22
|
+
triggering builds, allocating runners, and interacting with running Capsule
|
|
23
|
+
sandboxes from Python.
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
- Python `>= 3.10`
|
|
28
|
+
- access to a running Capsule control plane
|
|
29
|
+
- an API token if your deployment requires authenticated requests
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install capsule-sdk
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
For local development:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cd sdk/python
|
|
41
|
+
python3 -m venv .venv
|
|
42
|
+
source .venv/bin/activate
|
|
43
|
+
pip install -e ".[dev]"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
|
|
48
|
+
The SDK can be configured directly in code or through environment variables.
|
|
49
|
+
|
|
50
|
+
| Parameter | Env var | Default |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `base_url` | `CAPSULE_BASE_URL` | `http://localhost:8080` |
|
|
53
|
+
| `token` | `CAPSULE_TOKEN` | `None` |
|
|
54
|
+
| `request_timeout` | `CAPSULE_REQUEST_TIMEOUT` | `30.0` |
|
|
55
|
+
| `startup_timeout` | `CAPSULE_STARTUP_TIMEOUT` | `45.0` |
|
|
56
|
+
| `operation_timeout` | `CAPSULE_OPERATION_TIMEOUT` | `120.0` |
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
export CAPSULE_BASE_URL="http://localhost:8080"
|
|
62
|
+
export CAPSULE_TOKEN="my-token"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Quickstart
|
|
66
|
+
|
|
67
|
+
The fastest way to get started is the high-level `workloads` API.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from capsule_sdk import CapsuleClient, RunnerConfig
|
|
71
|
+
|
|
72
|
+
cfg = (
|
|
73
|
+
RunnerConfig("My dev sandbox")
|
|
74
|
+
.with_base_image("ubuntu:22.04")
|
|
75
|
+
.with_commands(["apt-get update", "apt-get install -y python3"])
|
|
76
|
+
.with_tier("m")
|
|
77
|
+
.with_ttl(3600)
|
|
78
|
+
.with_auto_pause(True)
|
|
79
|
+
.with_auto_rollout(True)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
with CapsuleClient(base_url="http://localhost:8080", token="my-token") as client:
|
|
83
|
+
workload = client.workloads.onboard(cfg)
|
|
84
|
+
|
|
85
|
+
with client.workloads.start(workload) as runner:
|
|
86
|
+
output, code = runner.exec_collect("python3", "-c", "print('hello')")
|
|
87
|
+
print(output, code)
|
|
88
|
+
|
|
89
|
+
runner.write_text("/workspace/hello.txt", "hello")
|
|
90
|
+
print(runner.read_text("/workspace/hello.txt"))
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Onboard From YAML
|
|
94
|
+
|
|
95
|
+
You can also onboard directly from an `onboard.yaml`-style file:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from capsule_sdk import CapsuleClient
|
|
99
|
+
|
|
100
|
+
with CapsuleClient(base_url="http://localhost:8080", token="my-token") as client:
|
|
101
|
+
workload = client.workloads.onboard_yaml(
|
|
102
|
+
"examples/afs/onboard.yaml",
|
|
103
|
+
name="afs-sandbox",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
with client.workloads.start("afs-sandbox") as runner:
|
|
107
|
+
print(runner.read_text("/etc/hostname"))
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The AFS example is an example workload name, not a special SDK mode. See
|
|
111
|
+
`examples/afs/` for the underlying config shape.
|
|
112
|
+
|
|
113
|
+
## Async Quickstart
|
|
114
|
+
|
|
115
|
+
Use the async client in event-loop-native applications:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
import asyncio
|
|
119
|
+
|
|
120
|
+
from capsule_sdk import AsyncCapsuleClient, RunnerConfig
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
async def main() -> None:
|
|
124
|
+
cfg = (
|
|
125
|
+
RunnerConfig("My async sandbox")
|
|
126
|
+
.with_base_image("ubuntu:22.04")
|
|
127
|
+
.with_commands(["echo async-ready"])
|
|
128
|
+
.with_tier("m")
|
|
129
|
+
.with_ttl(3600)
|
|
130
|
+
.with_auto_pause(True)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
async with AsyncCapsuleClient(base_url="http://localhost:8080", token="my-token") as client:
|
|
134
|
+
workload = await client.workloads.onboard(cfg)
|
|
135
|
+
runner = await client.workloads.start(workload)
|
|
136
|
+
|
|
137
|
+
async with runner:
|
|
138
|
+
result = await runner.exec_collect("sh", "-lc", "printf hello")
|
|
139
|
+
print(result.stdout, result.exit_code)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
asyncio.run(main())
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Low-Level APIs
|
|
146
|
+
|
|
147
|
+
For finer control, work directly with the resource clients:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from capsule_sdk import CapsuleClient
|
|
151
|
+
|
|
152
|
+
with CapsuleClient(base_url="http://localhost:8080", token="my-token") as client:
|
|
153
|
+
with client.runners.allocate_ready("my-workload-key") as runner:
|
|
154
|
+
for event in runner.exec("echo", "hello"):
|
|
155
|
+
if event.type == "stdout":
|
|
156
|
+
print(event.data, end="")
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Key low-level surfaces:
|
|
160
|
+
|
|
161
|
+
- `client.runners`
|
|
162
|
+
- `client.workloads`
|
|
163
|
+
- `client.snapshots`
|
|
164
|
+
- `client.runner_configs`
|
|
165
|
+
|
|
166
|
+
## Key Concepts
|
|
167
|
+
|
|
168
|
+
| SDK concept | Server primitive | Description |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| `RunnerConfig` | `LayeredConfig` | Declarative workload shape |
|
|
171
|
+
| `workloads.onboard()` | create + build | Register a workload from Python or YAML |
|
|
172
|
+
| `workloads.start()` | allocate + wait | Start a ready runner by workload name |
|
|
173
|
+
| `runners.allocate_ready()` | `/runners/allocate` | Allocate and wait for a usable runner |
|
|
174
|
+
| `RunnerSession` | runner handle | High-level exec, file, shell, pause, and resume API |
|
|
175
|
+
|
|
176
|
+
## Retry And Timeout Behavior
|
|
177
|
+
|
|
178
|
+
- `request_timeout` applies to a single HTTP request
|
|
179
|
+
- `startup_timeout` covers "get me a usable runner"
|
|
180
|
+
- `operation_timeout` applies to host-side file, PTY, and stream operations
|
|
181
|
+
- `allocate()` retries transient control-plane and capacity errors until `startup_timeout`
|
|
182
|
+
- `workloads.start()` is the preferred high-level path for named workloads
|
|
183
|
+
- `from_config()` waits for runner readiness by default; use `wait_ready=False` for lower-level control
|
|
184
|
+
|
|
185
|
+
## Host Reconnection
|
|
186
|
+
|
|
187
|
+
The SDK caches host addresses returned by `allocate()` and `connect()`. If a
|
|
188
|
+
host proxy becomes unavailable during a safe retryable operation, the SDK will
|
|
189
|
+
refresh the host via `connect()` and retry once when possible.
|
|
190
|
+
|
|
191
|
+
## Live End-To-End Test
|
|
192
|
+
|
|
193
|
+
The repository includes an explicit live SDK E2E at `sdk/python/tests/e2e_live.py`.
|
|
194
|
+
It exercises config registration, build enqueue, allocation, exec, file ops,
|
|
195
|
+
PTY, pause/resume, release, and config cleanup against a real control plane.
|
|
196
|
+
|
|
197
|
+
Run it with:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
make sdk-python-e2e
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
If you are not using the default address:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
CAPSULE_BASE_URL="http://localhost:8080" make sdk-python-e2e
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Development Checks
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
python -m ruff check src/capsule_sdk/ tests/
|
|
213
|
+
python -m pyright src/capsule_sdk/
|
|
214
|
+
python -m pytest tests/ -v --ignore=tests/e2e_live.py --ignore=tests/e2e_live_async.py
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
For contract tests against a live control plane:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
CAPSULE_BASE_URL=http://localhost:8080 CAPSULE_TOKEN=test-token \
|
|
221
|
+
python -m pytest tests/test_contract.py -v -m contract
|
|
222
|
+
```
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Capsule SDK
|
|
2
|
+
|
|
3
|
+
The Capsule SDK is the recommended client surface for registering workloads,
|
|
4
|
+
triggering builds, allocating runners, and interacting with running Capsule
|
|
5
|
+
sandboxes from Python.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Python `>= 3.10`
|
|
10
|
+
- access to a running Capsule control plane
|
|
11
|
+
- an API token if your deployment requires authenticated requests
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install capsule-sdk
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
For local development:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd sdk/python
|
|
23
|
+
python3 -m venv .venv
|
|
24
|
+
source .venv/bin/activate
|
|
25
|
+
pip install -e ".[dev]"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
The SDK can be configured directly in code or through environment variables.
|
|
31
|
+
|
|
32
|
+
| Parameter | Env var | Default |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `base_url` | `CAPSULE_BASE_URL` | `http://localhost:8080` |
|
|
35
|
+
| `token` | `CAPSULE_TOKEN` | `None` |
|
|
36
|
+
| `request_timeout` | `CAPSULE_REQUEST_TIMEOUT` | `30.0` |
|
|
37
|
+
| `startup_timeout` | `CAPSULE_STARTUP_TIMEOUT` | `45.0` |
|
|
38
|
+
| `operation_timeout` | `CAPSULE_OPERATION_TIMEOUT` | `120.0` |
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
export CAPSULE_BASE_URL="http://localhost:8080"
|
|
44
|
+
export CAPSULE_TOKEN="my-token"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quickstart
|
|
48
|
+
|
|
49
|
+
The fastest way to get started is the high-level `workloads` API.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from capsule_sdk import CapsuleClient, RunnerConfig
|
|
53
|
+
|
|
54
|
+
cfg = (
|
|
55
|
+
RunnerConfig("My dev sandbox")
|
|
56
|
+
.with_base_image("ubuntu:22.04")
|
|
57
|
+
.with_commands(["apt-get update", "apt-get install -y python3"])
|
|
58
|
+
.with_tier("m")
|
|
59
|
+
.with_ttl(3600)
|
|
60
|
+
.with_auto_pause(True)
|
|
61
|
+
.with_auto_rollout(True)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
with CapsuleClient(base_url="http://localhost:8080", token="my-token") as client:
|
|
65
|
+
workload = client.workloads.onboard(cfg)
|
|
66
|
+
|
|
67
|
+
with client.workloads.start(workload) as runner:
|
|
68
|
+
output, code = runner.exec_collect("python3", "-c", "print('hello')")
|
|
69
|
+
print(output, code)
|
|
70
|
+
|
|
71
|
+
runner.write_text("/workspace/hello.txt", "hello")
|
|
72
|
+
print(runner.read_text("/workspace/hello.txt"))
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Onboard From YAML
|
|
76
|
+
|
|
77
|
+
You can also onboard directly from an `onboard.yaml`-style file:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from capsule_sdk import CapsuleClient
|
|
81
|
+
|
|
82
|
+
with CapsuleClient(base_url="http://localhost:8080", token="my-token") as client:
|
|
83
|
+
workload = client.workloads.onboard_yaml(
|
|
84
|
+
"examples/afs/onboard.yaml",
|
|
85
|
+
name="afs-sandbox",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
with client.workloads.start("afs-sandbox") as runner:
|
|
89
|
+
print(runner.read_text("/etc/hostname"))
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The AFS example is an example workload name, not a special SDK mode. See
|
|
93
|
+
`examples/afs/` for the underlying config shape.
|
|
94
|
+
|
|
95
|
+
## Async Quickstart
|
|
96
|
+
|
|
97
|
+
Use the async client in event-loop-native applications:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import asyncio
|
|
101
|
+
|
|
102
|
+
from capsule_sdk import AsyncCapsuleClient, RunnerConfig
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def main() -> None:
|
|
106
|
+
cfg = (
|
|
107
|
+
RunnerConfig("My async sandbox")
|
|
108
|
+
.with_base_image("ubuntu:22.04")
|
|
109
|
+
.with_commands(["echo async-ready"])
|
|
110
|
+
.with_tier("m")
|
|
111
|
+
.with_ttl(3600)
|
|
112
|
+
.with_auto_pause(True)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
async with AsyncCapsuleClient(base_url="http://localhost:8080", token="my-token") as client:
|
|
116
|
+
workload = await client.workloads.onboard(cfg)
|
|
117
|
+
runner = await client.workloads.start(workload)
|
|
118
|
+
|
|
119
|
+
async with runner:
|
|
120
|
+
result = await runner.exec_collect("sh", "-lc", "printf hello")
|
|
121
|
+
print(result.stdout, result.exit_code)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
asyncio.run(main())
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Low-Level APIs
|
|
128
|
+
|
|
129
|
+
For finer control, work directly with the resource clients:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from capsule_sdk import CapsuleClient
|
|
133
|
+
|
|
134
|
+
with CapsuleClient(base_url="http://localhost:8080", token="my-token") as client:
|
|
135
|
+
with client.runners.allocate_ready("my-workload-key") as runner:
|
|
136
|
+
for event in runner.exec("echo", "hello"):
|
|
137
|
+
if event.type == "stdout":
|
|
138
|
+
print(event.data, end="")
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Key low-level surfaces:
|
|
142
|
+
|
|
143
|
+
- `client.runners`
|
|
144
|
+
- `client.workloads`
|
|
145
|
+
- `client.snapshots`
|
|
146
|
+
- `client.runner_configs`
|
|
147
|
+
|
|
148
|
+
## Key Concepts
|
|
149
|
+
|
|
150
|
+
| SDK concept | Server primitive | Description |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `RunnerConfig` | `LayeredConfig` | Declarative workload shape |
|
|
153
|
+
| `workloads.onboard()` | create + build | Register a workload from Python or YAML |
|
|
154
|
+
| `workloads.start()` | allocate + wait | Start a ready runner by workload name |
|
|
155
|
+
| `runners.allocate_ready()` | `/runners/allocate` | Allocate and wait for a usable runner |
|
|
156
|
+
| `RunnerSession` | runner handle | High-level exec, file, shell, pause, and resume API |
|
|
157
|
+
|
|
158
|
+
## Retry And Timeout Behavior
|
|
159
|
+
|
|
160
|
+
- `request_timeout` applies to a single HTTP request
|
|
161
|
+
- `startup_timeout` covers "get me a usable runner"
|
|
162
|
+
- `operation_timeout` applies to host-side file, PTY, and stream operations
|
|
163
|
+
- `allocate()` retries transient control-plane and capacity errors until `startup_timeout`
|
|
164
|
+
- `workloads.start()` is the preferred high-level path for named workloads
|
|
165
|
+
- `from_config()` waits for runner readiness by default; use `wait_ready=False` for lower-level control
|
|
166
|
+
|
|
167
|
+
## Host Reconnection
|
|
168
|
+
|
|
169
|
+
The SDK caches host addresses returned by `allocate()` and `connect()`. If a
|
|
170
|
+
host proxy becomes unavailable during a safe retryable operation, the SDK will
|
|
171
|
+
refresh the host via `connect()` and retry once when possible.
|
|
172
|
+
|
|
173
|
+
## Live End-To-End Test
|
|
174
|
+
|
|
175
|
+
The repository includes an explicit live SDK E2E at `sdk/python/tests/e2e_live.py`.
|
|
176
|
+
It exercises config registration, build enqueue, allocation, exec, file ops,
|
|
177
|
+
PTY, pause/resume, release, and config cleanup against a real control plane.
|
|
178
|
+
|
|
179
|
+
Run it with:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
make sdk-python-e2e
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
If you are not using the default address:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
CAPSULE_BASE_URL="http://localhost:8080" make sdk-python-e2e
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Development Checks
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
python -m ruff check src/capsule_sdk/ tests/
|
|
195
|
+
python -m pyright src/capsule_sdk/
|
|
196
|
+
python -m pytest tests/ -v --ignore=tests/e2e_live.py --ignore=tests/e2e_live_async.py
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
For contract tests against a live control plane:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
CAPSULE_BASE_URL=http://localhost:8080 CAPSULE_TOKEN=test-token \
|
|
203
|
+
python -m pytest tests/test_contract.py -v -m contract
|
|
204
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling", "packaging>=24.2"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "capsule-sdk"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Python SDK for Capsule"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"httpx>=0.27,<1.0",
|
|
14
|
+
"pydantic>=2.0,<3.0",
|
|
15
|
+
"PyYAML>=6.0",
|
|
16
|
+
"websockets>=12.0,<15.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=8.0",
|
|
22
|
+
"pytest-httpx>=0.30",
|
|
23
|
+
"ruff>=0.4",
|
|
24
|
+
"pyright>=1.1",
|
|
25
|
+
"respx>=0.21",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["src/capsule_sdk"]
|
|
30
|
+
|
|
31
|
+
[tool.hatch.version]
|
|
32
|
+
path = "src/capsule_sdk/_version.py"
|
|
33
|
+
|
|
34
|
+
[tool.ruff]
|
|
35
|
+
target-version = "py310"
|
|
36
|
+
line-length = 120
|
|
37
|
+
|
|
38
|
+
[tool.ruff.lint]
|
|
39
|
+
select = ["E", "F", "I", "N", "UP", "B", "SIM"]
|
|
40
|
+
ignore = ["N818", "SIM117"]
|
|
41
|
+
|
|
42
|
+
[tool.pyright]
|
|
43
|
+
pythonVersion = "3.10"
|
|
44
|
+
typeCheckingMode = "strict"
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
markers = [
|
|
48
|
+
"contract: tests that run against a live control-plane instance",
|
|
49
|
+
]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""capsule-sdk: Python SDK for capsule."""
|
|
2
|
+
|
|
3
|
+
from capsule_sdk._errors import (
|
|
4
|
+
CapsuleAllocationTimeoutError,
|
|
5
|
+
CapsuleAuthError,
|
|
6
|
+
CapsuleConflict,
|
|
7
|
+
CapsuleConnectionError,
|
|
8
|
+
CapsuleError,
|
|
9
|
+
CapsuleHTTPError,
|
|
10
|
+
CapsuleNotFound,
|
|
11
|
+
CapsuleOperationTimeoutError,
|
|
12
|
+
CapsuleRateLimited,
|
|
13
|
+
CapsuleRequestTimeoutError,
|
|
14
|
+
CapsuleRunnerUnavailableError,
|
|
15
|
+
CapsuleServiceUnavailable,
|
|
16
|
+
CapsuleTimeoutError,
|
|
17
|
+
)
|
|
18
|
+
from capsule_sdk._shell_async import AsyncShellSession
|
|
19
|
+
from capsule_sdk._version import __version__
|
|
20
|
+
from capsule_sdk.async_client import AsyncCapsuleClient
|
|
21
|
+
from capsule_sdk.async_runner_config import AsyncRunnerConfigs
|
|
22
|
+
from capsule_sdk.async_runner_session import AsyncRunnerSession
|
|
23
|
+
from capsule_sdk.client import CapsuleClient
|
|
24
|
+
from capsule_sdk.models.workload import WorkloadSummary
|
|
25
|
+
from capsule_sdk.resources.async_runners import AsyncRunners
|
|
26
|
+
from capsule_sdk.resources.async_snapshots import AsyncSnapshots
|
|
27
|
+
from capsule_sdk.resources.async_workloads import AsyncWorkloads
|
|
28
|
+
from capsule_sdk.resources.workloads import Workloads
|
|
29
|
+
from capsule_sdk.runner_config import RunnerConfig, RunnerConfigs
|
|
30
|
+
from capsule_sdk.runner_session import RunnerSession
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"CapsuleAuthError",
|
|
34
|
+
"CapsuleAllocationTimeoutError",
|
|
35
|
+
"AsyncCapsuleClient",
|
|
36
|
+
"AsyncRunners",
|
|
37
|
+
"AsyncRunnerConfigs",
|
|
38
|
+
"AsyncRunnerSession",
|
|
39
|
+
"AsyncShellSession",
|
|
40
|
+
"AsyncSnapshots",
|
|
41
|
+
"AsyncWorkloads",
|
|
42
|
+
"CapsuleClient",
|
|
43
|
+
"CapsuleConflict",
|
|
44
|
+
"CapsuleConnectionError",
|
|
45
|
+
"CapsuleError",
|
|
46
|
+
"CapsuleHTTPError",
|
|
47
|
+
"CapsuleNotFound",
|
|
48
|
+
"CapsuleOperationTimeoutError",
|
|
49
|
+
"CapsuleRequestTimeoutError",
|
|
50
|
+
"CapsuleRateLimited",
|
|
51
|
+
"CapsuleRunnerUnavailableError",
|
|
52
|
+
"CapsuleServiceUnavailable",
|
|
53
|
+
"CapsuleTimeoutError",
|
|
54
|
+
"RunnerConfig",
|
|
55
|
+
"RunnerConfigs",
|
|
56
|
+
"RunnerSession",
|
|
57
|
+
"WorkloadSummary",
|
|
58
|
+
"Workloads",
|
|
59
|
+
"__version__",
|
|
60
|
+
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from capsule_sdk._version import __version__
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class ConnectionConfig:
|
|
11
|
+
"""Resolved connection configuration."""
|
|
12
|
+
|
|
13
|
+
base_url: str
|
|
14
|
+
token: str | None
|
|
15
|
+
request_timeout: float
|
|
16
|
+
startup_timeout: float
|
|
17
|
+
operation_timeout: float
|
|
18
|
+
user_agent: str
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def timeout(self) -> float:
|
|
22
|
+
"""Backward-compatible alias for the request timeout."""
|
|
23
|
+
return self.request_timeout
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def resolve(
|
|
27
|
+
cls,
|
|
28
|
+
*,
|
|
29
|
+
base_url: str | None = None,
|
|
30
|
+
token: str | None = None,
|
|
31
|
+
timeout: float = 30.0,
|
|
32
|
+
request_timeout: float | None = None,
|
|
33
|
+
startup_timeout: float | None = None,
|
|
34
|
+
operation_timeout: float | None = None,
|
|
35
|
+
) -> ConnectionConfig:
|
|
36
|
+
resolved_base_url = (
|
|
37
|
+
base_url
|
|
38
|
+
or os.environ.get("CAPSULE_BASE_URL")
|
|
39
|
+
or "http://localhost:8080"
|
|
40
|
+
).rstrip("/")
|
|
41
|
+
|
|
42
|
+
resolved_token = token if token is not None else os.environ.get("CAPSULE_TOKEN")
|
|
43
|
+
resolved_request_timeout = (
|
|
44
|
+
request_timeout
|
|
45
|
+
if request_timeout is not None
|
|
46
|
+
else float(os.environ.get("CAPSULE_REQUEST_TIMEOUT", timeout))
|
|
47
|
+
)
|
|
48
|
+
resolved_startup_timeout = (
|
|
49
|
+
startup_timeout
|
|
50
|
+
if startup_timeout is not None
|
|
51
|
+
else float(os.environ.get("CAPSULE_STARTUP_TIMEOUT", 45.0))
|
|
52
|
+
)
|
|
53
|
+
resolved_operation_timeout = (
|
|
54
|
+
operation_timeout
|
|
55
|
+
if operation_timeout is not None
|
|
56
|
+
else float(os.environ.get("CAPSULE_OPERATION_TIMEOUT", 120.0))
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return cls(
|
|
60
|
+
base_url=resolved_base_url,
|
|
61
|
+
token=resolved_token,
|
|
62
|
+
request_timeout=resolved_request_timeout,
|
|
63
|
+
startup_timeout=resolved_startup_timeout,
|
|
64
|
+
operation_timeout=resolved_operation_timeout,
|
|
65
|
+
user_agent=f"capsule-sdk-python/{__version__}",
|
|
66
|
+
)
|