grasp-sdk 0.1.9__py3-none-any.whl → 0.2.0a1__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.
- examples/example_async_context.py +122 -0
- examples/example_binary_file_support.py +127 -0
- examples/example_grasp_usage.py +82 -0
- examples/example_readfile_usage.py +136 -0
- examples/grasp_terminal.py +110 -0
- examples/grasp_usage.py +111 -0
- examples/test_async_context.py +64 -0
- examples/test_get_replay_screenshots.py +90 -0
- examples/test_grasp_classes.py +80 -0
- examples/test_python_script.py +160 -0
- examples/test_removed_methods.py +80 -0
- examples/test_shutdown_deprecation.py +62 -0
- examples/test_terminal_updates.py +196 -0
- grasp_sdk/__init__.py +131 -239
- grasp_sdk/grasp/__init__.py +26 -0
- grasp_sdk/grasp/browser.py +69 -0
- grasp_sdk/grasp/index.py +122 -0
- grasp_sdk/grasp/server.py +250 -0
- grasp_sdk/grasp/session.py +108 -0
- grasp_sdk/grasp/terminal.py +32 -0
- grasp_sdk/grasp/utils.py +90 -0
- grasp_sdk/models/__init__.py +1 -1
- grasp_sdk/services/__init__.py +8 -1
- grasp_sdk/services/browser.py +66 -28
- grasp_sdk/services/filesystem.py +94 -0
- grasp_sdk/services/sandbox.py +391 -20
- grasp_sdk/services/terminal.py +177 -0
- grasp_sdk/utils/auth.py +6 -8
- {grasp_sdk-0.1.9.dist-info → grasp_sdk-0.2.0a1.dist-info}/METADATA +2 -3
- grasp_sdk-0.2.0a1.dist-info/RECORD +36 -0
- {grasp_sdk-0.1.9.dist-info → grasp_sdk-0.2.0a1.dist-info}/top_level.txt +1 -0
- grasp_sdk-0.1.9.dist-info/RECORD +0 -14
- {grasp_sdk-0.1.9.dist-info → grasp_sdk-0.2.0a1.dist-info}/WHEEL +0 -0
- {grasp_sdk-0.1.9.dist-info → grasp_sdk-0.2.0a1.dist-info}/entry_points.txt +0 -0
grasp_sdk/services/browser.py
CHANGED
|
@@ -25,20 +25,20 @@ class CDPConnection:
|
|
|
25
25
|
ws_url: str,
|
|
26
26
|
http_url: str,
|
|
27
27
|
port: int,
|
|
28
|
-
|
|
28
|
+
id: str,
|
|
29
29
|
):
|
|
30
30
|
self.ws_url = ws_url
|
|
31
31
|
self.http_url = http_url
|
|
32
32
|
self.port = port
|
|
33
|
-
self.
|
|
33
|
+
self.id = id
|
|
34
34
|
|
|
35
35
|
def to_dict(self) -> Dict[str, Any]:
|
|
36
36
|
"""Convert to dictionary representation."""
|
|
37
37
|
return {
|
|
38
|
+
'id': self.id,
|
|
38
39
|
'wsUrl': self.ws_url,
|
|
39
40
|
'httpUrl': self.http_url,
|
|
40
|
-
'port': self.port
|
|
41
|
-
'pid': self.pid
|
|
41
|
+
'port': self.port
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
|
|
@@ -77,6 +77,7 @@ class BrowserService:
|
|
|
77
77
|
self.cdp_connection: Optional[CDPConnection] = None
|
|
78
78
|
self.browser_process: Optional[CommandEventEmitter] = None
|
|
79
79
|
self._health_check_task: Optional[asyncio.Task] = None
|
|
80
|
+
self.config['envs']['APIKEY'] = sandbox_config['key']
|
|
80
81
|
|
|
81
82
|
def _get_default_logger(self):
|
|
82
83
|
"""Gets or creates a default logger instance."""
|
|
@@ -99,7 +100,6 @@ class BrowserService:
|
|
|
99
100
|
"""
|
|
100
101
|
self.logger.info('Initializing Browser service')
|
|
101
102
|
envs = {
|
|
102
|
-
'CDP_PORT': '9222',
|
|
103
103
|
'BROWSER_ARGS': json.dumps(self.config['args']),
|
|
104
104
|
'LAUNCH_TIMEOUT': str(self.config['launchTimeout']),
|
|
105
105
|
'SANDBOX_TIMEOUT': str(self.sandbox_service.timeout),
|
|
@@ -107,16 +107,46 @@ class BrowserService:
|
|
|
107
107
|
'NODE_ENV': 'production',
|
|
108
108
|
# 'SANDBOX_ID': self.sandbox_service.id,
|
|
109
109
|
'WORKSPACE': self.sandbox_service.workspace,
|
|
110
|
-
'
|
|
111
|
-
'BS_INGESTING_HOST': 's1363065.eu-nbg-2.betterstackdata.com',
|
|
112
|
-
'SENTRY_DSN': 'https://21fa729ceb72d7f0adef06b4f786c067@o4509574910509056.ingest.us.sentry.io/4509574913720320',
|
|
110
|
+
'BROWSER_TYPE': browser_type,
|
|
113
111
|
**self.config['envs']
|
|
114
112
|
}
|
|
115
|
-
await self.sandbox_service.create_sandbox(f'grasp-run-{browser_type}', envs)
|
|
116
|
-
if(self.sandbox_service.sandbox is not None):
|
|
117
|
-
|
|
113
|
+
await self.sandbox_service.create_sandbox(f'grasp-run-{browser_type}-v2', envs)
|
|
114
|
+
# if(self.sandbox_service.sandbox is not None):
|
|
115
|
+
# await self.sandbox_service.sandbox.files.write('/home/user/.sandbox_id', self.id)
|
|
118
116
|
self.logger.info('Grasp sandbox initialized successfully')
|
|
119
117
|
|
|
118
|
+
async def connect(self, sandbox_id: str) -> CDPConnection:
|
|
119
|
+
"""Connect to an existing sandbox.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
sandbox_id: ID of the sandbox to connect to
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
CDP connection information
|
|
126
|
+
"""
|
|
127
|
+
self.logger.info('Initializing Browser service')
|
|
128
|
+
await self.sandbox_service.connect_sandbox(sandbox_id)
|
|
129
|
+
self.logger.info('Browser service initialized successfully')
|
|
130
|
+
|
|
131
|
+
# Read CDP connection info from file
|
|
132
|
+
if not self.sandbox_service.sandbox:
|
|
133
|
+
raise RuntimeError('Sandbox is not available')
|
|
134
|
+
|
|
135
|
+
cdp_content = await self.sandbox_service.sandbox.files.read(
|
|
136
|
+
'/home/user/.grasp-cdp.json'
|
|
137
|
+
)
|
|
138
|
+
cdp_data = json.loads(cdp_content)
|
|
139
|
+
|
|
140
|
+
# Create CDPConnection object
|
|
141
|
+
self.cdp_connection = CDPConnection(
|
|
142
|
+
ws_url=cdp_data['wsUrl'],
|
|
143
|
+
http_url=cdp_data['httpUrl'],
|
|
144
|
+
port=cdp_data['port'],
|
|
145
|
+
id=cdp_data['id']
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return self.cdp_connection
|
|
149
|
+
|
|
120
150
|
async def launch_browser(
|
|
121
151
|
self,
|
|
122
152
|
) -> CDPConnection:
|
|
@@ -174,15 +204,22 @@ class BrowserService:
|
|
|
174
204
|
# Create CDP connection info
|
|
175
205
|
self.cdp_connection = result
|
|
176
206
|
|
|
207
|
+
# Write CDP connection info to file
|
|
208
|
+
if not self.sandbox_service.sandbox:
|
|
209
|
+
raise RuntimeError('Sandbox is not available')
|
|
210
|
+
|
|
211
|
+
await self.sandbox_service.sandbox.files.write(
|
|
212
|
+
'/home/user/.grasp-cdp.json',
|
|
213
|
+
json.dumps(self.cdp_connection.to_dict())
|
|
214
|
+
)
|
|
215
|
+
|
|
177
216
|
self.logger.info(
|
|
178
217
|
f'Chromium browser launched successfully (cdpPort: 9222, wsUrl: {self.cdp_connection.ws_url})'
|
|
179
218
|
)
|
|
180
219
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
self._start_health_check()
|
|
185
|
-
)
|
|
220
|
+
self._health_check_task = asyncio.create_task(
|
|
221
|
+
self._start_health_check()
|
|
222
|
+
)
|
|
186
223
|
|
|
187
224
|
return self.cdp_connection
|
|
188
225
|
|
|
@@ -201,11 +238,11 @@ class BrowserService:
|
|
|
201
238
|
def on_stderr(data: str) -> None:
|
|
202
239
|
self.logger.info(f'Browser stderr: {data}')
|
|
203
240
|
|
|
204
|
-
def on_exit(exit_code: int) -> None:
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
241
|
+
# def on_exit(exit_code: int) -> None:
|
|
242
|
+
# self.logger.info(f'Browser process exited (exitCode: {exit_code})')
|
|
243
|
+
# self.cdp_connection = None
|
|
244
|
+
# self.browser_process = None
|
|
245
|
+
# asyncio.create_task(self.sandbox_service.destroy())
|
|
209
246
|
|
|
210
247
|
def on_error(error: Exception) -> None:
|
|
211
248
|
self.logger.error(f'Browser process error: {error}')
|
|
@@ -214,7 +251,7 @@ class BrowserService:
|
|
|
214
251
|
if hasattr(self.browser_process, 'on'):
|
|
215
252
|
self.browser_process.on('stdout', on_stdout)
|
|
216
253
|
self.browser_process.on('stderr', on_stderr)
|
|
217
|
-
self.browser_process.on('exit', on_exit)
|
|
254
|
+
# self.browser_process.on('exit', on_exit)
|
|
218
255
|
self.browser_process.on('error', on_error)
|
|
219
256
|
|
|
220
257
|
async def _wait_for_cdp_ready(self) -> CDPConnection:
|
|
@@ -252,7 +289,7 @@ class BrowserService:
|
|
|
252
289
|
ws_url = metadata['webSocketDebuggerUrl'].replace(
|
|
253
290
|
'ws://', 'wss://'
|
|
254
291
|
).replace(
|
|
255
|
-
|
|
292
|
+
'127.0.0.1:9222', host
|
|
256
293
|
)
|
|
257
294
|
|
|
258
295
|
http_url = f'https://{host}'
|
|
@@ -260,7 +297,8 @@ class BrowserService:
|
|
|
260
297
|
connection = CDPConnection(
|
|
261
298
|
ws_url=ws_url,
|
|
262
299
|
http_url=http_url,
|
|
263
|
-
port=9222
|
|
300
|
+
port=9222,
|
|
301
|
+
id=str(self.sandbox_service.id)
|
|
264
302
|
)
|
|
265
303
|
|
|
266
304
|
self.logger.info(f'CDP server is ready (metadata: {metadata})')
|
|
@@ -338,8 +376,8 @@ class BrowserService:
|
|
|
338
376
|
self._health_check_task = None
|
|
339
377
|
|
|
340
378
|
# Kill the browser process
|
|
341
|
-
if hasattr(self.browser_process, 'kill'):
|
|
342
|
-
|
|
379
|
+
# if hasattr(self.browser_process, 'kill'):
|
|
380
|
+
# await self.browser_process.kill()
|
|
343
381
|
|
|
344
382
|
self.browser_process = None
|
|
345
383
|
self.cdp_connection = None
|
|
@@ -347,8 +385,8 @@ class BrowserService:
|
|
|
347
385
|
self.logger.info('Chromium browser stopped successfully')
|
|
348
386
|
|
|
349
387
|
except Exception as error:
|
|
350
|
-
self.logger.
|
|
351
|
-
raise
|
|
388
|
+
self.logger.debug(f'Error stopping browser: {error}')
|
|
389
|
+
# raise
|
|
352
390
|
|
|
353
391
|
async def cleanup(self) -> None:
|
|
354
392
|
"""Cleanup all resources including Grasp sandbox.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Filesystem service for managing file operations in sandbox.
|
|
4
|
+
|
|
5
|
+
This module provides Python implementation of the filesystem service,
|
|
6
|
+
equivalent to the TypeScript version in src/services/filesystem.service.ts
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from typing import Optional, Dict, Union
|
|
11
|
+
from ..utils.logger import get_logger, Logger
|
|
12
|
+
from .sandbox import SandboxService
|
|
13
|
+
from .browser import CDPConnection
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FileSystemService:
|
|
17
|
+
"""
|
|
18
|
+
Filesystem service for managing file operations in sandbox.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, sandbox: SandboxService, connection: CDPConnection):
|
|
22
|
+
"""Initialize filesystem service.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
sandbox: The sandbox service instance
|
|
26
|
+
connection: The CDP connection for browser integration
|
|
27
|
+
"""
|
|
28
|
+
self.sandbox = sandbox
|
|
29
|
+
self.connection = connection
|
|
30
|
+
self.logger = self._get_default_logger()
|
|
31
|
+
|
|
32
|
+
def _get_default_logger(self) -> Logger:
|
|
33
|
+
"""Gets or creates a default logger instance.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Logger instance
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
return get_logger().child('FileSystemService')
|
|
40
|
+
except Exception:
|
|
41
|
+
# If logger is not initialized, create a default one
|
|
42
|
+
from ..utils.logger import Logger
|
|
43
|
+
default_logger = Logger({
|
|
44
|
+
'level': 'debug' if self.sandbox.is_debug else 'info',
|
|
45
|
+
'console': True
|
|
46
|
+
})
|
|
47
|
+
return default_logger.child('FileSystemService')
|
|
48
|
+
|
|
49
|
+
async def upload_file(self, local_path: str, remote_path: str) -> None:
|
|
50
|
+
"""Upload file from local to sandbox.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
local_path: Local file path
|
|
54
|
+
remote_path: Remote file path in sandbox
|
|
55
|
+
"""
|
|
56
|
+
upload_result = await self.sandbox.upload_file_to_sandbox(local_path, remote_path)
|
|
57
|
+
return upload_result
|
|
58
|
+
|
|
59
|
+
async def download_file(self, remote_path: str, local_path: str) -> None:
|
|
60
|
+
"""Download file from sandbox to local.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
remote_path: Remote file path in sandbox
|
|
64
|
+
local_path: Local file path
|
|
65
|
+
"""
|
|
66
|
+
download_result = await self.sandbox.copy_file_from_sandbox(remote_path, local_path)
|
|
67
|
+
return download_result
|
|
68
|
+
|
|
69
|
+
async def write_file(self, remote_path: str, content: Union[str, bytes]) -> None:
|
|
70
|
+
"""Write content to file in sandbox.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
remote_path: Remote file path in sandbox
|
|
74
|
+
content: File content to write (string or bytes)
|
|
75
|
+
"""
|
|
76
|
+
write_result = await self.sandbox.write_file_to_sandbox(remote_path, content)
|
|
77
|
+
return write_result
|
|
78
|
+
|
|
79
|
+
async def read_file(
|
|
80
|
+
self,
|
|
81
|
+
remote_path: str,
|
|
82
|
+
options: Optional[Dict[str, str]] = None
|
|
83
|
+
) -> Union[str, bytes]:
|
|
84
|
+
"""Read content from file in sandbox.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
remote_path: Remote file path in sandbox
|
|
88
|
+
options: Dictionary with 'encoding' key ('utf8', 'base64', or 'binary')
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
File content as string or bytes depending on encoding
|
|
92
|
+
"""
|
|
93
|
+
read_result = await self.sandbox.read_file_from_sandbox(remote_path, options)
|
|
94
|
+
return read_result
|