hud-python 0.4.11__py3-none-any.whl → 0.4.13__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 (63) hide show
  1. hud/__main__.py +8 -0
  2. hud/agents/base.py +7 -8
  3. hud/agents/langchain.py +2 -2
  4. hud/agents/tests/test_openai.py +3 -1
  5. hud/cli/__init__.py +114 -52
  6. hud/cli/build.py +121 -71
  7. hud/cli/debug.py +2 -2
  8. hud/cli/{mcp_server.py → dev.py} +101 -38
  9. hud/cli/eval.py +175 -90
  10. hud/cli/init.py +442 -64
  11. hud/cli/list_func.py +72 -71
  12. hud/cli/pull.py +1 -2
  13. hud/cli/push.py +35 -23
  14. hud/cli/remove.py +35 -41
  15. hud/cli/tests/test_analyze.py +2 -1
  16. hud/cli/tests/test_analyze_metadata.py +42 -49
  17. hud/cli/tests/test_build.py +28 -52
  18. hud/cli/tests/test_cursor.py +1 -1
  19. hud/cli/tests/test_debug.py +1 -1
  20. hud/cli/tests/test_list_func.py +75 -64
  21. hud/cli/tests/test_main_module.py +30 -0
  22. hud/cli/tests/test_mcp_server.py +3 -3
  23. hud/cli/tests/test_pull.py +30 -61
  24. hud/cli/tests/test_push.py +70 -89
  25. hud/cli/tests/test_registry.py +36 -38
  26. hud/cli/tests/test_utils.py +1 -1
  27. hud/cli/utils/__init__.py +1 -0
  28. hud/cli/{docker_utils.py → utils/docker.py} +36 -0
  29. hud/cli/{env_utils.py → utils/environment.py} +7 -7
  30. hud/cli/{interactive.py → utils/interactive.py} +91 -19
  31. hud/cli/{analyze_metadata.py → utils/metadata.py} +12 -8
  32. hud/cli/{registry.py → utils/registry.py} +28 -30
  33. hud/cli/{remote_runner.py → utils/remote_runner.py} +1 -1
  34. hud/cli/utils/runner.py +134 -0
  35. hud/cli/utils/server.py +250 -0
  36. hud/clients/base.py +1 -1
  37. hud/clients/fastmcp.py +5 -13
  38. hud/clients/mcp_use.py +6 -10
  39. hud/server/server.py +35 -5
  40. hud/shared/exceptions.py +11 -0
  41. hud/shared/tests/test_exceptions.py +22 -0
  42. hud/telemetry/tests/__init__.py +0 -0
  43. hud/telemetry/tests/test_replay.py +40 -0
  44. hud/telemetry/tests/test_trace.py +63 -0
  45. hud/tools/base.py +20 -3
  46. hud/tools/computer/hud.py +15 -6
  47. hud/tools/executors/tests/test_base_executor.py +27 -0
  48. hud/tools/response.py +12 -8
  49. hud/tools/tests/test_response.py +60 -0
  50. hud/tools/tests/test_tools_init.py +49 -0
  51. hud/utils/design.py +19 -8
  52. hud/utils/mcp.py +17 -5
  53. hud/utils/tests/test_mcp.py +112 -0
  54. hud/utils/tests/test_version.py +1 -1
  55. hud/version.py +1 -1
  56. {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/METADATA +16 -13
  57. {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/RECORD +62 -52
  58. hud/cli/runner.py +0 -160
  59. /hud/cli/{cursor.py → utils/cursor.py} +0 -0
  60. /hud/cli/{utils.py → utils/logging.py} +0 -0
  61. {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/WHEEL +0 -0
  62. {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/entry_points.txt +0 -0
  63. {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/licenses/LICENSE +0 -0
@@ -3,20 +3,17 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
- import os
7
- import tempfile
8
- from pathlib import Path
9
6
  from unittest import mock
10
7
 
11
8
  import pytest
12
9
  import yaml
13
10
 
14
- from hud.cli.analyze_metadata import (
11
+ from hud.cli.utils.metadata import (
15
12
  analyze_from_metadata,
16
13
  check_local_cache,
17
14
  fetch_lock_from_registry,
18
15
  )
19
- from hud.cli.registry import save_to_registry
16
+ from hud.cli.utils.registry import save_to_registry
20
17
 
21
18
 
22
19
  @pytest.fixture
@@ -79,9 +76,7 @@ class TestFetchLockFromRegistry:
79
76
  """Test successful fetch from registry."""
80
77
  mock_response = mock.Mock()
81
78
  mock_response.status_code = 200
82
- mock_response.json.return_value = {
83
- "lock": yaml.dump({"test": "data"})
84
- }
79
+ mock_response.json.return_value = {"lock": yaml.dump({"test": "data"})}
85
80
  mock_get.return_value = mock_response
86
81
 
87
82
  result = fetch_lock_from_registry("test/env:latest")
@@ -93,9 +88,7 @@ class TestFetchLockFromRegistry:
93
88
  """Test fetch when response has lock_data key."""
94
89
  mock_response = mock.Mock()
95
90
  mock_response.status_code = 200
96
- mock_response.json.return_value = {
97
- "lock_data": {"test": "data"}
98
- }
91
+ mock_response.json.return_value = {"lock_data": {"test": "data"}}
99
92
  mock_get.return_value = mock_response
100
93
 
101
94
  result = fetch_lock_from_registry("test/env:latest")
@@ -120,10 +113,10 @@ class TestFetchLockFromRegistry:
120
113
  mock_get.return_value = mock_response
121
114
 
122
115
  fetch_lock_from_registry("test/env")
123
-
124
- # Check that the URL includes :latest
116
+
117
+ # Check that the URL includes :latest (URL-encoded)
125
118
  call_args = mock_get.call_args
126
- assert "test/env:latest" in call_args[0][0]
119
+ assert "test/env%3Alatest" in call_args[0][0]
127
120
 
128
121
  @mock.patch("requests.get")
129
122
  def test_fetch_lock_failure(self, mock_get):
@@ -150,11 +143,11 @@ class TestCheckLocalCache:
150
143
  def test_check_local_cache_found(self, mock_registry_dir, sample_lock_data, monkeypatch):
151
144
  """Test finding lock data in local cache."""
152
145
  # Mock registry directory
153
- monkeypatch.setattr("hud.cli.registry.get_registry_dir", lambda: mock_registry_dir)
154
-
146
+ monkeypatch.setattr("hud.cli.utils.registry.get_registry_dir", lambda: mock_registry_dir)
147
+
155
148
  # Save sample data to registry
156
149
  save_to_registry(sample_lock_data, "test/environment:latest", verbose=False)
157
-
150
+
158
151
  # Check cache
159
152
  result = check_local_cache("test/environment:latest")
160
153
  assert result is not None
@@ -162,21 +155,21 @@ class TestCheckLocalCache:
162
155
 
163
156
  def test_check_local_cache_not_found(self, mock_registry_dir, monkeypatch):
164
157
  """Test when lock data not in local cache."""
165
- monkeypatch.setattr("hud.cli.registry.get_registry_dir", lambda: mock_registry_dir)
166
-
158
+ monkeypatch.setattr("hud.cli.utils.registry.get_registry_dir", lambda: mock_registry_dir)
159
+
167
160
  result = check_local_cache("nonexistent/env:latest")
168
161
  assert result is None
169
162
 
170
163
  def test_check_local_cache_invalid_yaml(self, mock_registry_dir, monkeypatch):
171
164
  """Test when lock file has invalid YAML."""
172
- monkeypatch.setattr("hud.cli.registry.get_registry_dir", lambda: mock_registry_dir)
173
-
165
+ monkeypatch.setattr("hud.cli.utils.registry.get_registry_dir", lambda: mock_registry_dir)
166
+
174
167
  # Create invalid lock file
175
- digest = "sha256:invalid"
168
+ digest = "invalid"
176
169
  lock_file = mock_registry_dir / digest / "hud.lock.yaml"
177
170
  lock_file.parent.mkdir(parents=True)
178
171
  lock_file.write_text("invalid: yaml: content:")
179
-
172
+
180
173
  result = check_local_cache("test/invalid:latest")
181
174
  assert result is None
182
175
 
@@ -189,60 +182,60 @@ class TestCheckLocalCache:
189
182
  class TestAnalyzeFromMetadata:
190
183
  """Test the main analyze_from_metadata function."""
191
184
 
192
- @mock.patch("hud.cli.analyze_metadata.check_local_cache")
193
- @mock.patch("hud.cli.analyze_metadata.console")
185
+ @mock.patch("hud.cli.utils.metadata.check_local_cache")
186
+ @mock.patch("hud.cli.utils.metadata.console")
194
187
  async def test_analyze_from_local_cache(self, mock_console, mock_check, sample_lock_data):
195
188
  """Test analyzing from local cache."""
196
189
  mock_check.return_value = sample_lock_data
197
-
190
+
198
191
  await analyze_from_metadata("test/env:latest", "json", verbose=False)
199
-
192
+
200
193
  mock_check.assert_called_once_with("test/env:latest")
201
194
  # Should output JSON
202
195
  mock_console.print_json.assert_called_once()
203
196
 
204
- @mock.patch("hud.cli.analyze_metadata.check_local_cache")
205
- @mock.patch("hud.cli.analyze_metadata.fetch_lock_from_registry")
206
- @mock.patch("hud.cli.analyze_metadata.save_to_registry")
207
- @mock.patch("hud.cli.analyze_metadata.console")
197
+ @mock.patch("hud.cli.utils.metadata.check_local_cache")
198
+ @mock.patch("hud.cli.utils.metadata.fetch_lock_from_registry")
199
+ @mock.patch("hud.cli.utils.registry.save_to_registry")
200
+ @mock.patch("hud.cli.utils.metadata.console")
208
201
  async def test_analyze_from_registry(
209
202
  self, mock_console, mock_save, mock_fetch, mock_check, sample_lock_data
210
203
  ):
211
204
  """Test analyzing from registry when not in cache."""
212
205
  mock_check.return_value = None
213
206
  mock_fetch.return_value = sample_lock_data
214
-
207
+
215
208
  await analyze_from_metadata("test/env:latest", "json", verbose=False)
216
-
209
+
217
210
  mock_check.assert_called_once()
218
211
  mock_fetch.assert_called_once()
219
212
  mock_save.assert_called_once() # Should save to cache
220
213
  mock_console.print_json.assert_called_once()
221
214
 
222
- @mock.patch("hud.cli.analyze_metadata.check_local_cache")
223
- @mock.patch("hud.cli.analyze_metadata.fetch_lock_from_registry")
224
- @mock.patch("hud.cli.analyze_metadata.design")
225
- @mock.patch("hud.cli.analyze_metadata.console")
215
+ @mock.patch("hud.cli.utils.metadata.check_local_cache")
216
+ @mock.patch("hud.cli.utils.metadata.fetch_lock_from_registry")
217
+ @mock.patch("hud.cli.utils.metadata.design")
218
+ @mock.patch("hud.cli.utils.metadata.console")
226
219
  async def test_analyze_not_found(self, mock_console, mock_design, mock_fetch, mock_check):
227
220
  """Test when environment not found anywhere."""
228
221
  mock_check.return_value = None
229
222
  mock_fetch.return_value = None
230
-
223
+
231
224
  await analyze_from_metadata("test/notfound:latest", "json", verbose=False)
232
-
225
+
233
226
  # Should show error
234
227
  mock_design.error.assert_called_with("Environment metadata not found")
235
228
  # Should print suggestions
236
229
  mock_console.print.assert_called()
237
230
 
238
- @mock.patch("hud.cli.analyze_metadata.check_local_cache")
239
- @mock.patch("hud.cli.analyze_metadata.console")
231
+ @mock.patch("hud.cli.utils.metadata.check_local_cache")
232
+ @mock.patch("hud.cli.utils.metadata.console")
240
233
  async def test_analyze_verbose_mode(self, mock_console, mock_check, sample_lock_data):
241
234
  """Test verbose mode includes input schemas."""
242
235
  mock_check.return_value = sample_lock_data
243
-
236
+
244
237
  await analyze_from_metadata("test/env:latest", "json", verbose=True)
245
-
238
+
246
239
  # In verbose mode, the JSON output should include input schemas
247
240
  mock_console.print_json.assert_called_once()
248
241
  # Get the JSON string that was printed
@@ -250,13 +243,13 @@ class TestAnalyzeFromMetadata:
250
243
  output_data = json.loads(call_args)
251
244
  assert "inputSchema" in output_data["tools"][0]
252
245
 
253
- @mock.patch("hud.cli.analyze_metadata.check_local_cache")
254
- @mock.patch("hud.cli.analyze_metadata.fetch_lock_from_registry")
246
+ @mock.patch("hud.cli.utils.metadata.check_local_cache")
247
+ @mock.patch("hud.cli.utils.metadata.fetch_lock_from_registry")
255
248
  async def test_analyze_registry_reference_parsing(self, mock_fetch, mock_check):
256
249
  """Test parsing of different registry reference formats."""
257
250
  mock_check.return_value = None
258
251
  mock_fetch.return_value = {"test": "data"}
259
-
252
+
260
253
  # Test different reference formats
261
254
  test_cases = [
262
255
  ("docker.io/org/name:tag", "org/name:tag"),
@@ -265,13 +258,13 @@ class TestAnalyzeFromMetadata:
265
258
  ("org/name", "org/name"),
266
259
  ("name:tag", "name:tag"),
267
260
  ]
268
-
261
+
269
262
  for input_ref, expected_call in test_cases:
270
263
  await analyze_from_metadata(input_ref, "json", verbose=False)
271
-
264
+
272
265
  # Check what was passed to fetch_lock_from_registry
273
266
  calls = mock_fetch.call_args_list
274
267
  last_call = calls[-1][0][0]
275
-
268
+
276
269
  # The function might add :latest, so check base name
277
270
  assert expected_call.split(":")[0] in last_call
@@ -3,9 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import subprocess
6
- import tempfile
7
- from datetime import datetime
8
- from pathlib import Path
9
6
  from unittest import mock
10
7
 
11
8
  import pytest
@@ -81,11 +78,7 @@ class TestGetExistingVersion:
81
78
 
82
79
  def test_get_version_from_lock(self, tmp_path):
83
80
  """Test extracting version from lock file."""
84
- lock_data = {
85
- "build": {
86
- "version": "1.2.3"
87
- }
88
- }
81
+ lock_data = {"build": {"version": "1.2.3"}}
89
82
  lock_path = tmp_path / "hud.lock.yaml"
90
83
  lock_path.write_text(yaml.dump(lock_data))
91
84
 
@@ -119,8 +112,7 @@ class TestGetDockerImageDigest:
119
112
  """Test successfully getting image digest."""
120
113
  # Note: The function expects to parse a list from the string representation
121
114
  mock_run.return_value = mock.Mock(
122
- stdout="['docker.io/library/test@sha256:abc123']",
123
- returncode=0
115
+ stdout="['docker.io/library/test@sha256:abc123']", returncode=0
124
116
  )
125
117
 
126
118
  result = get_docker_image_digest("test:latest")
@@ -149,10 +141,7 @@ class TestGetDockerImageId:
149
141
  @mock.patch("subprocess.run")
150
142
  def test_get_id_success(self, mock_run):
151
143
  """Test successfully getting image ID."""
152
- mock_run.return_value = mock.Mock(
153
- stdout="sha256:abc123def456",
154
- returncode=0
155
- )
144
+ mock_run.return_value = mock.Mock(stdout="sha256:abc123def456", returncode=0)
156
145
 
157
146
  result = get_docker_image_id("test:latest")
158
147
  assert result == "sha256:abc123def456"
@@ -223,13 +212,13 @@ class TestAnalyzeMcpEnvironment:
223
212
  # Setup mock client
224
213
  mock_client = mock.AsyncMock()
225
214
  mock_client_class.return_value = mock_client
226
-
215
+
227
216
  # Mock tool
228
217
  mock_tool = mock.Mock()
229
218
  mock_tool.name = "test_tool"
230
219
  mock_tool.description = "Test tool"
231
220
  mock_tool.inputSchema = {"type": "object"}
232
-
221
+
233
222
  mock_client.list_tools.return_value = [mock_tool]
234
223
 
235
224
  result = await analyze_mcp_environment("test:latest")
@@ -264,7 +253,7 @@ class TestAnalyzeMcpEnvironment:
264
253
 
265
254
  # Just test that it runs without error in verbose mode
266
255
  result = await analyze_mcp_environment("test:latest", verbose=True)
267
-
256
+
268
257
  assert result["success"] is True
269
258
  assert "initializeMs" in result
270
259
 
@@ -272,35 +261,31 @@ class TestAnalyzeMcpEnvironment:
272
261
  class TestBuildDockerImage:
273
262
  """Test building Docker images."""
274
263
 
275
- @mock.patch("subprocess.Popen")
276
- def test_build_success(self, mock_popen, tmp_path):
264
+ @mock.patch("subprocess.run")
265
+ def test_build_success(self, mock_run, tmp_path):
277
266
  """Test successful Docker build."""
278
267
  # Create Dockerfile
279
268
  dockerfile = tmp_path / "Dockerfile"
280
269
  dockerfile.write_text("FROM python:3.11")
281
270
 
282
271
  # Mock successful process
283
- mock_process = mock.Mock()
284
- mock_process.stdout = ["Step 1/1 : FROM python:3.11\n", "Successfully built abc123\n"]
285
- mock_process.wait.return_value = None
286
- mock_process.returncode = 0
287
- mock_popen.return_value = mock_process
272
+ mock_result = mock.Mock()
273
+ mock_result.returncode = 0
274
+ mock_run.return_value = mock_result
288
275
 
289
276
  result = build_docker_image(tmp_path, "test:latest")
290
277
  assert result is True
291
278
 
292
- @mock.patch("subprocess.Popen")
293
- def test_build_failure(self, mock_popen, tmp_path):
279
+ @mock.patch("subprocess.run")
280
+ def test_build_failure(self, mock_run, tmp_path):
294
281
  """Test failed Docker build."""
295
282
  dockerfile = tmp_path / "Dockerfile"
296
283
  dockerfile.write_text("FROM python:3.11")
297
284
 
298
285
  # Mock failed process
299
- mock_process = mock.Mock()
300
- mock_process.stdout = ["Error: failed to build\n"]
301
- mock_process.wait.return_value = None
302
- mock_process.returncode = 1
303
- mock_popen.return_value = mock_process
286
+ mock_result = mock.Mock()
287
+ mock_result.returncode = 1
288
+ mock_run.return_value = mock_result
304
289
 
305
290
  result = build_docker_image(tmp_path, "test:latest")
306
291
  assert result is False
@@ -310,22 +295,20 @@ class TestBuildDockerImage:
310
295
  result = build_docker_image(tmp_path, "test:latest")
311
296
  assert result is False
312
297
 
313
- @mock.patch("subprocess.Popen")
314
- def test_build_with_no_cache(self, mock_popen, tmp_path):
298
+ @mock.patch("subprocess.run")
299
+ def test_build_with_no_cache(self, mock_run, tmp_path):
315
300
  """Test build with --no-cache flag."""
316
301
  dockerfile = tmp_path / "Dockerfile"
317
302
  dockerfile.write_text("FROM python:3.11")
318
303
 
319
- mock_process = mock.Mock()
320
- mock_process.stdout = []
321
- mock_process.wait.return_value = None
322
- mock_process.returncode = 0
323
- mock_popen.return_value = mock_process
304
+ mock_result = mock.Mock()
305
+ mock_result.returncode = 0
306
+ mock_run.return_value = mock_result
324
307
 
325
308
  build_docker_image(tmp_path, "test:latest", no_cache=True)
326
309
 
327
310
  # Check that --no-cache was included
328
- call_args = mock_popen.call_args[0][0]
311
+ call_args = mock_run.call_args[0][0]
329
312
  assert "--no-cache" in call_args
330
313
 
331
314
 
@@ -336,12 +319,10 @@ class TestBuildEnvironment:
336
319
  @mock.patch("hud.cli.build.analyze_mcp_environment")
337
320
  @mock.patch("hud.cli.build.save_to_registry")
338
321
  @mock.patch("hud.cli.build.get_docker_image_id")
339
- @mock.patch("subprocess.Popen")
340
322
  @mock.patch("subprocess.run")
341
323
  def test_build_environment_success(
342
324
  self,
343
325
  mock_run,
344
- mock_popen,
345
326
  mock_get_id,
346
327
  mock_save_registry,
347
328
  mock_analyze,
@@ -352,14 +333,14 @@ class TestBuildEnvironment:
352
333
  # Setup directory structure
353
334
  env_dir = tmp_path / "test-env"
354
335
  env_dir.mkdir()
355
-
336
+
356
337
  # Create pyproject.toml
357
338
  pyproject = env_dir / "pyproject.toml"
358
339
  pyproject.write_text("""
359
340
  [tool.hud]
360
341
  image = "test/env:dev"
361
342
  """)
362
-
343
+
363
344
  # Create Dockerfile
364
345
  dockerfile = env_dir / "Dockerfile"
365
346
  dockerfile.write_text("""
@@ -379,16 +360,11 @@ ENV API_KEY
379
360
  ],
380
361
  }
381
362
  mock_get_id.return_value = "sha256:abc123"
382
-
363
+
383
364
  # Mock final rebuild
384
- mock_process = mock.Mock()
385
- # Create a mock file-like object with read method
386
- mock_stdout = mock.Mock()
387
- mock_stdout.read.return_value = ""
388
- mock_process.stdout = mock_stdout
389
- mock_process.wait.return_value = None
390
- mock_process.returncode = 0
391
- mock_popen.return_value = mock_process
365
+ mock_result = mock.Mock()
366
+ mock_result.returncode = 0
367
+ mock_run.return_value = mock_result
392
368
 
393
369
  # Run build
394
370
  build_environment(str(env_dir), "test/env:latest")
@@ -9,7 +9,7 @@ from unittest.mock import mock_open, patch
9
9
 
10
10
  import pytest
11
11
 
12
- from hud.cli.cursor import get_cursor_config_path, list_cursor_servers, parse_cursor_config
12
+ from hud.cli.utils.cursor import get_cursor_config_path, list_cursor_servers, parse_cursor_config
13
13
 
14
14
 
15
15
  class TestParseCursorConfig:
@@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch
8
8
  import pytest
9
9
 
10
10
  from hud.cli.debug import debug_mcp_stdio
11
- from hud.cli.utils import CaptureLogger
11
+ from hud.cli.utils.logging import CaptureLogger
12
12
 
13
13
 
14
14
  class TestDebugMCPStdio: