hud-python 0.4.21__py3-none-any.whl → 0.4.23__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 +37 -37
- hud/agents/claude.py +11 -6
- hud/agents/grounded_openai.py +282 -0
- hud/agents/misc/response_agent.py +3 -2
- hud/agents/openai.py +2 -2
- hud/agents/openai_chat_generic.py +3 -1
- hud/agents/tests/test_client.py +6 -1
- hud/agents/tests/test_grounded_openai_agent.py +155 -0
- hud/cli/__init__.py +34 -24
- hud/cli/analyze.py +27 -26
- hud/cli/build.py +50 -46
- hud/cli/debug.py +7 -7
- hud/cli/dev.py +107 -99
- hud/cli/eval.py +33 -31
- hud/cli/hf.py +53 -53
- hud/cli/init.py +28 -28
- hud/cli/list_func.py +22 -22
- hud/cli/pull.py +36 -36
- hud/cli/push.py +76 -74
- hud/cli/remove.py +42 -40
- hud/cli/rl/__init__.py +2 -2
- hud/cli/rl/init.py +41 -41
- hud/cli/rl/pod.py +97 -91
- hud/cli/rl/ssh.py +42 -40
- hud/cli/rl/train.py +75 -73
- hud/cli/rl/utils.py +10 -10
- hud/cli/tests/test_analyze.py +1 -1
- hud/cli/tests/test_analyze_metadata.py +2 -2
- hud/cli/tests/test_pull.py +45 -45
- hud/cli/tests/test_push.py +31 -29
- hud/cli/tests/test_registry.py +15 -15
- hud/cli/utils/environment.py +11 -11
- hud/cli/utils/interactive.py +18 -18
- hud/cli/utils/logging.py +12 -12
- hud/cli/utils/metadata.py +12 -12
- hud/cli/utils/registry.py +5 -5
- hud/cli/utils/runner.py +23 -23
- hud/cli/utils/server.py +16 -16
- hud/settings.py +6 -0
- hud/shared/hints.py +7 -7
- hud/tools/executors/tests/test_base_executor.py +1 -1
- hud/tools/executors/xdo.py +1 -1
- hud/tools/grounding/__init__.py +13 -0
- hud/tools/grounding/config.py +54 -0
- hud/tools/grounding/grounded_tool.py +314 -0
- hud/tools/grounding/grounder.py +302 -0
- hud/tools/grounding/tests/__init__.py +1 -0
- hud/tools/grounding/tests/test_grounded_tool.py +196 -0
- hud/tools/tests/test_playwright_tool.py +1 -1
- hud/tools/tests/test_tools_init.py +1 -1
- hud/tools/tests/test_utils.py +2 -2
- hud/types.py +4 -4
- hud/utils/__init__.py +3 -3
- hud/utils/agent_factories.py +86 -0
- hud/utils/{design.py → hud_console.py} +39 -33
- hud/utils/pretty_errors.py +6 -6
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.4.21.dist-info → hud_python-0.4.23.dist-info}/METADATA +3 -1
- {hud_python-0.4.21.dist-info → hud_python-0.4.23.dist-info}/RECORD +63 -54
- {hud_python-0.4.21.dist-info → hud_python-0.4.23.dist-info}/WHEEL +0 -0
- {hud_python-0.4.21.dist-info → hud_python-0.4.23.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.21.dist-info → hud_python-0.4.23.dist-info}/licenses/LICENSE +0 -0
hud/cli/rl/train.py
CHANGED
|
@@ -11,7 +11,7 @@ from typing import Any
|
|
|
11
11
|
import typer
|
|
12
12
|
|
|
13
13
|
from hud.settings import settings
|
|
14
|
-
from hud.utils.
|
|
14
|
+
from hud.utils.hud_console import HUDConsole
|
|
15
15
|
|
|
16
16
|
from .pod import run_prime_training
|
|
17
17
|
from .utils import (
|
|
@@ -20,7 +20,7 @@ from .utils import (
|
|
|
20
20
|
validate_dataset_name,
|
|
21
21
|
)
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
hud_console = HUDConsole()
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def find_task_json_files() -> list[Path]:
|
|
@@ -67,7 +67,7 @@ def train_command_wrapper(
|
|
|
67
67
|
) -> None:
|
|
68
68
|
"""Wrapper to handle interactive prompts before entering async context."""
|
|
69
69
|
# Pre-flight checks for required environment variables
|
|
70
|
-
|
|
70
|
+
hud_console.section_title("🔍 Pre-flight Checks")
|
|
71
71
|
|
|
72
72
|
missing_vars = []
|
|
73
73
|
|
|
@@ -75,30 +75,32 @@ def train_command_wrapper(
|
|
|
75
75
|
if not settings.api_key:
|
|
76
76
|
missing_vars.append("HUD_API_KEY")
|
|
77
77
|
else:
|
|
78
|
-
|
|
78
|
+
hud_console.success("✓ HUD_API_KEY configured")
|
|
79
79
|
|
|
80
80
|
# Check WANDB API key (optional but recommended)
|
|
81
81
|
if not getattr(settings, "wandb_api_key", None):
|
|
82
|
-
|
|
82
|
+
hud_console.warning(
|
|
83
|
+
"⚠ WANDB_API_KEY not set (optional but recommended for training metrics)"
|
|
84
|
+
)
|
|
83
85
|
else:
|
|
84
|
-
|
|
86
|
+
hud_console.success("✓ WANDB_API_KEY configured")
|
|
85
87
|
|
|
86
88
|
# Check PRIME API key (required for remote training)
|
|
87
89
|
if provider == "prime" and not getattr(settings, "prime_api_key", None):
|
|
88
90
|
missing_vars.append("PRIME_API_KEY")
|
|
89
91
|
elif provider == "prime":
|
|
90
|
-
|
|
92
|
+
hud_console.success("✓ PRIME_API_KEY configured")
|
|
91
93
|
|
|
92
94
|
if missing_vars:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
hud_console.error(f"Missing required environment variables: {', '.join(missing_vars)}")
|
|
96
|
+
hud_console.info("")
|
|
97
|
+
hud_console.info("Set them using one of these methods:")
|
|
98
|
+
hud_console.info("1. Environment variables:")
|
|
97
99
|
for var in missing_vars:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
hud_console.command_example(f"export {var}=your-{var.lower().replace('_', '-')}")
|
|
101
|
+
hud_console.info("")
|
|
102
|
+
hud_console.info("2. Create a .env file in your project root:")
|
|
103
|
+
hud_console.command_example(
|
|
102
104
|
"\n".join([f"{var}=your-{var.lower().replace('_', '-')}" for var in missing_vars]),
|
|
103
105
|
"env",
|
|
104
106
|
)
|
|
@@ -114,7 +116,7 @@ def train_command_wrapper(
|
|
|
114
116
|
yaml_files = list(config_dir.glob("*.yaml"))
|
|
115
117
|
if len(yaml_files) == 1:
|
|
116
118
|
config = yaml_files[0]
|
|
117
|
-
|
|
119
|
+
hud_console.info(f"Using config: {config}")
|
|
118
120
|
|
|
119
121
|
# Store user choice for pod creation
|
|
120
122
|
auto_create_pod = None
|
|
@@ -128,20 +130,20 @@ def train_command_wrapper(
|
|
|
128
130
|
config_dir = Path("configs")
|
|
129
131
|
yaml_files = list(config_dir.glob("*.yaml"))
|
|
130
132
|
config_names = [f.name for f in yaml_files]
|
|
131
|
-
selected_config =
|
|
133
|
+
selected_config = hud_console.select(
|
|
132
134
|
"Multiple config files found. Select one:", config_names
|
|
133
135
|
)
|
|
134
136
|
config = config_dir / selected_config
|
|
135
137
|
else:
|
|
136
138
|
# No config found, offer to generate
|
|
137
|
-
generate_config =
|
|
139
|
+
generate_config = hud_console.select(
|
|
138
140
|
"No config file found. Would you like to generate one?",
|
|
139
141
|
["Yes, generate config", "No, I'll create it manually"],
|
|
140
142
|
)
|
|
141
143
|
|
|
142
144
|
if generate_config == "Yes, generate config":
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
hud_console.info("Running 'hud rl init' to generate config...")
|
|
146
|
+
hud_console.info("")
|
|
145
147
|
# Import here to avoid circular imports
|
|
146
148
|
from .init import init_command_wrapper
|
|
147
149
|
|
|
@@ -153,29 +155,29 @@ def train_command_wrapper(
|
|
|
153
155
|
yaml_files = list(config_dir.glob("*.yaml"))
|
|
154
156
|
if yaml_files:
|
|
155
157
|
config = yaml_files[0]
|
|
156
|
-
|
|
158
|
+
hud_console.success(f"Using generated config: {config}")
|
|
157
159
|
else:
|
|
158
|
-
|
|
160
|
+
hud_console.error("Config generation failed")
|
|
159
161
|
raise typer.Exit(1)
|
|
160
162
|
else:
|
|
161
|
-
|
|
163
|
+
hud_console.info("Please create a config file and try again")
|
|
162
164
|
raise typer.Exit(1)
|
|
163
165
|
|
|
164
166
|
if "dataset" in missing:
|
|
165
167
|
if missing["dataset"] == "multiple_json":
|
|
166
168
|
# Multiple JSON files found, let user choose
|
|
167
169
|
json_files = find_task_json_files()
|
|
168
|
-
|
|
169
|
-
file_choice =
|
|
170
|
+
hud_console.info("Multiple task files found:")
|
|
171
|
+
file_choice = hud_console.select(
|
|
170
172
|
"Select a task file to use:",
|
|
171
173
|
choices=[str(f) for f in json_files],
|
|
172
174
|
)
|
|
173
175
|
dataset = file_choice
|
|
174
|
-
|
|
176
|
+
hud_console.success(f"Selected: {dataset}")
|
|
175
177
|
elif missing["dataset"] == "none":
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
178
|
+
hud_console.error("No dataset specified and no task JSON files found")
|
|
179
|
+
hud_console.info("Please use --dataset or create a tasks.json file")
|
|
180
|
+
hud_console.hint(
|
|
179
181
|
"Example: hud hf --name my-org/my-tasks # Generate tasks from HUD evaluation"
|
|
180
182
|
)
|
|
181
183
|
raise typer.Exit(1)
|
|
@@ -201,12 +203,12 @@ def train_command_wrapper(
|
|
|
201
203
|
value = parts[1].strip()
|
|
202
204
|
if value and value != "None":
|
|
203
205
|
has_global_team = True
|
|
204
|
-
|
|
206
|
+
hud_console.info("Using globally configured team ID")
|
|
205
207
|
break
|
|
206
208
|
|
|
207
209
|
if not has_global_team:
|
|
208
210
|
# Only ask if no global team is configured
|
|
209
|
-
auto_create_pod =
|
|
211
|
+
auto_create_pod = hud_console.select(
|
|
210
212
|
"How would you like to create the Prime Intellect pod?",
|
|
211
213
|
["Personal account (automated)", "Team account (enter team ID)"],
|
|
212
214
|
)
|
|
@@ -217,7 +219,7 @@ def train_command_wrapper(
|
|
|
217
219
|
|
|
218
220
|
# Save it globally automatically
|
|
219
221
|
subprocess.run(["prime", "config", "set-team-id", team_id]) # noqa: S603, S607
|
|
220
|
-
|
|
222
|
+
hud_console.success("Team ID saved globally")
|
|
221
223
|
|
|
222
224
|
auto_create_pod = (
|
|
223
225
|
"Personal account (automated)" # Treat as automated after getting team ID
|
|
@@ -249,13 +251,13 @@ async def train_command(
|
|
|
249
251
|
team_id: str | None = None,
|
|
250
252
|
) -> None:
|
|
251
253
|
"""Run RL training on HUD environments."""
|
|
252
|
-
|
|
254
|
+
hud_console.header("🤖 HUD RL Training")
|
|
253
255
|
|
|
254
256
|
# Get environment image
|
|
255
257
|
image = detect_image_name()
|
|
256
258
|
if not image:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
+
hud_console.error("No environment image found")
|
|
260
|
+
hud_console.hint("Run 'hud build' first or specify with 'hud rl init <image>'")
|
|
259
261
|
raise typer.Exit(1)
|
|
260
262
|
|
|
261
263
|
# Handle dataset (JSON file or HuggingFace dataset)
|
|
@@ -269,17 +271,17 @@ async def train_command(
|
|
|
269
271
|
if json_files:
|
|
270
272
|
if len(json_files) == 1:
|
|
271
273
|
dataset = str(json_files[0])
|
|
272
|
-
|
|
274
|
+
hud_console.info(f"Found task file: {dataset}")
|
|
273
275
|
is_json_file = True
|
|
274
276
|
else:
|
|
275
277
|
# This case should have been handled in train_command_wrapper
|
|
276
|
-
|
|
278
|
+
hud_console.error("Multiple task files found but none selected")
|
|
277
279
|
raise typer.Exit(1)
|
|
278
280
|
else:
|
|
279
281
|
# Use dataset from lock file
|
|
280
282
|
dataset = get_primary_dataset()
|
|
281
283
|
if dataset:
|
|
282
|
-
|
|
284
|
+
hud_console.info(f"Using dataset from lock file: {dataset}")
|
|
283
285
|
|
|
284
286
|
# Check if dataset is a file path
|
|
285
287
|
if dataset and Path(dataset).exists() and dataset.endswith(".json"):
|
|
@@ -288,7 +290,7 @@ async def train_command(
|
|
|
288
290
|
# Validate dataset
|
|
289
291
|
if dataset and is_json_file:
|
|
290
292
|
# Load and validate JSON file
|
|
291
|
-
|
|
293
|
+
hud_console.info(f"Validating task file: {dataset}")
|
|
292
294
|
try:
|
|
293
295
|
with open(dataset) as f: # noqa: ASYNC230
|
|
294
296
|
tasks_data = json.load(f)
|
|
@@ -299,17 +301,17 @@ async def train_command(
|
|
|
299
301
|
elif isinstance(tasks_data, list):
|
|
300
302
|
tasks = tasks_data
|
|
301
303
|
else:
|
|
302
|
-
|
|
304
|
+
hud_console.error("Invalid tasks file format")
|
|
303
305
|
raise typer.Exit(1)
|
|
304
306
|
|
|
305
307
|
dataset_size = len(tasks)
|
|
306
308
|
if dataset_size < 4:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
309
|
+
hud_console.error(f"Task file has only {dataset_size} tasks")
|
|
310
|
+
hud_console.info("RL training requires at least 4 tasks for proper batching")
|
|
311
|
+
hud_console.hint("Consider adding more tasks to your JSON file")
|
|
310
312
|
raise typer.Exit(1)
|
|
311
313
|
|
|
312
|
-
|
|
314
|
+
hud_console.success(f"✓ Task file has {dataset_size} tasks")
|
|
313
315
|
|
|
314
316
|
# Check and convert MCP configs to remote if needed
|
|
315
317
|
if tasks:
|
|
@@ -328,7 +330,7 @@ async def train_command(
|
|
|
328
330
|
config_type = "local"
|
|
329
331
|
|
|
330
332
|
if config_type == "local":
|
|
331
|
-
|
|
333
|
+
hud_console.info("Converting local MCP configs to remote for training...")
|
|
332
334
|
|
|
333
335
|
# Get the image name from lock file or environment
|
|
334
336
|
from .utils import get_image_from_lock
|
|
@@ -336,18 +338,18 @@ async def train_command(
|
|
|
336
338
|
env_image = image or get_image_from_lock()
|
|
337
339
|
|
|
338
340
|
if not env_image:
|
|
339
|
-
|
|
340
|
-
|
|
341
|
+
hud_console.error("No image found for remote MCP conversion")
|
|
342
|
+
hud_console.hint("Run 'hud build' first")
|
|
341
343
|
raise typer.Exit(1)
|
|
342
344
|
|
|
343
345
|
# Check if image needs to be pushed
|
|
344
346
|
if "/" not in env_image or env_image.startswith("local/"):
|
|
345
|
-
|
|
346
|
-
|
|
347
|
+
hud_console.warning(f"Image '{env_image}' appears to be local only")
|
|
348
|
+
hud_console.info("Running 'hud push' to make it publicly available...")
|
|
347
349
|
from hud.cli.push import push_command
|
|
348
350
|
|
|
349
351
|
push_command(directory=".", yes=True)
|
|
350
|
-
|
|
352
|
+
hud_console.success("Image pushed successfully")
|
|
351
353
|
# Re-read image name after push
|
|
352
354
|
env_image = get_image_from_lock()
|
|
353
355
|
|
|
@@ -364,18 +366,18 @@ async def train_command(
|
|
|
364
366
|
}
|
|
365
367
|
task["mcp_config"] = remote_config
|
|
366
368
|
|
|
367
|
-
|
|
369
|
+
hud_console.success("✓ Converted all tasks to use remote MCP configs")
|
|
368
370
|
|
|
369
371
|
# Save the modified tasks back to the file
|
|
370
372
|
with open(dataset, "w") as f: # noqa: ASYNC230
|
|
371
373
|
json.dump(tasks, f, indent=2)
|
|
372
|
-
|
|
374
|
+
hud_console.info("Updated task file with remote configs")
|
|
373
375
|
except json.JSONDecodeError as e:
|
|
374
|
-
|
|
376
|
+
hud_console.error(f"Invalid JSON in task file: {e}")
|
|
375
377
|
raise typer.Exit(1) from e
|
|
376
378
|
elif dataset:
|
|
377
379
|
# Validate HuggingFace dataset
|
|
378
|
-
|
|
380
|
+
hud_console.info(f"Validating dataset: {dataset}")
|
|
379
381
|
try:
|
|
380
382
|
# Try to load dataset info from HuggingFace
|
|
381
383
|
from datasets import load_dataset_builder
|
|
@@ -386,21 +388,21 @@ async def train_command(
|
|
|
386
388
|
# Check split sizes
|
|
387
389
|
train_size = ds_info.splits.get("train", None) if ds_info.splits else None
|
|
388
390
|
if train_size and train_size.num_examples < 4:
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
391
|
+
hud_console.error(f"Dataset '{dataset}' has only {train_size.num_examples} tasks")
|
|
392
|
+
hud_console.info("RL training requires at least 4 tasks for proper batching")
|
|
393
|
+
hud_console.hint("Consider adding more tasks or duplicating existing ones")
|
|
392
394
|
raise typer.Exit(1)
|
|
393
395
|
elif train_size:
|
|
394
396
|
dataset_size = train_size.num_examples
|
|
395
|
-
|
|
397
|
+
hud_console.success(f"✓ Dataset has {dataset_size} tasks")
|
|
396
398
|
except Exception as e:
|
|
397
399
|
# If we can't validate, warn but continue
|
|
398
|
-
|
|
399
|
-
|
|
400
|
+
hud_console.warning(f"Could not validate dataset size: {e}")
|
|
401
|
+
hud_console.info("Proceeding with training - ensure dataset has at least 4 tasks")
|
|
400
402
|
|
|
401
403
|
# Display configuration
|
|
402
|
-
|
|
403
|
-
|
|
404
|
+
hud_console.section_title("📋 Training Configuration")
|
|
405
|
+
hud_console.json_config(
|
|
404
406
|
json.dumps(
|
|
405
407
|
{
|
|
406
408
|
"Model": model,
|
|
@@ -416,13 +418,13 @@ async def train_command(
|
|
|
416
418
|
)
|
|
417
419
|
|
|
418
420
|
if not config:
|
|
419
|
-
|
|
420
|
-
|
|
421
|
+
hud_console.error("No config file found")
|
|
422
|
+
hud_console.hint("Run 'hud rl init' to generate a config file")
|
|
421
423
|
raise typer.Exit(1)
|
|
422
424
|
|
|
423
425
|
if not dataset:
|
|
424
|
-
|
|
425
|
-
|
|
426
|
+
hud_console.error("No dataset found")
|
|
427
|
+
hud_console.hint("Run 'hud hf tasks.json' to create a dataset")
|
|
426
428
|
raise typer.Exit(1)
|
|
427
429
|
|
|
428
430
|
# Always run remote training
|
|
@@ -499,14 +501,14 @@ def create_dataset_interactive() -> str | None:
|
|
|
499
501
|
# Check if tasks.json exists
|
|
500
502
|
tasks_file = Path("tasks.json")
|
|
501
503
|
if not tasks_file.exists():
|
|
502
|
-
|
|
504
|
+
hud_console.error("No tasks.json file found")
|
|
503
505
|
return None
|
|
504
506
|
|
|
505
507
|
# Prompt for dataset name
|
|
506
508
|
dataset_name = typer.prompt("Enter HuggingFace dataset name (e.g., username/dataset-name)")
|
|
507
509
|
|
|
508
510
|
if not validate_dataset_name(dataset_name):
|
|
509
|
-
|
|
511
|
+
hud_console.error("Invalid dataset name format")
|
|
510
512
|
return None
|
|
511
513
|
|
|
512
514
|
# Run hf command
|
|
@@ -519,9 +521,9 @@ def create_dataset_interactive() -> str | None:
|
|
|
519
521
|
if result.returncode == 0:
|
|
520
522
|
return dataset_name
|
|
521
523
|
else:
|
|
522
|
-
|
|
524
|
+
hud_console.error("Failed to create dataset")
|
|
523
525
|
if result.stderr:
|
|
524
|
-
|
|
526
|
+
hud_console.error(result.stderr)
|
|
525
527
|
return None
|
|
526
528
|
|
|
527
529
|
|
|
@@ -539,7 +541,7 @@ async def run_remote_training(
|
|
|
539
541
|
is_json_file: bool = False,
|
|
540
542
|
) -> None:
|
|
541
543
|
"""Run training on remote infrastructure."""
|
|
542
|
-
|
|
544
|
+
hud_console.section_title("🚀 Remote Training")
|
|
543
545
|
|
|
544
546
|
if provider == "prime":
|
|
545
547
|
await run_prime_training(
|
|
@@ -555,6 +557,6 @@ async def run_remote_training(
|
|
|
555
557
|
is_json_file,
|
|
556
558
|
)
|
|
557
559
|
else:
|
|
558
|
-
|
|
559
|
-
|
|
560
|
+
hud_console.error(f"Provider '{provider}' not yet supported")
|
|
561
|
+
hud_console.info("Currently supported: prime")
|
|
560
562
|
raise typer.Exit(1)
|
hud/cli/rl/utils.py
CHANGED
|
@@ -8,9 +8,9 @@ from typing import Any
|
|
|
8
8
|
|
|
9
9
|
import yaml
|
|
10
10
|
|
|
11
|
-
from hud.utils.
|
|
11
|
+
from hud.utils.hud_console import HUDConsole
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
hud_console = HUDConsole()
|
|
14
14
|
|
|
15
15
|
logger = logging.getLogger(__name__)
|
|
16
16
|
|
|
@@ -25,7 +25,7 @@ def read_lock_file() -> dict[str, Any]:
|
|
|
25
25
|
with open(lock_file) as f:
|
|
26
26
|
return yaml.safe_load(f) or {}
|
|
27
27
|
except Exception as e:
|
|
28
|
-
|
|
28
|
+
hud_console.warning(f"Could not read hud.lock.yaml: {e}")
|
|
29
29
|
return {}
|
|
30
30
|
|
|
31
31
|
|
|
@@ -38,7 +38,7 @@ def write_lock_file(data: dict[str, Any]) -> bool:
|
|
|
38
38
|
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
|
|
39
39
|
return True
|
|
40
40
|
except Exception as e:
|
|
41
|
-
|
|
41
|
+
hud_console.warning(f"Could not write hud.lock.yaml: {e}")
|
|
42
42
|
return False
|
|
43
43
|
|
|
44
44
|
|
|
@@ -109,26 +109,26 @@ def validate_dataset_name(name: str) -> bool:
|
|
|
109
109
|
return False
|
|
110
110
|
|
|
111
111
|
if "/" not in name:
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
hud_console.error(f"Invalid dataset name: {name}")
|
|
113
|
+
hud_console.info("Dataset name should be in format: org/dataset")
|
|
114
114
|
return False
|
|
115
115
|
|
|
116
116
|
parts = name.split("/")
|
|
117
117
|
if len(parts) != 2:
|
|
118
|
-
|
|
118
|
+
hud_console.error(f"Invalid dataset name: {name}")
|
|
119
119
|
return False
|
|
120
120
|
|
|
121
121
|
org, dataset = parts
|
|
122
122
|
if not org or not dataset:
|
|
123
|
-
|
|
123
|
+
hud_console.error(f"Invalid dataset name: {name}")
|
|
124
124
|
return False
|
|
125
125
|
|
|
126
126
|
# Check for valid characters (alphanumeric, dash, underscore)
|
|
127
127
|
import re
|
|
128
128
|
|
|
129
129
|
if not re.match(r"^[a-zA-Z0-9_-]+$", org) or not re.match(r"^[a-zA-Z0-9_-]+$", dataset):
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
hud_console.error(f"Invalid characters in dataset name: {name}")
|
|
131
|
+
hud_console.info("Use only letters, numbers, dashes, and underscores")
|
|
132
132
|
return False
|
|
133
133
|
|
|
134
134
|
return True
|
hud/cli/tests/test_analyze.py
CHANGED
|
@@ -251,7 +251,7 @@ class TestDisplayFunctions:
|
|
|
251
251
|
|
|
252
252
|
# Check console was called multiple times
|
|
253
253
|
assert mock_console.print.call_count > 0
|
|
254
|
-
# The
|
|
254
|
+
# The hud_console.section_title uses its own console, not the patched one
|
|
255
255
|
# Just verify the function ran without errors
|
|
256
256
|
|
|
257
257
|
def test_display_markdown_basic(self) -> None:
|
|
@@ -216,7 +216,7 @@ class TestAnalyzeFromMetadata:
|
|
|
216
216
|
@mock.patch("hud.cli.utils.metadata.fetch_lock_from_registry")
|
|
217
217
|
@mock.patch("hud.cli.utils.metadata.design")
|
|
218
218
|
@mock.patch("hud.cli.utils.metadata.console")
|
|
219
|
-
async def test_analyze_not_found(self, mock_console,
|
|
219
|
+
async def test_analyze_not_found(self, mock_console, mock_hud_console, mock_fetch, mock_check):
|
|
220
220
|
"""Test when environment not found anywhere."""
|
|
221
221
|
mock_check.return_value = None
|
|
222
222
|
mock_fetch.return_value = None
|
|
@@ -224,7 +224,7 @@ class TestAnalyzeFromMetadata:
|
|
|
224
224
|
await analyze_from_metadata("test/notfound:latest", "json", verbose=False)
|
|
225
225
|
|
|
226
226
|
# Should show error
|
|
227
|
-
|
|
227
|
+
mock_hud_console.error.assert_called_with("Environment metadata not found")
|
|
228
228
|
# Should print suggestions
|
|
229
229
|
mock_console.print.assert_called()
|
|
230
230
|
|
hud/cli/tests/test_pull.py
CHANGED
|
@@ -180,15 +180,15 @@ class TestFormatSize:
|
|
|
180
180
|
class TestPullEnvironment:
|
|
181
181
|
"""Test the main pull_environment function."""
|
|
182
182
|
|
|
183
|
-
@mock.patch("hud.cli.pull.
|
|
183
|
+
@mock.patch("hud.cli.pull.HUDConsole")
|
|
184
184
|
@mock.patch("hud.cli.pull.save_to_registry")
|
|
185
185
|
@mock.patch("subprocess.Popen")
|
|
186
|
-
def test_pull_with_lock_file(self, mock_popen, mock_save,
|
|
186
|
+
def test_pull_with_lock_file(self, mock_popen, mock_save, mock_hud_console_class, tmp_path):
|
|
187
187
|
"""Test pulling with a lock file."""
|
|
188
|
-
# Create mock
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
188
|
+
# Create mock hud_console instance
|
|
189
|
+
mock_hud_console = mock.Mock()
|
|
190
|
+
mock_hud_console.console = mock.Mock()
|
|
191
|
+
mock_hud_console_class.return_value = mock_hud_console
|
|
192
192
|
|
|
193
193
|
# Create lock file
|
|
194
194
|
lock_data = {
|
|
@@ -218,15 +218,15 @@ class TestPullEnvironment:
|
|
|
218
218
|
# Verify lock was saved to registry
|
|
219
219
|
mock_save.assert_called_once()
|
|
220
220
|
|
|
221
|
-
@mock.patch("hud.cli.pull.
|
|
221
|
+
@mock.patch("hud.cli.pull.HUDConsole")
|
|
222
222
|
@mock.patch("hud.cli.pull.fetch_lock_from_registry")
|
|
223
223
|
@mock.patch("subprocess.Popen")
|
|
224
|
-
def test_pull_from_registry(self, mock_popen, mock_fetch,
|
|
224
|
+
def test_pull_from_registry(self, mock_popen, mock_fetch, mock_hud_console_class):
|
|
225
225
|
"""Test pulling from HUD registry."""
|
|
226
|
-
# Create mock
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
226
|
+
# Create mock hud_console instance
|
|
227
|
+
mock_hud_console = mock.Mock()
|
|
228
|
+
mock_hud_console.console = mock.Mock()
|
|
229
|
+
mock_hud_console_class.return_value = mock_hud_console
|
|
230
230
|
|
|
231
231
|
# Mock registry response
|
|
232
232
|
lock_data = {"image": "docker.io/org/env:latest@sha256:def456", "tools": []}
|
|
@@ -250,18 +250,18 @@ class TestPullEnvironment:
|
|
|
250
250
|
call_args = mock_popen.call_args[0][0]
|
|
251
251
|
assert "docker.io/org/env:latest@sha256:def456" in call_args
|
|
252
252
|
|
|
253
|
-
@mock.patch("hud.cli.pull.
|
|
253
|
+
@mock.patch("hud.cli.pull.HUDConsole")
|
|
254
254
|
@mock.patch("hud.cli.pull.get_docker_manifest")
|
|
255
255
|
@mock.patch("hud.cli.pull.fetch_lock_from_registry")
|
|
256
256
|
@mock.patch("subprocess.Popen")
|
|
257
257
|
def test_pull_docker_image_direct(
|
|
258
|
-
self, mock_popen, mock_fetch, mock_manifest,
|
|
258
|
+
self, mock_popen, mock_fetch, mock_manifest, mock_hud_console_class
|
|
259
259
|
):
|
|
260
260
|
"""Test pulling Docker image directly."""
|
|
261
|
-
# Create mock
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
261
|
+
# Create mock hud_console instance
|
|
262
|
+
mock_hud_console = mock.Mock()
|
|
263
|
+
mock_hud_console.console = mock.Mock()
|
|
264
|
+
mock_hud_console_class.return_value = mock_hud_console
|
|
265
265
|
|
|
266
266
|
# Mock no registry data
|
|
267
267
|
mock_fetch.return_value = None
|
|
@@ -284,28 +284,28 @@ class TestPullEnvironment:
|
|
|
284
284
|
call_args = mock_popen.call_args[0][0]
|
|
285
285
|
assert call_args == ["docker", "pull", "ubuntu:latest"]
|
|
286
286
|
|
|
287
|
-
@mock.patch("hud.cli.pull.
|
|
288
|
-
def test_pull_verify_only(self,
|
|
287
|
+
@mock.patch("hud.cli.pull.HUDConsole")
|
|
288
|
+
def test_pull_verify_only(self, mock_hud_console_class):
|
|
289
289
|
"""Test verify-only mode."""
|
|
290
|
-
# Create mock
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
290
|
+
# Create mock hud_console instance
|
|
291
|
+
mock_hud_console = mock.Mock()
|
|
292
|
+
mock_hud_console.console = mock.Mock()
|
|
293
|
+
mock_hud_console_class.return_value = mock_hud_console
|
|
294
294
|
|
|
295
295
|
# Should not actually pull
|
|
296
296
|
pull_environment("test:latest", verify_only=True)
|
|
297
297
|
|
|
298
298
|
# Check success message
|
|
299
|
-
|
|
299
|
+
mock_hud_console.success.assert_called_with("Verification complete")
|
|
300
300
|
|
|
301
|
-
@mock.patch("hud.cli.pull.
|
|
301
|
+
@mock.patch("hud.cli.pull.HUDConsole")
|
|
302
302
|
@mock.patch("subprocess.Popen")
|
|
303
|
-
def test_pull_docker_failure(self, mock_popen,
|
|
303
|
+
def test_pull_docker_failure(self, mock_popen, mock_hud_console_class):
|
|
304
304
|
"""Test handling Docker pull failure."""
|
|
305
|
-
# Create mock
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
305
|
+
# Create mock hud_console instance
|
|
306
|
+
mock_hud_console = mock.Mock()
|
|
307
|
+
mock_hud_console.console = mock.Mock()
|
|
308
|
+
mock_hud_console_class.return_value = mock_hud_console
|
|
309
309
|
|
|
310
310
|
# Mock docker pull failure
|
|
311
311
|
mock_process = mock.Mock()
|
|
@@ -318,16 +318,16 @@ class TestPullEnvironment:
|
|
|
318
318
|
with pytest.raises(typer.Exit):
|
|
319
319
|
pull_environment("invalid:image", yes=True)
|
|
320
320
|
|
|
321
|
-
|
|
321
|
+
mock_hud_console.error.assert_called_with("Pull failed")
|
|
322
322
|
|
|
323
|
-
@mock.patch("hud.cli.pull.
|
|
323
|
+
@mock.patch("hud.cli.pull.HUDConsole")
|
|
324
324
|
@mock.patch("typer.confirm")
|
|
325
|
-
def test_pull_user_cancels(self, mock_confirm,
|
|
325
|
+
def test_pull_user_cancels(self, mock_confirm, mock_hud_console_class):
|
|
326
326
|
"""Test when user cancels pull."""
|
|
327
|
-
# Create mock
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
327
|
+
# Create mock hud_console instance
|
|
328
|
+
mock_hud_console = mock.Mock()
|
|
329
|
+
mock_hud_console.console = mock.Mock()
|
|
330
|
+
mock_hud_console_class.return_value = mock_hud_console
|
|
331
331
|
|
|
332
332
|
mock_confirm.return_value = False
|
|
333
333
|
|
|
@@ -335,15 +335,15 @@ class TestPullEnvironment:
|
|
|
335
335
|
pull_environment("test:latest", yes=False)
|
|
336
336
|
|
|
337
337
|
assert exc_info.value.exit_code == 0
|
|
338
|
-
|
|
338
|
+
mock_hud_console.info.assert_called_with("Aborted")
|
|
339
339
|
|
|
340
|
-
@mock.patch("hud.cli.pull.
|
|
341
|
-
def test_pull_nonexistent_lock_file(self,
|
|
340
|
+
@mock.patch("hud.cli.pull.HUDConsole")
|
|
341
|
+
def test_pull_nonexistent_lock_file(self, mock_hud_console_class):
|
|
342
342
|
"""Test pulling with non-existent lock file."""
|
|
343
|
-
# Create mock
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
343
|
+
# Create mock hud_console instance
|
|
344
|
+
mock_hud_console = mock.Mock()
|
|
345
|
+
mock_hud_console.console = mock.Mock()
|
|
346
|
+
mock_hud_console_class.return_value = mock_hud_console
|
|
347
347
|
|
|
348
348
|
with pytest.raises(typer.Exit):
|
|
349
349
|
pull_environment("nonexistent.yaml")
|