chalkcompute 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.
- chalkcompute-0.1.0/.gitignore +3 -0
- chalkcompute-0.1.0/PKG-INFO +365 -0
- chalkcompute-0.1.0/README.md +355 -0
- chalkcompute-0.1.0/chalkcompute/__init__.py +52 -0
- chalkcompute-0.1.0/chalkcompute/_proto/__init__.py +0 -0
- chalkcompute-0.1.0/chalkcompute/_proto/auth/__init__.py +0 -0
- chalkcompute-0.1.0/chalkcompute/_proto/auth/v1/__init__.py +0 -0
- chalkcompute-0.1.0/chalkcompute/_proto/auth/v1/permissions_pb2.py +107 -0
- chalkcompute-0.1.0/chalkcompute/_proto/auth/v1/permissions_pb2.pyi +84 -0
- chalkcompute-0.1.0/chalkcompute/_proto/container/__init__.py +0 -0
- chalkcompute-0.1.0/chalkcompute/_proto/container/v1/__init__.py +0 -0
- chalkcompute-0.1.0/chalkcompute/_proto/container/v1/service_pb2.py +96 -0
- chalkcompute-0.1.0/chalkcompute/_proto/container/v1/service_pb2.pyi +222 -0
- chalkcompute-0.1.0/chalkcompute/_proto/container/v1/service_pb2_grpc.py +318 -0
- chalkcompute-0.1.0/chalkcompute/_proto/sandbox/__init__.py +0 -0
- chalkcompute-0.1.0/chalkcompute/_proto/sandbox/v1/__init__.py +0 -0
- chalkcompute-0.1.0/chalkcompute/_proto/sandbox/v1/service_pb2.py +114 -0
- chalkcompute-0.1.0/chalkcompute/_proto/sandbox/v1/service_pb2.pyi +305 -0
- chalkcompute-0.1.0/chalkcompute/_proto/sandbox/v1/service_pb2_grpc.py +394 -0
- chalkcompute-0.1.0/chalkcompute/_proto/utils/__init__.py +0 -0
- chalkcompute-0.1.0/chalkcompute/_proto/utils/v1/__init__.py +0 -0
- chalkcompute-0.1.0/chalkcompute/_proto/utils/v1/encoding_pb2.py +41 -0
- chalkcompute-0.1.0/chalkcompute/_proto/utils/v1/encoding_pb2.pyi +23 -0
- chalkcompute-0.1.0/chalkcompute/_proto/volume/__init__.py +0 -0
- chalkcompute-0.1.0/chalkcompute/_proto/volume/v1/__init__.py +0 -0
- chalkcompute-0.1.0/chalkcompute/_proto/volume/v1/volume_pb2.py +91 -0
- chalkcompute-0.1.0/chalkcompute/_proto/volume/v1/volume_pb2.pyi +127 -0
- chalkcompute-0.1.0/chalkcompute/_proto/volume/v1/volume_pb2_grpc.py +401 -0
- chalkcompute-0.1.0/chalkcompute/container.py +562 -0
- chalkcompute-0.1.0/chalkcompute/image.py +320 -0
- chalkcompute-0.1.0/chalkcompute/sandbox.py +596 -0
- chalkcompute-0.1.0/chalkcompute/volume.py +436 -0
- chalkcompute-0.1.0/pyproject.toml +33 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chalkcompute
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SDK for Chalk sandboxes, containers, and volumes
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: grpcio>=1.60.0
|
|
7
|
+
Requires-Dist: httpx>=0.25.0
|
|
8
|
+
Requires-Dist: protobuf>=4.25.0
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# Chalk Sandbox SDK
|
|
12
|
+
|
|
13
|
+
Python SDK for the Chalk Sandbox gRPC service. Create sandboxes, execute commands, and stream output over bidirectional gRPC streams.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
pip install grpcio protobuf
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from sandbox import SandboxClient
|
|
25
|
+
|
|
26
|
+
client = SandboxClient("localhost:50051")
|
|
27
|
+
|
|
28
|
+
# Create a sandbox from a pre-built image
|
|
29
|
+
sandbox = client.create(image="ubuntu:latest", name="my-sandbox")
|
|
30
|
+
|
|
31
|
+
# Run a command
|
|
32
|
+
result = sandbox.exec("echo", "hello world")
|
|
33
|
+
print(result.stdout_text) # "hello world"
|
|
34
|
+
print(result.exit_code) # 0
|
|
35
|
+
|
|
36
|
+
# Clean up
|
|
37
|
+
sandbox.terminate()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Declarative images
|
|
41
|
+
|
|
42
|
+
Build custom container images with a fluent API instead of writing Dockerfiles.
|
|
43
|
+
The image spec is serialized as protobuf and transmitted to the sandbox service,
|
|
44
|
+
which builds and caches the image before starting the container.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from sandbox import SandboxClient
|
|
48
|
+
from image import Image
|
|
49
|
+
|
|
50
|
+
client = SandboxClient("localhost:50051")
|
|
51
|
+
|
|
52
|
+
# Build a data-science image declaratively
|
|
53
|
+
img = (
|
|
54
|
+
Image.debian_slim("3.12")
|
|
55
|
+
.pip_install(["pandas", "numpy", "scikit-learn"])
|
|
56
|
+
.run_commands(
|
|
57
|
+
"apt-get update && apt-get install -y git curl",
|
|
58
|
+
)
|
|
59
|
+
.workdir("/home/user/app")
|
|
60
|
+
.env({"PYTHONDONTWRITEBYTECODE": "1"})
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
sandbox = client.create(image=img, name="data-science")
|
|
64
|
+
result = sandbox.exec("python", "-c", "import pandas; print(pandas.__version__)")
|
|
65
|
+
print(result.stdout_text)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Base images
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
# Arbitrary base image
|
|
72
|
+
img = Image.base("node:22-slim")
|
|
73
|
+
|
|
74
|
+
# Convenience: python + debian slim
|
|
75
|
+
img = Image.debian_slim("3.12") # python:3.12-slim-bookworm
|
|
76
|
+
|
|
77
|
+
# From an existing Dockerfile (contents are inlined, so you can chain more steps)
|
|
78
|
+
img = Image.from_dockerfile("Dockerfile").pip_install(["extra-dep"])
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Build steps
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
img = (
|
|
85
|
+
Image.debian_slim("3.12")
|
|
86
|
+
# Install Python packages
|
|
87
|
+
.pip_install(["requests>=2.28", "flask"])
|
|
88
|
+
|
|
89
|
+
# Install from a requirements.txt (read locally, inlined into the spec)
|
|
90
|
+
.pip_install_from_requirements("requirements.txt")
|
|
91
|
+
|
|
92
|
+
# Run shell commands (each becomes a Docker RUN layer)
|
|
93
|
+
.run_commands(
|
|
94
|
+
"apt-get update && apt-get install -y git",
|
|
95
|
+
"mkdir -p /app/data",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Add local files into the image
|
|
99
|
+
.add_local_file("config.yaml", "/app/config.yaml")
|
|
100
|
+
.add_local_file("entrypoint.sh", "/app/entrypoint.sh", mode=0o755)
|
|
101
|
+
.add_local_dir("src", "/app/src")
|
|
102
|
+
|
|
103
|
+
# Raw Dockerfile instructions
|
|
104
|
+
.dockerfile_commands(["EXPOSE 8080", "HEALTHCHECK CMD curl -f http://localhost:8080/"])
|
|
105
|
+
|
|
106
|
+
# Image-level configuration
|
|
107
|
+
.workdir("/app")
|
|
108
|
+
.env({"FLASK_APP": "app:create_app"})
|
|
109
|
+
.entrypoint(["/app/entrypoint.sh"])
|
|
110
|
+
.cmd(["serve"])
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Immutable composition
|
|
115
|
+
|
|
116
|
+
Each builder method returns a new `Image`, so intermediate images can be shared:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
base = Image.debian_slim("3.12").pip_install(["requests"])
|
|
120
|
+
|
|
121
|
+
# Two different images that share the same base
|
|
122
|
+
api_image = base.pip_install(["flask"]).workdir("/api")
|
|
123
|
+
worker_image = base.pip_install(["celery"]).workdir("/worker")
|
|
124
|
+
|
|
125
|
+
api_sandbox = client.create(image=api_image, name="api")
|
|
126
|
+
worker_sandbox = client.create(image=worker_image, name="worker")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Connecting
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from sandbox import SandboxClient
|
|
133
|
+
import grpc
|
|
134
|
+
|
|
135
|
+
# Insecure (local dev)
|
|
136
|
+
client = SandboxClient("localhost:50051")
|
|
137
|
+
|
|
138
|
+
# With TLS
|
|
139
|
+
creds = grpc.ssl_channel_credentials()
|
|
140
|
+
client = SandboxClient("sandbox.example.com:443", credentials=creds)
|
|
141
|
+
|
|
142
|
+
# As a context manager
|
|
143
|
+
with SandboxClient("localhost:50051") as client:
|
|
144
|
+
...
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Sandbox lifecycle
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
# Create with resource limits
|
|
151
|
+
sandbox = client.create(
|
|
152
|
+
image="ubuntu:latest",
|
|
153
|
+
name="dev-sandbox",
|
|
154
|
+
cpu="2",
|
|
155
|
+
memory="4Gi",
|
|
156
|
+
env={"DEBIAN_FRONTEND": "noninteractive"},
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# List all sandboxes
|
|
160
|
+
for info in client.list():
|
|
161
|
+
print(f"{info.id} {info.state} {info.name}")
|
|
162
|
+
|
|
163
|
+
# Get a handle to an existing sandbox by ID
|
|
164
|
+
sandbox = client.get(id="550e8400-e29b-41d4-a716-446655440000")
|
|
165
|
+
|
|
166
|
+
# Fetch info from server
|
|
167
|
+
print(sandbox.info.state)
|
|
168
|
+
sandbox.refresh() # force re-fetch
|
|
169
|
+
|
|
170
|
+
# Terminate
|
|
171
|
+
sandbox.terminate()
|
|
172
|
+
sandbox.terminate(grace_period_seconds=30)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Executing commands
|
|
176
|
+
|
|
177
|
+
### Run and wait
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
result = sandbox.exec("ls", "-la", "/tmp")
|
|
181
|
+
for line in result.stdout:
|
|
182
|
+
print(line)
|
|
183
|
+
for line in result.stderr:
|
|
184
|
+
print(f"ERR: {line}")
|
|
185
|
+
print(f"exit code: {result.exit_code}")
|
|
186
|
+
|
|
187
|
+
# Or get the full text at once
|
|
188
|
+
print(result.stdout_text)
|
|
189
|
+
print(result.stderr_text)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Stream output in real time
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
for event in sandbox.exec_stream("make", "build", workdir="/app"):
|
|
196
|
+
if event.stdout:
|
|
197
|
+
print(event.stdout, end="")
|
|
198
|
+
if event.stderr:
|
|
199
|
+
print(event.stderr, end="", file=sys.stderr)
|
|
200
|
+
if event.is_exited:
|
|
201
|
+
print(f"\nDone: exit code {event.exit_code}")
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Interactive processes (stdin + signals)
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
process = sandbox.exec_start("bash")
|
|
208
|
+
|
|
209
|
+
process.write_stdin("echo hello\n")
|
|
210
|
+
process.write_stdin("exit\n")
|
|
211
|
+
process.close_stdin()
|
|
212
|
+
|
|
213
|
+
for event in process.output():
|
|
214
|
+
if event.stdout:
|
|
215
|
+
print(event.stdout, end="")
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Send signals to running processes:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
import signal
|
|
222
|
+
|
|
223
|
+
process = sandbox.exec_start("sleep", "300")
|
|
224
|
+
process.send_signal(signal.SIGTERM)
|
|
225
|
+
result = process.wait()
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Options
|
|
229
|
+
|
|
230
|
+
All exec methods accept the same keyword arguments:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
result = sandbox.exec(
|
|
234
|
+
"python", "train.py",
|
|
235
|
+
workdir="/app", # working directory
|
|
236
|
+
timeout_secs=3600, # kill after 1 hour
|
|
237
|
+
env={"CUDA_VISIBLE_DEVICES": "0"}, # environment variables
|
|
238
|
+
)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Examples
|
|
242
|
+
|
|
243
|
+
### Clone a GitHub repo into a sandbox
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
from sandbox import SandboxClient
|
|
247
|
+
|
|
248
|
+
client = SandboxClient("localhost:50051")
|
|
249
|
+
sandbox = client.create(image="ubuntu:latest", name="repo-sandbox")
|
|
250
|
+
|
|
251
|
+
# Install git
|
|
252
|
+
sandbox.exec("apt-get", "update")
|
|
253
|
+
sandbox.exec("apt-get", "install", "-y", "git")
|
|
254
|
+
|
|
255
|
+
# Clone
|
|
256
|
+
result = sandbox.exec(
|
|
257
|
+
"git", "clone", "https://github.com/chalk-ai/chalk.git", "/workspace/chalk"
|
|
258
|
+
)
|
|
259
|
+
if result.exit_code != 0:
|
|
260
|
+
print(f"Clone failed: {result.stderr_text}")
|
|
261
|
+
else:
|
|
262
|
+
# List what we got
|
|
263
|
+
result = sandbox.exec("ls", "-la", "/workspace/chalk")
|
|
264
|
+
for line in result.stdout:
|
|
265
|
+
print(line)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Spawn an OpenCode agent in a sandbox
|
|
269
|
+
|
|
270
|
+
[OpenCode](https://github.com/opencode-ai/opencode) is a terminal-based AI coding agent. You can run it inside a sandbox to give it an isolated environment to work in.
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
from sandbox import SandboxClient
|
|
274
|
+
|
|
275
|
+
client = SandboxClient("localhost:50051")
|
|
276
|
+
sandbox = client.create(
|
|
277
|
+
image="ubuntu:latest",
|
|
278
|
+
name="opencode-agent",
|
|
279
|
+
cpu="2",
|
|
280
|
+
memory="4Gi",
|
|
281
|
+
env={
|
|
282
|
+
"ANTHROPIC_API_KEY": "sk-ant-...",
|
|
283
|
+
},
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Install dependencies
|
|
287
|
+
sandbox.exec("apt-get", "update")
|
|
288
|
+
sandbox.exec("apt-get", "install", "-y", "git", "curl", "build-essential")
|
|
289
|
+
|
|
290
|
+
# Install Go (opencode is a Go binary)
|
|
291
|
+
sandbox.exec("bash", "-c", "curl -fsSL https://go.dev/dl/go1.24.1.linux-amd64.tar.gz | tar -C /usr/local -xz")
|
|
292
|
+
sandbox.exec("bash", "-c", "echo 'export PATH=$PATH:/usr/local/go/bin:/root/go/bin' >> /root/.bashrc")
|
|
293
|
+
|
|
294
|
+
# Install opencode
|
|
295
|
+
sandbox.exec("bash", "-c", "export PATH=$PATH:/usr/local/go/bin:/root/go/bin && go install github.com/opencode-ai/opencode@latest")
|
|
296
|
+
|
|
297
|
+
# Clone a repo to work on
|
|
298
|
+
sandbox.exec("git", "clone", "https://github.com/your-org/your-repo.git", "/workspace/repo")
|
|
299
|
+
|
|
300
|
+
# Run opencode non-interactively with a prompt
|
|
301
|
+
result = sandbox.exec(
|
|
302
|
+
"bash", "-c",
|
|
303
|
+
"export PATH=$PATH:/usr/local/go/bin:/root/go/bin && cd /workspace/repo && opencode -p 'fix the failing tests in pkg/auth'",
|
|
304
|
+
timeout_secs=600,
|
|
305
|
+
)
|
|
306
|
+
print(result.stdout_text)
|
|
307
|
+
|
|
308
|
+
# Or run it interactively and feed it commands
|
|
309
|
+
process = sandbox.exec_start(
|
|
310
|
+
"bash", "-c",
|
|
311
|
+
"export PATH=$PATH:/usr/local/go/bin:/root/go/bin && cd /workspace/repo && opencode",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Stream its output
|
|
315
|
+
for event in process.output():
|
|
316
|
+
if event.stdout:
|
|
317
|
+
print(event.stdout, end="")
|
|
318
|
+
if event.stderr:
|
|
319
|
+
print(event.stderr, end="", file=sys.stderr)
|
|
320
|
+
if event.is_exited:
|
|
321
|
+
break
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Long-running build with real-time output
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
sandbox = client.create(image="node:22", name="build")
|
|
328
|
+
|
|
329
|
+
sandbox.exec("git", "clone", "https://github.com/your-org/frontend.git", "/app")
|
|
330
|
+
sandbox.exec("npm", "install", workdir="/app")
|
|
331
|
+
|
|
332
|
+
# Stream the build output as it happens
|
|
333
|
+
for event in sandbox.exec_stream("npm", "run", "build", workdir="/app"):
|
|
334
|
+
if event.stdout:
|
|
335
|
+
print(event.stdout, end="")
|
|
336
|
+
if event.stderr:
|
|
337
|
+
print(event.stderr, end="", file=sys.stderr)
|
|
338
|
+
if event.is_exited and event.exit_code != 0:
|
|
339
|
+
print(f"Build failed with exit code {event.exit_code}")
|
|
340
|
+
|
|
341
|
+
sandbox.terminate()
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## CLI tools
|
|
345
|
+
|
|
346
|
+
### `sandbox_exec.py` - Run a command
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
python sandbox_exec.py --target localhost:50051 --sandbox-id <id> --exec "ls -la"
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### `sandbox_stdout.py` - Interactive shell
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
echo "echo hello" | python sandbox_stdout.py --target localhost:50051 --sandbox-id <id> --exec "bash"
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Regenerating proto stubs
|
|
359
|
+
|
|
360
|
+
If the proto definition changes, regenerate the Python stubs:
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
pip install grpcio-tools
|
|
364
|
+
./generate.sh
|
|
365
|
+
```
|