ni.datastore 0.1.0.dev2__tar.gz → 0.1.0.dev3__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.
Files changed (33) hide show
  1. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/PKG-INFO +5 -5
  2. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/pyproject.toml +5 -5
  3. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/data/_data_store_client.py +68 -15
  4. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/data/_grpc_conversion.py +11 -5
  5. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/data/_types/_published_measurement.py +37 -17
  6. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/data/_types/_step.py +11 -4
  7. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/data/_types/_test_result.py +38 -16
  8. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/metadata/_metadata_store_client.py +63 -18
  9. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/metadata/_types/_hardware_item.py +11 -4
  10. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/metadata/_types/_operator.py +11 -4
  11. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/metadata/_types/_software_item.py +11 -4
  12. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/metadata/_types/_test.py +11 -4
  13. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/metadata/_types/_test_adapter.py +11 -4
  14. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/metadata/_types/_test_description.py +11 -4
  15. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/metadata/_types/_test_station.py +11 -4
  16. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/metadata/_types/_uut.py +21 -7
  17. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/metadata/_types/_uut_instance.py +11 -4
  18. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/LICENSE +0 -0
  19. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/README.md +0 -0
  20. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/__init__.py +0 -0
  21. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/data/__init__.py +0 -0
  22. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/data/_types/__init__.py +0 -0
  23. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/data/_types/_published_condition.py +0 -0
  24. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/data/_types/py.typed +0 -0
  25. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/data/py.typed +0 -0
  26. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/metadata/__init__.py +0 -0
  27. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/metadata/_grpc_conversion.py +0 -0
  28. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/metadata/_types/__init__.py +0 -0
  29. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/metadata/_types/_alias.py +0 -0
  30. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/metadata/_types/_extension_schema.py +0 -0
  31. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/metadata/_types/py.typed +0 -0
  32. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/metadata/py.typed +0 -0
  33. {ni_datastore-0.1.0.dev2 → ni_datastore-0.1.0.dev3}/src/ni/datastore/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ni.datastore
3
- Version: 0.1.0.dev2
3
+ Version: 0.1.0.dev3
4
4
  Summary: APIs for publishing and retrieving data from the NI Measurement Data Store
5
5
  License: MIT
6
6
  Keywords: datastore
@@ -24,10 +24,10 @@ Classifier: Programming Language :: Python :: 3.12
24
24
  Classifier: Programming Language :: Python :: 3.13
25
25
  Classifier: Programming Language :: Python :: Implementation :: CPython
26
26
  Requires-Dist: hightime (>=0.3.0.dev0)
27
- Requires-Dist: ni-datamonikers-v1-client (>=0.1.0.dev0)
28
- Requires-Dist: ni-measurements-data-v1-client (>=0.1.0.dev0)
29
- Requires-Dist: ni-measurements-metadata-v1-client (>=0.1.0.dev0)
30
- Requires-Dist: ni-protobuf-types (>=0.1.0.dev3)
27
+ Requires-Dist: ni-datamonikers-v1-client (>=0.1.0.dev1)
28
+ Requires-Dist: ni-measurements-data-v1-client (>=0.1.0.dev1)
29
+ Requires-Dist: ni-measurements-metadata-v1-client (>=0.1.0.dev1)
30
+ Requires-Dist: ni-protobuf-types (>=1.0.1.dev0)
31
31
  Requires-Dist: protobuf (>=4.21)
32
32
  Description-Content-Type: text/markdown
33
33
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ni.datastore"
3
- version = "0.1.0-dev2"
3
+ version = "0.1.0-dev3"
4
4
  license = "MIT"
5
5
  description = "APIs for publishing and retrieving data from the NI Measurement Data Store"
6
6
  authors = [{name = "NI", email = "opensource@ni.com"}]
@@ -37,10 +37,10 @@ requires-poetry = '>=2.1,<3.0'
37
37
  [tool.poetry.dependencies]
38
38
  python = "^3.9"
39
39
  protobuf = {version=">=4.21"}
40
- ni-datamonikers-v1-client = { version = ">=0.1.0.dev0", allow-prereleases = true }
41
- ni-measurements-data-v1-client = { version = ">=0.1.0.dev0", allow-prereleases = true }
42
- ni-measurements-metadata-v1-client = { version = ">=0.1.0.dev0", allow-prereleases = true }
43
- ni-protobuf-types = { version = ">=0.1.0.dev3", allow-prereleases = true }
40
+ ni-datamonikers-v1-client = { version = ">=0.1.0.dev1", allow-prereleases = true }
41
+ ni-measurements-data-v1-client = { version = ">=0.1.0.dev1", allow-prereleases = true }
42
+ ni-measurements-metadata-v1-client = { version = ">=0.1.0.dev1", allow-prereleases = true }
43
+ ni-protobuf-types = { version = ">=1.0.1.dev0", allow-prereleases = true }
44
44
  hightime = { version = ">=0.3.0.dev0", allow-prereleases = true }
45
45
 
46
46
  [tool.poetry.group.dev.dependencies]
@@ -3,9 +3,11 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
- from collections.abc import Iterable
6
+ import sys
7
+ from collections.abc import Iterable, Sequence
7
8
  from threading import Lock
8
- from typing import Type, TypeVar, overload
9
+ from types import TracebackType
10
+ from typing import TYPE_CHECKING, Type, TypeVar, overload
9
11
  from urllib.parse import urlparse
10
12
 
11
13
  import hightime as ht
@@ -48,6 +50,12 @@ from ni.protobuf.types.precision_timestamp_conversion import (
48
50
  )
49
51
  from ni_grpc_extensions.channelpool import GrpcChannelPool
50
52
 
53
+ if TYPE_CHECKING:
54
+ if sys.version_info >= (3, 11):
55
+ from typing import Self
56
+ else:
57
+ from typing_extensions import Self
58
+
51
59
  TRead = TypeVar("TRead")
52
60
 
53
61
  _logger = logging.getLogger(__name__)
@@ -57,6 +65,7 @@ class DataStoreClient:
57
65
  """Data store client for publishing and reading data."""
58
66
 
59
67
  __slots__ = (
68
+ "_closed",
60
69
  "_discovery_client",
61
70
  "_grpc_channel",
62
71
  "_grpc_channel_pool",
@@ -66,6 +75,9 @@ class DataStoreClient:
66
75
  "_moniker_clients_lock",
67
76
  )
68
77
 
78
+ _DATA_STORE_CLIENT_CLOSED_ERROR = "This DataStoreClient has been closed. Create a new DataStoreClient for further interaction with the data store."
79
+
80
+ _closed: bool
69
81
  _discovery_client: DiscoveryClient | None
70
82
  _grpc_channel: Channel | None
71
83
  _grpc_channel_pool: GrpcChannelPool | None
@@ -102,6 +114,35 @@ class DataStoreClient:
102
114
  self._data_store_client_lock = Lock()
103
115
  self._moniker_clients_lock = Lock()
104
116
 
117
+ self._closed = False
118
+
119
+ def __enter__(self) -> Self:
120
+ """Enter the runtime context of the data store client."""
121
+ return self
122
+
123
+ def __exit__(
124
+ self,
125
+ exc_type: type[BaseException] | None,
126
+ exc_val: BaseException | None,
127
+ traceback: TracebackType | None,
128
+ ) -> None:
129
+ """Exit the runtime context of the data store client."""
130
+ self.close()
131
+
132
+ def close(self) -> None:
133
+ """Close the data store client and clean up resources that it owns."""
134
+ self._closed = True
135
+
136
+ with self._data_store_client_lock:
137
+ if self._data_store_client is not None:
138
+ self._data_store_client.close()
139
+ self._data_store_client = None
140
+
141
+ with self._moniker_clients_lock:
142
+ for _, moniker_client in self._moniker_clients_by_service_location.items():
143
+ moniker_client.close()
144
+ self._moniker_clients_by_service_location.clear()
145
+
105
146
  def publish_condition(
106
147
  self,
107
148
  condition_name: str,
@@ -174,7 +215,7 @@ class DataStoreClient:
174
215
  hardware_item_ids: Iterable[str] = tuple(),
175
216
  test_adapter_ids: Iterable[str] = tuple(),
176
217
  software_item_ids: Iterable[str] = tuple(),
177
- ) -> Iterable[PublishedMeasurement]:
218
+ ) -> Sequence[PublishedMeasurement]:
178
219
  """Publish a batch of N values of a measurement to the data store."""
179
220
  publish_request = PublishMeasurementBatchRequest(
180
221
  measurement_name=measurement_name,
@@ -253,7 +294,7 @@ class DataStoreClient:
253
294
  get_response = self._get_data_store_client().get_test_result(get_request)
254
295
  return TestResult.from_protobuf(get_response.test_result)
255
296
 
256
- def query_conditions(self, odata_query: str) -> Iterable[PublishedCondition]:
297
+ def query_conditions(self, odata_query: str = "") -> Sequence[PublishedCondition]:
257
298
  """Query conditions from the data store."""
258
299
  query_request = QueryConditionsRequest(odata_query=odata_query)
259
300
  query_response = self._get_data_store_client().query_conditions(query_request)
@@ -262,7 +303,7 @@ class DataStoreClient:
262
303
  for published_condition in query_response.published_conditions
263
304
  ]
264
305
 
265
- def query_measurements(self, odata_query: str) -> Iterable[PublishedMeasurement]:
306
+ def query_measurements(self, odata_query: str = "") -> Sequence[PublishedMeasurement]:
266
307
  """Query measurements from the data store."""
267
308
  query_request = QueryMeasurementsRequest(odata_query=odata_query)
268
309
  query_response = self._get_data_store_client().query_measurements(query_request)
@@ -271,32 +312,44 @@ class DataStoreClient:
271
312
  for published_measurement in query_response.published_measurements
272
313
  ]
273
314
 
274
- def query_steps(self, odata_query: str) -> Iterable[Step]:
315
+ def query_steps(self, odata_query: str = "") -> Sequence[Step]:
275
316
  """Query steps from the data store."""
276
317
  query_request = QueryStepsRequest(odata_query=odata_query)
277
318
  query_response = self._get_data_store_client().query_steps(query_request)
278
319
  return [Step.from_protobuf(step) for step in query_response.steps]
279
320
 
280
321
  def _get_data_store_client(self) -> DataStoreServiceClient:
322
+ if self._closed:
323
+ raise RuntimeError(self._DATA_STORE_CLIENT_CLOSED_ERROR)
324
+
281
325
  if self._data_store_client is None:
282
326
  with self._data_store_client_lock:
283
327
  if self._data_store_client is None:
284
- self._data_store_client = DataStoreServiceClient(
285
- discovery_client=self._discovery_client,
286
- grpc_channel=self._grpc_channel,
287
- grpc_channel_pool=self._grpc_channel_pool,
288
- )
328
+ self._data_store_client = self._instantiate_data_store_client()
289
329
  return self._data_store_client
290
330
 
331
+ def _instantiate_data_store_client(self) -> DataStoreServiceClient:
332
+ return DataStoreServiceClient(
333
+ discovery_client=self._discovery_client,
334
+ grpc_channel=self._grpc_channel,
335
+ grpc_channel_pool=self._grpc_channel_pool,
336
+ )
337
+
291
338
  def _get_moniker_client(self, service_location: str) -> MonikerClient:
339
+ if self._closed:
340
+ raise RuntimeError(self._DATA_STORE_CLIENT_CLOSED_ERROR)
341
+
292
342
  parsed_service_location = urlparse(service_location).netloc
293
343
  if parsed_service_location not in self._moniker_clients_by_service_location:
294
344
  with self._moniker_clients_lock:
295
345
  if parsed_service_location not in self._moniker_clients_by_service_location:
296
346
  self._moniker_clients_by_service_location[parsed_service_location] = (
297
- MonikerClient(
298
- service_location=parsed_service_location,
299
- grpc_channel_pool=self._grpc_channel_pool,
300
- )
347
+ self._instantiate_moniker_client(parsed_service_location)
301
348
  )
302
349
  return self._moniker_clients_by_service_location[parsed_service_location]
350
+
351
+ def _instantiate_moniker_client(self, parsed_service_location: str) -> MonikerClient:
352
+ return MonikerClient(
353
+ service_location=parsed_service_location,
354
+ grpc_channel_pool=self._grpc_channel_pool,
355
+ )
@@ -44,11 +44,16 @@ from ni.protobuf.types.waveform_pb2 import (
44
44
  I16AnalogWaveform,
45
45
  I16ComplexWaveform,
46
46
  )
47
+ from ni.protobuf.types.xydata_conversion import (
48
+ float64_xydata_from_protobuf,
49
+ float64_xydata_to_protobuf,
50
+ )
47
51
  from ni.protobuf.types.xydata_pb2 import DoubleXYData
48
52
  from nitypes.complex import ComplexInt32DType
49
53
  from nitypes.scalar import Scalar
50
54
  from nitypes.vector import Vector
51
55
  from nitypes.waveform import AnalogWaveform, ComplexWaveform, DigitalWaveform, Spectrum
56
+ from nitypes.xy_data import XYData
52
57
 
53
58
  _logger = logging.getLogger(__name__)
54
59
 
@@ -137,6 +142,11 @@ def populate_publish_measurement_request_value(
137
142
  raise TypeError(f"Unsupported Spectrum dtype: {value.dtype}")
138
143
  elif isinstance(value, DigitalWaveform):
139
144
  publish_request.digital_waveform.CopyFrom(digital_waveform_to_protobuf(value))
145
+ elif isinstance(value, XYData):
146
+ if value.dtype == np.float64:
147
+ publish_request.x_y_data.CopyFrom(float64_xydata_to_protobuf(value))
148
+ else:
149
+ raise TypeError(f"Unsupported XYData dtype: {value.dtype}")
140
150
  elif isinstance(value, Iterable):
141
151
  if not value:
142
152
  raise ValueError("Cannot publish an empty Iterable.")
@@ -152,7 +162,6 @@ def populate_publish_measurement_request_value(
152
162
  raise TypeError(
153
163
  f"Unsupported measurement value type: {type(value)}. Please consult the documentation."
154
164
  )
155
- # TODO: Implement conversion from proper XYData type
156
165
 
157
166
 
158
167
  def populate_publish_measurement_batch_request_values(
@@ -208,10 +217,7 @@ def unpack_and_convert_from_protobuf_any(read_value: Any) -> object:
208
217
  elif value_type == DoubleXYData.DESCRIPTOR.full_name:
209
218
  xydata = DoubleXYData()
210
219
  read_value.Unpack(xydata)
211
- _logger.warning(
212
- "DoubleXYData conversion is not yet implemented. Returning the raw protobuf object."
213
- )
214
- return xydata
220
+ return float64_xydata_from_protobuf(xydata)
215
221
  elif value_type == VectorProto.DESCRIPTOR.full_name:
216
222
  vector = VectorProto()
217
223
  read_value.Unpack(vector)
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Iterable
5
+ from typing import Iterable, MutableSequence
6
6
 
7
7
  import hightime as ht
8
8
  from ni.datamonikers.v1.data_moniker_pb2 import Moniker
@@ -23,13 +23,13 @@ class PublishedMeasurement:
23
23
 
24
24
  __slots__ = (
25
25
  "moniker",
26
- "published_conditions",
26
+ "_published_conditions",
27
27
  "published_measurement_id",
28
28
  "test_result_id",
29
29
  "step_id",
30
- "software_item_ids",
31
- "hardware_item_ids",
32
- "test_adapter_ids",
30
+ "_software_item_ids",
31
+ "_hardware_item_ids",
32
+ "_test_adapter_ids",
33
33
  "measurement_name",
34
34
  "data_type",
35
35
  "measurement_notes",
@@ -40,6 +40,26 @@ class PublishedMeasurement:
40
40
  "error_information",
41
41
  )
42
42
 
43
+ @property
44
+ def published_conditions(self) -> MutableSequence[PublishedCondition]:
45
+ """The published conditions associated with the published measurement."""
46
+ return self._published_conditions
47
+
48
+ @property
49
+ def software_item_ids(self) -> MutableSequence[str]:
50
+ """The software item IDs associated with the published measurement."""
51
+ return self._software_item_ids
52
+
53
+ @property
54
+ def hardware_item_ids(self) -> MutableSequence[str]:
55
+ """The hardware item IDs associated with the published measurement."""
56
+ return self._hardware_item_ids
57
+
58
+ @property
59
+ def test_adapter_ids(self) -> MutableSequence[str]:
60
+ """The test adapter IDs associated with the published measurement."""
61
+ return self._test_adapter_ids
62
+
43
63
  def __init__(
44
64
  self,
45
65
  *,
@@ -62,20 +82,20 @@ class PublishedMeasurement:
62
82
  ) -> None:
63
83
  """Initialize a PublishedMeasurement instance."""
64
84
  self.moniker = moniker
65
- self.published_conditions: Iterable[PublishedCondition] = (
66
- published_conditions if published_conditions is not None else []
85
+ self._published_conditions: MutableSequence[PublishedCondition] = (
86
+ list(published_conditions) if published_conditions is not None else []
67
87
  )
68
88
  self.published_measurement_id = published_measurement_id
69
89
  self.test_result_id = test_result_id
70
90
  self.step_id = step_id
71
- self.software_item_ids: Iterable[str] = (
72
- software_item_ids if software_item_ids is not None else []
91
+ self._software_item_ids: MutableSequence[str] = (
92
+ list(software_item_ids) if software_item_ids is not None else []
73
93
  )
74
- self.hardware_item_ids: Iterable[str] = (
75
- hardware_item_ids if hardware_item_ids is not None else []
94
+ self._hardware_item_ids: MutableSequence[str] = (
95
+ list(hardware_item_ids) if hardware_item_ids is not None else []
76
96
  )
77
- self.test_adapter_ids: Iterable[str] = (
78
- test_adapter_ids if test_adapter_ids is not None else []
97
+ self._test_adapter_ids: MutableSequence[str] = (
98
+ list(test_adapter_ids) if test_adapter_ids is not None else []
79
99
  )
80
100
  self.measurement_name = measurement_name
81
101
  self.data_type = data_type
@@ -166,13 +186,13 @@ class PublishedMeasurement:
166
186
  return NotImplemented
167
187
  return (
168
188
  self.moniker == other.moniker
169
- and list(self.published_conditions) == list(other.published_conditions)
189
+ and self.published_conditions == other.published_conditions
170
190
  and self.published_measurement_id == other.published_measurement_id
171
191
  and self.test_result_id == other.test_result_id
172
192
  and self.step_id == other.step_id
173
- and list(self.software_item_ids) == list(other.software_item_ids)
174
- and list(self.hardware_item_ids) == list(other.hardware_item_ids)
175
- and list(self.test_adapter_ids) == list(other.test_adapter_ids)
193
+ and self.software_item_ids == other.software_item_ids
194
+ and self.hardware_item_ids == other.hardware_item_ids
195
+ and self.test_adapter_ids == other.test_adapter_ids
176
196
  and self.measurement_name == other.measurement_name
177
197
  and self.data_type == other.data_type
178
198
  and self.measurement_notes == other.measurement_notes
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import MutableMapping
5
+ from typing import Mapping, MutableMapping
6
6
 
7
7
  import hightime as ht
8
8
  from ni.datastore.metadata._grpc_conversion import (
@@ -32,7 +32,7 @@ class Step:
32
32
  "_start_date_time",
33
33
  "_end_date_time",
34
34
  "link",
35
- "extensions",
35
+ "_extensions",
36
36
  "schema_id",
37
37
  )
38
38
 
@@ -46,6 +46,11 @@ class Step:
46
46
  """Get the end date and time of the step execution."""
47
47
  return self._end_date_time
48
48
 
49
+ @property
50
+ def extensions(self) -> MutableMapping[str, str]:
51
+ """The extensions of the step."""
52
+ return self._extensions
53
+
49
54
  def __init__(
50
55
  self,
51
56
  *,
@@ -57,7 +62,7 @@ class Step:
57
62
  step_type: str = "",
58
63
  notes: str = "",
59
64
  link: str = "",
60
- extensions: MutableMapping[str, str] | None = None,
65
+ extensions: Mapping[str, str] | None = None,
61
66
  schema_id: str = "",
62
67
  ) -> None:
63
68
  """Initialize a Step instance."""
@@ -69,7 +74,9 @@ class Step:
69
74
  self.step_type = step_type
70
75
  self.notes = notes
71
76
  self.link = link
72
- self.extensions: MutableMapping[str, str] = extensions if extensions is not None else {}
77
+ self._extensions: MutableMapping[str, str] = (
78
+ dict(extensions) if extensions is not None else {}
79
+ )
73
80
  self.schema_id = schema_id
74
81
 
75
82
  self._start_date_time: ht.datetime | None = None
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Iterable, MutableMapping
5
+ from typing import Iterable, Mapping, MutableMapping, MutableSequence
6
6
 
7
7
  import hightime as ht
8
8
  from ni.datastore.metadata._grpc_conversion import (
@@ -28,15 +28,15 @@ class TestResult:
28
28
  "operator_id",
29
29
  "test_station_id",
30
30
  "test_description_id",
31
- "software_item_ids",
32
- "hardware_item_ids",
33
- "test_adapter_ids",
31
+ "_software_item_ids",
32
+ "_hardware_item_ids",
33
+ "_test_adapter_ids",
34
34
  "test_result_name",
35
35
  "_start_date_time",
36
36
  "_end_date_time",
37
37
  "_outcome",
38
38
  "link",
39
- "extensions",
39
+ "_extensions",
40
40
  "schema_id",
41
41
  )
42
42
 
@@ -55,6 +55,26 @@ class TestResult:
55
55
  """Get the outcome of the test execution."""
56
56
  return self._outcome
57
57
 
58
+ @property
59
+ def software_item_ids(self) -> MutableSequence[str]:
60
+ """The software item IDs associated with the test result."""
61
+ return self._software_item_ids
62
+
63
+ @property
64
+ def hardware_item_ids(self) -> MutableSequence[str]:
65
+ """The hardware item IDs associated with the test result."""
66
+ return self._hardware_item_ids
67
+
68
+ @property
69
+ def test_adapter_ids(self) -> MutableSequence[str]:
70
+ """The test adapter IDs associated with the test result."""
71
+ return self._test_adapter_ids
72
+
73
+ @property
74
+ def extensions(self) -> MutableMapping[str, str]:
75
+ """The extensions of the test result."""
76
+ return self._extensions
77
+
58
78
  def __init__(
59
79
  self,
60
80
  *,
@@ -68,7 +88,7 @@ class TestResult:
68
88
  test_adapter_ids: Iterable[str] | None = None,
69
89
  test_result_name: str = "",
70
90
  link: str = "",
71
- extensions: MutableMapping[str, str] | None = None,
91
+ extensions: Mapping[str, str] | None = None,
72
92
  schema_id: str = "",
73
93
  ) -> None:
74
94
  """Initialize a TestResult instance."""
@@ -77,18 +97,20 @@ class TestResult:
77
97
  self.operator_id = operator_id
78
98
  self.test_station_id = test_station_id
79
99
  self.test_description_id = test_description_id
80
- self.software_item_ids: Iterable[str] = (
81
- software_item_ids if software_item_ids is not None else []
100
+ self._software_item_ids: MutableSequence[str] = (
101
+ list(software_item_ids) if software_item_ids is not None else []
82
102
  )
83
- self.hardware_item_ids: Iterable[str] = (
84
- hardware_item_ids if hardware_item_ids is not None else []
103
+ self._hardware_item_ids: MutableSequence[str] = (
104
+ list(hardware_item_ids) if hardware_item_ids is not None else []
85
105
  )
86
- self.test_adapter_ids: Iterable[str] = (
87
- test_adapter_ids if test_adapter_ids is not None else []
106
+ self._test_adapter_ids: MutableSequence[str] = (
107
+ list(test_adapter_ids) if test_adapter_ids is not None else []
88
108
  )
89
109
  self.test_result_name = test_result_name
90
110
  self.link = link
91
- self.extensions: MutableMapping[str, str] = extensions if extensions is not None else {}
111
+ self._extensions: MutableMapping[str, str] = (
112
+ dict(extensions) if extensions is not None else {}
113
+ )
92
114
  self.schema_id = schema_id
93
115
 
94
116
  self._start_date_time: ht.datetime | None = None
@@ -164,9 +186,9 @@ class TestResult:
164
186
  and self.operator_id == other.operator_id
165
187
  and self.test_station_id == other.test_station_id
166
188
  and self.test_description_id == other.test_description_id
167
- and list(self.software_item_ids) == list(other.software_item_ids)
168
- and list(self.hardware_item_ids) == list(other.hardware_item_ids)
169
- and list(self.test_adapter_ids) == list(other.test_adapter_ids)
189
+ and self.software_item_ids == other.software_item_ids
190
+ and self.hardware_item_ids == other.hardware_item_ids
191
+ and self.test_adapter_ids == other.test_adapter_ids
170
192
  and self.test_result_name == other.test_result_name
171
193
  and self.start_date_time == other.start_date_time
172
194
  and self.end_date_time == other.end_date_time
@@ -3,9 +3,12 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
- from collections.abc import Iterable
6
+ import sys
7
+ from collections.abc import Sequence
7
8
  from pathlib import Path
8
9
  from threading import Lock
10
+ from types import TracebackType
11
+ from typing import TYPE_CHECKING
9
12
 
10
13
  from grpc import Channel
11
14
  from ni.datastore.metadata._types._alias import Alias
@@ -20,7 +23,9 @@ from ni.datastore.metadata._types._test_station import TestStation
20
23
  from ni.datastore.metadata._types._uut import Uut
21
24
  from ni.datastore.metadata._types._uut_instance import UutInstance
22
25
  from ni.measurementlink.discovery.v1.client import DiscoveryClient
23
- from ni.measurements.metadata.v1.client import MetadataStoreClient as MetadataStoreServiceClient
26
+ from ni.measurements.metadata.v1.client import (
27
+ MetadataStoreClient as MetadataStoreServiceClient,
28
+ )
24
29
  from ni.measurements.metadata.v1.metadata_store_service_pb2 import (
25
30
  CreateAliasRequest,
26
31
  CreateHardwareItemRequest,
@@ -58,6 +63,12 @@ from ni.measurements.metadata.v1.metadata_store_service_pb2 import (
58
63
  )
59
64
  from ni_grpc_extensions.channelpool import GrpcChannelPool
60
65
 
66
+ if TYPE_CHECKING:
67
+ if sys.version_info >= (3, 11):
68
+ from typing import Self
69
+ else:
70
+ from typing_extensions import Self
71
+
61
72
  _logger = logging.getLogger(__name__)
62
73
 
63
74
 
@@ -65,6 +76,7 @@ class MetadataStoreClient:
65
76
  """Metadata store client for publishing and reading metadata."""
66
77
 
67
78
  __slots__ = (
79
+ "_closed",
68
80
  "_discovery_client",
69
81
  "_grpc_channel",
70
82
  "_grpc_channel_pool",
@@ -72,6 +84,9 @@ class MetadataStoreClient:
72
84
  "_metadata_store_client_lock",
73
85
  )
74
86
 
87
+ _METADATA_STORE_CLIENT_CLOSED_ERROR = "This MetadataStoreClient has been closed. Create a new MetadataStoreClient for further interaction with the metadata store."
88
+
89
+ _closed: bool
75
90
  _discovery_client: DiscoveryClient | None
76
91
  _grpc_channel: Channel | None
77
92
  _grpc_channel_pool: GrpcChannelPool | None
@@ -101,6 +116,30 @@ class MetadataStoreClient:
101
116
  self._metadata_store_client = None
102
117
  self._metadata_store_client_lock = Lock()
103
118
 
119
+ self._closed = False
120
+
121
+ def __enter__(self) -> Self:
122
+ """Enter the runtime context of the metadata store client."""
123
+ return self
124
+
125
+ def __exit__(
126
+ self,
127
+ exc_type: type[BaseException] | None,
128
+ exc_val: BaseException | None,
129
+ traceback: TracebackType | None,
130
+ ) -> None:
131
+ """Exit the runtime context of the metadata store client."""
132
+ self.close()
133
+
134
+ def close(self) -> None:
135
+ """Close the metadata store client and clean up resources that it owns."""
136
+ self._closed = True
137
+
138
+ with self._metadata_store_client_lock:
139
+ if self._metadata_store_client is not None:
140
+ self._metadata_store_client.close()
141
+ self._metadata_store_client = None
142
+
104
143
  def create_uut_instance(self, uut_instance: UutInstance) -> str:
105
144
  """Create a UUT instance in the metadata store."""
106
145
  create_request = CreateUutInstanceRequest(uut_instance=uut_instance.to_protobuf())
@@ -113,7 +152,7 @@ class MetadataStoreClient:
113
152
  get_response = self._get_metadata_store_client().get_uut_instance(get_request)
114
153
  return UutInstance.from_protobuf(get_response.uut_instance)
115
154
 
116
- def query_uut_instances(self, odata_query: str) -> Iterable[UutInstance]:
155
+ def query_uut_instances(self, odata_query: str = "") -> Sequence[UutInstance]:
117
156
  """Query UUT instances from the metadata store."""
118
157
  query_request = QueryUutInstancesRequest(odata_query=odata_query)
119
158
  query_response = self._get_metadata_store_client().query_uut_instances(query_request)
@@ -133,7 +172,7 @@ class MetadataStoreClient:
133
172
  get_response = self._get_metadata_store_client().get_uut(get_request)
134
173
  return Uut.from_protobuf(get_response.uut)
135
174
 
136
- def query_uuts(self, odata_query: str) -> Iterable[Uut]:
175
+ def query_uuts(self, odata_query: str = "") -> Sequence[Uut]:
137
176
  """Query UUTs from the metadata store."""
138
177
  query_request = QueryUutsRequest(odata_query=odata_query)
139
178
  query_response = self._get_metadata_store_client().query_uuts(query_request)
@@ -151,7 +190,7 @@ class MetadataStoreClient:
151
190
  get_response = self._get_metadata_store_client().get_operator(get_request)
152
191
  return Operator.from_protobuf(get_response.operator)
153
192
 
154
- def query_operators(self, odata_query: str) -> Iterable[Operator]:
193
+ def query_operators(self, odata_query: str = "") -> Sequence[Operator]:
155
194
  """Query operators from the metadata store."""
156
195
  query_request = QueryOperatorsRequest(odata_query=odata_query)
157
196
  query_response = self._get_metadata_store_client().query_operators(query_request)
@@ -171,7 +210,7 @@ class MetadataStoreClient:
171
210
  get_response = self._get_metadata_store_client().get_test_description(get_request)
172
211
  return TestDescription.from_protobuf(get_response.test_description)
173
212
 
174
- def query_test_descriptions(self, odata_query: str) -> Iterable[TestDescription]:
213
+ def query_test_descriptions(self, odata_query: str = "") -> Sequence[TestDescription]:
175
214
  """Query test descriptions from the metadata store."""
176
215
  query_request = QueryTestDescriptionsRequest(odata_query=odata_query)
177
216
  query_response = self._get_metadata_store_client().query_test_descriptions(query_request)
@@ -192,7 +231,7 @@ class MetadataStoreClient:
192
231
  get_response = self._get_metadata_store_client().get_test(get_request)
193
232
  return Test.from_protobuf(get_response.test)
194
233
 
195
- def query_tests(self, odata_query: str) -> Iterable[Test]:
234
+ def query_tests(self, odata_query: str = "") -> Sequence[Test]:
196
235
  """Query tests from the metadata store."""
197
236
  query_request = QueryTestsRequest(odata_query=odata_query)
198
237
  query_response = self._get_metadata_store_client().query_tests(query_request)
@@ -210,7 +249,7 @@ class MetadataStoreClient:
210
249
  get_response = self._get_metadata_store_client().get_test_station(get_request)
211
250
  return TestStation.from_protobuf(get_response.test_station)
212
251
 
213
- def query_test_stations(self, odata_query: str) -> Iterable[TestStation]:
252
+ def query_test_stations(self, odata_query: str = "") -> Sequence[TestStation]:
214
253
  """Query test stations from the metadata store."""
215
254
  query_request = QueryTestStationsRequest(odata_query=odata_query)
216
255
  query_response = self._get_metadata_store_client().query_test_stations(query_request)
@@ -230,7 +269,7 @@ class MetadataStoreClient:
230
269
  get_response = self._get_metadata_store_client().get_hardware_item(get_request)
231
270
  return HardwareItem.from_protobuf(get_response.hardware_item)
232
271
 
233
- def query_hardware_items(self, odata_query: str) -> Iterable[HardwareItem]:
272
+ def query_hardware_items(self, odata_query: str = "") -> Sequence[HardwareItem]:
234
273
  """Query hardware items from the metadata store."""
235
274
  query_request = QueryHardwareItemsRequest(odata_query=odata_query)
236
275
  query_response = self._get_metadata_store_client().query_hardware_items(query_request)
@@ -251,7 +290,7 @@ class MetadataStoreClient:
251
290
  get_response = self._get_metadata_store_client().get_software_item(get_request)
252
291
  return SoftwareItem.from_protobuf(get_response.software_item)
253
292
 
254
- def query_software_items(self, odata_query: str) -> Iterable[SoftwareItem]:
293
+ def query_software_items(self, odata_query: str = "") -> Sequence[SoftwareItem]:
255
294
  """Query software items from the metadata store."""
256
295
  query_request = QuerySoftwareItemsRequest(odata_query=odata_query)
257
296
  query_response = self._get_metadata_store_client().query_software_items(query_request)
@@ -272,7 +311,7 @@ class MetadataStoreClient:
272
311
  get_response = self._get_metadata_store_client().get_test_adapter(get_request)
273
312
  return TestAdapter.from_protobuf(get_response.test_adapter)
274
313
 
275
- def query_test_adapters(self, odata_query: str) -> Iterable[TestAdapter]:
314
+ def query_test_adapters(self, odata_query: str = "") -> Sequence[TestAdapter]:
276
315
  """Query test adapters from the metadata store."""
277
316
  query_request = QueryTestAdaptersRequest(odata_query=odata_query)
278
317
  query_response = self._get_metadata_store_client().query_test_adapters(query_request)
@@ -308,7 +347,7 @@ class MetadataStoreClient:
308
347
  register_response = self._get_metadata_store_client().register_schema(register_request)
309
348
  return register_response.schema_id
310
349
 
311
- def list_schemas(self) -> Iterable[ExtensionSchema]:
350
+ def list_schemas(self) -> Sequence[ExtensionSchema]:
312
351
  """List all schemas in the metadata store."""
313
352
  list_request = ListSchemasRequest()
314
353
  list_response = self._get_metadata_store_client().list_schemas(list_request)
@@ -364,19 +403,25 @@ class MetadataStoreClient:
364
403
  delete_response = self._get_metadata_store_client().delete_alias(delete_request)
365
404
  return delete_response.unregistered
366
405
 
367
- def query_aliases(self, odata_query: str) -> Iterable[Alias]:
406
+ def query_aliases(self, odata_query: str = "") -> Sequence[Alias]:
368
407
  """Query aliases from the metadata store."""
369
408
  query_request = QueryAliasesRequest(odata_query=odata_query)
370
409
  query_response = self._get_metadata_store_client().query_aliases(query_request)
371
410
  return [Alias.from_protobuf(alias) for alias in query_response.aliases]
372
411
 
373
412
  def _get_metadata_store_client(self) -> MetadataStoreServiceClient:
413
+ if self._closed:
414
+ raise RuntimeError(self._METADATA_STORE_CLIENT_CLOSED_ERROR)
415
+
374
416
  if self._metadata_store_client is None:
375
417
  with self._metadata_store_client_lock:
376
418
  if self._metadata_store_client is None:
377
- self._metadata_store_client = MetadataStoreServiceClient(
378
- discovery_client=self._discovery_client,
379
- grpc_channel=self._grpc_channel,
380
- grpc_channel_pool=self._grpc_channel_pool,
381
- )
419
+ self._metadata_store_client = self._instantiate_metadata_store_client()
382
420
  return self._metadata_store_client
421
+
422
+ def _instantiate_metadata_store_client(self) -> MetadataStoreServiceClient:
423
+ return MetadataStoreServiceClient(
424
+ discovery_client=self._discovery_client,
425
+ grpc_channel=self._grpc_channel,
426
+ grpc_channel_pool=self._grpc_channel_pool,
427
+ )
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import MutableMapping
5
+ from typing import Mapping, MutableMapping
6
6
 
7
7
  from ni.datastore.metadata._grpc_conversion import (
8
8
  populate_extension_value_message_map,
@@ -24,10 +24,15 @@ class HardwareItem:
24
24
  "asset_identifier",
25
25
  "calibration_due_date",
26
26
  "link",
27
- "extensions",
27
+ "_extensions",
28
28
  "schema_id",
29
29
  )
30
30
 
31
+ @property
32
+ def extensions(self) -> MutableMapping[str, str]:
33
+ """The extensions of the hardware item."""
34
+ return self._extensions
35
+
31
36
  def __init__(
32
37
  self,
33
38
  *,
@@ -38,7 +43,7 @@ class HardwareItem:
38
43
  asset_identifier: str = "",
39
44
  calibration_due_date: str = "",
40
45
  link: str = "",
41
- extensions: MutableMapping[str, str] | None = None,
46
+ extensions: Mapping[str, str] | None = None,
42
47
  schema_id: str = "",
43
48
  ) -> None:
44
49
  """Initialize a HardwareItem instance."""
@@ -49,7 +54,9 @@ class HardwareItem:
49
54
  self.asset_identifier = asset_identifier
50
55
  self.calibration_due_date = calibration_due_date
51
56
  self.link = link
52
- self.extensions: MutableMapping[str, str] = extensions if extensions is not None else {}
57
+ self._extensions: MutableMapping[str, str] = (
58
+ dict(extensions) if extensions is not None else {}
59
+ )
53
60
  self.schema_id = schema_id
54
61
 
55
62
  @staticmethod
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import MutableMapping
5
+ from typing import Mapping, MutableMapping
6
6
 
7
7
  from ni.datastore.metadata._grpc_conversion import (
8
8
  populate_extension_value_message_map,
@@ -20,24 +20,31 @@ class Operator:
20
20
  "operator_name",
21
21
  "role",
22
22
  "link",
23
- "extensions",
23
+ "_extensions",
24
24
  "schema_id",
25
25
  )
26
26
 
27
+ @property
28
+ def extensions(self) -> MutableMapping[str, str]:
29
+ """The extensions of the operator."""
30
+ return self._extensions
31
+
27
32
  def __init__(
28
33
  self,
29
34
  *,
30
35
  operator_name: str = "",
31
36
  role: str = "",
32
37
  link: str = "",
33
- extensions: MutableMapping[str, str] | None = None,
38
+ extensions: Mapping[str, str] | None = None,
34
39
  schema_id: str = "",
35
40
  ) -> None:
36
41
  """Initialize an Operator instance."""
37
42
  self.operator_name = operator_name
38
43
  self.role = role
39
44
  self.link = link
40
- self.extensions: MutableMapping[str, str] = extensions if extensions is not None else {}
45
+ self._extensions: MutableMapping[str, str] = (
46
+ dict(extensions) if extensions is not None else {}
47
+ )
41
48
  self.schema_id = schema_id
42
49
 
43
50
  @staticmethod
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import MutableMapping
5
+ from typing import Mapping, MutableMapping
6
6
 
7
7
  from ni.datastore.metadata._grpc_conversion import (
8
8
  populate_extension_value_message_map,
@@ -20,24 +20,31 @@ class SoftwareItem:
20
20
  "product",
21
21
  "version",
22
22
  "link",
23
- "extensions",
23
+ "_extensions",
24
24
  "schema_id",
25
25
  )
26
26
 
27
+ @property
28
+ def extensions(self) -> MutableMapping[str, str]:
29
+ """The extensions of the software item."""
30
+ return self._extensions
31
+
27
32
  def __init__(
28
33
  self,
29
34
  *,
30
35
  product: str = "",
31
36
  version: str = "",
32
37
  link: str = "",
33
- extensions: MutableMapping[str, str] | None = None,
38
+ extensions: Mapping[str, str] | None = None,
34
39
  schema_id: str = "",
35
40
  ) -> None:
36
41
  """Initialize a SoftwareItem instance."""
37
42
  self.product = product
38
43
  self.version = version
39
44
  self.link = link
40
- self.extensions: MutableMapping[str, str] = extensions if extensions is not None else {}
45
+ self._extensions: MutableMapping[str, str] = (
46
+ dict(extensions) if extensions is not None else {}
47
+ )
41
48
  self.schema_id = schema_id
42
49
 
43
50
  @staticmethod
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import MutableMapping
5
+ from typing import Mapping, MutableMapping
6
6
 
7
7
  from ni.datastore.metadata._grpc_conversion import (
8
8
  populate_extension_value_message_map,
@@ -20,24 +20,31 @@ class Test:
20
20
  "test_name",
21
21
  "description",
22
22
  "link",
23
- "extensions",
23
+ "_extensions",
24
24
  "schema_id",
25
25
  )
26
26
 
27
+ @property
28
+ def extensions(self) -> MutableMapping[str, str]:
29
+ """The extensions of the test."""
30
+ return self._extensions
31
+
27
32
  def __init__(
28
33
  self,
29
34
  *,
30
35
  test_name: str = "",
31
36
  description: str = "",
32
37
  link: str = "",
33
- extensions: MutableMapping[str, str] | None = None,
38
+ extensions: Mapping[str, str] | None = None,
34
39
  schema_id: str = "",
35
40
  ) -> None:
36
41
  """Initialize a Test instance."""
37
42
  self.test_name = test_name
38
43
  self.description = description
39
44
  self.link = link
40
- self.extensions: MutableMapping[str, str] = extensions if extensions is not None else {}
45
+ self._extensions: MutableMapping[str, str] = (
46
+ dict(extensions) if extensions is not None else {}
47
+ )
41
48
  self.schema_id = schema_id
42
49
 
43
50
  @staticmethod
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import MutableMapping
5
+ from typing import Mapping, MutableMapping
6
6
 
7
7
  from ni.datastore.metadata._grpc_conversion import (
8
8
  populate_extension_value_message_map,
@@ -25,10 +25,15 @@ class TestAdapter:
25
25
  "asset_identifier",
26
26
  "calibration_due_date",
27
27
  "link",
28
- "extensions",
28
+ "_extensions",
29
29
  "schema_id",
30
30
  )
31
31
 
32
+ @property
33
+ def extensions(self) -> MutableMapping[str, str]:
34
+ """The extensions of the test adapter."""
35
+ return self._extensions
36
+
32
37
  def __init__(
33
38
  self,
34
39
  *,
@@ -40,7 +45,7 @@ class TestAdapter:
40
45
  asset_identifier: str = "",
41
46
  calibration_due_date: str = "",
42
47
  link: str = "",
43
- extensions: MutableMapping[str, str] | None = None,
48
+ extensions: Mapping[str, str] | None = None,
44
49
  schema_id: str = "",
45
50
  ) -> None:
46
51
  """Initialize a TestAdapter instance."""
@@ -52,7 +57,9 @@ class TestAdapter:
52
57
  self.asset_identifier = asset_identifier
53
58
  self.calibration_due_date = calibration_due_date
54
59
  self.link = link
55
- self.extensions: MutableMapping[str, str] = extensions if extensions is not None else {}
60
+ self._extensions: MutableMapping[str, str] = (
61
+ dict(extensions) if extensions is not None else {}
62
+ )
56
63
  self.schema_id = schema_id
57
64
 
58
65
  @staticmethod
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import MutableMapping
5
+ from typing import Mapping, MutableMapping
6
6
 
7
7
  from ni.datastore.metadata._grpc_conversion import (
8
8
  populate_extension_value_message_map,
@@ -20,24 +20,31 @@ class TestDescription:
20
20
  "uut_id",
21
21
  "test_description_name",
22
22
  "link",
23
- "extensions",
23
+ "_extensions",
24
24
  "schema_id",
25
25
  )
26
26
 
27
+ @property
28
+ def extensions(self) -> MutableMapping[str, str]:
29
+ """The extensions of the test description."""
30
+ return self._extensions
31
+
27
32
  def __init__(
28
33
  self,
29
34
  *,
30
35
  uut_id: str = "",
31
36
  test_description_name: str = "",
32
37
  link: str = "",
33
- extensions: MutableMapping[str, str] | None = None,
38
+ extensions: Mapping[str, str] | None = None,
34
39
  schema_id: str = "",
35
40
  ) -> None:
36
41
  """Initialize a TestDescription instance."""
37
42
  self.uut_id = uut_id
38
43
  self.test_description_name = test_description_name
39
44
  self.link = link
40
- self.extensions: MutableMapping[str, str] = extensions if extensions is not None else {}
45
+ self._extensions: MutableMapping[str, str] = (
46
+ dict(extensions) if extensions is not None else {}
47
+ )
41
48
  self.schema_id = schema_id
42
49
 
43
50
  @staticmethod
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import MutableMapping
5
+ from typing import Mapping, MutableMapping
6
6
 
7
7
  from ni.datastore.metadata._grpc_conversion import (
8
8
  populate_extension_value_message_map,
@@ -20,24 +20,31 @@ class TestStation:
20
20
  "test_station_name",
21
21
  "asset_identifier",
22
22
  "link",
23
- "extensions",
23
+ "_extensions",
24
24
  "schema_id",
25
25
  )
26
26
 
27
+ @property
28
+ def extensions(self) -> MutableMapping[str, str]:
29
+ """The extensions of the test station."""
30
+ return self._extensions
31
+
27
32
  def __init__(
28
33
  self,
29
34
  *,
30
35
  test_station_name: str = "",
31
36
  asset_identifier: str = "",
32
37
  link: str = "",
33
- extensions: MutableMapping[str, str] | None = None,
38
+ extensions: Mapping[str, str] | None = None,
34
39
  schema_id: str = "",
35
40
  ) -> None:
36
41
  """Initialize a TestStation instance."""
37
42
  self.test_station_name = test_station_name
38
43
  self.asset_identifier = asset_identifier
39
44
  self.link = link
40
- self.extensions: MutableMapping[str, str] = extensions if extensions is not None else {}
45
+ self._extensions: MutableMapping[str, str] = (
46
+ dict(extensions) if extensions is not None else {}
47
+ )
41
48
  self.schema_id = schema_id
42
49
 
43
50
  @staticmethod
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Iterable, MutableMapping
5
+ from typing import Iterable, Mapping, MutableMapping, MutableSequence
6
6
 
7
7
  from ni.datastore.metadata._grpc_conversion import (
8
8
  populate_extension_value_message_map,
@@ -19,13 +19,23 @@ class Uut:
19
19
  __slots__ = (
20
20
  "model_name",
21
21
  "family",
22
- "manufacturers",
22
+ "_manufacturers",
23
23
  "part_number",
24
24
  "link",
25
- "extensions",
25
+ "_extensions",
26
26
  "schema_id",
27
27
  )
28
28
 
29
+ @property
30
+ def manufacturers(self) -> MutableSequence[str]:
31
+ """The manufacturers of the UUT."""
32
+ return self._manufacturers
33
+
34
+ @property
35
+ def extensions(self) -> MutableMapping[str, str]:
36
+ """The extensions of the UUT."""
37
+ return self._extensions
38
+
29
39
  def __init__(
30
40
  self,
31
41
  *,
@@ -34,16 +44,20 @@ class Uut:
34
44
  manufacturers: Iterable[str] | None = None,
35
45
  part_number: str = "",
36
46
  link: str = "",
37
- extensions: MutableMapping[str, str] | None = None,
47
+ extensions: Mapping[str, str] | None = None,
38
48
  schema_id: str = "",
39
49
  ) -> None:
40
50
  """Initialize a Uut instance."""
41
51
  self.model_name = model_name
42
52
  self.family = family
43
- self.manufacturers: Iterable[str] = manufacturers if manufacturers is not None else []
53
+ self._manufacturers: MutableSequence[str] = (
54
+ list(manufacturers) if manufacturers is not None else []
55
+ )
44
56
  self.part_number = part_number
45
57
  self.link = link
46
- self.extensions: MutableMapping[str, str] = extensions if extensions is not None else {}
58
+ self._extensions: MutableMapping[str, str] = (
59
+ dict(extensions) if extensions is not None else {}
60
+ )
47
61
  self.schema_id = schema_id
48
62
 
49
63
  @staticmethod
@@ -80,7 +94,7 @@ class Uut:
80
94
  return (
81
95
  self.model_name == other.model_name
82
96
  and self.family == other.family
83
- and list(self.manufacturers) == list(other.manufacturers)
97
+ and self.manufacturers == other.manufacturers
84
98
  and self.part_number == other.part_number
85
99
  and self.link == other.link
86
100
  and self.extensions == other.extensions
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import MutableMapping
5
+ from typing import Mapping, MutableMapping
6
6
 
7
7
  from ni.datastore.metadata._grpc_conversion import (
8
8
  populate_extension_value_message_map,
@@ -23,10 +23,15 @@ class UutInstance:
23
23
  "firmware_version",
24
24
  "hardware_version",
25
25
  "link",
26
- "extensions",
26
+ "_extensions",
27
27
  "schema_id",
28
28
  )
29
29
 
30
+ @property
31
+ def extensions(self) -> MutableMapping[str, str]:
32
+ """The extensions of the UUT instance."""
33
+ return self._extensions
34
+
30
35
  def __init__(
31
36
  self,
32
37
  *,
@@ -36,7 +41,7 @@ class UutInstance:
36
41
  firmware_version: str = "",
37
42
  hardware_version: str = "",
38
43
  link: str = "",
39
- extensions: MutableMapping[str, str] | None = None,
44
+ extensions: Mapping[str, str] | None = None,
40
45
  schema_id: str = "",
41
46
  ) -> None:
42
47
  """Initialize a UutInstance instance."""
@@ -46,7 +51,9 @@ class UutInstance:
46
51
  self.firmware_version = firmware_version
47
52
  self.hardware_version = hardware_version
48
53
  self.link = link
49
- self.extensions: MutableMapping[str, str] = extensions if extensions is not None else {}
54
+ self._extensions: MutableMapping[str, str] = (
55
+ dict(extensions) if extensions is not None else {}
56
+ )
50
57
  self.schema_id = schema_id
51
58
 
52
59
  @staticmethod