hud-python 0.4.51__py3-none-any.whl → 0.4.53__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hud-python might be problematic. Click here for more details.

Files changed (88) hide show
  1. hud/__init__.py +13 -1
  2. hud/agents/base.py +14 -3
  3. hud/agents/lite_llm.py +1 -1
  4. hud/agents/openai_chat_generic.py +15 -3
  5. hud/agents/tests/test_base.py +9 -2
  6. hud/agents/tests/test_base_runtime.py +164 -0
  7. hud/cli/__init__.py +18 -25
  8. hud/cli/build.py +35 -27
  9. hud/cli/dev.py +11 -29
  10. hud/cli/eval.py +114 -145
  11. hud/cli/tests/test_analyze_module.py +120 -0
  12. hud/cli/tests/test_build.py +26 -3
  13. hud/cli/tests/test_build_failure.py +41 -0
  14. hud/cli/tests/test_build_module.py +50 -0
  15. hud/cli/tests/test_cli_more_wrappers.py +30 -0
  16. hud/cli/tests/test_cli_root.py +134 -0
  17. hud/cli/tests/test_eval.py +4 -0
  18. hud/cli/tests/test_mcp_server.py +8 -7
  19. hud/cli/tests/test_push_happy.py +74 -0
  20. hud/cli/tests/test_push_wrapper.py +23 -0
  21. hud/cli/utils/docker.py +120 -1
  22. hud/cli/utils/runner.py +1 -1
  23. hud/cli/utils/tasks.py +4 -1
  24. hud/cli/utils/tests/__init__.py +0 -0
  25. hud/cli/utils/tests/test_config.py +58 -0
  26. hud/cli/utils/tests/test_docker.py +93 -0
  27. hud/cli/utils/tests/test_docker_hints.py +71 -0
  28. hud/cli/utils/tests/test_env_check.py +74 -0
  29. hud/cli/utils/tests/test_environment.py +42 -0
  30. hud/cli/utils/tests/test_interactive_module.py +60 -0
  31. hud/cli/utils/tests/test_local_runner.py +50 -0
  32. hud/cli/utils/tests/test_logging_utils.py +23 -0
  33. hud/cli/utils/tests/test_metadata.py +49 -0
  34. hud/cli/utils/tests/test_package_runner.py +35 -0
  35. hud/cli/utils/tests/test_registry_utils.py +49 -0
  36. hud/cli/utils/tests/test_remote_runner.py +25 -0
  37. hud/cli/utils/tests/test_runner_modules.py +52 -0
  38. hud/cli/utils/tests/test_source_hash.py +36 -0
  39. hud/cli/utils/tests/test_tasks.py +80 -0
  40. hud/cli/utils/version_check.py +257 -0
  41. hud/clients/base.py +1 -1
  42. hud/clients/mcp_use.py +3 -1
  43. hud/datasets/parallel.py +2 -2
  44. hud/datasets/runner.py +85 -24
  45. hud/datasets/tests/__init__.py +0 -0
  46. hud/datasets/tests/test_runner.py +106 -0
  47. hud/datasets/tests/test_utils.py +228 -0
  48. hud/otel/config.py +8 -6
  49. hud/otel/context.py +4 -4
  50. hud/otel/exporters.py +231 -57
  51. hud/otel/tests/__init__.py +0 -1
  52. hud/otel/tests/test_instrumentation.py +207 -0
  53. hud/rl/learner.py +1 -1
  54. hud/server/tests/test_server_extra.py +2 -0
  55. hud/shared/exceptions.py +35 -9
  56. hud/shared/hints.py +25 -0
  57. hud/shared/requests.py +15 -3
  58. hud/shared/tests/test_exceptions.py +39 -30
  59. hud/shared/tests/test_hints.py +167 -0
  60. hud/telemetry/__init__.py +30 -6
  61. hud/telemetry/async_context.py +331 -0
  62. hud/telemetry/job.py +51 -12
  63. hud/telemetry/tests/test_async_context.py +242 -0
  64. hud/telemetry/tests/test_instrument.py +414 -0
  65. hud/telemetry/tests/test_job.py +609 -0
  66. hud/telemetry/tests/test_trace.py +184 -6
  67. hud/telemetry/trace.py +16 -17
  68. hud/tools/computer/qwen.py +4 -1
  69. hud/tools/computer/settings.py +2 -2
  70. hud/tools/executors/base.py +4 -2
  71. hud/tools/tests/test_submit.py +85 -0
  72. hud/tools/tests/test_types.py +193 -0
  73. hud/types.py +7 -1
  74. hud/utils/agent_factories.py +1 -3
  75. hud/utils/mcp.py +1 -1
  76. hud/utils/task_tracking.py +223 -0
  77. hud/utils/tests/test_agent_factories.py +60 -0
  78. hud/utils/tests/test_mcp.py +4 -6
  79. hud/utils/tests/test_pretty_errors.py +186 -0
  80. hud/utils/tests/test_tasks.py +187 -0
  81. hud/utils/tests/test_tool_shorthand.py +154 -0
  82. hud/utils/tests/test_version.py +1 -1
  83. hud/version.py +1 -1
  84. {hud_python-0.4.51.dist-info → hud_python-0.4.53.dist-info}/METADATA +48 -48
  85. {hud_python-0.4.51.dist-info → hud_python-0.4.53.dist-info}/RECORD +88 -47
  86. {hud_python-0.4.51.dist-info → hud_python-0.4.53.dist-info}/WHEEL +0 -0
  87. {hud_python-0.4.51.dist-info → hud_python-0.4.53.dist-info}/entry_points.txt +0 -0
  88. {hud_python-0.4.51.dist-info → hud_python-0.4.53.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,257 @@
1
+ """Version checking utilities for HUD CLI.
2
+
3
+ This module handles checking for updates to the hud-python package
4
+ and prompting users to upgrade when a new version is available.
5
+
6
+ Features:
7
+ - Checks PyPI for the latest version of hud-python
8
+ - Caches results for 6 hours to avoid excessive API calls
9
+ - Displays a friendly prompt when an update is available
10
+ - Can be disabled with HUD_SKIP_VERSION_CHECK=1 environment variable
11
+
12
+ The version check runs automatically at the start of most CLI commands,
13
+ but is skipped for help and version commands to keep them fast.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import contextlib
19
+ import json
20
+ import os
21
+ import time
22
+ from pathlib import Path
23
+ from typing import NamedTuple
24
+
25
+ import httpx
26
+ from packaging import version
27
+
28
+ from hud.utils.hud_console import HUDConsole
29
+
30
+ # Cache location for version check data
31
+ CACHE_DIR = Path.home() / ".hud" / ".cache"
32
+ VERSION_CACHE_FILE = CACHE_DIR / "version_check.json"
33
+
34
+ # Cache duration in seconds (6 hours)
35
+ CACHE_DURATION = 6 * 60 * 60
36
+
37
+ # PyPI API URL for package info
38
+ PYPI_URL = "https://pypi.org/pypi/hud-python/json"
39
+
40
+
41
+ class VersionInfo(NamedTuple):
42
+ """Version information from PyPI."""
43
+
44
+ latest: str
45
+ current: str
46
+ is_outdated: bool
47
+ checked_at: float
48
+
49
+
50
+ def _get_current_version() -> str:
51
+ """Get the currently installed version of hud-python."""
52
+ try:
53
+ from hud import __version__
54
+
55
+ return __version__
56
+ except ImportError:
57
+ return "unknown"
58
+
59
+
60
+ def _fetch_latest_version() -> str | None:
61
+ """Fetch the latest version from PyPI.
62
+
63
+ Returns:
64
+ The latest version string, or None if the request fails.
65
+ """
66
+ try:
67
+ with httpx.Client(timeout=3.0) as client:
68
+ response = client.get(PYPI_URL)
69
+ if response.status_code == 200:
70
+ data = response.json()
71
+ return data["info"]["version"]
72
+ except Exception: # noqa: S110
73
+ # Silently fail - we don't want to disrupt the user's workflow
74
+ # if PyPI is down or there's a network issue
75
+ pass
76
+ return None
77
+
78
+
79
+ def _load_cache() -> VersionInfo | None:
80
+ """Load cached version information.
81
+
82
+ Returns:
83
+ Cached VersionInfo if valid, None otherwise.
84
+ """
85
+ if not VERSION_CACHE_FILE.exists():
86
+ return None
87
+
88
+ try:
89
+ with open(VERSION_CACHE_FILE) as f:
90
+ data = json.load(f)
91
+
92
+ # Check if cache is still valid
93
+ if time.time() - data["checked_at"] > CACHE_DURATION:
94
+ return None
95
+
96
+ return VersionInfo(
97
+ latest=data["latest"],
98
+ current=data["current"],
99
+ is_outdated=data["is_outdated"],
100
+ checked_at=data["checked_at"],
101
+ )
102
+ except Exception:
103
+ # If cache is corrupted, return None
104
+ return None
105
+
106
+
107
+ def _save_cache(info: VersionInfo) -> None:
108
+ """Save version information to cache.
109
+
110
+ Args:
111
+ info: Version information to cache.
112
+ """
113
+ try:
114
+ # Create cache directory if it doesn't exist
115
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
116
+
117
+ with open(VERSION_CACHE_FILE, "w") as f:
118
+ json.dump(
119
+ {
120
+ "latest": info.latest,
121
+ "current": info.current,
122
+ "is_outdated": info.is_outdated,
123
+ "checked_at": info.checked_at,
124
+ },
125
+ f,
126
+ )
127
+ except Exception: # noqa: S110
128
+ # Silently fail if we can't write cache
129
+ pass
130
+
131
+
132
+ def _compare_versions(current: str, latest: str) -> bool:
133
+ """Compare versions to determine if an update is available.
134
+
135
+ Args:
136
+ current: Current version string
137
+ latest: Latest version string
138
+
139
+ Returns:
140
+ True if latest is newer than current, False otherwise.
141
+ """
142
+ if current == "unknown":
143
+ return False
144
+
145
+ try:
146
+ current_v = version.parse(current)
147
+ latest_v = version.parse(latest)
148
+ return latest_v > current_v
149
+ except Exception:
150
+ # If we can't parse versions, assume no update needed
151
+ return False
152
+
153
+
154
+ def check_for_updates() -> VersionInfo | None:
155
+ """Check for updates to hud-python.
156
+
157
+ This function checks PyPI for the latest version and caches the result
158
+ for 6 hours to avoid excessive API calls.
159
+
160
+ Returns:
161
+ VersionInfo if check succeeds, None otherwise.
162
+ """
163
+ # Check if we're in CI/testing environment
164
+ if os.environ.get("CI") or os.environ.get("HUD_SKIP_VERSION_CHECK"):
165
+ return None
166
+
167
+ # Get current version first
168
+ current = _get_current_version()
169
+ if current == "unknown":
170
+ return None
171
+
172
+ # Try to load from cache
173
+ cached_info = _load_cache()
174
+
175
+ # If cache exists but current version has changed (user upgraded), invalidate cache
176
+ if cached_info and cached_info.current != current:
177
+ cached_info = None # Force fresh check
178
+
179
+ if cached_info:
180
+ # Update the current version in the cached info to reflect reality
181
+ # but keep the cached latest version and timestamp
182
+ return VersionInfo(
183
+ latest=cached_info.latest,
184
+ current=current, # Use actual current version, not cached
185
+ is_outdated=_compare_versions(current, cached_info.latest),
186
+ checked_at=cached_info.checked_at,
187
+ )
188
+
189
+ # Fetch latest version from PyPI
190
+ latest = _fetch_latest_version()
191
+ if not latest:
192
+ return None
193
+
194
+ # Compare versions
195
+ is_outdated = _compare_versions(current, latest)
196
+
197
+ # Create version info
198
+ info = VersionInfo(
199
+ latest=latest,
200
+ current=current,
201
+ is_outdated=is_outdated,
202
+ checked_at=time.time(),
203
+ )
204
+
205
+ # Save to cache
206
+ _save_cache(info)
207
+
208
+ return info
209
+
210
+
211
+ def display_update_prompt(console: HUDConsole | None = None) -> None:
212
+ """Display update prompt if a new version is available.
213
+
214
+ This function checks for updates and displays a prompt to the user
215
+ if their version is outdated.
216
+
217
+ Args:
218
+ console: HUDConsole instance for output. If None, creates a new one.
219
+ """
220
+ if console is None:
221
+ console = HUDConsole()
222
+
223
+ try:
224
+ info = check_for_updates()
225
+ if info and info.is_outdated:
226
+ # Create update message
227
+ update_msg = (
228
+ f"🆕 A new version of hud-python is available: "
229
+ f"[bold cyan]{info.latest}[/bold cyan] "
230
+ f"(current: [dim]{info.current}[/dim])\n"
231
+ f" Run: [bold yellow]uv tool upgrade hud-python[/bold yellow] to update"
232
+ )
233
+
234
+ # Display as a subtle but noticeable panel
235
+ console._stdout_console.print(
236
+ f"\n[yellow]{update_msg}[/yellow]\n",
237
+ highlight=False,
238
+ )
239
+ except Exception: # noqa: S110
240
+ # Never let version checking disrupt the user's workflow
241
+ pass
242
+
243
+
244
+ def force_version_check() -> VersionInfo | None:
245
+ """Force a version check, bypassing the cache.
246
+
247
+ This is useful for explicit version checks or testing.
248
+
249
+ Returns:
250
+ VersionInfo if check succeeds, None otherwise.
251
+ """
252
+ # Clear the cache to force a fresh check
253
+ if VERSION_CACHE_FILE.exists():
254
+ with contextlib.suppress(Exception):
255
+ VERSION_CACHE_FILE.unlink()
256
+
257
+ return check_for_updates()
hud/clients/base.py CHANGED
@@ -170,7 +170,7 @@ class BaseHUDClient(AgentMCPClient):
170
170
  if self._initialized:
171
171
  await self._disconnect()
172
172
  self._initialized = False
173
- hud_console.info("Shutdown completed")
173
+ hud_console.info("Environment Shutdown completed")
174
174
  else:
175
175
  hud_console.debug("Client was not initialized, skipping disconnect")
176
176
 
hud/clients/mcp_use.py CHANGED
@@ -92,7 +92,9 @@ class MCPUseHUDClient(BaseHUDClient):
92
92
  try:
93
93
  assert self._client is not None # noqa: S101
94
94
  self._sessions = await self._client.create_all_sessions()
95
- hud_console.info(f"Created {len(self._sessions)} MCP sessions")
95
+ session_count = len(self._sessions)
96
+ session_text = "session" if session_count == 1 else "sessions"
97
+ hud_console.info(f"Created {session_count} MCP {session_text}")
96
98
 
97
99
  # Configure validation for all sessions based on client setting
98
100
  try:
hud/datasets/parallel.py CHANGED
@@ -111,13 +111,13 @@ def _process_worker(
111
111
  """Process a single task with telemetry tracking."""
112
112
  async with sem:
113
113
  try:
114
- # Create trace for this task (linked to the job) - match original format
114
+ # Create trace for this task (linked to the job)
115
115
  task_name = task_dict.get("prompt") or f"Task {index}"
116
116
 
117
117
  # Use the job_id to group all tasks under the same job
118
118
  raw_task_id = task_dict.get("id")
119
119
  safe_task_id = str(raw_task_id) if raw_task_id is not None else None
120
- with hud.trace(task_name, job_id=job_id, task_id=safe_task_id):
120
+ async with hud.async_trace(task_name, job_id=job_id, task_id=safe_task_id):
121
121
  # Convert dict to Task
122
122
  task = Task(**task_dict)
123
123
 
hud/datasets/runner.py CHANGED
@@ -28,8 +28,11 @@ async def run_dataset(
28
28
  split: str = "train",
29
29
  auto_respond: bool = False,
30
30
  ) -> list[Any]:
31
- """
32
- Run all tasks in a dataset with automatic job tracking.
31
+ """Run all tasks in a dataset with automatic job and telemetry tracking.
32
+
33
+ This function handles concurrent task execution with proper telemetry collection.
34
+ All tasks are executed in parallel up to `max_concurrent`, with full telemetry
35
+ automatically uploaded to the HUD platform.
33
36
 
34
37
  Args:
35
38
  name: Name for the job
@@ -37,23 +40,27 @@ async def run_dataset(
37
40
  Dataset object, OR list of Task objects
38
41
  agent_class: Agent class to instantiate (e.g., ClaudeAgent)
39
42
  agent_config: Configuration/kwargs for agent (model, etc.)
40
- max_concurrent: Maximum parallel task execution
43
+ max_concurrent: Maximum parallel task execution. Higher values improve throughput
44
+ but may increase memory usage. Recommended: 30-200 depending on
45
+ task complexity and available resources.
41
46
  metadata: Optional metadata for the job
42
47
  max_steps: Maximum steps per task
43
48
  split: Dataset split to use when loading from string (default: "train")
44
49
  auto_respond: Whether to use auto-response agent
45
50
 
46
51
  Returns:
47
- List of results from agent.run() in dataset order
52
+ List of results from agent.run() in dataset order. Telemetry is automatically
53
+ collected and uploaded for all tasks.
48
54
 
49
55
  Example:
50
56
  >>> from hud.agents import ClaudeAgent
51
- >>> # Option 1: From dataset string identifier
57
+ >>> # Basic usage with dataset identifier
52
58
  >>> results = await run_dataset(
53
59
  ... "SheetBench Eval",
54
60
  ... "hud-evals/SheetBench-50",
55
61
  ... ClaudeAgent,
56
62
  ... {"model": "claude-3-5-sonnet-20241022"},
63
+ ... max_concurrent=100, # Adjust based on your needs
57
64
  ... )
58
65
  >>> # Option 2: From HuggingFace dataset object
59
66
  >>> from datasets import load_dataset
@@ -62,9 +69,12 @@ async def run_dataset(
62
69
  >>> # Option 3: From list of dicts
63
70
  >>> tasks = [{"prompt": "...", "mcp_config": {...}, ...}, ...]
64
71
  >>> results = await run_dataset("browser_eval", tasks, ClaudeAgent)
72
+
73
+ Note:
74
+ Telemetry collection and upload is handled automatically. The function ensures
75
+ all telemetry is flushed before returning, even at high concurrency levels.
65
76
  """
66
- # Import here to avoid circular imports
67
- import hud
77
+ import hud # Import here to avoid circular imports
68
78
 
69
79
  dataset_link = None
70
80
 
@@ -91,33 +101,84 @@ async def run_dataset(
91
101
  except Exception:
92
102
  logger.warning("Failed to extract dataset verification info")
93
103
 
94
- with hud.job(name, metadata=job_metadata, dataset_link=dataset_link) as job_obj:
104
+ # Use async job context manager for high-concurrency telemetry
105
+ async with hud.async_job(name, metadata=job_metadata, dataset_link=dataset_link) as job_obj:
95
106
  # Run tasks with semaphore for concurrency control
96
107
  sem = asyncio.Semaphore(max_concurrent)
97
108
  results: list[Any | None] = [None] * len(dataset)
98
109
 
99
110
  async def _worker(index: int, task_dict: Any, max_steps: int = 10) -> None:
100
111
  async with sem:
101
- # Create trace for this task
102
- task_name = task_dict.get("prompt") or f"Task {index}"
103
-
104
- # Ensure task_id is a string for baggage propagation
105
- raw_task_id = task_dict.get("id")
106
- safe_task_id = str(raw_task_id) if raw_task_id is not None else None
107
- with hud.trace(task_name, job_id=job_obj.id, task_id=safe_task_id):
108
- # Convert dict to Task here, at trace level
109
- task = Task(**task_dict)
110
-
111
- agent = agent_class(**(agent_config or {}))
112
-
113
- if auto_respond:
114
- agent.response_agent = ResponseAgent()
115
- results[index] = await agent.run(task, max_steps=max_steps)
112
+ try:
113
+ # Create trace for this task
114
+ task_name = task_dict.get("prompt") or f"Task {index}"
115
+
116
+ # Ensure task_id is a string for baggage propagation
117
+ raw_task_id = task_dict.get("id")
118
+ safe_task_id = str(raw_task_id) if raw_task_id is not None else None
119
+ async with hud.async_trace(task_name, job_id=job_obj.id, task_id=safe_task_id):
120
+ # with hud.trace(task_name, job_id=job_obj.id, task_id=safe_task_id):
121
+ # Convert dict to Task here, at trace level
122
+ task = Task(**task_dict)
123
+
124
+ agent = agent_class(**(agent_config or {}))
125
+
126
+ if auto_respond:
127
+ agent.response_agent = ResponseAgent()
128
+ results[index] = await agent.run(task, max_steps=max_steps)
129
+ except Exception as e:
130
+ logger.exception("Task %s failed: %s", index, e)
131
+ results[index] = None
116
132
 
117
133
  # Execute all tasks
118
- await asyncio.gather(
134
+ worker_results = await asyncio.gather(
119
135
  *[_worker(i, task, max_steps=max_steps) for i, task in enumerate(dataset)],
120
136
  return_exceptions=True, # Don't fail entire batch on one error
121
137
  )
122
138
 
139
+ # Log any exceptions that occurred
140
+ for i, result in enumerate(worker_results):
141
+ if isinstance(result, Exception):
142
+ logger.error("Worker %s failed with exception: %s", i, result, exc_info=result)
143
+
144
+ # Ensure all telemetry is uploaded before returning
145
+ await _flush_telemetry()
146
+
123
147
  return results
148
+
149
+
150
+ async def _flush_telemetry() -> None:
151
+ """Flush all pending telemetry operations.
152
+
153
+ Ensures complete telemetry upload by:
154
+ 1. Waiting for all async status updates to complete
155
+ 2. Forcing OpenTelemetry span processor to export remaining spans
156
+
157
+ This prevents telemetry loss at high concurrency (200+ tasks) by ensuring
158
+ all operations complete before process exit.
159
+ """
160
+ from hud.otel.config import is_telemetry_configured
161
+ from hud.utils import hud_console
162
+ from hud.utils.task_tracking import wait_all_tasks
163
+
164
+ hud_console.info("Uploading telemetry...")
165
+
166
+ # Step 1: Wait for async status updates (job/trace status)
167
+ completed_tasks = await wait_all_tasks(timeout_seconds=20.0)
168
+ if completed_tasks > 0:
169
+ hud_console.info(f"Completed {completed_tasks} pending telemetry tasks")
170
+
171
+ # Step 2: Flush OpenTelemetry span exports
172
+ if is_telemetry_configured():
173
+ try:
174
+ from opentelemetry import trace
175
+ from opentelemetry.sdk.trace import TracerProvider
176
+
177
+ provider = trace.get_tracer_provider()
178
+ if isinstance(provider, TracerProvider):
179
+ provider.force_flush(timeout_millis=20000)
180
+ logger.debug("OpenTelemetry spans flushed successfully")
181
+ except Exception as e:
182
+ logger.warning("Failed to flush OpenTelemetry: %s", e)
183
+
184
+ hud_console.info("Telemetry uploaded successfully")
File without changes
@@ -0,0 +1,106 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest.mock import AsyncMock, MagicMock, patch
4
+
5
+ import pytest
6
+
7
+ from hud.datasets.runner import _flush_telemetry
8
+
9
+
10
+ @pytest.mark.asyncio
11
+ async def test_flush_telemetry():
12
+ """Test _flush_telemetry function."""
13
+ with (
14
+ patch("hud.otel.config.is_telemetry_configured", return_value=True),
15
+ patch("hud.utils.hud_console.hud_console"),
16
+ patch("hud.utils.task_tracking.wait_all_tasks", new_callable=AsyncMock) as mock_wait,
17
+ patch("opentelemetry.trace.get_tracer_provider") as mock_get_provider,
18
+ ):
19
+ from opentelemetry.sdk.trace import TracerProvider
20
+
21
+ mock_provider = MagicMock(spec=TracerProvider)
22
+ mock_provider.force_flush.return_value = True
23
+ mock_get_provider.return_value = mock_provider
24
+
25
+ mock_wait.return_value = 5
26
+
27
+ await _flush_telemetry()
28
+
29
+ mock_wait.assert_called_once()
30
+ mock_provider.force_flush.assert_called_once_with(timeout_millis=20000)
31
+
32
+
33
+ @pytest.mark.asyncio
34
+ async def test_flush_telemetry_no_telemetry():
35
+ """Test _flush_telemetry when telemetry is not configured."""
36
+ with (
37
+ patch("hud.otel.config.is_telemetry_configured", return_value=False),
38
+ patch("hud.utils.hud_console.hud_console"),
39
+ patch("hud.utils.task_tracking.wait_all_tasks", new_callable=AsyncMock) as mock_wait,
40
+ patch("opentelemetry.trace.get_tracer_provider"),
41
+ ):
42
+ mock_wait.return_value = 0
43
+
44
+ await _flush_telemetry()
45
+
46
+ mock_wait.assert_called_once()
47
+
48
+
49
+ @pytest.mark.asyncio
50
+ async def test_flush_telemetry_exception():
51
+ """Test _flush_telemetry handles exceptions gracefully."""
52
+ with (
53
+ patch("hud.otel.config.is_telemetry_configured", return_value=True),
54
+ patch("hud.utils.hud_console.hud_console"),
55
+ patch("hud.utils.task_tracking.wait_all_tasks", new_callable=AsyncMock) as mock_wait,
56
+ patch("opentelemetry.trace.get_tracer_provider") as mock_get_provider,
57
+ ):
58
+ from opentelemetry.sdk.trace import TracerProvider
59
+
60
+ mock_provider = MagicMock(spec=TracerProvider)
61
+ mock_provider.force_flush.side_effect = Exception("Flush failed")
62
+ mock_get_provider.return_value = mock_provider
63
+
64
+ mock_wait.return_value = 3
65
+
66
+ # Should not raise
67
+ await _flush_telemetry()
68
+
69
+
70
+ @pytest.mark.asyncio
71
+ async def test_flush_telemetry_no_completed_tasks():
72
+ """Test _flush_telemetry when no tasks were completed."""
73
+ with (
74
+ patch("hud.otel.config.is_telemetry_configured", return_value=True),
75
+ patch("hud.utils.hud_console.hud_console"),
76
+ patch("hud.utils.task_tracking.wait_all_tasks", new_callable=AsyncMock) as mock_wait,
77
+ patch("opentelemetry.trace.get_tracer_provider") as mock_get_provider,
78
+ ):
79
+ from opentelemetry.sdk.trace import TracerProvider
80
+
81
+ mock_provider = MagicMock(spec=TracerProvider)
82
+ mock_get_provider.return_value = mock_provider
83
+
84
+ mock_wait.return_value = 0
85
+
86
+ await _flush_telemetry()
87
+
88
+ mock_provider.force_flush.assert_called_once()
89
+
90
+
91
+ @pytest.mark.asyncio
92
+ async def test_flush_telemetry_non_sdk_provider():
93
+ """Test _flush_telemetry with non-SDK TracerProvider."""
94
+ with (
95
+ patch("hud.otel.config.is_telemetry_configured", return_value=True),
96
+ patch("hud.utils.hud_console.hud_console"),
97
+ patch("hud.utils.task_tracking.wait_all_tasks", new_callable=AsyncMock) as mock_wait,
98
+ patch("opentelemetry.trace.get_tracer_provider") as mock_get_provider,
99
+ ):
100
+ # Return a non-TracerProvider object
101
+ mock_get_provider.return_value = MagicMock(spec=object)
102
+
103
+ mock_wait.return_value = 2
104
+
105
+ # Should not raise
106
+ await _flush_telemetry()