grasp-sdk 0.2.0a1__py3-none-any.whl → 0.2.0b2__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.

@@ -31,7 +31,7 @@ async def example_with_context_manager():
31
31
 
32
32
  # 创建终端
33
33
  print("💻 创建终端...")
34
- terminal = session.terminal.create()
34
+ terminal = session.terminal
35
35
  print(f"✅ 终端已创建: {type(terminal).__name__}")
36
36
 
37
37
  # 访问文件系统
@@ -34,7 +34,7 @@ async def example_grasp_usage():
34
34
  print(f"✓ Browser WebSocket endpoint: {browser_endpoint}")
35
35
 
36
36
  # Create terminal
37
- terminal = session.terminal.create()
37
+ terminal = session.terminal
38
38
  print("✓ Terminal service created")
39
39
 
40
40
  # Access file system
@@ -36,7 +36,8 @@ async def main():
36
36
  # 'debug': True,
37
37
  },
38
38
  'keepAliveMS': 10000,
39
- 'timeout': 3600000, # 容器最长运行1小时(最大值可以为一天 86400000)
39
+ # 'logLevel': 'error',
40
+ #'timeout': 3600000, # 容器最长运行1小时(最大值可以为一天 86400000)
40
41
  })
41
42
 
42
43
  connection = session.browser.connection
@@ -44,7 +45,7 @@ async def main():
44
45
 
45
46
  # Create terminal connection
46
47
  print("📡 Creating terminal connection...")
47
- terminal = session.terminal.create()
48
+ terminal = session.terminal
48
49
 
49
50
  print("✅ Terminal connected!")
50
51
 
@@ -2,7 +2,7 @@
2
2
  """测试 Grasp 类的功能"""
3
3
 
4
4
  import os
5
- from grasp_sdk import Grasp, GraspSession, GraspBrowser, GraspTerminal
5
+ from grasp_sdk import Grasp, GraspSession, GraspBrowser
6
6
 
7
7
  def test_grasp_classes():
8
8
  """测试所有 Grasp 类的基本功能"""
@@ -25,7 +25,6 @@ def test_grasp_classes():
25
25
  print(f" ✓ Grasp 类可用: {Grasp is not None}")
26
26
  print(f" ✓ GraspSession 类可用: {GraspSession is not None}")
27
27
  print(f" ✓ GraspBrowser 类可用: {GraspBrowser is not None}")
28
- print(f" ✓ GraspTerminal 类可用: {GraspTerminal is not None}")
29
28
 
30
29
  # 测试方法可用性
31
30
  print("\n3. 测试方法可用性:")
@@ -17,7 +17,7 @@ def test_removed_methods():
17
17
  print("=" * 30)
18
18
 
19
19
  try:
20
- from grasp_sdk import Grasp, GraspSession, GraspBrowser, GraspTerminal
20
+ from grasp_sdk import Grasp, GraspSession, GraspBrowser
21
21
  print("✅ 核心类导入成功")
22
22
  except ImportError as e:
23
23
  print(f"❌ 核心类导入失败: {e}")
@@ -111,7 +111,7 @@ async def test_terminal_service():
111
111
  mock_sandbox.run_command.return_value = mock_emitter
112
112
 
113
113
  # Test run_command
114
- result = await terminal.run_command('echo "Hello World"', {'timeout': 5000})
114
+ result = await terminal.run_command('echo "Hello World"', {'timeout_ms': 5000})
115
115
  print("✓ run_command executed successfully")
116
116
 
117
117
  # Verify the command was called with correct options
@@ -152,7 +152,7 @@ async def test_command_options_compatibility():
152
152
  # Test that ICommandOptions supports 'inBackground' parameter
153
153
  options: ICommandOptions = {
154
154
  'inBackground': True,
155
- 'timeout': 5000,
155
+ 'timeout_ms': 5000,
156
156
  'cwd': '/tmp',
157
157
  'nohup': False
158
158
  }
grasp_sdk/__init__.py CHANGED
@@ -16,7 +16,6 @@ from .utils.logger import get_logger
16
16
  from .grasp import (
17
17
  GraspServer,
18
18
  GraspBrowser,
19
- GraspTerminal,
20
19
  GraspSession,
21
20
  Grasp,
22
21
  _servers,
@@ -26,7 +25,7 @@ from .grasp import (
26
25
  # Import CDPConnection for type annotation
27
26
  from .services.browser import CDPConnection
28
27
 
29
- __version__ = "0.2.0a1"
28
+ __version__ = "0.2.0b2"
30
29
  __author__ = "Grasp Team"
31
30
  __email__ = "team@grasp.dev"
32
31
 
@@ -177,7 +176,6 @@ default = {
177
176
  'Grasp': Grasp,
178
177
  'GraspSession': GraspSession,
179
178
  'GraspBrowser': GraspBrowser,
180
- 'GraspTerminal': GraspTerminal,
181
179
  'launch_browser': launch_browser,
182
180
  'shutdown': shutdown,
183
181
  }
@@ -4,7 +4,6 @@ from typing import Dict
4
4
 
5
5
  from .server import GraspServer
6
6
  from .browser import GraspBrowser
7
- from .terminal import GraspTerminal
8
7
  from .session import GraspSession
9
8
  from .index import Grasp
10
9
  from .utils import (
@@ -18,7 +17,6 @@ _servers: Dict[str, GraspServer] = _servers
18
17
  __all__ = [
19
18
  'GraspServer',
20
19
  'GraspBrowser',
21
- 'GraspTerminal',
22
20
  'GraspSession',
23
21
  'Grasp',
24
22
  'shutdown',
@@ -1,21 +1,36 @@
1
1
  """GraspBrowser class for browser interface."""
2
2
 
3
3
  import aiohttp
4
- from typing import Optional, Dict, Any
4
+ import os
5
+ from typing import Optional, Dict, Any, TYPE_CHECKING
5
6
 
6
7
  from ..services.browser import CDPConnection
7
8
 
9
+ if TYPE_CHECKING:
10
+ from .session import GraspSession
11
+
8
12
 
9
13
  class GraspBrowser:
10
14
  """Browser interface for Grasp session."""
11
15
 
12
- def __init__(self, connection: CDPConnection):
16
+ def __init__(self, connection: CDPConnection, session: Optional['GraspSession'] = None):
13
17
  """Initialize GraspBrowser.
14
18
 
15
19
  Args:
16
20
  connection: CDP connection instance
21
+ session: GraspSession instance for accessing terminal and files services
17
22
  """
18
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
19
34
 
20
35
  def get_endpoint(self) -> str:
21
36
  """Get browser WebSocket endpoint URL.
@@ -49,21 +64,99 @@ class GraspBrowser:
49
64
  last_screenshot = page_info.get('lastScreenshot')
50
65
  session_id = page_info.get('sessionId')
51
66
  targets = page_info.get('targets', {})
67
+ target_id = page_info.get('targetId')
68
+ page_loaded = page_info.get('pageLoaded')
52
69
 
53
- if session_id and session_id in targets:
54
- target_info = targets[session_id].copy()
55
- target_info['lastScreenshot'] = last_screenshot
56
- return target_info
70
+ current_target = {}
71
+ for target in targets.values():
72
+ if target.get('targetId') == target_id:
73
+ current_target = target
74
+ break
57
75
 
58
- return None
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
59
139
  except Exception:
60
140
  return None
61
141
 
62
- def get_liveview_url(self) -> Optional[str]:
63
- """Get liveview URL (TODO: implementation pending).
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.
64
148
 
65
149
  Returns:
66
- Liveview URL or None
150
+ Liveview page URL or None if not ready
67
151
  """
68
- # TODO: Implement liveview URL generation
69
- return None
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
grasp_sdk/grasp/index.py CHANGED
@@ -5,6 +5,7 @@ from typing import Dict, Optional, Any
5
5
 
6
6
  from .server import GraspServer
7
7
  from .session import GraspSession
8
+ from ..utils.config import get_config
8
9
 
9
10
 
10
11
  class Grasp:
@@ -32,12 +33,13 @@ class Grasp:
32
33
  Returns:
33
34
  GraspSession instance
34
35
  """
36
+ config = get_config()
35
37
  browser_options = options.get('browser', {})
36
- browser_options['key'] = self.key
37
- browser_options['keepAliveMS'] = options.get('keepAliveMS', 10000)
38
- browser_options['timeout'] = options.get('timeout', 900000)
39
- browser_options['debug'] = options.get('debug', False)
40
- browser_options['logLevel'] = options.get('logLevel', 'info')
38
+ browser_options['key'] = self.key or config['sandbox']['key']
39
+ browser_options['keepAliveMS'] = options.get('keepAliveMS', config['sandbox']['keepAliveMs'])
40
+ browser_options['timeout'] = options.get('timeout', config['sandbox']['timeout'])
41
+ browser_options['debug'] = options.get('debug', config['sandbox']['debug'])
42
+ browser_options['logLevel'] = options.get('logLevel', config['logger']['level'])
41
43
 
42
44
  server = GraspServer(browser_options)
43
45
  cdp_connection = await server.create_browser_task()
@@ -116,7 +118,10 @@ class Grasp:
116
118
 
117
119
  if not cdp_connection:
118
120
  # Create new server with no timeout for existing sessions
119
- server = GraspServer({'timeout': 0})
121
+ server = GraspServer({
122
+ 'timeout': 0,
123
+ 'key': self.key,
124
+ })
120
125
  cdp_connection = await server.connect_browser_task(session_id)
121
126
 
122
127
  return GraspSession(cdp_connection)
@@ -2,10 +2,12 @@
2
2
 
3
3
  import asyncio
4
4
  import websockets
5
+ from typing import Optional
5
6
  from .browser import GraspBrowser
6
- from .terminal import GraspTerminal
7
+ from ..services.terminal import TerminalService
7
8
  from ..services.browser import CDPConnection
8
9
  from ..services.filesystem import FileSystemService
10
+ from ..utils.logger import get_logger
9
11
 
10
12
 
11
13
  class GraspSession:
@@ -18,53 +20,56 @@ class GraspSession:
18
20
  connection: CDP connection instance
19
21
  """
20
22
  self.connection = connection
21
- self.browser = GraspBrowser(connection)
22
- self.terminal = GraspTerminal(connection)
23
+ self.browser = GraspBrowser(connection, self)
23
24
 
24
25
  # Create files service
25
26
  connection_id = connection.id
26
27
  try:
27
- from . import _servers
28
+ from .utils import _servers
28
29
  server = _servers[connection_id]
29
30
  if server.sandbox is None:
30
31
  raise RuntimeError('Sandbox is not available for file system service')
31
32
  self.files = FileSystemService(server.sandbox, connection)
33
+ self.terminal = TerminalService(server.sandbox, connection)
32
34
  except (ImportError, KeyError) as e:
33
35
  # In test environment or when server is not available, create a mock files service
34
- print(f"Warning: Files service not available: {e}")
36
+ self.logger.debug(f"Warning: Files service not available: {e}")
35
37
  raise e
36
38
 
37
39
  # WebSocket connection for keep-alive
38
40
  self._ws = None
39
- self._keep_alive_task = None
41
+ self._is_closed = False
42
+
43
+ self.logger = get_logger().child('GraspSession')
40
44
 
41
45
  # Initialize WebSocket connection and start keep-alive
42
- print('create session...')
46
+ # self.logger.debug('create session...')
43
47
  asyncio.create_task(self._initialize_websocket())
44
48
 
45
49
  async def _initialize_websocket(self) -> None:
46
50
  """Initialize WebSocket connection and start keep-alive."""
47
51
  try:
48
52
  self._ws = await websockets.connect(self.connection.ws_url)
49
- await self._keep_alive()
53
+ if not self._is_closed:
54
+ await self._keep_alive()
55
+ else:
56
+ await self._ws.close()
50
57
  except Exception as e:
51
- print(f"Failed to initialize WebSocket connection: {e}")
58
+ self.logger.debug(f"Failed to initialize WebSocket connection: {e}")
52
59
 
53
60
  async def _keep_alive(self) -> None:
54
61
  """Keep WebSocket connection alive with periodic ping."""
55
- if self._ws is None:
56
- return
57
-
58
- async def ping_loop():
59
- while self._ws and self._ws.close_code is None:
60
- try:
61
- await self._ws.ping()
62
- await asyncio.sleep(10) # 10 seconds interval, same as TypeScript version
63
- except Exception as e:
64
- print(f"Keep-alive ping failed: {e}")
65
- break
66
-
67
- self._keep_alive_task = asyncio.create_task(ping_loop())
62
+ ws = self._ws
63
+ await asyncio.sleep(10) # 10 seconds interval, same as TypeScript version
64
+ if ws and ws.close_code is None:
65
+ try:
66
+ # Send ping and wait for pong response
67
+ pong_waiter = ws.ping()
68
+ await pong_waiter
69
+ # Recursively call keep_alive after receiving pong
70
+ await self._keep_alive()
71
+ except Exception as e:
72
+ self.logger.debug(f"Keep-alive ping failed: {e}")
68
73
 
69
74
  @property
70
75
  def id(self) -> str:
@@ -75,34 +80,67 @@ class GraspSession:
75
80
  """
76
81
  return self.connection.id
77
82
 
83
+ def is_running(self) -> bool:
84
+ """Check if the session is currently running.
85
+
86
+ Returns:
87
+ True if the session is running, False otherwise
88
+ """
89
+ try:
90
+ from .utils import _servers
91
+ from ..models import SandboxStatus
92
+ server = _servers[self.id]
93
+ if server.sandbox is None:
94
+ return False
95
+ status = server.sandbox.get_status()
96
+ return status == SandboxStatus.RUNNING
97
+ except (ImportError, KeyError) as e:
98
+ self.logger.error(f"Failed to check running status: {e}")
99
+ return False
100
+
101
+ def get_host(self, port: int) -> Optional[str]:
102
+ """Get the external host address for a specific port.
103
+
104
+ Args:
105
+ port: Port number to get host for
106
+
107
+ Returns:
108
+ External host address or None if sandbox not available
109
+ """
110
+ try:
111
+ from .utils import _servers
112
+ server = _servers[self.id]
113
+ if server.sandbox is None:
114
+ self.logger.warn('Cannot get host: sandbox not available')
115
+ return None
116
+ return server.sandbox.get_sandbox_host(port)
117
+ except (ImportError, KeyError) as e:
118
+ self.logger.error(f"Failed to get host: {e}")
119
+ return None
120
+
78
121
  async def close(self) -> None:
79
122
  """Close the session and cleanup resources."""
80
- # Stop keep-alive task
81
- if self._keep_alive_task:
82
- self._keep_alive_task.cancel()
83
- try:
84
- await self._keep_alive_task
85
- except asyncio.CancelledError:
86
- pass
123
+ # Set closed flag to prevent new keep-alive cycles
124
+ self._is_closed = True
87
125
 
88
126
  # Close WebSocket connection with timeout
89
127
  if self._ws and self._ws.close_code is None:
90
128
  try:
91
129
  # Add timeout to prevent hanging on close
92
130
  await asyncio.wait_for(self._ws.close(), timeout=5.0)
93
- print('✅ WebSocket closed successfully')
131
+ self.logger.debug('✅ WebSocket closed successfully')
94
132
  except asyncio.TimeoutError:
95
- print('⚠️ WebSocket close timeout, forcing termination')
133
+ self.logger.debug('⚠️ WebSocket close timeout, forcing termination')
96
134
  # Force close if timeout
97
135
  if hasattr(self._ws, 'transport') and self._ws.transport:
98
136
  self._ws.transport.close()
99
137
  except Exception as e:
100
- print(f"Failed to close WebSocket connection: {e}")
138
+ self.logger.error(f"Failed to close WebSocket connection: {e}")
101
139
 
102
140
  # Cleanup server resources
103
141
  try:
104
- from . import _servers
142
+ from .utils import _servers
105
143
  await _servers[self.id].cleanup()
106
144
  except (ImportError, KeyError) as e:
107
145
  # In test environment or when server is not available, skip cleanup
108
- print(f"Warning: Server cleanup not available: {e}")
146
+ self.logger.error(f"Warning: Server cleanup not available: {e}")
@@ -23,6 +23,7 @@ class ISandboxConfig(TypedDict):
23
23
  timeout: int # Required: Default timeout in milliseconds
24
24
  workspace: NotRequired[str] # Optional: Grasp workspace ID
25
25
  debug: NotRequired[bool] # Optional: Enable debug mode for detailed logging
26
+ keepAliveMs: NotRequired[int] # Optional: Keep-alive time in milliseconds
26
27
 
27
28
 
28
29
  class IBrowserConfig(TypedDict):
@@ -35,19 +36,23 @@ class IBrowserConfig(TypedDict):
35
36
 
36
37
  class ICommandOptions(TypedDict):
37
38
  """Command execution options interface."""
38
- inBackground: NotRequired[bool] # Whether to run command in background
39
- timeout: NotRequired[int] # Timeout in milliseconds
39
+ inBackground: NotRequired[bool] # Whether to run command in background (corresponds to 'background' in TypeScript)
40
+ timeout_ms: NotRequired[int] # Timeout in milliseconds (corresponds to 'timeoutMs' in TypeScript)
40
41
  cwd: NotRequired[str] # Working directory
41
- nohup: NotRequired[bool] # Whether to use nohup for command execution
42
+ user: NotRequired[str] # User to run the command as (default: 'user')
43
+ envs: NotRequired[Dict[str, str]] # Environment variables for the command
44
+ nohup: NotRequired[bool] # Whether to use nohup for command execution (Grasp-specific extension)
45
+ # Note: onStdout and onStderr callbacks are handled differently in Python implementation
42
46
 
43
47
 
44
48
  class IScriptOptions(TypedDict):
45
49
  """Script execution options interface."""
46
50
  type: str # Required: Script type: 'cjs' for CommonJS, 'esm' for ES Modules, 'py' for Python
47
51
  cwd: NotRequired[str] # Working directory
48
- timeoutMs: NotRequired[int] # Timeout in milliseconds
52
+ timeout_ms: NotRequired[int] # Timeout in milliseconds
49
53
  background: NotRequired[bool] # Run in background
50
- nohup: NotRequired[bool] # Use nohup for background execution
54
+ user: NotRequired[str] # User to run the script as (default: 'user')
55
+ nohup: NotRequired[bool] # Use nohup for background execution (Grasp-specific extension)
51
56
  envs: NotRequired[Dict[str, str]] # The environment variables
52
57
  preCommand: NotRequired[str] # Pre command
53
58
 
@@ -106,6 +106,7 @@ class BrowserService:
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
  'BROWSER_TYPE': browser_type,
111
112
  **self.config['envs']
@@ -185,7 +186,7 @@ class BrowserService:
185
186
  'type': 'esm',
186
187
  'background': True,
187
188
  'nohup': not self.sandbox_service.is_debug,
188
- 'timeoutMs': 0,
189
+ 'timeout_ms': 0,
189
190
  'preCommand': ''
190
191
  }
191
192
 
@@ -376,8 +377,9 @@ class BrowserService:
376
377
  self._health_check_task = None
377
378
 
378
379
  # Kill the browser process
379
- # if hasattr(self.browser_process, 'kill'):
380
- # await self.browser_process.kill()
380
+ if self.browser_process is not None:
381
+ if hasattr(self.browser_process, 'kill'):
382
+ await self.browser_process.kill()
381
383
 
382
384
  self.browser_process = None
383
385
  self.cdp_connection = None
@@ -91,4 +91,33 @@ class FileSystemService:
91
91
  File content as string or bytes depending on encoding
92
92
  """
93
93
  read_result = await self.sandbox.read_file_from_sandbox(remote_path, options)
94
- return read_result
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
@@ -42,6 +42,8 @@ class CommandEventEmitter:
42
42
  def set_handle(self, handle: Any) -> None:
43
43
  """Set the command handle (used internally)."""
44
44
  self.handle = handle
45
+ if self.is_killed:
46
+ self.handle.kill()
45
47
 
46
48
  def on(self, event: str, callback: Callable) -> None:
47
49
  """Register event listener."""
@@ -98,9 +100,9 @@ class CommandEventEmitter:
98
100
 
99
101
  async def kill(self) -> None:
100
102
  """Kill the running command."""
101
- if not self.handle:
102
- raise RuntimeError('Command handle not set')
103
- await self.handle.kill()
103
+ self.is_killed = True
104
+ if self.handle:
105
+ await self.handle.kill()
104
106
 
105
107
  def get_handle(self) -> Optional[Any]:
106
108
  """Get the original command handle."""
@@ -284,10 +286,11 @@ class SandboxService:
284
286
  options = {}
285
287
 
286
288
  cwd = options.get('cwd', self.DEFAULT_WORKING_DIRECTORY)
287
- timeout_ms = options.get('timeout', self.config['timeout'])
289
+ timeout_ms = options.get('timeout_ms', 0)
288
290
  use_nohup = options.get('nohup', False)
289
291
  in_background = options.get('inBackground', False)
290
292
  envs = options.get('envs', {})
293
+ user = options.get('user', 'user')
291
294
 
292
295
  # print(f'command: {command}')
293
296
  # print(f'🚀 options {options}')
@@ -316,9 +319,10 @@ class SandboxService:
316
319
  result = await self.sandbox.commands.run(
317
320
  final_command,
318
321
  cwd=cwd,
319
- timeout=timeout_ms,
322
+ timeout=timeout_ms // 1000,
320
323
  envs=envs,
321
324
  background=in_background,
325
+ user=user,
322
326
  )
323
327
  else:
324
328
  raise RuntimeError('Sandbox commands interface not available')
@@ -344,6 +348,7 @@ class SandboxService:
344
348
  timeout=timeout_ms // 1000,
345
349
  envs=envs,
346
350
  background=in_background,
351
+ user=user,
347
352
  )
348
353
  return None
349
354
 
@@ -363,6 +368,7 @@ class SandboxService:
363
368
  timeout=timeout_ms // 1000,
364
369
  background=True,
365
370
  envs=envs,
371
+ user=user,
366
372
  on_stdout=lambda data: event_emitter.emit('stdout', data),
367
373
  on_stderr=lambda data: event_emitter.emit('stderr', data)
368
374
  )
@@ -467,10 +473,11 @@ class SandboxService:
467
473
  # Prepare command options
468
474
  cmd_options: Optional[Any] = {
469
475
  'cwd': options.get('cwd'),
470
- 'timeout': options.get('timeoutMs'),
476
+ 'timeout_ms': options.get('timeout_ms', 0),
471
477
  'inBackground': options.get('background', False),
472
478
  'nohup': options.get('nohup', False),
473
479
  'envs': envs,
480
+ 'user': options.get('user', 'user'),
474
481
  }
475
482
 
476
483
  # Execute the script
@@ -858,7 +865,7 @@ class SandboxService:
858
865
  raise ValueError(error_msg)
859
866
 
860
867
  remote_path = src
861
- local_path = os.path.join(dist, str(self.id), str(int(time.time() * 1000)))
868
+ local_path = dist
862
869
 
863
870
  try:
864
871
  self.logger.debug('🗂️ Starting recursive directory sync', {
@@ -138,6 +138,11 @@ class Commands:
138
138
  # Force background execution for streaming
139
139
  bg_options: ICommandOptions = {**options, 'inBackground': True, 'nohup': False}
140
140
 
141
+ # Set browser endpoint in environment variables
142
+ if 'envs' not in bg_options:
143
+ bg_options['envs'] = {}
144
+ bg_options['envs']['BROWSER_ENDPOINT'] = self.connection.ws_url
145
+
141
146
  emitter: Optional[CommandEventEmitter] = await self.sandbox.run_command(
142
147
  command, bg_options
143
148
  )
@@ -4,7 +4,7 @@ This package contains utility functions and classes for configuration,
4
4
  logging, authentication, and other common functionality."""
5
5
 
6
6
  # Import all utility modules
7
- from .config import get_config, get_sandbox_config, get_browser_config, DEFAULT_CONFIG
7
+ from .config import get_config, get_sandbox_config, DEFAULT_CONFIG
8
8
  from .logger import Logger, init_logger, get_logger, debug, info, warn, error
9
9
  from .auth import verify, login, AuthError, KeyVerificationError, AuthManager
10
10
 
@@ -12,7 +12,6 @@ __all__ = [
12
12
  # Configuration
13
13
  'get_config',
14
14
  'get_sandbox_config',
15
- 'get_browser_config',
16
15
  'DEFAULT_CONFIG',
17
16
  # Logging
18
17
  'Logger',
grasp_sdk/utils/config.py CHANGED
@@ -25,9 +25,10 @@ def get_config() -> Dict[str, Any]:
25
25
  'key': os.getenv('GRASP_KEY', ''),
26
26
  'timeout': int(os.getenv('GRASP_SERVICE_TIMEOUT', '900000')),
27
27
  'debug': os.getenv('GRASP_DEBUG', 'false').lower() == 'true',
28
+ 'keepAliveMs': int(os.getenv('GRASP_KEEP_ALIVE_MS', '10000')),
28
29
  },
29
30
  'logger': {
30
- 'level': os.getenv('GRASP_LOG_LEVEL', 'info'),
31
+ 'level': os.getenv('GRASP_LOG_LEVEL', None),
31
32
  'console': True,
32
33
  'file': os.getenv('GRASP_LOG_FILE'),
33
34
  },
@@ -54,40 +55,9 @@ def get_sandbox_config() -> ISandboxConfig:
54
55
 
55
56
  return sandbox_config
56
57
 
57
-
58
- def get_browser_config() -> IBrowserConfig:
59
- """Gets browser-specific configuration.
60
-
61
- Returns:
62
- IBrowserConfig: Browser configuration object.
63
- """
64
- return IBrowserConfig(
65
- args=[
66
- '--no-sandbox',
67
- '--disable-setuid-sandbox',
68
- '--disable-dev-shm-usage',
69
- '--disable-gpu',
70
- '--remote-debugging-port=9222',
71
- '--remote-debugging-address=0.0.0.0',
72
- ],
73
- headless=os.getenv('GRASP_HEADLESS', 'true').lower() == 'true',
74
- launchTimeout=int(os.getenv('GRASP_LAUNCH_TIMEOUT', '30000')),
75
- envs={
76
- 'PLAYWRIGHT_BROWSERS_PATH': '0',
77
- 'DISPLAY': ':99',
78
- },
79
- )
80
-
81
-
82
58
  # Default configuration constants
83
59
  DEFAULT_CONFIG = {
84
- 'PLAYWRIGHT_BROWSERS_PATH': '0',
85
60
  'WORKING_DIRECTORY': '/home/user',
86
- 'SCREENSHOT_PATH': '/home/user',
87
- 'DEFAULT_VIEWPORT': {
88
- 'width': 1280,
89
- 'height': 720,
90
- },
91
61
  }
92
62
 
93
63
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grasp_sdk
3
- Version: 0.2.0a1
3
+ Version: 0.2.0b2
4
4
  Summary: Python SDK for Grasp E2B - Browser automation and sandbox management
5
5
  Home-page: https://github.com/grasp-team/grasp-e2b
6
6
  Author: Grasp Team
@@ -0,0 +1,33 @@
1
+ examples/example_async_context.py,sha256=Ml-WDaKWTZQKeFMoYEPwEFm1W1k7_8Uqyqa6bRTLrNg,4022
2
+ examples/example_binary_file_support.py,sha256=katDD0u8Wd0D3-8QbsNIK7-dnMwWQSPNdl3kqHOQgq0,4432
3
+ examples/example_grasp_usage.py,sha256=mFC04yD_ONaIO_z6jlOvKbgI01WW_F1m17sseiiKTyI,2722
4
+ examples/example_readfile_usage.py,sha256=2KM0lJoMdlroIogaZr_dIqsh_BOrc3GmY1AD2N-2nK8,4505
5
+ examples/grasp_terminal.py,sha256=LRgW1e2jTPkYDytBaM3c1JolEGDIuQMeDZqOW9WhR8k,3442
6
+ examples/grasp_usage.py,sha256=kOq2eU51TX6C8oWXC19-JLWshCssBoc_8Y9h2Yv901Y,3806
7
+ examples/test_async_context.py,sha256=0iDq7ofGM8gUcH5m6MUrgLiRbaEka3mgKO-s9DWZdpM,2379
8
+ examples/test_grasp_classes.py,sha256=MOf3I6V1jZSltHVM1LmZOTkYXLmj3l1CVsUWYooJVVE,3059
9
+ examples/test_python_script.py,sha256=dqJSmvmrq2fdNXynJtMjqMXorrrM08ho6pFibIAZMpI,5265
10
+ examples/test_removed_methods.py,sha256=xGF_AMjoGOB3b70hBosZCnEHj_jEfnvc1yC_fJaI31o,2793
11
+ examples/test_terminal_updates.py,sha256=LihPUOVBOeM5Ee2A4ka4IwZH2vf9ZCiCesvpHWwD2Lk,6694
12
+ grasp_sdk/__init__.py,sha256=-6oNerErTrMFr3ptR788_CSqs2C6diE77LK_8yglN9E,5161
13
+ grasp_sdk/grasp/__init__.py,sha256=op-QqDUSLjuVhoztmmFfw1ubDDKwAPSAYCTsPFwW6vY,442
14
+ grasp_sdk/grasp/browser.py,sha256=2DWAsDjKpg1MKycQFIpMPHlxVew-Aatdh98Syj-lEkE,5734
15
+ grasp_sdk/grasp/index.py,sha256=PMEFUDvPlW76pf2uBfht6h6rSLpqOrQYuSygx3vfbF8,4352
16
+ grasp_sdk/grasp/server.py,sha256=arAYJZuJNozDuQTCX7HFgO9p9pA_TC-Sz5fz68jnKKY,8053
17
+ grasp_sdk/grasp/session.py,sha256=IxzpWyN8RIxZusgHkAPZGJP81OL2KO62r7gJehPGgng,5579
18
+ grasp_sdk/grasp/utils.py,sha256=j22tUI9yZacsdURObs-a5hxVM7sHsPBO4msRxdVb3MY,2691
19
+ grasp_sdk/models/__init__.py,sha256=M8ZpJARtNqZ5pczXxShUcfieJSCXSrfGARWcsefMi6A,3181
20
+ grasp_sdk/services/__init__.py,sha256=tfiAU211-iMObTflMX4y4TYPU6uXr8g5J0tcc7hLzYs,540
21
+ grasp_sdk/services/browser.py,sha256=oKT4Rgng0H1xcd6vHB_MrZRL6kL1qRujxa_-HH4-0Bg,15317
22
+ grasp_sdk/services/filesystem.py,sha256=mDLDqlLA70MnWqTUxnmpVr-6WZJZLZADa42IWjG7QaM,4184
23
+ grasp_sdk/services/sandbox.py,sha256=h6balEQg-CqblkdaqHQwltB-unXPTjAP-dCZGICBFsY,35766
24
+ grasp_sdk/services/terminal.py,sha256=Y7v3H1l_-fQp9C31lEf-57TP1ls9DwpiE7JfumVIDv8,6037
25
+ grasp_sdk/utils/__init__.py,sha256=0Z_1QTXQg4XQhCEgLdnNK0o3q927n15zAHc16QxtAtE,770
26
+ grasp_sdk/utils/auth.py,sha256=W51xyswim48wy2x3xGFFOfWXFAmG4jAwh5wxDgWjKP4,7261
27
+ grasp_sdk/utils/config.py,sha256=FAuAFNXPwtm6E463w6-QUBPDV9VgyfvZlmCncMI_oDQ,3183
28
+ grasp_sdk/utils/logger.py,sha256=k6WDmzL3cPTcQbiiTD4Q6fsDdUO_kyXQHz7nlmtv7G4,7228
29
+ grasp_sdk-0.2.0b2.dist-info/METADATA,sha256=MLxLEb7PU35Ruv3IOhLd8uYxcGuZwenU7nZd4DyFx4w,10121
30
+ grasp_sdk-0.2.0b2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
+ grasp_sdk-0.2.0b2.dist-info/entry_points.txt,sha256=roDjUu4JR6b1tUtRmq018mw167AbfC5zQiOtv3o6IMo,45
32
+ grasp_sdk-0.2.0b2.dist-info/top_level.txt,sha256=TCCFvqgXTCdDpREPkfO5su7ymn5KEA0WuzSSDjvA8Es,19
33
+ grasp_sdk-0.2.0b2.dist-info/RECORD,,
@@ -1,90 +0,0 @@
1
- #!/usr/bin/env python3
2
- """测试 get_replay_screenshots 功能的示例脚本
3
-
4
- 这个脚本演示如何使用 get_replay_screenshots 函数获取截图目录路径。
5
- """
6
-
7
- import asyncio
8
- import os
9
- from grasp_sdk import launch_browser, get_replay_screenshots
10
-
11
-
12
- async def test_get_replay_screenshots():
13
- """测试 get_replay_screenshots 功能"""
14
- print("🚀 启动浏览器服务...")
15
-
16
- try:
17
- # 启动浏览器并获取连接信息
18
- connection = await launch_browser({
19
- 'headless': True,
20
- 'type': 'chromium'
21
- })
22
-
23
- print(f"✅ 浏览器启动成功!")
24
- print(f"连接 ID: {connection.id}")
25
- print(f"WebSocket URL: {connection.ws_url or connection.ws_url}")
26
- print(f"HTTP URL: {connection.http_url}")
27
-
28
- # 获取截图目录路径
29
- print("\n📸 获取截图目录路径...")
30
- screenshots_dir = await get_replay_screenshots(connection)
31
-
32
- print(f"✅ 截图目录路径: {screenshots_dir}")
33
-
34
- # 检查目录是否存在
35
- if os.path.exists(screenshots_dir):
36
- print(f"📁 目录已存在")
37
-
38
- # 列出目录中的文件
39
- files = os.listdir(screenshots_dir)
40
- if files:
41
- print(f"📋 目录中有 {len(files)} 个文件:")
42
- for file in files[:5]: # 只显示前5个文件
43
- print(f" - {file}")
44
- if len(files) > 5:
45
- print(f" ... 还有 {len(files) - 5} 个文件")
46
- else:
47
- print("📂 目录为空")
48
- else:
49
- print(f"⚠️ 目录不存在,将在首次使用时创建")
50
-
51
- # 测试错误处理
52
- print("\n🧪 测试错误处理...")
53
-
54
- # 测试缺少 id 字段的情况
55
- try:
56
- invalid_connection = {
57
- 'ws_url': connection.ws_url,
58
- 'http_url': connection.http_url
59
- # 缺少 'id' 字段
60
- }
61
- await get_replay_screenshots(invalid_connection)
62
- except ValueError as e:
63
- print(f"✅ 正确捕获 ValueError: {e}")
64
-
65
- # 测试无效 ID 的情况
66
- try:
67
- invalid_connection = {
68
- 'id': 'invalid-id-123',
69
- 'ws_url': connection.ws_url,
70
- 'http_url': connection.http_url
71
- }
72
- await get_replay_screenshots(invalid_connection)
73
- except KeyError as e:
74
- print(f"✅ 正确捕获 KeyError: {e}")
75
-
76
- print("\n🎉 所有测试完成!")
77
-
78
- except Exception as e:
79
- print(f"❌ 测试失败: {e}")
80
- raise
81
-
82
-
83
- if __name__ == "__main__":
84
- # 检查环境变量
85
- if not os.getenv('GRASP_KEY'):
86
- print("⚠️ 警告: 未设置 GRASP_KEY 环境变量")
87
- print("请设置: export GRASP_KEY=your_api_key")
88
-
89
- # 运行测试
90
- asyncio.run(test_get_replay_screenshots())
@@ -1,62 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- 测试 shutdown 方法的废弃警告
4
- """
5
-
6
- import warnings
7
- import sys
8
- import os
9
-
10
- # 添加项目路径
11
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
12
-
13
- from grasp_sdk import shutdown
14
-
15
- def test_shutdown_deprecation():
16
- """测试 shutdown 方法是否正确发出废弃警告"""
17
- print("🧪 测试 shutdown 方法的废弃警告...")
18
-
19
- # 设置警告过滤器显示所有警告
20
- warnings.filterwarnings("always")
21
-
22
- # 捕获警告
23
- with warnings.catch_warnings(record=True) as w:
24
- warnings.simplefilter("always")
25
-
26
- print("📞 调用 shutdown(None)...")
27
-
28
- # 调用废弃的 shutdown 方法
29
- try:
30
- shutdown(None)
31
- print("✅ shutdown 方法调用完成")
32
- except SystemExit:
33
- print("⚠️ shutdown 方法触发了系统退出")
34
- except Exception as e:
35
- print(f"⚠️ shutdown 方法调用出现异常: {e}")
36
-
37
- # 检查是否有废弃警告
38
- print(f"\n📊 捕获到 {len(w)} 个警告")
39
- deprecation_warnings = [warning for warning in w if issubclass(warning.category, DeprecationWarning)]
40
-
41
- if deprecation_warnings:
42
- print(f"✅ 检测到 {len(deprecation_warnings)} 个废弃警告:")
43
- for warning in deprecation_warnings:
44
- print(f" - {warning.message}")
45
- print(f" 文件: {warning.filename}:{warning.lineno}")
46
- else:
47
- print("❌ 未检测到废弃警告")
48
- if w:
49
- print("其他警告:")
50
- for warning in w:
51
- print(f" - {warning.category.__name__}: {warning.message}")
52
-
53
- print("\n📋 废弃方法迁移指南:")
54
- print(" 旧方式: shutdown(connection)")
55
- print(" 新方式: await session.close()")
56
- print("\n💡 建议:")
57
- print(" - 使用 grasp.launch() 获取 session")
58
- print(" - 使用 await session.close() 关闭会话")
59
- print(" - 避免直接使用 shutdown() 方法")
60
-
61
- if __name__ == '__main__':
62
- test_shutdown_deprecation()
@@ -1,32 +0,0 @@
1
- """GraspTerminal class for terminal interface."""
2
-
3
- from ..services.browser import CDPConnection
4
- from ..services.terminal import TerminalService
5
-
6
-
7
- class GraspTerminal:
8
- """Terminal interface for Grasp session."""
9
-
10
- def __init__(self, connection: CDPConnection):
11
- """Initialize GraspTerminal.
12
-
13
- Args:
14
- connection: CDP connection instance
15
- """
16
- self.connection = connection
17
-
18
- def create(self) -> TerminalService:
19
- """Create a new terminal service instance.
20
-
21
- Returns:
22
- TerminalService instance
23
-
24
- Raises:
25
- RuntimeError: If sandbox is not available
26
- """
27
- connection_id = self.connection.id
28
- from . import _servers
29
- server = _servers[connection_id]
30
- if server.sandbox is None:
31
- raise RuntimeError('Sandbox is not available')
32
- return TerminalService(server.sandbox, self.connection)
@@ -1,36 +0,0 @@
1
- examples/example_async_context.py,sha256=W4PvU-Ck6wEc6c5L9S0i3RoeHhJ6TtqiZFRZKSVS8QU,4031
2
- examples/example_binary_file_support.py,sha256=katDD0u8Wd0D3-8QbsNIK7-dnMwWQSPNdl3kqHOQgq0,4432
3
- examples/example_grasp_usage.py,sha256=_mX25NLn6nF96MHU8LWq65sL5D4XSISYRDP6X3BQ48s,2731
4
- examples/example_readfile_usage.py,sha256=2KM0lJoMdlroIogaZr_dIqsh_BOrc3GmY1AD2N-2nK8,4505
5
- examples/grasp_terminal.py,sha256=dDstcnCs89paJ3o2SxAmHmI5_190RUzI_c8gX6aieUg,3415
6
- examples/grasp_usage.py,sha256=kOq2eU51TX6C8oWXC19-JLWshCssBoc_8Y9h2Yv901Y,3806
7
- examples/test_async_context.py,sha256=0iDq7ofGM8gUcH5m6MUrgLiRbaEka3mgKO-s9DWZdpM,2379
8
- examples/test_get_replay_screenshots.py,sha256=fXK_lbQ7KvXC6rnLVwO_hORZXhhHLdpdOYNoDXEYaEs,2991
9
- examples/test_grasp_classes.py,sha256=qeghDiw-91JN_2YerSfMLeMi4oJiJBeQ1NtlYzmK2AI,3148
10
- examples/test_python_script.py,sha256=dqJSmvmrq2fdNXynJtMjqMXorrrM08ho6pFibIAZMpI,5265
11
- examples/test_removed_methods.py,sha256=iW9Wxi8wRXdIffOGV_EjYOtDujOTyJoWpdMXTIExlOI,2808
12
- examples/test_shutdown_deprecation.py,sha256=QbzLld_3Ur8MjOKSiOrfluW507HYtnfTbgbfwb-OYWo,2127
13
- examples/test_terminal_updates.py,sha256=qh04AuM7T3TuwtOAHaH70YRY2ur88rVj3j-StlYFkQs,6688
14
- grasp_sdk/__init__.py,sha256=Sjru_RR0LAOm84WY2fxpsHfcxRLI3kjmEO321Yj5t0E,5216
15
- grasp_sdk/grasp/__init__.py,sha256=5i8Now_-re_FtijbkCJhQp-H_WOaV9ncj39vVjM2UKE,499
16
- grasp_sdk/grasp/browser.py,sha256=wmt9W38s-__BTqBAODjvfWBg-1dGCvx4PfGTNy3Sj6U,2295
17
- grasp_sdk/grasp/index.py,sha256=2p8mbrKiV0sC4YBr2ncUcF821BOOtFhh6T-aQUdR-ow,4103
18
- grasp_sdk/grasp/server.py,sha256=arAYJZuJNozDuQTCX7HFgO9p9pA_TC-Sz5fz68jnKKY,8053
19
- grasp_sdk/grasp/session.py,sha256=uIo5pQfyJp70fCfWSHiLow_UTVajTTJCIXjPb5IK40A,4023
20
- grasp_sdk/grasp/terminal.py,sha256=7GhfhStfdKykjwgm1xQZDjlePUaEJ83Zls3QFRi5BPs,955
21
- grasp_sdk/grasp/utils.py,sha256=j22tUI9yZacsdURObs-a5hxVM7sHsPBO4msRxdVb3MY,2691
22
- grasp_sdk/models/__init__.py,sha256=7uza9Y0yk6rFpQmyjaw0dVDf5uvaho42biuU1RiUzYU,2636
23
- grasp_sdk/services/__init__.py,sha256=tfiAU211-iMObTflMX4y4TYPU6uXr8g5J0tcc7hLzYs,540
24
- grasp_sdk/services/browser.py,sha256=RPrLjK8GB92Q1ZmDr0cVooCmv8do8RgHShRY2_999As,15218
25
- grasp_sdk/services/filesystem.py,sha256=XjeOgJJiPAUIBMvGLb8S9k1TBVan6ntl7Jg6PzNsGoc,3238
26
- grasp_sdk/services/sandbox.py,sha256=nIR9Xb5CiwxbwneCa-i-YD60mL7UwMVO9EEhIdRuxac,35587
27
- grasp_sdk/services/terminal.py,sha256=zBoWpOHxBM6Fi50SDd4fEypqBGd595UblLfp_912XBk,5807
28
- grasp_sdk/utils/__init__.py,sha256=IQzRV-iZJXanSlaXBcgXBCcOXTVBCY6ZujxQpDTGW9w,816
29
- grasp_sdk/utils/auth.py,sha256=W51xyswim48wy2x3xGFFOfWXFAmG4jAwh5wxDgWjKP4,7261
30
- grasp_sdk/utils/config.py,sha256=NhHnUwmYOaaV_DZVep_BXsKoZPNT_-yRsiV04q83IXw,3967
31
- grasp_sdk/utils/logger.py,sha256=k6WDmzL3cPTcQbiiTD4Q6fsDdUO_kyXQHz7nlmtv7G4,7228
32
- grasp_sdk-0.2.0a1.dist-info/METADATA,sha256=8XBosDPayz3Im-418024HVby_0yUwiUs8-00d6T88fw,10121
33
- grasp_sdk-0.2.0a1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
34
- grasp_sdk-0.2.0a1.dist-info/entry_points.txt,sha256=roDjUu4JR6b1tUtRmq018mw167AbfC5zQiOtv3o6IMo,45
35
- grasp_sdk-0.2.0a1.dist-info/top_level.txt,sha256=TCCFvqgXTCdDpREPkfO5su7ymn5KEA0WuzSSDjvA8Es,19
36
- grasp_sdk-0.2.0a1.dist-info/RECORD,,