sandforge-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.
- sandforge_sdk-0.1.0/PKG-INFO +391 -0
- sandforge_sdk-0.1.0/README.md +365 -0
- sandforge_sdk-0.1.0/pyproject.toml +33 -0
- sandforge_sdk-0.1.0/sandforge/__init__.py +66 -0
- sandforge_sdk-0.1.0/sandforge/client.py +448 -0
- sandforge_sdk-0.1.0/sandforge/py.typed +0 -0
- sandforge_sdk-0.1.0/sandforge/types.py +146 -0
- sandforge_sdk-0.1.0/sandforge_sdk.egg-info/PKG-INFO +391 -0
- sandforge_sdk-0.1.0/sandforge_sdk.egg-info/SOURCES.txt +14 -0
- sandforge_sdk-0.1.0/sandforge_sdk.egg-info/dependency_links.txt +1 -0
- sandforge_sdk-0.1.0/sandforge_sdk.egg-info/requires.txt +8 -0
- sandforge_sdk-0.1.0/sandforge_sdk.egg-info/top_level.txt +2 -0
- sandforge_sdk-0.1.0/setup.cfg +4 -0
- sandforge_sdk-0.1.0/setup.py +42 -0
- sandforge_sdk-0.1.0/tests/__init__.py +0 -0
- sandforge_sdk-0.1.0/tests/test_client.py +313 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sandforge-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Sandforge hypervisor sandbox platform
|
|
5
|
+
Home-page: https://github.com/yanurag-dev/sandforge
|
|
6
|
+
Author: Anurag Yadav
|
|
7
|
+
Author-email: Anurag Yadav <yadavanurag1310@gmail.com>
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
Project-URL: Homepage, https://github.com/yanurag-dev/sandforge
|
|
10
|
+
Project-URL: Repository, https://github.com/yanurag-dev/sandforge
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: requests>=2.33.0
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-cov>=2.10; extra == "dev"
|
|
20
|
+
Requires-Dist: black>=21.0; extra == "dev"
|
|
21
|
+
Requires-Dist: mypy>=0.910; extra == "dev"
|
|
22
|
+
Requires-Dist: flake8>=3.9; extra == "dev"
|
|
23
|
+
Dynamic: author
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: requires-python
|
|
26
|
+
|
|
27
|
+
# Sandforge Python SDK
|
|
28
|
+
|
|
29
|
+
The Sandforge Python SDK provides a client library for interacting with the Sandforge hypervisor sandbox platform. It enables you to create, manage, and execute commands in isolated sandboxes programmatically.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
Install the SDK from the repository:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install -e .
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or with development dependencies:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install -e ".[dev]"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
### Basic Usage
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from sandforge import Client, SandboxSpec
|
|
51
|
+
|
|
52
|
+
# Create a client pointing to your Sandforge control plane
|
|
53
|
+
client = Client("http://localhost:8080")
|
|
54
|
+
|
|
55
|
+
# Create a sandbox with default configuration
|
|
56
|
+
sandbox = client.create_sandbox()
|
|
57
|
+
|
|
58
|
+
# Run a command
|
|
59
|
+
result = sandbox.commands.run(["echo", "Hello, Sandforge!"])
|
|
60
|
+
print(result.stdout) # "Hello, Sandforge!\n"
|
|
61
|
+
|
|
62
|
+
# Clean up
|
|
63
|
+
sandbox.kill()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Custom Sandbox Configuration
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from sandforge import Client, SandboxSpec, WorkspaceMount
|
|
70
|
+
|
|
71
|
+
spec = SandboxSpec(
|
|
72
|
+
cpu=4,
|
|
73
|
+
memory_mb=2048,
|
|
74
|
+
disk_gb=20,
|
|
75
|
+
timeout_sec=3600,
|
|
76
|
+
network_mode="fetch", # Allow package downloads
|
|
77
|
+
mounts=[
|
|
78
|
+
WorkspaceMount(
|
|
79
|
+
host_path="/path/to/project",
|
|
80
|
+
guest_path="/workspace",
|
|
81
|
+
read_only=False,
|
|
82
|
+
),
|
|
83
|
+
],
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
client = Client("http://localhost:8080")
|
|
87
|
+
sandbox = client.create_sandbox(spec)
|
|
88
|
+
|
|
89
|
+
# Work with the mounted directory
|
|
90
|
+
result = sandbox.commands.run(["ls", "-la", "/workspace"])
|
|
91
|
+
print(result.stdout)
|
|
92
|
+
|
|
93
|
+
sandbox.kill()
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Command Execution with Environment Variables
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from sandforge import Client
|
|
100
|
+
|
|
101
|
+
client = Client("http://localhost:8080")
|
|
102
|
+
sandbox = client.create_sandbox()
|
|
103
|
+
|
|
104
|
+
# Run with custom environment variables
|
|
105
|
+
result = sandbox.commands.run(
|
|
106
|
+
command=["python", "-c", "import os; print(os.environ.get('MY_VAR'))"],
|
|
107
|
+
cwd="/",
|
|
108
|
+
env={"MY_VAR": "Hello World"},
|
|
109
|
+
timeout_sec=30,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
print(result.stdout) # "Hello World\n"
|
|
113
|
+
print(result.exit_code) # 0
|
|
114
|
+
|
|
115
|
+
sandbox.kill()
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Error Handling
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from sandforge import Client, NetworkError, SandboxNotFoundError
|
|
122
|
+
|
|
123
|
+
client = Client("http://localhost:8080")
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
sandbox = client.create_sandbox()
|
|
127
|
+
result = sandbox.commands.run(["false"]) # Command that fails
|
|
128
|
+
|
|
129
|
+
if result.exit_code != 0:
|
|
130
|
+
print(f"Command failed with exit code {result.exit_code}")
|
|
131
|
+
print(f"stderr: {result.stderr}")
|
|
132
|
+
|
|
133
|
+
sandbox.kill()
|
|
134
|
+
|
|
135
|
+
except NetworkError as e:
|
|
136
|
+
print(f"Connection error: {e}")
|
|
137
|
+
except SandboxNotFoundError as e:
|
|
138
|
+
print(f"Sandbox not found: {e}")
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Sandbox Information
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from sandforge import Client
|
|
145
|
+
|
|
146
|
+
client = Client("http://localhost:8080")
|
|
147
|
+
sandbox = client.create_sandbox()
|
|
148
|
+
|
|
149
|
+
# Get sandbox information
|
|
150
|
+
info = sandbox.info()
|
|
151
|
+
print(f"Sandbox ID: {info.id}")
|
|
152
|
+
print(f"State: {info.state}") # "ready", "executing", "destroyed", etc.
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## API Reference
|
|
156
|
+
|
|
157
|
+
### Client
|
|
158
|
+
|
|
159
|
+
The main entry point for interacting with Sandforge.
|
|
160
|
+
|
|
161
|
+
#### Constructor
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
Client(base_url: str, timeout: int = 60)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
- `base_url`: The control plane URL (e.g., "http://localhost:8080")
|
|
168
|
+
- `timeout`: Request timeout in seconds (default: 60)
|
|
169
|
+
|
|
170
|
+
#### Methods
|
|
171
|
+
|
|
172
|
+
##### `create_sandbox(spec: Optional[SandboxSpec] = None) -> SandboxHandle`
|
|
173
|
+
|
|
174
|
+
Create a new sandbox.
|
|
175
|
+
|
|
176
|
+
- Returns: A `SandboxHandle` to the created sandbox
|
|
177
|
+
- Raises: `NetworkError` or `SandforgeException`
|
|
178
|
+
|
|
179
|
+
##### `exec(sandbox_id: str, request: ExecRequest) -> ExecResult`
|
|
180
|
+
|
|
181
|
+
Execute a command in a sandbox.
|
|
182
|
+
|
|
183
|
+
- Returns: An `ExecResult` with exit code, stdout, and stderr
|
|
184
|
+
- Raises: `NetworkError` or `SandforgeException`
|
|
185
|
+
|
|
186
|
+
##### `get_status(sandbox_id: str) -> str`
|
|
187
|
+
|
|
188
|
+
Get the current state of a sandbox.
|
|
189
|
+
|
|
190
|
+
- Returns: The sandbox state as a string
|
|
191
|
+
- Raises: `NetworkError`
|
|
192
|
+
|
|
193
|
+
##### `get_info(sandbox_id: str) -> SandboxInfo`
|
|
194
|
+
|
|
195
|
+
Get detailed information about a sandbox.
|
|
196
|
+
|
|
197
|
+
- Returns: A `SandboxInfo` object
|
|
198
|
+
- Raises: `NetworkError`
|
|
199
|
+
|
|
200
|
+
##### `destroy(sandbox_id: str) -> None`
|
|
201
|
+
|
|
202
|
+
Destroy a sandbox.
|
|
203
|
+
|
|
204
|
+
- Raises: `NetworkError` or `SandforgeException`
|
|
205
|
+
|
|
206
|
+
### SandboxHandle
|
|
207
|
+
|
|
208
|
+
A handle to a created sandbox with convenience APIs.
|
|
209
|
+
|
|
210
|
+
#### Properties
|
|
211
|
+
|
|
212
|
+
- `id`: The sandbox ID (string)
|
|
213
|
+
|
|
214
|
+
#### Methods
|
|
215
|
+
|
|
216
|
+
##### `kill() -> None`
|
|
217
|
+
|
|
218
|
+
Destroy the sandbox.
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
sandbox.kill()
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
##### `info() -> SandboxInfo`
|
|
225
|
+
|
|
226
|
+
Get sandbox information.
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
info = sandbox.info()
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
#### Nested APIs
|
|
233
|
+
|
|
234
|
+
##### `commands`
|
|
235
|
+
|
|
236
|
+
The `CommandsAPI` for executing commands.
|
|
237
|
+
|
|
238
|
+
###### `run(command, cwd="/", env=None, timeout_sec=60) -> ExecResult`
|
|
239
|
+
|
|
240
|
+
Run a command in the sandbox.
|
|
241
|
+
|
|
242
|
+
- `command`: List of command and arguments
|
|
243
|
+
- `cwd`: Working directory (default: "/")
|
|
244
|
+
- `env`: Dictionary of environment variables (default: {})
|
|
245
|
+
- `timeout_sec`: Command timeout in seconds (default: 60)
|
|
246
|
+
- Returns: `ExecResult` with exit code, stdout, and stderr
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
result = sandbox.commands.run(
|
|
250
|
+
["python", "script.py"],
|
|
251
|
+
cwd="/workspace",
|
|
252
|
+
env={"PYTHONUNBUFFERED": "1"},
|
|
253
|
+
timeout_sec=300,
|
|
254
|
+
)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
##### `files`
|
|
258
|
+
|
|
259
|
+
The `FilesAPI` for reading files from the sandbox.
|
|
260
|
+
|
|
261
|
+
###### `read(path: str) -> str`
|
|
262
|
+
|
|
263
|
+
Read a file from the sandbox.
|
|
264
|
+
|
|
265
|
+
**Note:** This method is currently not implemented and raises `NotImplementedError`. VSOCK copyout support is coming soon.
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
try:
|
|
269
|
+
content = sandbox.files.read("/etc/hostname")
|
|
270
|
+
except NotImplementedError:
|
|
271
|
+
print("files.read() not yet supported")
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Types
|
|
275
|
+
|
|
276
|
+
#### SandboxSpec
|
|
277
|
+
|
|
278
|
+
Specification for creating a sandbox.
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
SandboxSpec(
|
|
282
|
+
backend: str = "macos-vz", # "linux-kvm", "linux-firecracker", "macos-vz"
|
|
283
|
+
cpu: int = 2, # Number of vCPUs
|
|
284
|
+
memory_mb: int = 512, # Memory in MB
|
|
285
|
+
disk_gb: int = 10, # Disk size in GB
|
|
286
|
+
timeout_sec: int = 3600, # Sandbox lifetime in seconds
|
|
287
|
+
network_mode: str = "offline", # "offline", "fetch", "full"
|
|
288
|
+
task_isolation: str = "container", # "container", "process"
|
|
289
|
+
mounts: List[WorkspaceMount] = [], # Mounted directories
|
|
290
|
+
)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
#### WorkspaceMount
|
|
294
|
+
|
|
295
|
+
A directory mount from host to guest.
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
WorkspaceMount(
|
|
299
|
+
host_path: str, # Path on the host
|
|
300
|
+
guest_path: str, # Path in the sandbox
|
|
301
|
+
read_only: bool = False, # Whether the mount is read-only
|
|
302
|
+
)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
#### ExecRequest
|
|
306
|
+
|
|
307
|
+
A request to execute a command.
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
ExecRequest(
|
|
311
|
+
command: List[str], # Command and arguments
|
|
312
|
+
cwd: str = "/", # Working directory
|
|
313
|
+
env: Dict[str, str] = {}, # Environment variables
|
|
314
|
+
timeout_sec: int = 60, # Timeout in seconds
|
|
315
|
+
)
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
#### ExecResult
|
|
319
|
+
|
|
320
|
+
The result of command execution.
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
ExecResult(
|
|
324
|
+
exit_code: int, # Command exit code
|
|
325
|
+
stdout: str, # Standard output
|
|
326
|
+
stderr: str, # Standard error
|
|
327
|
+
artifacts: List[str] = [], # Paths to generated artifacts
|
|
328
|
+
)
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
#### SandboxInfo
|
|
332
|
+
|
|
333
|
+
Information about a sandbox.
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
SandboxInfo(
|
|
337
|
+
id: str, # Sandbox ID
|
|
338
|
+
state: str, # Current state (e.g., "ready", "executing", "destroyed")
|
|
339
|
+
)
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Exceptions
|
|
343
|
+
|
|
344
|
+
All exceptions inherit from `SandforgeException`.
|
|
345
|
+
|
|
346
|
+
- **SandforgeException**: Base exception for all Sandforge errors
|
|
347
|
+
- **NetworkError**: Network communication error with the control plane
|
|
348
|
+
- **SandboxNotFoundError**: Sandbox does not exist
|
|
349
|
+
- **ExecutionError**: Command execution failed
|
|
350
|
+
- **InvalidSpecError**: Invalid sandbox specification
|
|
351
|
+
|
|
352
|
+
## Error Handling
|
|
353
|
+
|
|
354
|
+
The SDK provides specific exception types for different error scenarios:
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
from sandforge import Client, NetworkError, SandboxNotFoundError, SandforgeException
|
|
358
|
+
|
|
359
|
+
client = Client("http://localhost:8080")
|
|
360
|
+
|
|
361
|
+
try:
|
|
362
|
+
sandbox = client.create_sandbox()
|
|
363
|
+
result = sandbox.commands.run(["exit", "1"])
|
|
364
|
+
except NetworkError as e:
|
|
365
|
+
print(f"Network error: {e}")
|
|
366
|
+
except SandboxNotFoundError as e:
|
|
367
|
+
print(f"Sandbox not found: {e}")
|
|
368
|
+
except SandforgeException as e:
|
|
369
|
+
print(f"Sandforge error: {e}")
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Running Tests
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
pip install -e ".[dev]"
|
|
376
|
+
pytest tests/
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Contributing
|
|
380
|
+
|
|
381
|
+
Contributions are welcome! Please ensure code passes linting and type checks:
|
|
382
|
+
|
|
383
|
+
```bash
|
|
384
|
+
black sandforge/
|
|
385
|
+
flake8 sandforge/
|
|
386
|
+
mypy sandforge/
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
## License
|
|
390
|
+
|
|
391
|
+
Apache License 2.0. See LICENSE in the repository root for details.
|