hud-python 0.3.4__py3-none-any.whl → 0.4.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 hud-python might be problematic. Click here for more details.

Files changed (192) hide show
  1. hud/__init__.py +22 -89
  2. hud/agents/__init__.py +17 -0
  3. hud/agents/art.py +101 -0
  4. hud/agents/base.py +599 -0
  5. hud/{mcp → agents}/claude.py +373 -321
  6. hud/{mcp → agents}/langchain.py +250 -250
  7. hud/agents/misc/__init__.py +7 -0
  8. hud/{agent → agents}/misc/response_agent.py +80 -80
  9. hud/{mcp → agents}/openai.py +352 -334
  10. hud/agents/openai_chat_generic.py +154 -0
  11. hud/{mcp → agents}/tests/__init__.py +1 -1
  12. hud/agents/tests/test_base.py +742 -0
  13. hud/agents/tests/test_claude.py +324 -0
  14. hud/{mcp → agents}/tests/test_client.py +363 -324
  15. hud/{mcp → agents}/tests/test_openai.py +237 -238
  16. hud/cli/__init__.py +617 -0
  17. hud/cli/__main__.py +8 -0
  18. hud/cli/analyze.py +371 -0
  19. hud/cli/analyze_metadata.py +230 -0
  20. hud/cli/build.py +427 -0
  21. hud/cli/clone.py +185 -0
  22. hud/cli/cursor.py +92 -0
  23. hud/cli/debug.py +392 -0
  24. hud/cli/docker_utils.py +83 -0
  25. hud/cli/init.py +281 -0
  26. hud/cli/interactive.py +353 -0
  27. hud/cli/mcp_server.py +756 -0
  28. hud/cli/pull.py +336 -0
  29. hud/cli/push.py +379 -0
  30. hud/cli/remote_runner.py +311 -0
  31. hud/cli/runner.py +160 -0
  32. hud/cli/tests/__init__.py +3 -0
  33. hud/cli/tests/test_analyze.py +284 -0
  34. hud/cli/tests/test_cli_init.py +265 -0
  35. hud/cli/tests/test_cli_main.py +27 -0
  36. hud/cli/tests/test_clone.py +142 -0
  37. hud/cli/tests/test_cursor.py +253 -0
  38. hud/cli/tests/test_debug.py +453 -0
  39. hud/cli/tests/test_mcp_server.py +139 -0
  40. hud/cli/tests/test_utils.py +388 -0
  41. hud/cli/utils.py +263 -0
  42. hud/clients/README.md +143 -0
  43. hud/clients/__init__.py +16 -0
  44. hud/clients/base.py +354 -0
  45. hud/clients/fastmcp.py +202 -0
  46. hud/clients/mcp_use.py +278 -0
  47. hud/clients/tests/__init__.py +1 -0
  48. hud/clients/tests/test_client_integration.py +111 -0
  49. hud/clients/tests/test_fastmcp.py +342 -0
  50. hud/clients/tests/test_protocol.py +188 -0
  51. hud/clients/utils/__init__.py +1 -0
  52. hud/clients/utils/retry_transport.py +160 -0
  53. hud/datasets.py +322 -192
  54. hud/misc/__init__.py +1 -0
  55. hud/{agent → misc}/claude_plays_pokemon.py +292 -283
  56. hud/otel/__init__.py +35 -0
  57. hud/otel/collector.py +142 -0
  58. hud/otel/config.py +164 -0
  59. hud/otel/context.py +536 -0
  60. hud/otel/exporters.py +366 -0
  61. hud/otel/instrumentation.py +97 -0
  62. hud/otel/processors.py +118 -0
  63. hud/otel/tests/__init__.py +1 -0
  64. hud/otel/tests/test_processors.py +197 -0
  65. hud/server/__init__.py +5 -5
  66. hud/server/context.py +114 -0
  67. hud/server/helper/__init__.py +5 -0
  68. hud/server/low_level.py +132 -0
  69. hud/server/server.py +166 -0
  70. hud/server/tests/__init__.py +3 -0
  71. hud/settings.py +73 -79
  72. hud/shared/__init__.py +5 -0
  73. hud/{exceptions.py → shared/exceptions.py} +180 -180
  74. hud/{server → shared}/requests.py +264 -264
  75. hud/shared/tests/test_exceptions.py +157 -0
  76. hud/{server → shared}/tests/test_requests.py +275 -275
  77. hud/telemetry/__init__.py +25 -30
  78. hud/telemetry/instrument.py +379 -0
  79. hud/telemetry/job.py +309 -141
  80. hud/telemetry/replay.py +74 -0
  81. hud/telemetry/trace.py +83 -0
  82. hud/tools/__init__.py +33 -34
  83. hud/tools/base.py +365 -65
  84. hud/tools/bash.py +161 -137
  85. hud/tools/computer/__init__.py +15 -13
  86. hud/tools/computer/anthropic.py +437 -414
  87. hud/tools/computer/hud.py +376 -328
  88. hud/tools/computer/openai.py +295 -286
  89. hud/tools/computer/settings.py +82 -0
  90. hud/tools/edit.py +314 -290
  91. hud/tools/executors/__init__.py +30 -30
  92. hud/tools/executors/base.py +539 -532
  93. hud/tools/executors/pyautogui.py +621 -619
  94. hud/tools/executors/tests/__init__.py +1 -1
  95. hud/tools/executors/tests/test_base_executor.py +338 -338
  96. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  97. hud/tools/executors/xdo.py +511 -503
  98. hud/tools/{playwright_tool.py → playwright.py} +412 -379
  99. hud/tools/tests/__init__.py +3 -3
  100. hud/tools/tests/test_base.py +282 -0
  101. hud/tools/tests/test_bash.py +158 -152
  102. hud/tools/tests/test_bash_extended.py +197 -0
  103. hud/tools/tests/test_computer.py +425 -52
  104. hud/tools/tests/test_computer_actions.py +34 -34
  105. hud/tools/tests/test_edit.py +259 -240
  106. hud/tools/tests/test_init.py +27 -27
  107. hud/tools/tests/test_playwright_tool.py +183 -183
  108. hud/tools/tests/test_tools.py +145 -157
  109. hud/tools/tests/test_utils.py +156 -156
  110. hud/tools/types.py +72 -0
  111. hud/tools/utils.py +50 -50
  112. hud/types.py +136 -89
  113. hud/utils/__init__.py +10 -16
  114. hud/utils/async_utils.py +65 -0
  115. hud/utils/design.py +168 -0
  116. hud/utils/mcp.py +55 -0
  117. hud/utils/progress.py +149 -149
  118. hud/utils/telemetry.py +66 -66
  119. hud/utils/tests/test_async_utils.py +173 -0
  120. hud/utils/tests/test_init.py +17 -21
  121. hud/utils/tests/test_progress.py +261 -225
  122. hud/utils/tests/test_telemetry.py +82 -37
  123. hud/utils/tests/test_version.py +8 -8
  124. hud/version.py +7 -7
  125. hud_python-0.4.0.dist-info/METADATA +474 -0
  126. hud_python-0.4.0.dist-info/RECORD +132 -0
  127. hud_python-0.4.0.dist-info/entry_points.txt +3 -0
  128. {hud_python-0.3.4.dist-info → hud_python-0.4.0.dist-info}/licenses/LICENSE +21 -21
  129. hud/adapters/__init__.py +0 -8
  130. hud/adapters/claude/__init__.py +0 -5
  131. hud/adapters/claude/adapter.py +0 -180
  132. hud/adapters/claude/tests/__init__.py +0 -1
  133. hud/adapters/claude/tests/test_adapter.py +0 -519
  134. hud/adapters/common/__init__.py +0 -6
  135. hud/adapters/common/adapter.py +0 -178
  136. hud/adapters/common/tests/test_adapter.py +0 -289
  137. hud/adapters/common/types.py +0 -446
  138. hud/adapters/operator/__init__.py +0 -5
  139. hud/adapters/operator/adapter.py +0 -108
  140. hud/adapters/operator/tests/__init__.py +0 -1
  141. hud/adapters/operator/tests/test_adapter.py +0 -370
  142. hud/agent/__init__.py +0 -19
  143. hud/agent/base.py +0 -126
  144. hud/agent/claude.py +0 -271
  145. hud/agent/langchain.py +0 -215
  146. hud/agent/misc/__init__.py +0 -3
  147. hud/agent/operator.py +0 -268
  148. hud/agent/tests/__init__.py +0 -1
  149. hud/agent/tests/test_base.py +0 -202
  150. hud/env/__init__.py +0 -11
  151. hud/env/client.py +0 -35
  152. hud/env/docker_client.py +0 -349
  153. hud/env/environment.py +0 -446
  154. hud/env/local_docker_client.py +0 -358
  155. hud/env/remote_client.py +0 -212
  156. hud/env/remote_docker_client.py +0 -292
  157. hud/gym.py +0 -130
  158. hud/job.py +0 -773
  159. hud/mcp/__init__.py +0 -17
  160. hud/mcp/base.py +0 -631
  161. hud/mcp/client.py +0 -312
  162. hud/mcp/tests/test_base.py +0 -512
  163. hud/mcp/tests/test_claude.py +0 -294
  164. hud/task.py +0 -149
  165. hud/taskset.py +0 -237
  166. hud/telemetry/_trace.py +0 -347
  167. hud/telemetry/context.py +0 -230
  168. hud/telemetry/exporter.py +0 -575
  169. hud/telemetry/instrumentation/__init__.py +0 -3
  170. hud/telemetry/instrumentation/mcp.py +0 -259
  171. hud/telemetry/instrumentation/registry.py +0 -59
  172. hud/telemetry/mcp_models.py +0 -270
  173. hud/telemetry/tests/__init__.py +0 -1
  174. hud/telemetry/tests/test_context.py +0 -210
  175. hud/telemetry/tests/test_trace.py +0 -312
  176. hud/tools/helper/README.md +0 -56
  177. hud/tools/helper/__init__.py +0 -9
  178. hud/tools/helper/mcp_server.py +0 -78
  179. hud/tools/helper/server_initialization.py +0 -115
  180. hud/tools/helper/utils.py +0 -58
  181. hud/trajectory.py +0 -94
  182. hud/utils/agent.py +0 -37
  183. hud/utils/common.py +0 -256
  184. hud/utils/config.py +0 -120
  185. hud/utils/deprecation.py +0 -115
  186. hud/utils/misc.py +0 -53
  187. hud/utils/tests/test_common.py +0 -277
  188. hud/utils/tests/test_config.py +0 -129
  189. hud_python-0.3.4.dist-info/METADATA +0 -284
  190. hud_python-0.3.4.dist-info/RECORD +0 -120
  191. /hud/{adapters/common → shared}/tests/__init__.py +0 -0
  192. {hud_python-0.3.4.dist-info → hud_python-0.4.0.dist-info}/WHEEL +0 -0
@@ -1,275 +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()
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.shared.exceptions import (
13
+ HudAuthenticationError,
14
+ HudNetworkError,
15
+ HudRequestError,
16
+ HudTimeoutError,
17
+ )
18
+ from hud.shared.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.shared.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():
161
+ """Test automatic client creation when not provided."""
162
+ with patch("hud.shared.requests._create_default_async_client") as mock_create_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.shared.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()
hud/telemetry/__init__.py CHANGED
@@ -1,30 +1,25 @@
1
- """
2
- HUD Telemetry module.
3
-
4
- Provides context managers and utilities for capturing MCP telemetry data.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- # Main trace functions
10
- from hud.telemetry._trace import init_telemetry, trace, trace_open
11
- from hud.telemetry.context import flush_buffer, get_current_task_run_id
12
- from hud.telemetry.exporter import flush
13
- from hud.telemetry.job import get_current_job_id, get_current_job_name, job
14
-
15
- __all__ = [
16
- # Management
17
- "flush",
18
- "flush_buffer",
19
- # Context management
20
- "get_current_job_id",
21
- "get_current_job_name",
22
- "get_current_task_run_id",
23
- # Management
24
- "init_telemetry",
25
- # Job context
26
- "job",
27
- # Trace functions
28
- "trace",
29
- "trace_open",
30
- ]
1
+ """HUD Telemetry - User-facing APIs for tracing and job management.
2
+
3
+ This module provides the main telemetry APIs that users interact with:
4
+ - trace: Context manager for tracing code execution
5
+ - job: Context manager and utilities for job management
6
+ - instrument: Decorator for instrumenting functions
7
+ - get_trace: Retrieve collected traces for replay/analysis
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from .instrument import instrument
13
+ from .job import Job, create_job, job
14
+ from .replay import clear_trace, get_trace
15
+ from .trace import trace
16
+
17
+ __all__ = [
18
+ "Job",
19
+ "clear_trace",
20
+ "create_job",
21
+ "get_trace",
22
+ "instrument",
23
+ "job",
24
+ "trace",
25
+ ]