humalab 0.1.0__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.
- humalab/__init__.py +34 -0
- humalab/assets/__init__.py +10 -0
- humalab/assets/archive.py +101 -0
- humalab/assets/files/__init__.py +4 -0
- humalab/assets/files/resource_file.py +131 -0
- humalab/assets/files/urdf_file.py +103 -0
- humalab/assets/resource_operator.py +139 -0
- humalab/constants.py +50 -0
- humalab/dists/__init__.py +24 -0
- humalab/dists/bernoulli.py +84 -0
- humalab/dists/categorical.py +81 -0
- humalab/dists/discrete.py +103 -0
- humalab/dists/distribution.py +49 -0
- humalab/dists/gaussian.py +96 -0
- humalab/dists/log_uniform.py +97 -0
- humalab/dists/truncated_gaussian.py +129 -0
- humalab/dists/uniform.py +95 -0
- humalab/episode.py +306 -0
- humalab/humalab.py +219 -0
- humalab/humalab_api_client.py +966 -0
- humalab/humalab_config.py +122 -0
- humalab/humalab_test.py +527 -0
- humalab/metrics/__init__.py +17 -0
- humalab/metrics/code.py +59 -0
- humalab/metrics/metric.py +96 -0
- humalab/metrics/scenario_stats.py +163 -0
- humalab/metrics/summary.py +75 -0
- humalab/run.py +325 -0
- humalab/scenarios/__init__.py +11 -0
- humalab/scenarios/scenario.py +375 -0
- humalab/scenarios/scenario_operator.py +114 -0
- humalab/scenarios/scenario_test.py +792 -0
- humalab/utils.py +37 -0
- humalab-0.1.0.dist-info/METADATA +43 -0
- humalab-0.1.0.dist-info/RECORD +39 -0
- humalab-0.1.0.dist-info/WHEEL +5 -0
- humalab-0.1.0.dist-info/entry_points.txt +2 -0
- humalab-0.1.0.dist-info/licenses/LICENSE +21 -0
- humalab-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import yaml
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
class HumalabConfig:
|
|
6
|
+
"""Manages HumaLab SDK configuration settings.
|
|
7
|
+
|
|
8
|
+
Configuration is stored in ~/.humalab/config.yaml and includes workspace path,
|
|
9
|
+
API credentials, and connection settings. Values are automatically loaded on
|
|
10
|
+
initialization and saved when modified through property setters.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
workspace_path (str): The local workspace directory path.
|
|
14
|
+
base_url (str): The HumaLab API base URL.
|
|
15
|
+
api_key (str): The API key for authentication.
|
|
16
|
+
timeout (float): Request timeout in seconds.
|
|
17
|
+
"""
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self._config = {
|
|
20
|
+
"workspace_path": "",
|
|
21
|
+
"base_url": "",
|
|
22
|
+
"api_key": "",
|
|
23
|
+
"timeout": 30.0,
|
|
24
|
+
}
|
|
25
|
+
self._workspace_path = ""
|
|
26
|
+
self._base_url = ""
|
|
27
|
+
self._api_key = ""
|
|
28
|
+
self._timeout = 30.0
|
|
29
|
+
self._load_config()
|
|
30
|
+
|
|
31
|
+
def _load_config(self):
|
|
32
|
+
"""Load configuration from ~/.humalab/config.yaml."""
|
|
33
|
+
home_path = Path.home()
|
|
34
|
+
config_path = home_path / ".humalab" / "config.yaml"
|
|
35
|
+
if not config_path.exists():
|
|
36
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
config_path.touch()
|
|
38
|
+
with open(config_path, "r") as f:
|
|
39
|
+
self._config = yaml.safe_load(f) or {}
|
|
40
|
+
self._workspace_path = os.path.expanduser(self._config["workspace_path"]) if self._config and "workspace_path" in self._config else home_path
|
|
41
|
+
self._base_url = self._config["base_url"] if self._config and "base_url" in self._config else ""
|
|
42
|
+
self._api_key = self._config["api_key"] if self._config and "api_key" in self._config else ""
|
|
43
|
+
self._timeout = self._config["timeout"] if self._config and "timeout" in self._config else 30.0
|
|
44
|
+
|
|
45
|
+
def _save(self) -> None:
|
|
46
|
+
"""Save current configuration to ~/.humalab/config.yaml."""
|
|
47
|
+
yaml.dump(self._config, open(Path.home() / ".humalab" / "config.yaml", "w"))
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def workspace_path(self) -> str:
|
|
51
|
+
"""The local workspace directory path.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
str: The workspace path.
|
|
55
|
+
"""
|
|
56
|
+
return str(self._workspace_path)
|
|
57
|
+
|
|
58
|
+
@workspace_path.setter
|
|
59
|
+
def workspace_path(self, path: str) -> None:
|
|
60
|
+
self._workspace_path = path
|
|
61
|
+
self._config["workspace_path"] = path
|
|
62
|
+
self._save()
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def base_url(self) -> str:
|
|
66
|
+
"""The HumaLab API base URL.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
str: The base URL.
|
|
70
|
+
"""
|
|
71
|
+
return str(self._base_url)
|
|
72
|
+
|
|
73
|
+
@base_url.setter
|
|
74
|
+
def base_url(self, base_url: str) -> None:
|
|
75
|
+
"""Set the HumaLab API base URL and save to config.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
base_url (str): The new base URL.
|
|
79
|
+
"""
|
|
80
|
+
self._base_url = base_url
|
|
81
|
+
self._config["base_url"] = base_url
|
|
82
|
+
self._save()
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def api_key(self) -> str:
|
|
86
|
+
"""The API key for authentication.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
str: The API key.
|
|
90
|
+
"""
|
|
91
|
+
return str(self._api_key)
|
|
92
|
+
|
|
93
|
+
@api_key.setter
|
|
94
|
+
def api_key(self, api_key: str) -> None:
|
|
95
|
+
"""Set the API key and save to config.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
api_key (str): The new API key.
|
|
99
|
+
"""
|
|
100
|
+
self._api_key = api_key
|
|
101
|
+
self._config["api_key"] = api_key
|
|
102
|
+
self._save()
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def timeout(self) -> float:
|
|
106
|
+
"""Request timeout in seconds.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
float: The timeout value.
|
|
110
|
+
"""
|
|
111
|
+
return self._timeout
|
|
112
|
+
|
|
113
|
+
@timeout.setter
|
|
114
|
+
def timeout(self, timeout: float) -> None:
|
|
115
|
+
"""Set the request timeout and save to config.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
timeout (float): The new timeout in seconds.
|
|
119
|
+
"""
|
|
120
|
+
self._timeout = timeout
|
|
121
|
+
self._config["timeout"] = timeout
|
|
122
|
+
self._save()
|
humalab/humalab_test.py
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import patch, MagicMock, Mock
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from humalab.constants import DEFAULT_PROJECT
|
|
6
|
+
from humalab import humalab
|
|
7
|
+
from humalab.run import Run
|
|
8
|
+
from humalab.scenarios.scenario import Scenario
|
|
9
|
+
from humalab.humalab_config import HumalabConfig
|
|
10
|
+
from humalab.humalab_api_client import HumaLabApiClient
|
|
11
|
+
from humalab.humalab_api_client import EpisodeStatus, RunStatus
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HumalabTest(unittest.TestCase):
|
|
15
|
+
"""Unit tests for humalab module functions."""
|
|
16
|
+
|
|
17
|
+
def setUp(self):
|
|
18
|
+
"""Set up test fixtures before each test method."""
|
|
19
|
+
# Reset the global _cur_run before each test
|
|
20
|
+
humalab._cur_run = None
|
|
21
|
+
|
|
22
|
+
def tearDown(self):
|
|
23
|
+
"""Clean up after each test method."""
|
|
24
|
+
# Reset the global _cur_run after each test
|
|
25
|
+
humalab._cur_run = None
|
|
26
|
+
|
|
27
|
+
# Tests for _pull_scenario
|
|
28
|
+
|
|
29
|
+
def test_pull_scenario_should_return_scenario_when_no_scenario_id(self):
|
|
30
|
+
"""Test that _pull_scenario returns scenario when scenario_id is None."""
|
|
31
|
+
# Pre-condition
|
|
32
|
+
client = Mock()
|
|
33
|
+
scenario = {"key": "value"}
|
|
34
|
+
project = "test_project"
|
|
35
|
+
|
|
36
|
+
# In-test
|
|
37
|
+
result = humalab._pull_scenario(client=client, project=project, scenario=scenario, scenario_id=None)
|
|
38
|
+
|
|
39
|
+
# Post-condition
|
|
40
|
+
self.assertEqual(result, scenario)
|
|
41
|
+
client.get_scenario.assert_not_called()
|
|
42
|
+
|
|
43
|
+
def test_pull_scenario_should_fetch_scenario_from_client_when_scenario_id_provided(self):
|
|
44
|
+
"""Test that _pull_scenario fetches from API when scenario_id is provided."""
|
|
45
|
+
# Pre-condition
|
|
46
|
+
client = Mock()
|
|
47
|
+
project = "test_project"
|
|
48
|
+
scenario_id = "test-scenario-id"
|
|
49
|
+
yaml_content = "scenario: test"
|
|
50
|
+
client.get_scenario.return_value = {"yaml_content": yaml_content}
|
|
51
|
+
|
|
52
|
+
# In-test
|
|
53
|
+
result = humalab._pull_scenario(client=client, project=project, scenario=None, scenario_id=scenario_id)
|
|
54
|
+
|
|
55
|
+
# Post-condition
|
|
56
|
+
self.assertEqual(result, yaml_content)
|
|
57
|
+
client.get_scenario.assert_called_once_with(project_name=project, uuid=scenario_id, version=None)
|
|
58
|
+
|
|
59
|
+
def test_pull_scenario_should_prefer_scenario_id_over_scenario(self):
|
|
60
|
+
"""Test that _pull_scenario uses scenario_id even when scenario is provided."""
|
|
61
|
+
# Pre-condition
|
|
62
|
+
client = Mock()
|
|
63
|
+
project = "test_project"
|
|
64
|
+
scenario = {"key": "value"}
|
|
65
|
+
scenario_id = "test-scenario-id"
|
|
66
|
+
yaml_content = "scenario: from_api"
|
|
67
|
+
client.get_scenario.return_value = {"yaml_content": yaml_content}
|
|
68
|
+
|
|
69
|
+
# In-test
|
|
70
|
+
result = humalab._pull_scenario(client=client, project=project, scenario=scenario, scenario_id=scenario_id)
|
|
71
|
+
|
|
72
|
+
# Post-condition
|
|
73
|
+
self.assertEqual(result, yaml_content)
|
|
74
|
+
client.get_scenario.assert_called_once_with(project_name=project, uuid=scenario_id, version=None)
|
|
75
|
+
|
|
76
|
+
# Tests for init context manager
|
|
77
|
+
|
|
78
|
+
@patch('humalab.humalab.HumaLabApiClient')
|
|
79
|
+
@patch('humalab.humalab.HumalabConfig')
|
|
80
|
+
@patch('humalab.humalab.Scenario')
|
|
81
|
+
@patch('humalab.humalab.Run')
|
|
82
|
+
def test_init_should_create_run_with_provided_parameters(self, mock_run_class, mock_scenario_class, mock_config_class, mock_api_client_class):
|
|
83
|
+
"""Test that init() creates a Run with provided parameters."""
|
|
84
|
+
# Pre-condition
|
|
85
|
+
project = "test_project"
|
|
86
|
+
name = "test_name"
|
|
87
|
+
description = "test_description"
|
|
88
|
+
run_id = "test_id"
|
|
89
|
+
tags = ["tag1", "tag2"]
|
|
90
|
+
scenario_data = {"key": "value"}
|
|
91
|
+
|
|
92
|
+
mock_config = Mock()
|
|
93
|
+
mock_config.base_url = "http://localhost:8000"
|
|
94
|
+
mock_config.api_key = "test_key"
|
|
95
|
+
mock_config.timeout = 30.0
|
|
96
|
+
mock_config_class.return_value = mock_config
|
|
97
|
+
|
|
98
|
+
mock_api_client = Mock()
|
|
99
|
+
mock_api_client.create_project.return_value = {"name": project}
|
|
100
|
+
mock_api_client.get_run.return_value = {"run_id": run_id, "name": name, "description": description, "tags": tags}
|
|
101
|
+
mock_api_client_class.return_value = mock_api_client
|
|
102
|
+
|
|
103
|
+
mock_scenario_inst = Mock()
|
|
104
|
+
mock_scenario_class.return_value = mock_scenario_inst
|
|
105
|
+
|
|
106
|
+
mock_run_inst = Mock()
|
|
107
|
+
mock_run_class.return_value = mock_run_inst
|
|
108
|
+
|
|
109
|
+
# In-test
|
|
110
|
+
with humalab.init(
|
|
111
|
+
project=project,
|
|
112
|
+
name=name,
|
|
113
|
+
description=description,
|
|
114
|
+
id=run_id,
|
|
115
|
+
tags=tags,
|
|
116
|
+
scenario=scenario_data
|
|
117
|
+
) as run:
|
|
118
|
+
# Post-condition
|
|
119
|
+
self.assertEqual(run, mock_run_inst)
|
|
120
|
+
mock_run_class.assert_called_once()
|
|
121
|
+
call_kwargs = mock_run_class.call_args.kwargs
|
|
122
|
+
self.assertEqual(call_kwargs['project'], project)
|
|
123
|
+
self.assertEqual(call_kwargs['name'], name)
|
|
124
|
+
self.assertEqual(call_kwargs['description'], description)
|
|
125
|
+
self.assertEqual(call_kwargs['id'], run_id)
|
|
126
|
+
self.assertEqual(call_kwargs['tags'], tags)
|
|
127
|
+
self.assertEqual(call_kwargs['scenario'], mock_scenario_inst)
|
|
128
|
+
|
|
129
|
+
# Verify finish was called
|
|
130
|
+
mock_run_inst.finish.assert_called_once()
|
|
131
|
+
|
|
132
|
+
@patch('humalab.humalab.HumaLabApiClient')
|
|
133
|
+
@patch('humalab.humalab.HumalabConfig')
|
|
134
|
+
@patch('humalab.humalab.Scenario')
|
|
135
|
+
@patch('humalab.humalab.Run')
|
|
136
|
+
def test_init_should_use_config_defaults_when_parameters_not_provided(self, mock_run_class, mock_scenario_class, mock_config_class, mock_api_client_class):
|
|
137
|
+
"""Test that init() uses config defaults when parameters are not provided."""
|
|
138
|
+
# Pre-condition
|
|
139
|
+
mock_config = Mock()
|
|
140
|
+
mock_config.base_url = "http://config:8000"
|
|
141
|
+
mock_config.api_key = "config_key"
|
|
142
|
+
mock_config.timeout = 60.0
|
|
143
|
+
mock_config_class.return_value = mock_config
|
|
144
|
+
|
|
145
|
+
mock_api_client = Mock()
|
|
146
|
+
mock_api_client.create_project.return_value = {"name": DEFAULT_PROJECT}
|
|
147
|
+
mock_api_client.get_run.return_value = {"run_id": "", "name": "", "description": "", "tags": None}
|
|
148
|
+
mock_api_client_class.return_value = mock_api_client
|
|
149
|
+
|
|
150
|
+
mock_scenario_inst = Mock()
|
|
151
|
+
mock_scenario_class.return_value = mock_scenario_inst
|
|
152
|
+
|
|
153
|
+
mock_run_inst = Mock()
|
|
154
|
+
mock_run_class.return_value = mock_run_inst
|
|
155
|
+
|
|
156
|
+
# In-test
|
|
157
|
+
with humalab.init() as run:
|
|
158
|
+
# Post-condition
|
|
159
|
+
call_kwargs = mock_run_class.call_args.kwargs
|
|
160
|
+
self.assertEqual(call_kwargs['project'], DEFAULT_PROJECT)
|
|
161
|
+
self.assertEqual(call_kwargs['name'], "")
|
|
162
|
+
self.assertEqual(call_kwargs['description'], "")
|
|
163
|
+
self.assertIsNotNone(call_kwargs['id']) # UUID generated
|
|
164
|
+
self.assertIsNone(call_kwargs['tags'])
|
|
165
|
+
|
|
166
|
+
mock_run_inst.finish.assert_called_once()
|
|
167
|
+
|
|
168
|
+
@patch('humalab.humalab.HumaLabApiClient')
|
|
169
|
+
@patch('humalab.humalab.HumalabConfig')
|
|
170
|
+
@patch('humalab.humalab.Scenario')
|
|
171
|
+
@patch('humalab.humalab.Run')
|
|
172
|
+
def test_init_should_generate_uuid_when_id_not_provided(self, mock_run_class, mock_scenario_class, mock_config_class, mock_api_client_class):
|
|
173
|
+
"""Test that init() generates a UUID when id is not provided."""
|
|
174
|
+
# Pre-condition
|
|
175
|
+
mock_config = Mock()
|
|
176
|
+
mock_config.base_url = "http://localhost:8000"
|
|
177
|
+
mock_config.api_key = "test_key"
|
|
178
|
+
mock_config.timeout = 30.0
|
|
179
|
+
mock_config_class.return_value = mock_config
|
|
180
|
+
|
|
181
|
+
# Mock HTTP 404 error for get_run (run doesn't exist yet)
|
|
182
|
+
import requests
|
|
183
|
+
http_error = requests.HTTPError()
|
|
184
|
+
http_error.response = Mock()
|
|
185
|
+
http_error.response.status_code = 404
|
|
186
|
+
|
|
187
|
+
mock_api_client = Mock()
|
|
188
|
+
mock_api_client.create_project.return_value = {"name": DEFAULT_PROJECT}
|
|
189
|
+
mock_api_client.get_run.side_effect = http_error
|
|
190
|
+
# Mock create_run to return a valid UUID
|
|
191
|
+
generated_uuid = str(uuid.uuid4())
|
|
192
|
+
mock_api_client.create_run.return_value = {"run_id": generated_uuid, "name": "", "description": "", "tags": None}
|
|
193
|
+
mock_api_client_class.return_value = mock_api_client
|
|
194
|
+
|
|
195
|
+
mock_scenario_inst = Mock()
|
|
196
|
+
mock_scenario_class.return_value = mock_scenario_inst
|
|
197
|
+
|
|
198
|
+
mock_run_inst = Mock()
|
|
199
|
+
mock_run_class.return_value = mock_run_inst
|
|
200
|
+
|
|
201
|
+
# In-test
|
|
202
|
+
with humalab.init() as run:
|
|
203
|
+
# Post-condition
|
|
204
|
+
call_kwargs = mock_run_class.call_args.kwargs
|
|
205
|
+
run_id = call_kwargs['id']
|
|
206
|
+
# Verify it's a valid UUID
|
|
207
|
+
uuid.UUID(run_id) # Will raise ValueError if not valid
|
|
208
|
+
|
|
209
|
+
mock_run_inst.finish.assert_called_once()
|
|
210
|
+
|
|
211
|
+
@patch('humalab.humalab.HumaLabApiClient')
|
|
212
|
+
@patch('humalab.humalab.HumalabConfig')
|
|
213
|
+
@patch('humalab.humalab.Scenario')
|
|
214
|
+
@patch('humalab.humalab.Run')
|
|
215
|
+
def test_init_should_initialize_scenario_with_seed(self, mock_run_class, mock_scenario_class, mock_config_class, mock_api_client_class):
|
|
216
|
+
"""Test that init() initializes scenario with provided seed."""
|
|
217
|
+
# Pre-condition
|
|
218
|
+
seed = 42
|
|
219
|
+
scenario_data = {"key": "value"}
|
|
220
|
+
|
|
221
|
+
mock_config = Mock()
|
|
222
|
+
mock_config.base_url = "http://localhost:8000"
|
|
223
|
+
mock_config.api_key = "test_key"
|
|
224
|
+
mock_config.timeout = 30.0
|
|
225
|
+
mock_config_class.return_value = mock_config
|
|
226
|
+
|
|
227
|
+
mock_api_client = Mock()
|
|
228
|
+
mock_api_client.create_project.return_value = {"name": DEFAULT_PROJECT}
|
|
229
|
+
mock_api_client.get_run.return_value = {"run_id": "", "name": "", "description": "", "tags": None}
|
|
230
|
+
mock_api_client_class.return_value = mock_api_client
|
|
231
|
+
|
|
232
|
+
mock_scenario_inst = Mock()
|
|
233
|
+
mock_scenario_class.return_value = mock_scenario_inst
|
|
234
|
+
|
|
235
|
+
mock_run_inst = Mock()
|
|
236
|
+
mock_run_class.return_value = mock_run_inst
|
|
237
|
+
|
|
238
|
+
# In-test
|
|
239
|
+
with humalab.init(scenario=scenario_data, seed=seed) as run:
|
|
240
|
+
# Post-condition
|
|
241
|
+
mock_scenario_inst.init.assert_called_once()
|
|
242
|
+
call_kwargs = mock_scenario_inst.init.call_args.kwargs
|
|
243
|
+
self.assertEqual(call_kwargs['seed'], seed)
|
|
244
|
+
self.assertEqual(call_kwargs['scenario'], scenario_data)
|
|
245
|
+
|
|
246
|
+
mock_run_inst.finish.assert_called_once()
|
|
247
|
+
|
|
248
|
+
@patch('humalab.humalab.HumaLabApiClient')
|
|
249
|
+
@patch('humalab.humalab.HumalabConfig')
|
|
250
|
+
@patch('humalab.humalab.Scenario')
|
|
251
|
+
@patch('humalab.humalab.Run')
|
|
252
|
+
def test_init_should_pull_scenario_from_api_when_scenario_id_provided(self, mock_run_class, mock_scenario_class, mock_config_class, mock_api_client_class):
|
|
253
|
+
"""Test that init() pulls scenario from API when scenario_id is provided."""
|
|
254
|
+
# Pre-condition
|
|
255
|
+
scenario_id = "test-scenario-id"
|
|
256
|
+
yaml_content = "scenario: from_api"
|
|
257
|
+
|
|
258
|
+
mock_config = Mock()
|
|
259
|
+
mock_config.base_url = "http://localhost:8000"
|
|
260
|
+
mock_config.api_key = "test_key"
|
|
261
|
+
mock_config.timeout = 30.0
|
|
262
|
+
mock_config_class.return_value = mock_config
|
|
263
|
+
|
|
264
|
+
mock_api_client = Mock()
|
|
265
|
+
mock_api_client.create_project.return_value = {"name": DEFAULT_PROJECT}
|
|
266
|
+
mock_api_client.get_run.return_value = {"run_id": "", "name": "", "description": "", "tags": None}
|
|
267
|
+
mock_api_client.get_scenario.return_value = {"yaml_content": yaml_content}
|
|
268
|
+
mock_api_client_class.return_value = mock_api_client
|
|
269
|
+
|
|
270
|
+
mock_scenario_inst = Mock()
|
|
271
|
+
mock_scenario_class.return_value = mock_scenario_inst
|
|
272
|
+
|
|
273
|
+
mock_run_inst = Mock()
|
|
274
|
+
mock_run_class.return_value = mock_run_inst
|
|
275
|
+
|
|
276
|
+
# In-test
|
|
277
|
+
with humalab.init(scenario_id=scenario_id) as run:
|
|
278
|
+
# Post-condition
|
|
279
|
+
mock_api_client.get_scenario.assert_called_once_with(project_name='default', uuid=scenario_id, version=None)
|
|
280
|
+
mock_scenario_inst.init.assert_called_once()
|
|
281
|
+
call_kwargs = mock_scenario_inst.init.call_args.kwargs
|
|
282
|
+
self.assertEqual(call_kwargs['scenario'], yaml_content)
|
|
283
|
+
|
|
284
|
+
mock_run_inst.finish.assert_called_once()
|
|
285
|
+
|
|
286
|
+
@patch('humalab.humalab.HumaLabApiClient')
|
|
287
|
+
@patch('humalab.humalab.HumalabConfig')
|
|
288
|
+
@patch('humalab.humalab.Scenario')
|
|
289
|
+
@patch('humalab.humalab.Run')
|
|
290
|
+
def test_init_should_set_global_cur_run(self, mock_run_class, mock_scenario_class, mock_config_class, mock_api_client_class):
|
|
291
|
+
"""Test that init() sets the global _cur_run variable."""
|
|
292
|
+
# Pre-condition
|
|
293
|
+
mock_config = Mock()
|
|
294
|
+
mock_config.base_url = "http://localhost:8000"
|
|
295
|
+
mock_config.api_key = "test_key"
|
|
296
|
+
mock_config.timeout = 30.0
|
|
297
|
+
mock_config_class.return_value = mock_config
|
|
298
|
+
|
|
299
|
+
mock_api_client = Mock()
|
|
300
|
+
mock_api_client.create_project.return_value = {"name": DEFAULT_PROJECT}
|
|
301
|
+
mock_api_client.get_run.return_value = {"run_id": "", "name": "", "description": "", "tags": None}
|
|
302
|
+
mock_api_client_class.return_value = mock_api_client
|
|
303
|
+
|
|
304
|
+
mock_scenario_inst = Mock()
|
|
305
|
+
mock_scenario_class.return_value = mock_scenario_inst
|
|
306
|
+
|
|
307
|
+
mock_run_inst = Mock()
|
|
308
|
+
mock_run_class.return_value = mock_run_inst
|
|
309
|
+
|
|
310
|
+
# In-test
|
|
311
|
+
self.assertIsNone(humalab._cur_run)
|
|
312
|
+
with humalab.init() as run:
|
|
313
|
+
# Post-condition
|
|
314
|
+
self.assertEqual(humalab._cur_run, mock_run_inst)
|
|
315
|
+
|
|
316
|
+
mock_run_inst.finish.assert_called_once()
|
|
317
|
+
|
|
318
|
+
@patch('humalab.humalab.HumaLabApiClient')
|
|
319
|
+
@patch('humalab.humalab.HumalabConfig')
|
|
320
|
+
@patch('humalab.humalab.Scenario')
|
|
321
|
+
@patch('humalab.humalab.Run')
|
|
322
|
+
def test_init_should_call_finish_on_exception(self, mock_run_class, mock_scenario_class, mock_config_class, mock_api_client_class):
|
|
323
|
+
"""Test that init() calls finish even when exception occurs in context."""
|
|
324
|
+
# Pre-condition
|
|
325
|
+
mock_config = Mock()
|
|
326
|
+
mock_config.base_url = "http://localhost:8000"
|
|
327
|
+
mock_config.api_key = "test_key"
|
|
328
|
+
mock_config.timeout = 30.0
|
|
329
|
+
mock_config_class.return_value = mock_config
|
|
330
|
+
|
|
331
|
+
mock_api_client = Mock()
|
|
332
|
+
mock_api_client.create_project.return_value = {"name": DEFAULT_PROJECT}
|
|
333
|
+
mock_api_client.get_run.return_value = {"run_id": "", "name": "", "description": "", "tags": None}
|
|
334
|
+
mock_api_client_class.return_value = mock_api_client
|
|
335
|
+
|
|
336
|
+
mock_scenario_inst = Mock()
|
|
337
|
+
mock_scenario_class.return_value = mock_scenario_inst
|
|
338
|
+
|
|
339
|
+
mock_run_inst = Mock()
|
|
340
|
+
mock_run_class.return_value = mock_run_inst
|
|
341
|
+
|
|
342
|
+
# In-test & Post-condition
|
|
343
|
+
with self.assertRaises(RuntimeError):
|
|
344
|
+
with humalab.init() as run:
|
|
345
|
+
raise RuntimeError("Test exception")
|
|
346
|
+
|
|
347
|
+
# Verify finish was still called
|
|
348
|
+
mock_run_inst.finish.assert_called_once()
|
|
349
|
+
|
|
350
|
+
@patch('humalab.humalab.HumaLabApiClient')
|
|
351
|
+
@patch('humalab.humalab.HumalabConfig')
|
|
352
|
+
@patch('humalab.humalab.Scenario')
|
|
353
|
+
@patch('humalab.humalab.Run')
|
|
354
|
+
def test_init_should_create_api_client_with_custom_parameters(self, mock_run_class, mock_scenario_class, mock_config_class, mock_api_client_class):
|
|
355
|
+
"""Test that init() creates API client with custom base_url, api_key, and timeout."""
|
|
356
|
+
# Pre-condition
|
|
357
|
+
base_url = "http://custom:9000"
|
|
358
|
+
api_key = "custom_key"
|
|
359
|
+
timeout = 120.0
|
|
360
|
+
|
|
361
|
+
mock_config = Mock()
|
|
362
|
+
mock_config.base_url = "http://localhost:8000"
|
|
363
|
+
mock_config.api_key = "default_key"
|
|
364
|
+
mock_config.timeout = 30.0
|
|
365
|
+
mock_config_class.return_value = mock_config
|
|
366
|
+
|
|
367
|
+
mock_api_client = Mock()
|
|
368
|
+
mock_api_client.create_project.return_value = {"name": DEFAULT_PROJECT}
|
|
369
|
+
mock_api_client.get_run.return_value = {"run_id": "", "name": "", "description": "", "tags": None}
|
|
370
|
+
mock_api_client_class.return_value = mock_api_client
|
|
371
|
+
|
|
372
|
+
mock_scenario_inst = Mock()
|
|
373
|
+
mock_scenario_class.return_value = mock_scenario_inst
|
|
374
|
+
|
|
375
|
+
mock_run_inst = Mock()
|
|
376
|
+
mock_run_class.return_value = mock_run_inst
|
|
377
|
+
|
|
378
|
+
# In-test
|
|
379
|
+
with humalab.init(base_url=base_url, api_key=api_key, timeout=timeout) as run:
|
|
380
|
+
# Post-condition
|
|
381
|
+
mock_api_client_class.assert_called_once_with(
|
|
382
|
+
base_url=base_url,
|
|
383
|
+
api_key=api_key,
|
|
384
|
+
timeout=timeout
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
mock_run_inst.finish.assert_called_once()
|
|
388
|
+
|
|
389
|
+
# Tests for finish function
|
|
390
|
+
|
|
391
|
+
def test_finish_should_call_finish_on_current_run_with_default_status(self):
|
|
392
|
+
"""Test that finish() calls finish on the current run with default status."""
|
|
393
|
+
# Pre-condition
|
|
394
|
+
mock_run = Mock()
|
|
395
|
+
humalab._cur_run = mock_run
|
|
396
|
+
|
|
397
|
+
# In-test
|
|
398
|
+
humalab.finish()
|
|
399
|
+
|
|
400
|
+
# Post-condition
|
|
401
|
+
mock_run.finish.assert_called_once_with(status=RunStatus.FINISHED, err_msg=None)
|
|
402
|
+
|
|
403
|
+
def test_finish_should_call_finish_on_current_run_with_custom_status(self):
|
|
404
|
+
"""Test that finish() calls finish on the current run with custom status."""
|
|
405
|
+
# Pre-condition
|
|
406
|
+
mock_run = Mock()
|
|
407
|
+
humalab._cur_run = mock_run
|
|
408
|
+
status = RunStatus.ERRORED
|
|
409
|
+
|
|
410
|
+
# In-test
|
|
411
|
+
humalab.finish(status=status)
|
|
412
|
+
|
|
413
|
+
# Post-condition
|
|
414
|
+
mock_run.finish.assert_called_once_with(status=status, err_msg=None)
|
|
415
|
+
|
|
416
|
+
def test_finish_should_call_finish_on_current_run_with_err_msg_parameter(self):
|
|
417
|
+
"""Test that finish() calls finish on the current run with err_msg parameter."""
|
|
418
|
+
# Pre-condition
|
|
419
|
+
mock_run = Mock()
|
|
420
|
+
humalab._cur_run = mock_run
|
|
421
|
+
err_msg = "Test error message"
|
|
422
|
+
|
|
423
|
+
# In-test
|
|
424
|
+
humalab.finish(err_msg=err_msg)
|
|
425
|
+
|
|
426
|
+
# Post-condition
|
|
427
|
+
mock_run.finish.assert_called_once_with(status=RunStatus.FINISHED, err_msg=err_msg)
|
|
428
|
+
|
|
429
|
+
def test_finish_should_do_nothing_when_no_current_run(self):
|
|
430
|
+
"""Test that finish() does nothing when _cur_run is None."""
|
|
431
|
+
# Pre-condition
|
|
432
|
+
humalab._cur_run = None
|
|
433
|
+
|
|
434
|
+
# In-test
|
|
435
|
+
humalab.finish() # Should not raise any exception
|
|
436
|
+
|
|
437
|
+
# Post-condition
|
|
438
|
+
# No exception means success
|
|
439
|
+
self.assertIsNone(humalab._cur_run)
|
|
440
|
+
|
|
441
|
+
# Tests for login function
|
|
442
|
+
|
|
443
|
+
@patch('humalab.humalab.HumalabConfig')
|
|
444
|
+
def test_login_should_set_api_key_when_provided(self, mock_config_class):
|
|
445
|
+
"""Test that login() sets the api_key when provided."""
|
|
446
|
+
# Pre-condition
|
|
447
|
+
mock_config = Mock()
|
|
448
|
+
mock_config.api_key = "old_key"
|
|
449
|
+
mock_config.base_url = "http://localhost:8000"
|
|
450
|
+
mock_config.timeout = 30.0
|
|
451
|
+
mock_config_class.return_value = mock_config
|
|
452
|
+
|
|
453
|
+
new_key = "new_api_key"
|
|
454
|
+
|
|
455
|
+
# In-test
|
|
456
|
+
result = humalab.login(api_key=new_key)
|
|
457
|
+
|
|
458
|
+
# Post-condition
|
|
459
|
+
self.assertTrue(result)
|
|
460
|
+
self.assertEqual(mock_config.api_key, new_key)
|
|
461
|
+
|
|
462
|
+
@patch('humalab.humalab.HumalabConfig')
|
|
463
|
+
def test_login_should_keep_existing_key_when_not_provided(self, mock_config_class):
|
|
464
|
+
"""Test that login() keeps existing api_key when key is not provided."""
|
|
465
|
+
# Pre-condition
|
|
466
|
+
existing_key = "existing_key"
|
|
467
|
+
existing_url = "http://localhost:8000"
|
|
468
|
+
existing_timeout = 30.0
|
|
469
|
+
mock_config = Mock()
|
|
470
|
+
mock_config.api_key = existing_key
|
|
471
|
+
mock_config.base_url = existing_url
|
|
472
|
+
mock_config.timeout = existing_timeout
|
|
473
|
+
mock_config_class.return_value = mock_config
|
|
474
|
+
|
|
475
|
+
# In-test
|
|
476
|
+
result = humalab.login()
|
|
477
|
+
|
|
478
|
+
# Post-condition
|
|
479
|
+
self.assertTrue(result)
|
|
480
|
+
self.assertEqual(mock_config.api_key, existing_key)
|
|
481
|
+
self.assertEqual(mock_config.base_url, existing_url)
|
|
482
|
+
self.assertEqual(mock_config.timeout, existing_timeout)
|
|
483
|
+
|
|
484
|
+
@patch('humalab.humalab.HumalabConfig')
|
|
485
|
+
def test_login_should_return_true(self, mock_config_class):
|
|
486
|
+
"""Test that login() always returns True."""
|
|
487
|
+
# Pre-condition
|
|
488
|
+
mock_config = Mock()
|
|
489
|
+
mock_config.api_key = "test_key"
|
|
490
|
+
mock_config.base_url = "http://localhost:8000"
|
|
491
|
+
mock_config.timeout = 30.0
|
|
492
|
+
mock_config_class.return_value = mock_config
|
|
493
|
+
|
|
494
|
+
# In-test
|
|
495
|
+
result = humalab.login()
|
|
496
|
+
|
|
497
|
+
# Post-condition
|
|
498
|
+
self.assertTrue(result)
|
|
499
|
+
|
|
500
|
+
@patch('humalab.humalab.HumalabConfig')
|
|
501
|
+
def test_login_should_accept_optional_parameters(self, mock_config_class):
|
|
502
|
+
"""Test that login() accepts optional parameters without errors."""
|
|
503
|
+
# Pre-condition
|
|
504
|
+
mock_config = Mock()
|
|
505
|
+
mock_config.api_key = "old_key"
|
|
506
|
+
mock_config.base_url = "http://old:8000"
|
|
507
|
+
mock_config.timeout = 30.0
|
|
508
|
+
mock_config_class.return_value = mock_config
|
|
509
|
+
|
|
510
|
+
# In-test
|
|
511
|
+
result = humalab.login(
|
|
512
|
+
api_key="test_key",
|
|
513
|
+
relogin=True,
|
|
514
|
+
host="http://localhost:8000",
|
|
515
|
+
force=True,
|
|
516
|
+
timeout=60.0
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# Post-condition
|
|
520
|
+
self.assertTrue(result)
|
|
521
|
+
self.assertEqual(mock_config.api_key, "test_key")
|
|
522
|
+
self.assertEqual(mock_config.base_url, "http://localhost:8000")
|
|
523
|
+
self.assertEqual(mock_config.timeout, 60.0)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
if __name__ == "__main__":
|
|
527
|
+
unittest.main()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Metrics tracking and management.
|
|
2
|
+
|
|
3
|
+
This module provides classes for tracking various types of metrics during runs and episodes,
|
|
4
|
+
including general metrics, summary statistics, code artifacts, and scenario statistics.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .metric import Metrics
|
|
8
|
+
from .code import Code
|
|
9
|
+
from .scenario_stats import ScenarioStats
|
|
10
|
+
from .summary import Summary
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"Code",
|
|
14
|
+
"Metrics",
|
|
15
|
+
"ScenarioStats",
|
|
16
|
+
"Summary",
|
|
17
|
+
]
|