hud-python 0.3.5__py3-none-any.whl → 0.4.1__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/__init__.py +22 -89
- hud/agents/__init__.py +15 -0
- hud/agents/art.py +101 -0
- hud/agents/base.py +599 -0
- hud/{mcp → agents}/claude.py +373 -321
- hud/{mcp → agents}/langchain.py +250 -250
- hud/agents/misc/__init__.py +7 -0
- hud/{agent → agents}/misc/response_agent.py +80 -80
- hud/{mcp → agents}/openai.py +352 -334
- hud/agents/openai_chat_generic.py +154 -0
- hud/{mcp → agents}/tests/__init__.py +1 -1
- hud/agents/tests/test_base.py +742 -0
- hud/agents/tests/test_claude.py +324 -0
- hud/{mcp → agents}/tests/test_client.py +363 -324
- hud/{mcp → agents}/tests/test_openai.py +237 -238
- hud/cli/__init__.py +617 -0
- hud/cli/__main__.py +8 -0
- hud/cli/analyze.py +371 -0
- hud/cli/analyze_metadata.py +230 -0
- hud/cli/build.py +427 -0
- hud/cli/clone.py +185 -0
- hud/cli/cursor.py +92 -0
- hud/cli/debug.py +392 -0
- hud/cli/docker_utils.py +83 -0
- hud/cli/init.py +281 -0
- hud/cli/interactive.py +353 -0
- hud/cli/mcp_server.py +756 -0
- hud/cli/pull.py +336 -0
- hud/cli/push.py +370 -0
- hud/cli/remote_runner.py +311 -0
- hud/cli/runner.py +160 -0
- hud/cli/tests/__init__.py +3 -0
- hud/cli/tests/test_analyze.py +284 -0
- hud/cli/tests/test_cli_init.py +265 -0
- hud/cli/tests/test_cli_main.py +27 -0
- hud/cli/tests/test_clone.py +142 -0
- hud/cli/tests/test_cursor.py +253 -0
- hud/cli/tests/test_debug.py +453 -0
- hud/cli/tests/test_mcp_server.py +139 -0
- hud/cli/tests/test_utils.py +388 -0
- hud/cli/utils.py +263 -0
- hud/clients/README.md +143 -0
- hud/clients/__init__.py +16 -0
- hud/clients/base.py +379 -0
- hud/clients/fastmcp.py +222 -0
- hud/clients/mcp_use.py +278 -0
- hud/clients/tests/__init__.py +1 -0
- hud/clients/tests/test_client_integration.py +111 -0
- hud/clients/tests/test_fastmcp.py +342 -0
- hud/clients/tests/test_protocol.py +188 -0
- hud/clients/utils/__init__.py +1 -0
- hud/clients/utils/retry_transport.py +160 -0
- hud/datasets.py +322 -192
- hud/misc/__init__.py +1 -0
- hud/{agent → misc}/claude_plays_pokemon.py +292 -283
- hud/otel/__init__.py +35 -0
- hud/otel/collector.py +142 -0
- hud/otel/config.py +164 -0
- hud/otel/context.py +536 -0
- hud/otel/exporters.py +366 -0
- hud/otel/instrumentation.py +97 -0
- hud/otel/processors.py +118 -0
- hud/otel/tests/__init__.py +1 -0
- hud/otel/tests/test_processors.py +197 -0
- hud/server/__init__.py +5 -5
- hud/server/context.py +114 -0
- hud/server/helper/__init__.py +5 -0
- hud/server/low_level.py +132 -0
- hud/server/server.py +166 -0
- hud/server/tests/__init__.py +3 -0
- hud/settings.py +73 -79
- hud/shared/__init__.py +5 -0
- hud/{exceptions.py → shared/exceptions.py} +180 -180
- hud/{server → shared}/requests.py +264 -264
- hud/shared/tests/test_exceptions.py +157 -0
- hud/{server → shared}/tests/test_requests.py +275 -275
- hud/telemetry/__init__.py +25 -30
- hud/telemetry/instrument.py +379 -0
- hud/telemetry/job.py +309 -141
- hud/telemetry/replay.py +74 -0
- hud/telemetry/trace.py +83 -0
- hud/tools/__init__.py +33 -34
- hud/tools/base.py +365 -65
- hud/tools/bash.py +161 -137
- hud/tools/computer/__init__.py +15 -13
- hud/tools/computer/anthropic.py +437 -420
- hud/tools/computer/hud.py +376 -334
- hud/tools/computer/openai.py +295 -292
- hud/tools/computer/settings.py +82 -0
- hud/tools/edit.py +314 -290
- hud/tools/executors/__init__.py +30 -30
- hud/tools/executors/base.py +539 -532
- hud/tools/executors/pyautogui.py +621 -619
- hud/tools/executors/tests/__init__.py +1 -1
- hud/tools/executors/tests/test_base_executor.py +338 -338
- hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
- hud/tools/executors/xdo.py +511 -503
- hud/tools/{playwright_tool.py → playwright.py} +412 -379
- hud/tools/tests/__init__.py +3 -3
- hud/tools/tests/test_base.py +282 -0
- hud/tools/tests/test_bash.py +158 -152
- hud/tools/tests/test_bash_extended.py +197 -0
- hud/tools/tests/test_computer.py +425 -52
- hud/tools/tests/test_computer_actions.py +34 -34
- hud/tools/tests/test_edit.py +259 -240
- hud/tools/tests/test_init.py +27 -27
- hud/tools/tests/test_playwright_tool.py +183 -183
- hud/tools/tests/test_tools.py +145 -157
- hud/tools/tests/test_utils.py +156 -156
- hud/tools/types.py +72 -0
- hud/tools/utils.py +50 -50
- hud/types.py +136 -89
- hud/utils/__init__.py +10 -16
- hud/utils/async_utils.py +65 -0
- hud/utils/design.py +168 -0
- hud/utils/mcp.py +55 -0
- hud/utils/progress.py +149 -149
- hud/utils/telemetry.py +66 -66
- hud/utils/tests/test_async_utils.py +173 -0
- hud/utils/tests/test_init.py +17 -21
- hud/utils/tests/test_progress.py +261 -225
- hud/utils/tests/test_telemetry.py +82 -37
- hud/utils/tests/test_version.py +8 -8
- hud/version.py +7 -7
- hud_python-0.4.1.dist-info/METADATA +476 -0
- hud_python-0.4.1.dist-info/RECORD +132 -0
- hud_python-0.4.1.dist-info/entry_points.txt +3 -0
- {hud_python-0.3.5.dist-info → hud_python-0.4.1.dist-info}/licenses/LICENSE +21 -21
- hud/adapters/__init__.py +0 -8
- hud/adapters/claude/__init__.py +0 -5
- hud/adapters/claude/adapter.py +0 -180
- hud/adapters/claude/tests/__init__.py +0 -1
- hud/adapters/claude/tests/test_adapter.py +0 -519
- hud/adapters/common/__init__.py +0 -6
- hud/adapters/common/adapter.py +0 -178
- hud/adapters/common/tests/test_adapter.py +0 -289
- hud/adapters/common/types.py +0 -446
- hud/adapters/operator/__init__.py +0 -5
- hud/adapters/operator/adapter.py +0 -108
- hud/adapters/operator/tests/__init__.py +0 -1
- hud/adapters/operator/tests/test_adapter.py +0 -370
- hud/agent/__init__.py +0 -19
- hud/agent/base.py +0 -126
- hud/agent/claude.py +0 -271
- hud/agent/langchain.py +0 -215
- hud/agent/misc/__init__.py +0 -3
- hud/agent/operator.py +0 -268
- hud/agent/tests/__init__.py +0 -1
- hud/agent/tests/test_base.py +0 -202
- hud/env/__init__.py +0 -11
- hud/env/client.py +0 -35
- hud/env/docker_client.py +0 -349
- hud/env/environment.py +0 -446
- hud/env/local_docker_client.py +0 -358
- hud/env/remote_client.py +0 -212
- hud/env/remote_docker_client.py +0 -292
- hud/gym.py +0 -130
- hud/job.py +0 -773
- hud/mcp/__init__.py +0 -17
- hud/mcp/base.py +0 -631
- hud/mcp/client.py +0 -312
- hud/mcp/tests/test_base.py +0 -512
- hud/mcp/tests/test_claude.py +0 -294
- hud/task.py +0 -149
- hud/taskset.py +0 -237
- hud/telemetry/_trace.py +0 -347
- hud/telemetry/context.py +0 -230
- hud/telemetry/exporter.py +0 -575
- hud/telemetry/instrumentation/__init__.py +0 -3
- hud/telemetry/instrumentation/mcp.py +0 -259
- hud/telemetry/instrumentation/registry.py +0 -59
- hud/telemetry/mcp_models.py +0 -270
- hud/telemetry/tests/__init__.py +0 -1
- hud/telemetry/tests/test_context.py +0 -210
- hud/telemetry/tests/test_trace.py +0 -312
- hud/tools/helper/README.md +0 -56
- hud/tools/helper/__init__.py +0 -9
- hud/tools/helper/mcp_server.py +0 -78
- hud/tools/helper/server_initialization.py +0 -115
- hud/tools/helper/utils.py +0 -58
- hud/trajectory.py +0 -94
- hud/utils/agent.py +0 -37
- hud/utils/common.py +0 -256
- hud/utils/config.py +0 -120
- hud/utils/deprecation.py +0 -115
- hud/utils/misc.py +0 -53
- hud/utils/tests/test_common.py +0 -277
- hud/utils/tests/test_config.py +0 -129
- hud_python-0.3.5.dist-info/METADATA +0 -284
- hud_python-0.3.5.dist-info/RECORD +0 -120
- /hud/{adapters/common → shared}/tests/__init__.py +0 -0
- {hud_python-0.3.5.dist-info → hud_python-0.4.1.dist-info}/WHEEL +0 -0
hud/utils/deprecation.py
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
"""Deprecation utilities for HUD SDK."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import functools
|
|
6
|
-
import logging
|
|
7
|
-
import warnings
|
|
8
|
-
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
|
9
|
-
|
|
10
|
-
logger = logging.getLogger(__name__)
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING:
|
|
13
|
-
from collections.abc import Callable
|
|
14
|
-
T = TypeVar("T")
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def deprecated(
|
|
18
|
-
reason: str,
|
|
19
|
-
*,
|
|
20
|
-
version: str | None = None,
|
|
21
|
-
replacement: str | None = None,
|
|
22
|
-
removal_version: str | None = None,
|
|
23
|
-
) -> Callable[[T], T]:
|
|
24
|
-
"""
|
|
25
|
-
Decorator to mark functions, methods, or classes as deprecated.
|
|
26
|
-
|
|
27
|
-
Args:
|
|
28
|
-
reason: Explanation of why this is deprecated
|
|
29
|
-
version: Version when this was deprecated (e.g., "1.0.0")
|
|
30
|
-
replacement: What to use instead
|
|
31
|
-
removal_version: Version when this will be removed
|
|
32
|
-
|
|
33
|
-
Example:
|
|
34
|
-
@deprecated(
|
|
35
|
-
reason="Use TaskConfig instead",
|
|
36
|
-
replacement="hud.datasets.TaskConfig",
|
|
37
|
-
version="0.3.0",
|
|
38
|
-
removal_version="0.4.0"
|
|
39
|
-
)
|
|
40
|
-
class OldClass:
|
|
41
|
-
pass
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
def decorator(obj: T) -> T:
|
|
45
|
-
message_parts = [f"{obj.__module__}.{obj.__qualname__} is deprecated"]
|
|
46
|
-
|
|
47
|
-
if version:
|
|
48
|
-
message_parts.append(f"(deprecated since v{version})")
|
|
49
|
-
|
|
50
|
-
message_parts.append(f": {reason}")
|
|
51
|
-
|
|
52
|
-
if replacement:
|
|
53
|
-
message_parts.append(f". Use {replacement} instead")
|
|
54
|
-
|
|
55
|
-
if removal_version:
|
|
56
|
-
message_parts.append(f". Will be removed in v{removal_version}")
|
|
57
|
-
|
|
58
|
-
deprecation_message = " ".join(message_parts) + "."
|
|
59
|
-
|
|
60
|
-
if isinstance(obj, type):
|
|
61
|
-
# Handle class deprecation
|
|
62
|
-
original_init = obj.__init__
|
|
63
|
-
|
|
64
|
-
@functools.wraps(original_init)
|
|
65
|
-
def new_init(self: Any, *args: Any, **kwargs: Any) -> None:
|
|
66
|
-
warnings.warn(deprecation_message, DeprecationWarning, stacklevel=2)
|
|
67
|
-
logger.warning(deprecation_message)
|
|
68
|
-
original_init(self, *args, **kwargs)
|
|
69
|
-
|
|
70
|
-
obj.__init__ = new_init
|
|
71
|
-
|
|
72
|
-
# Update docstring
|
|
73
|
-
if obj.__doc__:
|
|
74
|
-
obj.__doc__ = f"**DEPRECATED**: {deprecation_message}\n\n{obj.__doc__}"
|
|
75
|
-
else:
|
|
76
|
-
obj.__doc__ = f"**DEPRECATED**: {deprecation_message}"
|
|
77
|
-
|
|
78
|
-
else:
|
|
79
|
-
# Handle function/method deprecation
|
|
80
|
-
func = cast("Callable[..., Any]", obj)
|
|
81
|
-
|
|
82
|
-
@functools.wraps(func)
|
|
83
|
-
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
84
|
-
warnings.warn(deprecation_message, DeprecationWarning, stacklevel=2)
|
|
85
|
-
logger.warning(deprecation_message)
|
|
86
|
-
return func(*args, **kwargs)
|
|
87
|
-
|
|
88
|
-
# Update docstring
|
|
89
|
-
if wrapper.__doc__:
|
|
90
|
-
wrapper.__doc__ = f"**DEPRECATED**: {deprecation_message}\n\n{wrapper.__doc__}"
|
|
91
|
-
else:
|
|
92
|
-
wrapper.__doc__ = f"**DEPRECATED**: {deprecation_message}"
|
|
93
|
-
|
|
94
|
-
return cast("T", wrapper)
|
|
95
|
-
|
|
96
|
-
return obj
|
|
97
|
-
|
|
98
|
-
return decorator
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def emit_deprecation_warning(
|
|
102
|
-
message: str,
|
|
103
|
-
category: type[Warning] = DeprecationWarning,
|
|
104
|
-
stacklevel: int = 2,
|
|
105
|
-
) -> None:
|
|
106
|
-
"""
|
|
107
|
-
Emit a deprecation warning with both warnings and logging.
|
|
108
|
-
|
|
109
|
-
Args:
|
|
110
|
-
message: The deprecation message
|
|
111
|
-
category: Warning category (default: DeprecationWarning)
|
|
112
|
-
stacklevel: Stack level for warning (default: 2)
|
|
113
|
-
"""
|
|
114
|
-
warnings.warn(message, category, stacklevel=stacklevel)
|
|
115
|
-
logger.warning(message)
|
hud/utils/misc.py
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
from typing import TYPE_CHECKING, Any
|
|
5
|
-
|
|
6
|
-
from hud.server import make_request
|
|
7
|
-
from hud.settings import settings
|
|
8
|
-
|
|
9
|
-
if TYPE_CHECKING:
|
|
10
|
-
from hud.env.environment import Environment # Import Environment for type hinting
|
|
11
|
-
|
|
12
|
-
logger = logging.getLogger(__name__)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
async def upload_env_telemetry(
|
|
16
|
-
environment: Environment,
|
|
17
|
-
results: Any,
|
|
18
|
-
api_key: str | None = None,
|
|
19
|
-
) -> None:
|
|
20
|
-
"""
|
|
21
|
-
Sends telemetry data (results from a cloud runner) to the HUD telemetry upload endpoint.
|
|
22
|
-
"""
|
|
23
|
-
environment_id = environment.client.env_id # type: ignore
|
|
24
|
-
|
|
25
|
-
if not api_key:
|
|
26
|
-
api_key = settings.api_key
|
|
27
|
-
|
|
28
|
-
if not api_key:
|
|
29
|
-
raise ValueError("API key must be provided either as an argument or set in hud.settings.")
|
|
30
|
-
|
|
31
|
-
endpoint_url = f"{settings.base_url}/v2/environments/{environment_id}/telemetry-upload"
|
|
32
|
-
|
|
33
|
-
request_payload = {
|
|
34
|
-
"results": {
|
|
35
|
-
"steps": results,
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
logger.debug("Sending telemetry to %s for env_id: %s", endpoint_url, environment_id)
|
|
40
|
-
|
|
41
|
-
try:
|
|
42
|
-
await make_request(
|
|
43
|
-
method="POST",
|
|
44
|
-
url=endpoint_url,
|
|
45
|
-
json=request_payload,
|
|
46
|
-
api_key=api_key,
|
|
47
|
-
)
|
|
48
|
-
logger.info("Successfully uploaded telemetry for environment_id: %s", environment_id)
|
|
49
|
-
except Exception as e:
|
|
50
|
-
logger.error(
|
|
51
|
-
"Failed to upload telemetry for environment_id: %s. Error: %s", environment_id, e
|
|
52
|
-
)
|
|
53
|
-
raise
|
hud/utils/tests/test_common.py
DELETED
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import io
|
|
4
|
-
import tarfile
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
7
|
-
|
|
8
|
-
import pytest
|
|
9
|
-
|
|
10
|
-
from hud.utils.common import directory_to_tar_bytes, get_gym_id
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING:
|
|
13
|
-
import pytest_mock
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def test_directory_to_tar_bytes(tmpdir_factory: pytest.TempdirFactory):
|
|
17
|
-
"""Test that a directory can be converted to a tar bytes object."""
|
|
18
|
-
temp_dir = tmpdir_factory.mktemp("test_dir")
|
|
19
|
-
temp_dir_path = Path(temp_dir)
|
|
20
|
-
|
|
21
|
-
(temp_dir_path / "test.txt").write_text("test content")
|
|
22
|
-
|
|
23
|
-
nested_dir = temp_dir_path / "nested"
|
|
24
|
-
nested_dir.mkdir(exist_ok=True)
|
|
25
|
-
(nested_dir / "file.txt").write_text("nested content")
|
|
26
|
-
|
|
27
|
-
tar_bytes = directory_to_tar_bytes(temp_dir_path)
|
|
28
|
-
assert tar_bytes is not None
|
|
29
|
-
assert len(tar_bytes) > 0
|
|
30
|
-
|
|
31
|
-
with tarfile.open(fileobj=io.BytesIO(tar_bytes), mode="r:*") as tar:
|
|
32
|
-
members = tar.getmembers()
|
|
33
|
-
member_names = {m.name for m in members}
|
|
34
|
-
|
|
35
|
-
assert "test.txt" in member_names
|
|
36
|
-
assert "nested/file.txt" in member_names
|
|
37
|
-
|
|
38
|
-
test_content = tar.extractfile("test.txt")
|
|
39
|
-
assert test_content is not None
|
|
40
|
-
assert test_content.read().decode() == "test content"
|
|
41
|
-
|
|
42
|
-
nested_content = tar.extractfile("nested/file.txt")
|
|
43
|
-
assert nested_content is not None
|
|
44
|
-
assert nested_content.read().decode() == "nested content"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@pytest.mark.asyncio
|
|
48
|
-
async def test_get_gym_id(mocker: pytest_mock.MockerFixture):
|
|
49
|
-
"""Test that the gym ID can be retrieved."""
|
|
50
|
-
mocker.patch("hud.utils.common.make_request", return_value={"id": "test_gym_id"})
|
|
51
|
-
gym_id = await get_gym_id("test_gym")
|
|
52
|
-
assert gym_id == "test_gym_id"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_function_config_stores_function_name_args_and_optional_id():
|
|
56
|
-
"""FunctionConfig should store function name, args list, and optional id."""
|
|
57
|
-
from hud.utils.common import FunctionConfig
|
|
58
|
-
|
|
59
|
-
# Minimal config
|
|
60
|
-
minimal = FunctionConfig(function="test_func", args=[])
|
|
61
|
-
assert minimal.function == "test_func"
|
|
62
|
-
assert minimal.args == []
|
|
63
|
-
assert minimal.id is None
|
|
64
|
-
|
|
65
|
-
# With args
|
|
66
|
-
with_args = FunctionConfig(function="navigate", args=["https://example.com", {"wait": True}])
|
|
67
|
-
assert with_args.function == "navigate"
|
|
68
|
-
assert len(with_args.args) == 2
|
|
69
|
-
assert with_args.args[0] == "https://example.com"
|
|
70
|
-
assert with_args.args[1] == {"wait": True}
|
|
71
|
-
|
|
72
|
-
# With id
|
|
73
|
-
with_id = FunctionConfig(
|
|
74
|
-
function="complex_operation",
|
|
75
|
-
args=[42, "test", {"nested": {"key": "value"}}],
|
|
76
|
-
id="op_123",
|
|
77
|
-
)
|
|
78
|
-
assert with_id.function == "complex_operation"
|
|
79
|
-
assert len(with_id.args) == 3
|
|
80
|
-
assert with_id.id == "op_123"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
@pytest.mark.asyncio
|
|
84
|
-
async def test_get_gym_id_fetches_id_from_api_response(
|
|
85
|
-
mocker: pytest_mock.MockerFixture,
|
|
86
|
-
):
|
|
87
|
-
"""get_gym_id should extract 'id' field from API response."""
|
|
88
|
-
# Arrange
|
|
89
|
-
api_response = {"id": "gym-123", "name": "Test Gym", "status": "active"}
|
|
90
|
-
mocker.patch("hud.utils.common.make_request", return_value=api_response)
|
|
91
|
-
|
|
92
|
-
# Act
|
|
93
|
-
gym_id = await get_gym_id("test_gym")
|
|
94
|
-
|
|
95
|
-
# Assert
|
|
96
|
-
assert gym_id == "gym-123"
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
@pytest.mark.asyncio
|
|
100
|
-
async def test_get_gym_id_propagates_network_errors(mocker: pytest_mock.MockerFixture):
|
|
101
|
-
"""get_gym_id should propagate exceptions from make_request."""
|
|
102
|
-
# Arrange
|
|
103
|
-
mocker.patch("hud.utils.common.make_request", side_effect=ConnectionError("API unavailable"))
|
|
104
|
-
|
|
105
|
-
# Act & Assert
|
|
106
|
-
with pytest.raises(ConnectionError, match="API unavailable"):
|
|
107
|
-
await get_gym_id("test_gym")
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
@pytest.mark.asyncio
|
|
111
|
-
async def test_get_gym_id_raises_key_error_when_id_missing(
|
|
112
|
-
mocker: pytest_mock.MockerFixture,
|
|
113
|
-
):
|
|
114
|
-
"""get_gym_id should raise KeyError when response lacks 'id' field."""
|
|
115
|
-
# Arrange
|
|
116
|
-
incomplete_response = {"name": "Test Gym", "status": "active"} # Missing 'id'
|
|
117
|
-
mocker.patch("hud.utils.common.make_request", return_value=incomplete_response)
|
|
118
|
-
|
|
119
|
-
# Act & Assert
|
|
120
|
-
with pytest.raises(KeyError):
|
|
121
|
-
await get_gym_id("test_gym")
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def test_directory_to_tar_bytes_creates_valid_tar_archive(
|
|
125
|
-
tmpdir_factory: pytest.TempdirFactory,
|
|
126
|
-
):
|
|
127
|
-
"""directory_to_tar_bytes should create a valid tar archive containing all files."""
|
|
128
|
-
# Arrange
|
|
129
|
-
temp_dir = tmpdir_factory.mktemp("test_archive")
|
|
130
|
-
temp_dir_path = Path(temp_dir)
|
|
131
|
-
|
|
132
|
-
# Create test structure
|
|
133
|
-
(temp_dir_path / "file1.txt").write_text("content1")
|
|
134
|
-
(temp_dir_path / "file2.py").write_text("import os\nprint('hello')")
|
|
135
|
-
|
|
136
|
-
subdir = temp_dir_path / "subdir"
|
|
137
|
-
subdir.mkdir()
|
|
138
|
-
(subdir / "nested.json").write_text('{"key": "value"}')
|
|
139
|
-
|
|
140
|
-
# Act
|
|
141
|
-
tar_bytes = directory_to_tar_bytes(temp_dir_path)
|
|
142
|
-
|
|
143
|
-
# Assert
|
|
144
|
-
assert isinstance(tar_bytes, bytes)
|
|
145
|
-
assert len(tar_bytes) > 0
|
|
146
|
-
|
|
147
|
-
# Verify contents
|
|
148
|
-
with tarfile.open(fileobj=io.BytesIO(tar_bytes), mode="r:*") as tar:
|
|
149
|
-
members = {m.name for m in tar.getmembers()}
|
|
150
|
-
assert "file1.txt" in members
|
|
151
|
-
assert "file2.py" in members
|
|
152
|
-
assert "subdir/nested.json" in members
|
|
153
|
-
|
|
154
|
-
# Verify file contents
|
|
155
|
-
content = tar.extractfile("file1.txt")
|
|
156
|
-
assert content is not None
|
|
157
|
-
assert content.read().decode() == "content1"
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def test_directory_to_tar_bytes_handles_empty_directory(
|
|
161
|
-
tmpdir_factory: pytest.TempdirFactory,
|
|
162
|
-
):
|
|
163
|
-
"""directory_to_tar_bytes should handle empty directories gracefully."""
|
|
164
|
-
# Arrange
|
|
165
|
-
empty_dir = tmpdir_factory.mktemp("empty")
|
|
166
|
-
empty_dir_path = Path(empty_dir)
|
|
167
|
-
|
|
168
|
-
# Act
|
|
169
|
-
tar_bytes = directory_to_tar_bytes(empty_dir_path)
|
|
170
|
-
|
|
171
|
-
# Assert
|
|
172
|
-
assert isinstance(tar_bytes, bytes)
|
|
173
|
-
assert len(tar_bytes) > 0 # Even empty tar has headers
|
|
174
|
-
|
|
175
|
-
with tarfile.open(fileobj=io.BytesIO(tar_bytes), mode="r:*") as tar:
|
|
176
|
-
members = tar.getmembers()
|
|
177
|
-
# May contain the directory itself or be completely empty
|
|
178
|
-
assert len(members) >= 0
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def test_directory_to_tar_bytes_preserves_directory_structure(
|
|
182
|
-
tmpdir_factory: pytest.TempdirFactory,
|
|
183
|
-
):
|
|
184
|
-
"""directory_to_tar_bytes should preserve nested directory structure."""
|
|
185
|
-
# Arrange
|
|
186
|
-
root = tmpdir_factory.mktemp("root")
|
|
187
|
-
root_path = Path(root)
|
|
188
|
-
|
|
189
|
-
# Create nested structure
|
|
190
|
-
(root_path / "a" / "b" / "c").mkdir(parents=True)
|
|
191
|
-
(root_path / "a" / "file1.txt").write_text("in a")
|
|
192
|
-
(root_path / "a" / "b" / "file2.txt").write_text("in b")
|
|
193
|
-
(root_path / "a" / "b" / "c" / "file3.txt").write_text("in c")
|
|
194
|
-
|
|
195
|
-
# Act
|
|
196
|
-
tar_bytes = directory_to_tar_bytes(root_path)
|
|
197
|
-
|
|
198
|
-
# Assert
|
|
199
|
-
with tarfile.open(fileobj=io.BytesIO(tar_bytes), mode="r:*") as tar:
|
|
200
|
-
members = {m.name for m in tar.getmembers()}
|
|
201
|
-
assert "a/file1.txt" in members
|
|
202
|
-
assert "a/b/file2.txt" in members
|
|
203
|
-
assert "a/b/c/file3.txt" in members
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def test_directory_to_tar_bytes_with_exclusions(tmpdir_factory: pytest.TempdirFactory):
|
|
207
|
-
"""Test directory_to_tar_bytes with files to exclude."""
|
|
208
|
-
temp_dir = tmpdir_factory.mktemp("test_exclude_dir")
|
|
209
|
-
temp_dir_path = Path(temp_dir)
|
|
210
|
-
|
|
211
|
-
# Create various files
|
|
212
|
-
(temp_dir_path / "include_me.txt").write_text("include")
|
|
213
|
-
(temp_dir_path / ".git").mkdir()
|
|
214
|
-
(temp_dir_path / ".git" / "config").write_text("git config")
|
|
215
|
-
(temp_dir_path / "__pycache__").mkdir()
|
|
216
|
-
(temp_dir_path / "__pycache__" / "module.pyc").write_bytes(b"pyc content")
|
|
217
|
-
(temp_dir_path / "normal_dir").mkdir()
|
|
218
|
-
(temp_dir_path / "normal_dir" / "file.py").write_text("python code")
|
|
219
|
-
|
|
220
|
-
tar_bytes = directory_to_tar_bytes(temp_dir_path)
|
|
221
|
-
|
|
222
|
-
# Check contents
|
|
223
|
-
with tarfile.open(fileobj=io.BytesIO(tar_bytes), mode="r:*") as tar:
|
|
224
|
-
member_names = {m.name for m in tar.getmembers()}
|
|
225
|
-
|
|
226
|
-
# Should include regular files and directories
|
|
227
|
-
assert "include_me.txt" in member_names
|
|
228
|
-
assert "normal_dir/file.py" in member_names
|
|
229
|
-
|
|
230
|
-
# Implementation might exclude common patterns like .git and __pycache__
|
|
231
|
-
# This depends on the actual implementation
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
def test_directory_to_tar_bytes_empty_directory(tmpdir_factory: pytest.TempdirFactory):
|
|
235
|
-
"""Test directory_to_tar_bytes with empty directory."""
|
|
236
|
-
temp_dir = tmpdir_factory.mktemp("empty_dir")
|
|
237
|
-
temp_dir_path = Path(temp_dir)
|
|
238
|
-
|
|
239
|
-
tar_bytes = directory_to_tar_bytes(temp_dir_path)
|
|
240
|
-
|
|
241
|
-
# Should still create a valid tar even if empty
|
|
242
|
-
assert tar_bytes is not None
|
|
243
|
-
assert len(tar_bytes) > 0
|
|
244
|
-
|
|
245
|
-
with tarfile.open(fileobj=io.BytesIO(tar_bytes), mode="r:*") as tar:
|
|
246
|
-
members = tar.getmembers()
|
|
247
|
-
# Might be empty or contain just the root directory
|
|
248
|
-
assert len(members) >= 0
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
def test_directory_to_tar_bytes_symlinks(tmpdir_factory: pytest.TempdirFactory):
|
|
252
|
-
"""Test directory_to_tar_bytes with symbolic links."""
|
|
253
|
-
temp_dir = tmpdir_factory.mktemp("symlink_dir")
|
|
254
|
-
temp_dir_path = Path(temp_dir)
|
|
255
|
-
|
|
256
|
-
# Create a file and a symlink to it
|
|
257
|
-
target_file = temp_dir_path / "target.txt"
|
|
258
|
-
target_file.write_text("target content")
|
|
259
|
-
|
|
260
|
-
symlink = temp_dir_path / "link_to_target.txt"
|
|
261
|
-
try:
|
|
262
|
-
symlink.symlink_to(target_file)
|
|
263
|
-
has_symlink = True
|
|
264
|
-
except OSError:
|
|
265
|
-
# Symlinks might not be supported on all systems (e.g., Windows without admin)
|
|
266
|
-
has_symlink = False
|
|
267
|
-
|
|
268
|
-
tar_bytes = directory_to_tar_bytes(temp_dir_path)
|
|
269
|
-
|
|
270
|
-
with tarfile.open(fileobj=io.BytesIO(tar_bytes), mode="r:*") as tar:
|
|
271
|
-
members = {m.name: m for m in tar.getmembers()}
|
|
272
|
-
|
|
273
|
-
assert "target.txt" in members
|
|
274
|
-
|
|
275
|
-
if has_symlink:
|
|
276
|
-
# Check how symlinks are handled (might be followed or preserved)
|
|
277
|
-
assert "link_to_target.txt" in members
|
hud/utils/tests/test_config.py
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
|
|
5
|
-
from hud.utils.common import FunctionConfig
|
|
6
|
-
from hud.utils.config import (
|
|
7
|
-
_is_list_of_configs,
|
|
8
|
-
_is_valid_python_name,
|
|
9
|
-
_split_and_validate_path,
|
|
10
|
-
_validate_hud_config,
|
|
11
|
-
expand_config,
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@pytest.mark.parametrize(
|
|
16
|
-
"config, expected",
|
|
17
|
-
[
|
|
18
|
-
("test", [{"function": "test", "args": [], "id": None}]),
|
|
19
|
-
(("test",), [{"function": "test", "args": [], "id": None}]),
|
|
20
|
-
(
|
|
21
|
-
[FunctionConfig(function="test", args=[])],
|
|
22
|
-
[{"function": "test", "args": [], "id": None}],
|
|
23
|
-
),
|
|
24
|
-
({"function": "test", "args": []}, [{"function": "test", "args": [], "id": None}]),
|
|
25
|
-
(
|
|
26
|
-
{"function": "test", "args": ["arg1"]},
|
|
27
|
-
[{"function": "test", "args": ["arg1"], "id": None}],
|
|
28
|
-
),
|
|
29
|
-
(
|
|
30
|
-
{"function": "test", "args": ["arg1"], "id": "test_id"},
|
|
31
|
-
[{"function": "test", "args": ["arg1"], "id": "test_id"}],
|
|
32
|
-
),
|
|
33
|
-
(("test", "arg1", "arg2"), [{"function": "test", "args": ["arg1", "arg2"], "id": None}]),
|
|
34
|
-
],
|
|
35
|
-
)
|
|
36
|
-
def test_expand_config(config, expected):
|
|
37
|
-
result = expand_config(config)
|
|
38
|
-
assert len(result) == len(expected)
|
|
39
|
-
for i, item in enumerate(result):
|
|
40
|
-
assert item.function == expected[i]["function"]
|
|
41
|
-
assert item.args == expected[i]["args"]
|
|
42
|
-
assert item.id == expected[i]["id"]
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
@pytest.mark.parametrize(
|
|
46
|
-
"name, expected",
|
|
47
|
-
[
|
|
48
|
-
("valid_name", True),
|
|
49
|
-
("ValidName", True),
|
|
50
|
-
("valid_name_123", True),
|
|
51
|
-
("_valid_name", True),
|
|
52
|
-
("123_invalid", False),
|
|
53
|
-
("invalid-name", False),
|
|
54
|
-
("", False),
|
|
55
|
-
],
|
|
56
|
-
)
|
|
57
|
-
def test_is_valid_python_name(name, expected):
|
|
58
|
-
assert _is_valid_python_name(name) == expected
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def test_validate_hud_config_valid():
|
|
62
|
-
config = {"function": "test.func", "args": ["arg1", "arg2"]}
|
|
63
|
-
result = _validate_hud_config(config)
|
|
64
|
-
assert result.function == "test.func"
|
|
65
|
-
assert result.args == ["arg1", "arg2"]
|
|
66
|
-
assert result.id is None
|
|
67
|
-
|
|
68
|
-
# Test with single arg (not in a list)
|
|
69
|
-
config = {"function": "test.func", "args": "arg1"}
|
|
70
|
-
result = _validate_hud_config(config)
|
|
71
|
-
assert result.function == "test.func"
|
|
72
|
-
assert result.args == ["arg1"]
|
|
73
|
-
|
|
74
|
-
# Test with ID
|
|
75
|
-
config = {"function": "test.func", "args": [], "id": "test_id"}
|
|
76
|
-
result = _validate_hud_config(config)
|
|
77
|
-
assert result.id == "test_id"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def test_validate_hud_config_invalid():
|
|
81
|
-
with pytest.raises(ValueError, match="function must be a string"):
|
|
82
|
-
_validate_hud_config({"args": []})
|
|
83
|
-
|
|
84
|
-
with pytest.raises(ValueError, match="function must be a string"):
|
|
85
|
-
_validate_hud_config({"function": 123, "args": []})
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def test_split_and_validate_path_valid():
|
|
89
|
-
# none should raise
|
|
90
|
-
_split_and_validate_path("module.submodule.function")
|
|
91
|
-
_split_and_validate_path("function")
|
|
92
|
-
_split_and_validate_path("Module_123.function_456")
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def test_split_and_validate_path_invalid():
|
|
96
|
-
with pytest.raises(ValueError, match="Invalid Python identifier in path"):
|
|
97
|
-
_split_and_validate_path("invalid-module.function")
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def test_is_list_of_configs():
|
|
101
|
-
valid_list = [
|
|
102
|
-
FunctionConfig(function="test1", args=[]),
|
|
103
|
-
FunctionConfig(function="test2", args=["arg1"]),
|
|
104
|
-
]
|
|
105
|
-
assert _is_list_of_configs(valid_list) is True
|
|
106
|
-
|
|
107
|
-
# Empty list
|
|
108
|
-
assert _is_list_of_configs([]) is True
|
|
109
|
-
|
|
110
|
-
# Invalid: not a list
|
|
111
|
-
assert _is_list_of_configs("not_a_list") is False
|
|
112
|
-
|
|
113
|
-
# Invalid: list with non-FunctionConfig items
|
|
114
|
-
invalid_list = [FunctionConfig(function="test", args=[]), "not_a_function_config"]
|
|
115
|
-
assert _is_list_of_configs(invalid_list) is False
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def test_expand_config_errors():
|
|
119
|
-
with pytest.raises(ValueError):
|
|
120
|
-
empty_tuple = ()
|
|
121
|
-
expand_config(empty_tuple) # type: ignore
|
|
122
|
-
|
|
123
|
-
with pytest.raises(ValueError):
|
|
124
|
-
invalid_tuple = (123, "arg1")
|
|
125
|
-
expand_config(invalid_tuple) # type: ignore
|
|
126
|
-
|
|
127
|
-
with pytest.raises(ValueError, match="Unknown configuration type"):
|
|
128
|
-
invalid_value = 123
|
|
129
|
-
expand_config(invalid_value) # type: ignore
|