hud-python 0.3.5__py3-none-any.whl → 0.4.1__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 +15 -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 +370 -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 +379 -0
  45. hud/clients/fastmcp.py +222 -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 -420
  87. hud/tools/computer/hud.py +376 -334
  88. hud/tools/computer/openai.py +295 -292
  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.1.dist-info/METADATA +476 -0
  126. hud_python-0.4.1.dist-info/RECORD +132 -0
  127. hud_python-0.4.1.dist-info/entry_points.txt +3 -0
  128. {hud_python-0.3.5.dist-info → hud_python-0.4.1.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.5.dist-info/METADATA +0 -284
  190. hud_python-0.3.5.dist-info/RECORD +0 -120
  191. /hud/{adapters/common → shared}/tests/__init__.py +0 -0
  192. {hud_python-0.3.5.dist-info → hud_python-0.4.1.dist-info}/WHEEL +0 -0
@@ -1,264 +1,264 @@
1
- """
2
- HTTP request utilities for the HUD API.
3
- """
4
-
5
- from __future__ import annotations
6
-
7
- import asyncio
8
- import logging
9
- import ssl
10
- import time
11
- from typing import Any
12
-
13
- import httpx
14
-
15
- from hud.exceptions import (
16
- HudAuthenticationError,
17
- HudNetworkError,
18
- HudRequestError,
19
- HudTimeoutError,
20
- )
21
-
22
- # Set up logger
23
- logger = logging.getLogger("hud.http")
24
- logger.setLevel(logging.INFO)
25
-
26
-
27
- # Long running requests can take up to 10 minutes.
28
- _DEFAULT_TIMEOUT = 600.0
29
- _DEFAULT_LIMITS = httpx.Limits(
30
- max_connections=1000,
31
- max_keepalive_connections=1000,
32
- keepalive_expiry=10.0,
33
- )
34
-
35
-
36
- async def _handle_retry(
37
- attempt: int, max_retries: int, retry_delay: float, url: str, error_msg: str
38
- ) -> None:
39
- """Helper function to handle retry logic and logging."""
40
- retry_time = retry_delay * (2 ** (attempt - 1)) # Exponential backoff
41
- logger.debug(
42
- "%s from %s, retrying in %.2f seconds (attempt %d/%d)",
43
- error_msg,
44
- url,
45
- retry_time,
46
- attempt,
47
- max_retries,
48
- )
49
- await asyncio.sleep(retry_time)
50
-
51
-
52
- def _create_default_async_client() -> httpx.AsyncClient:
53
- """Create a default httpx AsyncClient with standard configuration."""
54
- return httpx.AsyncClient(
55
- timeout=_DEFAULT_TIMEOUT,
56
- limits=_DEFAULT_LIMITS,
57
- )
58
-
59
-
60
- def _create_default_sync_client() -> httpx.Client:
61
- """Create a default httpx Client with standard configuration."""
62
- return httpx.Client(
63
- timeout=_DEFAULT_TIMEOUT,
64
- limits=_DEFAULT_LIMITS,
65
- )
66
-
67
-
68
- async def make_request(
69
- method: str,
70
- url: str,
71
- json: Any | None = None,
72
- api_key: str | None = None,
73
- max_retries: int = 4,
74
- retry_delay: float = 2.0,
75
- client: httpx.AsyncClient | None = None,
76
- ) -> dict[str, Any]:
77
- """
78
- Make an asynchronous HTTP request to the HUD API.
79
-
80
- Args:
81
- method: HTTP method (GET, POST, etc.)
82
- url: Full URL for the request
83
- json: Optional JSON serializable data
84
- api_key: API key for authentication
85
- max_retries: Maximum number of retries
86
- retry_delay: Delay between retries
87
- *,
88
- client: Optional custom httpx.AsyncClient
89
-
90
- Returns:
91
- dict: JSON response from the server
92
-
93
- Raises:
94
- HudAuthenticationError: If API key is missing or invalid.
95
- HudRequestError: If the request fails with a non-retryable status code.
96
- HudNetworkError: If there are network-related issues.
97
- HudTimeoutError: If the request times out.
98
- """
99
- if not api_key:
100
- raise HudAuthenticationError("API key is required but not provided")
101
-
102
- headers = {"Authorization": f"Bearer {api_key}"}
103
- retry_status_codes = [502, 503, 504]
104
- attempt = 0
105
- should_close_client = False
106
-
107
- if client is None:
108
- client = _create_default_async_client()
109
- should_close_client = True
110
-
111
- try:
112
- while attempt <= max_retries:
113
- attempt += 1
114
-
115
- try:
116
- response = await client.request(method=method, url=url, json=json, headers=headers)
117
-
118
- # Check if we got a retriable status code
119
- if response.status_code in retry_status_codes and attempt <= max_retries:
120
- await _handle_retry(
121
- attempt,
122
- max_retries,
123
- retry_delay,
124
- url,
125
- f"Received status {response.status_code}",
126
- )
127
- continue
128
-
129
- response.raise_for_status()
130
- result = response.json()
131
- return result
132
- except httpx.TimeoutException as e:
133
- raise HudTimeoutError(f"Request timed out: {e!s}") from None
134
- except httpx.HTTPStatusError as e:
135
- raise HudRequestError.from_httpx_error(e) from None
136
- except httpx.RequestError as e:
137
- if attempt <= max_retries:
138
- await _handle_retry(
139
- attempt, max_retries, retry_delay, url, f"Network error: {e}"
140
- )
141
- continue
142
- else:
143
- raise HudNetworkError(f"Network error: {e!s}") from None
144
- except ssl.SSLError as e:
145
- if attempt <= max_retries:
146
- await _handle_retry(attempt, max_retries, retry_delay, url, f"SSL error: {e}")
147
- continue
148
- else:
149
- raise HudNetworkError(f"SSL error: {e!s}") from None
150
- except Exception as e:
151
- raise HudRequestError(f"Unexpected error: {e!s}") from None
152
- raise HudRequestError(f"Request failed after {max_retries} retries with unknown error")
153
- finally:
154
- if should_close_client:
155
- await client.aclose()
156
-
157
-
158
- def make_request_sync(
159
- method: str,
160
- url: str,
161
- json: Any | None = None,
162
- api_key: str | None = None,
163
- max_retries: int = 4,
164
- retry_delay: float = 2.0,
165
- *,
166
- client: httpx.Client | None = None,
167
- ) -> dict[str, Any]:
168
- """
169
- Make a synchronous HTTP request to the HUD API.
170
-
171
- Args:
172
- method: HTTP method (GET, POST, etc.)
173
- url: Full URL for the request
174
- json: Optional JSON serializable data
175
- api_key: API key for authentication
176
- max_retries: Maximum number of retries
177
- retry_delay: Delay between retries
178
- client: Optional custom httpx.Client
179
-
180
- Returns:
181
- dict: JSON response from the server
182
-
183
- Raises:
184
- HudAuthenticationError: If API key is missing or invalid.
185
- HudRequestError: If the request fails with a non-retryable status code.
186
- HudNetworkError: If there are network-related issues.
187
- HudTimeoutError: If the request times out.
188
- """
189
- if not api_key:
190
- raise HudAuthenticationError("API key is required but not provided")
191
-
192
- headers = {"Authorization": f"Bearer {api_key}"}
193
- retry_status_codes = [502, 503, 504]
194
- attempt = 0
195
- should_close_client = False
196
-
197
- if client is None:
198
- client = _create_default_sync_client()
199
- should_close_client = True
200
-
201
- try:
202
- while attempt <= max_retries:
203
- attempt += 1
204
-
205
- try:
206
- response = client.request(method=method, url=url, json=json, headers=headers)
207
-
208
- # Check if we got a retriable status code
209
- if response.status_code in retry_status_codes and attempt <= max_retries:
210
- retry_time = retry_delay * (2 ** (attempt - 1)) # Exponential backoff
211
- logger.debug(
212
- "Received status %d from %s, retrying in %.2f seconds (attempt %d/%d)",
213
- response.status_code,
214
- url,
215
- retry_time,
216
- attempt,
217
- max_retries,
218
- )
219
- time.sleep(retry_time)
220
- continue
221
-
222
- response.raise_for_status()
223
- result = response.json()
224
- return result
225
- except httpx.TimeoutException as e:
226
- raise HudTimeoutError(f"Request timed out: {e!s}") from None
227
- except httpx.HTTPStatusError as e:
228
- raise HudRequestError.from_httpx_error(e) from None
229
- except httpx.RequestError as e:
230
- if attempt <= max_retries:
231
- retry_time = retry_delay * (2 ** (attempt - 1))
232
- logger.debug(
233
- "Network error %s from %s, retrying in %.2f seconds (attempt %d/%d)",
234
- str(e),
235
- url,
236
- retry_time,
237
- attempt,
238
- max_retries,
239
- )
240
- time.sleep(retry_time)
241
- continue
242
- else:
243
- raise HudNetworkError(f"Network error: {e!s}") from None
244
- except ssl.SSLError as e:
245
- if attempt <= max_retries:
246
- retry_time = retry_delay * (2 ** (attempt - 1)) # Exponential backoff
247
- logger.debug(
248
- "SSL error %s from %s, retrying in %.2f seconds (attempt %d/%d)",
249
- str(e),
250
- url,
251
- retry_time,
252
- attempt,
253
- max_retries,
254
- )
255
- time.sleep(retry_time)
256
- continue
257
- else:
258
- raise HudNetworkError(f"SSL error: {e!s}") from None
259
- except Exception as e:
260
- raise HudRequestError(f"Unexpected error: {e!s}") from None
261
- raise HudRequestError(f"Request failed after {max_retries} retries with unknown error")
262
- finally:
263
- if should_close_client:
264
- client.close()
1
+ """
2
+ HTTP request utilities for the HUD API.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import asyncio
8
+ import logging
9
+ import ssl
10
+ import time
11
+ from typing import Any
12
+
13
+ import httpx
14
+
15
+ from hud.shared.exceptions import (
16
+ HudAuthenticationError,
17
+ HudNetworkError,
18
+ HudRequestError,
19
+ HudTimeoutError,
20
+ )
21
+
22
+ # Set up logger
23
+ logger = logging.getLogger("hud.http")
24
+ logger.setLevel(logging.INFO)
25
+
26
+
27
+ # Long running requests can take up to 10 minutes.
28
+ _DEFAULT_TIMEOUT = 600.0
29
+ _DEFAULT_LIMITS = httpx.Limits(
30
+ max_connections=1000,
31
+ max_keepalive_connections=1000,
32
+ keepalive_expiry=10.0,
33
+ )
34
+
35
+
36
+ async def _handle_retry(
37
+ attempt: int, max_retries: int, retry_delay: float, url: str, error_msg: str
38
+ ) -> None:
39
+ """Helper function to handle retry logic and logging."""
40
+ retry_time = retry_delay * (2 ** (attempt - 1)) # Exponential backoff
41
+ logger.debug(
42
+ "%s from %s, retrying in %.2f seconds (attempt %d/%d)",
43
+ error_msg,
44
+ url,
45
+ retry_time,
46
+ attempt,
47
+ max_retries,
48
+ )
49
+ await asyncio.sleep(retry_time)
50
+
51
+
52
+ def _create_default_async_client() -> httpx.AsyncClient:
53
+ """Create a default httpx AsyncClient with standard configuration."""
54
+ return httpx.AsyncClient(
55
+ timeout=_DEFAULT_TIMEOUT,
56
+ limits=_DEFAULT_LIMITS,
57
+ )
58
+
59
+
60
+ def _create_default_sync_client() -> httpx.Client:
61
+ """Create a default httpx Client with standard configuration."""
62
+ return httpx.Client(
63
+ timeout=_DEFAULT_TIMEOUT,
64
+ limits=_DEFAULT_LIMITS,
65
+ )
66
+
67
+
68
+ async def make_request(
69
+ method: str,
70
+ url: str,
71
+ json: Any | None = None,
72
+ api_key: str | None = None,
73
+ max_retries: int = 4,
74
+ retry_delay: float = 2.0,
75
+ client: httpx.AsyncClient | None = None,
76
+ ) -> dict[str, Any]:
77
+ """
78
+ Make an asynchronous HTTP request to the HUD API.
79
+
80
+ Args:
81
+ method: HTTP method (GET, POST, etc.)
82
+ url: Full URL for the request
83
+ json: Optional JSON serializable data
84
+ api_key: API key for authentication
85
+ max_retries: Maximum number of retries
86
+ retry_delay: Delay between retries
87
+ *,
88
+ client: Optional custom httpx.AsyncClient
89
+
90
+ Returns:
91
+ dict: JSON response from the server
92
+
93
+ Raises:
94
+ HudAuthenticationError: If API key is missing or invalid.
95
+ HudRequestError: If the request fails with a non-retryable status code.
96
+ HudNetworkError: If there are network-related issues.
97
+ HudTimeoutError: If the request times out.
98
+ """
99
+ if not api_key:
100
+ raise HudAuthenticationError("API key is required but not provided")
101
+
102
+ headers = {"Authorization": f"Bearer {api_key}"}
103
+ retry_status_codes = [502, 503, 504]
104
+ attempt = 0
105
+ should_close_client = False
106
+
107
+ if client is None:
108
+ client = _create_default_async_client()
109
+ should_close_client = True
110
+
111
+ try:
112
+ while attempt <= max_retries:
113
+ attempt += 1
114
+
115
+ try:
116
+ response = await client.request(method=method, url=url, json=json, headers=headers)
117
+
118
+ # Check if we got a retriable status code
119
+ if response.status_code in retry_status_codes and attempt <= max_retries:
120
+ await _handle_retry(
121
+ attempt,
122
+ max_retries,
123
+ retry_delay,
124
+ url,
125
+ f"Received status {response.status_code}",
126
+ )
127
+ continue
128
+
129
+ response.raise_for_status()
130
+ result = response.json()
131
+ return result
132
+ except httpx.TimeoutException as e:
133
+ raise HudTimeoutError(f"Request timed out: {e!s}") from None
134
+ except httpx.HTTPStatusError as e:
135
+ raise HudRequestError.from_httpx_error(e) from None
136
+ except httpx.RequestError as e:
137
+ if attempt <= max_retries:
138
+ await _handle_retry(
139
+ attempt, max_retries, retry_delay, url, f"Network error: {e}"
140
+ )
141
+ continue
142
+ else:
143
+ raise HudNetworkError(f"Network error: {e!s}") from None
144
+ except ssl.SSLError as e:
145
+ if attempt <= max_retries:
146
+ await _handle_retry(attempt, max_retries, retry_delay, url, f"SSL error: {e}")
147
+ continue
148
+ else:
149
+ raise HudNetworkError(f"SSL error: {e!s}") from None
150
+ except Exception as e:
151
+ raise HudRequestError(f"Unexpected error: {e!s}") from None
152
+ raise HudRequestError(f"Request failed after {max_retries} retries with unknown error")
153
+ finally:
154
+ if should_close_client:
155
+ await client.aclose()
156
+
157
+
158
+ def make_request_sync(
159
+ method: str,
160
+ url: str,
161
+ json: Any | None = None,
162
+ api_key: str | None = None,
163
+ max_retries: int = 4,
164
+ retry_delay: float = 2.0,
165
+ *,
166
+ client: httpx.Client | None = None,
167
+ ) -> dict[str, Any]:
168
+ """
169
+ Make a synchronous HTTP request to the HUD API.
170
+
171
+ Args:
172
+ method: HTTP method (GET, POST, etc.)
173
+ url: Full URL for the request
174
+ json: Optional JSON serializable data
175
+ api_key: API key for authentication
176
+ max_retries: Maximum number of retries
177
+ retry_delay: Delay between retries
178
+ client: Optional custom httpx.Client
179
+
180
+ Returns:
181
+ dict: JSON response from the server
182
+
183
+ Raises:
184
+ HudAuthenticationError: If API key is missing or invalid.
185
+ HudRequestError: If the request fails with a non-retryable status code.
186
+ HudNetworkError: If there are network-related issues.
187
+ HudTimeoutError: If the request times out.
188
+ """
189
+ if not api_key:
190
+ raise HudAuthenticationError("API key is required but not provided")
191
+
192
+ headers = {"Authorization": f"Bearer {api_key}"}
193
+ retry_status_codes = [502, 503, 504]
194
+ attempt = 0
195
+ should_close_client = False
196
+
197
+ if client is None:
198
+ client = _create_default_sync_client()
199
+ should_close_client = True
200
+
201
+ try:
202
+ while attempt <= max_retries:
203
+ attempt += 1
204
+
205
+ try:
206
+ response = client.request(method=method, url=url, json=json, headers=headers)
207
+
208
+ # Check if we got a retriable status code
209
+ if response.status_code in retry_status_codes and attempt <= max_retries:
210
+ retry_time = retry_delay * (2 ** (attempt - 1)) # Exponential backoff
211
+ logger.debug(
212
+ "Received status %d from %s, retrying in %.2f seconds (attempt %d/%d)",
213
+ response.status_code,
214
+ url,
215
+ retry_time,
216
+ attempt,
217
+ max_retries,
218
+ )
219
+ time.sleep(retry_time)
220
+ continue
221
+
222
+ response.raise_for_status()
223
+ result = response.json()
224
+ return result
225
+ except httpx.TimeoutException as e:
226
+ raise HudTimeoutError(f"Request timed out: {e!s}") from None
227
+ except httpx.HTTPStatusError as e:
228
+ raise HudRequestError.from_httpx_error(e) from None
229
+ except httpx.RequestError as e:
230
+ if attempt <= max_retries:
231
+ retry_time = retry_delay * (2 ** (attempt - 1))
232
+ logger.debug(
233
+ "Network error %s from %s, retrying in %.2f seconds (attempt %d/%d)",
234
+ str(e),
235
+ url,
236
+ retry_time,
237
+ attempt,
238
+ max_retries,
239
+ )
240
+ time.sleep(retry_time)
241
+ continue
242
+ else:
243
+ raise HudNetworkError(f"Network error: {e!s}") from None
244
+ except ssl.SSLError as e:
245
+ if attempt <= max_retries:
246
+ retry_time = retry_delay * (2 ** (attempt - 1)) # Exponential backoff
247
+ logger.debug(
248
+ "SSL error %s from %s, retrying in %.2f seconds (attempt %d/%d)",
249
+ str(e),
250
+ url,
251
+ retry_time,
252
+ attempt,
253
+ max_retries,
254
+ )
255
+ time.sleep(retry_time)
256
+ continue
257
+ else:
258
+ raise HudNetworkError(f"SSL error: {e!s}") from None
259
+ except Exception as e:
260
+ raise HudRequestError(f"Unexpected error: {e!s}") from None
261
+ raise HudRequestError(f"Request failed after {max_retries} retries with unknown error")
262
+ finally:
263
+ if should_close_client:
264
+ client.close()