hopx-ai 0.1.11__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.
hopx_ai/env_vars.py ADDED
@@ -0,0 +1,242 @@
1
+ """Environment variables resource for Bunnyshell Sandboxes."""
2
+
3
+ from typing import Dict, Optional
4
+ import logging
5
+ from ._agent_client import AgentHTTPClient
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class EnvironmentVariables:
11
+ """
12
+ Environment variables resource.
13
+
14
+ Provides methods for managing environment variables inside the sandbox at runtime.
15
+
16
+ Features:
17
+ - Get all environment variables
18
+ - Set/replace all environment variables
19
+ - Update specific environment variables (merge)
20
+ - Delete individual environment variables
21
+
22
+ Example:
23
+ >>> sandbox = Sandbox.create(template="code-interpreter")
24
+ >>>
25
+ >>> # Get all environment variables
26
+ >>> env = sandbox.env.get_all()
27
+ >>> print(env)
28
+ >>>
29
+ >>> # Set multiple variables (replaces all)
30
+ >>> sandbox.env.set_all({
31
+ ... "API_KEY": "sk-prod-xyz",
32
+ ... "DATABASE_URL": "postgres://localhost/db"
33
+ ... })
34
+ >>>
35
+ >>> # Update specific variables (merge)
36
+ >>> sandbox.env.update({
37
+ ... "NODE_ENV": "production",
38
+ ... "DEBUG": "false"
39
+ ... })
40
+ >>>
41
+ >>> # Delete a variable
42
+ >>> sandbox.env.delete("DEBUG")
43
+ """
44
+
45
+ def __init__(self, client: AgentHTTPClient):
46
+ """
47
+ Initialize EnvironmentVariables resource.
48
+
49
+ Args:
50
+ client: Shared agent HTTP client
51
+ """
52
+ self._client = client
53
+ logger.debug("EnvironmentVariables resource initialized")
54
+
55
+ def get_all(self, *, timeout: Optional[int] = None) -> Dict[str, str]:
56
+ """
57
+ Get all environment variables.
58
+
59
+ Args:
60
+ timeout: Request timeout in seconds (overrides default)
61
+
62
+ Returns:
63
+ Dictionary of environment variables
64
+
65
+ Example:
66
+ >>> env = sandbox.env.get_all()
67
+ >>> print(env.get("PATH"))
68
+ >>> print(env.get("HOME"))
69
+ """
70
+ logger.debug("Getting all environment variables")
71
+
72
+ response = self._client.get(
73
+ "/env",
74
+ operation="get environment variables",
75
+ timeout=timeout
76
+ )
77
+
78
+ # response is httpx.Response, need to parse JSON
79
+ try:
80
+ data = response.json()
81
+ return data.get("env_vars", {})
82
+ except Exception:
83
+ # If response is empty or not JSON, return empty dict
84
+ return {}
85
+
86
+ def set_all(
87
+ self,
88
+ env_vars: Dict[str, str],
89
+ *,
90
+ timeout: Optional[int] = None
91
+ ) -> Dict[str, str]:
92
+ """
93
+ Set/replace all environment variables.
94
+
95
+ This replaces ALL existing environment variables with the provided ones.
96
+ Use update() if you want to merge instead.
97
+
98
+ Args:
99
+ env_vars: Dictionary of environment variables to set
100
+ timeout: Request timeout in seconds (overrides default)
101
+
102
+ Returns:
103
+ Updated dictionary of all environment variables
104
+
105
+ Example:
106
+ >>> sandbox.env.set_all({
107
+ ... "API_KEY": "sk-prod-xyz",
108
+ ... "DATABASE_URL": "postgres://localhost/db",
109
+ ... "NODE_ENV": "production"
110
+ ... })
111
+ """
112
+ logger.debug(f"Setting {len(env_vars)} environment variables (replace all)")
113
+
114
+ response = self._client.put(
115
+ "/env",
116
+ json={"env_vars": env_vars},
117
+ operation="set environment variables",
118
+ timeout=timeout
119
+ )
120
+
121
+ # Agent returns 204 No Content (empty response) on success
122
+ if response.status_code == 204 or not response.content:
123
+ return env_vars # Return what we set
124
+
125
+ data = response.json()
126
+ return data.get("env_vars", {})
127
+
128
+ def update(
129
+ self,
130
+ env_vars: Dict[str, str],
131
+ *,
132
+ timeout: Optional[int] = None
133
+ ) -> Dict[str, str]:
134
+ """
135
+ Update specific environment variables (merge).
136
+
137
+ This merges the provided variables with existing ones.
138
+ Existing variables not specified are preserved.
139
+
140
+ Args:
141
+ env_vars: Dictionary of environment variables to update/add
142
+ timeout: Request timeout in seconds (overrides default)
143
+
144
+ Returns:
145
+ Updated dictionary of all environment variables
146
+
147
+ Example:
148
+ >>> # Add/update specific variables
149
+ >>> sandbox.env.update({
150
+ ... "NODE_ENV": "production",
151
+ ... "DEBUG": "false"
152
+ ... })
153
+ >>>
154
+ >>> # Existing variables like PATH, HOME, etc. are preserved
155
+ """
156
+ logger.debug(f"Updating {len(env_vars)} environment variables (merge)")
157
+
158
+ response = self._client.patch(
159
+ "/env",
160
+ json={"env_vars": env_vars},
161
+ operation="update environment variables",
162
+ timeout=timeout
163
+ )
164
+
165
+ # Handle empty response
166
+ if response.content:
167
+ data = response.json()
168
+ return data.get("env_vars", {})
169
+ else:
170
+ # Return success (empty response means success for PATCH)
171
+ return env_vars
172
+
173
+ def delete(self, key: str, *, timeout: Optional[int] = None) -> Dict[str, str]:
174
+ """
175
+ Delete an environment variable.
176
+
177
+ Args:
178
+ key: Environment variable name to delete
179
+ timeout: Request timeout in seconds (overrides default)
180
+
181
+ Returns:
182
+ Updated dictionary of all environment variables
183
+
184
+ Example:
185
+ >>> sandbox.env.delete("DEBUG")
186
+ >>> sandbox.env.delete("TEMP_TOKEN")
187
+ """
188
+ logger.debug(f"Deleting environment variable: {key}")
189
+
190
+ response = self._client.delete(
191
+ f"/env/{key}",
192
+ operation="delete environment variable",
193
+ context={"key": key},
194
+ timeout=timeout
195
+ )
196
+
197
+ data = response.json()
198
+ return data.get("env_vars", {})
199
+
200
+ def get(self, key: str, default: Optional[str] = None) -> Optional[str]:
201
+ """
202
+ Get a specific environment variable value.
203
+
204
+ Convenience method that fetches all variables and returns the requested one.
205
+
206
+ Args:
207
+ key: Environment variable name
208
+ default: Default value if variable doesn't exist
209
+
210
+ Returns:
211
+ Variable value or default
212
+
213
+ Example:
214
+ >>> api_key = sandbox.env.get("API_KEY")
215
+ >>> db_url = sandbox.env.get("DATABASE_URL", "postgres://localhost/db")
216
+ """
217
+ env_vars = self.get_all()
218
+ return env_vars.get(key, default)
219
+
220
+ def set(self, key: str, value: str, *, timeout: Optional[int] = None) -> Dict[str, str]:
221
+ """
222
+ Set a single environment variable.
223
+
224
+ Convenience method that updates just one variable (merge).
225
+
226
+ Args:
227
+ key: Environment variable name
228
+ value: Environment variable value
229
+ timeout: Request timeout in seconds (overrides default)
230
+
231
+ Returns:
232
+ Updated dictionary of all environment variables
233
+
234
+ Example:
235
+ >>> sandbox.env.set("API_KEY", "sk-prod-xyz")
236
+ >>> sandbox.env.set("NODE_ENV", "production")
237
+ """
238
+ return self.update({key: value}, timeout=timeout)
239
+
240
+ def __repr__(self) -> str:
241
+ return f"<EnvironmentVariables client={self._client}>"
242
+
hopx_ai/errors.py ADDED
@@ -0,0 +1,249 @@
1
+ """HOPX.AI SDK exceptions."""
2
+
3
+ from typing import Optional, Dict, Any, List
4
+
5
+ # Import ErrorCode enum from generated models for type-safe error codes
6
+ from .models import ErrorCode
7
+
8
+ __all__ = [
9
+ "ErrorCode", # Re-export for convenience
10
+ "HopxError",
11
+ "APIError",
12
+ "AuthenticationError",
13
+ "NotFoundError",
14
+ "ValidationError",
15
+ "ResourceLimitError",
16
+ "AgentError",
17
+ "FileNotFoundError",
18
+ "FileOperationError",
19
+ "CodeExecutionError",
20
+ "CommandExecutionError",
21
+ "DesktopNotAvailableError",
22
+ ]
23
+
24
+
25
+ class HopxError(Exception):
26
+ """Base exception for all HOPX.AI SDK errors."""
27
+
28
+ def __init__(
29
+ self,
30
+ message: str,
31
+ *,
32
+ code: Optional[str] = None,
33
+ request_id: Optional[str] = None,
34
+ status_code: Optional[int] = None,
35
+ details: Optional[Dict[str, Any]] = None,
36
+ ):
37
+ super().__init__(message)
38
+ self.message = message
39
+ self.code = code
40
+ self.request_id = request_id
41
+ self.status_code = status_code
42
+ self.details = details or {}
43
+
44
+ def __str__(self) -> str:
45
+ parts = [self.message]
46
+ if self.code:
47
+ parts.append(f"(code: {self.code})")
48
+ if self.request_id:
49
+ parts.append(f"[request_id: {self.request_id}]")
50
+ return " ".join(parts)
51
+
52
+ def __repr__(self) -> str:
53
+ return f"{self.__class__.__name__}({self.message!r}, code={self.code!r})"
54
+
55
+
56
+ class APIError(HopxError):
57
+ """API request failed."""
58
+
59
+ def __init__(
60
+ self,
61
+ message: str,
62
+ *,
63
+ status_code: Optional[int] = None,
64
+ **kwargs
65
+ ):
66
+ super().__init__(message, **kwargs)
67
+ self.status_code = status_code
68
+
69
+
70
+ class AuthenticationError(APIError):
71
+ """Authentication failed (401)."""
72
+ pass
73
+
74
+
75
+ class NotFoundError(APIError):
76
+ """Resource not found (404)."""
77
+ pass
78
+
79
+
80
+ class ValidationError(APIError):
81
+ """Request validation failed (400)."""
82
+
83
+ def __init__(
84
+ self,
85
+ message: str,
86
+ *,
87
+ field: Optional[str] = None,
88
+ **kwargs
89
+ ):
90
+ super().__init__(message, **kwargs)
91
+ self.field = field
92
+
93
+
94
+ class RateLimitError(APIError):
95
+ """Rate limit exceeded (429)."""
96
+
97
+ def __init__(
98
+ self,
99
+ message: str,
100
+ *,
101
+ retry_after: Optional[int] = None,
102
+ **kwargs
103
+ ):
104
+ super().__init__(message, **kwargs)
105
+ self.retry_after = retry_after
106
+
107
+ def __str__(self) -> str:
108
+ msg = super().__str__()
109
+ if self.retry_after:
110
+ msg += f" (retry after {self.retry_after}s)"
111
+ return msg
112
+
113
+
114
+ class ResourceLimitError(APIError):
115
+ """Resource limit exceeded."""
116
+
117
+ def __init__(
118
+ self,
119
+ message: str,
120
+ *,
121
+ limit: Optional[int] = None,
122
+ current: Optional[int] = None,
123
+ available: Optional[int] = None,
124
+ upgrade_url: Optional[str] = None,
125
+ **kwargs
126
+ ):
127
+ super().__init__(message, **kwargs)
128
+ self.limit = limit
129
+ self.current = current
130
+ self.available = available
131
+ self.upgrade_url = upgrade_url
132
+
133
+ def __str__(self) -> str:
134
+ msg = super().__str__()
135
+ if self.limit and self.current:
136
+ msg += f" (current: {self.current}/{self.limit})"
137
+ if self.upgrade_url:
138
+ msg += f"\nUpgrade at: {self.upgrade_url}"
139
+ return msg
140
+
141
+
142
+ class ServerError(APIError):
143
+ """Server error (5xx)."""
144
+ pass
145
+
146
+
147
+ class NetworkError(BunnyshellError):
148
+ """Network communication failed."""
149
+ pass
150
+
151
+
152
+ class TimeoutError(NetworkError):
153
+ """Request timed out."""
154
+ pass
155
+
156
+
157
+ # =============================================================================
158
+ # AGENT OPERATION ERRORS
159
+ # =============================================================================
160
+
161
+ class AgentError(BunnyshellError):
162
+ """Base error for agent operations."""
163
+ pass
164
+
165
+
166
+ class FileNotFoundError(AgentError):
167
+ """File or directory not found in sandbox."""
168
+
169
+ def __init__(self, message: str = "File not found", path: Optional[str] = None, **kwargs):
170
+ # Use provided code or default
171
+ kwargs.setdefault('code', 'file_not_found')
172
+ super().__init__(message, **kwargs)
173
+ self.path = path
174
+
175
+
176
+ class FileOperationError(AgentError):
177
+ """File operation failed."""
178
+
179
+ def __init__(self, message: str = "File operation failed", operation: Optional[str] = None, **kwargs):
180
+ # Use provided code or default
181
+ kwargs.setdefault('code', 'file_operation_failed')
182
+ super().__init__(message, **kwargs)
183
+ self.operation = operation
184
+
185
+
186
+ class CodeExecutionError(AgentError):
187
+ """Code execution failed."""
188
+
189
+ def __init__(self, message: str = "Code execution failed", language: Optional[str] = None, **kwargs):
190
+ # Use provided code or default
191
+ kwargs.setdefault('code', 'code_execution_failed')
192
+ super().__init__(message, **kwargs)
193
+ self.language = language
194
+
195
+
196
+ class CommandExecutionError(AgentError):
197
+ """Command execution failed."""
198
+
199
+ def __init__(self, message: str = "Command execution failed", command: Optional[str] = None, **kwargs):
200
+ # Use provided code or default
201
+ kwargs.setdefault('code', 'command_execution_failed')
202
+ super().__init__(message, **kwargs)
203
+ self.command = command
204
+
205
+
206
+ class DesktopNotAvailableError(AgentError):
207
+ """Desktop automation not available in this sandbox."""
208
+
209
+ def __init__(
210
+ self,
211
+ message: str = "Desktop automation not available",
212
+ missing_dependencies: Optional[List[str]] = None,
213
+ **kwargs
214
+ ):
215
+ # Use provided code or default
216
+ kwargs.setdefault('code', 'desktop_not_available')
217
+ super().__init__(message, **kwargs)
218
+ self.missing_dependencies = missing_dependencies or []
219
+ self.docs_url = "https://docs.hopx.ai/desktop-automation"
220
+ self.install_command = self._get_install_command()
221
+
222
+ def _get_install_command(self) -> str:
223
+ """Generate install command for missing dependencies."""
224
+ if not self.missing_dependencies:
225
+ # Return default desktop dependencies
226
+ deps = [
227
+ "xvfb",
228
+ "tigervnc-standalone-server",
229
+ "xdotool",
230
+ "wmctrl",
231
+ "xclip",
232
+ "imagemagick",
233
+ "ffmpeg",
234
+ "tesseract-ocr"
235
+ ]
236
+ return f"apt-get update && apt-get install -y {' '.join(deps)}"
237
+
238
+ return f"apt-get install -y {' '.join(self.missing_dependencies)}"
239
+
240
+ def __str__(self) -> str:
241
+ msg = super().__str__()
242
+ if self.missing_dependencies:
243
+ msg += f"\n\nMissing dependencies: {', '.join(self.missing_dependencies)}"
244
+ msg += f"\n\nDocumentation: {self.docs_url}"
245
+ if self.install_command:
246
+ msg += f"\n\nTo enable desktop automation, add to your Dockerfile:"
247
+ msg += f"\nRUN {self.install_command}"
248
+ return msg
249
+