grasp-sdk 0.1.9__py3-none-any.whl → 0.2.0b1__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 +111 -0
- examples/grasp_usage.py +111 -0
- examples/test_async_context.py +64 -0
- examples/test_grasp_classes.py +79 -0
- examples/test_python_script.py +160 -0
- examples/test_removed_methods.py +80 -0
- examples/test_terminal_updates.py +196 -0
- grasp_sdk/__init__.py +129 -239
- grasp_sdk/grasp/__init__.py +24 -0
- grasp_sdk/grasp/browser.py +162 -0
- grasp_sdk/grasp/index.py +127 -0
- grasp_sdk/grasp/server.py +250 -0
- grasp_sdk/grasp/session.py +146 -0
- grasp_sdk/grasp/utils.py +90 -0
- grasp_sdk/models/__init__.py +11 -6
- grasp_sdk/services/__init__.py +8 -1
- grasp_sdk/services/browser.py +68 -28
- grasp_sdk/services/filesystem.py +123 -0
- grasp_sdk/services/sandbox.py +404 -26
- grasp_sdk/services/terminal.py +182 -0
- grasp_sdk/utils/__init__.py +1 -2
- grasp_sdk/utils/auth.py +6 -8
- grasp_sdk/utils/config.py +2 -32
- {grasp_sdk-0.1.9.dist-info → grasp_sdk-0.2.0b1.dist-info}/METADATA +2 -3
- grasp_sdk-0.2.0b1.dist-info/RECORD +33 -0
- {grasp_sdk-0.1.9.dist-info → grasp_sdk-0.2.0b1.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.0b1.dist-info}/WHEEL +0 -0
- {grasp_sdk-0.1.9.dist-info → grasp_sdk-0.2.0b1.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,24 +100,54 @@ 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),
|
|
106
106
|
'HEADLESS': str(self.config['headless']).lower(),
|
|
107
107
|
'NODE_ENV': 'production',
|
|
108
108
|
# 'SANDBOX_ID': self.sandbox_service.id,
|
|
109
|
+
'PLAYWRIGHT_BROWSERS_PATH': '0',
|
|
109
110
|
'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',
|
|
111
|
+
'BROWSER_TYPE': browser_type,
|
|
113
112
|
**self.config['envs']
|
|
114
113
|
}
|
|
115
|
-
await self.sandbox_service.create_sandbox(f'grasp-run-{browser_type}', envs)
|
|
116
|
-
if(self.sandbox_service.sandbox is not None):
|
|
117
|
-
|
|
114
|
+
await self.sandbox_service.create_sandbox(f'grasp-run-{browser_type}-v2', envs)
|
|
115
|
+
# if(self.sandbox_service.sandbox is not None):
|
|
116
|
+
# await self.sandbox_service.sandbox.files.write('/home/user/.sandbox_id', self.id)
|
|
118
117
|
self.logger.info('Grasp sandbox initialized successfully')
|
|
119
118
|
|
|
119
|
+
async def connect(self, sandbox_id: str) -> CDPConnection:
|
|
120
|
+
"""Connect to an existing sandbox.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
sandbox_id: ID of the sandbox to connect to
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
CDP connection information
|
|
127
|
+
"""
|
|
128
|
+
self.logger.info('Initializing Browser service')
|
|
129
|
+
await self.sandbox_service.connect_sandbox(sandbox_id)
|
|
130
|
+
self.logger.info('Browser service initialized successfully')
|
|
131
|
+
|
|
132
|
+
# Read CDP connection info from file
|
|
133
|
+
if not self.sandbox_service.sandbox:
|
|
134
|
+
raise RuntimeError('Sandbox is not available')
|
|
135
|
+
|
|
136
|
+
cdp_content = await self.sandbox_service.sandbox.files.read(
|
|
137
|
+
'/home/user/.grasp-cdp.json'
|
|
138
|
+
)
|
|
139
|
+
cdp_data = json.loads(cdp_content)
|
|
140
|
+
|
|
141
|
+
# Create CDPConnection object
|
|
142
|
+
self.cdp_connection = CDPConnection(
|
|
143
|
+
ws_url=cdp_data['wsUrl'],
|
|
144
|
+
http_url=cdp_data['httpUrl'],
|
|
145
|
+
port=cdp_data['port'],
|
|
146
|
+
id=cdp_data['id']
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return self.cdp_connection
|
|
150
|
+
|
|
120
151
|
async def launch_browser(
|
|
121
152
|
self,
|
|
122
153
|
) -> CDPConnection:
|
|
@@ -174,15 +205,22 @@ class BrowserService:
|
|
|
174
205
|
# Create CDP connection info
|
|
175
206
|
self.cdp_connection = result
|
|
176
207
|
|
|
208
|
+
# Write CDP connection info to file
|
|
209
|
+
if not self.sandbox_service.sandbox:
|
|
210
|
+
raise RuntimeError('Sandbox is not available')
|
|
211
|
+
|
|
212
|
+
await self.sandbox_service.sandbox.files.write(
|
|
213
|
+
'/home/user/.grasp-cdp.json',
|
|
214
|
+
json.dumps(self.cdp_connection.to_dict())
|
|
215
|
+
)
|
|
216
|
+
|
|
177
217
|
self.logger.info(
|
|
178
218
|
f'Chromium browser launched successfully (cdpPort: 9222, wsUrl: {self.cdp_connection.ws_url})'
|
|
179
219
|
)
|
|
180
220
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
self._start_health_check()
|
|
185
|
-
)
|
|
221
|
+
self._health_check_task = asyncio.create_task(
|
|
222
|
+
self._start_health_check()
|
|
223
|
+
)
|
|
186
224
|
|
|
187
225
|
return self.cdp_connection
|
|
188
226
|
|
|
@@ -201,11 +239,11 @@ class BrowserService:
|
|
|
201
239
|
def on_stderr(data: str) -> None:
|
|
202
240
|
self.logger.info(f'Browser stderr: {data}')
|
|
203
241
|
|
|
204
|
-
def on_exit(exit_code: int) -> None:
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
242
|
+
# def on_exit(exit_code: int) -> None:
|
|
243
|
+
# self.logger.info(f'Browser process exited (exitCode: {exit_code})')
|
|
244
|
+
# self.cdp_connection = None
|
|
245
|
+
# self.browser_process = None
|
|
246
|
+
# asyncio.create_task(self.sandbox_service.destroy())
|
|
209
247
|
|
|
210
248
|
def on_error(error: Exception) -> None:
|
|
211
249
|
self.logger.error(f'Browser process error: {error}')
|
|
@@ -214,7 +252,7 @@ class BrowserService:
|
|
|
214
252
|
if hasattr(self.browser_process, 'on'):
|
|
215
253
|
self.browser_process.on('stdout', on_stdout)
|
|
216
254
|
self.browser_process.on('stderr', on_stderr)
|
|
217
|
-
self.browser_process.on('exit', on_exit)
|
|
255
|
+
# self.browser_process.on('exit', on_exit)
|
|
218
256
|
self.browser_process.on('error', on_error)
|
|
219
257
|
|
|
220
258
|
async def _wait_for_cdp_ready(self) -> CDPConnection:
|
|
@@ -252,7 +290,7 @@ class BrowserService:
|
|
|
252
290
|
ws_url = metadata['webSocketDebuggerUrl'].replace(
|
|
253
291
|
'ws://', 'wss://'
|
|
254
292
|
).replace(
|
|
255
|
-
|
|
293
|
+
'127.0.0.1:9222', host
|
|
256
294
|
)
|
|
257
295
|
|
|
258
296
|
http_url = f'https://{host}'
|
|
@@ -260,7 +298,8 @@ class BrowserService:
|
|
|
260
298
|
connection = CDPConnection(
|
|
261
299
|
ws_url=ws_url,
|
|
262
300
|
http_url=http_url,
|
|
263
|
-
port=9222
|
|
301
|
+
port=9222,
|
|
302
|
+
id=str(self.sandbox_service.id)
|
|
264
303
|
)
|
|
265
304
|
|
|
266
305
|
self.logger.info(f'CDP server is ready (metadata: {metadata})')
|
|
@@ -338,8 +377,9 @@ class BrowserService:
|
|
|
338
377
|
self._health_check_task = None
|
|
339
378
|
|
|
340
379
|
# Kill the browser process
|
|
341
|
-
if
|
|
342
|
-
|
|
380
|
+
if self.browser_process is not None:
|
|
381
|
+
if hasattr(self.browser_process, 'kill'):
|
|
382
|
+
await self.browser_process.kill()
|
|
343
383
|
|
|
344
384
|
self.browser_process = None
|
|
345
385
|
self.cdp_connection = None
|
|
@@ -347,8 +387,8 @@ class BrowserService:
|
|
|
347
387
|
self.logger.info('Chromium browser stopped successfully')
|
|
348
388
|
|
|
349
389
|
except Exception as error:
|
|
350
|
-
self.logger.
|
|
351
|
-
raise
|
|
390
|
+
self.logger.debug(f'Error stopping browser: {error}')
|
|
391
|
+
# raise
|
|
352
392
|
|
|
353
393
|
async def cleanup(self) -> None:
|
|
354
394
|
"""Cleanup all resources including Grasp sandbox.
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
|
95
|
+
|
|
96
|
+
async def sync_downloads_directory(
|
|
97
|
+
self,
|
|
98
|
+
local_path: str,
|
|
99
|
+
remote_path: Optional[str] = None
|
|
100
|
+
) -> str:
|
|
101
|
+
"""Synchronize downloads directory from sandbox to local filesystem.
|
|
102
|
+
|
|
103
|
+
This method is experimental and may change or be removed in future versions.
|
|
104
|
+
Use with caution in production environments.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
local_path: Local directory path to sync to
|
|
108
|
+
remote_path: Remote directory path to sync from (defaults to local_path)
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Local sync directory path
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
RuntimeError: If sandbox is not running or sync fails
|
|
115
|
+
"""
|
|
116
|
+
if remote_path is None:
|
|
117
|
+
remote_path = local_path
|
|
118
|
+
|
|
119
|
+
sync_result = await self.sandbox.sync_downloads_directory(
|
|
120
|
+
dist=local_path,
|
|
121
|
+
src=remote_path
|
|
122
|
+
)
|
|
123
|
+
return sync_result
|