hud-python 0.2.2__py3-none-any.whl → 0.2.4__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 hud-python might be problematic. Click here for more details.
- hud/__init__.py +4 -3
- hud/adapters/claude/adapter.py +5 -14
- hud/adapters/common/adapter.py +3 -3
- hud/adapters/common/tests/__init__.py +0 -0
- hud/adapters/common/tests/test_adapter.py +277 -0
- hud/adapters/common/types.py +3 -3
- hud/adapters/operator/adapter.py +16 -23
- hud/agent/__init__.py +8 -1
- hud/agent/base.py +28 -28
- hud/agent/claude.py +69 -60
- hud/agent/langchain.py +32 -26
- hud/agent/operator.py +75 -67
- hud/env/__init__.py +5 -5
- hud/env/client.py +2 -2
- hud/env/docker_client.py +37 -39
- hud/env/environment.py +91 -66
- hud/env/local_docker_client.py +5 -7
- hud/env/remote_client.py +39 -32
- hud/env/remote_docker_client.py +13 -3
- hud/evaluators/__init__.py +2 -3
- hud/evaluators/base.py +4 -3
- hud/evaluators/inspect.py +3 -8
- hud/evaluators/judge.py +34 -58
- hud/evaluators/match.py +42 -49
- hud/evaluators/remote.py +13 -26
- hud/evaluators/tests/__init__.py +0 -0
- hud/evaluators/tests/test_inspect.py +12 -0
- hud/evaluators/tests/test_judge.py +231 -0
- hud/evaluators/tests/test_match.py +115 -0
- hud/evaluators/tests/test_remote.py +98 -0
- hud/exceptions.py +167 -0
- hud/gym.py +9 -7
- hud/job.py +179 -109
- hud/server/__init__.py +2 -2
- hud/server/requests.py +148 -186
- hud/server/tests/__init__.py +0 -0
- hud/server/tests/test_requests.py +275 -0
- hud/settings.py +3 -2
- hud/task.py +9 -19
- hud/taskset.py +44 -11
- hud/trajectory.py +6 -9
- hud/types.py +12 -9
- hud/utils/__init__.py +2 -2
- hud/utils/common.py +36 -15
- hud/utils/config.py +45 -30
- hud/utils/progress.py +34 -21
- hud/utils/telemetry.py +10 -11
- hud/utils/tests/__init__.py +0 -0
- hud/utils/tests/test_common.py +52 -0
- hud/utils/tests/test_config.py +129 -0
- hud/utils/tests/test_progress.py +225 -0
- hud/utils/tests/test_telemetry.py +37 -0
- hud/utils/tests/test_version.py +8 -0
- {hud_python-0.2.2.dist-info → hud_python-0.2.4.dist-info}/METADATA +9 -6
- hud_python-0.2.4.dist-info/RECORD +62 -0
- hud_python-0.2.2.dist-info/RECORD +0 -46
- {hud_python-0.2.2.dist-info → hud_python-0.2.4.dist-info}/WHEEL +0 -0
- {hud_python-0.2.2.dist-info → hud_python-0.2.4.dist-info}/licenses/LICENSE +0 -0
hud/server/requests.py
CHANGED
|
@@ -11,95 +11,25 @@ from typing import Any
|
|
|
11
11
|
|
|
12
12
|
import httpx
|
|
13
13
|
|
|
14
|
+
from hud.exceptions import (
|
|
15
|
+
HudAuthenticationError,
|
|
16
|
+
HudNetworkError,
|
|
17
|
+
HudRequestError,
|
|
18
|
+
HudTimeoutError,
|
|
19
|
+
)
|
|
20
|
+
|
|
14
21
|
# Set up logger
|
|
15
22
|
logger = logging.getLogger("hud.http")
|
|
16
23
|
logger.setLevel(logging.DEBUG)
|
|
17
24
|
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
response_text: str | None = None,
|
|
27
|
-
response_json: dict[str, Any] | None = None,
|
|
28
|
-
response_headers: dict[str, str] | None = None,
|
|
29
|
-
) -> None:
|
|
30
|
-
self.message = message
|
|
31
|
-
self.status_code = status_code
|
|
32
|
-
self.response_text = response_text
|
|
33
|
-
self.response_json = response_json
|
|
34
|
-
self.response_headers = response_headers
|
|
35
|
-
super().__init__(message)
|
|
36
|
-
|
|
37
|
-
def __str__(self) -> str:
|
|
38
|
-
parts = [self.message]
|
|
39
|
-
|
|
40
|
-
if self.status_code:
|
|
41
|
-
parts.append(f"Status: {self.status_code}")
|
|
42
|
-
|
|
43
|
-
if self.response_text:
|
|
44
|
-
parts.append(f"Response Text: {self.response_text}")
|
|
45
|
-
|
|
46
|
-
if self.response_json:
|
|
47
|
-
parts.append(f"Response JSON: {self.response_json}")
|
|
48
|
-
|
|
49
|
-
if self.response_headers:
|
|
50
|
-
parts.append(f"Headers: {self.response_headers}")
|
|
51
|
-
|
|
52
|
-
return " | ".join(parts)
|
|
53
|
-
|
|
54
|
-
@classmethod
|
|
55
|
-
def from_http_error(
|
|
56
|
-
cls, error: httpx.HTTPStatusError, context: str = ""
|
|
57
|
-
) -> RequestError:
|
|
58
|
-
"""Create a RequestError from an HTTP error response"""
|
|
59
|
-
response = error.response
|
|
60
|
-
status_code = response.status_code
|
|
61
|
-
response_text = response.text
|
|
62
|
-
response_headers = dict(response.headers)
|
|
63
|
-
|
|
64
|
-
# Try to get detailed error info from JSON if available
|
|
65
|
-
response_json = None
|
|
66
|
-
try:
|
|
67
|
-
response_json = response.json()
|
|
68
|
-
detail = response_json.get("detail")
|
|
69
|
-
if detail:
|
|
70
|
-
message = f"Request failed: {detail}"
|
|
71
|
-
else:
|
|
72
|
-
# If no detail field but we have JSON, include a summary
|
|
73
|
-
message = f"Request failed with status {status_code}"
|
|
74
|
-
if (
|
|
75
|
-
len(response_json) <= 5
|
|
76
|
-
): # If it's a small object, include it in the message
|
|
77
|
-
message += f" - JSON response: {response_json}"
|
|
78
|
-
except Exception:
|
|
79
|
-
# Fallback to simple message if JSON parsing fails
|
|
80
|
-
message = f"Request failed with status {status_code}"
|
|
81
|
-
|
|
82
|
-
# Add context if provided
|
|
83
|
-
if context:
|
|
84
|
-
message = f"{context}: {message}"
|
|
85
|
-
|
|
86
|
-
# Log the error details
|
|
87
|
-
logger.error(
|
|
88
|
-
"HTTP error from HUD SDK: %s | URL: %s | Status: %s | Response: %s%s",
|
|
89
|
-
message,
|
|
90
|
-
response.url,
|
|
91
|
-
status_code,
|
|
92
|
-
response_text[:500],
|
|
93
|
-
"..." if len(response_text) > 500 else "",
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
return cls(
|
|
97
|
-
message=message,
|
|
98
|
-
status_code=status_code,
|
|
99
|
-
response_text=response_text,
|
|
100
|
-
response_json=response_json,
|
|
101
|
-
response_headers=response_headers,
|
|
102
|
-
)
|
|
26
|
+
# Long running requests can take up to 10 minutes.
|
|
27
|
+
_DEFAULT_TIMEOUT = 600.0
|
|
28
|
+
_DEFAULT_LIMITS = httpx.Limits(
|
|
29
|
+
max_connections=1000,
|
|
30
|
+
max_keepalive_connections=1000,
|
|
31
|
+
keepalive_expiry=10.0,
|
|
32
|
+
)
|
|
103
33
|
|
|
104
34
|
|
|
105
35
|
async def _handle_retry(
|
|
@@ -118,6 +48,22 @@ async def _handle_retry(
|
|
|
118
48
|
await asyncio.sleep(retry_time)
|
|
119
49
|
|
|
120
50
|
|
|
51
|
+
def _create_default_async_client() -> httpx.AsyncClient:
|
|
52
|
+
"""Create a default httpx AsyncClient with standard configuration."""
|
|
53
|
+
return httpx.AsyncClient(
|
|
54
|
+
timeout=_DEFAULT_TIMEOUT,
|
|
55
|
+
limits=_DEFAULT_LIMITS,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _create_default_sync_client() -> httpx.Client:
|
|
60
|
+
"""Create a default httpx Client with standard configuration."""
|
|
61
|
+
return httpx.Client(
|
|
62
|
+
timeout=_DEFAULT_TIMEOUT,
|
|
63
|
+
limits=_DEFAULT_LIMITS,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
121
67
|
async def make_request(
|
|
122
68
|
method: str,
|
|
123
69
|
url: str,
|
|
@@ -125,6 +71,7 @@ async def make_request(
|
|
|
125
71
|
api_key: str | None = None,
|
|
126
72
|
max_retries: int = 4,
|
|
127
73
|
retry_delay: float = 2.0,
|
|
74
|
+
client: httpx.AsyncClient | None = None,
|
|
128
75
|
) -> dict[str, Any]:
|
|
129
76
|
"""
|
|
130
77
|
Make an asynchronous HTTP request to the HUD API.
|
|
@@ -136,62 +83,69 @@ async def make_request(
|
|
|
136
83
|
api_key: API key for authentication
|
|
137
84
|
max_retries: Maximum number of retries
|
|
138
85
|
retry_delay: Delay between retries
|
|
86
|
+
*,
|
|
87
|
+
client: Optional custom httpx.AsyncClient
|
|
88
|
+
|
|
139
89
|
Returns:
|
|
140
90
|
dict: JSON response from the server
|
|
141
91
|
|
|
142
92
|
Raises:
|
|
143
|
-
|
|
93
|
+
HudAuthenticationError: If API key is missing or invalid.
|
|
94
|
+
HudRequestError: If the request fails with a non-retryable status code.
|
|
95
|
+
HudNetworkError: If there are network-related issues.
|
|
96
|
+
HudTimeoutError: If the request times out.
|
|
144
97
|
"""
|
|
145
98
|
if not api_key:
|
|
146
|
-
raise
|
|
99
|
+
raise HudAuthenticationError("API key is required but not provided")
|
|
147
100
|
|
|
148
101
|
headers = {"Authorization": f"Bearer {api_key}"}
|
|
149
102
|
retry_status_codes = [502, 503, 504]
|
|
150
103
|
attempt = 0
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
raise
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
104
|
+
should_close_client = False
|
|
105
|
+
|
|
106
|
+
if client is None:
|
|
107
|
+
client = _create_default_async_client()
|
|
108
|
+
should_close_client = True
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
while attempt <= max_retries:
|
|
112
|
+
attempt += 1
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
response = await client.request(method=method, url=url, json=json, headers=headers)
|
|
116
|
+
|
|
117
|
+
# Check if we got a retriable status code
|
|
118
|
+
if response.status_code in retry_status_codes and attempt <= max_retries:
|
|
119
|
+
await _handle_retry(
|
|
120
|
+
attempt,
|
|
121
|
+
max_retries,
|
|
122
|
+
retry_delay,
|
|
123
|
+
url,
|
|
124
|
+
f"Received status {response.status_code}",
|
|
125
|
+
)
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
response.raise_for_status()
|
|
129
|
+
result = response.json()
|
|
130
|
+
return result
|
|
131
|
+
except httpx.TimeoutException as e:
|
|
132
|
+
raise HudTimeoutError(f"Request timed out: {e!s}") from None
|
|
133
|
+
except httpx.HTTPStatusError as e:
|
|
134
|
+
raise HudRequestError.from_httpx_error(e) from None
|
|
135
|
+
except httpx.RequestError as e:
|
|
136
|
+
if attempt <= max_retries:
|
|
137
|
+
await _handle_retry(
|
|
138
|
+
attempt, max_retries, retry_delay, url, f"Network error: {e}"
|
|
139
|
+
)
|
|
140
|
+
continue
|
|
141
|
+
else:
|
|
142
|
+
raise HudNetworkError(f"Network error: {e!s}") from None
|
|
143
|
+
except Exception as e:
|
|
144
|
+
raise HudRequestError(f"Unexpected error: {e!s}") from None
|
|
145
|
+
raise HudRequestError(f"Request failed after {max_retries} retries with unknown error")
|
|
146
|
+
finally:
|
|
147
|
+
if should_close_client:
|
|
148
|
+
await client.aclose()
|
|
195
149
|
|
|
196
150
|
|
|
197
151
|
def make_request_sync(
|
|
@@ -201,6 +155,8 @@ def make_request_sync(
|
|
|
201
155
|
api_key: str | None = None,
|
|
202
156
|
max_retries: int = 4,
|
|
203
157
|
retry_delay: float = 2.0,
|
|
158
|
+
*,
|
|
159
|
+
client: httpx.Client | None = None,
|
|
204
160
|
) -> dict[str, Any]:
|
|
205
161
|
"""
|
|
206
162
|
Make a synchronous HTTP request to the HUD API.
|
|
@@ -212,69 +168,75 @@ def make_request_sync(
|
|
|
212
168
|
api_key: API key for authentication
|
|
213
169
|
max_retries: Maximum number of retries
|
|
214
170
|
retry_delay: Delay between retries
|
|
171
|
+
client: Optional custom httpx.Client
|
|
172
|
+
|
|
215
173
|
Returns:
|
|
216
174
|
dict: JSON response from the server
|
|
217
175
|
|
|
218
176
|
Raises:
|
|
219
|
-
|
|
177
|
+
HudAuthenticationError: If API key is missing or invalid.
|
|
178
|
+
HudRequestError: If the request fails with a non-retryable status code.
|
|
179
|
+
HudNetworkError: If there are network-related issues.
|
|
180
|
+
HudTimeoutError: If the request times out.
|
|
220
181
|
"""
|
|
221
182
|
if not api_key:
|
|
222
|
-
raise
|
|
183
|
+
raise HudAuthenticationError("API key is required but not provided")
|
|
223
184
|
|
|
224
185
|
headers = {"Authorization": f"Bearer {api_key}"}
|
|
225
186
|
retry_status_codes = [502, 503, 504]
|
|
226
187
|
attempt = 0
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
retry_time
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
raise
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
188
|
+
should_close_client = False
|
|
189
|
+
|
|
190
|
+
if client is None:
|
|
191
|
+
client = _create_default_sync_client()
|
|
192
|
+
should_close_client = True
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
while attempt <= max_retries:
|
|
196
|
+
attempt += 1
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
response = client.request(method=method, url=url, json=json, headers=headers)
|
|
200
|
+
|
|
201
|
+
# Check if we got a retriable status code
|
|
202
|
+
if response.status_code in retry_status_codes and attempt <= max_retries:
|
|
203
|
+
retry_time = retry_delay * (2 ** (attempt - 1)) # Exponential backoff
|
|
204
|
+
logger.warning(
|
|
205
|
+
"Received status %d from %s, retrying in %.2f seconds (attempt %d/%d)",
|
|
206
|
+
response.status_code,
|
|
207
|
+
url,
|
|
208
|
+
retry_time,
|
|
209
|
+
attempt,
|
|
210
|
+
max_retries,
|
|
211
|
+
)
|
|
212
|
+
time.sleep(retry_time)
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
response.raise_for_status()
|
|
216
|
+
result = response.json()
|
|
217
|
+
return result
|
|
218
|
+
except httpx.TimeoutException as e:
|
|
219
|
+
raise HudTimeoutError(f"Request timed out: {e!s}") from None
|
|
220
|
+
except httpx.HTTPStatusError as e:
|
|
221
|
+
raise HudRequestError.from_httpx_error(e) from None
|
|
222
|
+
except httpx.RequestError as e:
|
|
223
|
+
if attempt <= max_retries:
|
|
224
|
+
retry_time = retry_delay * (2 ** (attempt - 1))
|
|
225
|
+
logger.warning(
|
|
226
|
+
"Network error %s from %s, retrying in %.2f seconds (attempt %d/%d)",
|
|
227
|
+
str(e),
|
|
228
|
+
url,
|
|
229
|
+
retry_time,
|
|
230
|
+
attempt,
|
|
231
|
+
max_retries,
|
|
232
|
+
)
|
|
233
|
+
time.sleep(retry_time)
|
|
234
|
+
continue
|
|
235
|
+
else:
|
|
236
|
+
raise HudNetworkError(f"Network error: {e!s}") from None
|
|
237
|
+
except Exception as e:
|
|
238
|
+
raise HudRequestError(f"Unexpected error: {e!s}") from None
|
|
239
|
+
raise HudRequestError(f"Request failed after {max_retries} retries with unknown error")
|
|
240
|
+
finally:
|
|
241
|
+
if should_close_client:
|
|
242
|
+
client.close()
|
|
File without changes
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Tests for the HTTP request utilities in the HUD API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from http import HTTPStatus
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
from unittest.mock import AsyncMock, Mock, patch
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from hud.exceptions import (
|
|
13
|
+
HudAuthenticationError,
|
|
14
|
+
HudNetworkError,
|
|
15
|
+
HudRequestError,
|
|
16
|
+
HudTimeoutError,
|
|
17
|
+
)
|
|
18
|
+
from hud.server.requests import (
|
|
19
|
+
_handle_retry,
|
|
20
|
+
make_request,
|
|
21
|
+
make_request_sync,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _create_mock_response(
|
|
29
|
+
status_code: int = 200,
|
|
30
|
+
json_data: dict[str, Any] | None = None,
|
|
31
|
+
raise_exception: Exception | None = None,
|
|
32
|
+
) -> Callable[[httpx.Request], httpx.Response]:
|
|
33
|
+
"""Create a mock response handler for httpx.MockTransport."""
|
|
34
|
+
|
|
35
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
36
|
+
if "Authorization" not in request.headers:
|
|
37
|
+
return httpx.Response(HTTPStatus.UNAUTHORIZED, json={"error": "Unauthorized"})
|
|
38
|
+
|
|
39
|
+
if raise_exception:
|
|
40
|
+
raise raise_exception
|
|
41
|
+
|
|
42
|
+
return httpx.Response(status_code, json=json_data or {"result": "success"}, request=request)
|
|
43
|
+
|
|
44
|
+
return handler
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.mark.asyncio
|
|
48
|
+
async def test_handle_retry():
|
|
49
|
+
"""Test the retry handler."""
|
|
50
|
+
with patch("asyncio.sleep") as mock_sleep:
|
|
51
|
+
mock_sleep.return_value = None
|
|
52
|
+
await _handle_retry(
|
|
53
|
+
attempt=2,
|
|
54
|
+
max_retries=3,
|
|
55
|
+
retry_delay=1.0,
|
|
56
|
+
url="https://example.com",
|
|
57
|
+
error_msg="Test error",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Check exponential backoff formula: delay * (2 ^ (attempt - 1))
|
|
61
|
+
mock_sleep.assert_awaited_once_with(2.0)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.mark.asyncio
|
|
65
|
+
async def test_make_request_success():
|
|
66
|
+
"""Test successful async request."""
|
|
67
|
+
expected_data = {"id": "123", "name": "test"}
|
|
68
|
+
async_client = httpx.AsyncClient(
|
|
69
|
+
transport=httpx.MockTransport(_create_mock_response(200, expected_data))
|
|
70
|
+
)
|
|
71
|
+
result = await make_request(
|
|
72
|
+
"GET", "https://api.test.com/data", api_key="test-key", client=async_client
|
|
73
|
+
)
|
|
74
|
+
assert result == expected_data
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@pytest.mark.asyncio
|
|
78
|
+
async def test_make_request_no_api_key():
|
|
79
|
+
"""Test request without API key."""
|
|
80
|
+
with pytest.raises(HudAuthenticationError):
|
|
81
|
+
await make_request("GET", "https://api.test.com/data", api_key=None)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@pytest.mark.asyncio
|
|
85
|
+
async def test_make_request_http_error():
|
|
86
|
+
"""Test HTTP error handling."""
|
|
87
|
+
async_client = httpx.AsyncClient(
|
|
88
|
+
transport=httpx.MockTransport(_create_mock_response(404, {"error": "Not found"}))
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
with pytest.raises(HudRequestError) as excinfo:
|
|
92
|
+
await make_request(
|
|
93
|
+
"GET", "https://api.test.com/data", api_key="test-key", client=async_client
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
assert "404" in str(excinfo.value)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@pytest.mark.asyncio
|
|
100
|
+
async def test_make_request_network_error():
|
|
101
|
+
"""Test network error handling with retry exhaustion."""
|
|
102
|
+
request_error = httpx.RequestError(
|
|
103
|
+
"Connection error", request=httpx.Request("GET", "https://api.test.com")
|
|
104
|
+
)
|
|
105
|
+
async_client = httpx.AsyncClient(
|
|
106
|
+
transport=httpx.MockTransport(_create_mock_response(raise_exception=request_error))
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Replace handle_retry to avoid sleep
|
|
110
|
+
with patch("hud.server.requests._handle_retry", AsyncMock()) as mock_retry:
|
|
111
|
+
mock_retry.return_value = None
|
|
112
|
+
|
|
113
|
+
with pytest.raises(HudNetworkError) as excinfo:
|
|
114
|
+
await make_request(
|
|
115
|
+
"GET",
|
|
116
|
+
"https://api.test.com/data",
|
|
117
|
+
api_key="test-key",
|
|
118
|
+
max_retries=2,
|
|
119
|
+
retry_delay=0.01,
|
|
120
|
+
client=async_client,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
assert "Connection error" in str(excinfo.value)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@pytest.mark.asyncio
|
|
127
|
+
async def test_make_request_timeout():
|
|
128
|
+
"""Test timeout error handling."""
|
|
129
|
+
timeout_error = httpx.TimeoutException(
|
|
130
|
+
"Request timed out", request=httpx.Request("GET", "https://api.test.com")
|
|
131
|
+
)
|
|
132
|
+
async_client = httpx.AsyncClient(
|
|
133
|
+
transport=httpx.MockTransport(_create_mock_response(raise_exception=timeout_error))
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
with pytest.raises(HudTimeoutError) as excinfo:
|
|
137
|
+
await make_request(
|
|
138
|
+
"GET", "https://api.test.com/data", api_key="test-key", client=async_client
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
assert "timed out" in str(excinfo.value)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@pytest.mark.asyncio
|
|
145
|
+
async def test_make_request_unexpected_error():
|
|
146
|
+
"""Test handling of unexpected errors."""
|
|
147
|
+
unexpected_error = ValueError("Unexpected error")
|
|
148
|
+
async_client = httpx.AsyncClient(
|
|
149
|
+
transport=httpx.MockTransport(_create_mock_response(raise_exception=unexpected_error))
|
|
150
|
+
)
|
|
151
|
+
with pytest.raises(HudRequestError) as excinfo:
|
|
152
|
+
await make_request(
|
|
153
|
+
"GET", "https://api.test.com/data", api_key="test-key", client=async_client
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
assert "Unexpected error" in str(excinfo.value)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@pytest.mark.asyncio
|
|
160
|
+
async def test_make_request_auto_client_creation(mocker):
|
|
161
|
+
"""Test automatic client creation when not provided."""
|
|
162
|
+
mock_create_client = mocker.patch("hud.server.requests._create_default_async_client")
|
|
163
|
+
mock_client = AsyncMock()
|
|
164
|
+
mock_client.request.return_value = httpx.Response(
|
|
165
|
+
200, json={"result": "success"}, request=httpx.Request("GET", "https://api.test.com")
|
|
166
|
+
)
|
|
167
|
+
mock_client.aclose = AsyncMock()
|
|
168
|
+
mock_create_client.return_value = mock_client
|
|
169
|
+
|
|
170
|
+
result = await make_request("GET", "https://api.test.com/data", api_key="test-key")
|
|
171
|
+
|
|
172
|
+
assert result == {"result": "success"}
|
|
173
|
+
mock_client.aclose.assert_awaited_once()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_make_request_sync_success():
|
|
177
|
+
"""Test successful sync request."""
|
|
178
|
+
expected_data = {"id": "123", "name": "test"}
|
|
179
|
+
sync_client = httpx.Client(
|
|
180
|
+
transport=httpx.MockTransport(_create_mock_response(200, expected_data))
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
result = make_request_sync(
|
|
184
|
+
"GET", "https://api.test.com/data", api_key="test-key", client=sync_client
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
assert result == expected_data
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_make_request_sync_no_api_key():
|
|
191
|
+
"""Test sync request without API key."""
|
|
192
|
+
with pytest.raises(HudAuthenticationError):
|
|
193
|
+
make_request_sync("GET", "https://api.test.com/data", api_key=None)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_make_request_sync_http_error():
|
|
197
|
+
"""Test HTTP error handling."""
|
|
198
|
+
sync_client = httpx.Client(
|
|
199
|
+
transport=httpx.MockTransport(_create_mock_response(404, {"error": "Not found"}))
|
|
200
|
+
)
|
|
201
|
+
with pytest.raises(HudRequestError) as excinfo:
|
|
202
|
+
make_request_sync(
|
|
203
|
+
"GET", "https://api.test.com/data", api_key="test-key", client=sync_client
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
assert "404" in str(excinfo.value)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_make_request_sync_network_error():
|
|
210
|
+
"""Test network error handling with retry exhaustion."""
|
|
211
|
+
request_error = httpx.RequestError(
|
|
212
|
+
"Connection error", request=httpx.Request("GET", "https://api.test.com")
|
|
213
|
+
)
|
|
214
|
+
sync_client = httpx.Client(
|
|
215
|
+
transport=httpx.MockTransport(_create_mock_response(raise_exception=request_error))
|
|
216
|
+
)
|
|
217
|
+
with patch("time.sleep", lambda _: None):
|
|
218
|
+
with pytest.raises(HudNetworkError) as excinfo:
|
|
219
|
+
make_request_sync(
|
|
220
|
+
"GET",
|
|
221
|
+
"https://api.test.com/data",
|
|
222
|
+
api_key="test-key",
|
|
223
|
+
max_retries=2,
|
|
224
|
+
retry_delay=0.01,
|
|
225
|
+
client=sync_client,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
assert "Connection error" in str(excinfo.value)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def test_make_request_sync_timeout():
|
|
232
|
+
"""Test timeout error handling."""
|
|
233
|
+
timeout_error = httpx.TimeoutException(
|
|
234
|
+
"Request timed out", request=httpx.Request("GET", "https://api.test.com")
|
|
235
|
+
)
|
|
236
|
+
sync_client = httpx.Client(
|
|
237
|
+
transport=httpx.MockTransport(_create_mock_response(raise_exception=timeout_error))
|
|
238
|
+
)
|
|
239
|
+
with pytest.raises(HudTimeoutError) as excinfo:
|
|
240
|
+
make_request_sync(
|
|
241
|
+
"GET", "https://api.test.com/data", api_key="test-key", client=sync_client
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
assert "timed out" in str(excinfo.value)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_make_request_sync_unexpected_error():
|
|
248
|
+
"""Test handling of unexpected errors."""
|
|
249
|
+
unexpected_error = ValueError("Unexpected error")
|
|
250
|
+
sync_client = httpx.Client(
|
|
251
|
+
transport=httpx.MockTransport(_create_mock_response(raise_exception=unexpected_error))
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
with pytest.raises(HudRequestError) as excinfo:
|
|
255
|
+
make_request_sync(
|
|
256
|
+
"GET", "https://api.test.com/data", api_key="test-key", client=sync_client
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
assert "Unexpected error" in str(excinfo.value)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_make_request_sync_auto_client_creation():
|
|
263
|
+
"""Test automatic client creation when not provided."""
|
|
264
|
+
with patch("hud.server.requests._create_default_sync_client") as mock_create_client:
|
|
265
|
+
mock_client = Mock()
|
|
266
|
+
mock_client.request.return_value = httpx.Response(
|
|
267
|
+
200, json={"result": "success"}, request=httpx.Request("GET", "https://api.test.com")
|
|
268
|
+
)
|
|
269
|
+
mock_client.close = Mock()
|
|
270
|
+
mock_create_client.return_value = mock_client
|
|
271
|
+
|
|
272
|
+
result = make_request_sync("GET", "https://api.test.com/data", api_key="test-key")
|
|
273
|
+
|
|
274
|
+
assert result == {"result": "success"}
|
|
275
|
+
mock_client.close.assert_called_once()
|