hud-python 0.4.2__py3-none-any.whl → 0.4.4__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/cli/pull.py CHANGED
@@ -9,14 +9,11 @@ import click
9
9
  import requests
10
10
  import typer
11
11
  import yaml
12
- from rich.console import Console
13
12
  from rich.table import Table
14
13
 
15
14
  from hud.settings import settings
16
15
  from hud.utils.design import HUDDesign
17
16
 
18
- console = Console()
19
-
20
17
 
21
18
  def get_docker_manifest(image: str) -> dict | None:
22
19
  """Get manifest from Docker registry without pulling the image."""
@@ -32,8 +29,7 @@ def get_docker_manifest(image: str) -> dict | None:
32
29
 
33
30
  return json.loads(result.stdout)
34
31
  except Exception:
35
- click.echo("Failed to get Docker manifest", err=True)
36
- return None
32
+ return None
37
33
 
38
34
 
39
35
  def get_image_size_from_manifest(manifest: dict) -> int | None:
@@ -52,7 +48,6 @@ def get_image_size_from_manifest(manifest: dict) -> int | None:
52
48
 
53
49
  return total_size if total_size > 0 else None
54
50
  except Exception:
55
- click.echo("Failed to get image size from manifest", err=True)
56
51
  return None
57
52
 
58
53
 
@@ -200,10 +195,8 @@ def pull_environment(
200
195
  # Minimal data from Docker manifest
201
196
  table.add_row("Source", "Docker Registry")
202
197
  if not yes:
203
- console.print(
204
- "\n[yellow]Note:[/yellow] Limited metadata available from Docker registry."
205
- )
206
- console.print("For full environment details, use a lock file.\n")
198
+ design.warning("Note: Limited metadata available from Docker registry.")
199
+ design.info("For full environment details, use a lock file.\n")
207
200
  else:
208
201
  # Full lock file data
209
202
  if "build" in lock_data:
@@ -233,18 +226,19 @@ def pull_environment(
233
226
  # No metadata available
234
227
  table.add_row("Source", "Unknown")
235
228
 
236
- console.print(table)
229
+ # Use design's console to maintain consistent output
230
+ design.console.print(table)
237
231
 
238
232
  # Tool summary (show after table)
239
233
  if lock_data and "tools" in lock_data:
240
- console.print("\n[bold]Available Tools:[/bold]")
234
+ design.section_title("Available Tools")
241
235
  for tool in lock_data["tools"]:
242
- console.print(f" • {tool['name']}: {tool['description']}")
236
+ design.info(f"• {tool['name']}: {tool['description']}")
243
237
 
244
238
  # Show warnings if no metadata
245
239
  if not lock_data and not yes:
246
- console.print("\n[yellow]Warning:[/yellow] No metadata available for this image.")
247
- console.print("The image will be pulled without verification.")
240
+ design.warning("No metadata available for this image.")
241
+ design.info("The image will be pulled without verification.")
248
242
 
249
243
  # If verify only, stop here
250
244
  if verify_only:
@@ -253,7 +247,7 @@ def pull_environment(
253
247
 
254
248
  # Ask for confirmation unless --yes
255
249
  if not yes:
256
- console.print()
250
+ design.info("")
257
251
  # Show simple name for confirmation, not the full digest
258
252
  if ":" in image_ref and "@" in image_ref:
259
253
  simple_name = image_ref.split("@")[0]
@@ -278,7 +272,7 @@ def pull_environment(
278
272
 
279
273
  for line in process.stdout or []:
280
274
  if verbose or "Downloading" in line or "Extracting" in line or "Pull complete" in line:
281
- click.echo(line.rstrip(), err=True)
275
+ design.info(line.rstrip())
282
276
 
283
277
  process.wait()
284
278
 
@@ -311,14 +305,14 @@ def pull_environment(
311
305
  # Extract simple name for examples
312
306
  simple_ref = image_ref.split("@")[0] if ":" in image_ref and "@" in image_ref else image_ref
313
307
 
314
- console.print("1. Quick analysis (from metadata):")
315
- console.print(f" [cyan]hud analyze {simple_ref}[/cyan]\n")
316
-
317
- console.print("2. Live analysis (runs container):")
318
- console.print(f" [cyan]hud analyze {simple_ref} --live[/cyan]\n")
319
-
320
- console.print("3. Run the environment:")
321
- console.print(f" [cyan]hud run {simple_ref}[/cyan]")
308
+ design.info("1. Quick analysis (from metadata):")
309
+ design.command_example(f"hud analyze {simple_ref}")
310
+ design.info("")
311
+ design.info("2. Live analysis (runs container):")
312
+ design.command_example(f"hud analyze {simple_ref} --live")
313
+ design.info("")
314
+ design.info("3. Run the environment:")
315
+ design.command_example(f"hud run {simple_ref}")
322
316
 
323
317
 
324
318
  def pull_command(
hud/cli/push.py CHANGED
@@ -6,17 +6,13 @@ import json
6
6
  import subprocess
7
7
  from pathlib import Path
8
8
 
9
- import click
10
9
  import requests
11
10
  import typer
12
11
  import yaml
13
- from rich.console import Console
14
12
 
15
13
  from hud.settings import settings
16
14
  from hud.utils.design import HUDDesign
17
15
 
18
- console = Console()
19
-
20
16
 
21
17
  def get_docker_username() -> str | None:
22
18
  """Get the current Docker username if logged in."""
@@ -51,9 +47,9 @@ def get_docker_username() -> str | None:
51
47
  if username and username != "token": # Skip token-based auth
52
48
  return username
53
49
  except Exception:
54
- click.echo("Failed to decode auth info", err=True)
50
+ pass # Silent failure, try other methods
55
51
  except Exception:
56
- click.echo("Failed to get Docker username", err=True)
52
+ pass # Silent failure, try other methods
57
53
 
58
54
  # Alternative: Check credsStore/credHelpers
59
55
  for config_path in config_paths:
@@ -89,11 +85,11 @@ def get_docker_username() -> str | None:
89
85
  if username and username != "token":
90
86
  return username
91
87
  except Exception:
92
- click.echo("Failed to get Docker username", err=True)
88
+ pass # Silent failure, try other methods
93
89
  except Exception:
94
- click.echo("Failed to get Docker username", err=True)
90
+ pass # Silent failure, try other methods
95
91
  except Exception:
96
- click.echo("Failed to get Docker username", err=True)
92
+ pass # Silent failure, try other methods
97
93
  return None
98
94
 
99
95
 
@@ -108,7 +104,6 @@ def get_docker_image_labels(image: str) -> dict:
108
104
  )
109
105
  return json.loads(result.stdout.strip()) or {}
110
106
  except Exception:
111
- click.echo("Failed to get Docker image labels", err=True)
112
107
  return {}
113
108
 
114
109
 
@@ -136,11 +131,12 @@ def push_environment(
136
131
  # Check for API key first
137
132
  if not settings.api_key:
138
133
  design.error("No HUD API key found")
139
- console.print("\n[yellow]A HUD API key is required to push environments.[/yellow]")
140
- console.print("\nTo get started:")
141
- console.print(" 1. Get your API key at: [link]https://hud.so/settings[/link]")
142
- console.print(" 2. Set it: [cyan]export HUD_API_KEY=your-key-here[/cyan]")
143
- console.print(" 3. Try again: [cyan]hud push[/cyan]\n")
134
+ design.warning("A HUD API key is required to push environments.")
135
+ design.info("\nTo get started:")
136
+ design.info("1. Get your API key at: https://hud.so/settings")
137
+ design.command_example("export HUD_API_KEY=your-key-here", "Set your API key")
138
+ design.command_example("hud push", "Try again")
139
+ design.info("")
144
140
  raise typer.Exit(1)
145
141
 
146
142
  # Load lock file
@@ -152,6 +148,9 @@ def push_environment(
152
148
  if not local_image and "build" in lock_data:
153
149
  # New format might have image elsewhere
154
150
  local_image = lock_data.get("image", "")
151
+
152
+ # Get internal version from lock file
153
+ internal_version = lock_data.get("build", {}).get("version", None)
155
154
 
156
155
  # If no image specified, try to be smart
157
156
  if not image:
@@ -172,14 +171,20 @@ def push_environment(
172
171
  if "/" in base_name:
173
172
  base_name = base_name.split("/")[-1]
174
173
 
175
- # Use provided tag or default
176
- final_tag = tag if tag else current_tag
174
+ # Use provided tag, or internal version, or current tag as fallback
175
+ if tag:
176
+ final_tag = tag
177
+ design.info(f"Using specified tag: {tag}")
178
+ elif internal_version:
179
+ final_tag = internal_version
180
+ design.info(f"Using internal version from lock file: {internal_version}")
181
+ else:
182
+ final_tag = current_tag
183
+ design.info(f"Using current tag: {current_tag}")
177
184
 
178
185
  # Suggest a registry image
179
186
  image = f"{username}/{base_name}:{final_tag}"
180
187
  design.info(f"Auto-detected Docker username: {username}")
181
- if tag:
182
- design.info(f"Using specified tag: {tag}")
183
188
  design.info(f"Will push to: {image}")
184
189
 
185
190
  if not yes and not typer.confirm(f"\nPush to {image}?"):
@@ -190,44 +195,77 @@ def push_environment(
190
195
  "Not logged in to Docker Hub. Please specify --image or run 'docker login'"
191
196
  )
192
197
  raise typer.Exit(1)
193
- elif tag:
198
+ elif tag or internal_version:
194
199
  # Handle tag when image is provided
200
+ # Prefer explicit tag over internal version
201
+ final_tag = tag if tag else internal_version
202
+
195
203
  if ":" in image:
196
204
  # Image already has a tag
197
205
  existing_tag = image.split(":")[-1]
198
- if existing_tag != tag:
199
- design.warning(f"Image already has tag '{existing_tag}', overriding with '{tag}'")
200
- image = image.rsplit(":", 1)[0] + f":{tag}"
206
+ if existing_tag != final_tag:
207
+ if tag:
208
+ design.warning(f"Image already has tag '{existing_tag}', overriding with '{final_tag}'")
209
+ else:
210
+ design.info(f"Image has tag '{existing_tag}', but using internal version '{final_tag}'")
211
+ image = image.rsplit(":", 1)[0] + f":{final_tag}"
201
212
  # else: tags match, no action needed
202
213
  else:
203
- # Image has no tag, append the specified one
204
- image = f"{image}:{tag}"
205
- design.info(f"Using specified tag: {tag}")
214
+ # Image has no tag, append the appropriate one
215
+ image = f"{image}:{final_tag}"
216
+
217
+ if tag:
218
+ design.info(f"Using specified tag: {tag}")
219
+ else:
220
+ design.info(f"Using internal version from lock file: {internal_version}")
206
221
  design.info(f"Will push to: {image}")
207
222
 
208
223
  # Verify local image exists
209
224
  # Extract the tag part (before @sha256:...) for Docker operations
210
225
  local_tag = local_image.split("@")[0] if "@" in local_image else local_image
211
-
212
- # Verify the image exists locally
213
- try:
214
- subprocess.run(["docker", "inspect", local_tag], capture_output=True, check=True) # noqa: S603, S607
215
- except subprocess.CalledProcessError:
216
- design.error(f"Local image not found: {local_tag}")
217
- design.info("Run 'hud build' first to create the image")
218
- raise typer.Exit(1) # noqa: B904
226
+
227
+ # Also check for version-tagged image if we have internal version
228
+ version_tag = None
229
+ if internal_version and ":" in local_tag:
230
+ base_name = local_tag.split(":")[0]
231
+ version_tag = f"{base_name}:{internal_version}"
232
+
233
+ # Try to find the image - prefer version tag if it exists
234
+ image_to_push = None
235
+ if version_tag:
236
+ try:
237
+ subprocess.run(["docker", "inspect", version_tag], capture_output=True, check=True) # noqa: S603, S607
238
+ image_to_push = version_tag
239
+ design.info(f"Found version-tagged image: {version_tag}")
240
+ except subprocess.CalledProcessError:
241
+ pass
242
+
243
+ if not image_to_push:
244
+ try:
245
+ subprocess.run(["docker", "inspect", local_tag], capture_output=True, check=True) # noqa: S603, S607
246
+ image_to_push = local_tag
247
+ except subprocess.CalledProcessError:
248
+ design.error(f"Local image not found: {local_tag}")
249
+ if version_tag:
250
+ design.error(f"Also tried: {version_tag}")
251
+ design.info("Run 'hud build' first to create the image")
252
+ raise typer.Exit(1) # noqa: B904
219
253
 
220
254
  # Check if local image has the expected label
221
- labels = get_docker_image_labels(local_tag)
255
+ labels = get_docker_image_labels(image_to_push)
222
256
  expected_label = labels.get("org.hud.manifest.head", "")
257
+ version_label = labels.get("org.hud.version", "")
223
258
 
224
259
  # Skip hash verification - the lock file may have been updated with digest after build
225
- if verbose and expected_label:
226
- design.info(f"Image label: {expected_label[:12]}...")
260
+ if verbose:
261
+ if expected_label:
262
+ design.info(f"Image label: {expected_label[:12]}...")
263
+ if version_label:
264
+ design.info(f"Version label: {version_label}")
227
265
 
228
266
  # Tag the image for push
229
- design.progress_message(f"Tagging {local_tag} as {image}")
230
- subprocess.run(["docker", "tag", local_tag, image], check=True) # noqa: S603, S607
267
+ design.progress_message(f"Tagging {image_to_push} as {image}")
268
+ subprocess.run(["docker", "tag", image_to_push, image], check=True) # noqa: S603, S607
231
269
 
232
270
  # Push the image
233
271
  design.progress_message(f"Pushing {image} to registry...")
@@ -243,7 +281,7 @@ def push_environment(
243
281
  )
244
282
 
245
283
  for line in process.stdout or []:
246
- click.echo(line.rstrip(), err=True)
284
+ design.info(line.rstrip())
247
285
 
248
286
  process.wait()
249
287
 
@@ -267,8 +305,8 @@ def push_environment(
267
305
  design.success("Push complete!")
268
306
 
269
307
  # Show the final image reference
270
- console.print("\n[bold green]✓ Pushed image:[/bold green]")
271
- console.print(f" [bold cyan]{pushed_digest}[/bold cyan]\n")
308
+ design.section_title("Pushed Image")
309
+ design.status_item("Registry", pushed_digest, primary=True)
272
310
 
273
311
  # Update the lock file with registry information
274
312
  lock_data["image"] = pushed_digest
@@ -286,7 +324,7 @@ def push_environment(
286
324
  with open(lock_path, "w") as f:
287
325
  yaml.dump(lock_data, f, default_flow_style=False, sort_keys=False)
288
326
 
289
- console.print("[green]✓[/green] Updated lock file with registry image")
327
+ design.success("Updated lock file with registry image")
290
328
 
291
329
  # Upload lock file to HUD registry
292
330
  try:
@@ -324,34 +362,30 @@ def push_environment(
324
362
 
325
363
  if response.status_code in [200, 201]:
326
364
  design.success("Metadata uploaded to HUD registry")
327
- console.print(
328
- f" Others can now pull with: [cyan]hud pull {name_with_tag}[/cyan]\n"
329
- )
365
+ design.info("Others can now pull with:")
366
+ design.command_example(f"hud pull {name_with_tag}")
367
+ design.info("")
330
368
  else:
331
369
  design.warning(f"Could not upload to registry: {response.status_code}")
332
370
  if verbose:
333
371
  design.info(f"Response: {response.text}")
334
- console.print(" Share [cyan]hud.lock.yaml[/cyan] manually\n")
372
+ design.info("Share hud.lock.yaml manually\n")
335
373
  else:
336
374
  if verbose:
337
375
  design.info("Could not parse registry path for upload")
338
- console.print(
339
- " Share [cyan]hud.lock.yaml[/cyan] to let others reproduce your exact environment\n" # noqa: E501
340
- )
376
+ design.info("Share hud.lock.yaml to let others reproduce your exact environment\n")
341
377
  except Exception as e:
342
378
  design.warning(f"Registry upload failed: {e}")
343
- console.print(" Share [cyan]hud.lock.yaml[/cyan] manually\n")
379
+ design.info("Share hud.lock.yaml manually\n")
344
380
 
345
381
  # Show usage examples
346
382
  design.section_title("What's Next?")
347
383
 
348
- console.print("Test locally:")
349
- console.print(f" [cyan]hud run {image}[/cyan]\n")
350
-
351
- console.print("Share environment:")
352
- console.print(
353
- " Share the updated [cyan]hud.lock.yaml[/cyan] for others to reproduce your exact environment" # noqa: E501
354
- )
384
+ design.info("Test locally:")
385
+ design.command_example(f"hud run {image}")
386
+ design.info("")
387
+ design.info("Share environment:")
388
+ design.info(" Share the updated hud.lock.yaml for others to reproduce your exact environment")
355
389
 
356
390
  # TODO: Upload lock file to HUD registry
357
391
  if sign:
hud/clients/base.py CHANGED
@@ -149,10 +149,9 @@ class BaseHUDClient(AgentMCPClient):
149
149
  "Please ensure your HUD_API_KEY environment variable is set correctly. "
150
150
  "You can get an API key at https://app.hud.so"
151
151
  ) from e
152
- # Generic 401 error
153
152
  raise RuntimeError(
154
- f"Authentication failed (401 Unauthorized). "
155
- f"Please check your credentials or API key."
153
+ "Authentication failed (401 Unauthorized). "
154
+ "Please check your credentials or API key."
156
155
  ) from e
157
156
  raise
158
157
 
hud/clients/fastmcp.py CHANGED
@@ -73,7 +73,7 @@ class FastMCPHUDClient(BaseHUDClient):
73
73
  return
74
74
 
75
75
  # Create FastMCP client with the custom transport
76
- timeout = 5 * 60 # 5 minutes
76
+ timeout = 10 * 60 # 5 minutes
77
77
  os.environ["FASTMCP_CLIENT_INIT_TIMEOUT"] = str(timeout)
78
78
 
79
79
  # Create custom transport with retry support for HTTP servers
@@ -82,7 +82,27 @@ class FastMCPHUDClient(BaseHUDClient):
82
82
 
83
83
  if self._stack is None:
84
84
  self._stack = AsyncExitStack()
85
- await self._stack.enter_async_context(self._client)
85
+ try:
86
+ await self._stack.enter_async_context(self._client)
87
+ except Exception as e:
88
+ # Check for authentication errors
89
+ error_msg = str(e)
90
+ if "401" in error_msg or "Unauthorized" in error_msg:
91
+ # Check if connecting to HUD API
92
+ for server_config in mcp_config.values():
93
+ url = server_config.get("url", "")
94
+ if "mcp.hud.so" in url:
95
+ raise RuntimeError(
96
+ "Authentication failed for HUD API. "
97
+ "Please ensure your HUD_API_KEY environment variable is set correctly. "
98
+ "You can get an API key at https://app.hud.so"
99
+ ) from e
100
+ # Generic 401 error
101
+ raise RuntimeError(
102
+ f"Authentication failed (401 Unauthorized). "
103
+ f"Please check your credentials or API key."
104
+ ) from e
105
+ raise
86
106
 
87
107
  # Configure validation for output schemas based on client setting
88
108
  from mcp.client.session import ValidationOptions
hud/clients/mcp_use.py CHANGED
@@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Any
7
7
 
8
8
  from mcp import Implementation
9
9
  from mcp.shared.exceptions import McpError
10
- from mcp_use.client import MCPClient as MCPUseClient
11
10
  from pydantic import AnyUrl
12
11
 
13
12
  from hud.types import MCPToolCall, MCPToolResult
@@ -17,8 +16,16 @@ from .base import BaseHUDClient
17
16
 
18
17
  if TYPE_CHECKING:
19
18
  from mcp import types
19
+ from mcp_use.client import MCPClient as MCPUseClient
20
20
  from mcp_use.session import MCPSession as MCPUseSession
21
21
 
22
+ try:
23
+ from mcp_use.client import MCPClient as MCPUseClient
24
+ from mcp_use.session import MCPSession as MCPUseSession
25
+ except ImportError:
26
+ MCPUseClient = None # type: ignore[misc, assignment]
27
+ MCPUseSession = None # type: ignore[misc, assignment]
28
+
22
29
  logger = logging.getLogger(__name__)
23
30
 
24
31
 
@@ -39,9 +46,15 @@ class MCPUseHUDClient(BaseHUDClient):
39
46
  """
40
47
  super().__init__(mcp_config=mcp_config, **kwargs)
41
48
 
42
- self._sessions: dict[str, MCPUseSession] = {}
49
+ if MCPUseClient is None or MCPUseSession is None:
50
+ raise ImportError(
51
+ "MCP-use dependencies are not available. "
52
+ "Please install the optional agent dependencies: pip install 'hud-python[agent]'"
53
+ )
54
+
55
+ self._sessions: dict[str, Any] = {} # Will be MCPUseSession when available
43
56
  self._tool_map: dict[str, tuple[str, types.Tool]] = {}
44
- self._client: MCPUseClient | None = None
57
+ self._client: Any | None = None # Will be MCPUseClient when available
45
58
 
46
59
  async def _connect(self, mcp_config: dict[str, dict[str, Any]]) -> None:
47
60
  """Create all sessions for MCP-use client."""
@@ -50,23 +63,30 @@ class MCPUseHUDClient(BaseHUDClient):
50
63
  return
51
64
 
52
65
  config = {"mcpServers": mcp_config}
66
+ if MCPUseClient is None:
67
+ raise ImportError("MCPUseClient is not available")
53
68
  self._client = MCPUseClient.from_dict(config)
54
69
  try:
70
+ assert self._client is not None # For type checker
55
71
  self._sessions = await self._client.create_all_sessions()
56
72
  logger.info("Created %d MCP sessions", len(self._sessions))
57
73
 
58
74
  # Configure validation for all sessions based on client setting
59
- from mcp.client.session import ValidationOptions
60
-
61
- for session in self._sessions.values():
62
- if (
63
- hasattr(session, "connector")
64
- and hasattr(session.connector, "client_session")
65
- and session.connector.client_session is not None
66
- ):
67
- session.connector.client_session._validation_options = ValidationOptions(
68
- strict_output_validation=self._strict_validation
69
- )
75
+ try:
76
+ from mcp.client.session import ValidationOptions # type: ignore[import-not-found]
77
+
78
+ for session in self._sessions.values():
79
+ if (
80
+ hasattr(session, "connector")
81
+ and hasattr(session.connector, "client_session")
82
+ and session.connector.client_session is not None
83
+ ):
84
+ session.connector.client_session._validation_options = ValidationOptions(
85
+ strict_output_validation=self._strict_validation
86
+ )
87
+ except ImportError:
88
+ # ValidationOptions may not be available in some mcp versions
89
+ pass
70
90
 
71
91
  # Log session details in verbose mode
72
92
  if self.verbose and self._sessions:
@@ -273,6 +293,6 @@ class MCPUseHUDClient(BaseHUDClient):
273
293
  logger.debug("MCP-use client disconnected")
274
294
 
275
295
  # Legacy compatibility methods (limited; tests should not rely on these)
276
- def get_sessions(self) -> dict[str, MCPUseSession]:
296
+ def get_sessions(self) -> dict[str, Any]:
277
297
  """Get active MCP sessions."""
278
298
  return self._sessions
hud/datasets.py CHANGED
@@ -12,6 +12,7 @@ from datasets import Dataset, load_dataset
12
12
  from pydantic import BaseModel, Field, field_validator
13
13
 
14
14
  from hud.agents.misc import ResponseAgent
15
+ from hud.settings import settings
15
16
 
16
17
  from .types import MCPToolCall
17
18
 
@@ -94,6 +95,10 @@ class Task(BaseModel):
94
95
 
95
96
  # Start with current environment variables
96
97
  mapping = dict(os.environ)
98
+ mapping.update(settings.model_dump())
99
+
100
+ if settings.api_key:
101
+ mapping["HUD_API_KEY"] = settings.api_key
97
102
 
98
103
  def substitute_in_value(obj: Any) -> Any:
99
104
  """Recursively substitute variables in nested structures."""