grasp-sdk 0.1.8__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 +92 -63
- 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.8.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.8.dist-info → grasp_sdk-0.2.0a1.dist-info}/top_level.txt +1 -0
- grasp_sdk/sandbox/bootstrap-chrome-stable.mjs +0 -69
- grasp_sdk/sandbox/chrome-stable.mjs +0 -424
- grasp_sdk/sandbox/chromium.mjs +0 -395
- grasp_sdk/sandbox/http-proxy.mjs +0 -324
- grasp_sdk-0.1.8.dist-info/RECORD +0 -18
- {grasp_sdk-0.1.8.dist-info → grasp_sdk-0.2.0a1.dist-info}/WHEEL +0 -0
- {grasp_sdk-0.1.8.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:
|
|
@@ -147,7 +177,7 @@ class BrowserService:
|
|
|
147
177
|
# except FileNotFoundError:
|
|
148
178
|
# raise RuntimeError(f'Browser script not found: {script_path}')
|
|
149
179
|
|
|
150
|
-
playwright_script = '/home/user/http-proxy.
|
|
180
|
+
playwright_script = '/home/user/http-proxy.js'
|
|
151
181
|
|
|
152
182
|
# Prepare script options
|
|
153
183
|
from ..models import IScriptOptions
|
|
@@ -156,7 +186,7 @@ class BrowserService:
|
|
|
156
186
|
'background': True,
|
|
157
187
|
'nohup': not self.sandbox_service.is_debug,
|
|
158
188
|
'timeoutMs': 0,
|
|
159
|
-
'preCommand': ''
|
|
189
|
+
'preCommand': ''
|
|
160
190
|
}
|
|
161
191
|
|
|
162
192
|
# Run the Playwright script in background
|
|
@@ -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:
|
|
@@ -226,7 +263,7 @@ class BrowserService:
|
|
|
226
263
|
Raises:
|
|
227
264
|
RuntimeError: If CDP server fails to become ready within timeout
|
|
228
265
|
"""
|
|
229
|
-
delay_ms =
|
|
266
|
+
delay_ms = 50
|
|
230
267
|
max_attempts = self.config['launchTimeout'] // delay_ms
|
|
231
268
|
|
|
232
269
|
for attempt in range(1, max_attempts + 1):
|
|
@@ -240,40 +277,32 @@ class BrowserService:
|
|
|
240
277
|
)
|
|
241
278
|
|
|
242
279
|
# Check if CDP endpoint is responding
|
|
243
|
-
|
|
244
|
-
'
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
connection = CDPConnection(
|
|
270
|
-
ws_url=ws_url,
|
|
271
|
-
http_url=http_url,
|
|
272
|
-
port=9222
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
self.logger.info(f'CDP server is ready (metadata: {metadata})')
|
|
276
|
-
return connection
|
|
280
|
+
async with aiohttp.ClientSession() as session:
|
|
281
|
+
async with session.get(f'https://{host}/json/version') as response:
|
|
282
|
+
if response.status == 200:
|
|
283
|
+
response_text = await response.text()
|
|
284
|
+
if 'Browser' in response_text:
|
|
285
|
+
stdout_content = response_text
|
|
286
|
+
metadata = json.loads(stdout_content)
|
|
287
|
+
|
|
288
|
+
# Update URLs for external access
|
|
289
|
+
ws_url = metadata['webSocketDebuggerUrl'].replace(
|
|
290
|
+
'ws://', 'wss://'
|
|
291
|
+
).replace(
|
|
292
|
+
'127.0.0.1:9222', host
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
http_url = f'https://{host}'
|
|
296
|
+
|
|
297
|
+
connection = CDPConnection(
|
|
298
|
+
ws_url=ws_url,
|
|
299
|
+
http_url=http_url,
|
|
300
|
+
port=9222,
|
|
301
|
+
id=str(self.sandbox_service.id)
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
self.logger.info(f'CDP server is ready (metadata: {metadata})')
|
|
305
|
+
return connection
|
|
277
306
|
|
|
278
307
|
except Exception as error:
|
|
279
308
|
self.logger.debug(
|
|
@@ -347,8 +376,8 @@ class BrowserService:
|
|
|
347
376
|
self._health_check_task = None
|
|
348
377
|
|
|
349
378
|
# Kill the browser process
|
|
350
|
-
if hasattr(self.browser_process, 'kill'):
|
|
351
|
-
|
|
379
|
+
# if hasattr(self.browser_process, 'kill'):
|
|
380
|
+
# await self.browser_process.kill()
|
|
352
381
|
|
|
353
382
|
self.browser_process = None
|
|
354
383
|
self.cdp_connection = None
|
|
@@ -356,8 +385,8 @@ class BrowserService:
|
|
|
356
385
|
self.logger.info('Chromium browser stopped successfully')
|
|
357
386
|
|
|
358
387
|
except Exception as error:
|
|
359
|
-
self.logger.
|
|
360
|
-
raise
|
|
388
|
+
self.logger.debug(f'Error stopping browser: {error}')
|
|
389
|
+
# raise
|
|
361
390
|
|
|
362
391
|
async def cleanup(self) -> None:
|
|
363
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
|