grasp-sdk 0.1.0__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.

@@ -0,0 +1,227 @@
1
+ """Authentication utilities for Grasp SDK Python implementation.
2
+
3
+ This module provides authentication and key verification functionality
4
+ for the Grasp platform.
5
+ """
6
+
7
+ import asyncio
8
+ from typing import Dict, Any, Optional, TYPE_CHECKING
9
+ from urllib.parse import quote
10
+
11
+ try:
12
+ import aiohttp
13
+ except ImportError:
14
+ aiohttp = None
15
+
16
+ if TYPE_CHECKING:
17
+ from aiohttp import ClientSession
18
+
19
+ from ..models import ISandboxConfig
20
+
21
+
22
+ class AuthError(Exception):
23
+ """Exception raised for authentication errors."""
24
+ pass
25
+
26
+
27
+ class KeyVerificationError(AuthError):
28
+ """Exception raised when key verification fails."""
29
+ pass
30
+
31
+
32
+ async def login(token: str) -> Dict[str, Any]:
33
+ """Authenticates with the Grasp platform using a token.
34
+
35
+ Args:
36
+ token: Authentication token to verify
37
+
38
+ Returns:
39
+ Dict[str, Any]: Response from the authentication API
40
+
41
+ Raises:
42
+ AuthError: If authentication fails
43
+ ImportError: If aiohttp is not installed
44
+ """
45
+ if aiohttp is None:
46
+ raise ImportError("aiohttp is required for authentication. Install with: pip install aiohttp")
47
+
48
+ url = f"https://d1toyru2btfpfr.cloudfront.net/api/key/verify?token={quote(token)}"
49
+
50
+ try:
51
+ if aiohttp is None:
52
+ raise ImportError("aiohttp is required for authentication. Install with: pip install aiohttp")
53
+
54
+ async with aiohttp.ClientSession() as session:
55
+ async with session.get(url) as response:
56
+ if response.status == 200:
57
+ return await response.json()
58
+ elif response.status == 401:
59
+ raise AuthError("Invalid authentication token")
60
+ elif response.status == 403:
61
+ raise AuthError("Access forbidden - token may be expired")
62
+ else:
63
+ response.raise_for_status()
64
+ return await response.json()
65
+ except Exception as e:
66
+ if aiohttp is not None and isinstance(e, aiohttp.ClientError):
67
+ raise AuthError(f"Network error during authentication: {str(e)}")
68
+ else:
69
+ raise AuthError(f"Unexpected error during authentication: {str(e)}")
70
+
71
+
72
+ async def verify(config: ISandboxConfig) -> Dict[str, Any]:
73
+ """Verifies the sandbox configuration and authenticates.
74
+
75
+ Args:
76
+ config: Sandbox configuration containing the API key
77
+
78
+ Returns:
79
+ Dict[str, Any]: Authentication response
80
+
81
+ Raises:
82
+ KeyVerificationError: If the key is missing or invalid
83
+ AuthError: If authentication fails
84
+ """
85
+ if not config['key']:
86
+ raise KeyVerificationError('Grasp key is required')
87
+
88
+ try:
89
+ return await login(config['key'])
90
+ except AuthError:
91
+ raise
92
+ except Exception as e:
93
+ raise KeyVerificationError(f"Key verification failed: {str(e)}")
94
+
95
+
96
+ def verify_sync(config: ISandboxConfig) -> Dict[str, Any]:
97
+ """Synchronous wrapper for the verify function.
98
+
99
+ Args:
100
+ config: Sandbox configuration containing the API key
101
+
102
+ Returns:
103
+ Dict[str, Any]: Authentication response
104
+
105
+ Raises:
106
+ KeyVerificationError: If the key is missing or invalid
107
+ AuthError: If authentication fails
108
+ """
109
+ try:
110
+ loop = asyncio.get_event_loop()
111
+ except RuntimeError:
112
+ loop = asyncio.new_event_loop()
113
+ asyncio.set_event_loop(loop)
114
+
115
+ return loop.run_until_complete(verify(config))
116
+
117
+
118
+ async def validate_token(token: str) -> bool:
119
+ """Validates a token without raising exceptions.
120
+
121
+ Args:
122
+ token: Token to validate
123
+
124
+ Returns:
125
+ bool: True if token is valid, False otherwise
126
+ """
127
+ try:
128
+ await login(token)
129
+ return True
130
+ except (AuthError, Exception):
131
+ return False
132
+
133
+
134
+ def validate_token_sync(token: str) -> bool:
135
+ """Synchronous wrapper for token validation.
136
+
137
+ Args:
138
+ token: Token to validate
139
+
140
+ Returns:
141
+ bool: True if token is valid, False otherwise
142
+ """
143
+ try:
144
+ loop = asyncio.get_event_loop()
145
+ except RuntimeError:
146
+ loop = asyncio.new_event_loop()
147
+ asyncio.set_event_loop(loop)
148
+
149
+ return loop.run_until_complete(validate_token(token))
150
+
151
+
152
+ class AuthManager:
153
+ """Authentication manager for handling multiple tokens and sessions."""
154
+
155
+ def __init__(self):
156
+ """Initialize the authentication manager."""
157
+ if aiohttp is None:
158
+ raise ImportError("aiohttp is required for AuthManager. Install with: pip install aiohttp")
159
+ self._verified_tokens: Dict[str, Dict[str, Any]] = {}
160
+ self._session: Optional['ClientSession'] = None
161
+
162
+ async def __aenter__(self):
163
+ """Async context manager entry."""
164
+ if aiohttp is None:
165
+ raise ImportError("aiohttp is required for AuthManager. Install with: pip install aiohttp")
166
+ self._session = aiohttp.ClientSession()
167
+ return self
168
+
169
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
170
+ """Async context manager exit."""
171
+ if self._session:
172
+ await self._session.close()
173
+
174
+ async def verify_token(self, token: str, force_refresh: bool = False) -> Dict[str, Any]:
175
+ """Verify a token with caching support.
176
+
177
+ Args:
178
+ token: Token to verify
179
+ force_refresh: Whether to force a new verification
180
+
181
+ Returns:
182
+ Dict[str, Any]: Verification response
183
+
184
+ Raises:
185
+ AuthError: If verification fails
186
+ """
187
+ if not force_refresh and token in self._verified_tokens:
188
+ return self._verified_tokens[token]
189
+
190
+ url = f"https://d1toyru2btfpfr.cloudfront.net/api/key/verify?token={quote(token)}"
191
+
192
+ if not self._session:
193
+ raise AuthError("AuthManager not properly initialized. Use as async context manager.")
194
+
195
+ try:
196
+ async with self._session.get(url) as response:
197
+ if response.status == 200:
198
+ result = await response.json()
199
+ self._verified_tokens[token] = result
200
+ return result
201
+ elif response.status == 401:
202
+ raise AuthError("Invalid authentication token")
203
+ elif response.status == 403:
204
+ raise AuthError("Access forbidden - token may be expired")
205
+ else:
206
+ response.raise_for_status()
207
+ return await response.json()
208
+ except Exception as e:
209
+ if aiohttp is not None and isinstance(e, aiohttp.ClientError):
210
+ raise AuthError(f"Network error during authentication: {str(e)}")
211
+ else:
212
+ raise AuthError(f"Unexpected error during authentication: {str(e)}")
213
+
214
+ def clear_cache(self) -> None:
215
+ """Clear the token verification cache."""
216
+ self._verified_tokens.clear()
217
+
218
+ def is_token_cached(self, token: str) -> bool:
219
+ """Check if a token is cached.
220
+
221
+ Args:
222
+ token: Token to check
223
+
224
+ Returns:
225
+ bool: True if token is cached
226
+ """
227
+ return token in self._verified_tokens
@@ -0,0 +1,150 @@
1
+ """Configuration management for Grasp SDK Python implementation.
2
+
3
+ This module provides configuration loading from environment variables
4
+ and default configuration constants.
5
+ """
6
+
7
+ import os
8
+ from typing import Dict, Any, Optional
9
+ from dotenv import load_dotenv
10
+ from ..models import ISandboxConfig, IBrowserConfig
11
+
12
+ # Load environment variables from .env.grasp file
13
+ load_dotenv('.env.grasp')
14
+
15
+
16
+ def get_config() -> Dict[str, Any]:
17
+ """Gets application configuration from environment variables.
18
+
19
+ Returns:
20
+ Dict[str, Any]: Application configuration object containing
21
+ sandbox and logger configurations.
22
+ """
23
+ return {
24
+ 'sandbox': {
25
+ 'key': os.getenv('GRASP_KEY', ''),
26
+ 'templateId': 'playwright-pnpm-template',
27
+ 'timeout': int(os.getenv('GRASP_SERVICE_TIMEOUT', '900000')),
28
+ 'debug': os.getenv('GRASP_DEBUG', 'false').lower() == 'true',
29
+ },
30
+ 'logger': {
31
+ 'level': os.getenv('GRASP_LOG_LEVEL', 'info'),
32
+ 'console': True,
33
+ 'file': os.getenv('GRASP_LOG_FILE'),
34
+ },
35
+ }
36
+
37
+
38
+ def get_sandbox_config() -> ISandboxConfig:
39
+ """Gets sandbox-specific configuration.
40
+
41
+ Returns:
42
+ ISandboxConfig: Sandbox configuration object.
43
+ """
44
+ config = get_config()
45
+ sandbox_config = ISandboxConfig(
46
+ key=config['sandbox']['key'],
47
+ templateId=config['sandbox']['templateId'],
48
+ timeout=config['sandbox']['timeout'],
49
+ debug=config['sandbox'].get('debug', False),
50
+ )
51
+
52
+ # Only add workspace if it exists
53
+ workspace = os.getenv('GRASP_WORKSPACE')
54
+ if workspace:
55
+ sandbox_config['workspace'] = workspace
56
+
57
+ return sandbox_config
58
+
59
+
60
+ def get_browser_config() -> IBrowserConfig:
61
+ """Gets browser-specific configuration.
62
+
63
+ Returns:
64
+ IBrowserConfig: Browser configuration object.
65
+ """
66
+ return IBrowserConfig(
67
+ cdpPort=int(os.getenv('GRASP_CDP_PORT', '9222')),
68
+ args=[
69
+ '--no-sandbox',
70
+ '--disable-setuid-sandbox',
71
+ '--disable-dev-shm-usage',
72
+ '--disable-gpu',
73
+ '--remote-debugging-port=9222',
74
+ '--remote-debugging-address=0.0.0.0',
75
+ ],
76
+ headless=os.getenv('GRASP_HEADLESS', 'true').lower() == 'true',
77
+ launchTimeout=int(os.getenv('GRASP_LAUNCH_TIMEOUT', '30000')),
78
+ envs={
79
+ 'PLAYWRIGHT_BROWSERS_PATH': '0',
80
+ 'DISPLAY': ':99',
81
+ },
82
+ )
83
+
84
+
85
+ # Default configuration constants
86
+ DEFAULT_CONFIG = {
87
+ 'PLAYWRIGHT_BROWSERS_PATH': '0',
88
+ 'WORKING_DIRECTORY': '/home/user',
89
+ 'SCREENSHOT_PATH': '/home/user',
90
+ 'DEFAULT_VIEWPORT': {
91
+ 'width': 1280,
92
+ 'height': 720,
93
+ },
94
+ }
95
+
96
+
97
+ # Environment variable names for easy reference
98
+ ENV_VARS = {
99
+ 'GRASP_KEY': 'GRASP_KEY',
100
+ 'GRASP_WORKSPACE': 'GRASP_WORKSPACE',
101
+ 'GRASP_SERVICE_TIMEOUT': 'GRASP_SERVICE_TIMEOUT',
102
+ 'GRASP_DEBUG': 'GRASP_DEBUG',
103
+ 'GRASP_LOG_LEVEL': 'GRASP_LOG_LEVEL',
104
+ 'GRASP_LOG_FILE': 'GRASP_LOG_FILE',
105
+ 'GRASP_CDP_PORT': 'GRASP_CDP_PORT',
106
+ 'GRASP_HEADLESS': 'GRASP_HEADLESS',
107
+ 'GRASP_LAUNCH_TIMEOUT': 'GRASP_LAUNCH_TIMEOUT',
108
+ }
109
+
110
+
111
+ def validate_config() -> bool:
112
+ """Validates that required configuration is present.
113
+
114
+ Returns:
115
+ bool: True if configuration is valid, False otherwise.
116
+ """
117
+ config = get_config()
118
+
119
+ # Check required fields
120
+ if not config['sandbox']['key']:
121
+ return False
122
+
123
+ # Validate timeout values
124
+ if config['sandbox']['timeout'] <= 0:
125
+ return False
126
+
127
+ return True
128
+
129
+
130
+ def get_env_var(key: str, default: Optional[str] = None) -> Optional[str]:
131
+ """Gets an environment variable with optional default.
132
+
133
+ Args:
134
+ key: Environment variable name
135
+ default: Default value if not found
136
+
137
+ Returns:
138
+ str or None: Environment variable value or default
139
+ """
140
+ return os.getenv(key, default)
141
+
142
+
143
+ def set_env_var(key: str, value: str) -> None:
144
+ """Sets an environment variable.
145
+
146
+ Args:
147
+ key: Environment variable name
148
+ value: Environment variable value
149
+ """
150
+ os.environ[key] = value
@@ -0,0 +1,233 @@
1
+ """Logging utilities for Grasp SDK Python implementation.
2
+
3
+ This module provides a structured logging system with support for
4
+ different log levels, console output, and file logging.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ import sys
10
+ from datetime import datetime
11
+ from typing import Any, Dict, Optional, Union
12
+ from enum import IntEnum
13
+
14
+
15
+ class LogLevel(IntEnum):
16
+ """Log levels with numeric values for comparison."""
17
+ DEBUG = 0
18
+ INFO = 1
19
+ WARN = 2
20
+ ERROR = 3
21
+
22
+
23
+ class Logger:
24
+ """Logger class for structured logging."""
25
+
26
+ def __init__(self, config: Dict[str, Any]):
27
+ """Initialize logger with configuration.
28
+
29
+ Args:
30
+ config: Logger configuration dictionary containing:
31
+ - level: Log level ('debug', 'info', 'warn', 'error')
32
+ - console: Whether to output to console
33
+ - file: Optional file path for logging
34
+ """
35
+ self.config = config
36
+ self.current_level = LogLevel[config['level'].upper()]
37
+ self._setup_file_logger()
38
+
39
+ def _setup_file_logger(self) -> None:
40
+ """Setup file logging if configured."""
41
+ if self.config.get('file'):
42
+ # Configure Python's built-in logging for file output
43
+ logging.basicConfig(
44
+ filename=self.config['file'],
45
+ level=getattr(logging, self.config['level'].upper()),
46
+ format='%(asctime)s [%(levelname)s] %(message)s',
47
+ datefmt='%Y-%m-%dT%H:%M:%S.%fZ'
48
+ )
49
+ self.file_logger = logging.getLogger('grasp_file')
50
+ else:
51
+ self.file_logger = None
52
+
53
+ def _format_message(self, level: str, message: str, data: Optional[Any] = None) -> str:
54
+ """Formats log message with timestamp and level.
55
+
56
+ Args:
57
+ level: Log level string
58
+ message: Log message
59
+ data: Additional data to log
60
+
61
+ Returns:
62
+ str: Formatted log string
63
+ """
64
+ timestamp = datetime.utcnow().isoformat() + 'Z'
65
+ data_str = f' {json.dumps(data)}' if data is not None else ''
66
+ return f'[{timestamp}] [{level.upper()}] {message}{data_str}'
67
+
68
+ def _log(self, level: str, message: str, data: Optional[Any] = None) -> None:
69
+ """Logs a message if the level is enabled.
70
+
71
+ Args:
72
+ level: Log level string
73
+ message: Log message
74
+ data: Additional data to log
75
+ """
76
+ level_enum = LogLevel[level.upper()]
77
+ if level_enum < self.current_level:
78
+ return
79
+
80
+ formatted_message = self._format_message(level, message, data)
81
+
82
+ # Console output
83
+ if self.config['console']:
84
+ if level == 'debug':
85
+ print(formatted_message, file=sys.stdout)
86
+ elif level == 'info':
87
+ print(formatted_message, file=sys.stdout)
88
+ elif level == 'warn':
89
+ print(formatted_message, file=sys.stderr)
90
+ elif level == 'error':
91
+ print(formatted_message, file=sys.stderr)
92
+
93
+ # File output
94
+ if self.file_logger:
95
+ if level == 'debug':
96
+ self.file_logger.debug(message, extra={'data': data} if data else None)
97
+ elif level == 'info':
98
+ self.file_logger.info(message, extra={'data': data} if data else None)
99
+ elif level == 'warn':
100
+ self.file_logger.warning(message, extra={'data': data} if data else None)
101
+ elif level == 'error':
102
+ self.file_logger.error(message, extra={'data': data} if data else None)
103
+
104
+ def debug(self, message: str, data: Optional[Any] = None) -> None:
105
+ """Logs debug message.
106
+
107
+ Args:
108
+ message: Debug message
109
+ data: Additional data
110
+ """
111
+ self._log('debug', message, data)
112
+
113
+ def info(self, message: str, data: Optional[Any] = None) -> None:
114
+ """Logs info message.
115
+
116
+ Args:
117
+ message: Info message
118
+ data: Additional data
119
+ """
120
+ self._log('info', message, data)
121
+
122
+ def warn(self, message: str, data: Optional[Any] = None) -> None:
123
+ """Logs warning message.
124
+
125
+ Args:
126
+ message: Warning message
127
+ data: Additional data
128
+ """
129
+ self._log('warn', message, data)
130
+
131
+ def error(self, message: str, data: Optional[Any] = None) -> None:
132
+ """Logs error message.
133
+
134
+ Args:
135
+ message: Error message
136
+ data: Additional data
137
+ """
138
+ self._log('error', message, data)
139
+
140
+ def child(self, context: str) -> 'Logger':
141
+ """Creates a child logger with additional context.
142
+
143
+ Args:
144
+ context: Context to add to all log messages
145
+
146
+ Returns:
147
+ Logger: New logger instance with context
148
+ """
149
+ child_logger = Logger(self.config)
150
+
151
+ # Override the _log method to add context
152
+ original_log = child_logger._log
153
+
154
+ def contextual_log(level: str, message: str, data: Optional[Any] = None) -> None:
155
+ original_log(level, f'[{context}] {message}', data)
156
+
157
+ child_logger._log = contextual_log
158
+ return child_logger
159
+
160
+
161
+ # Global logger instance
162
+ _default_logger: Optional[Logger] = None
163
+
164
+
165
+ def init_logger(config: Dict[str, Any]) -> None:
166
+ """Initializes the default logger.
167
+
168
+ Args:
169
+ config: Logger configuration dictionary
170
+ """
171
+ global _default_logger
172
+ _default_logger = Logger(config)
173
+
174
+
175
+ def get_logger() -> Logger:
176
+ """Gets the default logger instance.
177
+
178
+ Returns:
179
+ Logger: Default logger instance
180
+
181
+ Raises:
182
+ RuntimeError: If logger is not initialized
183
+ """
184
+ if _default_logger is None:
185
+ raise RuntimeError('Logger not initialized. Call init_logger() first.')
186
+ return _default_logger
187
+
188
+
189
+ def get_default_logger() -> Logger:
190
+ """Gets a default logger with basic configuration.
191
+
192
+ Returns:
193
+ Logger: Logger with default configuration
194
+ """
195
+ default_config = {
196
+ 'level': 'info',
197
+ 'console': True,
198
+ 'file': None,
199
+ }
200
+ return Logger(default_config)
201
+
202
+
203
+ # Convenience functions for quick logging
204
+ def debug(message: str, data: Optional[Any] = None) -> None:
205
+ """Quick debug logging function."""
206
+ try:
207
+ get_logger().debug(message, data)
208
+ except RuntimeError:
209
+ get_default_logger().debug(message, data)
210
+
211
+
212
+ def info(message: str, data: Optional[Any] = None) -> None:
213
+ """Quick info logging function."""
214
+ try:
215
+ get_logger().info(message, data)
216
+ except RuntimeError:
217
+ get_default_logger().info(message, data)
218
+
219
+
220
+ def warn(message: str, data: Optional[Any] = None) -> None:
221
+ """Quick warning logging function."""
222
+ try:
223
+ get_logger().warn(message, data)
224
+ except RuntimeError:
225
+ get_default_logger().warn(message, data)
226
+
227
+
228
+ def error(message: str, data: Optional[Any] = None) -> None:
229
+ """Quick error logging function."""
230
+ try:
231
+ get_logger().error(message, data)
232
+ except RuntimeError:
233
+ get_default_logger().error(message, data)