grasp-sdk 0.1.8__py3-none-any.whl → 0.2.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of grasp-sdk might be problematic. Click here for more details.

Files changed (38) hide show
  1. examples/example_async_context.py +122 -0
  2. examples/example_binary_file_support.py +127 -0
  3. examples/example_grasp_usage.py +82 -0
  4. examples/example_readfile_usage.py +136 -0
  5. examples/grasp_terminal.py +110 -0
  6. examples/grasp_usage.py +111 -0
  7. examples/test_async_context.py +64 -0
  8. examples/test_get_replay_screenshots.py +90 -0
  9. examples/test_grasp_classes.py +80 -0
  10. examples/test_python_script.py +160 -0
  11. examples/test_removed_methods.py +80 -0
  12. examples/test_shutdown_deprecation.py +62 -0
  13. examples/test_terminal_updates.py +196 -0
  14. grasp_sdk/__init__.py +131 -239
  15. grasp_sdk/grasp/__init__.py +26 -0
  16. grasp_sdk/grasp/browser.py +69 -0
  17. grasp_sdk/grasp/index.py +122 -0
  18. grasp_sdk/grasp/server.py +250 -0
  19. grasp_sdk/grasp/session.py +108 -0
  20. grasp_sdk/grasp/terminal.py +32 -0
  21. grasp_sdk/grasp/utils.py +90 -0
  22. grasp_sdk/models/__init__.py +1 -1
  23. grasp_sdk/services/__init__.py +8 -1
  24. grasp_sdk/services/browser.py +92 -63
  25. grasp_sdk/services/filesystem.py +94 -0
  26. grasp_sdk/services/sandbox.py +391 -20
  27. grasp_sdk/services/terminal.py +177 -0
  28. grasp_sdk/utils/auth.py +6 -8
  29. {grasp_sdk-0.1.8.dist-info → grasp_sdk-0.2.0a1.dist-info}/METADATA +2 -3
  30. grasp_sdk-0.2.0a1.dist-info/RECORD +36 -0
  31. {grasp_sdk-0.1.8.dist-info → grasp_sdk-0.2.0a1.dist-info}/top_level.txt +1 -0
  32. grasp_sdk/sandbox/bootstrap-chrome-stable.mjs +0 -69
  33. grasp_sdk/sandbox/chrome-stable.mjs +0 -424
  34. grasp_sdk/sandbox/chromium.mjs +0 -395
  35. grasp_sdk/sandbox/http-proxy.mjs +0 -324
  36. grasp_sdk-0.1.8.dist-info/RECORD +0 -18
  37. {grasp_sdk-0.1.8.dist-info → grasp_sdk-0.2.0a1.dist-info}/WHEEL +0 -0
  38. {grasp_sdk-0.1.8.dist-info → grasp_sdk-0.2.0a1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script to verify terminal service updates in Python SDK.
4
+
5
+ This script tests the updated terminal service functionality,
6
+ including the new kill() method and response.json() compatibility.
7
+ """
8
+
9
+ import asyncio
10
+ import sys
11
+ import os
12
+ from unittest.mock import Mock, AsyncMock, patch
13
+
14
+ # Add the parent directory to the path to import grasp_sdk
15
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
16
+
17
+ from grasp_sdk.services.terminal import TerminalService, StreamableCommandResult
18
+ from grasp_sdk.services.sandbox import SandboxService, CommandEventEmitter
19
+ from grasp_sdk.services.browser import CDPConnection
20
+ from grasp_sdk.models import ISandboxConfig
21
+
22
+
23
+ class MockCommandEventEmitter(CommandEventEmitter):
24
+ """Mock command event emitter for testing."""
25
+
26
+ def __init__(self):
27
+ super().__init__()
28
+ self.killed = False
29
+ self.stdout_data = "Test output\n"
30
+ self.stderr_data = "Test error\n"
31
+
32
+ async def wait(self):
33
+ """Mock wait method."""
34
+ await asyncio.sleep(0.1) # Simulate some processing time
35
+ return {
36
+ 'exit_code': 0,
37
+ 'stdout': self.stdout_data,
38
+ 'stderr': self.stderr_data
39
+ }
40
+
41
+ async def kill(self):
42
+ """Mock kill method."""
43
+ self.killed = True
44
+ print("Command killed successfully")
45
+
46
+
47
+ async def test_streamable_command_result():
48
+ """Test StreamableCommandResult functionality."""
49
+ print("\n=== Testing StreamableCommandResult ===")
50
+
51
+ # Create mock emitter and task
52
+ emitter = MockCommandEventEmitter()
53
+ task = asyncio.create_task(emitter.wait())
54
+
55
+ # Create StreamableCommandResult
56
+ result = StreamableCommandResult(emitter, task)
57
+
58
+ # Test stdout/stderr streams
59
+ print("✓ StreamableCommandResult created successfully")
60
+
61
+ # Test response.json() method
62
+ response_data = await result.json()
63
+ print(f"✓ Response JSON: {response_data}")
64
+
65
+ # Test end() method
66
+ end_result = await result.end()
67
+ print(f"✓ End result: {end_result}")
68
+
69
+ # Test kill() method
70
+ await result.kill()
71
+ print(f"✓ Kill method executed, emitter killed: {emitter.killed}")
72
+
73
+ print("✓ All StreamableCommandResult tests passed!")
74
+
75
+
76
+ async def test_terminal_service():
77
+ """Test TerminalService functionality."""
78
+ print("\n=== Testing TerminalService ===")
79
+
80
+ # Create mock sandbox and connection
81
+ sandbox_config: ISandboxConfig = {
82
+ 'key': 'test-key',
83
+ 'timeout': 30000,
84
+ 'debug': True
85
+ }
86
+
87
+ mock_sandbox = Mock(spec=SandboxService)
88
+ mock_sandbox.is_debug = True
89
+ mock_sandbox.run_command = AsyncMock()
90
+
91
+ mock_connection = Mock(spec=CDPConnection)
92
+ mock_connection.ws_url = 'ws://localhost:8080'
93
+
94
+ # Create terminal service
95
+ terminal = TerminalService(mock_sandbox, mock_connection)
96
+ print("✓ TerminalService created successfully")
97
+
98
+ # Mock websockets.connect
99
+ with patch('grasp_sdk.services.terminal.websockets.connect') as mock_connect:
100
+ mock_ws = AsyncMock()
101
+ mock_ws.open = True
102
+ mock_connect.return_value.__aenter__ = AsyncMock(return_value=mock_ws)
103
+ mock_connect.return_value.__aexit__ = AsyncMock(return_value=None)
104
+ # For direct await usage
105
+ async def mock_connect_func(*args, **kwargs):
106
+ return mock_ws
107
+ mock_connect.side_effect = mock_connect_func
108
+
109
+ # Mock emitter
110
+ mock_emitter = MockCommandEventEmitter()
111
+ mock_sandbox.run_command.return_value = mock_emitter
112
+
113
+ # Test run_command
114
+ result = await terminal.run_command('echo "Hello World"', {'timeout': 5000})
115
+ print("✓ run_command executed successfully")
116
+
117
+ # Verify the command was called with correct options
118
+ mock_sandbox.run_command.assert_called_once()
119
+ call_args = mock_sandbox.run_command.call_args
120
+ command_arg = call_args[0][0]
121
+ options_arg = call_args[0][1]
122
+
123
+ print(f"✓ Command: {command_arg}")
124
+ print(f"✓ Options: {options_arg}")
125
+
126
+ # Verify inBackground option is set correctly
127
+ assert options_arg['inBackground'] == True, "inBackground option should be True"
128
+ assert options_arg['nohup'] == False, "Nohup option should be False"
129
+ print("✓ Command options verified")
130
+
131
+ # Test result methods
132
+ response_data = await result.json()
133
+ print(f"✓ Response JSON: {response_data}")
134
+
135
+ # Test kill method
136
+ await result.kill()
137
+ print("✓ Kill method executed successfully")
138
+
139
+ # Test close method
140
+ terminal.close()
141
+ print("✓ Terminal service closed successfully")
142
+
143
+ print("✓ All TerminalService tests passed!")
144
+
145
+
146
+ async def test_command_options_compatibility():
147
+ """Test command options compatibility with TypeScript version."""
148
+ print("\n=== Testing Command Options Compatibility ===")
149
+
150
+ from grasp_sdk.models import ICommandOptions
151
+
152
+ # Test that ICommandOptions supports 'inBackground' parameter
153
+ options: ICommandOptions = {
154
+ 'inBackground': True,
155
+ 'timeout': 5000,
156
+ 'cwd': '/tmp',
157
+ 'nohup': False
158
+ }
159
+
160
+ print(f"✓ Command options created: {options}")
161
+ print("✓ 'inBackground' parameter is supported in ICommandOptions")
162
+
163
+ # Test that we can access the inBackground option
164
+ background_value = options.get('inBackground', False)
165
+ assert background_value == True, "inBackground option should be accessible"
166
+ print("✓ inBackground option accessible and correct")
167
+
168
+ print("✓ All command options compatibility tests passed!")
169
+
170
+
171
+ async def main():
172
+ """Run all tests."""
173
+ print("Starting Terminal Service Update Tests...")
174
+
175
+ try:
176
+ await test_streamable_command_result()
177
+ await test_terminal_service()
178
+ await test_command_options_compatibility()
179
+
180
+ print("\n🎉 All tests passed! Terminal service updates are working correctly.")
181
+ print("\n📋 Summary of updates:")
182
+ print(" • Added kill() method to StreamableCommandResult")
183
+ print(" • Added response.json() compatibility method")
184
+ print(" • Maintained ICommandOptions to use 'inBackground' for Python compatibility")
185
+ print(" • Improved error handling and WebSocket management")
186
+ print(" • Synchronized timeout values with TypeScript version")
187
+
188
+ except Exception as e:
189
+ print(f"\n❌ Test failed: {e}")
190
+ import traceback
191
+ traceback.print_exc()
192
+ sys.exit(1)
193
+
194
+
195
+ if __name__ == '__main__':
196
+ asyncio.run(main())
grasp_sdk/__init__.py CHANGED
@@ -1,291 +1,183 @@
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
+ GraspTerminal,
20
+ GraspSession,
21
+ Grasp,
22
+ _servers,
23
+ shutdown,
25
24
  )
26
25
 
27
- __version__ = "0.1.8"
26
+ # Import CDPConnection for type annotation
27
+ from .services.browser import CDPConnection
28
+
29
+ __version__ = "0.2.0a1"
28
30
  __author__ = "Grasp Team"
29
31
  __email__ = "team@grasp.dev"
30
32
 
33
+ # Global server registry (re-exported from grasp module)
34
+ _servers: Dict[str, GraspServer] = _servers
31
35
 
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
-
36
+ # Deprecated functions for backward compatibility
216
37
  async def launch_browser(
217
38
  options: Optional[Dict[str, Any]] = None
218
- ) -> Dict[str, Any]:
39
+ ) -> CDPConnection:
219
40
  """Launch a browser instance.
220
41
 
42
+ .. deprecated::
43
+ Use grasp.launch() instead. This method will be removed in a future version.
44
+
221
45
  Args:
222
46
  options: Launch options including type, headless, adblock settings
223
47
 
224
48
  Returns:
225
49
  Dictionary containing connection information
226
- """
50
+ """
51
+ import warnings
52
+ warnings.warn(
53
+ "launch_browser() is deprecated. Use grasp.launch() instead.",
54
+ DeprecationWarning,
55
+ stacklevel=2
56
+ )
227
57
  # Create server instance
228
58
  server = GraspServer(options)
229
-
59
+
230
60
  connection = await server.create_browser_task()
231
-
232
- # Register server
233
- if connection['id']:
234
- _servers[connection['id']] = server
235
-
61
+
236
62
  return connection
237
63
 
238
64
 
239
- async def _graceful_shutdown(signal_name: str) -> None:
240
- """Handle graceful shutdown.
65
+ async def _graceful_shutdown(reason: str = 'SIGTERM') -> None:
66
+ """Gracefully shutdown all servers.
241
67
 
242
68
  Args:
243
- signal_name: Name of the signal received
69
+ reason: Reason for shutdown
244
70
  """
245
- print(f'Received {signal_name}, starting cleanup...')
71
+ import os
72
+ logger = get_logger().child('shutdown')
73
+ logger.info(f'Graceful shutdown initiated: {reason}')
74
+
75
+ if not _servers:
76
+ logger.info('No active servers to shutdown')
77
+ # 强制终止进程
78
+ os._exit(0)
79
+ return
246
80
 
247
- # Cleanup all GraspServer instances
248
- for server_id in list(_servers.keys()):
249
- await _servers[server_id].cleanup()
250
- del _servers[server_id]
81
+ logger.info(f'Shutting down {len(_servers)} active servers...')
251
82
 
252
- print('All servers cleaned up, exiting...')
253
- sys.exit(0)
83
+ # Create cleanup tasks for all servers
84
+ cleanup_tasks = []
85
+ for server_id, server in _servers.items():
86
+ logger.info(f'Scheduling cleanup for server {server_id}')
87
+ cleanup_tasks.append(server.cleanup())
88
+
89
+ # Wait for all cleanups to complete
90
+ try:
91
+ await asyncio.gather(*cleanup_tasks, return_exceptions=True)
92
+ logger.info('All servers cleaned up successfully')
93
+ except Exception as error:
94
+ logger.error(f'Error during cleanup: {error}')
95
+
96
+ # Clear the servers registry
97
+ _servers.clear()
98
+ logger.info('Graceful shutdown completed')
99
+
100
+ # 强制终止进程,确保不会被其他异步任务阻塞
101
+ os._exit(0)
254
102
 
255
103
 
256
104
  def _setup_signal_handlers() -> None:
257
105
  """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
106
+ def signal_handler(signame):
107
+ logger = get_logger().child('signal')
108
+ logger.info(f'Received signal {signame}, initiating graceful shutdown...')
109
+
110
+ # 创建关闭任务
111
+ asyncio.create_task(_graceful_shutdown(signame))
112
+
113
+ # 尝试使用 asyncio 的信号处理器(更适合异步环境)
114
+ try:
115
+ loop = asyncio.get_running_loop()
116
+ for sig in [signal.SIGTERM, signal.SIGINT]:
117
+ loop.add_signal_handler(sig, lambda s=sig: signal_handler(signal.Signals(s).name))
118
+ except RuntimeError:
119
+ # 如果没有运行的事件循环,使用传统方式
120
+ def fallback_handler(signum, frame):
121
+ signal_name = signal.Signals(signum).name
122
+ logger = get_logger().child('signal')
123
+ logger.info(f'Received {signal_name}, but no event loop is running')
124
+ import os
125
+ os._exit(1)
126
+
127
+ signal.signal(signal.SIGTERM, fallback_handler)
128
+ signal.signal(signal.SIGINT, fallback_handler)
265
129
 
266
130
 
267
- # Setup signal handlers on import
131
+ async def shutdown(connection: Optional[str] = None) -> None:
132
+ """Shutdown Grasp servers (deprecated).
133
+
134
+ Args:
135
+ connection: Optional connection ID to shutdown specific server
136
+
137
+ Deprecated:
138
+ This function is deprecated. Use server.cleanup() or context managers instead.
139
+ """
140
+ warnings.warn(
141
+ "shutdown() is deprecated. Use server.cleanup() or context managers instead.",
142
+ DeprecationWarning,
143
+ stacklevel=2
144
+ )
145
+
146
+ logger = get_logger().child('shutdown')
147
+
148
+ if connection:
149
+ # Shutdown specific connection
150
+ if connection in _servers:
151
+ logger.info(f'Shutting down server {connection}')
152
+ await _servers[connection].cleanup()
153
+ else:
154
+ logger.warn(f'Server {connection} not found')
155
+ else:
156
+ # Shutdown all servers
157
+ await _graceful_shutdown('USER_SHUTDOWN')
158
+
159
+
160
+ # Setup signal handlers when module is imported
268
161
  _setup_signal_handlers()
269
162
 
270
-
271
163
  # Export all public APIs
272
164
  __all__ = [
273
165
  'GraspServer',
166
+ 'Grasp',
167
+ 'GraspSession',
168
+ 'GraspBrowser',
169
+ 'GraspTerminal',
274
170
  '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',
171
+ 'shutdown',
285
172
  ]
286
173
 
287
-
288
174
  # Default export equivalent
289
175
  default = {
290
176
  'GraspServer': GraspServer,
177
+ 'Grasp': Grasp,
178
+ 'GraspSession': GraspSession,
179
+ 'GraspBrowser': GraspBrowser,
180
+ 'GraspTerminal': GraspTerminal,
181
+ 'launch_browser': launch_browser,
182
+ 'shutdown': shutdown,
291
183
  }
@@ -0,0 +1,26 @@
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 .terminal import GraspTerminal
8
+ from .session import GraspSession
9
+ from .index import Grasp
10
+ from .utils import (
11
+ _servers,
12
+ shutdown,
13
+ )
14
+
15
+ # Export the global servers registry
16
+ _servers: Dict[str, GraspServer] = _servers
17
+
18
+ __all__ = [
19
+ 'GraspServer',
20
+ 'GraspBrowser',
21
+ 'GraspTerminal',
22
+ 'GraspSession',
23
+ 'Grasp',
24
+ 'shutdown',
25
+ '_servers',
26
+ ]