hud-python 0.4.45__py3-none-any.whl → 0.5.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.
- hud/__init__.py +27 -7
- hud/agents/__init__.py +70 -5
- hud/agents/base.py +238 -500
- hud/agents/claude.py +236 -247
- hud/agents/gateway.py +42 -0
- hud/agents/gemini.py +264 -0
- hud/agents/gemini_cua.py +324 -0
- hud/agents/grounded_openai.py +98 -100
- hud/agents/misc/integration_test_agent.py +51 -20
- hud/agents/misc/response_agent.py +48 -36
- hud/agents/openai.py +282 -296
- hud/agents/{openai_chat_generic.py → openai_chat.py} +63 -33
- hud/agents/operator.py +199 -0
- hud/agents/resolver.py +70 -0
- hud/agents/tests/conftest.py +133 -0
- hud/agents/tests/test_base.py +300 -622
- hud/agents/tests/test_base_runtime.py +233 -0
- hud/agents/tests/test_claude.py +381 -214
- hud/agents/tests/test_client.py +9 -10
- hud/agents/tests/test_gemini.py +369 -0
- hud/agents/tests/test_grounded_openai_agent.py +65 -50
- hud/agents/tests/test_openai.py +377 -140
- hud/agents/tests/test_operator.py +362 -0
- hud/agents/tests/test_resolver.py +192 -0
- hud/agents/tests/test_run_eval.py +179 -0
- hud/agents/types.py +148 -0
- hud/cli/__init__.py +493 -546
- hud/cli/analyze.py +43 -5
- hud/cli/build.py +699 -113
- hud/cli/debug.py +8 -5
- hud/cli/dev.py +889 -732
- hud/cli/eval.py +793 -667
- hud/cli/flows/dev.py +167 -0
- hud/cli/flows/init.py +191 -0
- hud/cli/flows/tasks.py +153 -56
- hud/cli/flows/templates.py +151 -0
- hud/cli/flows/tests/__init__.py +1 -0
- hud/cli/flows/tests/test_dev.py +126 -0
- hud/cli/init.py +60 -58
- hud/cli/pull.py +1 -1
- hud/cli/push.py +38 -13
- hud/cli/rft.py +311 -0
- hud/cli/rft_status.py +145 -0
- hud/cli/tests/test_analyze.py +5 -5
- hud/cli/tests/test_analyze_metadata.py +3 -2
- hud/cli/tests/test_analyze_module.py +120 -0
- hud/cli/tests/test_build.py +110 -8
- hud/cli/tests/test_build_failure.py +41 -0
- hud/cli/tests/test_build_module.py +50 -0
- hud/cli/tests/test_cli_init.py +6 -1
- hud/cli/tests/test_cli_more_wrappers.py +30 -0
- hud/cli/tests/test_cli_root.py +140 -0
- hud/cli/tests/test_convert.py +361 -0
- hud/cli/tests/test_debug.py +12 -10
- hud/cli/tests/test_dev.py +197 -0
- hud/cli/tests/test_eval.py +251 -0
- hud/cli/tests/test_eval_bedrock.py +51 -0
- hud/cli/tests/test_init.py +124 -0
- hud/cli/tests/test_main_module.py +11 -5
- hud/cli/tests/test_mcp_server.py +12 -100
- hud/cli/tests/test_push.py +1 -1
- hud/cli/tests/test_push_happy.py +74 -0
- hud/cli/tests/test_push_wrapper.py +23 -0
- hud/cli/tests/test_registry.py +1 -1
- hud/cli/tests/test_utils.py +1 -1
- hud/cli/{rl → utils}/celebrate.py +14 -12
- hud/cli/utils/config.py +18 -1
- hud/cli/utils/docker.py +130 -4
- hud/cli/utils/env_check.py +9 -9
- hud/cli/utils/git.py +136 -0
- hud/cli/utils/interactive.py +39 -5
- hud/cli/utils/metadata.py +70 -1
- hud/cli/utils/runner.py +1 -1
- hud/cli/utils/server.py +2 -2
- hud/cli/utils/source_hash.py +3 -3
- hud/cli/utils/tasks.py +4 -1
- hud/cli/utils/tests/__init__.py +0 -0
- hud/cli/utils/tests/test_config.py +58 -0
- hud/cli/utils/tests/test_docker.py +93 -0
- hud/cli/utils/tests/test_docker_hints.py +71 -0
- hud/cli/utils/tests/test_env_check.py +74 -0
- hud/cli/utils/tests/test_environment.py +42 -0
- hud/cli/utils/tests/test_git.py +142 -0
- hud/cli/utils/tests/test_interactive_module.py +60 -0
- hud/cli/utils/tests/test_local_runner.py +50 -0
- hud/cli/utils/tests/test_logging_utils.py +23 -0
- hud/cli/utils/tests/test_metadata.py +49 -0
- hud/cli/utils/tests/test_package_runner.py +35 -0
- hud/cli/utils/tests/test_registry_utils.py +49 -0
- hud/cli/utils/tests/test_remote_runner.py +25 -0
- hud/cli/utils/tests/test_runner_modules.py +52 -0
- hud/cli/utils/tests/test_source_hash.py +36 -0
- hud/cli/utils/tests/test_tasks.py +80 -0
- hud/cli/utils/version_check.py +258 -0
- hud/cli/{rl → utils}/viewer.py +2 -2
- hud/clients/README.md +12 -11
- hud/clients/__init__.py +4 -3
- hud/clients/base.py +166 -26
- hud/clients/environment.py +51 -0
- hud/clients/fastmcp.py +13 -6
- hud/clients/mcp_use.py +45 -15
- hud/clients/tests/test_analyze_scenarios.py +206 -0
- hud/clients/tests/test_protocol.py +9 -3
- hud/datasets/__init__.py +23 -20
- hud/datasets/loader.py +326 -0
- hud/datasets/runner.py +198 -105
- hud/datasets/tests/__init__.py +0 -0
- hud/datasets/tests/test_loader.py +221 -0
- hud/datasets/tests/test_utils.py +315 -0
- hud/datasets/utils.py +270 -90
- hud/environment/__init__.py +52 -0
- hud/environment/connection.py +258 -0
- hud/environment/connectors/__init__.py +33 -0
- hud/environment/connectors/base.py +68 -0
- hud/environment/connectors/local.py +177 -0
- hud/environment/connectors/mcp_config.py +137 -0
- hud/environment/connectors/openai.py +101 -0
- hud/environment/connectors/remote.py +172 -0
- hud/environment/environment.py +835 -0
- hud/environment/integrations/__init__.py +45 -0
- hud/environment/integrations/adk.py +67 -0
- hud/environment/integrations/anthropic.py +196 -0
- hud/environment/integrations/gemini.py +92 -0
- hud/environment/integrations/langchain.py +82 -0
- hud/environment/integrations/llamaindex.py +68 -0
- hud/environment/integrations/openai.py +238 -0
- hud/environment/mock.py +306 -0
- hud/environment/router.py +263 -0
- hud/environment/scenarios.py +620 -0
- hud/environment/tests/__init__.py +1 -0
- hud/environment/tests/test_connection.py +317 -0
- hud/environment/tests/test_connectors.py +205 -0
- hud/environment/tests/test_environment.py +593 -0
- hud/environment/tests/test_integrations.py +257 -0
- hud/environment/tests/test_local_connectors.py +242 -0
- hud/environment/tests/test_scenarios.py +1086 -0
- hud/environment/tests/test_tools.py +208 -0
- hud/environment/types.py +23 -0
- hud/environment/utils/__init__.py +35 -0
- hud/environment/utils/formats.py +215 -0
- hud/environment/utils/schema.py +171 -0
- hud/environment/utils/tool_wrappers.py +113 -0
- hud/eval/__init__.py +67 -0
- hud/eval/context.py +727 -0
- hud/eval/display.py +299 -0
- hud/eval/instrument.py +187 -0
- hud/eval/manager.py +533 -0
- hud/eval/parallel.py +268 -0
- hud/eval/task.py +372 -0
- hud/eval/tests/__init__.py +1 -0
- hud/eval/tests/test_context.py +178 -0
- hud/eval/tests/test_eval.py +210 -0
- hud/eval/tests/test_manager.py +152 -0
- hud/eval/tests/test_parallel.py +168 -0
- hud/eval/tests/test_task.py +291 -0
- hud/eval/types.py +65 -0
- hud/eval/utils.py +194 -0
- hud/patches/__init__.py +19 -0
- hud/patches/mcp_patches.py +308 -0
- hud/patches/warnings.py +54 -0
- hud/samples/browser.py +4 -4
- hud/server/__init__.py +2 -1
- hud/server/low_level.py +2 -1
- hud/server/router.py +164 -0
- hud/server/server.py +567 -80
- hud/server/tests/test_mcp_server_integration.py +11 -11
- hud/server/tests/test_mcp_server_more.py +1 -1
- hud/server/tests/test_server_extra.py +2 -0
- hud/settings.py +45 -3
- hud/shared/exceptions.py +36 -10
- hud/shared/hints.py +26 -1
- hud/shared/requests.py +15 -3
- hud/shared/tests/test_exceptions.py +40 -31
- hud/shared/tests/test_hints.py +167 -0
- hud/telemetry/__init__.py +20 -19
- hud/telemetry/exporter.py +201 -0
- hud/telemetry/instrument.py +165 -253
- hud/telemetry/tests/test_eval_telemetry.py +356 -0
- hud/telemetry/tests/test_exporter.py +258 -0
- hud/telemetry/tests/test_instrument.py +401 -0
- hud/tools/__init__.py +18 -2
- hud/tools/agent.py +223 -0
- hud/tools/apply_patch.py +639 -0
- hud/tools/base.py +54 -4
- hud/tools/bash.py +2 -2
- hud/tools/computer/__init__.py +36 -3
- hud/tools/computer/anthropic.py +2 -2
- hud/tools/computer/gemini.py +385 -0
- hud/tools/computer/hud.py +23 -6
- hud/tools/computer/openai.py +20 -21
- hud/tools/computer/qwen.py +434 -0
- hud/tools/computer/settings.py +37 -0
- hud/tools/edit.py +3 -7
- hud/tools/executors/base.py +4 -2
- hud/tools/executors/pyautogui.py +1 -1
- hud/tools/grounding/grounded_tool.py +13 -18
- hud/tools/grounding/grounder.py +10 -31
- hud/tools/grounding/tests/test_grounded_tool.py +26 -44
- hud/tools/jupyter.py +330 -0
- hud/tools/playwright.py +18 -3
- hud/tools/shell.py +308 -0
- hud/tools/tests/test_agent_tool.py +355 -0
- hud/tools/tests/test_apply_patch.py +718 -0
- hud/tools/tests/test_computer.py +4 -9
- hud/tools/tests/test_computer_actions.py +24 -2
- hud/tools/tests/test_jupyter_tool.py +181 -0
- hud/tools/tests/test_shell.py +596 -0
- hud/tools/tests/test_submit.py +85 -0
- hud/tools/tests/test_types.py +193 -0
- hud/tools/types.py +21 -1
- hud/types.py +194 -56
- hud/utils/__init__.py +2 -0
- hud/utils/env.py +67 -0
- hud/utils/hud_console.py +89 -18
- hud/utils/mcp.py +15 -58
- hud/utils/strict_schema.py +162 -0
- hud/utils/tests/test_init.py +1 -2
- hud/utils/tests/test_mcp.py +1 -28
- hud/utils/tests/test_pretty_errors.py +186 -0
- hud/utils/tests/test_tool_shorthand.py +154 -0
- hud/utils/tests/test_version.py +1 -1
- hud/utils/types.py +20 -0
- hud/version.py +1 -1
- hud_python-0.5.13.dist-info/METADATA +264 -0
- hud_python-0.5.13.dist-info/RECORD +305 -0
- {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/WHEEL +1 -1
- hud/agents/langchain.py +0 -261
- hud/agents/lite_llm.py +0 -72
- hud/cli/rl/__init__.py +0 -180
- hud/cli/rl/config.py +0 -101
- hud/cli/rl/display.py +0 -133
- hud/cli/rl/gpu.py +0 -63
- hud/cli/rl/gpu_utils.py +0 -321
- hud/cli/rl/local_runner.py +0 -595
- hud/cli/rl/presets.py +0 -96
- hud/cli/rl/remote_runner.py +0 -463
- hud/cli/rl/rl_api.py +0 -150
- hud/cli/rl/vllm.py +0 -177
- hud/cli/rl/wait_utils.py +0 -89
- hud/datasets/parallel.py +0 -687
- hud/misc/__init__.py +0 -1
- hud/misc/claude_plays_pokemon.py +0 -292
- hud/otel/__init__.py +0 -35
- hud/otel/collector.py +0 -142
- hud/otel/config.py +0 -181
- hud/otel/context.py +0 -570
- hud/otel/exporters.py +0 -369
- hud/otel/instrumentation.py +0 -135
- hud/otel/processors.py +0 -121
- hud/otel/tests/__init__.py +0 -1
- hud/otel/tests/test_processors.py +0 -197
- hud/rl/README.md +0 -30
- hud/rl/__init__.py +0 -1
- hud/rl/actor.py +0 -176
- hud/rl/buffer.py +0 -405
- hud/rl/chat_template.jinja +0 -101
- hud/rl/config.py +0 -192
- hud/rl/distributed.py +0 -132
- hud/rl/learner.py +0 -637
- hud/rl/tests/__init__.py +0 -1
- hud/rl/tests/test_learner.py +0 -186
- hud/rl/train.py +0 -382
- hud/rl/types.py +0 -101
- hud/rl/utils/start_vllm_server.sh +0 -30
- hud/rl/utils.py +0 -524
- hud/rl/vllm_adapter.py +0 -143
- hud/telemetry/job.py +0 -352
- hud/telemetry/replay.py +0 -74
- hud/telemetry/tests/test_replay.py +0 -40
- hud/telemetry/tests/test_trace.py +0 -63
- hud/telemetry/trace.py +0 -158
- hud/utils/agent_factories.py +0 -86
- hud/utils/async_utils.py +0 -65
- hud/utils/group_eval.py +0 -223
- hud/utils/progress.py +0 -149
- hud/utils/tasks.py +0 -127
- hud/utils/tests/test_async_utils.py +0 -173
- hud/utils/tests/test_progress.py +0 -261
- hud_python-0.4.45.dist-info/METADATA +0 -552
- hud_python-0.4.45.dist-info/RECORD +0 -228
- {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/licenses/LICENSE +0 -0
hud/cli/utils/interactive.py
CHANGED
|
@@ -3,19 +3,21 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
7
|
|
|
8
8
|
import questionary
|
|
9
|
-
from mcp.types import TextContent
|
|
9
|
+
from mcp.types import ImageContent, TextContent
|
|
10
10
|
from rich.console import Console
|
|
11
11
|
from rich.panel import Panel
|
|
12
12
|
from rich.prompt import Prompt
|
|
13
13
|
from rich.syntax import Syntax
|
|
14
14
|
from rich.tree import Tree
|
|
15
15
|
|
|
16
|
-
from hud.clients import MCPClient
|
|
17
16
|
from hud.utils.hud_console import HUDConsole
|
|
18
17
|
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from hud.clients import MCPClient
|
|
20
|
+
|
|
19
21
|
console = Console()
|
|
20
22
|
|
|
21
23
|
|
|
@@ -38,6 +40,9 @@ class InteractiveMCPTester:
|
|
|
38
40
|
async def connect(self) -> bool:
|
|
39
41
|
"""Connect to the MCP server."""
|
|
40
42
|
try:
|
|
43
|
+
# Lazy import to avoid loading mcp_use on simple CLI commands
|
|
44
|
+
from hud.clients import MCPClient
|
|
45
|
+
|
|
41
46
|
# Create MCP config for HTTP transport
|
|
42
47
|
# Note: We explicitly set auth to None to prevent OAuth discovery attempts
|
|
43
48
|
config = {"server": {"url": self.server_url, "auth": None}}
|
|
@@ -45,7 +50,6 @@ class InteractiveMCPTester:
|
|
|
45
50
|
self.client = MCPClient(
|
|
46
51
|
mcp_config=config,
|
|
47
52
|
verbose=self.verbose,
|
|
48
|
-
auto_trace=False, # Disable telemetry for interactive testing
|
|
49
53
|
)
|
|
50
54
|
await self.client.initialize()
|
|
51
55
|
|
|
@@ -242,7 +246,27 @@ class InteractiveMCPTester:
|
|
|
242
246
|
# Prompt for each property
|
|
243
247
|
args = {}
|
|
244
248
|
for prop_name, prop_schema in properties.items():
|
|
245
|
-
prop_type = prop_schema.get("type"
|
|
249
|
+
prop_type = prop_schema.get("type")
|
|
250
|
+
if not prop_type and "anyOf" in prop_schema:
|
|
251
|
+
prop_type = next(
|
|
252
|
+
(
|
|
253
|
+
s.get("type")
|
|
254
|
+
for s in prop_schema.get("anyOf", [])
|
|
255
|
+
if s.get("type") != "null"
|
|
256
|
+
),
|
|
257
|
+
None,
|
|
258
|
+
)
|
|
259
|
+
if not prop_type and "oneOf" in prop_schema:
|
|
260
|
+
prop_type = next(
|
|
261
|
+
(
|
|
262
|
+
s.get("type")
|
|
263
|
+
for s in prop_schema.get("oneOf", [])
|
|
264
|
+
if s.get("type") != "null"
|
|
265
|
+
),
|
|
266
|
+
None,
|
|
267
|
+
)
|
|
268
|
+
prop_type = prop_type or "string"
|
|
269
|
+
|
|
246
270
|
description = prop_schema.get("description", "")
|
|
247
271
|
is_required = prop_name in required
|
|
248
272
|
|
|
@@ -353,6 +377,16 @@ class InteractiveMCPTester:
|
|
|
353
377
|
border_style="green" if not result.isError else "red",
|
|
354
378
|
)
|
|
355
379
|
)
|
|
380
|
+
elif isinstance(content, ImageContent):
|
|
381
|
+
mime_type = getattr(content, "mimeType", "image/png")
|
|
382
|
+
data_length = len(content.data) if hasattr(content, "data") else 0
|
|
383
|
+
console.print(
|
|
384
|
+
Panel(
|
|
385
|
+
f"📷 Image ({mime_type})\nSize: {data_length:,} bytes (base64 encoded)",
|
|
386
|
+
title="Result",
|
|
387
|
+
border_style="green" if not result.isError else "red",
|
|
388
|
+
)
|
|
389
|
+
)
|
|
356
390
|
else:
|
|
357
391
|
# Handle other content types
|
|
358
392
|
console.print(json.dumps(content, indent=2))
|
hud/cli/utils/metadata.py
CHANGED
|
@@ -32,7 +32,7 @@ def fetch_lock_from_registry(reference: str) -> dict | None:
|
|
|
32
32
|
|
|
33
33
|
# URL-encode the path segments to handle special characters in tags
|
|
34
34
|
url_safe_path = "/".join(quote(part, safe="") for part in reference.split("/"))
|
|
35
|
-
registry_url = f"{settings.
|
|
35
|
+
registry_url = f"{settings.hud_api_url.rstrip('/')}/registry/envs/{url_safe_path}"
|
|
36
36
|
|
|
37
37
|
headers = {}
|
|
38
38
|
if settings.api_key:
|
|
@@ -173,6 +173,8 @@ async def analyze_from_metadata(reference: str, output_format: str, verbose: boo
|
|
|
173
173
|
"tools": [],
|
|
174
174
|
"resources": [],
|
|
175
175
|
"prompts": [],
|
|
176
|
+
"scenarios": [],
|
|
177
|
+
"verbose": verbose,
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
# Add basic info
|
|
@@ -206,6 +208,73 @@ async def analyze_from_metadata(reference: str, output_format: str, verbose: boo
|
|
|
206
208
|
}
|
|
207
209
|
)
|
|
208
210
|
|
|
211
|
+
# Extract resources
|
|
212
|
+
if "resources" in lock_data:
|
|
213
|
+
for resource in lock_data["resources"]:
|
|
214
|
+
analysis["resources"].append(
|
|
215
|
+
{
|
|
216
|
+
"uri": resource.get("uri", ""),
|
|
217
|
+
"name": resource.get("name", ""),
|
|
218
|
+
"description": resource.get("description", ""),
|
|
219
|
+
"mime_type": resource.get("mimeType", resource.get("mime_type", "")),
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Extract prompts
|
|
224
|
+
if "prompts" in lock_data:
|
|
225
|
+
for prompt in lock_data["prompts"]:
|
|
226
|
+
analysis["prompts"].append(
|
|
227
|
+
{
|
|
228
|
+
"name": prompt.get("name", ""),
|
|
229
|
+
"description": prompt.get("description", ""),
|
|
230
|
+
"arguments": prompt.get("arguments", []),
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Derive scenarios from scenario prompts/resources if present
|
|
235
|
+
scenarios_by_id: dict[str, dict] = {}
|
|
236
|
+
for p in analysis["prompts"]:
|
|
237
|
+
desc = (p.get("description") or "").strip()
|
|
238
|
+
if not desc.startswith("[Setup]"):
|
|
239
|
+
continue
|
|
240
|
+
scenario_id = p.get("name")
|
|
241
|
+
if not scenario_id:
|
|
242
|
+
continue
|
|
243
|
+
env_name, scenario_name = ([*scenario_id.split(":", 1), ""])[:2]
|
|
244
|
+
scenarios_by_id[scenario_id] = {
|
|
245
|
+
"id": scenario_id,
|
|
246
|
+
"env": env_name,
|
|
247
|
+
"name": scenario_name or scenario_id,
|
|
248
|
+
"setup_description": desc,
|
|
249
|
+
"arguments": p.get("arguments") or [],
|
|
250
|
+
"has_setup_prompt": True,
|
|
251
|
+
"has_evaluate_resource": False,
|
|
252
|
+
}
|
|
253
|
+
for r in analysis["resources"]:
|
|
254
|
+
desc = (r.get("description") or "").strip()
|
|
255
|
+
if not desc.startswith("[Evaluate]"):
|
|
256
|
+
continue
|
|
257
|
+
scenario_id = r.get("uri")
|
|
258
|
+
if not scenario_id:
|
|
259
|
+
continue
|
|
260
|
+
env_name, scenario_name = ([*scenario_id.split(":", 1), ""])[:2]
|
|
261
|
+
if scenario_id not in scenarios_by_id:
|
|
262
|
+
scenarios_by_id[scenario_id] = {
|
|
263
|
+
"id": scenario_id,
|
|
264
|
+
"env": env_name,
|
|
265
|
+
"name": scenario_name or scenario_id,
|
|
266
|
+
"arguments": [],
|
|
267
|
+
"has_setup_prompt": False,
|
|
268
|
+
"has_evaluate_resource": True,
|
|
269
|
+
}
|
|
270
|
+
scenarios_by_id[scenario_id]["evaluate_description"] = desc
|
|
271
|
+
scenarios_by_id[scenario_id]["has_evaluate_resource"] = True
|
|
272
|
+
|
|
273
|
+
analysis["scenarios"] = sorted(
|
|
274
|
+
scenarios_by_id.values(),
|
|
275
|
+
key=lambda s: (str(s.get("env") or ""), str(s.get("name") or "")),
|
|
276
|
+
)
|
|
277
|
+
|
|
209
278
|
# Display results
|
|
210
279
|
hud_console.info("")
|
|
211
280
|
if source == "local":
|
hud/cli/utils/runner.py
CHANGED
|
@@ -16,7 +16,7 @@ def run_stdio_server(image: str, docker_args: list[str], verbose: bool) -> None:
|
|
|
16
16
|
"""Run Docker image as stdio MCP server (direct passthrough)."""
|
|
17
17
|
hud_console = HUDConsole() # Use stderr for stdio mode
|
|
18
18
|
|
|
19
|
-
# Build docker command
|
|
19
|
+
# Build docker command (image-only mode: do not auto-inject local .env)
|
|
20
20
|
docker_cmd = ["docker", "run", "--rm", "-i", *docker_args, image]
|
|
21
21
|
|
|
22
22
|
if verbose:
|
hud/cli/utils/server.py
CHANGED
|
@@ -138,9 +138,9 @@ class MCPServerManager:
|
|
|
138
138
|
logging.getLogger("uvicorn.access").setLevel(logging.ERROR)
|
|
139
139
|
logging.getLogger("uvicorn.error").setLevel(logging.ERROR)
|
|
140
140
|
|
|
141
|
-
import
|
|
141
|
+
from hud.patches.warnings import apply_default_warning_filters
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
apply_default_warning_filters(verbose=False)
|
|
144
144
|
|
|
145
145
|
try:
|
|
146
146
|
await proxy.run_async(
|
hud/cli/utils/source_hash.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Utilities to compute a fast, deterministic source hash for environments.
|
|
2
2
|
|
|
3
3
|
This intentionally focuses on the typical HUD environment layout and aims to be fast:
|
|
4
|
-
- Always include: Dockerfile, pyproject.toml
|
|
4
|
+
- Always include: Dockerfile.hud, Dockerfile, pyproject.toml
|
|
5
5
|
- Include directories: controller/, environment/, src/
|
|
6
6
|
- Exclude common build/runtime caches and lock files
|
|
7
7
|
|
|
@@ -40,8 +40,8 @@ EXCLUDE_FILES = {
|
|
|
40
40
|
"hud.lock.yaml",
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
INCLUDE_FILES = {"Dockerfile", "pyproject.toml"}
|
|
44
|
-
INCLUDE_DIRS = {"controller", "environment"}
|
|
43
|
+
INCLUDE_FILES = {"Dockerfile", "Dockerfile.hud", "pyproject.toml"}
|
|
44
|
+
INCLUDE_DIRS = {"server", "mcp", "controller", "environment"}
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def iter_source_files(root: Path) -> Iterable[Path]:
|
hud/cli/utils/tasks.py
CHANGED
|
@@ -18,9 +18,12 @@ def find_tasks_file(tasks_file: str | None, msg: str = "Select a tasks file") ->
|
|
|
18
18
|
]
|
|
19
19
|
all_files = [file for file in all_files if file[0] != "."] # Remove all config files
|
|
20
20
|
|
|
21
|
+
if not all_files:
|
|
22
|
+
# No task files found - raise a clear exception
|
|
23
|
+
raise FileNotFoundError("No task JSON or JSONL files found in current directory")
|
|
24
|
+
|
|
21
25
|
if len(all_files) == 1:
|
|
22
26
|
return str(all_files[0])
|
|
23
|
-
|
|
24
27
|
else:
|
|
25
28
|
# Prompt user to select a file
|
|
26
29
|
return hud_console.select(msg, choices=all_files)
|
|
File without changes
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from hud.cli.utils.config import (
|
|
6
|
+
ensure_config_dir,
|
|
7
|
+
get_config_dir,
|
|
8
|
+
get_user_env_path,
|
|
9
|
+
load_env_file,
|
|
10
|
+
parse_env_file,
|
|
11
|
+
render_env_file,
|
|
12
|
+
save_env_file,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_parse_env_file_basic():
|
|
20
|
+
contents = """
|
|
21
|
+
# comment
|
|
22
|
+
KEY=VALUE
|
|
23
|
+
EMPTY=
|
|
24
|
+
NOEQ
|
|
25
|
+
SPACED = v
|
|
26
|
+
""" # noqa: W291
|
|
27
|
+
data = parse_env_file(contents)
|
|
28
|
+
assert data["KEY"] == "VALUE"
|
|
29
|
+
assert data["EMPTY"] == ""
|
|
30
|
+
assert data["SPACED"] == "v"
|
|
31
|
+
assert "NOEQ" not in data
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_render_and_load_roundtrip(tmp_path: Path):
|
|
35
|
+
env = {"A": "1", "B": "2"}
|
|
36
|
+
file_path = tmp_path / ".env"
|
|
37
|
+
rendered = render_env_file(env)
|
|
38
|
+
file_path.write_text(rendered, encoding="utf-8")
|
|
39
|
+
loaded = load_env_file(file_path)
|
|
40
|
+
assert loaded == env
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_get_paths(monkeypatch, tmp_path: Path):
|
|
44
|
+
from pathlib import Path as _Path
|
|
45
|
+
|
|
46
|
+
monkeypatch.setattr(_Path, "home", lambda: tmp_path)
|
|
47
|
+
cfg = get_config_dir()
|
|
48
|
+
assert str(cfg).replace("\\", "/").endswith("/.hud")
|
|
49
|
+
assert str(get_user_env_path()).replace("\\", "/").endswith("/.hud/.env")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_ensure_and_save(tmp_path: Path, monkeypatch):
|
|
53
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
54
|
+
cfg = ensure_config_dir()
|
|
55
|
+
assert cfg.exists()
|
|
56
|
+
out = save_env_file({"K": "V"})
|
|
57
|
+
assert out.exists()
|
|
58
|
+
assert load_env_file(out) == {"K": "V"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from hud.cli.utils.docker import (
|
|
8
|
+
build_run_command,
|
|
9
|
+
generate_container_name,
|
|
10
|
+
get_docker_cmd,
|
|
11
|
+
image_exists,
|
|
12
|
+
remove_container,
|
|
13
|
+
require_docker_running,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_build_run_command_basic():
|
|
18
|
+
cmd = build_run_command("my-image:latest")
|
|
19
|
+
assert cmd[:4] == ["docker", "run", "--rm", "-i"]
|
|
20
|
+
assert cmd[-1] == "my-image:latest"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_build_run_command_with_args():
|
|
24
|
+
cmd = build_run_command("img", ["-e", "K=V", "-p", "8080:8080"])
|
|
25
|
+
assert "-e" in cmd and "K=V" in cmd
|
|
26
|
+
assert "-p" in cmd and "8080:8080" in cmd
|
|
27
|
+
assert cmd[-1] == "img"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_generate_container_name():
|
|
31
|
+
assert generate_container_name("repo/name:tag") == "hud-repo-name-tag"
|
|
32
|
+
assert generate_container_name("a/b:c", prefix="x") == "x-a-b-c"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@patch("subprocess.run")
|
|
36
|
+
def test_image_exists_true(mock_run):
|
|
37
|
+
mock_run.return_value = MagicMock(returncode=0)
|
|
38
|
+
assert image_exists("any") is True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@patch("subprocess.run")
|
|
42
|
+
def test_image_exists_false(mock_run):
|
|
43
|
+
mock_run.return_value = MagicMock(returncode=1)
|
|
44
|
+
assert image_exists("any") is False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@patch("subprocess.run")
|
|
48
|
+
def test_get_docker_cmd_success(mock_run):
|
|
49
|
+
mock_run.return_value = MagicMock(
|
|
50
|
+
stdout='[{"Config": {"Cmd": ["python", "-m", "app"]}}]', returncode=0
|
|
51
|
+
)
|
|
52
|
+
assert get_docker_cmd("img") == ["python", "-m", "app"]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@patch("subprocess.run")
|
|
56
|
+
def test_get_docker_cmd_none(mock_run):
|
|
57
|
+
mock_run.return_value = MagicMock(stdout="[]", returncode=0)
|
|
58
|
+
assert get_docker_cmd("img") is None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@patch("subprocess.run")
|
|
62
|
+
def test_remove_container_ok(mock_run):
|
|
63
|
+
mock_run.return_value = MagicMock(returncode=0)
|
|
64
|
+
assert remove_container("x") is True
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@patch("shutil.which", return_value=None)
|
|
68
|
+
def test_require_docker_running_no_cli(_which):
|
|
69
|
+
import typer
|
|
70
|
+
|
|
71
|
+
with pytest.raises(typer.Exit):
|
|
72
|
+
require_docker_running()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@patch("shutil.which", return_value="docker")
|
|
76
|
+
@patch("subprocess.run")
|
|
77
|
+
def test_require_docker_running_ok(mock_run, _which):
|
|
78
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
79
|
+
require_docker_running() # should not raise
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@patch("shutil.which", return_value="docker")
|
|
83
|
+
@patch("subprocess.run")
|
|
84
|
+
def test_require_docker_running_error_emits_hints(mock_run, _which):
|
|
85
|
+
import typer
|
|
86
|
+
|
|
87
|
+
mock_run.return_value = MagicMock(
|
|
88
|
+
returncode=1,
|
|
89
|
+
stdout="Cannot connect to the Docker daemon",
|
|
90
|
+
stderr="",
|
|
91
|
+
)
|
|
92
|
+
with pytest.raises(typer.Exit):
|
|
93
|
+
require_docker_running()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from hud.cli.utils import docker as mod
|
|
8
|
+
|
|
9
|
+
pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="Prefers Linux")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_emit_docker_hints_windows(monkeypatch):
|
|
13
|
+
# Patch the global hud_console used by hint printing
|
|
14
|
+
|
|
15
|
+
fake = type(
|
|
16
|
+
"C",
|
|
17
|
+
(),
|
|
18
|
+
{
|
|
19
|
+
"error": lambda *a, **k: None,
|
|
20
|
+
"hint": lambda *a, **k: None,
|
|
21
|
+
"dim_info": lambda *a, **k: None,
|
|
22
|
+
},
|
|
23
|
+
)()
|
|
24
|
+
monkeypatch.setattr("hud.utils.hud_console.hud_console", fake, raising=False)
|
|
25
|
+
monkeypatch.setattr(mod.platform, "system", lambda: "Windows")
|
|
26
|
+
mod._emit_docker_hints("cannot connect to the docker daemon")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_emit_docker_hints_linux(monkeypatch):
|
|
30
|
+
fake = type(
|
|
31
|
+
"C",
|
|
32
|
+
(),
|
|
33
|
+
{
|
|
34
|
+
"error": lambda *a, **k: None,
|
|
35
|
+
"hint": lambda *a, **k: None,
|
|
36
|
+
"dim_info": lambda *a, **k: None,
|
|
37
|
+
},
|
|
38
|
+
)()
|
|
39
|
+
monkeypatch.setattr("hud.utils.hud_console.hud_console", fake, raising=False)
|
|
40
|
+
monkeypatch.setattr(mod.platform, "system", lambda: "Linux")
|
|
41
|
+
mod._emit_docker_hints("Cannot connect to the Docker daemon")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_emit_docker_hints_darwin(monkeypatch):
|
|
45
|
+
fake = type(
|
|
46
|
+
"C",
|
|
47
|
+
(),
|
|
48
|
+
{
|
|
49
|
+
"error": lambda *a, **k: None,
|
|
50
|
+
"hint": lambda *a, **k: None,
|
|
51
|
+
"dim_info": lambda *a, **k: None,
|
|
52
|
+
},
|
|
53
|
+
)()
|
|
54
|
+
monkeypatch.setattr("hud.utils.hud_console.hud_console", fake, raising=False)
|
|
55
|
+
monkeypatch.setattr(mod.platform, "system", lambda: "Darwin")
|
|
56
|
+
mod._emit_docker_hints("error during connect: is the docker daemon running")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_emit_docker_hints_generic(monkeypatch):
|
|
60
|
+
fake = type(
|
|
61
|
+
"C",
|
|
62
|
+
(),
|
|
63
|
+
{
|
|
64
|
+
"error": lambda *a, **k: None,
|
|
65
|
+
"hint": lambda *a, **k: None,
|
|
66
|
+
"dim_info": lambda *a, **k: None,
|
|
67
|
+
},
|
|
68
|
+
)()
|
|
69
|
+
monkeypatch.setattr("hud.utils.hud_console.hud_console", fake, raising=False)
|
|
70
|
+
monkeypatch.setattr(mod.platform, "system", lambda: "Other")
|
|
71
|
+
mod._emit_docker_hints("some unrelated error")
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime, timedelta
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
from hud.cli.utils.env_check import (
|
|
8
|
+
_collect_source_diffs,
|
|
9
|
+
_parse_generated_at,
|
|
10
|
+
ensure_built,
|
|
11
|
+
find_environment_dir,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_parse_generated_at_variants():
|
|
19
|
+
ts = _parse_generated_at({"build": {"generatedAt": datetime.now(UTC).isoformat()}})
|
|
20
|
+
assert isinstance(ts, float)
|
|
21
|
+
assert _parse_generated_at({}) is None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_collect_source_diffs_basic(tmp_path: Path):
|
|
25
|
+
env = tmp_path / "env"
|
|
26
|
+
env.mkdir()
|
|
27
|
+
# simulate files
|
|
28
|
+
(env / "Dockerfile").write_text("FROM python:3.11")
|
|
29
|
+
(env / "pyproject.toml").write_text("[tool.hud]")
|
|
30
|
+
(env / "a.txt").write_text("x")
|
|
31
|
+
|
|
32
|
+
# stored file list includes a non-existent file and old time
|
|
33
|
+
built_time = (datetime.now(UTC) - timedelta(days=1)).isoformat()
|
|
34
|
+
lock = {"build": {"sourceFiles": ["a.txt", "b.txt"], "generatedAt": built_time}}
|
|
35
|
+
|
|
36
|
+
# Patch list_source_files to return current env files
|
|
37
|
+
with patch("hud.cli.utils.env_check.list_source_files") as mock_list:
|
|
38
|
+
mock_list.return_value = [env / "a.txt", env / "Dockerfile"]
|
|
39
|
+
diffs = _collect_source_diffs(env, lock)
|
|
40
|
+
assert "Dockerfile" in diffs["added"]
|
|
41
|
+
assert "b.txt" in diffs["removed"]
|
|
42
|
+
assert "a.txt" in diffs["modified"] or "a.txt" in diffs["added"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_find_environment_dir_prefers_lock(tmp_path: Path):
|
|
46
|
+
# Create env as a sibling to tasks, so it will be in the candidates list
|
|
47
|
+
parent = tmp_path / "parent"
|
|
48
|
+
parent.mkdir()
|
|
49
|
+
tasks = parent / "tasks.json"
|
|
50
|
+
tasks.write_text("[]")
|
|
51
|
+
env = tmp_path / "env"
|
|
52
|
+
env.mkdir()
|
|
53
|
+
(env / "hud.lock.yaml").write_text("version: 1.3")
|
|
54
|
+
# Set cwd to env so it's in the candidate list
|
|
55
|
+
with patch("pathlib.Path.cwd", return_value=env):
|
|
56
|
+
found = find_environment_dir(tasks)
|
|
57
|
+
# Should find env because cwd returns env and it has hud.lock.yaml
|
|
58
|
+
assert found == env
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_ensure_built_no_lock_noninteractive(tmp_path: Path):
|
|
62
|
+
env = tmp_path / "e"
|
|
63
|
+
env.mkdir()
|
|
64
|
+
# Non-interactive: returns empty dict and does not raise
|
|
65
|
+
result = ensure_built(env, interactive=False)
|
|
66
|
+
assert result == {}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_ensure_built_interactive_build(tmp_path: Path):
|
|
70
|
+
env = tmp_path / "e"
|
|
71
|
+
env.mkdir()
|
|
72
|
+
# Simulate interactive=False path avoids prompts
|
|
73
|
+
result = ensure_built(env, interactive=False)
|
|
74
|
+
assert result == {}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from unittest.mock import MagicMock, patch
|
|
5
|
+
|
|
6
|
+
from hud.cli.utils.environment import get_image_name, image_exists, is_environment_directory
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_get_image_name_override():
|
|
13
|
+
name, source = get_image_name(".", image_override="custom:dev")
|
|
14
|
+
assert name == "custom:dev" and source == "override"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_get_image_name_auto(tmp_path: Path):
|
|
18
|
+
env = tmp_path / "my_env"
|
|
19
|
+
env.mkdir()
|
|
20
|
+
# Provide Dockerfile and pyproject to pass directory check later if used
|
|
21
|
+
(env / "Dockerfile").write_text("FROM python:3.11")
|
|
22
|
+
(env / "pyproject.toml").write_text("[tool.hud]\nimage='x'")
|
|
23
|
+
name, source = get_image_name(env)
|
|
24
|
+
# Because pyproject exists with image key, source should be cache
|
|
25
|
+
assert source == "cache"
|
|
26
|
+
assert name == "x"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_is_environment_directory(tmp_path: Path):
|
|
30
|
+
d = tmp_path / "env"
|
|
31
|
+
d.mkdir()
|
|
32
|
+
assert is_environment_directory(d) is False
|
|
33
|
+
(d / "Dockerfile").write_text("FROM python:3.11")
|
|
34
|
+
assert is_environment_directory(d) is False
|
|
35
|
+
(d / "pyproject.toml").write_text("[tool.hud]")
|
|
36
|
+
assert is_environment_directory(d) is True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@patch("subprocess.run")
|
|
40
|
+
def test_image_exists_true(mock_run):
|
|
41
|
+
mock_run.return_value = MagicMock(returncode=0)
|
|
42
|
+
assert image_exists("img") is True
|