hopx-ai 0.1.11__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.
@@ -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}>"