hopx-ai 0.1.15__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 hopx-ai might be problematic. Click here for more details.
- hopx_ai/__init__.py +114 -0
- hopx_ai/_agent_client.py +391 -0
- hopx_ai/_async_agent_client.py +223 -0
- hopx_ai/_async_cache.py +38 -0
- hopx_ai/_async_client.py +230 -0
- hopx_ai/_async_commands.py +58 -0
- hopx_ai/_async_env_vars.py +151 -0
- hopx_ai/_async_files.py +81 -0
- hopx_ai/_async_files_clean.py +489 -0
- hopx_ai/_async_terminal.py +184 -0
- hopx_ai/_client.py +230 -0
- hopx_ai/_generated/__init__.py +22 -0
- hopx_ai/_generated/models.py +502 -0
- hopx_ai/_temp_async_token.py +14 -0
- hopx_ai/_test_env_fix.py +30 -0
- hopx_ai/_utils.py +9 -0
- hopx_ai/_ws_client.py +141 -0
- hopx_ai/async_sandbox.py +763 -0
- hopx_ai/cache.py +97 -0
- hopx_ai/commands.py +174 -0
- hopx_ai/desktop.py +1227 -0
- hopx_ai/env_vars.py +244 -0
- hopx_ai/errors.py +249 -0
- hopx_ai/files.py +489 -0
- hopx_ai/models.py +274 -0
- hopx_ai/models_updated.py +270 -0
- hopx_ai/sandbox.py +1447 -0
- hopx_ai/template/__init__.py +47 -0
- hopx_ai/template/build_flow.py +540 -0
- hopx_ai/template/builder.py +300 -0
- hopx_ai/template/file_hasher.py +81 -0
- hopx_ai/template/ready_checks.py +106 -0
- hopx_ai/template/tar_creator.py +122 -0
- hopx_ai/template/types.py +199 -0
- hopx_ai/terminal.py +164 -0
- hopx_ai-0.1.15.dist-info/METADATA +462 -0
- hopx_ai-0.1.15.dist-info/RECORD +38 -0
- hopx_ai-0.1.15.dist-info/WHEEL +4 -0
hopx_ai/env_vars.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
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, "merge": True}, # ✅ FIXED: add merge flag
|
|
161
|
+
operation="update environment variables",
|
|
162
|
+
timeout=timeout
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Agent returns 204 No Content on success
|
|
166
|
+
if response.status_code == 204 or not response.content:
|
|
167
|
+
# Get current env vars to return updated state
|
|
168
|
+
return self.get_all()
|
|
169
|
+
|
|
170
|
+
data = response.json()
|
|
171
|
+
return data.get("env_vars", {})
|
|
172
|
+
|
|
173
|
+
def delete(self, key: str, *, timeout: Optional[int] = None) -> None:
|
|
174
|
+
"""
|
|
175
|
+
Delete a specific environment variable.
|
|
176
|
+
|
|
177
|
+
Note: Agent's DELETE /env clears ALL custom variables.
|
|
178
|
+
We work around this by re-setting all vars except the one to delete.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
key: Environment variable name to delete
|
|
182
|
+
timeout: Request timeout in seconds (overrides default)
|
|
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
|
+
# Get all current env vars
|
|
191
|
+
current_vars = self.get_all()
|
|
192
|
+
|
|
193
|
+
# Remove the specified variable
|
|
194
|
+
if key in current_vars:
|
|
195
|
+
del current_vars[key]
|
|
196
|
+
# Re-set all env vars without the deleted one
|
|
197
|
+
self.set_all(current_vars)
|
|
198
|
+
logger.debug(f"Environment variable {key} deleted")
|
|
199
|
+
else:
|
|
200
|
+
logger.debug(f"Environment variable {key} not found (already deleted)")
|
|
201
|
+
|
|
202
|
+
def get(self, key: str, default: Optional[str] = None) -> Optional[str]:
|
|
203
|
+
"""
|
|
204
|
+
Get a specific environment variable value.
|
|
205
|
+
|
|
206
|
+
Convenience method that fetches all variables and returns the requested one.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
key: Environment variable name
|
|
210
|
+
default: Default value if variable doesn't exist
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Variable value or default
|
|
214
|
+
|
|
215
|
+
Example:
|
|
216
|
+
>>> api_key = sandbox.env.get("API_KEY")
|
|
217
|
+
>>> db_url = sandbox.env.get("DATABASE_URL", "postgres://localhost/db")
|
|
218
|
+
"""
|
|
219
|
+
env_vars = self.get_all()
|
|
220
|
+
return env_vars.get(key, default)
|
|
221
|
+
|
|
222
|
+
def set(self, key: str, value: str, *, timeout: Optional[int] = None) -> Dict[str, str]:
|
|
223
|
+
"""
|
|
224
|
+
Set a single environment variable.
|
|
225
|
+
|
|
226
|
+
Convenience method that updates just one variable (merge).
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
key: Environment variable name
|
|
230
|
+
value: Environment variable value
|
|
231
|
+
timeout: Request timeout in seconds (overrides default)
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Updated dictionary of all environment variables
|
|
235
|
+
|
|
236
|
+
Example:
|
|
237
|
+
>>> sandbox.env.set("API_KEY", "sk-prod-xyz")
|
|
238
|
+
>>> sandbox.env.set("NODE_ENV", "production")
|
|
239
|
+
"""
|
|
240
|
+
return self.update({key: value}, timeout=timeout)
|
|
241
|
+
|
|
242
|
+
def __repr__(self) -> str:
|
|
243
|
+
return f"<EnvironmentVariables client={self._client}>"
|
|
244
|
+
|
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(HopxError):
|
|
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(HopxError):
|
|
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
|
+
|