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.

grasp_sdk/__init__.py CHANGED
@@ -1,291 +1,181 @@
1
- """Grasp E2B Python SDK
1
+ """Grasp Server Python SDK
2
2
 
3
- A Python SDK for E2B platform providing secure command execution
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 sys
10
- from typing import Dict, Optional, Any, Literal
9
+ from typing import Dict, Optional, Any
10
+ import warnings
11
11
 
12
12
  # Import utilities
13
- from .utils.logger import init_logger, get_logger
14
- from .utils.config import get_config
15
- from .services.browser import BrowserService
16
- from .services.sandbox import SandboxService
17
-
18
- # Import models and types
19
- from .models import (
20
- ISandboxConfig,
21
- IBrowserConfig,
22
- ICommandOptions,
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
- __version__ = "0.1.9"
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
- class GraspServer:
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
- ) -> Dict[str, Any]:
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(signal_name: str) -> None:
240
- """Handle graceful shutdown.
64
+ async def _graceful_shutdown(reason: str = 'SIGTERM') -> None:
65
+ """Gracefully shutdown all servers.
241
66
 
242
67
  Args:
243
- signal_name: Name of the signal received
68
+ reason: Reason for shutdown
244
69
  """
245
- print(f'Received {signal_name}, starting cleanup...')
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
- # Cleanup all GraspServer instances
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
- print('All servers cleaned up, exiting...')
253
- sys.exit(0)
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(signum, frame):
259
- signal_name = signal.Signals(signum).name
260
- asyncio.create_task(_graceful_shutdown(signal_name))
261
-
262
- # Register signal handlers
263
- signal.signal(signal.SIGINT, signal_handler) # Ctrl+C
264
- signal.signal(signal.SIGTERM, signal_handler) # Termination signal
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
- # Setup signal handlers on import
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
- 'ISandboxConfig',
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