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.
Files changed (57) hide show
  1. capsule_sdk-0.1.0/.gitignore +101 -0
  2. capsule_sdk-0.1.0/PKG-INFO +222 -0
  3. capsule_sdk-0.1.0/README.md +204 -0
  4. capsule_sdk-0.1.0/pyproject.toml +49 -0
  5. capsule_sdk-0.1.0/src/capsule_sdk/__init__.py +60 -0
  6. capsule_sdk-0.1.0/src/capsule_sdk/_config.py +66 -0
  7. capsule_sdk-0.1.0/src/capsule_sdk/_errors.py +142 -0
  8. capsule_sdk-0.1.0/src/capsule_sdk/_http.py +461 -0
  9. capsule_sdk-0.1.0/src/capsule_sdk/_http_async.py +429 -0
  10. capsule_sdk-0.1.0/src/capsule_sdk/_shell.py +118 -0
  11. capsule_sdk-0.1.0/src/capsule_sdk/_shell_async.py +127 -0
  12. capsule_sdk-0.1.0/src/capsule_sdk/_snapshot_commands.py +32 -0
  13. capsule_sdk-0.1.0/src/capsule_sdk/_version.py +1 -0
  14. capsule_sdk-0.1.0/src/capsule_sdk/async_client.py +47 -0
  15. capsule_sdk-0.1.0/src/capsule_sdk/async_runner_config.py +18 -0
  16. capsule_sdk-0.1.0/src/capsule_sdk/async_runner_session.py +207 -0
  17. capsule_sdk-0.1.0/src/capsule_sdk/client.py +47 -0
  18. capsule_sdk-0.1.0/src/capsule_sdk/models/__init__.py +74 -0
  19. capsule_sdk-0.1.0/src/capsule_sdk/models/common.py +31 -0
  20. capsule_sdk-0.1.0/src/capsule_sdk/models/file.py +53 -0
  21. capsule_sdk-0.1.0/src/capsule_sdk/models/layered_config.py +130 -0
  22. capsule_sdk-0.1.0/src/capsule_sdk/models/runner.py +134 -0
  23. capsule_sdk-0.1.0/src/capsule_sdk/models/snapshot.py +37 -0
  24. capsule_sdk-0.1.0/src/capsule_sdk/models/workload.py +24 -0
  25. capsule_sdk-0.1.0/src/capsule_sdk/py.typed +0 -0
  26. capsule_sdk-0.1.0/src/capsule_sdk/resources/__init__.py +8 -0
  27. capsule_sdk-0.1.0/src/capsule_sdk/resources/async_layered_configs.py +192 -0
  28. capsule_sdk-0.1.0/src/capsule_sdk/resources/async_runners.py +655 -0
  29. capsule_sdk-0.1.0/src/capsule_sdk/resources/async_snapshots.py +15 -0
  30. capsule_sdk-0.1.0/src/capsule_sdk/resources/async_workloads.py +270 -0
  31. capsule_sdk-0.1.0/src/capsule_sdk/resources/layered_configs.py +200 -0
  32. capsule_sdk-0.1.0/src/capsule_sdk/resources/runners.py +692 -0
  33. capsule_sdk-0.1.0/src/capsule_sdk/resources/snapshots.py +15 -0
  34. capsule_sdk-0.1.0/src/capsule_sdk/resources/workloads.py +276 -0
  35. capsule_sdk-0.1.0/src/capsule_sdk/runner_config.py +170 -0
  36. capsule_sdk-0.1.0/src/capsule_sdk/runner_session.py +254 -0
  37. capsule_sdk-0.1.0/tests/conftest.py +11 -0
  38. capsule_sdk-0.1.0/tests/e2e_live.py +186 -0
  39. capsule_sdk-0.1.0/tests/e2e_live_async.py +188 -0
  40. capsule_sdk-0.1.0/tests/test_async_client_surface.py +26 -0
  41. capsule_sdk-0.1.0/tests/test_async_http.py +233 -0
  42. capsule_sdk-0.1.0/tests/test_async_runner_session.py +130 -0
  43. capsule_sdk-0.1.0/tests/test_async_runners.py +216 -0
  44. capsule_sdk-0.1.0/tests/test_async_shell.py +133 -0
  45. capsule_sdk-0.1.0/tests/test_client_surface.py +24 -0
  46. capsule_sdk-0.1.0/tests/test_config.py +56 -0
  47. capsule_sdk-0.1.0/tests/test_contract.py +96 -0
  48. capsule_sdk-0.1.0/tests/test_errors.py +83 -0
  49. capsule_sdk-0.1.0/tests/test_files.py +171 -0
  50. capsule_sdk-0.1.0/tests/test_http_retries.py +149 -0
  51. capsule_sdk-0.1.0/tests/test_layered_configs.py +147 -0
  52. capsule_sdk-0.1.0/tests/test_runner_config.py +157 -0
  53. capsule_sdk-0.1.0/tests/test_runner_session.py +241 -0
  54. capsule_sdk-0.1.0/tests/test_runners.py +252 -0
  55. capsule_sdk-0.1.0/tests/test_shell.py +198 -0
  56. capsule_sdk-0.1.0/tests/test_snapshots.py +79 -0
  57. 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
+ )