ni.datastore 0.1.0.dev1__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 (36) hide show
  1. {ni_datastore-0.1.0.dev1 → ni_datastore-0.1.0.dev3}/PKG-INFO +5 -6
  2. {ni_datastore-0.1.0.dev1 → ni_datastore-0.1.0.dev3}/README.md +0 -1
  3. {ni_datastore-0.1.0.dev1 → ni_datastore-0.1.0.dev3}/pyproject.toml +5 -5
  4. ni_datastore-0.1.0.dev3/src/ni/datastore/__init__.py +1 -0
  5. ni_datastore-0.1.0.dev3/src/ni/datastore/data/__init__.py +30 -0
  6. ni_datastore-0.1.0.dev3/src/ni/datastore/data/_data_store_client.py +355 -0
  7. ni_datastore-0.1.0.dev1/src/ni/datastore/grpc_conversion.py → ni_datastore-0.1.0.dev3/src/ni/datastore/data/_grpc_conversion.py +83 -34
  8. ni_datastore-0.1.0.dev3/src/ni/datastore/data/_types/__init__.py +1 -0
  9. {ni_datastore-0.1.0.dev1/src/ni/datastore → ni_datastore-0.1.0.dev3/src/ni/datastore/data}/_types/_published_measurement.py +38 -18
  10. {ni_datastore-0.1.0.dev1/src/ni/datastore → ni_datastore-0.1.0.dev3/src/ni/datastore/data}/_types/_step.py +12 -5
  11. {ni_datastore-0.1.0.dev1/src/ni/datastore → ni_datastore-0.1.0.dev3/src/ni/datastore/data}/_types/_test_result.py +39 -17
  12. ni_datastore-0.1.0.dev3/src/ni/datastore/metadata/__init__.py +46 -0
  13. ni_datastore-0.1.0.dev3/src/ni/datastore/metadata/_grpc_conversion.py +39 -0
  14. ni_datastore-0.1.0.dev3/src/ni/datastore/metadata/_metadata_store_client.py +427 -0
  15. ni_datastore-0.1.0.dev3/src/ni/datastore/metadata/_types/__init__.py +1 -0
  16. {ni_datastore-0.1.0.dev1/src/ni/datastore → ni_datastore-0.1.0.dev3/src/ni/datastore/metadata}/_types/_hardware_item.py +12 -5
  17. {ni_datastore-0.1.0.dev1/src/ni/datastore → ni_datastore-0.1.0.dev3/src/ni/datastore/metadata}/_types/_operator.py +12 -5
  18. {ni_datastore-0.1.0.dev1/src/ni/datastore → ni_datastore-0.1.0.dev3/src/ni/datastore/metadata}/_types/_software_item.py +12 -5
  19. {ni_datastore-0.1.0.dev1/src/ni/datastore → ni_datastore-0.1.0.dev3/src/ni/datastore/metadata}/_types/_test.py +12 -5
  20. {ni_datastore-0.1.0.dev1/src/ni/datastore → ni_datastore-0.1.0.dev3/src/ni/datastore/metadata}/_types/_test_adapter.py +12 -5
  21. {ni_datastore-0.1.0.dev1/src/ni/datastore → ni_datastore-0.1.0.dev3/src/ni/datastore/metadata}/_types/_test_description.py +12 -5
  22. {ni_datastore-0.1.0.dev1/src/ni/datastore → ni_datastore-0.1.0.dev3/src/ni/datastore/metadata}/_types/_test_station.py +12 -5
  23. {ni_datastore-0.1.0.dev1/src/ni/datastore → ni_datastore-0.1.0.dev3/src/ni/datastore/metadata}/_types/_uut.py +22 -8
  24. {ni_datastore-0.1.0.dev1/src/ni/datastore → ni_datastore-0.1.0.dev3/src/ni/datastore/metadata}/_types/_uut_instance.py +12 -5
  25. ni_datastore-0.1.0.dev3/src/ni/datastore/metadata/_types/py.typed +0 -0
  26. ni_datastore-0.1.0.dev3/src/ni/datastore/metadata/py.typed +0 -0
  27. ni_datastore-0.1.0.dev3/src/ni/datastore/py.typed +0 -0
  28. ni_datastore-0.1.0.dev1/src/ni/datastore/__init__.py +0 -66
  29. ni_datastore-0.1.0.dev1/src/ni/datastore/_client.py +0 -652
  30. ni_datastore-0.1.0.dev1/src/ni/datastore/_types/__init__.py +0 -1
  31. {ni_datastore-0.1.0.dev1 → ni_datastore-0.1.0.dev3}/LICENSE +0 -0
  32. {ni_datastore-0.1.0.dev1/src/ni/datastore → ni_datastore-0.1.0.dev3/src/ni/datastore/data}/_types/_published_condition.py +0 -0
  33. {ni_datastore-0.1.0.dev1/src/ni/datastore → ni_datastore-0.1.0.dev3/src/ni/datastore/data}/_types/py.typed +0 -0
  34. {ni_datastore-0.1.0.dev1/src/ni/datastore → ni_datastore-0.1.0.dev3/src/ni/datastore/data}/py.typed +0 -0
  35. {ni_datastore-0.1.0.dev1/src/ni/datastore → ni_datastore-0.1.0.dev3/src/ni/datastore/metadata}/_types/_alias.py +0 -0
  36. {ni_datastore-0.1.0.dev1/src/ni/datastore → ni_datastore-0.1.0.dev3/src/ni/datastore/metadata}/_types/_extension_schema.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ni.datastore
3
- Version: 0.1.0.dev1
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,17 +24,16 @@ 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
 
34
34
  # Table of Contents
35
35
 
36
36
  - [Table of Contents](#table-of-contents)
37
- - [Measurement ]
38
37
  - [About](#about)
39
38
  - [Operating System Support](#operating-system-support)
40
39
  - [Python Version Support](#python-version-support)
@@ -1,7 +1,6 @@
1
1
  # Table of Contents
2
2
 
3
3
  - [Table of Contents](#table-of-contents)
4
- - [Measurement ]
5
4
  - [About](#about)
6
5
  - [Operating System Support](#operating-system-support)
7
6
  - [Python Version Support](#python-version-support)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ni.datastore"
3
- version = "0.1.0-dev1"
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]
@@ -0,0 +1 @@
1
+ """Public API for accessing the NI Data/Metadata Stores."""
@@ -0,0 +1,30 @@
1
+ """Public API for accessing the NI Data Store."""
2
+
3
+ from ni.datamonikers.v1.data_moniker_pb2 import Moniker
4
+ from ni.datastore.data._data_store_client import DataStoreClient
5
+ from ni.datastore.data._types._published_condition import PublishedCondition
6
+ from ni.datastore.data._types._published_measurement import PublishedMeasurement
7
+ from ni.datastore.data._types._step import Step
8
+ from ni.datastore.data._types._test_result import TestResult
9
+ from ni.measurements.data.v1.data_store_pb2 import ErrorInformation, Outcome
10
+
11
+ __all__ = [
12
+ "DataStoreClient",
13
+ "ErrorInformation",
14
+ "Moniker",
15
+ "Outcome",
16
+ "PublishedCondition",
17
+ "PublishedMeasurement",
18
+ "Step",
19
+ "TestResult",
20
+ ]
21
+
22
+ # Hide that it was not defined in this top-level package
23
+ DataStoreClient.__module__ = __name__
24
+ ErrorInformation.__module__ = __name__
25
+ Moniker.__module__ = __name__
26
+ Outcome.__module__ = __name__
27
+ PublishedCondition.__module__ = __name__
28
+ PublishedMeasurement.__module__ = __name__
29
+ Step.__module__ = __name__
30
+ TestResult.__module__ = __name__
@@ -0,0 +1,355 @@
1
+ """Data store client for publishing and reading data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import sys
7
+ from collections.abc import Iterable, Sequence
8
+ from threading import Lock
9
+ from types import TracebackType
10
+ from typing import TYPE_CHECKING, Type, TypeVar, overload
11
+ from urllib.parse import urlparse
12
+
13
+ import hightime as ht
14
+ from grpc import Channel
15
+ from ni.datamonikers.v1.client import MonikerClient
16
+ from ni.datamonikers.v1.data_moniker_pb2 import Moniker
17
+ from ni.datastore.data._grpc_conversion import (
18
+ get_publish_measurement_timestamp,
19
+ populate_publish_condition_batch_request_values,
20
+ populate_publish_condition_request_value,
21
+ populate_publish_measurement_batch_request_values,
22
+ populate_publish_measurement_request_value,
23
+ unpack_and_convert_from_protobuf_any,
24
+ )
25
+ from ni.datastore.data._types._published_condition import PublishedCondition
26
+ from ni.datastore.data._types._published_measurement import PublishedMeasurement
27
+ from ni.datastore.data._types._step import Step
28
+ from ni.datastore.data._types._test_result import TestResult
29
+ from ni.measurementlink.discovery.v1.client import DiscoveryClient
30
+ from ni.measurements.data.v1.client import DataStoreClient as DataStoreServiceClient
31
+ from ni.measurements.data.v1.data_store_pb2 import (
32
+ ErrorInformation,
33
+ Outcome,
34
+ )
35
+ from ni.measurements.data.v1.data_store_service_pb2 import (
36
+ CreateStepRequest,
37
+ CreateTestResultRequest,
38
+ GetStepRequest,
39
+ GetTestResultRequest,
40
+ PublishConditionBatchRequest,
41
+ PublishConditionRequest,
42
+ PublishMeasurementBatchRequest,
43
+ PublishMeasurementRequest,
44
+ QueryConditionsRequest,
45
+ QueryMeasurementsRequest,
46
+ QueryStepsRequest,
47
+ )
48
+ from ni.protobuf.types.precision_timestamp_conversion import (
49
+ hightime_datetime_to_protobuf,
50
+ )
51
+ from ni_grpc_extensions.channelpool import GrpcChannelPool
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
+
59
+ TRead = TypeVar("TRead")
60
+
61
+ _logger = logging.getLogger(__name__)
62
+
63
+
64
+ class DataStoreClient:
65
+ """Data store client for publishing and reading data."""
66
+
67
+ __slots__ = (
68
+ "_closed",
69
+ "_discovery_client",
70
+ "_grpc_channel",
71
+ "_grpc_channel_pool",
72
+ "_data_store_client",
73
+ "_data_store_client_lock",
74
+ "_moniker_clients_by_service_location",
75
+ "_moniker_clients_lock",
76
+ )
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
81
+ _discovery_client: DiscoveryClient | None
82
+ _grpc_channel: Channel | None
83
+ _grpc_channel_pool: GrpcChannelPool | None
84
+ _data_store_client: DataStoreServiceClient | None
85
+ _moniker_clients_by_service_location: dict[str, MonikerClient]
86
+ _data_store_client_lock: Lock
87
+ _moniker_clients_lock: Lock
88
+
89
+ def __init__(
90
+ self,
91
+ discovery_client: DiscoveryClient | None = None,
92
+ grpc_channel: Channel | None = None,
93
+ grpc_channel_pool: GrpcChannelPool | None = None,
94
+ ) -> None:
95
+ """Initialize the DataStoreClient.
96
+
97
+ Args:
98
+ discovery_client: An optional discovery client (recommended).
99
+
100
+ grpc_channel: An optional data store gRPC channel. Providing this channel will bypass
101
+ discovery service resolution of the data store. (Note: Reading data from a moniker
102
+ will still always use a channel corresponding to the service location specified by
103
+ that moniker.)
104
+
105
+ grpc_channel_pool: An optional gRPC channel pool (recommended).
106
+ """
107
+ self._discovery_client = discovery_client
108
+ self._grpc_channel = grpc_channel
109
+ self._grpc_channel_pool = grpc_channel_pool
110
+
111
+ self._data_store_client = None
112
+ self._moniker_clients_by_service_location = {}
113
+
114
+ self._data_store_client_lock = Lock()
115
+ self._moniker_clients_lock = Lock()
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
+
146
+ def publish_condition(
147
+ self,
148
+ condition_name: str,
149
+ type: str,
150
+ value: object,
151
+ step_id: str,
152
+ ) -> PublishedCondition:
153
+ """Publish a condition value to the data store."""
154
+ publish_request = PublishConditionRequest(
155
+ condition_name=condition_name,
156
+ type=type,
157
+ step_id=step_id,
158
+ )
159
+ populate_publish_condition_request_value(publish_request, value)
160
+ publish_response = self._get_data_store_client().publish_condition(publish_request)
161
+ return PublishedCondition.from_protobuf(publish_response.published_condition)
162
+
163
+ def publish_condition_batch(
164
+ self, condition_name: str, type: str, values: object, step_id: str
165
+ ) -> PublishedCondition:
166
+ """Publish a batch of N values for a condition to the data store."""
167
+ publish_request = PublishConditionBatchRequest(
168
+ condition_name=condition_name,
169
+ type=type,
170
+ step_id=step_id,
171
+ )
172
+ populate_publish_condition_batch_request_values(publish_request, values)
173
+ publish_response = self._get_data_store_client().publish_condition_batch(publish_request)
174
+ return PublishedCondition.from_protobuf(publish_response.published_condition)
175
+
176
+ def publish_measurement(
177
+ self,
178
+ measurement_name: str,
179
+ value: object, # More strongly typed Union[bool, AnalogWaveform] can be used if needed
180
+ step_id: str,
181
+ timestamp: ht.datetime | None = None,
182
+ outcome: Outcome.ValueType = Outcome.OUTCOME_UNSPECIFIED,
183
+ error_information: ErrorInformation | None = None,
184
+ hardware_item_ids: Iterable[str] = tuple(),
185
+ test_adapter_ids: Iterable[str] = tuple(),
186
+ software_item_ids: Iterable[str] = tuple(),
187
+ notes: str = "",
188
+ ) -> PublishedMeasurement:
189
+ """Publish a measurement value to the data store."""
190
+ publish_request = PublishMeasurementRequest(
191
+ measurement_name=measurement_name,
192
+ step_id=step_id,
193
+ outcome=outcome,
194
+ error_information=error_information,
195
+ hardware_item_ids=hardware_item_ids,
196
+ test_adapter_ids=test_adapter_ids,
197
+ software_item_ids=software_item_ids,
198
+ notes=notes,
199
+ )
200
+ populate_publish_measurement_request_value(publish_request, value)
201
+ publish_request.timestamp.CopyFrom(
202
+ get_publish_measurement_timestamp(publish_request, timestamp)
203
+ )
204
+ publish_response = self._get_data_store_client().publish_measurement(publish_request)
205
+ return PublishedMeasurement.from_protobuf(publish_response.published_measurement)
206
+
207
+ def publish_measurement_batch(
208
+ self,
209
+ measurement_name: str,
210
+ values: object,
211
+ step_id: str,
212
+ timestamps: Iterable[ht.datetime] = tuple(),
213
+ outcomes: Iterable[Outcome.ValueType] = tuple(),
214
+ error_information: Iterable[ErrorInformation] = tuple(),
215
+ hardware_item_ids: Iterable[str] = tuple(),
216
+ test_adapter_ids: Iterable[str] = tuple(),
217
+ software_item_ids: Iterable[str] = tuple(),
218
+ ) -> Sequence[PublishedMeasurement]:
219
+ """Publish a batch of N values of a measurement to the data store."""
220
+ publish_request = PublishMeasurementBatchRequest(
221
+ measurement_name=measurement_name,
222
+ step_id=step_id,
223
+ timestamp=[hightime_datetime_to_protobuf(ts) for ts in timestamps],
224
+ outcome=outcomes,
225
+ error_information=error_information,
226
+ hardware_item_ids=hardware_item_ids,
227
+ test_adapter_ids=test_adapter_ids,
228
+ software_item_ids=software_item_ids,
229
+ )
230
+ populate_publish_measurement_batch_request_values(publish_request, values)
231
+ publish_response = self._get_data_store_client().publish_measurement_batch(publish_request)
232
+ return [
233
+ PublishedMeasurement.from_protobuf(pm) for pm in publish_response.published_measurements
234
+ ]
235
+
236
+ @overload
237
+ def read_data(
238
+ self,
239
+ moniker_source: Moniker | PublishedMeasurement | PublishedCondition,
240
+ expected_type: Type[TRead],
241
+ ) -> TRead: ...
242
+
243
+ @overload
244
+ def read_data(
245
+ self,
246
+ moniker_source: Moniker | PublishedMeasurement | PublishedCondition,
247
+ ) -> object: ...
248
+
249
+ def read_data(
250
+ self,
251
+ moniker_source: Moniker | PublishedMeasurement | PublishedCondition,
252
+ expected_type: Type[TRead] | None = None,
253
+ ) -> TRead | object:
254
+ """Read data published to the data store."""
255
+ if isinstance(moniker_source, Moniker):
256
+ moniker = moniker_source
257
+ elif isinstance(moniker_source, PublishedMeasurement):
258
+ if moniker_source.moniker is None:
259
+ raise ValueError("PublishedMeasurement must have a Moniker to read data")
260
+ moniker = moniker_source.moniker
261
+ elif isinstance(moniker_source, PublishedCondition):
262
+ if moniker_source.moniker is None:
263
+ raise ValueError("PublishedCondition must have a Moniker to read data")
264
+ moniker = moniker_source.moniker
265
+
266
+ moniker_client = self._get_moniker_client(moniker.service_location)
267
+ read_result = moniker_client.read_from_moniker(moniker)
268
+ converted_data = unpack_and_convert_from_protobuf_any(read_result.value)
269
+ if expected_type is not None and not isinstance(converted_data, expected_type):
270
+ raise TypeError(f"Expected type {expected_type}, got {type(converted_data)}")
271
+ return converted_data
272
+
273
+ def create_step(self, step: Step) -> str:
274
+ """Create a step in the data store."""
275
+ create_request = CreateStepRequest(step=step.to_protobuf())
276
+ create_response = self._get_data_store_client().create_step(create_request)
277
+ return create_response.step_id
278
+
279
+ def get_step(self, step_id: str) -> Step:
280
+ """Get a step from the data store."""
281
+ get_request = GetStepRequest(step_id=step_id)
282
+ get_response = self._get_data_store_client().get_step(get_request)
283
+ return Step.from_protobuf(get_response.step)
284
+
285
+ def create_test_result(self, test_result: TestResult) -> str:
286
+ """Create a test result in the data store."""
287
+ create_request = CreateTestResultRequest(test_result=test_result.to_protobuf())
288
+ create_response = self._get_data_store_client().create_test_result(create_request)
289
+ return create_response.test_result_id
290
+
291
+ def get_test_result(self, test_result_id: str) -> TestResult:
292
+ """Get a test result from the data store."""
293
+ get_request = GetTestResultRequest(test_result_id=test_result_id)
294
+ get_response = self._get_data_store_client().get_test_result(get_request)
295
+ return TestResult.from_protobuf(get_response.test_result)
296
+
297
+ def query_conditions(self, odata_query: str = "") -> Sequence[PublishedCondition]:
298
+ """Query conditions from the data store."""
299
+ query_request = QueryConditionsRequest(odata_query=odata_query)
300
+ query_response = self._get_data_store_client().query_conditions(query_request)
301
+ return [
302
+ PublishedCondition.from_protobuf(published_condition)
303
+ for published_condition in query_response.published_conditions
304
+ ]
305
+
306
+ def query_measurements(self, odata_query: str = "") -> Sequence[PublishedMeasurement]:
307
+ """Query measurements from the data store."""
308
+ query_request = QueryMeasurementsRequest(odata_query=odata_query)
309
+ query_response = self._get_data_store_client().query_measurements(query_request)
310
+ return [
311
+ PublishedMeasurement.from_protobuf(published_measurement)
312
+ for published_measurement in query_response.published_measurements
313
+ ]
314
+
315
+ def query_steps(self, odata_query: str = "") -> Sequence[Step]:
316
+ """Query steps from the data store."""
317
+ query_request = QueryStepsRequest(odata_query=odata_query)
318
+ query_response = self._get_data_store_client().query_steps(query_request)
319
+ return [Step.from_protobuf(step) for step in query_response.steps]
320
+
321
+ def _get_data_store_client(self) -> DataStoreServiceClient:
322
+ if self._closed:
323
+ raise RuntimeError(self._DATA_STORE_CLIENT_CLOSED_ERROR)
324
+
325
+ if self._data_store_client is None:
326
+ with self._data_store_client_lock:
327
+ if self._data_store_client is None:
328
+ self._data_store_client = self._instantiate_data_store_client()
329
+ return self._data_store_client
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
+
338
+ def _get_moniker_client(self, service_location: str) -> MonikerClient:
339
+ if self._closed:
340
+ raise RuntimeError(self._DATA_STORE_CLIENT_CLOSED_ERROR)
341
+
342
+ parsed_service_location = urlparse(service_location).netloc
343
+ if parsed_service_location not in self._moniker_clients_by_service_location:
344
+ with self._moniker_clients_lock:
345
+ if parsed_service_location not in self._moniker_clients_by_service_location:
346
+ self._moniker_clients_by_service_location[parsed_service_location] = (
347
+ self._instantiate_moniker_client(parsed_service_location)
348
+ )
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
+ )
@@ -2,19 +2,23 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import datetime as std_datetime
5
6
  import logging
6
- from typing import MutableMapping
7
+ from typing import Iterable, cast
7
8
 
9
+ import hightime as ht
8
10
  import numpy as np
9
11
  from google.protobuf.any_pb2 import Any
10
- from google.protobuf.internal.containers import MessageMap
11
12
  from ni.measurements.data.v1.data_store_service_pb2 import (
12
13
  PublishConditionBatchRequest,
13
14
  PublishConditionRequest,
14
15
  PublishMeasurementBatchRequest,
15
16
  PublishMeasurementRequest,
16
17
  )
17
- from ni.measurements.metadata.v1.metadata_store_pb2 import ExtensionValue
18
+ from ni.protobuf.types.precision_timestamp_conversion import (
19
+ hightime_datetime_to_protobuf,
20
+ )
21
+ from ni.protobuf.types.precision_timestamp_pb2 import PrecisionTimestamp
18
22
  from ni.protobuf.types.scalar_conversion import scalar_to_protobuf
19
23
  from ni.protobuf.types.vector_conversion import vector_from_protobuf, vector_to_protobuf
20
24
  from ni.protobuf.types.vector_pb2 import Vector as VectorProto
@@ -40,11 +44,16 @@ from ni.protobuf.types.waveform_pb2 import (
40
44
  I16AnalogWaveform,
41
45
  I16ComplexWaveform,
42
46
  )
47
+ from ni.protobuf.types.xydata_conversion import (
48
+ float64_xydata_from_protobuf,
49
+ float64_xydata_to_protobuf,
50
+ )
43
51
  from ni.protobuf.types.xydata_pb2 import DoubleXYData
44
52
  from nitypes.complex import ComplexInt32DType
45
53
  from nitypes.scalar import Scalar
46
54
  from nitypes.vector import Vector
47
55
  from nitypes.waveform import AnalogWaveform, ComplexWaveform, DigitalWaveform, Spectrum
56
+ from nitypes.xy_data import XYData
48
57
 
49
58
  _logger = logging.getLogger(__name__)
50
59
 
@@ -73,9 +82,19 @@ def populate_publish_condition_batch_request_values(
73
82
  publish_request: PublishConditionBatchRequest, values: object
74
83
  ) -> None:
75
84
  """Assign a value to the scalar_values vector member of PublishConditionBatchRequest."""
76
- # TODO: Determine whether we wish to support primitive types such as a list of float
77
85
  if isinstance(values, Vector):
78
86
  publish_request.scalar_values.CopyFrom(vector_to_protobuf(values))
87
+ elif isinstance(values, Iterable):
88
+ if not values:
89
+ raise ValueError("Cannot publish an empty Iterable.")
90
+ try:
91
+ vector = Vector(values)
92
+ except (TypeError, ValueError):
93
+ raise TypeError(
94
+ f"Unsupported iterable: {values}. Subtype must be bool, float, int, or string."
95
+ )
96
+
97
+ publish_request.scalar_values.CopyFrom(vector_to_protobuf(vector))
79
98
  else:
80
99
  raise TypeError(
81
100
  f"Unsupported condition values type: {type(values)}. Please consult the documentation."
@@ -123,20 +142,45 @@ def populate_publish_measurement_request_value(
123
142
  raise TypeError(f"Unsupported Spectrum dtype: {value.dtype}")
124
143
  elif isinstance(value, DigitalWaveform):
125
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}")
150
+ elif isinstance(value, Iterable):
151
+ if not value:
152
+ raise ValueError("Cannot publish an empty Iterable.")
153
+ try:
154
+ vector = Vector(value)
155
+ except (TypeError, ValueError):
156
+ raise TypeError(
157
+ f"Unsupported iterable: {value}. Subtype must be bool, float, int, or string."
158
+ )
159
+
160
+ publish_request.vector.CopyFrom(vector_to_protobuf(vector))
126
161
  else:
127
162
  raise TypeError(
128
163
  f"Unsupported measurement value type: {type(value)}. Please consult the documentation."
129
164
  )
130
- # TODO: Implement conversion from proper XYData type
131
165
 
132
166
 
133
167
  def populate_publish_measurement_batch_request_values(
134
168
  publish_request: PublishMeasurementBatchRequest, values: object
135
169
  ) -> None:
136
170
  """Assign a value to the appropriate field of the PublishMeasurementBatchRequest object."""
137
- # TODO: Determine whether we wish to support primitive types such as a list of float
138
171
  if isinstance(values, Vector):
139
172
  publish_request.scalar_values.CopyFrom(vector_to_protobuf(values))
173
+ elif isinstance(values, Iterable):
174
+ if not values:
175
+ raise ValueError("Cannot publish an empty Iterable.")
176
+ try:
177
+ vector = Vector(values)
178
+ except (TypeError, ValueError):
179
+ raise TypeError(
180
+ f"Unsupported iterable: {values}. Subtype must be bool, float, int, or string."
181
+ )
182
+
183
+ publish_request.scalar_values.CopyFrom(vector_to_protobuf(vector))
140
184
  else:
141
185
  raise TypeError(
142
186
  f"Unsupported measurement values type: {type(values)}. Please consult the documentation."
@@ -173,10 +217,7 @@ def unpack_and_convert_from_protobuf_any(read_value: Any) -> object:
173
217
  elif value_type == DoubleXYData.DESCRIPTOR.full_name:
174
218
  xydata = DoubleXYData()
175
219
  read_value.Unpack(xydata)
176
- _logger.warning(
177
- "DoubleXYData conversion is not yet implemented. Returning the raw protobuf object."
178
- )
179
- return xydata
220
+ return float64_xydata_from_protobuf(xydata)
180
221
  elif value_type == VectorProto.DESCRIPTOR.full_name:
181
222
  vector = VectorProto()
182
223
  read_value.Unpack(vector)
@@ -185,29 +226,37 @@ def unpack_and_convert_from_protobuf_any(read_value: Any) -> object:
185
226
  raise TypeError(f"Unsupported data type Name: {value_type}")
186
227
 
187
228
 
188
- def populate_extension_value_message_map(
189
- destination: MessageMap[str, ExtensionValue],
190
- source: MutableMapping[str, str],
191
- ) -> None:
192
- """Populate a gRPC message map of string keys to ExtensionValue.
193
-
194
- The input is a mapping of string keys to string values.
195
- """
196
- for key, value in source.items():
197
- destination[key].string_value = value
229
+ def get_publish_measurement_timestamp(
230
+ publish_request: PublishMeasurementRequest, client_provided_timestamp: ht.datetime | None
231
+ ) -> PrecisionTimestamp:
232
+ """Determine the correct timestamp to use for publishing a measurement."""
233
+ no_client_timestamp_provided = client_provided_timestamp is None
234
+ if no_client_timestamp_provided:
235
+ publish_time = hightime_datetime_to_protobuf(ht.datetime.now(std_datetime.timezone.utc))
236
+ else:
237
+ publish_time = hightime_datetime_to_protobuf(cast(ht.datetime, client_provided_timestamp))
198
238
 
239
+ waveform_t0: PrecisionTimestamp | None = None
240
+ value_case = publish_request.WhichOneof("value")
241
+ if value_case == "double_analog_waveform":
242
+ waveform_t0 = publish_request.double_analog_waveform.t0
243
+ elif value_case == "i16_analog_waveform":
244
+ waveform_t0 = publish_request.i16_analog_waveform.t0
245
+ elif value_case == "double_complex_waveform":
246
+ waveform_t0 = publish_request.double_complex_waveform.t0
247
+ elif value_case == "i16_complex_waveform":
248
+ waveform_t0 = publish_request.i16_complex_waveform.t0
249
+ elif value_case == "digital_waveform":
250
+ waveform_t0 = publish_request.digital_waveform.t0
199
251
 
200
- def populate_from_extension_value_message_map(
201
- destination: MutableMapping[str, str],
202
- source: MessageMap[str, ExtensionValue],
203
- ) -> None:
204
- """Populate a mapping of string keys to stringvalues.
205
-
206
- The input is a gRPC message map of string keys to ExtensionValue.
207
- """
208
- for key, extension_value in source.items():
209
- value_case = extension_value.WhichOneof("metadata")
210
- if value_case == "string_value":
211
- destination[key] = extension_value.string_value
212
- else:
213
- raise TypeError(f"Unsupported ExtensionValue type for key '{key}': {value_case}")
252
+ # If an initialized waveform t0 value is present
253
+ if waveform_t0 is not None and waveform_t0 != PrecisionTimestamp():
254
+ if no_client_timestamp_provided:
255
+ # If the client did not provide a timestamp, use the waveform t0 value
256
+ publish_time = waveform_t0
257
+ elif publish_time != waveform_t0:
258
+ raise ValueError(
259
+ "The provided timestamp does not match the waveform t0. Please provide a matching timestamp or "
260
+ "omit the timestamp to use the waveform t0."
261
+ )
262
+ return publish_time
@@ -0,0 +1 @@
1
+ """Types for use in accessing the NI Data Store."""