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.
@@ -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()
@@ -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
+ ]