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/__init__.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bunnyshell Python SDK
|
|
3
|
+
|
|
4
|
+
Official Python client for Bunnyshell Sandboxes.
|
|
5
|
+
|
|
6
|
+
Sync Example:
|
|
7
|
+
>>> from bunnyshell import Sandbox
|
|
8
|
+
>>>
|
|
9
|
+
>>> # Create sandbox
|
|
10
|
+
>>> sandbox = Sandbox.create(template="code-interpreter")
|
|
11
|
+
>>> print(sandbox.get_info().public_host)
|
|
12
|
+
>>>
|
|
13
|
+
>>> # Cleanup
|
|
14
|
+
>>> sandbox.kill()
|
|
15
|
+
|
|
16
|
+
Async Example:
|
|
17
|
+
>>> from bunnyshell import AsyncSandbox
|
|
18
|
+
>>>
|
|
19
|
+
>>> async with AsyncSandbox.create(template="nodejs") as sandbox:
|
|
20
|
+
... info = await sandbox.get_info()
|
|
21
|
+
... print(f"Running at: {info.public_host}")
|
|
22
|
+
# Automatically killed when exiting context
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from .sandbox import Sandbox
|
|
26
|
+
from .async_sandbox import AsyncSandbox
|
|
27
|
+
from .models import (
|
|
28
|
+
SandboxInfo,
|
|
29
|
+
Template as SandboxTemplate,
|
|
30
|
+
TemplateResources,
|
|
31
|
+
ExecutionResult,
|
|
32
|
+
CommandResult,
|
|
33
|
+
FileInfo,
|
|
34
|
+
RichOutput,
|
|
35
|
+
# Desktop models
|
|
36
|
+
VNCInfo,
|
|
37
|
+
WindowInfo,
|
|
38
|
+
RecordingInfo,
|
|
39
|
+
DisplayInfo,
|
|
40
|
+
)
|
|
41
|
+
from .errors import (
|
|
42
|
+
HopxError,
|
|
43
|
+
APIError,
|
|
44
|
+
AuthenticationError,
|
|
45
|
+
NotFoundError,
|
|
46
|
+
RateLimitError,
|
|
47
|
+
ResourceLimitError,
|
|
48
|
+
ValidationError,
|
|
49
|
+
ServerError,
|
|
50
|
+
NetworkError,
|
|
51
|
+
TimeoutError,
|
|
52
|
+
# Agent operation errors
|
|
53
|
+
AgentError,
|
|
54
|
+
FileNotFoundError,
|
|
55
|
+
FileOperationError,
|
|
56
|
+
CodeExecutionError,
|
|
57
|
+
CommandExecutionError,
|
|
58
|
+
DesktopNotAvailableError,
|
|
59
|
+
)
|
|
60
|
+
# Template Building
|
|
61
|
+
from .template import (
|
|
62
|
+
Template,
|
|
63
|
+
create_template,
|
|
64
|
+
wait_for_port,
|
|
65
|
+
wait_for_url,
|
|
66
|
+
wait_for_file,
|
|
67
|
+
wait_for_process,
|
|
68
|
+
wait_for_command,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
__version__ = "0.1.8"
|
|
72
|
+
__all__ = [
|
|
73
|
+
"Sandbox",
|
|
74
|
+
"AsyncSandbox",
|
|
75
|
+
"SandboxInfo",
|
|
76
|
+
"SandboxTemplate",
|
|
77
|
+
"TemplateResources",
|
|
78
|
+
"ExecutionResult",
|
|
79
|
+
"CommandResult",
|
|
80
|
+
"FileInfo",
|
|
81
|
+
"RichOutput",
|
|
82
|
+
# Desktop models
|
|
83
|
+
"VNCInfo",
|
|
84
|
+
"WindowInfo",
|
|
85
|
+
"RecordingInfo",
|
|
86
|
+
"DisplayInfo",
|
|
87
|
+
# Errors
|
|
88
|
+
"HopxError",
|
|
89
|
+
"APIError",
|
|
90
|
+
"AuthenticationError",
|
|
91
|
+
"NotFoundError",
|
|
92
|
+
"RateLimitError",
|
|
93
|
+
"ResourceLimitError",
|
|
94
|
+
"ValidationError",
|
|
95
|
+
"ServerError",
|
|
96
|
+
"NetworkError",
|
|
97
|
+
"TimeoutError",
|
|
98
|
+
# Agent operation errors
|
|
99
|
+
"AgentError",
|
|
100
|
+
"FileNotFoundError",
|
|
101
|
+
"FileOperationError",
|
|
102
|
+
"CodeExecutionError",
|
|
103
|
+
"CommandExecutionError",
|
|
104
|
+
"DesktopNotAvailableError",
|
|
105
|
+
# Template Building
|
|
106
|
+
"Template",
|
|
107
|
+
"create_template",
|
|
108
|
+
"wait_for_port",
|
|
109
|
+
"wait_for_url",
|
|
110
|
+
"wait_for_file",
|
|
111
|
+
"wait_for_process",
|
|
112
|
+
"wait_for_command",
|
|
113
|
+
]
|
|
114
|
+
|
hopx_ai/_agent_client.py
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"""HTTP client for agent operations with retry logic."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Optional, Dict, Any
|
|
6
|
+
import httpx
|
|
7
|
+
from .errors import (
|
|
8
|
+
FileNotFoundError,
|
|
9
|
+
FileOperationError,
|
|
10
|
+
CodeExecutionError,
|
|
11
|
+
CommandExecutionError,
|
|
12
|
+
DesktopNotAvailableError,
|
|
13
|
+
AgentError,
|
|
14
|
+
NetworkError,
|
|
15
|
+
TimeoutError as BunnyshellTimeoutError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AgentHTTPClient:
|
|
22
|
+
"""
|
|
23
|
+
HTTP client for agent operations with retry logic and error handling.
|
|
24
|
+
|
|
25
|
+
Features:
|
|
26
|
+
- Connection pooling (reuses TCP connections)
|
|
27
|
+
- Automatic retries with exponential backoff
|
|
28
|
+
- Proper error wrapping to Bunnyshell exceptions
|
|
29
|
+
- Configurable timeouts
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
agent_url: str,
|
|
35
|
+
*,
|
|
36
|
+
jwt_token: Optional[str] = None,
|
|
37
|
+
timeout: int = 30,
|
|
38
|
+
max_retries: int = 3,
|
|
39
|
+
token_refresh_callback: Optional[callable] = None,
|
|
40
|
+
):
|
|
41
|
+
"""
|
|
42
|
+
Initialize agent HTTP client.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
agent_url: Agent base URL (e.g., https://7777-{id}.domain)
|
|
46
|
+
jwt_token: JWT token for agent authentication
|
|
47
|
+
timeout: Default timeout in seconds
|
|
48
|
+
max_retries: Maximum retry attempts
|
|
49
|
+
token_refresh_callback: Callback to refresh token on 401 (returns new token)
|
|
50
|
+
"""
|
|
51
|
+
self._agent_url = agent_url.rstrip('/')
|
|
52
|
+
self._jwt_token = jwt_token
|
|
53
|
+
self._timeout = timeout
|
|
54
|
+
self._max_retries = max_retries
|
|
55
|
+
self._token_refresh_callback = token_refresh_callback
|
|
56
|
+
|
|
57
|
+
# Build headers
|
|
58
|
+
headers = {}
|
|
59
|
+
if self._jwt_token:
|
|
60
|
+
headers['Authorization'] = f'Bearer {self._jwt_token}'
|
|
61
|
+
|
|
62
|
+
# Create reusable HTTP client with connection pooling
|
|
63
|
+
# Force IPv4 to avoid IPv6 timeout issues (270s delay)
|
|
64
|
+
self._client = httpx.Client(
|
|
65
|
+
timeout=httpx.Timeout(timeout),
|
|
66
|
+
limits=httpx.Limits(max_connections=10, max_keepalive_connections=5),
|
|
67
|
+
headers=headers,
|
|
68
|
+
transport=httpx.HTTPTransport(
|
|
69
|
+
local_address="0.0.0.0", # Force IPv4
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
logger.debug(f"Agent client initialized: {self._agent_url}")
|
|
74
|
+
|
|
75
|
+
def update_jwt_token(self, token: str) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Update JWT token for agent authentication.
|
|
78
|
+
Used internally when token is refreshed.
|
|
79
|
+
"""
|
|
80
|
+
self._jwt_token = token
|
|
81
|
+
|
|
82
|
+
# Update client headers
|
|
83
|
+
self._client.headers['Authorization'] = f'Bearer {token}'
|
|
84
|
+
|
|
85
|
+
def _should_retry(self, status_code: int) -> bool:
|
|
86
|
+
"""Check if request should be retried based on status code."""
|
|
87
|
+
return status_code in {429, 500, 502, 503, 504}
|
|
88
|
+
|
|
89
|
+
def _get_retry_delay(self, attempt: int) -> float:
|
|
90
|
+
"""Calculate retry delay with exponential backoff."""
|
|
91
|
+
return min(2 ** attempt, 10) # Max 10 seconds
|
|
92
|
+
|
|
93
|
+
def _wrap_error(
|
|
94
|
+
self,
|
|
95
|
+
error: Exception,
|
|
96
|
+
operation: str,
|
|
97
|
+
context: Optional[Dict[str, Any]] = None
|
|
98
|
+
) -> AgentError:
|
|
99
|
+
"""
|
|
100
|
+
Wrap httpx errors into Bunnyshell exceptions.
|
|
101
|
+
|
|
102
|
+
Uses Agent v3.1.1+ error codes for precise exception mapping:
|
|
103
|
+
- FILE_NOT_FOUND -> FileNotFoundError
|
|
104
|
+
- PATH_NOT_ALLOWED -> FileOperationError
|
|
105
|
+
- EXECUTION_FAILED/TIMEOUT -> CodeExecutionError
|
|
106
|
+
- COMMAND_FAILED -> CommandExecutionError
|
|
107
|
+
- DESKTOP_NOT_AVAILABLE -> DesktopNotAvailableError
|
|
108
|
+
|
|
109
|
+
Falls back to HTTP status code + context for older agents.
|
|
110
|
+
"""
|
|
111
|
+
context = context or {}
|
|
112
|
+
|
|
113
|
+
if isinstance(error, httpx.HTTPStatusError):
|
|
114
|
+
status_code = error.response.status_code
|
|
115
|
+
# Extract request ID from response headers (case-insensitive)
|
|
116
|
+
request_id = (
|
|
117
|
+
error.response.headers.get("X-Request-ID") or
|
|
118
|
+
error.response.headers.get("x-request-id") or
|
|
119
|
+
error.response.headers.get("X-Request-Id")
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Try to get error message, code, and details from response (Agent v3.1.1+)
|
|
123
|
+
error_code = None
|
|
124
|
+
error_details = {}
|
|
125
|
+
try:
|
|
126
|
+
error_data = error.response.json()
|
|
127
|
+
message = error_data.get("error", error_data.get("message", str(error)))
|
|
128
|
+
error_code = error_data.get("code") # Machine-readable error code (v3.1.1+)
|
|
129
|
+
error_details = error_data.get("details", {})
|
|
130
|
+
except:
|
|
131
|
+
message = f"HTTP {status_code}: {error.response.text[:100]}"
|
|
132
|
+
|
|
133
|
+
# Map error codes to specific exceptions (Agent v3.1.1+)
|
|
134
|
+
# Note: Agent returns UPPERCASE error codes (e.g., "FILE_NOT_FOUND")
|
|
135
|
+
if error_code:
|
|
136
|
+
# File-related errors
|
|
137
|
+
if error_code == "FILE_NOT_FOUND":
|
|
138
|
+
return FileNotFoundError(
|
|
139
|
+
message=message,
|
|
140
|
+
path=error_details.get("path") or context.get("path"),
|
|
141
|
+
request_id=request_id,
|
|
142
|
+
code=error_code
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if error_code == "PATH_NOT_ALLOWED":
|
|
146
|
+
return FileOperationError(
|
|
147
|
+
message=message,
|
|
148
|
+
operation=operation,
|
|
149
|
+
request_id=request_id,
|
|
150
|
+
status_code=status_code,
|
|
151
|
+
code=error_code
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if error_code in ("DIRECTORY_NOT_FOUND", "INVALID_PATH", "FILE_ALREADY_EXISTS"):
|
|
155
|
+
return FileOperationError(
|
|
156
|
+
message=message,
|
|
157
|
+
operation=operation,
|
|
158
|
+
request_id=request_id,
|
|
159
|
+
status_code=status_code,
|
|
160
|
+
code=error_code
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Execution errors
|
|
164
|
+
if error_code in ("EXECUTION_FAILED", "EXECUTION_TIMEOUT"):
|
|
165
|
+
return CodeExecutionError(
|
|
166
|
+
message=message,
|
|
167
|
+
language=context.get("language"),
|
|
168
|
+
request_id=request_id,
|
|
169
|
+
status_code=status_code,
|
|
170
|
+
code=error_code
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if error_code == "COMMAND_FAILED":
|
|
174
|
+
return CommandExecutionError(
|
|
175
|
+
message=message,
|
|
176
|
+
command=context.get("command"),
|
|
177
|
+
request_id=request_id,
|
|
178
|
+
status_code=status_code,
|
|
179
|
+
code=error_code
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Desktop errors
|
|
183
|
+
if error_code == "DESKTOP_NOT_AVAILABLE":
|
|
184
|
+
missing_deps = error_details.get("missing_dependencies", [])
|
|
185
|
+
return DesktopNotAvailableError(
|
|
186
|
+
message=message,
|
|
187
|
+
missing_dependencies=missing_deps,
|
|
188
|
+
request_id=request_id,
|
|
189
|
+
status_code=status_code,
|
|
190
|
+
code=error_code
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Generic errors with code
|
|
194
|
+
return AgentError(
|
|
195
|
+
message=message,
|
|
196
|
+
code=error_code,
|
|
197
|
+
request_id=request_id,
|
|
198
|
+
status_code=status_code
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Fallback: Map by status code + context (for older agents without error codes)
|
|
202
|
+
if status_code in (403, 404):
|
|
203
|
+
if "file" in operation.lower() or "read" in operation.lower() or "download" in operation.lower():
|
|
204
|
+
# 403/404 for files usually means not found
|
|
205
|
+
return FileNotFoundError(
|
|
206
|
+
message=message,
|
|
207
|
+
path=context.get("path"),
|
|
208
|
+
request_id=request_id
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# File operation errors
|
|
212
|
+
if "file" in operation.lower():
|
|
213
|
+
return FileOperationError(
|
|
214
|
+
message=message,
|
|
215
|
+
operation=operation,
|
|
216
|
+
request_id=request_id,
|
|
217
|
+
status_code=status_code
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Code execution errors
|
|
221
|
+
if "code" in operation.lower() or "execute" in operation.lower():
|
|
222
|
+
return CodeExecutionError(
|
|
223
|
+
message=message,
|
|
224
|
+
language=context.get("language"),
|
|
225
|
+
request_id=request_id,
|
|
226
|
+
status_code=status_code
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Command execution errors
|
|
230
|
+
if "command" in operation.lower():
|
|
231
|
+
return CommandExecutionError(
|
|
232
|
+
message=message,
|
|
233
|
+
command=context.get("command"),
|
|
234
|
+
request_id=request_id,
|
|
235
|
+
status_code=status_code
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Generic agent error
|
|
239
|
+
return AgentError(
|
|
240
|
+
message=message,
|
|
241
|
+
code=f"HTTP_{status_code}",
|
|
242
|
+
request_id=request_id,
|
|
243
|
+
status_code=status_code
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
elif isinstance(error, httpx.TimeoutException):
|
|
247
|
+
return BunnyshellTimeoutError(
|
|
248
|
+
f"{operation} timed out after {self._timeout}s"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
elif isinstance(error, httpx.NetworkError):
|
|
252
|
+
return NetworkError(f"{operation} failed: {error}")
|
|
253
|
+
|
|
254
|
+
else:
|
|
255
|
+
return AgentError(f"{operation} failed: {error}")
|
|
256
|
+
|
|
257
|
+
def _request(
|
|
258
|
+
self,
|
|
259
|
+
method: str,
|
|
260
|
+
endpoint: str,
|
|
261
|
+
*,
|
|
262
|
+
operation: str,
|
|
263
|
+
context: Optional[Dict[str, Any]] = None,
|
|
264
|
+
timeout: Optional[int] = None,
|
|
265
|
+
**kwargs
|
|
266
|
+
) -> httpx.Response:
|
|
267
|
+
"""
|
|
268
|
+
Make HTTP request with retry logic.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
method: HTTP method (GET, POST, etc.)
|
|
272
|
+
endpoint: API endpoint (e.g., /files/read)
|
|
273
|
+
operation: Operation name for error messages
|
|
274
|
+
context: Additional context for error handling
|
|
275
|
+
timeout: Request timeout (overrides default)
|
|
276
|
+
**kwargs: Additional arguments for httpx request
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
HTTP response
|
|
280
|
+
|
|
281
|
+
Raises:
|
|
282
|
+
AgentError: On request failure
|
|
283
|
+
"""
|
|
284
|
+
url = f"{self._agent_url}{endpoint}"
|
|
285
|
+
timeout_val = timeout or self._timeout
|
|
286
|
+
|
|
287
|
+
for attempt in range(self._max_retries):
|
|
288
|
+
try:
|
|
289
|
+
logger.debug(f"{method} {url} (attempt {attempt + 1}/{self._max_retries})")
|
|
290
|
+
|
|
291
|
+
response = self._client.request(
|
|
292
|
+
method=method,
|
|
293
|
+
url=url,
|
|
294
|
+
timeout=timeout_val,
|
|
295
|
+
**kwargs
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Raise for 4xx and 5xx status codes
|
|
299
|
+
response.raise_for_status()
|
|
300
|
+
|
|
301
|
+
return response
|
|
302
|
+
|
|
303
|
+
except httpx.HTTPStatusError as e:
|
|
304
|
+
# Handle 401 Unauthorized - try to refresh token
|
|
305
|
+
if e.response.status_code == 401 and self._token_refresh_callback and attempt == 0:
|
|
306
|
+
logger.info(f"{operation} got 401 Unauthorized, attempting token refresh...")
|
|
307
|
+
try:
|
|
308
|
+
# Call refresh callback to get new token
|
|
309
|
+
new_token = self._token_refresh_callback()
|
|
310
|
+
if new_token:
|
|
311
|
+
# Update token and retry request
|
|
312
|
+
self.update_jwt_token(new_token)
|
|
313
|
+
logger.info(f"{operation} token refreshed, retrying request...")
|
|
314
|
+
continue
|
|
315
|
+
except Exception as refresh_error:
|
|
316
|
+
logger.error(f"Token refresh failed: {refresh_error}")
|
|
317
|
+
# Fall through to raise original error
|
|
318
|
+
|
|
319
|
+
# Check if should retry
|
|
320
|
+
if attempt < self._max_retries - 1 and self._should_retry(e.response.status_code):
|
|
321
|
+
delay = self._get_retry_delay(attempt)
|
|
322
|
+
logger.warning(
|
|
323
|
+
f"{operation} failed with {e.response.status_code}, "
|
|
324
|
+
f"retrying in {delay}s (attempt {attempt + 1}/{self._max_retries})"
|
|
325
|
+
)
|
|
326
|
+
time.sleep(delay)
|
|
327
|
+
continue
|
|
328
|
+
|
|
329
|
+
# No more retries, raise wrapped error
|
|
330
|
+
raise self._wrap_error(e, operation, context)
|
|
331
|
+
|
|
332
|
+
except (httpx.TimeoutException, httpx.NetworkError) as e:
|
|
333
|
+
# Retry on timeout/network errors
|
|
334
|
+
if attempt < self._max_retries - 1:
|
|
335
|
+
delay = self._get_retry_delay(attempt)
|
|
336
|
+
logger.warning(
|
|
337
|
+
f"{operation} failed: {e}, "
|
|
338
|
+
f"retrying in {delay}s (attempt {attempt + 1}/{self._max_retries})"
|
|
339
|
+
)
|
|
340
|
+
time.sleep(delay)
|
|
341
|
+
continue
|
|
342
|
+
|
|
343
|
+
# No more retries
|
|
344
|
+
raise self._wrap_error(e, operation, context)
|
|
345
|
+
|
|
346
|
+
except Exception as e:
|
|
347
|
+
# Unexpected error, don't retry
|
|
348
|
+
raise self._wrap_error(e, operation, context)
|
|
349
|
+
|
|
350
|
+
# Should never reach here
|
|
351
|
+
raise AgentError(f"{operation} failed after {self._max_retries} attempts")
|
|
352
|
+
|
|
353
|
+
def get(self, endpoint: str, *, operation: str, context: Optional[Dict[str, Any]] = None, **kwargs) -> httpx.Response:
|
|
354
|
+
"""Make GET request."""
|
|
355
|
+
return self._request("GET", endpoint, operation=operation, context=context, **kwargs)
|
|
356
|
+
|
|
357
|
+
def post(self, endpoint: str, *, operation: str, context: Optional[Dict[str, Any]] = None, **kwargs) -> httpx.Response:
|
|
358
|
+
"""Make POST request."""
|
|
359
|
+
return self._request("POST", endpoint, operation=operation, context=context, **kwargs)
|
|
360
|
+
|
|
361
|
+
def put(self, endpoint: str, *, operation: str, context: Optional[Dict[str, Any]] = None, **kwargs) -> httpx.Response:
|
|
362
|
+
"""Make PUT request."""
|
|
363
|
+
return self._request("PUT", endpoint, operation=operation, context=context, **kwargs)
|
|
364
|
+
|
|
365
|
+
def patch(self, endpoint: str, *, operation: str, context: Optional[Dict[str, Any]] = None, **kwargs) -> httpx.Response:
|
|
366
|
+
"""Make PATCH request."""
|
|
367
|
+
return self._request("PATCH", endpoint, operation=operation, context=context, **kwargs)
|
|
368
|
+
|
|
369
|
+
def delete(self, endpoint: str, *, operation: str, context: Optional[Dict[str, Any]] = None, **kwargs) -> httpx.Response:
|
|
370
|
+
"""Make DELETE request."""
|
|
371
|
+
return self._request("DELETE", endpoint, operation=operation, context=context, **kwargs)
|
|
372
|
+
|
|
373
|
+
def close(self):
|
|
374
|
+
"""Close HTTP client and release connections."""
|
|
375
|
+
self._client.close()
|
|
376
|
+
|
|
377
|
+
def __del__(self):
|
|
378
|
+
"""Cleanup on deletion."""
|
|
379
|
+
try:
|
|
380
|
+
self.close()
|
|
381
|
+
except:
|
|
382
|
+
pass
|
|
383
|
+
|
|
384
|
+
def __enter__(self):
|
|
385
|
+
"""Context manager entry."""
|
|
386
|
+
return self
|
|
387
|
+
|
|
388
|
+
def __exit__(self, *args):
|
|
389
|
+
"""Context manager exit."""
|
|
390
|
+
self.close()
|
|
391
|
+
|