madsci.node_module 0.7.0__tar.gz → 0.7.1__tar.gz
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.
- {madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/PKG-INFO +1 -1
- {madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/madsci/node_module/abstract_node_module.py +94 -0
- {madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/pyproject.toml +1 -1
- {madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/tests/conftest.py +3 -0
- {madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/tests/test_argument_parsing.py +1 -1
- madsci_node_module-0.7.1/tests/test_node_registry.py +215 -0
- {madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/README.md +0 -0
- {madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/madsci/node_module/__init__.py +0 -0
- {madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/madsci/node_module/helpers.py +0 -0
- {madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/madsci/node_module/rest_node_module.py +0 -0
- {madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/madsci/node_module/type_analyzer.py +0 -0
- {madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/tests/test_action_parsing_integration.py +0 -0
- {madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/tests/test_helpers.py +0 -0
- {madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/tests/test_location_argument_serialization.py +0 -0
- {madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/tests/test_node.py +0 -0
- {madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/tests/test_node_infrastructure.py +0 -0
- {madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/tests/test_openapi_schemas.py +0 -0
- {madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/tests/test_rest_functionality.py +0 -0
- {madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/tests/test_rest_node_client.py +0 -0
- {madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/tests/test_rest_node_module.py +0 -0
- {madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/tests/test_rest_utils.py +0 -0
- {madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/tests/test_type_analyzer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: madsci.node_module
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: The Modular Autonomous Discovery for Science (MADSci) Node Module Helper Classes.
|
|
5
5
|
Author-Email: Tobias Ginsburg <tginsburg@anl.gov>, "Ryan D. Lewis" <ryan.lewis@anl.gov>, Casey Stone <cstone@anl.gov>, Doga Ozgulbas <dozgulbas@anl.gov>
|
|
6
6
|
License: MIT
|
{madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/madsci/node_module/abstract_node_module.py
RENAMED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"""Base Node Module helper classes."""
|
|
2
2
|
|
|
3
|
+
import atexit
|
|
3
4
|
import contextlib
|
|
4
5
|
import inspect
|
|
6
|
+
import logging
|
|
5
7
|
import threading
|
|
8
|
+
import weakref
|
|
6
9
|
from pathlib import Path
|
|
7
10
|
from typing import (
|
|
8
11
|
Annotated,
|
|
@@ -115,6 +118,12 @@ class AbstractNode(MadsciClientMixin):
|
|
|
115
118
|
module_name=module_name,
|
|
116
119
|
)
|
|
117
120
|
|
|
121
|
+
# Resolve stable identity from registry if enabled
|
|
122
|
+
self._resolver = None
|
|
123
|
+
self._atexit_registered = False
|
|
124
|
+
if self.config.enable_registry_resolution:
|
|
125
|
+
self._resolve_identity_from_registry()
|
|
126
|
+
|
|
118
127
|
global_ownership_info.node_id = self.node_info.node_id
|
|
119
128
|
self._configure_clients()
|
|
120
129
|
|
|
@@ -136,6 +145,23 @@ class AbstractNode(MadsciClientMixin):
|
|
|
136
145
|
"""Node Lifecycle and Public Methods"""
|
|
137
146
|
"""------------------------------------------------------------------------------------------------"""
|
|
138
147
|
|
|
148
|
+
def close(self) -> None:
|
|
149
|
+
"""Release registry identity and clean up client resources.
|
|
150
|
+
|
|
151
|
+
This method is idempotent and safe to call multiple times.
|
|
152
|
+
Call this before reassigning a node variable to a new node
|
|
153
|
+
with the same name (e.g. in notebook cells) to avoid
|
|
154
|
+
``RegistryLockError`` from lingering heartbeat threads.
|
|
155
|
+
"""
|
|
156
|
+
self._release_registry_identity()
|
|
157
|
+
self._resolver = None
|
|
158
|
+
self.teardown_clients()
|
|
159
|
+
|
|
160
|
+
def __del__(self) -> None:
|
|
161
|
+
"""Best-effort cleanup when the node is garbage-collected."""
|
|
162
|
+
with contextlib.suppress(Exception):
|
|
163
|
+
self.close()
|
|
164
|
+
|
|
139
165
|
def start_node(self) -> None:
|
|
140
166
|
"""
|
|
141
167
|
Called once to start the node.
|
|
@@ -170,6 +196,74 @@ class AbstractNode(MadsciClientMixin):
|
|
|
170
196
|
def shutdown_handler(self) -> None:
|
|
171
197
|
"""Called to shut down the node. Should be used to clean up any resources."""
|
|
172
198
|
|
|
199
|
+
def _resolve_identity_from_registry(self) -> None:
|
|
200
|
+
"""Resolve node identity from the ID Registry.
|
|
201
|
+
|
|
202
|
+
Uses IdentityResolver to look up or create a stable ID for this
|
|
203
|
+
node. The resolved ID is written back to ``self.node_info.node_id``.
|
|
204
|
+
|
|
205
|
+
A ``RegistryLockError`` (after retry exhaustion) is fatal — the
|
|
206
|
+
node cannot start without a stable identity. Other errors
|
|
207
|
+
(e.g. missing registry file) are non-fatal: a warning is logged
|
|
208
|
+
and the generated ID is kept.
|
|
209
|
+
"""
|
|
210
|
+
from madsci.common.registry.identity_resolver import ( # noqa: PLC0415
|
|
211
|
+
IdentityResolver,
|
|
212
|
+
)
|
|
213
|
+
from madsci.common.registry.lock_manager import ( # noqa: PLC0415
|
|
214
|
+
RegistryLockError,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
self._resolver = IdentityResolver(lab_url=self.config.lab_url)
|
|
219
|
+
|
|
220
|
+
node_name = self.node_info.node_name
|
|
221
|
+
|
|
222
|
+
result = self._resolver.resolve_with_info(
|
|
223
|
+
name=node_name,
|
|
224
|
+
component_type="node",
|
|
225
|
+
metadata={"module_name": self.node_info.module_name},
|
|
226
|
+
retry_timeout=self.config.registry_lock_timeout,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
self.node_info.node_id = result.id
|
|
230
|
+
|
|
231
|
+
if not self._atexit_registered:
|
|
232
|
+
ref = weakref.ref(self)
|
|
233
|
+
|
|
234
|
+
def _release_on_exit(weak_ref: weakref.ref = ref) -> None:
|
|
235
|
+
obj = weak_ref()
|
|
236
|
+
if obj is not None:
|
|
237
|
+
obj._release_registry_identity()
|
|
238
|
+
|
|
239
|
+
atexit.register(_release_on_exit)
|
|
240
|
+
self._atexit_registered = True
|
|
241
|
+
|
|
242
|
+
except RegistryLockError:
|
|
243
|
+
raise
|
|
244
|
+
except Exception:
|
|
245
|
+
logging.getLogger(__name__).warning(
|
|
246
|
+
"Registry identity resolution failed; using generated ID",
|
|
247
|
+
exc_info=True,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def _release_registry_identity(self) -> None:
|
|
251
|
+
"""Release the registry lock for this node's identity.
|
|
252
|
+
|
|
253
|
+
Called via ``atexit`` to allow other instances to acquire the
|
|
254
|
+
same name.
|
|
255
|
+
"""
|
|
256
|
+
if self._resolver is None:
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
self._resolver.release(self.node_info.node_name)
|
|
261
|
+
except Exception:
|
|
262
|
+
logging.getLogger(__name__).warning(
|
|
263
|
+
"Failed to release registry identity",
|
|
264
|
+
exc_info=True,
|
|
265
|
+
)
|
|
266
|
+
|
|
173
267
|
"""------------------------------------------------------------------------------------------------"""
|
|
174
268
|
"""Interface Methods"""
|
|
175
269
|
"""------------------------------------------------------------------------------------------------"""
|
|
@@ -117,10 +117,13 @@ def test_node_factory() -> Generator[Callable[..., TestNode], None, None]:
|
|
|
117
117
|
Configured TestNode instance
|
|
118
118
|
"""
|
|
119
119
|
# Create base config with identity fields
|
|
120
|
+
# Disable registry resolution by default in tests to prevent
|
|
121
|
+
# lock contention and side effects from shared registry files.
|
|
120
122
|
config_params = {
|
|
121
123
|
"test_required_param": 1,
|
|
122
124
|
"node_name": node_name,
|
|
123
125
|
"module_name": module_name,
|
|
126
|
+
"enable_registry_resolution": False,
|
|
124
127
|
**config_kwargs,
|
|
125
128
|
}
|
|
126
129
|
if config_overrides:
|
|
@@ -30,7 +30,7 @@ class TestArgumentParsingNode(RestNode):
|
|
|
30
30
|
"""Test node for argument parsing tests."""
|
|
31
31
|
|
|
32
32
|
__test__ = False
|
|
33
|
-
config: TestConfig = TestConfig()
|
|
33
|
+
config: TestConfig = TestConfig(enable_registry_resolution=False)
|
|
34
34
|
config_model = TestConfig
|
|
35
35
|
|
|
36
36
|
def startup_handler(self) -> None:
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Unit tests for registry resolution in AbstractNode."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
from madsci.common.types.node_types import NodeConfig
|
|
6
|
+
from madsci.common.types.registry_types import RegistryResolveResult
|
|
7
|
+
from madsci.common.utils import new_ulid_str
|
|
8
|
+
|
|
9
|
+
from madsci_node_module.tests.test_node import TestNode, TestNodeConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestNodeRegistrySettings:
|
|
13
|
+
"""Test the registry-related fields on NodeConfig."""
|
|
14
|
+
|
|
15
|
+
def test_registry_resolution_enabled_by_default(self) -> None:
|
|
16
|
+
"""Registry resolution is enabled by default in production.
|
|
17
|
+
|
|
18
|
+
Note: The test suite patches the default to False to prevent lock
|
|
19
|
+
contention. We verify explicit True still works.
|
|
20
|
+
"""
|
|
21
|
+
config = NodeConfig(enable_registry_resolution=True)
|
|
22
|
+
assert config.enable_registry_resolution is True
|
|
23
|
+
|
|
24
|
+
def test_lab_url_none_by_default(self) -> None:
|
|
25
|
+
"""Lab URL should be None by default."""
|
|
26
|
+
config = NodeConfig()
|
|
27
|
+
assert config.lab_url is None
|
|
28
|
+
|
|
29
|
+
def test_registry_lock_timeout_default(self) -> None:
|
|
30
|
+
"""Registry lock timeout should default to 60 seconds."""
|
|
31
|
+
config = NodeConfig()
|
|
32
|
+
assert config.registry_lock_timeout == 60.0
|
|
33
|
+
|
|
34
|
+
def test_can_disable_registry_resolution(self) -> None:
|
|
35
|
+
"""Should be able to disable registry resolution."""
|
|
36
|
+
config = NodeConfig(enable_registry_resolution=False)
|
|
37
|
+
assert config.enable_registry_resolution is False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TestNodeRegistryResolution:
|
|
41
|
+
"""Test the registry resolution integration in AbstractNode."""
|
|
42
|
+
|
|
43
|
+
def test_resolver_not_called_when_disabled(self) -> None:
|
|
44
|
+
"""When registry resolution is disabled, resolver should not be called."""
|
|
45
|
+
config = TestNodeConfig(
|
|
46
|
+
test_required_param=1,
|
|
47
|
+
node_name="test_node",
|
|
48
|
+
enable_registry_resolution=False,
|
|
49
|
+
)
|
|
50
|
+
node = TestNode(node_config=config)
|
|
51
|
+
|
|
52
|
+
assert node._resolver is None
|
|
53
|
+
|
|
54
|
+
def test_resolver_called_when_enabled(self) -> None:
|
|
55
|
+
"""When registry resolution is enabled, resolver should update node_info.node_id."""
|
|
56
|
+
resolved_id = new_ulid_str()
|
|
57
|
+
mock_resolver = MagicMock()
|
|
58
|
+
mock_resolver.resolve_with_info.return_value = RegistryResolveResult(
|
|
59
|
+
name="test_node",
|
|
60
|
+
id=resolved_id,
|
|
61
|
+
component_type="node",
|
|
62
|
+
is_new=False,
|
|
63
|
+
source="local",
|
|
64
|
+
)
|
|
65
|
+
mock_resolver_cls = MagicMock(return_value=mock_resolver)
|
|
66
|
+
|
|
67
|
+
config = TestNodeConfig(
|
|
68
|
+
test_required_param=1,
|
|
69
|
+
node_name="test_node",
|
|
70
|
+
enable_registry_resolution=True,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
with patch(
|
|
74
|
+
"madsci.common.registry.identity_resolver.IdentityResolver",
|
|
75
|
+
mock_resolver_cls,
|
|
76
|
+
):
|
|
77
|
+
node = TestNode(node_config=config)
|
|
78
|
+
|
|
79
|
+
assert node.node_info.node_id == resolved_id
|
|
80
|
+
|
|
81
|
+
def test_resolution_failure_falls_back_to_generated_id(self) -> None:
|
|
82
|
+
"""If registry resolution fails, the generated ID should be preserved."""
|
|
83
|
+
config = TestNodeConfig(
|
|
84
|
+
test_required_param=1,
|
|
85
|
+
node_name="test_node",
|
|
86
|
+
enable_registry_resolution=True,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
with patch(
|
|
90
|
+
"madsci.common.registry.identity_resolver.IdentityResolver",
|
|
91
|
+
side_effect=Exception("Connection refused"),
|
|
92
|
+
):
|
|
93
|
+
node = TestNode(node_config=config)
|
|
94
|
+
|
|
95
|
+
# The node should still have a valid node_id (the originally generated one)
|
|
96
|
+
assert node.node_info.node_id is not None
|
|
97
|
+
assert len(node.node_info.node_id) > 0
|
|
98
|
+
|
|
99
|
+
def test_stable_id_across_restarts(self) -> None:
|
|
100
|
+
"""Two sequential node instances with the same name should get the same ID."""
|
|
101
|
+
stable_id = new_ulid_str()
|
|
102
|
+
mock_resolver = MagicMock()
|
|
103
|
+
mock_resolver.resolve_with_info.return_value = RegistryResolveResult(
|
|
104
|
+
name="stable_node",
|
|
105
|
+
id=stable_id,
|
|
106
|
+
component_type="node",
|
|
107
|
+
is_new=False,
|
|
108
|
+
source="local",
|
|
109
|
+
)
|
|
110
|
+
mock_resolver_cls = MagicMock(return_value=mock_resolver)
|
|
111
|
+
|
|
112
|
+
with patch(
|
|
113
|
+
"madsci.common.registry.identity_resolver.IdentityResolver",
|
|
114
|
+
mock_resolver_cls,
|
|
115
|
+
):
|
|
116
|
+
node1 = TestNode(
|
|
117
|
+
node_config=TestNodeConfig(
|
|
118
|
+
test_required_param=1,
|
|
119
|
+
node_name="stable_node",
|
|
120
|
+
enable_registry_resolution=True,
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
node1._release_registry_identity()
|
|
124
|
+
|
|
125
|
+
node2 = TestNode(
|
|
126
|
+
node_config=TestNodeConfig(
|
|
127
|
+
test_required_param=1,
|
|
128
|
+
node_name="stable_node",
|
|
129
|
+
enable_registry_resolution=True,
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
assert node1.node_info.node_id == stable_id
|
|
134
|
+
assert node2.node_info.node_id == stable_id
|
|
135
|
+
|
|
136
|
+
def test_registry_disabled_generates_ulid(self) -> None:
|
|
137
|
+
"""With enable_registry_resolution=False, a fresh ULID should be generated."""
|
|
138
|
+
node1 = TestNode(
|
|
139
|
+
node_config=TestNodeConfig(
|
|
140
|
+
test_required_param=1,
|
|
141
|
+
node_name="ulid_node",
|
|
142
|
+
enable_registry_resolution=False,
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
node2 = TestNode(
|
|
146
|
+
node_config=TestNodeConfig(
|
|
147
|
+
test_required_param=1,
|
|
148
|
+
node_name="ulid_node",
|
|
149
|
+
enable_registry_resolution=False,
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Both should have IDs, but they should be different (fresh ULIDs)
|
|
154
|
+
assert node1.node_info.node_id is not None
|
|
155
|
+
assert node2.node_info.node_id is not None
|
|
156
|
+
assert node1.node_info.node_id != node2.node_info.node_id
|
|
157
|
+
|
|
158
|
+
def test_release_identity_calls_resolver_release(self) -> None:
|
|
159
|
+
"""_release_registry_identity() should call resolver.release()."""
|
|
160
|
+
config = TestNodeConfig(
|
|
161
|
+
test_required_param=1,
|
|
162
|
+
node_name="release_test_node",
|
|
163
|
+
enable_registry_resolution=False,
|
|
164
|
+
)
|
|
165
|
+
node = TestNode(node_config=config)
|
|
166
|
+
node._resolver = MagicMock()
|
|
167
|
+
|
|
168
|
+
node._release_registry_identity()
|
|
169
|
+
|
|
170
|
+
node._resolver.release.assert_called_once_with("release_test_node")
|
|
171
|
+
|
|
172
|
+
def test_release_identity_noop_without_resolver(self) -> None:
|
|
173
|
+
"""_release_registry_identity() should be a no-op when no resolver is set."""
|
|
174
|
+
config = TestNodeConfig(
|
|
175
|
+
test_required_param=1,
|
|
176
|
+
node_name="noop_node",
|
|
177
|
+
enable_registry_resolution=False,
|
|
178
|
+
)
|
|
179
|
+
node = TestNode(node_config=config)
|
|
180
|
+
|
|
181
|
+
# Should not raise
|
|
182
|
+
node._release_registry_identity()
|
|
183
|
+
|
|
184
|
+
def test_node_name_used_for_resolution(self) -> None:
|
|
185
|
+
"""The node name should be used for registry lookup."""
|
|
186
|
+
resolved_id = new_ulid_str()
|
|
187
|
+
mock_resolver = MagicMock()
|
|
188
|
+
mock_resolver.resolve_with_info.return_value = RegistryResolveResult(
|
|
189
|
+
name="custom_node",
|
|
190
|
+
id=resolved_id,
|
|
191
|
+
component_type="node",
|
|
192
|
+
is_new=True,
|
|
193
|
+
source="local",
|
|
194
|
+
)
|
|
195
|
+
mock_resolver_cls = MagicMock(return_value=mock_resolver)
|
|
196
|
+
|
|
197
|
+
config = TestNodeConfig(
|
|
198
|
+
test_required_param=1,
|
|
199
|
+
node_name="custom_node",
|
|
200
|
+
module_name="custom_module",
|
|
201
|
+
enable_registry_resolution=True,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
with patch(
|
|
205
|
+
"madsci.common.registry.identity_resolver.IdentityResolver",
|
|
206
|
+
mock_resolver_cls,
|
|
207
|
+
):
|
|
208
|
+
TestNode(node_config=config)
|
|
209
|
+
|
|
210
|
+
mock_resolver.resolve_with_info.assert_called_once_with(
|
|
211
|
+
name="custom_node",
|
|
212
|
+
component_type="node",
|
|
213
|
+
metadata={"module_name": "custom_module"},
|
|
214
|
+
retry_timeout=60.0,
|
|
215
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/madsci/node_module/rest_node_module.py
RENAMED
|
File without changes
|
|
File without changes
|
{madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/tests/test_action_parsing_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
{madsci_node_module-0.7.0 → madsci_node_module-0.7.1}/tests/test_location_argument_serialization.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|