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/__init__.py
CHANGED
|
@@ -1,291 +1,181 @@
|
|
|
1
|
-
"""Grasp
|
|
1
|
+
"""Grasp Server Python SDK
|
|
2
2
|
|
|
3
|
-
A Python SDK for
|
|
3
|
+
A Python SDK for Grasp platform providing secure command execution
|
|
4
4
|
and browser automation in isolated cloud environments.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import signal
|
|
9
|
-
import
|
|
10
|
-
|
|
9
|
+
from typing import Dict, Optional, Any
|
|
10
|
+
import warnings
|
|
11
11
|
|
|
12
12
|
# Import utilities
|
|
13
|
-
from .utils.logger import
|
|
14
|
-
|
|
15
|
-
from
|
|
16
|
-
from .
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
IScriptOptions,
|
|
24
|
-
SandboxStatus,
|
|
13
|
+
from .utils.logger import get_logger
|
|
14
|
+
|
|
15
|
+
# Import all classes from grasp module
|
|
16
|
+
from .grasp import (
|
|
17
|
+
GraspServer,
|
|
18
|
+
GraspBrowser,
|
|
19
|
+
GraspSession,
|
|
20
|
+
Grasp,
|
|
21
|
+
_servers,
|
|
22
|
+
shutdown,
|
|
25
23
|
)
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
# Import CDPConnection for type annotation
|
|
26
|
+
from .services.browser import CDPConnection
|
|
27
|
+
|
|
28
|
+
__version__ = "0.2.0b1"
|
|
28
29
|
__author__ = "Grasp Team"
|
|
29
30
|
__email__ = "team@grasp.dev"
|
|
30
31
|
|
|
32
|
+
# Global server registry (re-exported from grasp module)
|
|
33
|
+
_servers: Dict[str, GraspServer] = _servers
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
"""Main Grasp E2B class for browser automation."""
|
|
34
|
-
|
|
35
|
-
def __init__(self, sandbox_config: Optional[Dict[str, Any]] = None):
|
|
36
|
-
"""Initialize GraspServer with configuration.
|
|
37
|
-
|
|
38
|
-
Args:
|
|
39
|
-
sandbox_config: Optional sandbox configuration overrides
|
|
40
|
-
"""
|
|
41
|
-
if sandbox_config is None:
|
|
42
|
-
sandbox_config = {}
|
|
43
|
-
|
|
44
|
-
# Extract browser-specific options
|
|
45
|
-
browser_type = sandbox_config.pop('type', 'chromium')
|
|
46
|
-
headless = sandbox_config.pop('headless', True)
|
|
47
|
-
adblock = sandbox_config.pop('adblock', False)
|
|
48
|
-
logLevel = sandbox_config.pop('logLevel', '')
|
|
49
|
-
keepAliveMS = sandbox_config.pop('keepAliveMS', 0)
|
|
50
|
-
|
|
51
|
-
# Set default log level
|
|
52
|
-
if not logLevel:
|
|
53
|
-
logLevel = 'debug' if sandbox_config.get('debug', False) else 'info'
|
|
54
|
-
|
|
55
|
-
self.__browser_type = browser_type
|
|
56
|
-
|
|
57
|
-
# Create browser task
|
|
58
|
-
self.__browser_config = {
|
|
59
|
-
'headless': headless,
|
|
60
|
-
'envs': {
|
|
61
|
-
'ADBLOCK': 'true' if adblock else 'false',
|
|
62
|
-
'KEEP_ALIVE_MS': str(keepAliveMS),
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
config = get_config()
|
|
67
|
-
config['sandbox'].update(sandbox_config)
|
|
68
|
-
self.config = config
|
|
69
|
-
|
|
70
|
-
logger = config['logger']
|
|
71
|
-
logger['level'] = logLevel
|
|
72
|
-
|
|
73
|
-
# Initialize logger first
|
|
74
|
-
init_logger(logger)
|
|
75
|
-
self.logger = get_logger().child('GraspE2B')
|
|
76
|
-
|
|
77
|
-
self.browser_service: Optional[BrowserService] = None
|
|
78
|
-
|
|
79
|
-
self.logger.info('GraspE2B initialized')
|
|
80
|
-
|
|
81
|
-
async def __aenter__(self):
|
|
82
|
-
connection = await self.create_browser_task()
|
|
83
|
-
|
|
84
|
-
# Register server
|
|
85
|
-
# if connection['id']:
|
|
86
|
-
# _servers[connection['id']] = self
|
|
87
|
-
|
|
88
|
-
return connection
|
|
89
|
-
|
|
90
|
-
async def __aexit__(self, exc_type, exc, tb):
|
|
91
|
-
if self.browser_service and self.browser_service.id:
|
|
92
|
-
service_id = self.browser_service.id
|
|
93
|
-
self.logger.info(f'Closing browser service {service_id}')
|
|
94
|
-
await _servers[service_id].cleanup()
|
|
95
|
-
del _servers[service_id]
|
|
96
|
-
|
|
97
|
-
@property
|
|
98
|
-
def sandbox(self) -> Optional[SandboxService]:
|
|
99
|
-
"""Get the underlying sandbox service.
|
|
100
|
-
|
|
101
|
-
Returns:
|
|
102
|
-
SandboxService instance or None
|
|
103
|
-
"""
|
|
104
|
-
return self.browser_service.get_sandbox() if self.browser_service else None
|
|
105
|
-
|
|
106
|
-
def get_status(self) -> Optional[SandboxStatus]:
|
|
107
|
-
"""Get current sandbox status.
|
|
108
|
-
|
|
109
|
-
Returns:
|
|
110
|
-
Sandbox status or None
|
|
111
|
-
"""
|
|
112
|
-
return self.sandbox.get_status() if self.sandbox else None
|
|
113
|
-
|
|
114
|
-
def get_sandbox_id(self) -> Optional[str]:
|
|
115
|
-
"""Get sandbox ID.
|
|
116
|
-
|
|
117
|
-
Returns:
|
|
118
|
-
Sandbox ID or None
|
|
119
|
-
"""
|
|
120
|
-
return self.sandbox.get_sandbox_id() if self.sandbox else None
|
|
121
|
-
|
|
122
|
-
async def create_browser_task(
|
|
123
|
-
self,
|
|
124
|
-
) -> Dict[str, Any]:
|
|
125
|
-
"""Create and launch a browser task.
|
|
126
|
-
|
|
127
|
-
Args:
|
|
128
|
-
browser_type: Type of browser to launch
|
|
129
|
-
config: Browser configuration overrides
|
|
130
|
-
|
|
131
|
-
Returns:
|
|
132
|
-
Dictionary containing browser connection info
|
|
133
|
-
|
|
134
|
-
Raises:
|
|
135
|
-
RuntimeError: If browser service is already initialized
|
|
136
|
-
"""
|
|
137
|
-
if self.browser_service:
|
|
138
|
-
raise RuntimeError('Browser service can only be initialized once')
|
|
139
|
-
|
|
140
|
-
config = self.__browser_config
|
|
141
|
-
browser_type = self.__browser_type
|
|
142
|
-
|
|
143
|
-
if config is None:
|
|
144
|
-
config = {}
|
|
145
|
-
|
|
146
|
-
# Create base browser config
|
|
147
|
-
browser_config: IBrowserConfig = {
|
|
148
|
-
'headless': True,
|
|
149
|
-
'launchTimeout': 30000,
|
|
150
|
-
'args': [
|
|
151
|
-
'--disable-web-security',
|
|
152
|
-
'--disable-features=VizDisplayCompositor',
|
|
153
|
-
],
|
|
154
|
-
'envs': {},
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
# Apply user config overrides with type safety
|
|
158
|
-
if 'headless' in config:
|
|
159
|
-
browser_config['headless'] = config['headless']
|
|
160
|
-
if 'launchTimeout' in config:
|
|
161
|
-
browser_config['launchTimeout'] = config['launchTimeout']
|
|
162
|
-
if 'args' in config:
|
|
163
|
-
browser_config['args'] = config['args']
|
|
164
|
-
if 'envs' in config:
|
|
165
|
-
browser_config['envs'] = config['envs']
|
|
166
|
-
|
|
167
|
-
self.browser_service = BrowserService(
|
|
168
|
-
self.config['sandbox'],
|
|
169
|
-
browser_config
|
|
170
|
-
)
|
|
171
|
-
await self.browser_service.initialize(browser_type)
|
|
172
|
-
|
|
173
|
-
# Register server
|
|
174
|
-
_servers[str(self.browser_service.id)] = self
|
|
175
|
-
self.logger.info("🚀 Browser service initialized", {
|
|
176
|
-
'id': self.browser_service.id,
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
self.logger.info('🌐 Launching Chromium browser with CDP...')
|
|
180
|
-
cdp_connection = await self.browser_service.launch_browser()
|
|
181
|
-
|
|
182
|
-
self.logger.info('✅ Browser launched successfully!')
|
|
183
|
-
self.logger.debug(
|
|
184
|
-
f'CDP Connection Info (wsUrl: {cdp_connection.ws_url}, httpUrl: {cdp_connection.http_url})'
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
return {
|
|
188
|
-
'id': self.browser_service.id,
|
|
189
|
-
'ws_url': cdp_connection.ws_url,
|
|
190
|
-
'http_url': cdp_connection.http_url
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async def cleanup(self) -> None:
|
|
194
|
-
"""Cleanup resources.
|
|
195
|
-
|
|
196
|
-
Returns:
|
|
197
|
-
Promise that resolves when cleanup is complete
|
|
198
|
-
"""
|
|
199
|
-
self.logger.info('Starting cleanup process')
|
|
200
|
-
|
|
201
|
-
try:
|
|
202
|
-
# Cleanup browser service
|
|
203
|
-
if self.browser_service:
|
|
204
|
-
await self.browser_service.cleanup()
|
|
205
|
-
|
|
206
|
-
self.logger.info('Cleanup completed successfully')
|
|
207
|
-
except Exception as error:
|
|
208
|
-
self.logger.error(f'Cleanup failed: {error}')
|
|
209
|
-
raise
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
# Global server registry
|
|
213
|
-
_servers: Dict[str, GraspServer] = {}
|
|
214
|
-
|
|
215
|
-
|
|
35
|
+
# Deprecated functions for backward compatibility
|
|
216
36
|
async def launch_browser(
|
|
217
37
|
options: Optional[Dict[str, Any]] = None
|
|
218
|
-
) ->
|
|
38
|
+
) -> CDPConnection:
|
|
219
39
|
"""Launch a browser instance.
|
|
220
40
|
|
|
41
|
+
.. deprecated::
|
|
42
|
+
Use grasp.launch() instead. This method will be removed in a future version.
|
|
43
|
+
|
|
221
44
|
Args:
|
|
222
45
|
options: Launch options including type, headless, adblock settings
|
|
223
46
|
|
|
224
47
|
Returns:
|
|
225
48
|
Dictionary containing connection information
|
|
226
|
-
"""
|
|
49
|
+
"""
|
|
50
|
+
import warnings
|
|
51
|
+
warnings.warn(
|
|
52
|
+
"launch_browser() is deprecated. Use grasp.launch() instead.",
|
|
53
|
+
DeprecationWarning,
|
|
54
|
+
stacklevel=2
|
|
55
|
+
)
|
|
227
56
|
# Create server instance
|
|
228
57
|
server = GraspServer(options)
|
|
229
|
-
|
|
58
|
+
|
|
230
59
|
connection = await server.create_browser_task()
|
|
231
|
-
|
|
232
|
-
# Register server
|
|
233
|
-
if connection['id']:
|
|
234
|
-
_servers[connection['id']] = server
|
|
235
|
-
|
|
60
|
+
|
|
236
61
|
return connection
|
|
237
62
|
|
|
238
63
|
|
|
239
|
-
async def _graceful_shutdown(
|
|
240
|
-
"""
|
|
64
|
+
async def _graceful_shutdown(reason: str = 'SIGTERM') -> None:
|
|
65
|
+
"""Gracefully shutdown all servers.
|
|
241
66
|
|
|
242
67
|
Args:
|
|
243
|
-
|
|
68
|
+
reason: Reason for shutdown
|
|
244
69
|
"""
|
|
245
|
-
|
|
70
|
+
import os
|
|
71
|
+
logger = get_logger().child('shutdown')
|
|
72
|
+
logger.info(f'Graceful shutdown initiated: {reason}')
|
|
73
|
+
|
|
74
|
+
if not _servers:
|
|
75
|
+
logger.info('No active servers to shutdown')
|
|
76
|
+
# 强制终止进程
|
|
77
|
+
os._exit(0)
|
|
78
|
+
return
|
|
246
79
|
|
|
247
|
-
|
|
248
|
-
for server_id in list(_servers.keys()):
|
|
249
|
-
await _servers[server_id].cleanup()
|
|
250
|
-
del _servers[server_id]
|
|
80
|
+
logger.info(f'Shutting down {len(_servers)} active servers...')
|
|
251
81
|
|
|
252
|
-
|
|
253
|
-
|
|
82
|
+
# Create cleanup tasks for all servers
|
|
83
|
+
cleanup_tasks = []
|
|
84
|
+
for server_id, server in _servers.items():
|
|
85
|
+
logger.info(f'Scheduling cleanup for server {server_id}')
|
|
86
|
+
cleanup_tasks.append(server.cleanup())
|
|
87
|
+
|
|
88
|
+
# Wait for all cleanups to complete
|
|
89
|
+
try:
|
|
90
|
+
await asyncio.gather(*cleanup_tasks, return_exceptions=True)
|
|
91
|
+
logger.info('All servers cleaned up successfully')
|
|
92
|
+
except Exception as error:
|
|
93
|
+
logger.error(f'Error during cleanup: {error}')
|
|
94
|
+
|
|
95
|
+
# Clear the servers registry
|
|
96
|
+
_servers.clear()
|
|
97
|
+
logger.info('Graceful shutdown completed')
|
|
98
|
+
|
|
99
|
+
# 强制终止进程,确保不会被其他异步任务阻塞
|
|
100
|
+
os._exit(0)
|
|
254
101
|
|
|
255
102
|
|
|
256
103
|
def _setup_signal_handlers() -> None:
|
|
257
104
|
"""Setup signal handlers for graceful shutdown."""
|
|
258
|
-
def signal_handler(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
105
|
+
def signal_handler(signame):
|
|
106
|
+
logger = get_logger().child('signal')
|
|
107
|
+
logger.info(f'Received signal {signame}, initiating graceful shutdown...')
|
|
108
|
+
|
|
109
|
+
# 创建关闭任务
|
|
110
|
+
asyncio.create_task(_graceful_shutdown(signame))
|
|
111
|
+
|
|
112
|
+
# 尝试使用 asyncio 的信号处理器(更适合异步环境)
|
|
113
|
+
try:
|
|
114
|
+
loop = asyncio.get_running_loop()
|
|
115
|
+
for sig in [signal.SIGTERM, signal.SIGINT]:
|
|
116
|
+
loop.add_signal_handler(sig, lambda s=sig: signal_handler(signal.Signals(s).name))
|
|
117
|
+
except RuntimeError:
|
|
118
|
+
# 如果没有运行的事件循环,使用传统方式
|
|
119
|
+
def fallback_handler(signum, frame):
|
|
120
|
+
signal_name = signal.Signals(signum).name
|
|
121
|
+
logger = get_logger().child('signal')
|
|
122
|
+
logger.info(f'Received {signal_name}, but no event loop is running')
|
|
123
|
+
import os
|
|
124
|
+
os._exit(1)
|
|
125
|
+
|
|
126
|
+
signal.signal(signal.SIGTERM, fallback_handler)
|
|
127
|
+
signal.signal(signal.SIGINT, fallback_handler)
|
|
265
128
|
|
|
266
129
|
|
|
267
|
-
|
|
130
|
+
async def shutdown(connection: Optional[str] = None) -> None:
|
|
131
|
+
"""Shutdown Grasp servers (deprecated).
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
connection: Optional connection ID to shutdown specific server
|
|
135
|
+
|
|
136
|
+
Deprecated:
|
|
137
|
+
This function is deprecated. Use server.cleanup() or context managers instead.
|
|
138
|
+
"""
|
|
139
|
+
warnings.warn(
|
|
140
|
+
"shutdown() is deprecated. Use server.cleanup() or context managers instead.",
|
|
141
|
+
DeprecationWarning,
|
|
142
|
+
stacklevel=2
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
logger = get_logger().child('shutdown')
|
|
146
|
+
|
|
147
|
+
if connection:
|
|
148
|
+
# Shutdown specific connection
|
|
149
|
+
if connection in _servers:
|
|
150
|
+
logger.info(f'Shutting down server {connection}')
|
|
151
|
+
await _servers[connection].cleanup()
|
|
152
|
+
else:
|
|
153
|
+
logger.warn(f'Server {connection} not found')
|
|
154
|
+
else:
|
|
155
|
+
# Shutdown all servers
|
|
156
|
+
await _graceful_shutdown('USER_SHUTDOWN')
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# Setup signal handlers when module is imported
|
|
268
160
|
_setup_signal_handlers()
|
|
269
161
|
|
|
270
|
-
|
|
271
162
|
# Export all public APIs
|
|
272
163
|
__all__ = [
|
|
273
164
|
'GraspServer',
|
|
165
|
+
'Grasp',
|
|
166
|
+
'GraspSession',
|
|
167
|
+
'GraspBrowser',
|
|
168
|
+
'GraspTerminal',
|
|
274
169
|
'launch_browser',
|
|
275
|
-
'
|
|
276
|
-
'IBrowserConfig',
|
|
277
|
-
'ICommandOptions',
|
|
278
|
-
'IScriptOptions',
|
|
279
|
-
'SandboxStatus',
|
|
280
|
-
'get_config',
|
|
281
|
-
'init_logger',
|
|
282
|
-
'get_logger',
|
|
283
|
-
'BrowserService',
|
|
284
|
-
'SandboxService',
|
|
170
|
+
'shutdown',
|
|
285
171
|
]
|
|
286
172
|
|
|
287
|
-
|
|
288
173
|
# Default export equivalent
|
|
289
174
|
default = {
|
|
290
175
|
'GraspServer': GraspServer,
|
|
176
|
+
'Grasp': Grasp,
|
|
177
|
+
'GraspSession': GraspSession,
|
|
178
|
+
'GraspBrowser': GraspBrowser,
|
|
179
|
+
'launch_browser': launch_browser,
|
|
180
|
+
'shutdown': shutdown,
|
|
291
181
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Grasp SDK classes and utilities."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
from .server import GraspServer
|
|
6
|
+
from .browser import GraspBrowser
|
|
7
|
+
from .session import GraspSession
|
|
8
|
+
from .index import Grasp
|
|
9
|
+
from .utils import (
|
|
10
|
+
_servers,
|
|
11
|
+
shutdown,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Export the global servers registry
|
|
15
|
+
_servers: Dict[str, GraspServer] = _servers
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
'GraspServer',
|
|
19
|
+
'GraspBrowser',
|
|
20
|
+
'GraspSession',
|
|
21
|
+
'Grasp',
|
|
22
|
+
'shutdown',
|
|
23
|
+
'_servers',
|
|
24
|
+
]
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""GraspBrowser class for browser interface."""
|
|
2
|
+
|
|
3
|
+
import aiohttp
|
|
4
|
+
import os
|
|
5
|
+
from typing import Optional, Dict, Any, TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from ..services.browser import CDPConnection
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .session import GraspSession
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GraspBrowser:
|
|
14
|
+
"""Browser interface for Grasp session."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, connection: CDPConnection, session: Optional['GraspSession'] = None):
|
|
17
|
+
"""Initialize GraspBrowser.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
connection: CDP connection instance
|
|
21
|
+
session: GraspSession instance for accessing terminal and files services
|
|
22
|
+
"""
|
|
23
|
+
self.connection = connection
|
|
24
|
+
self.session = session
|
|
25
|
+
|
|
26
|
+
def get_host(self) -> str:
|
|
27
|
+
"""Get browser host.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Host for browser connection
|
|
31
|
+
"""
|
|
32
|
+
from urllib.parse import urlparse
|
|
33
|
+
return urlparse(self.connection.http_url).netloc
|
|
34
|
+
|
|
35
|
+
def get_endpoint(self) -> str:
|
|
36
|
+
"""Get browser WebSocket endpoint URL.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
WebSocket URL for browser connection
|
|
40
|
+
"""
|
|
41
|
+
return self.connection.ws_url
|
|
42
|
+
|
|
43
|
+
async def get_current_page_target_info(self) -> Optional[Dict[str, Any]]:
|
|
44
|
+
"""Get current page target information.
|
|
45
|
+
|
|
46
|
+
Warning:
|
|
47
|
+
This method is experimental and may change or be removed in future versions.
|
|
48
|
+
Use with caution in production environments.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Dictionary containing target info and last screenshot, or None if failed
|
|
52
|
+
"""
|
|
53
|
+
host = self.connection.http_url
|
|
54
|
+
api = f"{host}/api/page/info"
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
async with aiohttp.ClientSession() as session:
|
|
58
|
+
async with session.get(api) as response:
|
|
59
|
+
if not response.ok:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
data = await response.json()
|
|
63
|
+
page_info = data.get('pageInfo', {})
|
|
64
|
+
last_screenshot = page_info.get('lastScreenshot')
|
|
65
|
+
session_id = page_info.get('sessionId')
|
|
66
|
+
targets = page_info.get('targets', {})
|
|
67
|
+
target_id = page_info.get('targetId')
|
|
68
|
+
page_loaded = page_info.get('pageLoaded')
|
|
69
|
+
|
|
70
|
+
current_target = {}
|
|
71
|
+
for target in targets.values():
|
|
72
|
+
if target.get('targetId') == target_id:
|
|
73
|
+
current_target = target
|
|
74
|
+
break
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
'targetId': target_id,
|
|
78
|
+
'sessionId': session_id,
|
|
79
|
+
'pageLoaded': page_loaded,
|
|
80
|
+
**current_target,
|
|
81
|
+
'lastScreenshot': last_screenshot,
|
|
82
|
+
'targets': targets,
|
|
83
|
+
}
|
|
84
|
+
except Exception:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
async def download_replay_video(self, local_path: str) -> None:
|
|
88
|
+
"""Download replay video from screenshots.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
local_path: Local path to save the video file
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
RuntimeError: If session is not available or video generation fails
|
|
95
|
+
"""
|
|
96
|
+
if not self.session:
|
|
97
|
+
raise RuntimeError("Session is required for download_replay_video")
|
|
98
|
+
|
|
99
|
+
# Create terminal instance
|
|
100
|
+
terminal = self.session.terminal
|
|
101
|
+
|
|
102
|
+
# Generate file list for ffmpeg
|
|
103
|
+
command1 = await terminal.run_command(
|
|
104
|
+
"cd /home/user/downloads/grasp-screenshots && ls -1 | grep -v '^filelist.txt$' | sort | awk '{print \"file '\''\" $0 \"'\''\"}'> filelist.txt"
|
|
105
|
+
)
|
|
106
|
+
await command1.end()
|
|
107
|
+
|
|
108
|
+
# Generate video using ffmpeg
|
|
109
|
+
command2 = await terminal.run_command(
|
|
110
|
+
"cd /home/user/downloads/grasp-screenshots && ffmpeg -r 25 -f concat -safe 0 -i filelist.txt -vsync vfr -pix_fmt yuv420p output.mp4"
|
|
111
|
+
)
|
|
112
|
+
await command2.end()
|
|
113
|
+
|
|
114
|
+
# Determine final local path
|
|
115
|
+
if not local_path.endswith('.mp4'):
|
|
116
|
+
local_path = os.path.join(local_path, 'output.mp4')
|
|
117
|
+
|
|
118
|
+
# Download the generated video file
|
|
119
|
+
await self.session.files.download_file(
|
|
120
|
+
'/home/user/downloads/grasp-screenshots/output.mp4',
|
|
121
|
+
local_path
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
async def get_liveview_streaming_url(self) -> Optional[str]:
|
|
125
|
+
"""Get liveview streaming URL.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Liveview streaming URL or None if not ready
|
|
129
|
+
"""
|
|
130
|
+
host = self.connection.http_url
|
|
131
|
+
api = f"{host}/api/session/liveview/stream"
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
async with aiohttp.ClientSession() as session:
|
|
135
|
+
async with session.get(api) as response:
|
|
136
|
+
if not response.ok:
|
|
137
|
+
return None
|
|
138
|
+
return api
|
|
139
|
+
except Exception:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
async def get_liveview_page_url(self) -> Optional[str]:
|
|
143
|
+
"""Get liveview page URL.
|
|
144
|
+
|
|
145
|
+
Warning:
|
|
146
|
+
This method is experimental and may change or be removed in future versions.
|
|
147
|
+
Use with caution in production environments.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Liveview page URL or None if not ready
|
|
151
|
+
"""
|
|
152
|
+
host = self.connection.http_url
|
|
153
|
+
api = f"{host}/api/session/liveview/preview"
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
async with aiohttp.ClientSession() as session:
|
|
157
|
+
async with session.get(api) as response:
|
|
158
|
+
if not response.ok:
|
|
159
|
+
return None
|
|
160
|
+
return api
|
|
161
|
+
except Exception:
|
|
162
|
+
return None
|