hud-python 0.4.8__py3-none-any.whl → 0.4.10__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/agents/base.py +50 -1
- hud/cli/__init__.py +187 -11
- hud/cli/analyze_metadata.py +33 -42
- hud/cli/build.py +7 -0
- hud/cli/debug.py +8 -1
- hud/cli/env_utils.py +133 -0
- hud/cli/eval.py +302 -0
- hud/cli/list_func.py +213 -0
- hud/cli/mcp_server.py +3 -79
- hud/cli/pull.py +20 -15
- hud/cli/push.py +84 -41
- hud/cli/registry.py +155 -0
- hud/cli/remove.py +200 -0
- hud/cli/runner.py +1 -1
- hud/cli/tests/test_analyze_metadata.py +277 -0
- hud/cli/tests/test_build.py +450 -0
- hud/cli/tests/test_list_func.py +288 -0
- hud/cli/tests/test_pull.py +400 -0
- hud/cli/tests/test_push.py +379 -0
- hud/cli/tests/test_registry.py +264 -0
- hud/clients/base.py +13 -1
- hud/tools/__init__.py +2 -0
- hud/tools/response.py +54 -0
- hud/utils/design.py +10 -0
- hud/utils/mcp.py +14 -2
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.4.8.dist-info → hud_python-0.4.10.dist-info}/METADATA +12 -1
- {hud_python-0.4.8.dist-info → hud_python-0.4.10.dist-info}/RECORD +32 -20
- {hud_python-0.4.8.dist-info → hud_python-0.4.10.dist-info}/WHEEL +0 -0
- {hud_python-0.4.8.dist-info → hud_python-0.4.10.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.8.dist-info → hud_python-0.4.10.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Tests for analyze_metadata.py - Fast metadata analysis functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from unittest import mock
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
from hud.cli.analyze_metadata import (
|
|
15
|
+
analyze_from_metadata,
|
|
16
|
+
check_local_cache,
|
|
17
|
+
fetch_lock_from_registry,
|
|
18
|
+
)
|
|
19
|
+
from hud.cli.registry import save_to_registry
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def mock_registry_dir(tmp_path):
|
|
24
|
+
"""Create a mock registry directory."""
|
|
25
|
+
registry_dir = tmp_path / ".hud" / "envs"
|
|
26
|
+
registry_dir.mkdir(parents=True)
|
|
27
|
+
return registry_dir
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.fixture
|
|
31
|
+
def sample_lock_data():
|
|
32
|
+
"""Sample lock data for testing."""
|
|
33
|
+
return {
|
|
34
|
+
"image": "test/environment:latest",
|
|
35
|
+
"digest": "sha256:abc123",
|
|
36
|
+
"build": {
|
|
37
|
+
"timestamp": 1234567890,
|
|
38
|
+
"version": "1.0.0",
|
|
39
|
+
"hud_version": "0.1.0",
|
|
40
|
+
},
|
|
41
|
+
"environment": {
|
|
42
|
+
"initializeMs": 1500,
|
|
43
|
+
"toolCount": 5,
|
|
44
|
+
"variables": {"API_KEY": "required"},
|
|
45
|
+
},
|
|
46
|
+
"tools": [
|
|
47
|
+
{
|
|
48
|
+
"name": "test_tool",
|
|
49
|
+
"description": "A test tool",
|
|
50
|
+
"inputSchema": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"properties": {"message": {"type": "string"}},
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"resources": [
|
|
57
|
+
{
|
|
58
|
+
"uri": "test://resource",
|
|
59
|
+
"name": "Test Resource",
|
|
60
|
+
"description": "A test resource",
|
|
61
|
+
"mimeType": "text/plain",
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"prompts": [
|
|
65
|
+
{
|
|
66
|
+
"name": "test_prompt",
|
|
67
|
+
"description": "A test prompt",
|
|
68
|
+
"arguments": [{"name": "arg1", "description": "First argument"}],
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TestFetchLockFromRegistry:
|
|
75
|
+
"""Test fetching lock data from registry."""
|
|
76
|
+
|
|
77
|
+
@mock.patch("requests.get")
|
|
78
|
+
def test_fetch_lock_success(self, mock_get):
|
|
79
|
+
"""Test successful fetch from registry."""
|
|
80
|
+
mock_response = mock.Mock()
|
|
81
|
+
mock_response.status_code = 200
|
|
82
|
+
mock_response.json.return_value = {
|
|
83
|
+
"lock": yaml.dump({"test": "data"})
|
|
84
|
+
}
|
|
85
|
+
mock_get.return_value = mock_response
|
|
86
|
+
|
|
87
|
+
result = fetch_lock_from_registry("test/env:latest")
|
|
88
|
+
assert result == {"test": "data"}
|
|
89
|
+
mock_get.assert_called_once()
|
|
90
|
+
|
|
91
|
+
@mock.patch("requests.get")
|
|
92
|
+
def test_fetch_lock_with_lock_data(self, mock_get):
|
|
93
|
+
"""Test fetch when response has lock_data key."""
|
|
94
|
+
mock_response = mock.Mock()
|
|
95
|
+
mock_response.status_code = 200
|
|
96
|
+
mock_response.json.return_value = {
|
|
97
|
+
"lock_data": {"test": "data"}
|
|
98
|
+
}
|
|
99
|
+
mock_get.return_value = mock_response
|
|
100
|
+
|
|
101
|
+
result = fetch_lock_from_registry("test/env:latest")
|
|
102
|
+
assert result == {"test": "data"}
|
|
103
|
+
|
|
104
|
+
@mock.patch("requests.get")
|
|
105
|
+
def test_fetch_lock_direct_data(self, mock_get):
|
|
106
|
+
"""Test fetch when response is direct lock data."""
|
|
107
|
+
mock_response = mock.Mock()
|
|
108
|
+
mock_response.status_code = 200
|
|
109
|
+
mock_response.json.return_value = {"test": "data"}
|
|
110
|
+
mock_get.return_value = mock_response
|
|
111
|
+
|
|
112
|
+
result = fetch_lock_from_registry("test/env:latest")
|
|
113
|
+
assert result == {"test": "data"}
|
|
114
|
+
|
|
115
|
+
@mock.patch("requests.get")
|
|
116
|
+
def test_fetch_lock_adds_latest_tag(self, mock_get):
|
|
117
|
+
"""Test that :latest tag is added if missing."""
|
|
118
|
+
mock_response = mock.Mock()
|
|
119
|
+
mock_response.status_code = 404
|
|
120
|
+
mock_get.return_value = mock_response
|
|
121
|
+
|
|
122
|
+
fetch_lock_from_registry("test/env")
|
|
123
|
+
|
|
124
|
+
# Check that the URL includes :latest
|
|
125
|
+
call_args = mock_get.call_args
|
|
126
|
+
assert "test/env:latest" in call_args[0][0]
|
|
127
|
+
|
|
128
|
+
@mock.patch("requests.get")
|
|
129
|
+
def test_fetch_lock_failure(self, mock_get):
|
|
130
|
+
"""Test fetch failure returns None."""
|
|
131
|
+
mock_response = mock.Mock()
|
|
132
|
+
mock_response.status_code = 404
|
|
133
|
+
mock_get.return_value = mock_response
|
|
134
|
+
|
|
135
|
+
result = fetch_lock_from_registry("test/env:latest")
|
|
136
|
+
assert result is None
|
|
137
|
+
|
|
138
|
+
@mock.patch("requests.get")
|
|
139
|
+
def test_fetch_lock_exception(self, mock_get):
|
|
140
|
+
"""Test fetch exception returns None."""
|
|
141
|
+
mock_get.side_effect = Exception("Network error")
|
|
142
|
+
|
|
143
|
+
result = fetch_lock_from_registry("test/env:latest")
|
|
144
|
+
assert result is None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TestCheckLocalCache:
|
|
148
|
+
"""Test checking local cache for lock data."""
|
|
149
|
+
|
|
150
|
+
def test_check_local_cache_found(self, mock_registry_dir, sample_lock_data, monkeypatch):
|
|
151
|
+
"""Test finding lock data in local cache."""
|
|
152
|
+
# Mock registry directory
|
|
153
|
+
monkeypatch.setattr("hud.cli.registry.get_registry_dir", lambda: mock_registry_dir)
|
|
154
|
+
|
|
155
|
+
# Save sample data to registry
|
|
156
|
+
save_to_registry(sample_lock_data, "test/environment:latest", verbose=False)
|
|
157
|
+
|
|
158
|
+
# Check cache
|
|
159
|
+
result = check_local_cache("test/environment:latest")
|
|
160
|
+
assert result is not None
|
|
161
|
+
assert result["image"] == "test/environment:latest"
|
|
162
|
+
|
|
163
|
+
def test_check_local_cache_not_found(self, mock_registry_dir, monkeypatch):
|
|
164
|
+
"""Test when lock data not in local cache."""
|
|
165
|
+
monkeypatch.setattr("hud.cli.registry.get_registry_dir", lambda: mock_registry_dir)
|
|
166
|
+
|
|
167
|
+
result = check_local_cache("nonexistent/env:latest")
|
|
168
|
+
assert result is None
|
|
169
|
+
|
|
170
|
+
def test_check_local_cache_invalid_yaml(self, mock_registry_dir, monkeypatch):
|
|
171
|
+
"""Test when lock file has invalid YAML."""
|
|
172
|
+
monkeypatch.setattr("hud.cli.registry.get_registry_dir", lambda: mock_registry_dir)
|
|
173
|
+
|
|
174
|
+
# Create invalid lock file
|
|
175
|
+
digest = "sha256:invalid"
|
|
176
|
+
lock_file = mock_registry_dir / digest / "hud.lock.yaml"
|
|
177
|
+
lock_file.parent.mkdir(parents=True)
|
|
178
|
+
lock_file.write_text("invalid: yaml: content:")
|
|
179
|
+
|
|
180
|
+
result = check_local_cache("test/invalid:latest")
|
|
181
|
+
assert result is None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# Note: TestFormatAnalysisOutput class removed since format_analysis_output function doesn't exist
|
|
185
|
+
# The formatting is done inline within analyze_from_metadata
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@pytest.mark.asyncio
|
|
189
|
+
class TestAnalyzeFromMetadata:
|
|
190
|
+
"""Test the main analyze_from_metadata function."""
|
|
191
|
+
|
|
192
|
+
@mock.patch("hud.cli.analyze_metadata.check_local_cache")
|
|
193
|
+
@mock.patch("hud.cli.analyze_metadata.console")
|
|
194
|
+
async def test_analyze_from_local_cache(self, mock_console, mock_check, sample_lock_data):
|
|
195
|
+
"""Test analyzing from local cache."""
|
|
196
|
+
mock_check.return_value = sample_lock_data
|
|
197
|
+
|
|
198
|
+
await analyze_from_metadata("test/env:latest", "json", verbose=False)
|
|
199
|
+
|
|
200
|
+
mock_check.assert_called_once_with("test/env:latest")
|
|
201
|
+
# Should output JSON
|
|
202
|
+
mock_console.print_json.assert_called_once()
|
|
203
|
+
|
|
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")
|
|
208
|
+
async def test_analyze_from_registry(
|
|
209
|
+
self, mock_console, mock_save, mock_fetch, mock_check, sample_lock_data
|
|
210
|
+
):
|
|
211
|
+
"""Test analyzing from registry when not in cache."""
|
|
212
|
+
mock_check.return_value = None
|
|
213
|
+
mock_fetch.return_value = sample_lock_data
|
|
214
|
+
|
|
215
|
+
await analyze_from_metadata("test/env:latest", "json", verbose=False)
|
|
216
|
+
|
|
217
|
+
mock_check.assert_called_once()
|
|
218
|
+
mock_fetch.assert_called_once()
|
|
219
|
+
mock_save.assert_called_once() # Should save to cache
|
|
220
|
+
mock_console.print_json.assert_called_once()
|
|
221
|
+
|
|
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")
|
|
226
|
+
async def test_analyze_not_found(self, mock_console, mock_design, mock_fetch, mock_check):
|
|
227
|
+
"""Test when environment not found anywhere."""
|
|
228
|
+
mock_check.return_value = None
|
|
229
|
+
mock_fetch.return_value = None
|
|
230
|
+
|
|
231
|
+
await analyze_from_metadata("test/notfound:latest", "json", verbose=False)
|
|
232
|
+
|
|
233
|
+
# Should show error
|
|
234
|
+
mock_design.error.assert_called_with("Environment metadata not found")
|
|
235
|
+
# Should print suggestions
|
|
236
|
+
mock_console.print.assert_called()
|
|
237
|
+
|
|
238
|
+
@mock.patch("hud.cli.analyze_metadata.check_local_cache")
|
|
239
|
+
@mock.patch("hud.cli.analyze_metadata.console")
|
|
240
|
+
async def test_analyze_verbose_mode(self, mock_console, mock_check, sample_lock_data):
|
|
241
|
+
"""Test verbose mode includes input schemas."""
|
|
242
|
+
mock_check.return_value = sample_lock_data
|
|
243
|
+
|
|
244
|
+
await analyze_from_metadata("test/env:latest", "json", verbose=True)
|
|
245
|
+
|
|
246
|
+
# In verbose mode, the JSON output should include input schemas
|
|
247
|
+
mock_console.print_json.assert_called_once()
|
|
248
|
+
# Get the JSON string that was printed
|
|
249
|
+
call_args = mock_console.print_json.call_args[0][0]
|
|
250
|
+
output_data = json.loads(call_args)
|
|
251
|
+
assert "inputSchema" in output_data["tools"][0]
|
|
252
|
+
|
|
253
|
+
@mock.patch("hud.cli.analyze_metadata.check_local_cache")
|
|
254
|
+
@mock.patch("hud.cli.analyze_metadata.fetch_lock_from_registry")
|
|
255
|
+
async def test_analyze_registry_reference_parsing(self, mock_fetch, mock_check):
|
|
256
|
+
"""Test parsing of different registry reference formats."""
|
|
257
|
+
mock_check.return_value = None
|
|
258
|
+
mock_fetch.return_value = {"test": "data"}
|
|
259
|
+
|
|
260
|
+
# Test different reference formats
|
|
261
|
+
test_cases = [
|
|
262
|
+
("docker.io/org/name:tag", "org/name:tag"),
|
|
263
|
+
("registry-1.docker.io/org/name", "org/name"),
|
|
264
|
+
("org/name@sha256:abc", "org/name"),
|
|
265
|
+
("org/name", "org/name"),
|
|
266
|
+
("name:tag", "name:tag"),
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
for input_ref, expected_call in test_cases:
|
|
270
|
+
await analyze_from_metadata(input_ref, "json", verbose=False)
|
|
271
|
+
|
|
272
|
+
# Check what was passed to fetch_lock_from_registry
|
|
273
|
+
calls = mock_fetch.call_args_list
|
|
274
|
+
last_call = calls[-1][0][0]
|
|
275
|
+
|
|
276
|
+
# The function might add :latest, so check base name
|
|
277
|
+
assert expected_call.split(":")[0] in last_call
|