hopx-ai 0.1.10__py3-none-any.whl
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.
Potentially problematic release.
This version of hopx-ai might be problematic. Click here for more details.
- hopx_ai/__init__.py +114 -0
- hopx_ai/_agent_client.py +373 -0
- hopx_ai/_async_client.py +230 -0
- hopx_ai/_client.py +230 -0
- hopx_ai/_generated/__init__.py +22 -0
- hopx_ai/_generated/models.py +502 -0
- hopx_ai/_utils.py +9 -0
- hopx_ai/_ws_client.py +141 -0
- hopx_ai/async_sandbox.py +427 -0
- hopx_ai/cache.py +97 -0
- hopx_ai/commands.py +174 -0
- hopx_ai/desktop.py +1227 -0
- hopx_ai/env_vars.py +242 -0
- hopx_ai/errors.py +249 -0
- hopx_ai/files.py +489 -0
- hopx_ai/models.py +274 -0
- hopx_ai/models_updated.py +270 -0
- hopx_ai/sandbox.py +1439 -0
- hopx_ai/template/__init__.py +47 -0
- hopx_ai/template/build_flow.py +540 -0
- hopx_ai/template/builder.py +300 -0
- hopx_ai/template/file_hasher.py +81 -0
- hopx_ai/template/ready_checks.py +106 -0
- hopx_ai/template/tar_creator.py +122 -0
- hopx_ai/template/types.py +199 -0
- hopx_ai/terminal.py +164 -0
- hopx_ai-0.1.10.dist-info/METADATA +460 -0
- hopx_ai-0.1.10.dist-info/RECORD +29 -0
- hopx_ai-0.1.10.dist-info/WHEEL +4 -0
hopx_ai/async_sandbox.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"""Async Sandbox class - for async/await usage."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, List, AsyncIterator, Dict
|
|
4
|
+
from .models import SandboxInfo, Template
|
|
5
|
+
from ._async_client import AsyncHTTPClient
|
|
6
|
+
from ._utils import remove_none_values
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AsyncSandbox:
|
|
10
|
+
"""
|
|
11
|
+
Async Bunnyshell Sandbox - lightweight VM management with async/await.
|
|
12
|
+
|
|
13
|
+
For async Python applications (FastAPI, aiohttp, etc.)
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
>>> from bunnyshell import AsyncSandbox
|
|
17
|
+
>>>
|
|
18
|
+
>>> async with AsyncSandbox.create(template="nodejs") as sandbox:
|
|
19
|
+
... info = await sandbox.get_info()
|
|
20
|
+
... print(info.public_host)
|
|
21
|
+
# Automatically cleaned up!
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
sandbox_id: str,
|
|
27
|
+
*,
|
|
28
|
+
api_key: Optional[str] = None,
|
|
29
|
+
base_url: str = "https://api.hopx.dev",
|
|
30
|
+
timeout: int = 60,
|
|
31
|
+
max_retries: int = 3,
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
Initialize AsyncSandbox instance.
|
|
35
|
+
|
|
36
|
+
Note: Prefer using AsyncSandbox.create() or AsyncSandbox.connect() instead.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
sandbox_id: Sandbox ID
|
|
40
|
+
api_key: API key (or use HOPX_API_KEY env var)
|
|
41
|
+
base_url: API base URL
|
|
42
|
+
timeout: Request timeout in seconds
|
|
43
|
+
max_retries: Maximum number of retries
|
|
44
|
+
"""
|
|
45
|
+
self.sandbox_id = sandbox_id
|
|
46
|
+
self._client = AsyncHTTPClient(
|
|
47
|
+
api_key=api_key,
|
|
48
|
+
base_url=base_url,
|
|
49
|
+
timeout=timeout,
|
|
50
|
+
max_retries=max_retries,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# =============================================================================
|
|
54
|
+
# CLASS METHODS (Static - for creating/listing sandboxes)
|
|
55
|
+
# =============================================================================
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
async def create(
|
|
59
|
+
cls,
|
|
60
|
+
template: Optional[str] = None,
|
|
61
|
+
*,
|
|
62
|
+
template_id: Optional[str] = None,
|
|
63
|
+
region: Optional[str] = None,
|
|
64
|
+
timeout_seconds: Optional[int] = None,
|
|
65
|
+
internet_access: Optional[bool] = None,
|
|
66
|
+
env_vars: Optional[Dict[str, str]] = None,
|
|
67
|
+
api_key: Optional[str] = None,
|
|
68
|
+
base_url: str = "https://api.hopx.dev",
|
|
69
|
+
) -> "AsyncSandbox":
|
|
70
|
+
"""
|
|
71
|
+
Create a new sandbox (async).
|
|
72
|
+
|
|
73
|
+
You can create a sandbox in two ways:
|
|
74
|
+
1. From template ID (resources auto-loaded from template)
|
|
75
|
+
2. Custom sandbox (specify template name + resources)
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
template: Template name for custom sandbox (e.g., "code-interpreter", "nodejs")
|
|
79
|
+
template_id: Template ID to create from (resources auto-loaded, no vcpu/memory needed)
|
|
80
|
+
region: Preferred region (optional)
|
|
81
|
+
timeout_seconds: Auto-kill timeout in seconds (optional, default: no timeout)
|
|
82
|
+
internet_access: Enable internet access (optional, default: True)
|
|
83
|
+
env_vars: Environment variables (optional)
|
|
84
|
+
api_key: API key (or use HOPX_API_KEY env var)
|
|
85
|
+
base_url: API base URL
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
AsyncSandbox instance
|
|
89
|
+
|
|
90
|
+
Examples:
|
|
91
|
+
>>> # Create from template ID with timeout
|
|
92
|
+
>>> sandbox = await AsyncSandbox.create(
|
|
93
|
+
... template_id="282",
|
|
94
|
+
... timeout_seconds=600,
|
|
95
|
+
... internet_access=False
|
|
96
|
+
... )
|
|
97
|
+
|
|
98
|
+
>>> # Create custom sandbox
|
|
99
|
+
>>> sandbox = await AsyncSandbox.create(
|
|
100
|
+
... template="nodejs",
|
|
101
|
+
... timeout_seconds=300
|
|
102
|
+
... )
|
|
103
|
+
"""
|
|
104
|
+
client = AsyncHTTPClient(api_key=api_key, base_url=base_url)
|
|
105
|
+
|
|
106
|
+
# Validate parameters
|
|
107
|
+
if template_id:
|
|
108
|
+
# Create from template ID (resources from template)
|
|
109
|
+
# Convert template_id to string if it's an int (API may return int from build)
|
|
110
|
+
data = remove_none_values({
|
|
111
|
+
"template_id": str(template_id),
|
|
112
|
+
"region": region,
|
|
113
|
+
"timeout_seconds": timeout_seconds,
|
|
114
|
+
"internet_access": internet_access,
|
|
115
|
+
"env_vars": env_vars,
|
|
116
|
+
})
|
|
117
|
+
elif template:
|
|
118
|
+
# Create from template name (resources from template)
|
|
119
|
+
data = remove_none_values({
|
|
120
|
+
"template_name": template,
|
|
121
|
+
"region": region,
|
|
122
|
+
"timeout_seconds": timeout_seconds,
|
|
123
|
+
"internet_access": internet_access,
|
|
124
|
+
"env_vars": env_vars,
|
|
125
|
+
})
|
|
126
|
+
else:
|
|
127
|
+
raise ValueError("Either 'template' or 'template_id' must be provided")
|
|
128
|
+
|
|
129
|
+
response = await client.post("/v1/sandboxes", json=data)
|
|
130
|
+
sandbox_id = response["id"]
|
|
131
|
+
|
|
132
|
+
return cls(
|
|
133
|
+
sandbox_id=sandbox_id,
|
|
134
|
+
api_key=api_key,
|
|
135
|
+
base_url=base_url,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
async def connect(
|
|
140
|
+
cls,
|
|
141
|
+
sandbox_id: str,
|
|
142
|
+
*,
|
|
143
|
+
api_key: Optional[str] = None,
|
|
144
|
+
base_url: str = "https://api.hopx.dev",
|
|
145
|
+
) -> "AsyncSandbox":
|
|
146
|
+
"""
|
|
147
|
+
Connect to an existing sandbox (async).
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
sandbox_id: Sandbox ID
|
|
151
|
+
api_key: API key (or use HOPX_API_KEY env var)
|
|
152
|
+
base_url: API base URL
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
AsyncSandbox instance
|
|
156
|
+
|
|
157
|
+
Example:
|
|
158
|
+
>>> sandbox = await AsyncSandbox.connect("sandbox_id")
|
|
159
|
+
>>> info = await sandbox.get_info()
|
|
160
|
+
"""
|
|
161
|
+
instance = cls(
|
|
162
|
+
sandbox_id=sandbox_id,
|
|
163
|
+
api_key=api_key,
|
|
164
|
+
base_url=base_url,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Verify it exists
|
|
168
|
+
await instance.get_info()
|
|
169
|
+
|
|
170
|
+
return instance
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
async def list(
|
|
174
|
+
cls,
|
|
175
|
+
*,
|
|
176
|
+
status: Optional[str] = None,
|
|
177
|
+
region: Optional[str] = None,
|
|
178
|
+
limit: int = 100,
|
|
179
|
+
api_key: Optional[str] = None,
|
|
180
|
+
base_url: str = "https://api.hopx.dev",
|
|
181
|
+
) -> List["AsyncSandbox"]:
|
|
182
|
+
"""
|
|
183
|
+
List all sandboxes (async).
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
status: Filter by status
|
|
187
|
+
region: Filter by region
|
|
188
|
+
limit: Maximum number of results
|
|
189
|
+
api_key: API key
|
|
190
|
+
base_url: API base URL
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
List of AsyncSandbox instances
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
>>> sandboxes = await AsyncSandbox.list(status="running")
|
|
197
|
+
>>> for sb in sandboxes:
|
|
198
|
+
... info = await sb.get_info()
|
|
199
|
+
... print(info.public_host)
|
|
200
|
+
"""
|
|
201
|
+
client = AsyncHTTPClient(api_key=api_key, base_url=base_url)
|
|
202
|
+
|
|
203
|
+
params = remove_none_values({
|
|
204
|
+
"status": status,
|
|
205
|
+
"region": region,
|
|
206
|
+
"limit": limit,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
response = await client.get("/v1/sandboxes", params=params)
|
|
210
|
+
sandboxes_data = response.get("data", [])
|
|
211
|
+
|
|
212
|
+
return [
|
|
213
|
+
cls(
|
|
214
|
+
sandbox_id=sb["id"],
|
|
215
|
+
api_key=api_key,
|
|
216
|
+
base_url=base_url,
|
|
217
|
+
)
|
|
218
|
+
for sb in sandboxes_data
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
@classmethod
|
|
222
|
+
async def iter(
|
|
223
|
+
cls,
|
|
224
|
+
*,
|
|
225
|
+
status: Optional[str] = None,
|
|
226
|
+
region: Optional[str] = None,
|
|
227
|
+
api_key: Optional[str] = None,
|
|
228
|
+
base_url: str = "https://api.hopx.dev",
|
|
229
|
+
) -> AsyncIterator["AsyncSandbox"]:
|
|
230
|
+
"""
|
|
231
|
+
Lazy async iterator for sandboxes.
|
|
232
|
+
|
|
233
|
+
Yields sandboxes one by one, fetching pages as needed.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
status: Filter by status
|
|
237
|
+
region: Filter by region
|
|
238
|
+
api_key: API key
|
|
239
|
+
base_url: API base URL
|
|
240
|
+
|
|
241
|
+
Yields:
|
|
242
|
+
AsyncSandbox instances
|
|
243
|
+
|
|
244
|
+
Example:
|
|
245
|
+
>>> async for sandbox in AsyncSandbox.iter(status="running"):
|
|
246
|
+
... info = await sandbox.get_info()
|
|
247
|
+
... print(info.public_host)
|
|
248
|
+
... if found:
|
|
249
|
+
... break # Doesn't fetch remaining pages
|
|
250
|
+
"""
|
|
251
|
+
client = AsyncHTTPClient(api_key=api_key, base_url=base_url)
|
|
252
|
+
limit = 100
|
|
253
|
+
has_more = True
|
|
254
|
+
cursor = None
|
|
255
|
+
|
|
256
|
+
while has_more:
|
|
257
|
+
params = {"limit": limit}
|
|
258
|
+
if status:
|
|
259
|
+
params["status"] = status
|
|
260
|
+
if region:
|
|
261
|
+
params["region"] = region
|
|
262
|
+
if cursor:
|
|
263
|
+
params["cursor"] = cursor
|
|
264
|
+
|
|
265
|
+
response = await client.get("/v1/sandboxes", params=params)
|
|
266
|
+
|
|
267
|
+
for item in response.get("data", []):
|
|
268
|
+
yield cls(
|
|
269
|
+
sandbox_id=item["id"],
|
|
270
|
+
api_key=api_key,
|
|
271
|
+
base_url=base_url,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
has_more = response.get("has_more", False)
|
|
275
|
+
cursor = response.get("next_cursor")
|
|
276
|
+
|
|
277
|
+
@classmethod
|
|
278
|
+
async def list_templates(
|
|
279
|
+
cls,
|
|
280
|
+
*,
|
|
281
|
+
category: Optional[str] = None,
|
|
282
|
+
language: Optional[str] = None,
|
|
283
|
+
api_key: Optional[str] = None,
|
|
284
|
+
base_url: str = "https://api.hopx.dev",
|
|
285
|
+
) -> List[Template]:
|
|
286
|
+
"""
|
|
287
|
+
List available templates (async).
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
category: Filter by category
|
|
291
|
+
language: Filter by language
|
|
292
|
+
api_key: API key
|
|
293
|
+
base_url: API base URL
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
List of Template objects
|
|
297
|
+
|
|
298
|
+
Example:
|
|
299
|
+
>>> templates = await AsyncSandbox.list_templates()
|
|
300
|
+
>>> for t in templates:
|
|
301
|
+
... print(f"{t.name}: {t.display_name}")
|
|
302
|
+
"""
|
|
303
|
+
client = AsyncHTTPClient(api_key=api_key, base_url=base_url)
|
|
304
|
+
|
|
305
|
+
params = remove_none_values({
|
|
306
|
+
"category": category,
|
|
307
|
+
"language": language,
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
response = await client.get("/v1/templates", params=params)
|
|
311
|
+
templates_data = response.get("data", [])
|
|
312
|
+
|
|
313
|
+
return [Template(**t) for t in templates_data]
|
|
314
|
+
|
|
315
|
+
@classmethod
|
|
316
|
+
async def get_template(
|
|
317
|
+
cls,
|
|
318
|
+
name: str,
|
|
319
|
+
*,
|
|
320
|
+
api_key: Optional[str] = None,
|
|
321
|
+
base_url: str = "https://api.hopx.dev",
|
|
322
|
+
) -> Template:
|
|
323
|
+
"""
|
|
324
|
+
Get template details (async).
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
name: Template name
|
|
328
|
+
api_key: API key
|
|
329
|
+
base_url: API base URL
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Template object
|
|
333
|
+
|
|
334
|
+
Example:
|
|
335
|
+
>>> template = await AsyncSandbox.get_template("nodejs")
|
|
336
|
+
>>> print(template.description)
|
|
337
|
+
"""
|
|
338
|
+
client = AsyncHTTPClient(api_key=api_key, base_url=base_url)
|
|
339
|
+
response = await client.get(f"/v1/templates/{name}")
|
|
340
|
+
return Template(**response)
|
|
341
|
+
|
|
342
|
+
# =============================================================================
|
|
343
|
+
# INSTANCE METHODS (for managing individual sandbox)
|
|
344
|
+
# =============================================================================
|
|
345
|
+
|
|
346
|
+
async def get_info(self) -> SandboxInfo:
|
|
347
|
+
"""
|
|
348
|
+
Get current sandbox information (async).
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
SandboxInfo with current state
|
|
352
|
+
|
|
353
|
+
Example:
|
|
354
|
+
>>> info = await sandbox.get_info()
|
|
355
|
+
>>> print(f"Status: {info.status}")
|
|
356
|
+
"""
|
|
357
|
+
response = await self._client.get(f"/v1/sandboxes/{self.sandbox_id}")
|
|
358
|
+
return SandboxInfo(
|
|
359
|
+
sandbox_id=response["id"],
|
|
360
|
+
template_id=response.get("template_id"),
|
|
361
|
+
template_name=response.get("template_name"),
|
|
362
|
+
organization_id=response.get("organization_id", ""),
|
|
363
|
+
node_id=response.get("node_id"),
|
|
364
|
+
region=response.get("region"),
|
|
365
|
+
status=response["status"],
|
|
366
|
+
public_host=response.get("public_host") or response.get("direct_url", ""),
|
|
367
|
+
vcpu=response.get("resources", {}).get("vcpu"),
|
|
368
|
+
memory_mb=response.get("resources", {}).get("memory_mb"),
|
|
369
|
+
disk_mb=response.get("resources", {}).get("disk_mb"),
|
|
370
|
+
created_at=response.get("created_at"),
|
|
371
|
+
started_at=None,
|
|
372
|
+
end_at=None,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
async def stop(self) -> None:
|
|
376
|
+
"""Stop the sandbox (async)."""
|
|
377
|
+
await self._client.post(f"/v1/sandboxes/{self.sandbox_id}/stop")
|
|
378
|
+
|
|
379
|
+
async def start(self) -> None:
|
|
380
|
+
"""Start a stopped sandbox (async)."""
|
|
381
|
+
await self._client.post(f"/v1/sandboxes/{self.sandbox_id}/start")
|
|
382
|
+
|
|
383
|
+
async def pause(self) -> None:
|
|
384
|
+
"""Pause the sandbox (async)."""
|
|
385
|
+
await self._client.post(f"/v1/sandboxes/{self.sandbox_id}/pause")
|
|
386
|
+
|
|
387
|
+
async def resume(self) -> None:
|
|
388
|
+
"""Resume a paused sandbox (async)."""
|
|
389
|
+
await self._client.post(f"/v1/sandboxes/{self.sandbox_id}/resume")
|
|
390
|
+
|
|
391
|
+
async def kill(self) -> None:
|
|
392
|
+
"""
|
|
393
|
+
Destroy the sandbox immediately (async).
|
|
394
|
+
|
|
395
|
+
This action is irreversible.
|
|
396
|
+
|
|
397
|
+
Example:
|
|
398
|
+
>>> await sandbox.kill()
|
|
399
|
+
"""
|
|
400
|
+
await self._client.delete(f"/v1/sandboxes/{self.sandbox_id}")
|
|
401
|
+
|
|
402
|
+
# =============================================================================
|
|
403
|
+
# ASYNC CONTEXT MANAGER (auto-cleanup)
|
|
404
|
+
# =============================================================================
|
|
405
|
+
|
|
406
|
+
async def __aenter__(self) -> "AsyncSandbox":
|
|
407
|
+
"""Async context manager entry."""
|
|
408
|
+
return self
|
|
409
|
+
|
|
410
|
+
async def __aexit__(self, *args) -> None:
|
|
411
|
+
"""Async context manager exit - auto cleanup."""
|
|
412
|
+
try:
|
|
413
|
+
await self.kill()
|
|
414
|
+
except Exception:
|
|
415
|
+
# Ignore errors on cleanup
|
|
416
|
+
pass
|
|
417
|
+
|
|
418
|
+
# =============================================================================
|
|
419
|
+
# UTILITY METHODS
|
|
420
|
+
# =============================================================================
|
|
421
|
+
|
|
422
|
+
def __repr__(self) -> str:
|
|
423
|
+
return f"<AsyncSandbox {self.sandbox_id}>"
|
|
424
|
+
|
|
425
|
+
def __str__(self) -> str:
|
|
426
|
+
return f"AsyncSandbox(id={self.sandbox_id})"
|
|
427
|
+
|
hopx_ai/cache.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Cache management resource for Bunnyshell Sandboxes."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any, Optional
|
|
4
|
+
import logging
|
|
5
|
+
from ._agent_client import AgentHTTPClient
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Cache:
|
|
11
|
+
"""
|
|
12
|
+
Cache management resource.
|
|
13
|
+
|
|
14
|
+
Provides methods for managing execution result cache.
|
|
15
|
+
|
|
16
|
+
Features:
|
|
17
|
+
- Get cache statistics
|
|
18
|
+
- Clear cache
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
>>> sandbox = Sandbox.create(template="code-interpreter")
|
|
22
|
+
>>>
|
|
23
|
+
>>> # Get cache stats
|
|
24
|
+
>>> stats = sandbox.cache.stats()
|
|
25
|
+
>>> print(f"Cache hits: {stats['hits']}")
|
|
26
|
+
>>> print(f"Cache size: {stats['size']} MB")
|
|
27
|
+
>>>
|
|
28
|
+
>>> # Clear cache
|
|
29
|
+
>>> sandbox.cache.clear()
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, client: AgentHTTPClient):
|
|
33
|
+
"""
|
|
34
|
+
Initialize Cache resource.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
client: Shared agent HTTP client
|
|
38
|
+
"""
|
|
39
|
+
self._client = client
|
|
40
|
+
logger.debug("Cache resource initialized")
|
|
41
|
+
|
|
42
|
+
def stats(self, *, timeout: Optional[int] = None) -> Dict[str, Any]:
|
|
43
|
+
"""
|
|
44
|
+
Get cache statistics.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
timeout: Request timeout in seconds (overrides default)
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Dictionary with cache statistics (hits, misses, size, etc.)
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
>>> stats = sandbox.cache.stats()
|
|
54
|
+
>>> print(f"Cache hits: {stats['hits']}")
|
|
55
|
+
>>> print(f"Cache misses: {stats['misses']}")
|
|
56
|
+
>>> print(f"Hit rate: {stats['hit_rate']:.2%}")
|
|
57
|
+
>>> print(f"Cache size: {stats['size']} MB")
|
|
58
|
+
>>> print(f"Entry count: {stats['count']}")
|
|
59
|
+
"""
|
|
60
|
+
logger.debug("Getting cache statistics")
|
|
61
|
+
|
|
62
|
+
response = self._client.get(
|
|
63
|
+
"/cache/stats",
|
|
64
|
+
operation="get cache stats",
|
|
65
|
+
timeout=timeout
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return response.json()
|
|
69
|
+
|
|
70
|
+
def clear(self, *, timeout: Optional[int] = None) -> Dict[str, Any]:
|
|
71
|
+
"""
|
|
72
|
+
Clear the execution result cache.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
timeout: Request timeout in seconds (overrides default)
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Dictionary with confirmation message
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
>>> result = sandbox.cache.clear()
|
|
82
|
+
>>> print(result['message']) # "Cache cleared successfully"
|
|
83
|
+
>>> print(f"Entries removed: {result.get('entries_removed', 0)}")
|
|
84
|
+
"""
|
|
85
|
+
logger.debug("Clearing cache")
|
|
86
|
+
|
|
87
|
+
response = self._client.post(
|
|
88
|
+
"/cache/clear",
|
|
89
|
+
operation="clear cache",
|
|
90
|
+
timeout=timeout
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return response.json()
|
|
94
|
+
|
|
95
|
+
def __repr__(self) -> str:
|
|
96
|
+
return f"<Cache client={self._client}>"
|
|
97
|
+
|
hopx_ai/commands.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Command execution resource for Bunnyshell Sandboxes."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Dict
|
|
4
|
+
import logging
|
|
5
|
+
from .models import CommandResult
|
|
6
|
+
from ._agent_client import AgentHTTPClient
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Commands:
|
|
12
|
+
"""
|
|
13
|
+
Command execution resource.
|
|
14
|
+
|
|
15
|
+
Provides methods for running shell commands inside the sandbox.
|
|
16
|
+
|
|
17
|
+
Features:
|
|
18
|
+
- Automatic retry with exponential backoff
|
|
19
|
+
- Connection pooling for efficiency
|
|
20
|
+
- Proper error handling
|
|
21
|
+
- Configurable timeouts
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
>>> sandbox = Sandbox.create(template="code-interpreter")
|
|
25
|
+
>>>
|
|
26
|
+
>>> # Run simple command
|
|
27
|
+
>>> result = sandbox.commands.run('ls -la /workspace')
|
|
28
|
+
>>> print(result.stdout)
|
|
29
|
+
>>>
|
|
30
|
+
>>> # Check success
|
|
31
|
+
>>> if result.success:
|
|
32
|
+
... print("Command succeeded!")
|
|
33
|
+
... else:
|
|
34
|
+
... print(f"Failed: {result.stderr}")
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, client: AgentHTTPClient):
|
|
38
|
+
"""
|
|
39
|
+
Initialize Commands resource.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
client: Shared agent HTTP client
|
|
43
|
+
"""
|
|
44
|
+
self._client = client
|
|
45
|
+
logger.debug("Commands resource initialized")
|
|
46
|
+
|
|
47
|
+
def run(
|
|
48
|
+
self,
|
|
49
|
+
command: str,
|
|
50
|
+
*,
|
|
51
|
+
timeout: int = 30,
|
|
52
|
+
background: bool = False,
|
|
53
|
+
env: Optional[Dict[str, str]] = None,
|
|
54
|
+
working_dir: str = "/workspace",
|
|
55
|
+
) -> CommandResult:
|
|
56
|
+
"""
|
|
57
|
+
Run shell command.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
command: Shell command to run
|
|
61
|
+
timeout: Command timeout in seconds (default: 30)
|
|
62
|
+
background: Run in background (returns immediately)
|
|
63
|
+
env: Optional environment variables for this command only.
|
|
64
|
+
Priority: Request env > Global env > Agent env
|
|
65
|
+
working_dir: Working directory for command (default: /workspace)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
CommandResult with stdout, stderr, exit_code
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
CommandExecutionError: If command execution fails
|
|
72
|
+
TimeoutError: If command times out
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
>>> # Simple command
|
|
76
|
+
>>> result = sandbox.commands.run('ls -la')
|
|
77
|
+
>>> print(result.stdout)
|
|
78
|
+
>>> print(f"Exit code: {result.exit_code}")
|
|
79
|
+
>>>
|
|
80
|
+
>>> # With environment variables
|
|
81
|
+
>>> result = sandbox.commands.run(
|
|
82
|
+
... 'echo $API_KEY',
|
|
83
|
+
... env={"API_KEY": "sk-test-123"}
|
|
84
|
+
... )
|
|
85
|
+
>>>
|
|
86
|
+
>>> # With custom timeout
|
|
87
|
+
>>> result = sandbox.commands.run('npm install', timeout=300)
|
|
88
|
+
>>>
|
|
89
|
+
>>> # Check success
|
|
90
|
+
>>> if result.success:
|
|
91
|
+
... print("Success!")
|
|
92
|
+
... else:
|
|
93
|
+
... print(f"Failed with exit code {result.exit_code}")
|
|
94
|
+
... print(f"Error: {result.stderr}")
|
|
95
|
+
"""
|
|
96
|
+
if background:
|
|
97
|
+
return self._run_background(command, env=env, working_dir=working_dir)
|
|
98
|
+
|
|
99
|
+
logger.debug(f"Running command: {command[:50]}...")
|
|
100
|
+
|
|
101
|
+
# Build request payload
|
|
102
|
+
payload = {
|
|
103
|
+
"command": command,
|
|
104
|
+
"timeout": timeout,
|
|
105
|
+
"working_dir": working_dir
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Add optional environment variables
|
|
109
|
+
if env:
|
|
110
|
+
payload["env"] = env
|
|
111
|
+
|
|
112
|
+
response = self._client.post(
|
|
113
|
+
"/commands/run",
|
|
114
|
+
json=payload,
|
|
115
|
+
operation="run command",
|
|
116
|
+
context={"command": command},
|
|
117
|
+
timeout=timeout + 5 # Add buffer to HTTP timeout
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
data = response.json()
|
|
121
|
+
|
|
122
|
+
return CommandResult(
|
|
123
|
+
stdout=data.get("stdout", ""),
|
|
124
|
+
stderr=data.get("stderr", ""),
|
|
125
|
+
exit_code=data.get("exit_code", 0),
|
|
126
|
+
execution_time=data.get("execution_time", 0.0)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def _run_background(
|
|
130
|
+
self,
|
|
131
|
+
command: str,
|
|
132
|
+
env: Optional[Dict[str, str]] = None,
|
|
133
|
+
working_dir: str = "/workspace"
|
|
134
|
+
) -> CommandResult:
|
|
135
|
+
"""
|
|
136
|
+
Run command in background.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
command: Shell command to run
|
|
140
|
+
env: Optional environment variables
|
|
141
|
+
working_dir: Working directory
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
CommandResult with process info
|
|
145
|
+
"""
|
|
146
|
+
logger.debug(f"Running command in background: {command[:50]}...")
|
|
147
|
+
|
|
148
|
+
# Build request payload
|
|
149
|
+
payload = {"command": command, "working_dir": working_dir}
|
|
150
|
+
|
|
151
|
+
# Add optional environment variables
|
|
152
|
+
if env:
|
|
153
|
+
payload["env"] = env
|
|
154
|
+
|
|
155
|
+
response = self._client.post(
|
|
156
|
+
"/commands/background",
|
|
157
|
+
json=payload,
|
|
158
|
+
operation="run background command",
|
|
159
|
+
context={"command": command},
|
|
160
|
+
timeout=10
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
data = response.json()
|
|
164
|
+
|
|
165
|
+
# Return a CommandResult indicating background execution
|
|
166
|
+
return CommandResult(
|
|
167
|
+
stdout=f"Background process started: {data.get('process_id', 'unknown')}",
|
|
168
|
+
stderr="",
|
|
169
|
+
exit_code=0,
|
|
170
|
+
execution_time=0.0
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def __repr__(self) -> str:
|
|
174
|
+
return f"<Commands client={self._client}>"
|