grasp-sdk 0.1.0__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 grasp-sdk might be problematic. Click here for more details.
- grasp_sdk/__init__.py +262 -0
- grasp_sdk/models/__init__.py +78 -0
- grasp_sdk/sandbox/chrome-stable.mjs +381 -0
- grasp_sdk/sandbox/chromium.mjs +378 -0
- grasp_sdk/sandbox/jsconfig.json +22 -0
- grasp_sdk/services/__init__.py +8 -0
- grasp_sdk/services/browser.py +414 -0
- grasp_sdk/services/sandbox.py +583 -0
- grasp_sdk/utils/__init__.py +31 -0
- grasp_sdk/utils/auth.py +227 -0
- grasp_sdk/utils/config.py +150 -0
- grasp_sdk/utils/logger.py +233 -0
- grasp_sdk-0.1.0.dist-info/METADATA +201 -0
- grasp_sdk-0.1.0.dist-info/RECORD +17 -0
- grasp_sdk-0.1.0.dist-info/WHEEL +5 -0
- grasp_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- grasp_sdk-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Sandbox service for managing E2B sandbox lifecycle and operations.
|
|
4
|
+
|
|
5
|
+
This module provides Python implementation of the sandbox service,
|
|
6
|
+
equivalent to the TypeScript version in src/services/sandbox.service.ts
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import os
|
|
11
|
+
import tempfile
|
|
12
|
+
import time
|
|
13
|
+
from typing import Dict, Any, Optional, Union, List, Callable
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from e2b import AsyncSandbox
|
|
16
|
+
|
|
17
|
+
# Type definitions for E2B SDK
|
|
18
|
+
from typing import Any as CommandHandle, Any as CommandResult
|
|
19
|
+
|
|
20
|
+
from ..models import ISandboxConfig, SandboxStatus, IScriptOptions, ICommandOptions
|
|
21
|
+
from ..utils.logger import Logger, get_logger
|
|
22
|
+
from ..utils.config import get_config
|
|
23
|
+
from ..utils.auth import verify
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CommandEventEmitter:
|
|
27
|
+
"""
|
|
28
|
+
Extended event emitter for background command execution.
|
|
29
|
+
Emits 'stdout', 'stderr', and 'exit' events.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, handle: Optional[Any] = None):
|
|
33
|
+
self.handle = handle
|
|
34
|
+
self._callbacks: Dict[str, List[Callable]] = {
|
|
35
|
+
'stdout': [],
|
|
36
|
+
'stderr': [],
|
|
37
|
+
'exit': [],
|
|
38
|
+
'error': []
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def set_handle(self, handle: Any) -> None:
|
|
42
|
+
"""Set the command handle (used internally)."""
|
|
43
|
+
self.handle = handle
|
|
44
|
+
|
|
45
|
+
def on(self, event: str, callback: Callable) -> None:
|
|
46
|
+
"""Register event listener."""
|
|
47
|
+
if event not in self._callbacks:
|
|
48
|
+
self._callbacks[event] = []
|
|
49
|
+
self._callbacks[event].append(callback)
|
|
50
|
+
|
|
51
|
+
def emit(self, event: str, *args) -> None:
|
|
52
|
+
"""Emit event to all registered listeners."""
|
|
53
|
+
if event in self._callbacks:
|
|
54
|
+
for callback in self._callbacks[event]:
|
|
55
|
+
try:
|
|
56
|
+
callback(*args)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
# Ignore callback errors to prevent breaking the emitter
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
async def wait(self) -> Any:
|
|
62
|
+
"""Wait for command to complete."""
|
|
63
|
+
while not self.handle:
|
|
64
|
+
await asyncio.sleep(0.1)
|
|
65
|
+
return await self.handle.wait()
|
|
66
|
+
|
|
67
|
+
async def kill(self) -> None:
|
|
68
|
+
"""Kill the running command."""
|
|
69
|
+
if not self.handle:
|
|
70
|
+
raise RuntimeError('Command handle not set')
|
|
71
|
+
await self.handle.kill()
|
|
72
|
+
|
|
73
|
+
def get_handle(self) -> Optional[Any]:
|
|
74
|
+
"""Get the original command handle."""
|
|
75
|
+
return self.handle
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SandboxService:
|
|
79
|
+
"""
|
|
80
|
+
E2B Sandbox service for managing sandbox lifecycle and operations.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, config: ISandboxConfig):
|
|
84
|
+
self.config = config
|
|
85
|
+
self.sandbox: Optional[Any] = None
|
|
86
|
+
self.status: SandboxStatus = SandboxStatus.STOPPED
|
|
87
|
+
self.logger = self._get_default_logger()
|
|
88
|
+
|
|
89
|
+
# Default working directory
|
|
90
|
+
self.DEFAULT_WORKING_DIRECTORY = '/home/user'
|
|
91
|
+
|
|
92
|
+
def _get_default_logger(self) -> Logger:
|
|
93
|
+
"""Gets or creates a default logger instance."""
|
|
94
|
+
try:
|
|
95
|
+
return get_logger().child('SandboxService')
|
|
96
|
+
except Exception:
|
|
97
|
+
# If logger is not initialized, create a default one
|
|
98
|
+
from ..utils.logger import Logger
|
|
99
|
+
default_logger = Logger({
|
|
100
|
+
'level': 'debug' if self.config.get('debug', False) else 'info',
|
|
101
|
+
'console': True,
|
|
102
|
+
})
|
|
103
|
+
return default_logger.child('SandboxService')
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def id(self) -> Optional[str]:
|
|
107
|
+
"""Get sandbox ID."""
|
|
108
|
+
return self.sandbox.sandbox_id if self.sandbox else None
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def workspace(self) -> str:
|
|
112
|
+
"""Get workspace ID from API key."""
|
|
113
|
+
return self.config['key'][3:19]
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def is_debug(self) -> bool:
|
|
117
|
+
"""Check if debug mode is enabled."""
|
|
118
|
+
return bool(self.config.get('debug', False))
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def timeout(self) -> int:
|
|
122
|
+
"""Get timeout value."""
|
|
123
|
+
return self.config['timeout']
|
|
124
|
+
|
|
125
|
+
async def create_sandbox(self) -> None:
|
|
126
|
+
"""
|
|
127
|
+
Creates and starts a new sandbox.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
RuntimeError: If sandbox creation fails
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
self.status = SandboxStatus.CREATING
|
|
135
|
+
self.logger.info('Creating Grasp sandbox', {
|
|
136
|
+
'templateId': self.config['templateId'],
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
# Verify authentication
|
|
140
|
+
res = await verify(self.config)
|
|
141
|
+
if not res['success']:
|
|
142
|
+
raise RuntimeError('Authorization failed.')
|
|
143
|
+
|
|
144
|
+
api_key = res['data']['token']
|
|
145
|
+
|
|
146
|
+
# Create sandbox
|
|
147
|
+
|
|
148
|
+
# Use Sandbox constructor directly (e2b SDK 1.5.3+)
|
|
149
|
+
self.sandbox = await AsyncSandbox.create(
|
|
150
|
+
template=self.config['templateId'],
|
|
151
|
+
api_key=api_key,
|
|
152
|
+
timeout=self.config['timeout'] // 1000 # Convert ms to seconds
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
self.status = SandboxStatus.RUNNING
|
|
156
|
+
self.logger.info('Grasp sandbox created successfully', {
|
|
157
|
+
'sandboxId': getattr(self.sandbox, 'sandbox_id', 'unknown'),
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
except Exception as error:
|
|
161
|
+
self.status = SandboxStatus.ERROR
|
|
162
|
+
self.logger.error('Failed to create Grasp sandbox', str(error))
|
|
163
|
+
raise RuntimeError(f'Failed to create sandbox: {error}')
|
|
164
|
+
|
|
165
|
+
async def run_command(
|
|
166
|
+
self,
|
|
167
|
+
command: str,
|
|
168
|
+
options: Optional[ICommandOptions] = None,
|
|
169
|
+
quiet: bool = False
|
|
170
|
+
) -> Union[Any, CommandEventEmitter, None]:
|
|
171
|
+
"""
|
|
172
|
+
Runs a command in the sandbox.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
command: Command to execute
|
|
176
|
+
options: Execution options
|
|
177
|
+
quiet: Suppress error logging
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
CommandResult for synchronous execution,
|
|
181
|
+
CommandEventEmitter for background execution,
|
|
182
|
+
or None for nohup execution
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
RuntimeError: If sandbox is not running or command fails
|
|
186
|
+
"""
|
|
187
|
+
if not self.sandbox or self.status != SandboxStatus.RUNNING:
|
|
188
|
+
raise RuntimeError('Sandbox is not running. Call create_sandbox() first.')
|
|
189
|
+
|
|
190
|
+
if options is None:
|
|
191
|
+
options = {}
|
|
192
|
+
|
|
193
|
+
cwd = options.get('cwd', self.DEFAULT_WORKING_DIRECTORY)
|
|
194
|
+
timeout_ms = options.get('timeout', self.config['timeout'])
|
|
195
|
+
use_nohup = options.get('nohup', False)
|
|
196
|
+
in_background = options.get('inBackground', False)
|
|
197
|
+
envs = options.get('envs', {})
|
|
198
|
+
|
|
199
|
+
# print(f'command: {command}')
|
|
200
|
+
# print(f'🚀 options {options}')
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
self.logger.debug('Running command in sandbox', {
|
|
204
|
+
'command': command,
|
|
205
|
+
'cwd': cwd,
|
|
206
|
+
'timeout': timeout_ms,
|
|
207
|
+
'nohup': use_nohup,
|
|
208
|
+
'background': in_background
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
if not in_background:
|
|
212
|
+
# Foreground execution
|
|
213
|
+
final_command = command
|
|
214
|
+
if use_nohup:
|
|
215
|
+
# Create log directory
|
|
216
|
+
await self.sandbox.commands.run('mkdir -p ~/logs/grasp')
|
|
217
|
+
|
|
218
|
+
# Generate log file name using sandbox id
|
|
219
|
+
log_file = f'~/logs/grasp/log-{self.id}.log'
|
|
220
|
+
final_command = f'nohup {command} > {log_file} 2>&1 &'
|
|
221
|
+
|
|
222
|
+
if hasattr(self.sandbox, 'commands'):
|
|
223
|
+
result = await self.sandbox.commands.run(
|
|
224
|
+
final_command,
|
|
225
|
+
cwd=cwd,
|
|
226
|
+
timeout=timeout_ms,
|
|
227
|
+
envs=envs,
|
|
228
|
+
background=in_background,
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
raise RuntimeError('Sandbox commands interface not available')
|
|
232
|
+
|
|
233
|
+
if hasattr(result, 'error') and result.error:
|
|
234
|
+
self.logger.error(result.stderr)
|
|
235
|
+
|
|
236
|
+
return None if use_nohup else result
|
|
237
|
+
|
|
238
|
+
else:
|
|
239
|
+
# Background execution
|
|
240
|
+
if use_nohup:
|
|
241
|
+
# Create log directory
|
|
242
|
+
await self.sandbox.commands.run('mkdir -p ~/logs/grasp')
|
|
243
|
+
|
|
244
|
+
# Generate log file name using sandbox id
|
|
245
|
+
log_file = f'~/logs/grasp/log-{self.id}.log'
|
|
246
|
+
nohup_command = f'nohup {command} > {log_file} 2>&1 &'
|
|
247
|
+
print(f"💬 {nohup_command}")
|
|
248
|
+
await self.sandbox.commands.run(
|
|
249
|
+
nohup_command,
|
|
250
|
+
cwd=cwd,
|
|
251
|
+
timeout=timeout_ms // 1000,
|
|
252
|
+
envs=envs,
|
|
253
|
+
background=in_background,
|
|
254
|
+
)
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
# Create CommandEventEmitter for background execution
|
|
258
|
+
event_emitter = CommandEventEmitter()
|
|
259
|
+
|
|
260
|
+
# Schedule background execution
|
|
261
|
+
async def _execute_background():
|
|
262
|
+
try:
|
|
263
|
+
if self.sandbox and hasattr(self.sandbox, 'commands'):
|
|
264
|
+
commands_attr = getattr(self.sandbox, 'commands', None)
|
|
265
|
+
if not commands_attr:
|
|
266
|
+
raise RuntimeError("Sandbox commands not available")
|
|
267
|
+
handle = await commands_attr.run(
|
|
268
|
+
command,
|
|
269
|
+
cwd=cwd,
|
|
270
|
+
timeout=timeout_ms // 1000,
|
|
271
|
+
background=True,
|
|
272
|
+
envs=envs,
|
|
273
|
+
on_stdout=lambda data: (
|
|
274
|
+
self.logger.debug('Command stdout', {'data': data}),
|
|
275
|
+
event_emitter.emit('stdout', data)
|
|
276
|
+
)[1],
|
|
277
|
+
on_stderr=lambda data: (
|
|
278
|
+
self.logger.debug('Command stderr', {'data': data}),
|
|
279
|
+
event_emitter.emit('stderr', data)
|
|
280
|
+
)[1]
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
event_emitter.set_handle(handle)
|
|
284
|
+
else:
|
|
285
|
+
raise RuntimeError("Sandbox or commands not available")
|
|
286
|
+
|
|
287
|
+
# Wait for command completion (for non-nohup commands)
|
|
288
|
+
if not use_nohup:
|
|
289
|
+
try:
|
|
290
|
+
result = await handle.wait()
|
|
291
|
+
event_emitter.emit('exit', result.exit_code, result)
|
|
292
|
+
except Exception as error:
|
|
293
|
+
event_emitter.emit('error', error)
|
|
294
|
+
|
|
295
|
+
except Exception as error:
|
|
296
|
+
event_emitter.emit('error', error)
|
|
297
|
+
|
|
298
|
+
# Start background execution
|
|
299
|
+
asyncio.create_task(_execute_background())
|
|
300
|
+
return event_emitter
|
|
301
|
+
|
|
302
|
+
except Exception as error:
|
|
303
|
+
if not quiet:
|
|
304
|
+
self.logger.error('Command execution failed', str(error))
|
|
305
|
+
|
|
306
|
+
# Return error result as dict
|
|
307
|
+
return {
|
|
308
|
+
'exit_code': 1,
|
|
309
|
+
'stdout': '',
|
|
310
|
+
'stderr': str(error)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async def run_script(
|
|
314
|
+
self,
|
|
315
|
+
code: str,
|
|
316
|
+
options: Optional[IScriptOptions] = None
|
|
317
|
+
) -> Union[Any, CommandEventEmitter]:
|
|
318
|
+
"""
|
|
319
|
+
Runs JavaScript code in the sandbox.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
code: JavaScript code to execute
|
|
323
|
+
options: Script execution options
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
CommandResult for synchronous execution,
|
|
327
|
+
CommandEventEmitter for background execution
|
|
328
|
+
|
|
329
|
+
Raises:
|
|
330
|
+
RuntimeError: If sandbox is not running or script execution fails
|
|
331
|
+
"""
|
|
332
|
+
if not self.sandbox or self.status != SandboxStatus.RUNNING:
|
|
333
|
+
raise RuntimeError('Sandbox is not running. Call create_sandbox() first.')
|
|
334
|
+
|
|
335
|
+
if options is None:
|
|
336
|
+
options = {'type': 'cjs'}
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
# Generate temporary file name in working directory
|
|
340
|
+
timestamp = int(time.time() * 1000)
|
|
341
|
+
extension = 'mjs' if options['type'] == 'esm' else 'js'
|
|
342
|
+
working_dir = options.get('cwd', self.DEFAULT_WORKING_DIRECTORY)
|
|
343
|
+
script_path = f'{working_dir}/script_{timestamp}.{extension}'
|
|
344
|
+
|
|
345
|
+
self.logger.debug('Running JavaScript code in sandbox', {
|
|
346
|
+
'type': options['type'],
|
|
347
|
+
'scriptPath': script_path,
|
|
348
|
+
'codeLength': len(code),
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
# Write code to temporary file
|
|
352
|
+
await self.sandbox.files.write(script_path, code)
|
|
353
|
+
|
|
354
|
+
# Choose execution command based on type
|
|
355
|
+
pre_command = options.get('preCommand', '')
|
|
356
|
+
command = f'{pre_command}node {script_path}'
|
|
357
|
+
|
|
358
|
+
# Set environment variables
|
|
359
|
+
envs = options.get('envs', {})
|
|
360
|
+
envs['PLAYWRIGHT_BROWSERS_PATH'] = '0'
|
|
361
|
+
|
|
362
|
+
# Prepare command options
|
|
363
|
+
cmd_options: Optional[Any] = {
|
|
364
|
+
'cwd': options.get('cwd'),
|
|
365
|
+
'timeout': options.get('timeoutMs'),
|
|
366
|
+
'inBackground': options.get('background', False),
|
|
367
|
+
'nohup': options.get('nohup', False),
|
|
368
|
+
'envs': envs,
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
# Execute the script
|
|
372
|
+
result = await self.run_command(command, cmd_options)
|
|
373
|
+
|
|
374
|
+
# Cleanup temporary file (if not background execution)
|
|
375
|
+
if not options.get('background', False):
|
|
376
|
+
try:
|
|
377
|
+
await self.run_command(f'rm -f {script_path}')
|
|
378
|
+
except Exception as cleanup_error:
|
|
379
|
+
self.logger.warn('Failed to cleanup script file', {
|
|
380
|
+
'scriptPath': script_path,
|
|
381
|
+
'error': cleanup_error,
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
return result
|
|
385
|
+
|
|
386
|
+
except Exception as error:
|
|
387
|
+
self.logger.error('Script execution failed', str(error))
|
|
388
|
+
raise RuntimeError(f'Failed to execute script: {error}')
|
|
389
|
+
|
|
390
|
+
def _is_binary_file(self, file_path: str) -> bool:
|
|
391
|
+
"""
|
|
392
|
+
Check if a file is binary based on its extension.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
file_path: File path to check
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
True if file is binary, false otherwise
|
|
399
|
+
"""
|
|
400
|
+
binary_extensions = {
|
|
401
|
+
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.ico',
|
|
402
|
+
'.pdf', '.zip', '.tar', '.gz', '.rar', '.7z',
|
|
403
|
+
'.mp3', '.mp4', '.avi', '.mov', '.wmv', '.flv',
|
|
404
|
+
'.exe', '.dll', '.so', '.dylib',
|
|
405
|
+
'.bin', '.dat', '.db', '.sqlite'
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
ext = Path(file_path).suffix.lower()
|
|
409
|
+
return ext in binary_extensions
|
|
410
|
+
|
|
411
|
+
async def copy_file_from_sandbox(self, remote_path: str, local_path: str) -> None:
|
|
412
|
+
"""
|
|
413
|
+
Copy file from sandbox to local filesystem.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
remote_path: File path in sandbox
|
|
417
|
+
local_path: Local destination path
|
|
418
|
+
|
|
419
|
+
Raises:
|
|
420
|
+
RuntimeError: If sandbox is not running or copy fails
|
|
421
|
+
"""
|
|
422
|
+
if not self.sandbox or self.status != SandboxStatus.RUNNING:
|
|
423
|
+
raise RuntimeError('Sandbox is not running. Call create_sandbox() first.')
|
|
424
|
+
|
|
425
|
+
try:
|
|
426
|
+
self.logger.debug('Copying file from sandbox', {
|
|
427
|
+
'remotePath': remote_path,
|
|
428
|
+
'localPath': local_path,
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
# Check if file is binary
|
|
432
|
+
is_binary = self._is_binary_file(remote_path)
|
|
433
|
+
|
|
434
|
+
if is_binary:
|
|
435
|
+
# For binary files, read as bytes
|
|
436
|
+
file_content = await self.sandbox.files.read(remote_path, format='bytes')
|
|
437
|
+
with open(local_path, 'wb') as f:
|
|
438
|
+
f.write(file_content)
|
|
439
|
+
else:
|
|
440
|
+
# For text files, read as string
|
|
441
|
+
file_content = await self.sandbox.files.read(remote_path, format='text')
|
|
442
|
+
with open(local_path, 'w', encoding='utf-8') as f:
|
|
443
|
+
f.write(file_content)
|
|
444
|
+
|
|
445
|
+
self.logger.info(f'File copied from sandbox: {remote_path} -> {local_path}')
|
|
446
|
+
|
|
447
|
+
except Exception as error:
|
|
448
|
+
self.logger.error(f'Failed to copy file: {error}')
|
|
449
|
+
raise
|
|
450
|
+
|
|
451
|
+
async def upload_file_to_sandbox(self, local_path: str, remote_path: str) -> None:
|
|
452
|
+
"""
|
|
453
|
+
Uploads a file to the sandbox.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
local_path: Local file path
|
|
457
|
+
remote_path: Destination path in sandbox
|
|
458
|
+
|
|
459
|
+
Raises:
|
|
460
|
+
RuntimeError: If sandbox is not running or upload fails
|
|
461
|
+
"""
|
|
462
|
+
if not self.sandbox or self.status != SandboxStatus.RUNNING:
|
|
463
|
+
raise RuntimeError('Sandbox is not running. Call create_sandbox() first.')
|
|
464
|
+
|
|
465
|
+
try:
|
|
466
|
+
self.logger.debug('Uploading file to sandbox', {
|
|
467
|
+
'localPath': local_path,
|
|
468
|
+
'remotePath': remote_path,
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
with open(local_path, 'rb') as f:
|
|
472
|
+
file_content = f.read()
|
|
473
|
+
|
|
474
|
+
await self.sandbox.files.write(remote_path, file_content)
|
|
475
|
+
|
|
476
|
+
self.logger.debug('File uploaded successfully', {
|
|
477
|
+
'localPath': local_path,
|
|
478
|
+
'remotePath': remote_path,
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
except Exception as error:
|
|
482
|
+
self.logger.error('Failed to upload file to sandbox', str(error))
|
|
483
|
+
raise RuntimeError(f'Failed to upload file: {error}')
|
|
484
|
+
|
|
485
|
+
async def list_files(self, path: str) -> List[str]:
|
|
486
|
+
"""
|
|
487
|
+
Lists files in a sandbox directory.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
path: Directory path in sandbox
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
List of filenames
|
|
494
|
+
|
|
495
|
+
Raises:
|
|
496
|
+
RuntimeError: If sandbox is not running or listing fails
|
|
497
|
+
"""
|
|
498
|
+
if not self.sandbox or self.status != SandboxStatus.RUNNING:
|
|
499
|
+
raise RuntimeError('Sandbox is not running. Call create_sandbox() first.')
|
|
500
|
+
|
|
501
|
+
try:
|
|
502
|
+
self.logger.debug('Listing files in sandbox', {'path': path})
|
|
503
|
+
|
|
504
|
+
result = await self.run_command(f'ls -la "{path}"')
|
|
505
|
+
|
|
506
|
+
if hasattr(result, 'exit_code') and getattr(result, 'exit_code', 0) != 0:
|
|
507
|
+
raise RuntimeError(f'Failed to list files: {getattr(result, "stderr", "")}')
|
|
508
|
+
|
|
509
|
+
# Parse ls output to extract filenames
|
|
510
|
+
stdout = getattr(result, 'stdout', '') if result else ''
|
|
511
|
+
if stdout:
|
|
512
|
+
lines = stdout.split('\n')
|
|
513
|
+
files = []
|
|
514
|
+
|
|
515
|
+
for line in lines[1:]: # Skip first line (total)
|
|
516
|
+
line = line.strip()
|
|
517
|
+
if line:
|
|
518
|
+
parts = line.split()
|
|
519
|
+
if len(parts) >= 9: # Valid ls -la output
|
|
520
|
+
filename = parts[-1]
|
|
521
|
+
if filename not in ('.', '..'):
|
|
522
|
+
files.append(filename)
|
|
523
|
+
|
|
524
|
+
return files
|
|
525
|
+
|
|
526
|
+
return []
|
|
527
|
+
|
|
528
|
+
except Exception as error:
|
|
529
|
+
self.logger.error('Failed to list files', str(error))
|
|
530
|
+
raise RuntimeError(f'Failed to list files: {error}')
|
|
531
|
+
|
|
532
|
+
def get_status(self) -> SandboxStatus:
|
|
533
|
+
"""Gets the current sandbox status."""
|
|
534
|
+
return self.status
|
|
535
|
+
|
|
536
|
+
def get_sandbox_id(self) -> Optional[str]:
|
|
537
|
+
"""Gets sandbox ID if available."""
|
|
538
|
+
return getattr(self.sandbox, 'sandbox_id', None) if self.sandbox else None
|
|
539
|
+
|
|
540
|
+
def get_sandbox_host(self, port: int) -> Optional[str]:
|
|
541
|
+
"""
|
|
542
|
+
Gets the external host address for a specific port.
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
port: Port number to get host for
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
External host address or None if sandbox not available
|
|
549
|
+
"""
|
|
550
|
+
if not self.sandbox or self.status != SandboxStatus.RUNNING:
|
|
551
|
+
self.logger.warn('Cannot get sandbox host: sandbox not running')
|
|
552
|
+
return None
|
|
553
|
+
|
|
554
|
+
try:
|
|
555
|
+
host = self.sandbox.get_host(port)
|
|
556
|
+
self.logger.debug('Got sandbox host for port', {'port': port, 'host': host})
|
|
557
|
+
return host
|
|
558
|
+
except Exception as error:
|
|
559
|
+
self.logger.error('Failed to get sandbox host', {'port': port, 'error': error})
|
|
560
|
+
return None
|
|
561
|
+
|
|
562
|
+
async def destroy(self) -> None:
|
|
563
|
+
"""
|
|
564
|
+
Destroys the sandbox and cleans up resources.
|
|
565
|
+
|
|
566
|
+
Raises:
|
|
567
|
+
RuntimeError: If sandbox destruction fails
|
|
568
|
+
"""
|
|
569
|
+
if self.sandbox:
|
|
570
|
+
try:
|
|
571
|
+
self.logger.info('Destroying Grasp sandbox', {
|
|
572
|
+
'sandboxId': getattr(self.sandbox, 'sandbox_id', 'unknown'),
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
await self.sandbox.kill()
|
|
576
|
+
self.sandbox = None
|
|
577
|
+
self.status = SandboxStatus.STOPPED
|
|
578
|
+
|
|
579
|
+
self.logger.info('Grasp sandbox destroyed successfully')
|
|
580
|
+
|
|
581
|
+
except Exception as error:
|
|
582
|
+
self.logger.error('Failed to destroy sandbox', str(error))
|
|
583
|
+
raise RuntimeError(f'Failed to destroy sandbox: {error}')
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Utility modules for Grasp SDK Python implementation.
|
|
2
|
+
|
|
3
|
+
This package contains utility functions and classes for configuration,
|
|
4
|
+
logging, authentication, and other common functionality."""
|
|
5
|
+
|
|
6
|
+
# Import all utility modules
|
|
7
|
+
from .config import get_config, get_sandbox_config, get_browser_config, DEFAULT_CONFIG
|
|
8
|
+
from .logger import Logger, init_logger, get_logger, debug, info, warn, error
|
|
9
|
+
from .auth import verify, login, AuthError, KeyVerificationError, AuthManager
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
# Configuration
|
|
13
|
+
'get_config',
|
|
14
|
+
'get_sandbox_config',
|
|
15
|
+
'get_browser_config',
|
|
16
|
+
'DEFAULT_CONFIG',
|
|
17
|
+
# Logging
|
|
18
|
+
'Logger',
|
|
19
|
+
'init_logger',
|
|
20
|
+
'get_logger',
|
|
21
|
+
'debug',
|
|
22
|
+
'info',
|
|
23
|
+
'warn',
|
|
24
|
+
'error',
|
|
25
|
+
# Authentication
|
|
26
|
+
'verify',
|
|
27
|
+
'login',
|
|
28
|
+
'AuthError',
|
|
29
|
+
'KeyVerificationError',
|
|
30
|
+
'AuthManager',
|
|
31
|
+
]
|