From e0bca018d0cc2a7fb1900e3e62bb64c9ea2ace31 Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Fri, 15 May 2026 15:02:39 -0500 Subject: [PATCH 01/29] Initial Implementation Work - Add ability to populate PublishMeasurementBatchRequest with non-scalar measurement values. --- src/ni/datastore/data/_grpc_conversion.py | 107 +++++++- tests/unit/data/test_grpc_conversion.py | 263 +++++++++++++++++++- tests/unit/data/test_publish_measurement.py | 2 +- 3 files changed, 366 insertions(+), 6 deletions(-) diff --git a/src/ni/datastore/data/_grpc_conversion.py b/src/ni/datastore/data/_grpc_conversion.py index 2fcab116..84cb9e89 100644 --- a/src/ni/datastore/data/_grpc_conversion.py +++ b/src/ni/datastore/data/_grpc_conversion.py @@ -3,6 +3,7 @@ from __future__ import annotations import datetime as std_datetime +from itertools import chain import logging from typing import Iterable, cast @@ -162,13 +163,111 @@ def populate_publish_measurement_batch_request_values( if isinstance(values, Vector): publish_request.scalar_values.CopyFrom(vector_to_protobuf(values)) elif isinstance(values, Iterable): - if not values: - raise ValueError("Cannot publish an empty Iterable.") + values_iterator = iter(values) try: - vector = Vector(values) + first_value = next(values_iterator) + except StopIteration as exc: + raise ValueError("Cannot publish an empty Iterable.") from exc + + all_values = chain([first_value], values_iterator) + + if isinstance(first_value, Vector): + batch_values = publish_request.vector_values + + for value in all_values: + if not isinstance(value, Vector): + raise TypeError("Unsupported iterable: all values must be Vector.") + batch_values.vectors.add().CopyFrom(vector_to_protobuf(value)) + return + + if isinstance(first_value, AnalogWaveform): + if first_value.dtype == np.float64: + batch_values = publish_request.double_analog_waveform_values + + for value in all_values: + if not isinstance(value, AnalogWaveform) or value.dtype != np.float64: + raise TypeError( + "Unsupported iterable: all values must be float64 AnalogWaveform." + ) + batch_values.waveforms.add().CopyFrom(float64_analog_waveform_to_protobuf(value)) + return + if first_value.dtype == np.int16: + batch_values = publish_request.i16_analog_waveform_values + + for value in all_values: + if not isinstance(value, AnalogWaveform) or value.dtype != np.int16: + raise TypeError( + "Unsupported iterable: all values must be int16 AnalogWaveform." + ) + batch_values.waveforms.add().CopyFrom(int16_analog_waveform_to_protobuf(value)) + return + raise TypeError(f"Unsupported AnalogWaveform dtype: {first_value.dtype}") + return + + if isinstance(first_value, ComplexWaveform): + if first_value.dtype == np.complex128: + batch_values = publish_request.double_complex_waveform_values + + for value in all_values: + if not isinstance(value, ComplexWaveform) or value.dtype != np.complex128: + raise TypeError( + "Unsupported iterable: all values must be complex128 ComplexWaveform." + ) + batch_values.waveforms.add().CopyFrom(float64_complex_waveform_to_protobuf(value)) + return + if first_value.dtype == ComplexInt32DType: + batch_values = publish_request.i16_complex_waveform_values + + for value in all_values: + if not isinstance(value, ComplexWaveform) or value.dtype != ComplexInt32DType: + raise TypeError( + "Unsupported iterable: all values must be ComplexWaveform with ComplexInt32DType." + ) + batch_values.waveforms.add().CopyFrom(int16_complex_waveform_to_protobuf(value)) + return + raise TypeError(f"Unsupported ComplexWaveform dtype: {first_value.dtype}") + return + + if isinstance(first_value, Spectrum): + if first_value.dtype == np.float64: + batch_values = publish_request.double_spectrum_values + + for value in all_values: + if not isinstance(value, Spectrum) or value.dtype != np.float64: + raise TypeError("Unsupported iterable: all values must be float64 Spectrum.") + batch_values.waveforms.add().CopyFrom(float64_spectrum_to_protobuf(value)) + return + raise TypeError(f"Unsupported Spectrum dtype: {first_value.dtype}") + return + + if isinstance(first_value, DigitalWaveform): + batch_values = publish_request.digital_waveform_values + + for value in all_values: + if not isinstance(value, DigitalWaveform): + raise TypeError("Unsupported iterable: all values must be DigitalWaveform.") + batch_values.waveforms.add().CopyFrom(digital_waveform_to_protobuf(value)) + return + + if isinstance(first_value, XYData): + if first_value.dtype == np.float64: + batch_values = publish_request.x_y_data_values + + for value in all_values: + if not isinstance(value, XYData) or value.dtype != np.float64: + raise TypeError("Unsupported iterable: all values must be float64 XYData.") + batch_values.x_y_data.add().CopyFrom(float64_xydata_to_protobuf(value)) + return + raise TypeError(f"Unsupported XYData dtype: {first_value.dtype}") + return + + scalar_values = [first_value, *values_iterator] + try: + vector = Vector(cast(Iterable[bool | int | float | str], scalar_values)) except (TypeError, ValueError): raise TypeError( - f"Unsupported iterable: {values}. Subtype must be bool, float, int, or string." + f"Unsupported iterable. Subtype must be bool, float, int, string, Vector, " + "AnalogWaveform, ComplexWaveform, Spectrum, DigitalWaveform, or XYData." ) publish_request.scalar_values.CopyFrom(vector_to_protobuf(vector)) diff --git a/tests/unit/data/test_grpc_conversion.py b/tests/unit/data/test_grpc_conversion.py index c8893bbe..a7f7bd6c 100644 --- a/tests/unit/data/test_grpc_conversion.py +++ b/tests/unit/data/test_grpc_conversion.py @@ -208,7 +208,7 @@ def test___python_float64_xydata___populate_measurement___measurement_updated_co # ======================================================== # Populate Measurement Batch # ======================================================== -def test___python_vector_object___populate_measurement_batch___condition_updated_correctly() -> ( +def test___python_vector_object___populate_measurement_batch___measurement_updated_correctly() -> ( None ): vector_obj = Vector([1.0, 2.0, 3.0], "amps") @@ -218,3 +218,264 @@ def test___python_vector_object___populate_measurement_batch___condition_updated assert isinstance(request.scalar_values, vector_pb2.Vector) assert list(request.scalar_values.double_array.values) == [1.0, 2.0, 3.0] assert request.scalar_values.attributes["NI_UnitDescription"].string_value == "amps" + + +def test___python_vector_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: + values = [Vector([1.0, 2.0]), Vector([3.0, 4.0])] + request = PublishMeasurementBatchRequest() + + populate_publish_measurement_batch_request_values(request, values) + + assert len(request.vector_values.vectors) == 2 + assert list(request.vector_values.vectors[0].double_array.values) == [1.0, 2.0] + assert list(request.vector_values.vectors[1].double_array.values) == [3.0, 4.0] + + +def test___python_vector_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> None: + values = [Vector([1.0, 2.0]), AnalogWaveform(sample_count=2, raw_data=np.array([1.0, 2.0]))] + request = PublishMeasurementBatchRequest() + + with pytest.raises(TypeError): + populate_publish_measurement_batch_request_values(request, values) + + +def test___python_float64_analog_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: + values = [ + AnalogWaveform(sample_count=2, raw_data=np.array([1.25, -2.5], dtype=np.float64)), + AnalogWaveform(sample_count=3, raw_data=np.array([3.5, 4.75, -6.0], dtype=np.float64)), + ] + request = PublishMeasurementBatchRequest() + + populate_publish_measurement_batch_request_values(request, values) + + assert len(request.double_analog_waveform_values.waveforms) == 2 + assert list(request.double_analog_waveform_values.waveforms[0].y_data) == [1.25, -2.5] + assert list(request.double_analog_waveform_values.waveforms[1].y_data) == [3.5, 4.75, -6.0] + + +def test___python_float64_analog_waveform_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> None: + values = [ + AnalogWaveform(sample_count=2, raw_data=np.array([1.25, -2.5], dtype=np.float64)), + Vector([3.5, 4.75, -6.0]), + ] + request = PublishMeasurementBatchRequest() + + with pytest.raises(TypeError): + populate_publish_measurement_batch_request_values(request, values) + + +def test___python_float64_analog_waveform_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> None: + values = [ + AnalogWaveform(sample_count=2, raw_data=np.array([1.25, -2.5], dtype=np.float64)), + AnalogWaveform(sample_count=3, raw_data=np.array([7, 0, -8], dtype=np.int16)), + ] + request = PublishMeasurementBatchRequest() + + with pytest.raises(TypeError): + populate_publish_measurement_batch_request_values(request, values) + + +def test___python_int16_analog_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: + values = [ + AnalogWaveform(sample_count=2, raw_data=np.array([12, -3], dtype=np.int16)), + AnalogWaveform(sample_count=3, raw_data=np.array([7, 0, -8], dtype=np.int16)), + ] + request = PublishMeasurementBatchRequest() + + populate_publish_measurement_batch_request_values(request, values) + + assert len(request.i16_analog_waveform_values.waveforms) == 2 + assert list(request.i16_analog_waveform_values.waveforms[0].y_data) == [12, -3] + assert list(request.i16_analog_waveform_values.waveforms[1].y_data) == [7, 0, -8] + + +def test___python_int16_analog_waveform_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> None: + values = [ + AnalogWaveform(sample_count=2, raw_data=np.array([12, -3], dtype=np.int16)), + Vector([7.0, 0.0, -8.0]), + ] + request = PublishMeasurementBatchRequest() + + with pytest.raises(TypeError): + populate_publish_measurement_batch_request_values(request, values) + + +def test___python_int16_analog_waveform_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> None: + values = [ + AnalogWaveform(sample_count=2, raw_data=np.array([12, -3], dtype=np.int16)), + AnalogWaveform(sample_count=3, raw_data=np.array([3.5, 4.75, -6.0], dtype=np.float64)), + ] + request = PublishMeasurementBatchRequest() + + with pytest.raises(TypeError): + populate_publish_measurement_batch_request_values(request, values) + + +def test___python_float64_complex_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: + values = [ + ComplexWaveform(sample_count=2, raw_data=np.array([1.0 + 2.0j, -3.0 + 4.5j], dtype=np.complex128)), + ComplexWaveform( + sample_count=3, + raw_data=np.array([0.5 - 1.5j, 2.25 + 0.75j, -4.0 - 2.0j], dtype=np.complex128), + ), + ] + request = PublishMeasurementBatchRequest() + + populate_publish_measurement_batch_request_values(request, values) + + assert len(request.double_complex_waveform_values.waveforms) == 2 + assert list(request.double_complex_waveform_values.waveforms[0].y_data) == [1.0, 2.0, -3.0, 4.5] + assert list(request.double_complex_waveform_values.waveforms[1].y_data) == [0.5, -1.5, 2.25, 0.75, -4.0, -2.0] + + +def test___python_float64_complex_waveform_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> None: + values = [ + ComplexWaveform(sample_count=2, raw_data=np.array([1.0 + 2.0j, -3.0 + 4.5j], dtype=np.complex128)), + Vector([0.5, -1.5, 2.25, 0.75]), + ] + request = PublishMeasurementBatchRequest() + + with pytest.raises(TypeError): + populate_publish_measurement_batch_request_values(request, values) + + +def test___python_float64_complex_waveform_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> None: + values = [ + ComplexWaveform(sample_count=2, raw_data=np.array([1.0 + 2.0j, -3.0 + 4.5j], dtype=np.complex128)), + ComplexWaveform( + sample_count=3, + raw_data=np.array([(-7, 4), (0, -6), (8, 3)], dtype=ComplexInt32DType), + ), + ] + request = PublishMeasurementBatchRequest() + + with pytest.raises(TypeError): + populate_publish_measurement_batch_request_values(request, values) + + +def test___python_int16_complex_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: + values = [ + ComplexWaveform( + sample_count=2, + raw_data=np.array([(11, -2), (5, 9)], dtype=ComplexInt32DType), + ), + ComplexWaveform( + sample_count=3, + raw_data=np.array([(-7, 4), (0, -6), (8, 3)], dtype=ComplexInt32DType), + ), + ] + request = PublishMeasurementBatchRequest() + + populate_publish_measurement_batch_request_values(request, values) + + assert len(request.i16_complex_waveform_values.waveforms) == 2 + assert list(request.i16_complex_waveform_values.waveforms[0].y_data) == [11, -2, 5, 9] + assert list(request.i16_complex_waveform_values.waveforms[1].y_data) == [-7, 4, 0, -6, 8, 3] + + +def test___python_int16_complex_waveform_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> None: + values = [ + ComplexWaveform( + sample_count=2, + raw_data=np.array([(11, -2), (5, 9)], dtype=ComplexInt32DType), + ), + Vector([-7.0, 4.0, 0.0, -6.0]), + ] + request = PublishMeasurementBatchRequest() + + with pytest.raises(TypeError): + populate_publish_measurement_batch_request_values(request, values) + + +def test___python_int16_complex_waveform_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> None: + values = [ + ComplexWaveform( + sample_count=2, + raw_data=np.array([(11, -2), (5, 9)], dtype=ComplexInt32DType), + ), + ComplexWaveform( + sample_count=3, + raw_data=np.array([0.5 - 1.5j, 2.25 + 0.75j, -4.0 - 2.0j], dtype=np.complex128), + ), + ] + request = PublishMeasurementBatchRequest() + + with pytest.raises(TypeError): + populate_publish_measurement_batch_request_values(request, values) + + +def test___python_float64_spectrum_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: + values = [ + Spectrum.from_array_1d(np.array([1.0, 2.0])), + Spectrum.from_array_1d(np.array([3.0, 4.0])), + ] + request = PublishMeasurementBatchRequest() + + populate_publish_measurement_batch_request_values(request, values) + + assert len(request.double_spectrum_values.waveforms) == 2 + assert list(request.double_spectrum_values.waveforms[0].data) == [1.0, 2.0] + assert list(request.double_spectrum_values.waveforms[1].data) == [3.0, 4.0] + + +def test___python_float64_spectrum_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> None: + values = [ + Spectrum.from_array_1d(np.array([1.0, 2.0])), + Vector([3.0, 4.0]), + ] + request = PublishMeasurementBatchRequest() + + with pytest.raises(TypeError): + populate_publish_measurement_batch_request_values(request, values) + + +def test___python_uint8_digital_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: + values = [ + DigitalWaveform.from_lines([1], np.uint8), + DigitalWaveform.from_lines([0], np.uint8), + ] + request = PublishMeasurementBatchRequest() + + populate_publish_measurement_batch_request_values(request, values) + + assert len(request.digital_waveform_values.waveforms) == 2 + assert request.digital_waveform_values.waveforms[0].y_data == b"\x01" + assert request.digital_waveform_values.waveforms[1].y_data == b"\x00" + + +def test___python_uint8_digital_waveform_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> None: + values = [ + DigitalWaveform.from_lines([1], np.uint8), + Vector([0.0]), + ] + request = PublishMeasurementBatchRequest() + + with pytest.raises(TypeError): + populate_publish_measurement_batch_request_values(request, values) + + +def test___python_float64_xydata_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: + values = [ + XYData.from_arrays_1d([1.0], [2.0], np.float64), + XYData.from_arrays_1d([3.0], [4.0], np.float64), + ] + request = PublishMeasurementBatchRequest() + + populate_publish_measurement_batch_request_values(request, values) + + assert len(request.x_y_data_values.x_y_data) == 2 + assert list(request.x_y_data_values.x_y_data[0].x_data) == [1.0] + assert list(request.x_y_data_values.x_y_data[0].y_data) == [2.0] + assert list(request.x_y_data_values.x_y_data[1].x_data) == [3.0] + assert list(request.x_y_data_values.x_y_data[1].y_data) == [4.0] + + +def test___python_float64_xydata_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> None: + values = [ + XYData.from_arrays_1d([1.0], [2.0], np.float64), + Vector([3.0, 4.0]), + ] + request = PublishMeasurementBatchRequest() + + with pytest.raises(TypeError): + populate_publish_measurement_batch_request_values(request, values) diff --git a/tests/unit/data/test_publish_measurement.py b/tests/unit/data/test_publish_measurement.py index ac3742e7..580918d7 100644 --- a/tests/unit/data/test_publish_measurement.py +++ b/tests/unit/data/test_publish_measurement.py @@ -420,7 +420,7 @@ def test___unsupported_list___publish_measurement_batch___raises_type_error( step_id="step_id", ) - assert exc.value.args[0].startswith("Unsupported iterable:") + assert exc.value.args[0].startswith("Unsupported iterable.") def test___empty_list___publish_measurement_batch___raises_type_error( From 7aaf379e063a49d5e72b796b815ab763c7baea0f Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Fri, 15 May 2026 15:30:31 -0500 Subject: [PATCH 02/29] Adding test cases for unexpected dtype --- tests/unit/data/test_grpc_conversion.py | 47 +++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/unit/data/test_grpc_conversion.py b/tests/unit/data/test_grpc_conversion.py index a7f7bd6c..48418219 100644 --- a/tests/unit/data/test_grpc_conversion.py +++ b/tests/unit/data/test_grpc_conversion.py @@ -275,6 +275,17 @@ def test___python_float64_analog_waveform_iterable_with_mismatched_second_dtype_ populate_publish_measurement_batch_request_values(request, values) +def test___python_unsupported_dtype_analog_waveform_iterable___populate_measurement_batch___raises_error() -> None: + values = [ + AnalogWaveform(sample_count=2, raw_data=np.array([1.25, -2.5], dtype=np.float32)), + AnalogWaveform(sample_count=3, raw_data=np.array([3.5, 4.75, -6.0], dtype=np.float32)), + ] + request = PublishMeasurementBatchRequest() + + with pytest.raises(TypeError, match="Unsupported AnalogWaveform dtype"): + populate_publish_measurement_batch_request_values(request, values) + + def test___python_int16_analog_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: values = [ AnalogWaveform(sample_count=2, raw_data=np.array([12, -3], dtype=np.int16)), @@ -353,6 +364,20 @@ def test___python_float64_complex_waveform_iterable_with_mismatched_second_dtype populate_publish_measurement_batch_request_values(request, values) +def test___python_unsupported_dtype_complex_waveform_iterable___populate_measurement_batch___raises_error() -> None: + values = [ + ComplexWaveform(sample_count=2, raw_data=np.array([1.0 + 2.0j, -3.0 + 4.5j], dtype=np.complex64)), + ComplexWaveform( + sample_count=3, + raw_data=np.array([0.5 - 1.5j, 2.25 + 0.75j, -4.0 - 2.0j], dtype=np.complex64), + ), + ] + request = PublishMeasurementBatchRequest() + + with pytest.raises(TypeError, match="Unsupported ComplexWaveform dtype"): + populate_publish_measurement_batch_request_values(request, values) + + def test___python_int16_complex_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: values = [ ComplexWaveform( @@ -429,6 +454,17 @@ def test___python_float64_spectrum_iterable_with_mismatched_second_element___pop populate_publish_measurement_batch_request_values(request, values) +def test___python_unsupported_dtype_spectrum_iterable___populate_measurement_batch___raises_error() -> None: + values = [ + Spectrum.from_array_1d(np.array([1.0, 2.0], dtype=np.float32)), + Spectrum.from_array_1d(np.array([3.0, 4.0], dtype=np.float32)), + ] + request = PublishMeasurementBatchRequest() + + with pytest.raises(TypeError, match="Unsupported Spectrum dtype"): + populate_publish_measurement_batch_request_values(request, values) + + def test___python_uint8_digital_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: values = [ DigitalWaveform.from_lines([1], np.uint8), @@ -479,3 +515,14 @@ def test___python_float64_xydata_iterable_with_mismatched_second_element___popul with pytest.raises(TypeError): populate_publish_measurement_batch_request_values(request, values) + + +def test___python_unsupported_dtype_xydata_iterable___populate_measurement_batch___raises_error() -> None: + values = [ + XYData.from_arrays_1d([1.0], [2.0], np.float32), + XYData.from_arrays_1d([3.0], [4.0], np.float32), + ] + request = PublishMeasurementBatchRequest() + + with pytest.raises(TypeError, match="Unsupported XYData dtype"): + populate_publish_measurement_batch_request_values(request, values) From 3a34263b3742cb8c035405ed2477ac56cae0d00d Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Fri, 15 May 2026 15:41:15 -0500 Subject: [PATCH 03/29] Adding more tests for mismatched dtypes. --- tests/unit/data/test_grpc_conversion.py | 72 ++++++++++++++++--------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/tests/unit/data/test_grpc_conversion.py b/tests/unit/data/test_grpc_conversion.py index 48418219..892f9a76 100644 --- a/tests/unit/data/test_grpc_conversion.py +++ b/tests/unit/data/test_grpc_conversion.py @@ -275,17 +275,6 @@ def test___python_float64_analog_waveform_iterable_with_mismatched_second_dtype_ populate_publish_measurement_batch_request_values(request, values) -def test___python_unsupported_dtype_analog_waveform_iterable___populate_measurement_batch___raises_error() -> None: - values = [ - AnalogWaveform(sample_count=2, raw_data=np.array([1.25, -2.5], dtype=np.float32)), - AnalogWaveform(sample_count=3, raw_data=np.array([3.5, 4.75, -6.0], dtype=np.float32)), - ] - request = PublishMeasurementBatchRequest() - - with pytest.raises(TypeError, match="Unsupported AnalogWaveform dtype"): - populate_publish_measurement_batch_request_values(request, values) - - def test___python_int16_analog_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: values = [ AnalogWaveform(sample_count=2, raw_data=np.array([12, -3], dtype=np.int16)), @@ -322,6 +311,17 @@ def test___python_int16_analog_waveform_iterable_with_mismatched_second_dtype___ populate_publish_measurement_batch_request_values(request, values) +def test___python_unsupported_dtype_analog_waveform_iterable___populate_measurement_batch___raises_error() -> None: + values = [ + AnalogWaveform(sample_count=2, raw_data=np.array([1.25, -2.5], dtype=np.float32)), + AnalogWaveform(sample_count=3, raw_data=np.array([3.5, 4.75, -6.0], dtype=np.float32)), + ] + request = PublishMeasurementBatchRequest() + + with pytest.raises(TypeError, match="Unsupported AnalogWaveform dtype"): + populate_publish_measurement_batch_request_values(request, values) + + def test___python_float64_complex_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: values = [ ComplexWaveform(sample_count=2, raw_data=np.array([1.0 + 2.0j, -3.0 + 4.5j], dtype=np.complex128)), @@ -364,20 +364,6 @@ def test___python_float64_complex_waveform_iterable_with_mismatched_second_dtype populate_publish_measurement_batch_request_values(request, values) -def test___python_unsupported_dtype_complex_waveform_iterable___populate_measurement_batch___raises_error() -> None: - values = [ - ComplexWaveform(sample_count=2, raw_data=np.array([1.0 + 2.0j, -3.0 + 4.5j], dtype=np.complex64)), - ComplexWaveform( - sample_count=3, - raw_data=np.array([0.5 - 1.5j, 2.25 + 0.75j, -4.0 - 2.0j], dtype=np.complex64), - ), - ] - request = PublishMeasurementBatchRequest() - - with pytest.raises(TypeError, match="Unsupported ComplexWaveform dtype"): - populate_publish_measurement_batch_request_values(request, values) - - def test___python_int16_complex_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: values = [ ComplexWaveform( @@ -429,6 +415,20 @@ def test___python_int16_complex_waveform_iterable_with_mismatched_second_dtype__ populate_publish_measurement_batch_request_values(request, values) +def test___python_unsupported_dtype_complex_waveform_iterable___populate_measurement_batch___raises_error() -> None: + values = [ + ComplexWaveform(sample_count=2, raw_data=np.array([1.0 + 2.0j, -3.0 + 4.5j], dtype=np.complex64)), + ComplexWaveform( + sample_count=3, + raw_data=np.array([0.5 - 1.5j, 2.25 + 0.75j, -4.0 - 2.0j], dtype=np.complex64), + ), + ] + request = PublishMeasurementBatchRequest() + + with pytest.raises(TypeError, match="Unsupported ComplexWaveform dtype"): + populate_publish_measurement_batch_request_values(request, values) + + def test___python_float64_spectrum_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: values = [ Spectrum.from_array_1d(np.array([1.0, 2.0])), @@ -454,6 +454,17 @@ def test___python_float64_spectrum_iterable_with_mismatched_second_element___pop populate_publish_measurement_batch_request_values(request, values) +def test___python_float64_spectrum_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> None: + values = [ + Spectrum.from_array_1d(np.array([1.0, 2.0])), + Spectrum.from_array_1d(np.array([3.0, 4.0], dtype=np.float32)), + ] + request = PublishMeasurementBatchRequest() + + with pytest.raises(TypeError): + populate_publish_measurement_batch_request_values(request, values) + + def test___python_unsupported_dtype_spectrum_iterable___populate_measurement_batch___raises_error() -> None: values = [ Spectrum.from_array_1d(np.array([1.0, 2.0], dtype=np.float32)), @@ -517,6 +528,17 @@ def test___python_float64_xydata_iterable_with_mismatched_second_element___popul populate_publish_measurement_batch_request_values(request, values) +def test___python_float64_xydata_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> None: + values = [ + XYData.from_arrays_1d([1.0], [2.0], np.float64), + XYData.from_arrays_1d([3.0], [4.0], np.float32), + ] + request = PublishMeasurementBatchRequest() + + with pytest.raises(TypeError): + populate_publish_measurement_batch_request_values(request, values) + + def test___python_unsupported_dtype_xydata_iterable___populate_measurement_batch___raises_error() -> None: values = [ XYData.from_arrays_1d([1.0], [2.0], np.float32), From 335e0d713905ba67afd6a46ffa50d873df4c4606 Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Fri, 15 May 2026 15:43:18 -0500 Subject: [PATCH 04/29] Single-source type checking logic for populating PublishMeasurementBatchRequest with values. --- src/ni/datastore/data/_grpc_conversion.py | 131 ++++++++++++---------- 1 file changed, 70 insertions(+), 61 deletions(-) diff --git a/src/ni/datastore/data/_grpc_conversion.py b/src/ni/datastore/data/_grpc_conversion.py index 84cb9e89..76bf153c 100644 --- a/src/ni/datastore/data/_grpc_conversion.py +++ b/src/ni/datastore/data/_grpc_conversion.py @@ -5,7 +5,7 @@ import datetime as std_datetime from itertools import chain import logging -from typing import Iterable, cast +from typing import Any, Callable, Iterable, cast import hightime as ht import numpy as np @@ -160,6 +160,19 @@ def populate_publish_measurement_batch_request_values( publish_request: PublishMeasurementBatchRequest, values: object ) -> None: """Assign a value to the appropriate field of the PublishMeasurementBatchRequest object.""" + + def copy_batch_values( + repeated_field: Any, + batch_values: Iterable[object], + is_supported: Callable[[object], bool], + convert_value: Callable[[Any], Any], + error_message: str, + ) -> None: + for value in batch_values: + if not is_supported(value): + raise TypeError(error_message) + repeated_field.add().CopyFrom(convert_value(value)) + if isinstance(values, Vector): publish_request.scalar_values.CopyFrom(vector_to_protobuf(values)) elif isinstance(values, Iterable): @@ -172,94 +185,90 @@ def populate_publish_measurement_batch_request_values( all_values = chain([first_value], values_iterator) if isinstance(first_value, Vector): - batch_values = publish_request.vector_values - - for value in all_values: - if not isinstance(value, Vector): - raise TypeError("Unsupported iterable: all values must be Vector.") - batch_values.vectors.add().CopyFrom(vector_to_protobuf(value)) + copy_batch_values( + publish_request.vector_values.vectors, + all_values, + lambda value: isinstance(value, Vector), + vector_to_protobuf, + "Unsupported iterable: all values must be Vector.", + ) return if isinstance(first_value, AnalogWaveform): if first_value.dtype == np.float64: - batch_values = publish_request.double_analog_waveform_values - - for value in all_values: - if not isinstance(value, AnalogWaveform) or value.dtype != np.float64: - raise TypeError( - "Unsupported iterable: all values must be float64 AnalogWaveform." - ) - batch_values.waveforms.add().CopyFrom(float64_analog_waveform_to_protobuf(value)) + copy_batch_values( + publish_request.double_analog_waveform_values.waveforms, + all_values, + lambda value: isinstance(value, AnalogWaveform) and value.dtype == np.float64, + float64_analog_waveform_to_protobuf, + "Unsupported iterable: all values must be float64 AnalogWaveform.", + ) return if first_value.dtype == np.int16: - batch_values = publish_request.i16_analog_waveform_values - - for value in all_values: - if not isinstance(value, AnalogWaveform) or value.dtype != np.int16: - raise TypeError( - "Unsupported iterable: all values must be int16 AnalogWaveform." - ) - batch_values.waveforms.add().CopyFrom(int16_analog_waveform_to_protobuf(value)) + copy_batch_values( + publish_request.i16_analog_waveform_values.waveforms, + all_values, + lambda value: isinstance(value, AnalogWaveform) and value.dtype == np.int16, + int16_analog_waveform_to_protobuf, + "Unsupported iterable: all values must be int16 AnalogWaveform.", + ) return raise TypeError(f"Unsupported AnalogWaveform dtype: {first_value.dtype}") - return if isinstance(first_value, ComplexWaveform): if first_value.dtype == np.complex128: - batch_values = publish_request.double_complex_waveform_values - - for value in all_values: - if not isinstance(value, ComplexWaveform) or value.dtype != np.complex128: - raise TypeError( - "Unsupported iterable: all values must be complex128 ComplexWaveform." - ) - batch_values.waveforms.add().CopyFrom(float64_complex_waveform_to_protobuf(value)) + copy_batch_values( + publish_request.double_complex_waveform_values.waveforms, + all_values, + lambda value: isinstance(value, ComplexWaveform) and value.dtype == np.complex128, + float64_complex_waveform_to_protobuf, + "Unsupported iterable: all values must be complex128 ComplexWaveform.", + ) return if first_value.dtype == ComplexInt32DType: - batch_values = publish_request.i16_complex_waveform_values - - for value in all_values: - if not isinstance(value, ComplexWaveform) or value.dtype != ComplexInt32DType: - raise TypeError( - "Unsupported iterable: all values must be ComplexWaveform with ComplexInt32DType." - ) - batch_values.waveforms.add().CopyFrom(int16_complex_waveform_to_protobuf(value)) + copy_batch_values( + publish_request.i16_complex_waveform_values.waveforms, + all_values, + lambda value: isinstance(value, ComplexWaveform) and value.dtype == ComplexInt32DType, + int16_complex_waveform_to_protobuf, + "Unsupported iterable: all values must be ComplexWaveform with ComplexInt32DType.", + ) return raise TypeError(f"Unsupported ComplexWaveform dtype: {first_value.dtype}") - return if isinstance(first_value, Spectrum): if first_value.dtype == np.float64: - batch_values = publish_request.double_spectrum_values - - for value in all_values: - if not isinstance(value, Spectrum) or value.dtype != np.float64: - raise TypeError("Unsupported iterable: all values must be float64 Spectrum.") - batch_values.waveforms.add().CopyFrom(float64_spectrum_to_protobuf(value)) + copy_batch_values( + publish_request.double_spectrum_values.waveforms, + all_values, + lambda value: isinstance(value, Spectrum) and value.dtype == np.float64, + float64_spectrum_to_protobuf, + "Unsupported iterable: all values must be float64 Spectrum.", + ) return raise TypeError(f"Unsupported Spectrum dtype: {first_value.dtype}") - return if isinstance(first_value, DigitalWaveform): - batch_values = publish_request.digital_waveform_values - - for value in all_values: - if not isinstance(value, DigitalWaveform): - raise TypeError("Unsupported iterable: all values must be DigitalWaveform.") - batch_values.waveforms.add().CopyFrom(digital_waveform_to_protobuf(value)) + copy_batch_values( + publish_request.digital_waveform_values.waveforms, + all_values, + lambda value: isinstance(value, DigitalWaveform), + digital_waveform_to_protobuf, + "Unsupported iterable: all values must be DigitalWaveform.", + ) return if isinstance(first_value, XYData): if first_value.dtype == np.float64: - batch_values = publish_request.x_y_data_values - - for value in all_values: - if not isinstance(value, XYData) or value.dtype != np.float64: - raise TypeError("Unsupported iterable: all values must be float64 XYData.") - batch_values.x_y_data.add().CopyFrom(float64_xydata_to_protobuf(value)) + copy_batch_values( + publish_request.x_y_data_values.x_y_data, + all_values, + lambda value: isinstance(value, XYData) and value.dtype == np.float64, + float64_xydata_to_protobuf, + "Unsupported iterable: all values must be float64 XYData.", + ) return raise TypeError(f"Unsupported XYData dtype: {first_value.dtype}") - return scalar_values = [first_value, *values_iterator] try: From 22c581b3216ca84a06ceb23abddee50d9e8f0e34 Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Fri, 15 May 2026 15:55:47 -0500 Subject: [PATCH 05/29] Adding tests for populating PublishMeasurementBatchRequest from other types of (single) Vector and simple Iterables of primitive types. --- tests/unit/data/test_grpc_conversion.py | 72 +++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/tests/unit/data/test_grpc_conversion.py b/tests/unit/data/test_grpc_conversion.py index 892f9a76..da21cd12 100644 --- a/tests/unit/data/test_grpc_conversion.py +++ b/tests/unit/data/test_grpc_conversion.py @@ -208,18 +208,84 @@ def test___python_float64_xydata___populate_measurement___measurement_updated_co # ======================================================== # Populate Measurement Batch # ======================================================== -def test___python_vector_object___populate_measurement_batch___measurement_updated_correctly() -> ( +def test___python_double_vector_object___populate_measurement_batch___measurement_updated_correctly() -> ( None ): - vector_obj = Vector([1.0, 2.0, 3.0], "amps") + vector_obj = Vector([1.5, 2.5, 3.5], "amps") request = PublishMeasurementBatchRequest() populate_publish_measurement_batch_request_values(request, vector_obj) assert isinstance(request.scalar_values, vector_pb2.Vector) - assert list(request.scalar_values.double_array.values) == [1.0, 2.0, 3.0] + assert list(request.scalar_values.double_array.values) == [1.5, 2.5, 3.5] + assert request.scalar_values.attributes["NI_UnitDescription"].string_value == "amps" + + +def test___python_int_vector_object___populate_measurement_batch___measurement_updated_correctly() -> None: + vector_obj = Vector([1, 2, 3], "amps") + request = PublishMeasurementBatchRequest() + populate_publish_measurement_batch_request_values(request, vector_obj) + + assert isinstance(request.scalar_values, vector_pb2.Vector) + assert list(request.scalar_values.sint32_array.values) == [1, 2, 3] + assert request.scalar_values.attributes["NI_UnitDescription"].string_value == "amps" + + +def test___python_bool_vector_object___populate_measurement_batch___measurement_updated_correctly() -> None: + vector_obj = Vector([True, False, True], "amps") + request = PublishMeasurementBatchRequest() + populate_publish_measurement_batch_request_values(request, vector_obj) + + assert isinstance(request.scalar_values, vector_pb2.Vector) + assert list(request.scalar_values.bool_array.values) == [True, False, True] + assert request.scalar_values.attributes["NI_UnitDescription"].string_value == "amps" + + +def test___python_string_vector_object___populate_measurement_batch___measurement_updated_correctly() -> None: + vector_obj = Vector(["one", "two", "three"], "amps") + request = PublishMeasurementBatchRequest() + populate_publish_measurement_batch_request_values(request, vector_obj) + + assert isinstance(request.scalar_values, vector_pb2.Vector) + assert list(request.scalar_values.string_array.values) == ["one", "two", "three"] assert request.scalar_values.attributes["NI_UnitDescription"].string_value == "amps" +def test___python_double_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: + values = [1.5, 2.5, 3.5] + request = PublishMeasurementBatchRequest() + populate_publish_measurement_batch_request_values(request, values) + + assert isinstance(request.scalar_values, vector_pb2.Vector) + assert list(request.scalar_values.double_array.values) == [1.5, 2.5, 3.5] + + +def test___python_int_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: + values = [1, 2, 3] + request = PublishMeasurementBatchRequest() + populate_publish_measurement_batch_request_values(request, values) + + assert isinstance(request.scalar_values, vector_pb2.Vector) + assert list(request.scalar_values.sint32_array.values) == [1, 2, 3] + + +def test___python_bool_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: + values = [True, False, True] + request = PublishMeasurementBatchRequest() + populate_publish_measurement_batch_request_values(request, values) + + assert isinstance(request.scalar_values, vector_pb2.Vector) + assert list(request.scalar_values.bool_array.values) == [True, False, True] + + +def test___python_string_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: + values = ["one", "two", "three"] + request = PublishMeasurementBatchRequest() + populate_publish_measurement_batch_request_values(request, values) + + assert isinstance(request.scalar_values, vector_pb2.Vector) + assert list(request.scalar_values.string_array.values) == ["one", "two", "three"] + + def test___python_vector_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: values = [Vector([1.0, 2.0]), Vector([3.0, 4.0])] request = PublishMeasurementBatchRequest() From 07ee6a7b5a309906a04b53cc3de63ed895428116 Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Fri, 15 May 2026 16:19:11 -0500 Subject: [PATCH 06/29] Strengthen test assertions in error cases --- tests/unit/data/test_grpc_conversion.py | 28 ++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/unit/data/test_grpc_conversion.py b/tests/unit/data/test_grpc_conversion.py index da21cd12..af674ed1 100644 --- a/tests/unit/data/test_grpc_conversion.py +++ b/tests/unit/data/test_grpc_conversion.py @@ -301,7 +301,7 @@ def test___python_vector_iterable_with_mismatched_second_element___populate_meas values = [Vector([1.0, 2.0]), AnalogWaveform(sample_count=2, raw_data=np.array([1.0, 2.0]))] request = PublishMeasurementBatchRequest() - with pytest.raises(TypeError): + with pytest.raises(TypeError, match="Unsupported iterable"): populate_publish_measurement_batch_request_values(request, values) @@ -326,7 +326,7 @@ def test___python_float64_analog_waveform_iterable_with_mismatched_second_elemen ] request = PublishMeasurementBatchRequest() - with pytest.raises(TypeError): + with pytest.raises(TypeError, match="Unsupported iterable"): populate_publish_measurement_batch_request_values(request, values) @@ -337,7 +337,7 @@ def test___python_float64_analog_waveform_iterable_with_mismatched_second_dtype_ ] request = PublishMeasurementBatchRequest() - with pytest.raises(TypeError): + with pytest.raises(TypeError, match="Unsupported iterable"): populate_publish_measurement_batch_request_values(request, values) @@ -362,7 +362,7 @@ def test___python_int16_analog_waveform_iterable_with_mismatched_second_element_ ] request = PublishMeasurementBatchRequest() - with pytest.raises(TypeError): + with pytest.raises(TypeError, match="Unsupported iterable"): populate_publish_measurement_batch_request_values(request, values) @@ -373,7 +373,7 @@ def test___python_int16_analog_waveform_iterable_with_mismatched_second_dtype___ ] request = PublishMeasurementBatchRequest() - with pytest.raises(TypeError): + with pytest.raises(TypeError, match="Unsupported iterable"): populate_publish_measurement_batch_request_values(request, values) @@ -412,7 +412,7 @@ def test___python_float64_complex_waveform_iterable_with_mismatched_second_eleme ] request = PublishMeasurementBatchRequest() - with pytest.raises(TypeError): + with pytest.raises(TypeError, match="Unsupported iterable"): populate_publish_measurement_batch_request_values(request, values) @@ -426,7 +426,7 @@ def test___python_float64_complex_waveform_iterable_with_mismatched_second_dtype ] request = PublishMeasurementBatchRequest() - with pytest.raises(TypeError): + with pytest.raises(TypeError, match="Unsupported iterable"): populate_publish_measurement_batch_request_values(request, values) @@ -460,7 +460,7 @@ def test___python_int16_complex_waveform_iterable_with_mismatched_second_element ] request = PublishMeasurementBatchRequest() - with pytest.raises(TypeError): + with pytest.raises(TypeError, match="Unsupported iterable"): populate_publish_measurement_batch_request_values(request, values) @@ -477,7 +477,7 @@ def test___python_int16_complex_waveform_iterable_with_mismatched_second_dtype__ ] request = PublishMeasurementBatchRequest() - with pytest.raises(TypeError): + with pytest.raises(TypeError, match="Unsupported iterable"): populate_publish_measurement_batch_request_values(request, values) @@ -516,7 +516,7 @@ def test___python_float64_spectrum_iterable_with_mismatched_second_element___pop ] request = PublishMeasurementBatchRequest() - with pytest.raises(TypeError): + with pytest.raises(TypeError, match="Unsupported iterable"): populate_publish_measurement_batch_request_values(request, values) @@ -527,7 +527,7 @@ def test___python_float64_spectrum_iterable_with_mismatched_second_dtype___popul ] request = PublishMeasurementBatchRequest() - with pytest.raises(TypeError): + with pytest.raises(TypeError, match="Unsupported iterable"): populate_publish_measurement_batch_request_values(request, values) @@ -563,7 +563,7 @@ def test___python_uint8_digital_waveform_iterable_with_mismatched_second_element ] request = PublishMeasurementBatchRequest() - with pytest.raises(TypeError): + with pytest.raises(TypeError, match="Unsupported iterable"): populate_publish_measurement_batch_request_values(request, values) @@ -590,7 +590,7 @@ def test___python_float64_xydata_iterable_with_mismatched_second_element___popul ] request = PublishMeasurementBatchRequest() - with pytest.raises(TypeError): + with pytest.raises(TypeError, match="Unsupported iterable"): populate_publish_measurement_batch_request_values(request, values) @@ -601,7 +601,7 @@ def test___python_float64_xydata_iterable_with_mismatched_second_dtype___populat ] request = PublishMeasurementBatchRequest() - with pytest.raises(TypeError): + with pytest.raises(TypeError, match="Unsupported iterable"): populate_publish_measurement_batch_request_values(request, values) From bdd5a08d4fc6345ffb2051d5c26cc44d01fb4765 Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Mon, 18 May 2026 12:36:39 -0500 Subject: [PATCH 07/29] Adding further test cases for error scenarios when populating PublishMeasurementBatchRequest. --- tests/unit/data/test_grpc_conversion.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/unit/data/test_grpc_conversion.py b/tests/unit/data/test_grpc_conversion.py index af674ed1..79812e3e 100644 --- a/tests/unit/data/test_grpc_conversion.py +++ b/tests/unit/data/test_grpc_conversion.py @@ -614,3 +614,21 @@ def test___python_unsupported_dtype_xydata_iterable___populate_measurement_batch with pytest.raises(TypeError, match="Unsupported XYData dtype"): populate_publish_measurement_batch_request_values(request, values) + + +def test___empty_iterable___populate_measurement_batch___raises_error() -> None: + request = PublishMeasurementBatchRequest() + + with pytest.raises(ValueError, match="Cannot publish an empty Iterable\."): + populate_publish_measurement_batch_request_values(request, []) + + +def test___python_unsupported_iterable___populate_measurement_batch___raises_error() -> None: + values = [object(), object()] + request = PublishMeasurementBatchRequest() + + with pytest.raises( + TypeError, + match="Unsupported iterable\. Subtype must be", + ): + populate_publish_measurement_batch_request_values(request, values) From b54be785dfdceae465d1390d9bd602d4fc0a3e0a Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Mon, 18 May 2026 12:55:15 -0500 Subject: [PATCH 08/29] Adding acceptance tests for successfully publishing non-scalar measurement data as a batch --- ...publish_measurement_batch_and_read_data.py | 90 +++++++++++++++++-- 1 file changed, 85 insertions(+), 5 deletions(-) diff --git a/tests/acceptance/test_publish_measurement_batch_and_read_data.py b/tests/acceptance/test_publish_measurement_batch_and_read_data.py index d148a51c..0d0779ac 100644 --- a/tests/acceptance/test_publish_measurement_batch_and_read_data.py +++ b/tests/acceptance/test_publish_measurement_batch_and_read_data.py @@ -1,20 +1,22 @@ """Acceptance tests that publish various batch measurement values then reads the data back.""" +import numpy as np from ni.datastore.data import ( DataStoreClient, Step, TestResult, ) from nitypes.vector import Vector +from nitypes.waveform import AnalogWaveform from utilities import DataStoreContext -def test___publish_float___read_measurement_value_returns_vector( +def test___publish_batch_floats___read_measurement_value_returns_vector( acceptance_test_context: DataStoreContext, ) -> None: with DataStoreClient() as data_store_client: # Create TestResult metadata - test_result_name = "python batch publish float acceptance test" + test_result_name = "python publish batch floats acceptance test" test_result = TestResult(name=test_result_name) test_result_id = data_store_client.create_test_result(test_result) @@ -22,7 +24,7 @@ def test___publish_float___read_measurement_value_returns_vector( step = Step(name="Initial step", test_result_id=test_result_id) step_id = data_store_client.create_step(step) published_measurement_ids = data_store_client.publish_measurement_batch( - name="python batch publish float", + name="Test measurement", values=[1.0, 2.0, 3.0, 4.0], step_id=step_id, ) @@ -45,7 +47,7 @@ def test___publish_batch_vector___read_measurement_value_returns_vector( ) -> None: with DataStoreClient() as data_store_client: # Create TestResult metadata - test_result_name = "python publish scalar acceptance test" + test_result_name = "python publish batch Vector acceptance test" test_result = TestResult(name=test_result_name) test_result_id = data_store_client.create_test_result(test_result) @@ -56,7 +58,7 @@ def test___publish_batch_vector___read_measurement_value_returns_vector( step = Step(name="Initial step", test_result_id=test_result_id) step_id = data_store_client.create_step(step) published_measurement_ids = data_store_client.publish_measurement_batch( - name="python publish scalar", + name="Test measurement", values=expected_vector, step_id=step_id, ) @@ -71,3 +73,81 @@ def test___publish_batch_vector___read_measurement_value_returns_vector( published_measurement, expected_type=Vector ) assert vector == expected_vector + + +def test___publish_batch_double_analog_waveforms___read_measurement_value_returns_analog_waveform( + acceptance_test_context: DataStoreContext, +) -> None: + with DataStoreClient() as data_store_client: + test_result_name = "python publish batch AnalogWaveforms acceptance test" + test_result = TestResult(name=test_result_name) + test_result_id = data_store_client.create_test_result(test_result) + expected_waveforms = [ + AnalogWaveform(sample_count=3, raw_data=np.array([1.0, 2.0, 3.0], dtype=np.float64)), + AnalogWaveform(sample_count=3, raw_data=np.array([4.0, 5.0, 6.0], dtype=np.float64)), + ] + step = Step(name="Initial step", test_result_id=test_result_id) + step_id = data_store_client.create_step(step) + + published_measurement_ids = list( + data_store_client.publish_measurement_batch( + name="Test measurement", + values=expected_waveforms, + step_id=step_id, + ) + ) + + assert len(published_measurement_ids) == 2 + published_measurement_one = data_store_client.get_measurement( + published_measurement_ids[0] + ) + published_measurement_two = data_store_client.get_measurement( + published_measurement_ids[1] + ) + published_waveform_one = data_store_client.read_measurement_value( + published_measurement_one, expected_type=AnalogWaveform + ) + published_waveform_two = data_store_client.read_measurement_value( + published_measurement_two, expected_type=AnalogWaveform + ) + assert published_waveform_one == expected_waveforms[0] + assert published_waveform_two == expected_waveforms[1] + + +def test___publish_batch_vectors___read_measurement_value_returns_vector( + acceptance_test_context: DataStoreContext, +) -> None: + with DataStoreClient() as data_store_client: + test_result_name = "python publish batch Vectors acceptance test" + test_result = TestResult(name=test_result_name) + test_result_id = data_store_client.create_test_result(test_result) + expected_vectors = [ + Vector(values=[1, 2, 3], units="Volts"), + Vector(values=[4, 5, 6], units="Volts"), + ] + step = Step(name="Initial step", test_result_id=test_result_id) + step_id = data_store_client.create_step(step) + + published_measurement_ids = list( + data_store_client.publish_measurement_batch( + name="Test measurement", + values=expected_vectors, + step_id=step_id, + ) + ) + + assert len(published_measurement_ids) == 2 + published_measurement_one = data_store_client.get_measurement( + published_measurement_ids[0] + ) + published_measurement_two = data_store_client.get_measurement( + published_measurement_ids[1] + ) + published_vector_one = data_store_client.read_measurement_value( + published_measurement_one, expected_type=Vector + ) + published_vector_two = data_store_client.read_measurement_value( + published_measurement_two, expected_type=Vector + ) + assert published_vector_one == expected_vectors[0] + assert published_vector_two == expected_vectors[1] From 0f554addbe9177119a90588e5820a022c0e0a888 Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Mon, 18 May 2026 15:27:07 -0500 Subject: [PATCH 09/29] Refactor - change 'if' statements to 'else if' statements for greater clarity. --- src/ni/datastore/data/_grpc_conversion.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ni/datastore/data/_grpc_conversion.py b/src/ni/datastore/data/_grpc_conversion.py index 76bf153c..23c7ae77 100644 --- a/src/ni/datastore/data/_grpc_conversion.py +++ b/src/ni/datastore/data/_grpc_conversion.py @@ -204,7 +204,7 @@ def copy_batch_values( "Unsupported iterable: all values must be float64 AnalogWaveform.", ) return - if first_value.dtype == np.int16: + elif first_value.dtype == np.int16: copy_batch_values( publish_request.i16_analog_waveform_values.waveforms, all_values, @@ -225,7 +225,7 @@ def copy_batch_values( "Unsupported iterable: all values must be complex128 ComplexWaveform.", ) return - if first_value.dtype == ComplexInt32DType: + elif first_value.dtype == ComplexInt32DType: copy_batch_values( publish_request.i16_complex_waveform_values.waveforms, all_values, From 2cdf3dcb43c92b9a2cf48903e9dc5f09dcd57e62 Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Mon, 18 May 2026 16:54:22 -0500 Subject: [PATCH 10/29] Update 'ni.measurements.data.v1.client' dependency version --- poetry.lock | 36 ++++++++++++++++++------------------ pyproject.toml | 2 +- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/poetry.lock b/poetry.lock index b33877ab..dbd3a2c9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -1408,7 +1408,7 @@ fqdn = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} idna = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} isoduration = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format-nongpl\""} -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} rfc3986-validator = {version = ">0.1.0", optional = true, markers = "extra == \"format-nongpl\""} @@ -2447,36 +2447,36 @@ protobuf = ">=4.21" [[package]] name = "ni-measurements-data-v1-client" -version = "1.1.0.dev0" +version = "1.1.0.dev1" description = "gRPC Client for NI Data Store Service" optional = false python-versions = "<4.0,>=3.10" groups = ["main"] files = [ - {file = "ni_measurements_data_v1_client-1.1.0.dev0-py3-none-any.whl", hash = "sha256:d41f93ff1584461ef45dd4ea25c3ceaf763da53c0a2cc5e48c64f675e4ba3c00"}, - {file = "ni_measurements_data_v1_client-1.1.0.dev0.tar.gz", hash = "sha256:3043ef784d6dec4f476f3065e781ce8a1f2db38900c3a0ef5d90d6c08a9fa466"}, + {file = "ni_measurements_data_v1_client-1.1.0.dev1-py3-none-any.whl", hash = "sha256:556ea5e16dc9121c678bf4047c4f911226028f0880b0f64a97408e7863d8426c"}, + {file = "ni_measurements_data_v1_client-1.1.0.dev1.tar.gz", hash = "sha256:b006257e5fccd7842cd3f93b787be5f84a8ccf2af9b76dbec537ae4d139d4226"}, ] [package.dependencies] ni-measurementlink-discovery-v1-client = ">=1.1.0" -ni-measurements-data-v1-proto = ">=1.1.0.dev0" +ni-measurements-data-v1-proto = ">=1.1.0.dev1" [[package]] name = "ni-measurements-data-v1-proto" -version = "1.1.0.dev0" +version = "1.1.0.dev1" description = "Protobuf data types and service stubs for NI data store gRPC APIs" optional = false python-versions = "<4.0,>=3.10" groups = ["main"] files = [ - {file = "ni_measurements_data_v1_proto-1.1.0.dev0-py3-none-any.whl", hash = "sha256:c1e5ad669ab978c7202f58fac7eda2e19be2a6fcc4b07bc13f1904a5aad43809"}, - {file = "ni_measurements_data_v1_proto-1.1.0.dev0.tar.gz", hash = "sha256:99102d9e785031ce0797efa435fce975e9a0de8547b76079aaa7e35c871e7da4"}, + {file = "ni_measurements_data_v1_proto-1.1.0.dev1-py3-none-any.whl", hash = "sha256:a76ef8079f6d66d0ba3c577f20babacf8a15be196a343b5fa54b465c215de268"}, + {file = "ni_measurements_data_v1_proto-1.1.0.dev1.tar.gz", hash = "sha256:656220c42dcb94ef9d360bb19c1f852d215cd34299adb67fdbf782c7484ec53b"}, ] [package.dependencies] ni-datamonikers-v1-proto = ">=1.0.0" ni-measurements-metadata-v1-proto = ">=1.0.0" -ni-protobuf-types = ">=1.1.0" +ni-protobuf-types = ">=1.2.0.dev0" protobuf = ">=4.21" [[package]] @@ -2512,18 +2512,18 @@ protobuf = ">=4.21" [[package]] name = "ni-protobuf-types" -version = "1.1.0" +version = "1.2.0.dev0" description = "Protobuf data types for NI gRPC APIs" optional = false python-versions = "<4.0,>=3.10" groups = ["main"] files = [ - {file = "ni_protobuf_types-1.1.0-py3-none-any.whl", hash = "sha256:0c21c096cf8577483dade081c571305fe8d4cc759ce2c780e7437129a375942c"}, - {file = "ni_protobuf_types-1.1.0.tar.gz", hash = "sha256:98f0583405e219f6e128133c2f6c033f03cd83ebd3ce8098ad74ab99b8a253c1"}, + {file = "ni_protobuf_types-1.2.0.dev0-py3-none-any.whl", hash = "sha256:9e06049582d8eb0b7412a3fdbb628c45f8adb1d1f26107959ce2eb469c1dc1c8"}, + {file = "ni_protobuf_types-1.2.0.dev0.tar.gz", hash = "sha256:6d9ce29fd577d9d9b6da69fe882b894d8db5303a1f4b5dfb584603234f746463"}, ] [package.dependencies] -nitypes = ">=1.1.0dev1" +nitypes = ">=1.1.0.dev1" protobuf = ">=4.21" [[package]] @@ -2542,8 +2542,8 @@ files = [ black = ">=23.1,<26.0" click = ">=7.1.2" flake8 = [ - {version = ">=5.0,<6.0", markers = "python_version >= \"3.7\" and python_version < \"3.12\""}, {version = ">=6.1,<7.0", markers = "python_version >= \"3.12\" and python_version < \"4.0\""}, + {version = ">=5.0,<6.0", markers = "python_version >= \"3.7\" and python_version < \"3.12\""}, ] flake8-black = ">=0.2.1" flake8-docstrings = ">=1.5.0" @@ -2552,8 +2552,8 @@ isort = ">=5.10" pathspec = ">=0.11.1" pep8-naming = ">=0.11.1" pycodestyle = [ - {version = ">=2.9,<3.0", markers = "python_version >= \"3.7\" and python_version < \"3.12\""}, {version = ">=2.11,<3.0", markers = "python_version >= \"3.12\" and python_version < \"4.0\""}, + {version = ">=2.9,<3.0", markers = "python_version >= \"3.7\" and python_version < \"3.12\""}, ] setuptools = "<82" toml = ">=0.10.1" @@ -2573,8 +2573,8 @@ files = [ [package.dependencies] hightime = ">=0.2.2" numpy = [ - {version = ">=1.22", markers = "python_version >= \"3.9\" and python_version < \"3.13\""}, {version = ">=2.1", markers = "python_version >= \"3.13\" and python_version < \"4.0\""}, + {version = ">=1.22", markers = "python_version >= \"3.9\" and python_version < \"3.13\""}, ] typing-extensions = ">=4.13.2" @@ -4469,4 +4469,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "7b3289fda614c93cbdfd701d40173d131938304eb1870c23d40bb934fdb723b1" +content-hash = "8d65fe6510964e28aefc1f5b3337ce929cf90ad21f433d6738e64521b0dc8827" diff --git a/pyproject.toml b/pyproject.toml index 38247865..0631e414 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ requires-poetry = '>=2.1,<3.0' [tool.poetry.dependencies] python = "^3.10" protobuf = {version=">=4.21"} -ni-measurements-data-v1-client = { version = ">=1.1.0dev0", allow-prereleases = true } +ni-measurements-data-v1-client = { version = ">=1.1.0dev1", allow-prereleases = true } ni-measurements-metadata-v1-client = { version = ">=1.0.0" } ni-protobuf-types = { version = ">=1.1.0" } hightime = { version = ">=1.0.0" } From b2b931d3527e1eb9c3a4535bee566c3d9fb0b59b Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Tue, 19 May 2026 14:02:27 -0500 Subject: [PATCH 11/29] Update PublishConditionBatch test name for consistency with PublishMeasurementBatch test names --- tests/unit/data/test_grpc_conversion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/data/test_grpc_conversion.py b/tests/unit/data/test_grpc_conversion.py index 79812e3e..119b2cc3 100644 --- a/tests/unit/data/test_grpc_conversion.py +++ b/tests/unit/data/test_grpc_conversion.py @@ -61,7 +61,7 @@ def test___python_scalar_object___populate_condition___condition_updated_correct # ======================================================== # Populate Condition Batch # ======================================================== -def test___python_vector_object___populate_batch_condition___condition_updated_correctly() -> None: +def test___python_vector_object___populate_condition_batch___condition_updated_correctly() -> None: vector_obj = Vector([1.0, 2.0, 3.0], "amps") request = PublishConditionBatchRequest() populate_publish_condition_batch_request_values(request, vector_obj) From e183efa4517774d7063f35723871e9d619276573 Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Tue, 19 May 2026 14:52:11 -0500 Subject: [PATCH 12/29] Adding example for publish_condition_batch and publish_measurement_batch --- .../notebooks/publish/publish_batch.ipynb | 452 ++++++++++++++++++ 1 file changed, 452 insertions(+) create mode 100644 examples/notebooks/publish/publish_batch.ipynb diff --git a/examples/notebooks/publish/publish_batch.ipynb b/examples/notebooks/publish/publish_batch.ipynb new file mode 100644 index 00000000..9d2df4fb --- /dev/null +++ b/examples/notebooks/publish/publish_batch.ipynb @@ -0,0 +1,452 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e524afa7", + "metadata": {}, + "source": [ + "# Batch Publishing\n", + "\n", + "This notebook demonstrates how to use the `DataStoreClient` to batch publish N iterations of data within a given `Step`. Rather than calling `publish_condition` or `publish_measurement` N times to publish data for each of these N iterations, this data can instead be published by a single call to `publish_condition_batch` or `publish_measurement_batch`, respectively. Batch publishing can help improve overall publishing performance.\n", + "\n", + "**Note:** These batching APIs handle batch publishing N iterations of data for a single condition or measurement with the specified name. They do *not* support publishing data across multiple (distinctly named) conditions or multiple (distinctly named) measurements at once." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6876aeb1", + "metadata": {}, + "outputs": [], + "source": [ + "# Perform example-specific setup with the DataStoreContext. This is not needed when writing production code.\n", + "from utilities import DataStoreContext\n", + "data_store_context = DataStoreContext()\n", + "data_store_context.initialize()\n", + "\n", + "# Create the TestResult and Step into which we will later batch publish conditions and measurements.\n", + "from ni.datastore.data import DataStoreClient, Step, TestResult\n", + "\n", + "data_store_client = DataStoreClient()\n", + "\n", + "test_result = TestResult(name=\"Batch Publish Example\")\n", + "test_result_id = data_store_client.create_test_result(test_result)\n", + "\n", + "step = Step(name=\"Example Step\", test_result_id=test_result_id)\n", + "step_id = data_store_client.create_step(step)\n", + "\n", + "print(f\"Created Test Result: {test_result_id}\")\n", + "print(f\"Created Step: {step_id}\")" + ] + }, + { + "cell_type": "markdown", + "id": "94b7bc49", + "metadata": {}, + "source": [ + "## Batch Publishing Condition Values\n", + "\n", + "The `publish_condition_batch` method of `DataStoreClient` can be used to publish the value of a given condition across N parametric iterations at once. This usage of `publish_condition_batch` is equivalent to calling `publish_condition` for that same condition N times.\n", + "\n", + "The condition values themselves may be supplied to `publish_condition_batch` as either an `Iterable` or a `Vector`.\n", + "\n", + "Supported element types of the `Iterable` are `float`, `int`, `str`, and `bool`: " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c7c5e72", + "metadata": {}, + "outputs": [], + "source": [ + "float_condition_id = data_store_client.publish_condition_batch(\n", + " name=\"Example Float Condition\",\n", + " condition_type=\"Setup\",\n", + " values=[1.25, 2.5, 3.75, 5.0],\n", + " step_id=step_id,\n", + " )\n", + "\n", + "integer_condition_id = data_store_client.publish_condition_batch(\n", + " name=\"Example Integer Condition\",\n", + " condition_type=\"Setup\",\n", + " values=[1, 2, 3, 4],\n", + " step_id=step_id,\n", + " )\n", + "\n", + "string_condition_id = data_store_client.publish_condition_batch(\n", + " name=\"Example String Condition\",\n", + " condition_type=\"Setup\",\n", + " values=[\"cold\", \"ambient\", \"warm\", \"hot\"],\n", + " step_id=step_id,\n", + " )\n", + "\n", + "bool_condition_id = data_store_client.publish_condition_batch(\n", + " name=\"Example Bool Condition\",\n", + " condition_type=\"Setup\",\n", + " values=[True, False, True, False],\n", + " step_id=step_id,\n", + " )\n", + "\n", + "print(f\"Published Example Float Condition ID: {float_condition_id}\")\n", + "print(f\"Published Example Integer Condition ID: {integer_condition_id}\")\n", + "print(f\"Published Example String Condition ID: {string_condition_id}\")\n", + "print(f\"Published Example Bool Condition ID: {bool_condition_id}\")" + ] + }, + { + "cell_type": "markdown", + "id": "1056ff7a", + "metadata": {}, + "source": [ + "Supplying a `Vector` allows the client to specify additional information, such as units:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "73e3a3ba", + "metadata": {}, + "outputs": [], + "source": [ + "from nitypes.vector import Vector\n", + "\n", + "condition_vector = Vector(values=[0.5, 1.0, 1.5, 2.0], units=\"Amps\")\n", + "\n", + "vector_condition_id = data_store_client.publish_condition_batch(\n", + " name=\"Example Published-As-Vector Condition\",\n", + " condition_type=\"Setup\",\n", + " values=condition_vector,\n", + " step_id=step_id,\n", + " )\n", + "\n", + "print(f\"Published Example Published-As-Vector Condition ID: {vector_condition_id}\")" + ] + }, + { + "cell_type": "markdown", + "id": "67347f3d", + "metadata": {}, + "source": [ + "Published condition values can be read back in the same manner as when reading back condition values that were published individually via `publish_condition`.\n", + "\n", + "More specifically, condition values published via `publish_condition_batch` are read back as a `Vector` containing all N iterations of parametric data published for a particular condition:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e72da95e", + "metadata": {}, + "outputs": [], + "source": [ + "published_float_condition = data_store_client.get_condition(float_condition_id)\n", + "published_integer_condition = data_store_client.get_condition(integer_condition_id)\n", + "published_string_condition = data_store_client.get_condition(string_condition_id)\n", + "published_bool_condition = data_store_client.get_condition(bool_condition_id)\n", + "published_as_vector_condition = data_store_client.get_condition(vector_condition_id)\n", + "\n", + "read_back_float_vector = data_store_client.read_condition_value(published_float_condition, expected_type=Vector)\n", + "read_back_integer_vector = data_store_client.read_condition_value(published_integer_condition, expected_type=Vector)\n", + "read_back_string_vector = data_store_client.read_condition_value(published_string_condition, expected_type=Vector)\n", + "read_back_bool_vector = data_store_client.read_condition_value(published_bool_condition, expected_type=Vector)\n", + "read_back_vector = data_store_client.read_condition_value(published_as_vector_condition, expected_type=Vector)\n", + "\n", + "print(f\"Read Example Float Condition: {read_back_float_vector}\")\n", + "print(f\"Read Example Integer Condition: {read_back_integer_vector}\")\n", + "print(f\"Read Example String Condition: {read_back_string_vector}\")\n", + "print(f\"Read Example Bool Condition: {read_back_bool_vector}\")\n", + "print(f\"Read Example Published-As-Vector Condition: {read_back_vector}\")" + ] + }, + { + "cell_type": "markdown", + "id": "5bf5cab8", + "metadata": {}, + "source": [ + "## Batch Publishing Measurement Values\n", + "\n", + "The `publish_measurement_batch` method of `DataStoreClient` can similarly be used to publish the value of a given measurement across N parametric iterations at once. This usage of `publish_measurement_batch` is equivalent to calling `publish_measurement` for that same measurement N times.\n", + "\n", + "This is conceptually similar to the batch publishing of condition values. One important note, however, is that whereas conditions only support a given publish iteration containing a single scalar value, measurements support both **scalar** and **non-scalar** values.\n", + "\n", + "Examples of supported non-scalar types are `AnalogWaveform` and `Vector`. The process of batch publishing both scalar and non-scalar measurement values is similar, though as is the case for publishing scalar and non-scalar measurement values using the non-batched `publish_measurement` method, the process of later reading that measurement data from Measurement Data Services is slightly different between the two cases, as shown below.\n", + "\n", + "### Scalar Measurements\n", + "\n", + "Scalar measurement values may be supplied to `publish_measurement_batch` as either an `Iterable` or a `Vector`.\n", + "\n", + "Supported element types of the `Iterable` are `float`, `int`, `str`, and `bool`: " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63c8e0b5", + "metadata": {}, + "outputs": [], + "source": [ + "float_measurement_ids = data_store_client.publish_measurement_batch(\n", + " name=\"Example Float Measurement\",\n", + " values=[0.125, 0.25, 0.5, 1.0],\n", + " step_id=step_id,\n", + " )\n", + "\n", + "integer_measurement_ids = data_store_client.publish_measurement_batch(\n", + " name=\"Example Integer Measurement\",\n", + " values=[10, 20, 30, 40],\n", + " step_id=step_id,\n", + " )\n", + "\n", + "string_measurement_ids = data_store_client.publish_measurement_batch(\n", + " name=\"Example String Measurement\",\n", + " values=[\"nominal\", \"warning\", \"critical\", \"retest\"],\n", + " step_id=step_id,\n", + " )\n", + "\n", + "bool_measurement_ids = data_store_client.publish_measurement_batch(\n", + " name=\"Example Bool Measurement\",\n", + " values=[False, False, True, True],\n", + " step_id=step_id,\n", + " )\n", + "\n", + "print(f\"Published Example Float Measurement IDs: {float_measurement_ids}\")\n", + "print(f\"Published Example Integer Measurement IDs: {integer_measurement_ids}\")\n", + "print(f\"Published Example String Measurement IDs: {string_measurement_ids}\")\n", + "print(f\"Published Example Bool Measurement IDs: {bool_measurement_ids}\")" + ] + }, + { + "cell_type": "markdown", + "id": "50daa8b9", + "metadata": {}, + "source": [ + "Note that because these are scalar measurements, only a single measurement ID is present in the returned `Sequence` from each publish call. For non-scalar measurements, the `publish_measurement_batch` method returns a `Sequence` of N IDs, as we will see further below.\n", + "\n", + "Supplying a `Vector` allows the client to specify additional information, such as units:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b0148ddd", + "metadata": {}, + "outputs": [], + "source": [ + "measurement_vector = Vector(values=[1.2, 1.4, 1.6, 1.8], units=\"Volts\")\n", + "\n", + "vector_measurement_ids = data_store_client.publish_measurement_batch(\n", + " name=\"Example Published-As-Vector Measurement\",\n", + " values=measurement_vector,\n", + " step_id=step_id,\n", + " )\n", + "\n", + "print(f\"Published Example Published-As-Vector Measurement IDs: {vector_measurement_ids}\")" + ] + }, + { + "cell_type": "markdown", + "id": "2194f526", + "metadata": {}, + "source": [ + "Published measurement values can be read back in the same manner as when reading back measurement values that were published individually via `publish_measurement`.\n", + "\n", + "More specifically, scalar measurement values published via `publish_measurement_batch` are read back as a `Vector` containing all N iterations of parametric data published for a particular measurement:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e3514a41", + "metadata": {}, + "outputs": [], + "source": [ + "published_float_measurement = data_store_client.get_measurement(float_measurement_ids[0])\n", + "published_integer_measurement = data_store_client.get_measurement(integer_measurement_ids[0])\n", + "published_string_measurement = data_store_client.get_measurement(string_measurement_ids[0])\n", + "published_bool_measurement = data_store_client.get_measurement(bool_measurement_ids[0])\n", + "published_as_vector_measurement = data_store_client.get_measurement(vector_measurement_ids[0])\n", + "\n", + "read_back_float_measurement = data_store_client.read_measurement_value(published_float_measurement, expected_type=Vector)\n", + "read_back_integer_measurement = data_store_client.read_measurement_value(published_integer_measurement, expected_type=Vector)\n", + "read_back_string_measurement = data_store_client.read_measurement_value(published_string_measurement, expected_type=Vector)\n", + "read_back_bool_measurement = data_store_client.read_measurement_value(published_bool_measurement, expected_type=Vector)\n", + "read_back_vector_measurement = data_store_client.read_measurement_value(published_as_vector_measurement, expected_type=Vector)\n", + "\n", + "print(f\"Read Float Measurement: {read_back_float_measurement}\")\n", + "print(f\"Read Integer Measurement: {read_back_integer_measurement}\")\n", + "print(f\"Read String Measurement: {read_back_string_measurement}\")\n", + "print(f\"Read Bool Measurement: {read_back_bool_measurement}\")\n", + "print(f\"Read Published-As-Vector Measurement: {read_back_vector_measurement}\")" + ] + }, + { + "cell_type": "markdown", + "id": "90d45ba7", + "metadata": {}, + "source": [ + "### Non-Scalar Measurements\n", + "\n", + "Unlike condition values, a measurement value for a particular publish iteration may be non-scalar, such as an `AnalogWaveform` or `Vector`.\n", + "\n", + "To batch publish non-scalar values for a particular measurement, supply `publish_measurement_batch` with an `Iterable` containing N non-scalar elements. Each of the N elements in the `Iterable` will correspond to a single publish iteration. This is equivalent to calling the non-batched `publish_measurement` method N times for the measurement in question:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a9fa541", + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import timezone\n", + "import hightime as ht\n", + "import numpy as np\n", + "from nitypes.waveform import AnalogWaveform, Timing\n", + "\n", + "# Batch publish two AnalogWaveform (i.e., non-scalar) values for the Example AnalogWaveform Measurement.\n", + "waveforms = [\n", + " AnalogWaveform(\n", + " sample_count=4,\n", + " raw_data=np.array([0.0, 0.25, 0.5, 0.75], dtype=np.float64),\n", + " timing=Timing.create_with_regular_interval(\n", + " ht.timedelta(seconds=1e-3),\n", + " ht.datetime.now(timezone.utc),\n", + " ),\n", + " ),\n", + " AnalogWaveform(\n", + " sample_count=4,\n", + " raw_data=np.array([1.0, 0.85, 0.65, 0.4], dtype=np.float64),\n", + " timing=Timing.create_with_regular_interval(\n", + " ht.timedelta(seconds=1e-3),\n", + " ht.datetime.now(timezone.utc),\n", + " ),\n", + " ),\n", + "]\n", + "\n", + "waveform_measurement_ids = data_store_client.publish_measurement_batch(\n", + " name=\"Example AnalogWaveform Measurement\",\n", + " values=waveforms,\n", + " step_id=step_id,\n", + " )\n", + "\n", + "# Batch publish two Vector (i.e., non-scalar) values for the Example Vector Measurement.\n", + "vector_measurements = [\n", + " Vector(values=[1.0, 1.25, 1.5], units=\"Volts\"),\n", + " Vector(values=[2.0, 2.25, 2.5], units=\"Volts\"),\n", + "]\n", + "\n", + "vector_measurement_ids = data_store_client.publish_measurement_batch(\n", + " name=\"Example Vector Measurement\",\n", + " values=vector_measurements,\n", + " step_id=step_id,\n", + " )\n", + "\n", + "print(f\"Published Example AnalogWaveform Measurement IDs: {waveform_measurement_ids}\")\n", + "print(f\"Published Example Vector Measurement IDs: {vector_measurement_ids}\")" + ] + }, + { + "cell_type": "markdown", + "id": "e9ce1e23", + "metadata": {}, + "source": [ + "Published non-scalar measurement values can be read back in the same manner as when reading back measurement values that were published individually via `publish_measurement`.\n", + "\n", + "More specifically, non-scalar measurement values published via `publish_measurement_batch` are read back **individually**, with each published value corresponding to a separate `PublishedMeasurement`.\n", + "\n", + "The `parametric_index` of each `PublishedMeasurement` indicates the publish iteration to which it corresponds:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4596e9a9", + "metadata": {}, + "outputs": [], + "source": [ + "published_waveform_measurement_0 = data_store_client.get_measurement(waveform_measurement_ids[0])\n", + "published_waveform_measurement_1 = data_store_client.get_measurement(waveform_measurement_ids[1])\n", + "published_vector_measurement_0 = data_store_client.get_measurement(vector_measurement_ids[0])\n", + "published_vector_measurement_1 = data_store_client.get_measurement(vector_measurement_ids[1])\n", + "\n", + "read_back_waveform_measurement_0 = data_store_client.read_measurement_value(published_waveform_measurement_0, expected_type=AnalogWaveform)\n", + "read_back_waveform_measurement_1 = data_store_client.read_measurement_value(published_waveform_measurement_1, expected_type=AnalogWaveform)\n", + "read_back_vector_measurement_0 = data_store_client.read_measurement_value(published_vector_measurement_0, expected_type=Vector)\n", + "read_back_vector_measurement_1 = data_store_client.read_measurement_value(published_vector_measurement_1, expected_type=Vector)\n", + "\n", + "# The 'parametric_index' field of the PublishedMeasurement indicates\n", + "# which publish iteration the non-scalar measurement value corresponds to.\n", + "# In this example, we published two values for each (uniquely named) measurement,\n", + "# so the PublishedMeasurement corresponding to the first publish for each measurement \n", + "# has a 'parametric_index' of 0 and the PublishedMeasurement corresponding to the\n", + "# second publish for each measurement has a 'parametric_index' of 1.\n", + "print(\n", + " f\"{published_waveform_measurement_0.name} at Parametric Index \"\n", + " f\"{published_waveform_measurement_0.parametric_index}: \"\n", + " f\"{read_back_waveform_measurement_0.raw_data.tolist()}\"\n", + " )\n", + "print(\n", + " f\"{published_waveform_measurement_1.name} at Parametric Index \"\n", + " f\"{published_waveform_measurement_1.parametric_index}: \"\n", + " f\"{read_back_waveform_measurement_1.raw_data.tolist()}\"\n", + " )\n", + "\n", + "print(\n", + " f\"{published_vector_measurement_0.name} at Parametric Index \"\n", + " f\"{published_vector_measurement_0.parametric_index}: \"\n", + " f\"{read_back_vector_measurement_0}\"\n", + " )\n", + "print(\n", + " f\"{published_vector_measurement_1.name} at Parametric Index \"\n", + " f\"{published_vector_measurement_1.parametric_index}: \"\n", + " f\"{read_back_vector_measurement_1}\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "6492dcaf", + "metadata": {}, + "source": [ + "## Clean-Up\n", + "\n", + "Close the `DataStoreClient` and tear down the example-specific context when done." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b80c18f6", + "metadata": {}, + "outputs": [], + "source": [ + "data_store_client.close()\n", + "\n", + "# Perform example-specific cleanup. This is not needed when writing production code.\n", + "data_store_context.close()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From f6e0816d565f22480234311e1ac255a58c8ae490 Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Tue, 19 May 2026 15:16:00 -0500 Subject: [PATCH 13/29] Run linter and fix style errors --- src/ni/datastore/data/_grpc_conversion.py | 8 +- ...publish_measurement_batch_and_read_data.py | 16 +- tests/unit/data/test_grpc_conversion.py | 157 +++++++++++++----- 3 files changed, 128 insertions(+), 53 deletions(-) diff --git a/src/ni/datastore/data/_grpc_conversion.py b/src/ni/datastore/data/_grpc_conversion.py index 23c7ae77..81ab5790 100644 --- a/src/ni/datastore/data/_grpc_conversion.py +++ b/src/ni/datastore/data/_grpc_conversion.py @@ -3,8 +3,8 @@ from __future__ import annotations import datetime as std_datetime -from itertools import chain import logging +from itertools import chain from typing import Any, Callable, Iterable, cast import hightime as ht @@ -220,7 +220,8 @@ def copy_batch_values( copy_batch_values( publish_request.double_complex_waveform_values.waveforms, all_values, - lambda value: isinstance(value, ComplexWaveform) and value.dtype == np.complex128, + lambda value: isinstance(value, ComplexWaveform) + and value.dtype == np.complex128, float64_complex_waveform_to_protobuf, "Unsupported iterable: all values must be complex128 ComplexWaveform.", ) @@ -229,7 +230,8 @@ def copy_batch_values( copy_batch_values( publish_request.i16_complex_waveform_values.waveforms, all_values, - lambda value: isinstance(value, ComplexWaveform) and value.dtype == ComplexInt32DType, + lambda value: isinstance(value, ComplexWaveform) + and value.dtype == ComplexInt32DType, int16_complex_waveform_to_protobuf, "Unsupported iterable: all values must be ComplexWaveform with ComplexInt32DType.", ) diff --git a/tests/acceptance/test_publish_measurement_batch_and_read_data.py b/tests/acceptance/test_publish_measurement_batch_and_read_data.py index 0d0779ac..674da402 100644 --- a/tests/acceptance/test_publish_measurement_batch_and_read_data.py +++ b/tests/acceptance/test_publish_measurement_batch_and_read_data.py @@ -98,12 +98,8 @@ def test___publish_batch_double_analog_waveforms___read_measurement_value_return ) assert len(published_measurement_ids) == 2 - published_measurement_one = data_store_client.get_measurement( - published_measurement_ids[0] - ) - published_measurement_two = data_store_client.get_measurement( - published_measurement_ids[1] - ) + published_measurement_one = data_store_client.get_measurement(published_measurement_ids[0]) + published_measurement_two = data_store_client.get_measurement(published_measurement_ids[1]) published_waveform_one = data_store_client.read_measurement_value( published_measurement_one, expected_type=AnalogWaveform ) @@ -137,12 +133,8 @@ def test___publish_batch_vectors___read_measurement_value_returns_vector( ) assert len(published_measurement_ids) == 2 - published_measurement_one = data_store_client.get_measurement( - published_measurement_ids[0] - ) - published_measurement_two = data_store_client.get_measurement( - published_measurement_ids[1] - ) + published_measurement_one = data_store_client.get_measurement(published_measurement_ids[0]) + published_measurement_two = data_store_client.get_measurement(published_measurement_ids[1]) published_vector_one = data_store_client.read_measurement_value( published_measurement_one, expected_type=Vector ) diff --git a/tests/unit/data/test_grpc_conversion.py b/tests/unit/data/test_grpc_conversion.py index 119b2cc3..cae56660 100644 --- a/tests/unit/data/test_grpc_conversion.py +++ b/tests/unit/data/test_grpc_conversion.py @@ -220,7 +220,9 @@ def test___python_double_vector_object___populate_measurement_batch___measuremen assert request.scalar_values.attributes["NI_UnitDescription"].string_value == "amps" -def test___python_int_vector_object___populate_measurement_batch___measurement_updated_correctly() -> None: +def test___python_int_vector_object___populate_measurement_batch___measurement_updated_correctly() -> ( + None +): vector_obj = Vector([1, 2, 3], "amps") request = PublishMeasurementBatchRequest() populate_publish_measurement_batch_request_values(request, vector_obj) @@ -230,7 +232,9 @@ def test___python_int_vector_object___populate_measurement_batch___measurement_u assert request.scalar_values.attributes["NI_UnitDescription"].string_value == "amps" -def test___python_bool_vector_object___populate_measurement_batch___measurement_updated_correctly() -> None: +def test___python_bool_vector_object___populate_measurement_batch___measurement_updated_correctly() -> ( + None +): vector_obj = Vector([True, False, True], "amps") request = PublishMeasurementBatchRequest() populate_publish_measurement_batch_request_values(request, vector_obj) @@ -240,7 +244,9 @@ def test___python_bool_vector_object___populate_measurement_batch___measurement_ assert request.scalar_values.attributes["NI_UnitDescription"].string_value == "amps" -def test___python_string_vector_object___populate_measurement_batch___measurement_updated_correctly() -> None: +def test___python_string_vector_object___populate_measurement_batch___measurement_updated_correctly() -> ( + None +): vector_obj = Vector(["one", "two", "three"], "amps") request = PublishMeasurementBatchRequest() populate_publish_measurement_batch_request_values(request, vector_obj) @@ -250,7 +256,9 @@ def test___python_string_vector_object___populate_measurement_batch___measuremen assert request.scalar_values.attributes["NI_UnitDescription"].string_value == "amps" -def test___python_double_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: +def test___python_double_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( + None +): values = [1.5, 2.5, 3.5] request = PublishMeasurementBatchRequest() populate_publish_measurement_batch_request_values(request, values) @@ -259,7 +267,9 @@ def test___python_double_iterable___populate_measurement_batch___measurement_upd assert list(request.scalar_values.double_array.values) == [1.5, 2.5, 3.5] -def test___python_int_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: +def test___python_int_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( + None +): values = [1, 2, 3] request = PublishMeasurementBatchRequest() populate_publish_measurement_batch_request_values(request, values) @@ -268,7 +278,9 @@ def test___python_int_iterable___populate_measurement_batch___measurement_update assert list(request.scalar_values.sint32_array.values) == [1, 2, 3] -def test___python_bool_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: +def test___python_bool_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( + None +): values = [True, False, True] request = PublishMeasurementBatchRequest() populate_publish_measurement_batch_request_values(request, values) @@ -277,7 +289,9 @@ def test___python_bool_iterable___populate_measurement_batch___measurement_updat assert list(request.scalar_values.bool_array.values) == [True, False, True] -def test___python_string_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: +def test___python_string_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( + None +): values = ["one", "two", "three"] request = PublishMeasurementBatchRequest() populate_publish_measurement_batch_request_values(request, values) @@ -286,7 +300,9 @@ def test___python_string_iterable___populate_measurement_batch___measurement_upd assert list(request.scalar_values.string_array.values) == ["one", "two", "three"] -def test___python_vector_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: +def test___python_vector_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( + None +): values = [Vector([1.0, 2.0]), Vector([3.0, 4.0])] request = PublishMeasurementBatchRequest() @@ -297,7 +313,9 @@ def test___python_vector_iterable___populate_measurement_batch___measurement_upd assert list(request.vector_values.vectors[1].double_array.values) == [3.0, 4.0] -def test___python_vector_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> None: +def test___python_vector_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> ( + None +): values = [Vector([1.0, 2.0]), AnalogWaveform(sample_count=2, raw_data=np.array([1.0, 2.0]))] request = PublishMeasurementBatchRequest() @@ -305,7 +323,9 @@ def test___python_vector_iterable_with_mismatched_second_element___populate_meas populate_publish_measurement_batch_request_values(request, values) -def test___python_float64_analog_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: +def test___python_float64_analog_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( + None +): values = [ AnalogWaveform(sample_count=2, raw_data=np.array([1.25, -2.5], dtype=np.float64)), AnalogWaveform(sample_count=3, raw_data=np.array([3.5, 4.75, -6.0], dtype=np.float64)), @@ -319,7 +339,9 @@ def test___python_float64_analog_waveform_iterable___populate_measurement_batch_ assert list(request.double_analog_waveform_values.waveforms[1].y_data) == [3.5, 4.75, -6.0] -def test___python_float64_analog_waveform_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> None: +def test___python_float64_analog_waveform_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> ( + None +): values = [ AnalogWaveform(sample_count=2, raw_data=np.array([1.25, -2.5], dtype=np.float64)), Vector([3.5, 4.75, -6.0]), @@ -330,7 +352,9 @@ def test___python_float64_analog_waveform_iterable_with_mismatched_second_elemen populate_publish_measurement_batch_request_values(request, values) -def test___python_float64_analog_waveform_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> None: +def test___python_float64_analog_waveform_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> ( + None +): values = [ AnalogWaveform(sample_count=2, raw_data=np.array([1.25, -2.5], dtype=np.float64)), AnalogWaveform(sample_count=3, raw_data=np.array([7, 0, -8], dtype=np.int16)), @@ -341,7 +365,9 @@ def test___python_float64_analog_waveform_iterable_with_mismatched_second_dtype_ populate_publish_measurement_batch_request_values(request, values) -def test___python_int16_analog_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: +def test___python_int16_analog_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( + None +): values = [ AnalogWaveform(sample_count=2, raw_data=np.array([12, -3], dtype=np.int16)), AnalogWaveform(sample_count=3, raw_data=np.array([7, 0, -8], dtype=np.int16)), @@ -355,7 +381,9 @@ def test___python_int16_analog_waveform_iterable___populate_measurement_batch___ assert list(request.i16_analog_waveform_values.waveforms[1].y_data) == [7, 0, -8] -def test___python_int16_analog_waveform_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> None: +def test___python_int16_analog_waveform_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> ( + None +): values = [ AnalogWaveform(sample_count=2, raw_data=np.array([12, -3], dtype=np.int16)), Vector([7.0, 0.0, -8.0]), @@ -366,7 +394,9 @@ def test___python_int16_analog_waveform_iterable_with_mismatched_second_element_ populate_publish_measurement_batch_request_values(request, values) -def test___python_int16_analog_waveform_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> None: +def test___python_int16_analog_waveform_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> ( + None +): values = [ AnalogWaveform(sample_count=2, raw_data=np.array([12, -3], dtype=np.int16)), AnalogWaveform(sample_count=3, raw_data=np.array([3.5, 4.75, -6.0], dtype=np.float64)), @@ -377,7 +407,9 @@ def test___python_int16_analog_waveform_iterable_with_mismatched_second_dtype___ populate_publish_measurement_batch_request_values(request, values) -def test___python_unsupported_dtype_analog_waveform_iterable___populate_measurement_batch___raises_error() -> None: +def test___python_unsupported_dtype_analog_waveform_iterable___populate_measurement_batch___raises_error() -> ( + None +): values = [ AnalogWaveform(sample_count=2, raw_data=np.array([1.25, -2.5], dtype=np.float32)), AnalogWaveform(sample_count=3, raw_data=np.array([3.5, 4.75, -6.0], dtype=np.float32)), @@ -388,9 +420,13 @@ def test___python_unsupported_dtype_analog_waveform_iterable___populate_measurem populate_publish_measurement_batch_request_values(request, values) -def test___python_float64_complex_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: +def test___python_float64_complex_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( + None +): values = [ - ComplexWaveform(sample_count=2, raw_data=np.array([1.0 + 2.0j, -3.0 + 4.5j], dtype=np.complex128)), + ComplexWaveform( + sample_count=2, raw_data=np.array([1.0 + 2.0j, -3.0 + 4.5j], dtype=np.complex128) + ), ComplexWaveform( sample_count=3, raw_data=np.array([0.5 - 1.5j, 2.25 + 0.75j, -4.0 - 2.0j], dtype=np.complex128), @@ -402,12 +438,23 @@ def test___python_float64_complex_waveform_iterable___populate_measurement_batch assert len(request.double_complex_waveform_values.waveforms) == 2 assert list(request.double_complex_waveform_values.waveforms[0].y_data) == [1.0, 2.0, -3.0, 4.5] - assert list(request.double_complex_waveform_values.waveforms[1].y_data) == [0.5, -1.5, 2.25, 0.75, -4.0, -2.0] + assert list(request.double_complex_waveform_values.waveforms[1].y_data) == [ + 0.5, + -1.5, + 2.25, + 0.75, + -4.0, + -2.0, + ] -def test___python_float64_complex_waveform_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> None: +def test___python_float64_complex_waveform_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> ( + None +): values = [ - ComplexWaveform(sample_count=2, raw_data=np.array([1.0 + 2.0j, -3.0 + 4.5j], dtype=np.complex128)), + ComplexWaveform( + sample_count=2, raw_data=np.array([1.0 + 2.0j, -3.0 + 4.5j], dtype=np.complex128) + ), Vector([0.5, -1.5, 2.25, 0.75]), ] request = PublishMeasurementBatchRequest() @@ -416,9 +463,13 @@ def test___python_float64_complex_waveform_iterable_with_mismatched_second_eleme populate_publish_measurement_batch_request_values(request, values) -def test___python_float64_complex_waveform_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> None: +def test___python_float64_complex_waveform_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> ( + None +): values = [ - ComplexWaveform(sample_count=2, raw_data=np.array([1.0 + 2.0j, -3.0 + 4.5j], dtype=np.complex128)), + ComplexWaveform( + sample_count=2, raw_data=np.array([1.0 + 2.0j, -3.0 + 4.5j], dtype=np.complex128) + ), ComplexWaveform( sample_count=3, raw_data=np.array([(-7, 4), (0, -6), (8, 3)], dtype=ComplexInt32DType), @@ -430,7 +481,9 @@ def test___python_float64_complex_waveform_iterable_with_mismatched_second_dtype populate_publish_measurement_batch_request_values(request, values) -def test___python_int16_complex_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: +def test___python_int16_complex_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( + None +): values = [ ComplexWaveform( sample_count=2, @@ -450,7 +503,9 @@ def test___python_int16_complex_waveform_iterable___populate_measurement_batch__ assert list(request.i16_complex_waveform_values.waveforms[1].y_data) == [-7, 4, 0, -6, 8, 3] -def test___python_int16_complex_waveform_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> None: +def test___python_int16_complex_waveform_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> ( + None +): values = [ ComplexWaveform( sample_count=2, @@ -464,7 +519,9 @@ def test___python_int16_complex_waveform_iterable_with_mismatched_second_element populate_publish_measurement_batch_request_values(request, values) -def test___python_int16_complex_waveform_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> None: +def test___python_int16_complex_waveform_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> ( + None +): values = [ ComplexWaveform( sample_count=2, @@ -481,9 +538,13 @@ def test___python_int16_complex_waveform_iterable_with_mismatched_second_dtype__ populate_publish_measurement_batch_request_values(request, values) -def test___python_unsupported_dtype_complex_waveform_iterable___populate_measurement_batch___raises_error() -> None: +def test___python_unsupported_dtype_complex_waveform_iterable___populate_measurement_batch___raises_error() -> ( + None +): values = [ - ComplexWaveform(sample_count=2, raw_data=np.array([1.0 + 2.0j, -3.0 + 4.5j], dtype=np.complex64)), + ComplexWaveform( + sample_count=2, raw_data=np.array([1.0 + 2.0j, -3.0 + 4.5j], dtype=np.complex64) + ), ComplexWaveform( sample_count=3, raw_data=np.array([0.5 - 1.5j, 2.25 + 0.75j, -4.0 - 2.0j], dtype=np.complex64), @@ -495,7 +556,9 @@ def test___python_unsupported_dtype_complex_waveform_iterable___populate_measure populate_publish_measurement_batch_request_values(request, values) -def test___python_float64_spectrum_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: +def test___python_float64_spectrum_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( + None +): values = [ Spectrum.from_array_1d(np.array([1.0, 2.0])), Spectrum.from_array_1d(np.array([3.0, 4.0])), @@ -509,7 +572,9 @@ def test___python_float64_spectrum_iterable___populate_measurement_batch___measu assert list(request.double_spectrum_values.waveforms[1].data) == [3.0, 4.0] -def test___python_float64_spectrum_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> None: +def test___python_float64_spectrum_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> ( + None +): values = [ Spectrum.from_array_1d(np.array([1.0, 2.0])), Vector([3.0, 4.0]), @@ -520,7 +585,9 @@ def test___python_float64_spectrum_iterable_with_mismatched_second_element___pop populate_publish_measurement_batch_request_values(request, values) -def test___python_float64_spectrum_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> None: +def test___python_float64_spectrum_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> ( + None +): values = [ Spectrum.from_array_1d(np.array([1.0, 2.0])), Spectrum.from_array_1d(np.array([3.0, 4.0], dtype=np.float32)), @@ -531,7 +598,9 @@ def test___python_float64_spectrum_iterable_with_mismatched_second_dtype___popul populate_publish_measurement_batch_request_values(request, values) -def test___python_unsupported_dtype_spectrum_iterable___populate_measurement_batch___raises_error() -> None: +def test___python_unsupported_dtype_spectrum_iterable___populate_measurement_batch___raises_error() -> ( + None +): values = [ Spectrum.from_array_1d(np.array([1.0, 2.0], dtype=np.float32)), Spectrum.from_array_1d(np.array([3.0, 4.0], dtype=np.float32)), @@ -542,7 +611,9 @@ def test___python_unsupported_dtype_spectrum_iterable___populate_measurement_bat populate_publish_measurement_batch_request_values(request, values) -def test___python_uint8_digital_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: +def test___python_uint8_digital_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( + None +): values = [ DigitalWaveform.from_lines([1], np.uint8), DigitalWaveform.from_lines([0], np.uint8), @@ -556,7 +627,9 @@ def test___python_uint8_digital_waveform_iterable___populate_measurement_batch__ assert request.digital_waveform_values.waveforms[1].y_data == b"\x00" -def test___python_uint8_digital_waveform_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> None: +def test___python_uint8_digital_waveform_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> ( + None +): values = [ DigitalWaveform.from_lines([1], np.uint8), Vector([0.0]), @@ -567,7 +640,9 @@ def test___python_uint8_digital_waveform_iterable_with_mismatched_second_element populate_publish_measurement_batch_request_values(request, values) -def test___python_float64_xydata_iterable___populate_measurement_batch___measurement_updated_correctly() -> None: +def test___python_float64_xydata_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( + None +): values = [ XYData.from_arrays_1d([1.0], [2.0], np.float64), XYData.from_arrays_1d([3.0], [4.0], np.float64), @@ -583,7 +658,9 @@ def test___python_float64_xydata_iterable___populate_measurement_batch___measure assert list(request.x_y_data_values.x_y_data[1].y_data) == [4.0] -def test___python_float64_xydata_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> None: +def test___python_float64_xydata_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> ( + None +): values = [ XYData.from_arrays_1d([1.0], [2.0], np.float64), Vector([3.0, 4.0]), @@ -594,7 +671,9 @@ def test___python_float64_xydata_iterable_with_mismatched_second_element___popul populate_publish_measurement_batch_request_values(request, values) -def test___python_float64_xydata_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> None: +def test___python_float64_xydata_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> ( + None +): values = [ XYData.from_arrays_1d([1.0], [2.0], np.float64), XYData.from_arrays_1d([3.0], [4.0], np.float32), @@ -605,7 +684,9 @@ def test___python_float64_xydata_iterable_with_mismatched_second_dtype___populat populate_publish_measurement_batch_request_values(request, values) -def test___python_unsupported_dtype_xydata_iterable___populate_measurement_batch___raises_error() -> None: +def test___python_unsupported_dtype_xydata_iterable___populate_measurement_batch___raises_error() -> ( + None +): values = [ XYData.from_arrays_1d([1.0], [2.0], np.float32), XYData.from_arrays_1d([3.0], [4.0], np.float32), From ad142e107b6965a4583a47f4a48c7cfbec006698 Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Wed, 20 May 2026 11:48:29 -0500 Subject: [PATCH 14/29] Adding type-check assertions on PublishMeasurementBatchRequest population --- tests/unit/data/test_grpc_conversion.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/unit/data/test_grpc_conversion.py b/tests/unit/data/test_grpc_conversion.py index cae56660..3ea9f2d6 100644 --- a/tests/unit/data/test_grpc_conversion.py +++ b/tests/unit/data/test_grpc_conversion.py @@ -15,8 +15,11 @@ from ni.protobuf.types import ( scalar_pb2, vector_pb2, + vector_wrappers_pb2, waveform_pb2, + waveform_wrappers_pb2, xydata_pb2, + xydata_wrappers_pb2, ) from nitypes.complex import ComplexInt32DType from nitypes.scalar import Scalar @@ -308,6 +311,7 @@ def test___python_vector_iterable___populate_measurement_batch___measurement_upd populate_publish_measurement_batch_request_values(request, values) + assert isinstance(request.vector_values, vector_wrappers_pb2.VectorArrayValue) assert len(request.vector_values.vectors) == 2 assert list(request.vector_values.vectors[0].double_array.values) == [1.0, 2.0] assert list(request.vector_values.vectors[1].double_array.values) == [3.0, 4.0] @@ -334,6 +338,9 @@ def test___python_float64_analog_waveform_iterable___populate_measurement_batch_ populate_publish_measurement_batch_request_values(request, values) + assert isinstance( + request.double_analog_waveform_values, waveform_wrappers_pb2.DoubleAnalogWaveformArrayValue + ) assert len(request.double_analog_waveform_values.waveforms) == 2 assert list(request.double_analog_waveform_values.waveforms[0].y_data) == [1.25, -2.5] assert list(request.double_analog_waveform_values.waveforms[1].y_data) == [3.5, 4.75, -6.0] @@ -376,6 +383,9 @@ def test___python_int16_analog_waveform_iterable___populate_measurement_batch___ populate_publish_measurement_batch_request_values(request, values) + assert isinstance( + request.i16_analog_waveform_values, waveform_wrappers_pb2.I16AnalogWaveformArrayValue + ) assert len(request.i16_analog_waveform_values.waveforms) == 2 assert list(request.i16_analog_waveform_values.waveforms[0].y_data) == [12, -3] assert list(request.i16_analog_waveform_values.waveforms[1].y_data) == [7, 0, -8] @@ -436,6 +446,10 @@ def test___python_float64_complex_waveform_iterable___populate_measurement_batch populate_publish_measurement_batch_request_values(request, values) + assert isinstance( + request.double_complex_waveform_values, + waveform_wrappers_pb2.DoubleComplexWaveformArrayValue, + ) assert len(request.double_complex_waveform_values.waveforms) == 2 assert list(request.double_complex_waveform_values.waveforms[0].y_data) == [1.0, 2.0, -3.0, 4.5] assert list(request.double_complex_waveform_values.waveforms[1].y_data) == [ @@ -498,6 +512,9 @@ def test___python_int16_complex_waveform_iterable___populate_measurement_batch__ populate_publish_measurement_batch_request_values(request, values) + assert isinstance( + request.i16_complex_waveform_values, waveform_wrappers_pb2.I16ComplexWaveformArrayValue + ) assert len(request.i16_complex_waveform_values.waveforms) == 2 assert list(request.i16_complex_waveform_values.waveforms[0].y_data) == [11, -2, 5, 9] assert list(request.i16_complex_waveform_values.waveforms[1].y_data) == [-7, 4, 0, -6, 8, 3] @@ -567,6 +584,9 @@ def test___python_float64_spectrum_iterable___populate_measurement_batch___measu populate_publish_measurement_batch_request_values(request, values) + assert isinstance( + request.double_spectrum_values, waveform_wrappers_pb2.DoubleSpectrumArrayValue + ) assert len(request.double_spectrum_values.waveforms) == 2 assert list(request.double_spectrum_values.waveforms[0].data) == [1.0, 2.0] assert list(request.double_spectrum_values.waveforms[1].data) == [3.0, 4.0] @@ -622,6 +642,9 @@ def test___python_uint8_digital_waveform_iterable___populate_measurement_batch__ populate_publish_measurement_batch_request_values(request, values) + assert isinstance( + request.digital_waveform_values, waveform_wrappers_pb2.DigitalWaveformArrayValue + ) assert len(request.digital_waveform_values.waveforms) == 2 assert request.digital_waveform_values.waveforms[0].y_data == b"\x01" assert request.digital_waveform_values.waveforms[1].y_data == b"\x00" @@ -651,6 +674,7 @@ def test___python_float64_xydata_iterable___populate_measurement_batch___measure populate_publish_measurement_batch_request_values(request, values) + assert isinstance(request.x_y_data_values, xydata_wrappers_pb2.DoubleXYDataArrayValue) assert len(request.x_y_data_values.x_y_data) == 2 assert list(request.x_y_data_values.x_y_data[0].x_data) == [1.0] assert list(request.x_y_data_values.x_y_data[0].y_data) == [2.0] From 4696c1552075bb5c6554a157d35410195619596e Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Wed, 20 May 2026 11:57:05 -0500 Subject: [PATCH 15/29] Remove unnecessary list wrappers from acceptance tests --- ...publish_measurement_batch_and_read_data.py | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/tests/acceptance/test_publish_measurement_batch_and_read_data.py b/tests/acceptance/test_publish_measurement_batch_and_read_data.py index 674da402..ef9aea47 100644 --- a/tests/acceptance/test_publish_measurement_batch_and_read_data.py +++ b/tests/acceptance/test_publish_measurement_batch_and_read_data.py @@ -89,12 +89,10 @@ def test___publish_batch_double_analog_waveforms___read_measurement_value_return step = Step(name="Initial step", test_result_id=test_result_id) step_id = data_store_client.create_step(step) - published_measurement_ids = list( - data_store_client.publish_measurement_batch( - name="Test measurement", - values=expected_waveforms, - step_id=step_id, - ) + published_measurement_ids = data_store_client.publish_measurement_batch( + name="Test measurement", + values=expected_waveforms, + step_id=step_id, ) assert len(published_measurement_ids) == 2 @@ -124,12 +122,10 @@ def test___publish_batch_vectors___read_measurement_value_returns_vector( step = Step(name="Initial step", test_result_id=test_result_id) step_id = data_store_client.create_step(step) - published_measurement_ids = list( - data_store_client.publish_measurement_batch( - name="Test measurement", - values=expected_vectors, - step_id=step_id, - ) + published_measurement_ids = data_store_client.publish_measurement_batch( + name="Test measurement", + values=expected_vectors, + step_id=step_id, ) assert len(published_measurement_ids) == 2 From ebe4dc61463437722d8d8cdf3b418eac20b95dad Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Wed, 20 May 2026 12:00:18 -0500 Subject: [PATCH 16/29] Update acceptance test names for clarity --- .../test_publish_measurement_batch_and_read_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/acceptance/test_publish_measurement_batch_and_read_data.py b/tests/acceptance/test_publish_measurement_batch_and_read_data.py index ef9aea47..cacc1315 100644 --- a/tests/acceptance/test_publish_measurement_batch_and_read_data.py +++ b/tests/acceptance/test_publish_measurement_batch_and_read_data.py @@ -75,7 +75,7 @@ def test___publish_batch_vector___read_measurement_value_returns_vector( assert vector == expected_vector -def test___publish_batch_double_analog_waveforms___read_measurement_value_returns_analog_waveform( +def test___publish_batch_double_analog_waveforms___read_measurement_value_returns_each_analog_waveform( acceptance_test_context: DataStoreContext, ) -> None: with DataStoreClient() as data_store_client: @@ -108,7 +108,7 @@ def test___publish_batch_double_analog_waveforms___read_measurement_value_return assert published_waveform_two == expected_waveforms[1] -def test___publish_batch_vectors___read_measurement_value_returns_vector( +def test___publish_batch_vectors___read_measurement_value_returns_each_vector( acceptance_test_context: DataStoreContext, ) -> None: with DataStoreClient() as data_store_client: From 99ca7a91077ee430990b08879c99c10f78b438fb Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Wed, 20 May 2026 12:25:48 -0500 Subject: [PATCH 17/29] Convert separate if statements to else-if statements for consistency --- src/ni/datastore/data/_grpc_conversion.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/ni/datastore/data/_grpc_conversion.py b/src/ni/datastore/data/_grpc_conversion.py index 81ab5790..6da4c074 100644 --- a/src/ni/datastore/data/_grpc_conversion.py +++ b/src/ni/datastore/data/_grpc_conversion.py @@ -193,8 +193,7 @@ def copy_batch_values( "Unsupported iterable: all values must be Vector.", ) return - - if isinstance(first_value, AnalogWaveform): + elif isinstance(first_value, AnalogWaveform): if first_value.dtype == np.float64: copy_batch_values( publish_request.double_analog_waveform_values.waveforms, @@ -214,8 +213,7 @@ def copy_batch_values( ) return raise TypeError(f"Unsupported AnalogWaveform dtype: {first_value.dtype}") - - if isinstance(first_value, ComplexWaveform): + elif isinstance(first_value, ComplexWaveform): if first_value.dtype == np.complex128: copy_batch_values( publish_request.double_complex_waveform_values.waveforms, @@ -237,8 +235,7 @@ def copy_batch_values( ) return raise TypeError(f"Unsupported ComplexWaveform dtype: {first_value.dtype}") - - if isinstance(first_value, Spectrum): + elif isinstance(first_value, Spectrum): if first_value.dtype == np.float64: copy_batch_values( publish_request.double_spectrum_values.waveforms, @@ -249,8 +246,7 @@ def copy_batch_values( ) return raise TypeError(f"Unsupported Spectrum dtype: {first_value.dtype}") - - if isinstance(first_value, DigitalWaveform): + elif isinstance(first_value, DigitalWaveform): copy_batch_values( publish_request.digital_waveform_values.waveforms, all_values, @@ -259,8 +255,7 @@ def copy_batch_values( "Unsupported iterable: all values must be DigitalWaveform.", ) return - - if isinstance(first_value, XYData): + elif isinstance(first_value, XYData): if first_value.dtype == np.float64: copy_batch_values( publish_request.x_y_data_values.x_y_data, From 96e2cea95f107c26d088bf15bcbccc7fac1c019b Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Wed, 20 May 2026 12:33:22 -0500 Subject: [PATCH 18/29] Add test case for supplying a non-iterable --- tests/unit/data/test_grpc_conversion.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/unit/data/test_grpc_conversion.py b/tests/unit/data/test_grpc_conversion.py index 3ea9f2d6..ff4424a1 100644 --- a/tests/unit/data/test_grpc_conversion.py +++ b/tests/unit/data/test_grpc_conversion.py @@ -724,7 +724,7 @@ def test___python_unsupported_dtype_xydata_iterable___populate_measurement_batch def test___empty_iterable___populate_measurement_batch___raises_error() -> None: request = PublishMeasurementBatchRequest() - with pytest.raises(ValueError, match="Cannot publish an empty Iterable\."): + with pytest.raises(ValueError, match="Cannot publish an empty Iterable."): populate_publish_measurement_batch_request_values(request, []) @@ -734,6 +734,17 @@ def test___python_unsupported_iterable___populate_measurement_batch___raises_err with pytest.raises( TypeError, - match="Unsupported iterable\. Subtype must be", + match="Unsupported iterable. Subtype must be", + ): + populate_publish_measurement_batch_request_values(request, values) + + +def test___python_non_iterable___populate_measurement_batch___raises_error() -> None: + values = 42 + request = PublishMeasurementBatchRequest() + + with pytest.raises( + TypeError, + match="Unsupported measurement values type", ): populate_publish_measurement_batch_request_values(request, values) From c8260f53ffc2f41c02ab2852338ae9a96f4168fb Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Wed, 20 May 2026 13:40:58 -0500 Subject: [PATCH 19/29] Review feedback - update documentation. --- src/ni/datastore/data/_data_store_client.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/ni/datastore/data/_data_store_client.py b/src/ni/datastore/data/_data_store_client.py index 3705a38c..c0ad9ed7 100644 --- a/src/ni/datastore/data/_data_store_client.py +++ b/src/ni/datastore/data/_data_store_client.py @@ -295,7 +295,7 @@ def publish_measurement_batch( software_item_ids: Iterable[str] = tuple(), notes: str = "", ) -> Sequence[str]: - """Publish multiple scalar measurements at once for parametric sweeps. + """Publish multiple measurements at once for parametric sweeps. Args: name: The name used for associating/grouping @@ -303,7 +303,7 @@ def publish_measurement_batch( iterations. For example, "Temperature" can be used for associating temperature readings across multiple iterations. - values: The values of the (scalar) measurement being published + values: The values of the measurement being published across N iterations. step_id: The ID of the step associated with this measurement. This @@ -339,10 +339,9 @@ def publish_measurement_batch( notes: Any notes to be associated with the published measurements. Returns: - Sequence[str]: The ids of the published measurement ids. - NOTE: Using a Sequence is for future flexibility. - This sequence will currently always have a single measurement id - returned. + Sequence[str]: The IDs of the corresponding PublishedMeasurements. A single + ID will be returned when publishing scalar measurement values. + N IDs will be returned when publishing (N) non-scalar measurement values. """ publish_request = PublishMeasurementBatchRequest( name=name, From 1a4d952b6f41166d909417f8ca217fcc1bd7ca9e Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Wed, 20 May 2026 14:06:13 -0500 Subject: [PATCH 20/29] Review feedback - Simplify construction of 'scalar_values' --- src/ni/datastore/data/_grpc_conversion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ni/datastore/data/_grpc_conversion.py b/src/ni/datastore/data/_grpc_conversion.py index 6da4c074..aa82dbae 100644 --- a/src/ni/datastore/data/_grpc_conversion.py +++ b/src/ni/datastore/data/_grpc_conversion.py @@ -267,7 +267,7 @@ def copy_batch_values( return raise TypeError(f"Unsupported XYData dtype: {first_value.dtype}") - scalar_values = [first_value, *values_iterator] + scalar_values = list(all_values) try: vector = Vector(cast(Iterable[bool | int | float | str], scalar_values)) except (TypeError, ValueError): From 31748d5609ca497941e89d1fa9fa8ae2c16ff486 Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Wed, 20 May 2026 14:32:19 -0500 Subject: [PATCH 21/29] Review feedback - Parameterize several existing unit tests --- tests/unit/data/test_grpc_conversion.py | 110 +++++++----------------- 1 file changed, 31 insertions(+), 79 deletions(-) diff --git a/tests/unit/data/test_grpc_conversion.py b/tests/unit/data/test_grpc_conversion.py index ff4424a1..7f33f996 100644 --- a/tests/unit/data/test_grpc_conversion.py +++ b/tests/unit/data/test_grpc_conversion.py @@ -211,96 +211,48 @@ def test___python_float64_xydata___populate_measurement___measurement_updated_co # ======================================================== # Populate Measurement Batch # ======================================================== -def test___python_double_vector_object___populate_measurement_batch___measurement_updated_correctly() -> ( - None -): - vector_obj = Vector([1.5, 2.5, 3.5], "amps") - request = PublishMeasurementBatchRequest() - populate_publish_measurement_batch_request_values(request, vector_obj) - - assert isinstance(request.scalar_values, vector_pb2.Vector) - assert list(request.scalar_values.double_array.values) == [1.5, 2.5, 3.5] - assert request.scalar_values.attributes["NI_UnitDescription"].string_value == "amps" - - -def test___python_int_vector_object___populate_measurement_batch___measurement_updated_correctly() -> ( - None -): - vector_obj = Vector([1, 2, 3], "amps") - request = PublishMeasurementBatchRequest() - populate_publish_measurement_batch_request_values(request, vector_obj) - - assert isinstance(request.scalar_values, vector_pb2.Vector) - assert list(request.scalar_values.sint32_array.values) == [1, 2, 3] - assert request.scalar_values.attributes["NI_UnitDescription"].string_value == "amps" - - -def test___python_bool_vector_object___populate_measurement_batch___measurement_updated_correctly() -> ( - None -): - vector_obj = Vector([True, False, True], "amps") - request = PublishMeasurementBatchRequest() - populate_publish_measurement_batch_request_values(request, vector_obj) - - assert isinstance(request.scalar_values, vector_pb2.Vector) - assert list(request.scalar_values.bool_array.values) == [True, False, True] - assert request.scalar_values.attributes["NI_UnitDescription"].string_value == "amps" - - -def test___python_string_vector_object___populate_measurement_batch___measurement_updated_correctly() -> ( - None -): - vector_obj = Vector(["one", "two", "three"], "amps") - request = PublishMeasurementBatchRequest() - populate_publish_measurement_batch_request_values(request, vector_obj) - - assert isinstance(request.scalar_values, vector_pb2.Vector) - assert list(request.scalar_values.string_array.values) == ["one", "two", "three"] - assert request.scalar_values.attributes["NI_UnitDescription"].string_value == "amps" - - -def test___python_double_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( - None -): - values = [1.5, 2.5, 3.5] - request = PublishMeasurementBatchRequest() - populate_publish_measurement_batch_request_values(request, values) - - assert isinstance(request.scalar_values, vector_pb2.Vector) - assert list(request.scalar_values.double_array.values) == [1.5, 2.5, 3.5] - - -def test___python_int_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( - None -): - values = [1, 2, 3] - request = PublishMeasurementBatchRequest() - populate_publish_measurement_batch_request_values(request, values) - +def _assert_scalar_values( + request: PublishMeasurementBatchRequest, attribute_name: str, expected_values: list[object] +) -> None: assert isinstance(request.scalar_values, vector_pb2.Vector) - assert list(request.scalar_values.sint32_array.values) == [1, 2, 3] + assert list(getattr(request.scalar_values, attribute_name).values) == expected_values -def test___python_bool_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( - None -): - values = [True, False, True] +@pytest.mark.parametrize( + "values, attribute_name, expected_unit", + [ + (Vector([1.5, 2.5, 3.5], "amps"), "double_array", "amps"), + (Vector([1, 2, 3], "volts"), "sint32_array", "volts"), + (Vector([True, False, True], "state"), "bool_array", "state"), + (Vector(["one", "two", "three"], "labels"), "string_array", "labels"), + ], +) +def test___python_vector_object___populate_measurement_batch___measurement_updated_correctly( + values: Vector, attribute_name: str, expected_unit: str +) -> None: request = PublishMeasurementBatchRequest() populate_publish_measurement_batch_request_values(request, values) - assert isinstance(request.scalar_values, vector_pb2.Vector) - assert list(request.scalar_values.bool_array.values) == [True, False, True] + _assert_scalar_values(request, attribute_name, list(values)) + assert request.scalar_values.attributes["NI_UnitDescription"].string_value == expected_unit -def test___python_string_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( - None -): - values = ["one", "two", "three"] +@pytest.mark.parametrize( + "values, attribute_name", + [ + ([1.5, 2.5, 3.5], "double_array"), + ([1, 2, 3], "sint32_array"), + ([True, False, True], "bool_array"), + (["one", "two", "three"], "string_array"), + ], +) +def test___python_scalar_iterable___populate_measurement_batch___measurement_updated_correctly( + values: list[object], attribute_name: str +) -> None: request = PublishMeasurementBatchRequest() populate_publish_measurement_batch_request_values(request, values) - assert isinstance(request.scalar_values, vector_pb2.Vector) - assert list(request.scalar_values.string_array.values) == ["one", "two", "three"] + _assert_scalar_values(request, attribute_name, values) def test___python_vector_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( From 9044ef2de65dee118b33a44d58a0bc63a16dc1f6 Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Wed, 20 May 2026 14:40:38 -0500 Subject: [PATCH 22/29] Review feedback - Fix code analysis issue --- tests/unit/data/test_grpc_conversion.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/data/test_grpc_conversion.py b/tests/unit/data/test_grpc_conversion.py index 7f33f996..302a2f43 100644 --- a/tests/unit/data/test_grpc_conversion.py +++ b/tests/unit/data/test_grpc_conversion.py @@ -1,3 +1,5 @@ +from typing import Any + import numpy as np import pytest from ni.datastore.data._grpc_conversion import ( @@ -228,7 +230,7 @@ def _assert_scalar_values( ], ) def test___python_vector_object___populate_measurement_batch___measurement_updated_correctly( - values: Vector, attribute_name: str, expected_unit: str + values: Vector[Any], attribute_name: str, expected_unit: str ) -> None: request = PublishMeasurementBatchRequest() populate_publish_measurement_batch_request_values(request, values) From c555417ffbe39846dd2ad69ffe15989dac50f525 Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Wed, 20 May 2026 15:19:46 -0500 Subject: [PATCH 23/29] Review feedback - Make more tests parameterized --- tests/unit/data/test_grpc_conversion.py | 436 +++++++++++------------- 1 file changed, 199 insertions(+), 237 deletions(-) diff --git a/tests/unit/data/test_grpc_conversion.py b/tests/unit/data/test_grpc_conversion.py index 302a2f43..30574a62 100644 --- a/tests/unit/data/test_grpc_conversion.py +++ b/tests/unit/data/test_grpc_conversion.py @@ -271,16 +271,6 @@ def test___python_vector_iterable___populate_measurement_batch___measurement_upd assert list(request.vector_values.vectors[1].double_array.values) == [3.0, 4.0] -def test___python_vector_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> ( - None -): - values = [Vector([1.0, 2.0]), AnalogWaveform(sample_count=2, raw_data=np.array([1.0, 2.0]))] - request = PublishMeasurementBatchRequest() - - with pytest.raises(TypeError, match="Unsupported iterable"): - populate_publish_measurement_batch_request_values(request, values) - - def test___python_float64_analog_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( None ): @@ -300,32 +290,6 @@ def test___python_float64_analog_waveform_iterable___populate_measurement_batch_ assert list(request.double_analog_waveform_values.waveforms[1].y_data) == [3.5, 4.75, -6.0] -def test___python_float64_analog_waveform_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> ( - None -): - values = [ - AnalogWaveform(sample_count=2, raw_data=np.array([1.25, -2.5], dtype=np.float64)), - Vector([3.5, 4.75, -6.0]), - ] - request = PublishMeasurementBatchRequest() - - with pytest.raises(TypeError, match="Unsupported iterable"): - populate_publish_measurement_batch_request_values(request, values) - - -def test___python_float64_analog_waveform_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> ( - None -): - values = [ - AnalogWaveform(sample_count=2, raw_data=np.array([1.25, -2.5], dtype=np.float64)), - AnalogWaveform(sample_count=3, raw_data=np.array([7, 0, -8], dtype=np.int16)), - ] - request = PublishMeasurementBatchRequest() - - with pytest.raises(TypeError, match="Unsupported iterable"): - populate_publish_measurement_batch_request_values(request, values) - - def test___python_int16_analog_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( None ): @@ -345,45 +309,6 @@ def test___python_int16_analog_waveform_iterable___populate_measurement_batch___ assert list(request.i16_analog_waveform_values.waveforms[1].y_data) == [7, 0, -8] -def test___python_int16_analog_waveform_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> ( - None -): - values = [ - AnalogWaveform(sample_count=2, raw_data=np.array([12, -3], dtype=np.int16)), - Vector([7.0, 0.0, -8.0]), - ] - request = PublishMeasurementBatchRequest() - - with pytest.raises(TypeError, match="Unsupported iterable"): - populate_publish_measurement_batch_request_values(request, values) - - -def test___python_int16_analog_waveform_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> ( - None -): - values = [ - AnalogWaveform(sample_count=2, raw_data=np.array([12, -3], dtype=np.int16)), - AnalogWaveform(sample_count=3, raw_data=np.array([3.5, 4.75, -6.0], dtype=np.float64)), - ] - request = PublishMeasurementBatchRequest() - - with pytest.raises(TypeError, match="Unsupported iterable"): - populate_publish_measurement_batch_request_values(request, values) - - -def test___python_unsupported_dtype_analog_waveform_iterable___populate_measurement_batch___raises_error() -> ( - None -): - values = [ - AnalogWaveform(sample_count=2, raw_data=np.array([1.25, -2.5], dtype=np.float32)), - AnalogWaveform(sample_count=3, raw_data=np.array([3.5, 4.75, -6.0], dtype=np.float32)), - ] - request = PublishMeasurementBatchRequest() - - with pytest.raises(TypeError, match="Unsupported AnalogWaveform dtype"): - populate_publish_measurement_batch_request_values(request, values) - - def test___python_float64_complex_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( None ): @@ -416,39 +341,6 @@ def test___python_float64_complex_waveform_iterable___populate_measurement_batch ] -def test___python_float64_complex_waveform_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> ( - None -): - values = [ - ComplexWaveform( - sample_count=2, raw_data=np.array([1.0 + 2.0j, -3.0 + 4.5j], dtype=np.complex128) - ), - Vector([0.5, -1.5, 2.25, 0.75]), - ] - request = PublishMeasurementBatchRequest() - - with pytest.raises(TypeError, match="Unsupported iterable"): - populate_publish_measurement_batch_request_values(request, values) - - -def test___python_float64_complex_waveform_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> ( - None -): - values = [ - ComplexWaveform( - sample_count=2, raw_data=np.array([1.0 + 2.0j, -3.0 + 4.5j], dtype=np.complex128) - ), - ComplexWaveform( - sample_count=3, - raw_data=np.array([(-7, 4), (0, -6), (8, 3)], dtype=ComplexInt32DType), - ), - ] - request = PublishMeasurementBatchRequest() - - with pytest.raises(TypeError, match="Unsupported iterable"): - populate_publish_measurement_batch_request_values(request, values) - - def test___python_int16_complex_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( None ): @@ -474,59 +366,6 @@ def test___python_int16_complex_waveform_iterable___populate_measurement_batch__ assert list(request.i16_complex_waveform_values.waveforms[1].y_data) == [-7, 4, 0, -6, 8, 3] -def test___python_int16_complex_waveform_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> ( - None -): - values = [ - ComplexWaveform( - sample_count=2, - raw_data=np.array([(11, -2), (5, 9)], dtype=ComplexInt32DType), - ), - Vector([-7.0, 4.0, 0.0, -6.0]), - ] - request = PublishMeasurementBatchRequest() - - with pytest.raises(TypeError, match="Unsupported iterable"): - populate_publish_measurement_batch_request_values(request, values) - - -def test___python_int16_complex_waveform_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> ( - None -): - values = [ - ComplexWaveform( - sample_count=2, - raw_data=np.array([(11, -2), (5, 9)], dtype=ComplexInt32DType), - ), - ComplexWaveform( - sample_count=3, - raw_data=np.array([0.5 - 1.5j, 2.25 + 0.75j, -4.0 - 2.0j], dtype=np.complex128), - ), - ] - request = PublishMeasurementBatchRequest() - - with pytest.raises(TypeError, match="Unsupported iterable"): - populate_publish_measurement_batch_request_values(request, values) - - -def test___python_unsupported_dtype_complex_waveform_iterable___populate_measurement_batch___raises_error() -> ( - None -): - values = [ - ComplexWaveform( - sample_count=2, raw_data=np.array([1.0 + 2.0j, -3.0 + 4.5j], dtype=np.complex64) - ), - ComplexWaveform( - sample_count=3, - raw_data=np.array([0.5 - 1.5j, 2.25 + 0.75j, -4.0 - 2.0j], dtype=np.complex64), - ), - ] - request = PublishMeasurementBatchRequest() - - with pytest.raises(TypeError, match="Unsupported ComplexWaveform dtype"): - populate_publish_measurement_batch_request_values(request, values) - - def test___python_float64_spectrum_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( None ): @@ -546,45 +385,6 @@ def test___python_float64_spectrum_iterable___populate_measurement_batch___measu assert list(request.double_spectrum_values.waveforms[1].data) == [3.0, 4.0] -def test___python_float64_spectrum_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> ( - None -): - values = [ - Spectrum.from_array_1d(np.array([1.0, 2.0])), - Vector([3.0, 4.0]), - ] - request = PublishMeasurementBatchRequest() - - with pytest.raises(TypeError, match="Unsupported iterable"): - populate_publish_measurement_batch_request_values(request, values) - - -def test___python_float64_spectrum_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> ( - None -): - values = [ - Spectrum.from_array_1d(np.array([1.0, 2.0])), - Spectrum.from_array_1d(np.array([3.0, 4.0], dtype=np.float32)), - ] - request = PublishMeasurementBatchRequest() - - with pytest.raises(TypeError, match="Unsupported iterable"): - populate_publish_measurement_batch_request_values(request, values) - - -def test___python_unsupported_dtype_spectrum_iterable___populate_measurement_batch___raises_error() -> ( - None -): - values = [ - Spectrum.from_array_1d(np.array([1.0, 2.0], dtype=np.float32)), - Spectrum.from_array_1d(np.array([3.0, 4.0], dtype=np.float32)), - ] - request = PublishMeasurementBatchRequest() - - with pytest.raises(TypeError, match="Unsupported Spectrum dtype"): - populate_publish_measurement_batch_request_values(request, values) - - def test___python_uint8_digital_waveform_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( None ): @@ -604,19 +404,6 @@ def test___python_uint8_digital_waveform_iterable___populate_measurement_batch__ assert request.digital_waveform_values.waveforms[1].y_data == b"\x00" -def test___python_uint8_digital_waveform_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> ( - None -): - values = [ - DigitalWaveform.from_lines([1], np.uint8), - Vector([0.0]), - ] - request = PublishMeasurementBatchRequest() - - with pytest.raises(TypeError, match="Unsupported iterable"): - populate_publish_measurement_batch_request_values(request, values) - - def test___python_float64_xydata_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( None ): @@ -636,42 +423,217 @@ def test___python_float64_xydata_iterable___populate_measurement_batch___measure assert list(request.x_y_data_values.x_y_data[1].y_data) == [4.0] -def test___python_float64_xydata_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error() -> ( - None -): - values = [ - XYData.from_arrays_1d([1.0], [2.0], np.float64), - Vector([3.0, 4.0]), - ] +@pytest.mark.parametrize( + "values, error_message", + [ + pytest.param( + [ + Vector([1.0, 2.0]), + AnalogWaveform(sample_count=2, raw_data=np.array([1.0, 2.0])), + ], + "Unsupported iterable: all values must be Vector.", + id="vector", + ), + pytest.param( + [ + AnalogWaveform(sample_count=2, raw_data=np.array([1.25, -2.5], dtype=np.float64)), + Vector([1.0, 2.0]), + ], + "Unsupported iterable: all values must be float64 AnalogWaveform.", + id="float64_analog_waveform", + ), + pytest.param( + [ + AnalogWaveform(sample_count=2, raw_data=np.array([12, -3], dtype=np.int16)), + Vector([1.0, 2.0]), + ], + "Unsupported iterable: all values must be int16 AnalogWaveform.", + id="int16_analog_waveform", + ), + pytest.param( + [ + ComplexWaveform( + sample_count=2, + raw_data=np.array([1.0 + 2.0j, -3.0 + 4.5j], dtype=np.complex128), + ), + Vector([1.0, 2.0]), + ], + "Unsupported iterable: all values must be complex128 ComplexWaveform.", + id="float64_complex_waveform", + ), + pytest.param( + [ + ComplexWaveform( + sample_count=2, + raw_data=np.array([(11, -2), (5, 9)], dtype=ComplexInt32DType), + ), + Vector([1.0, 2.0]), + ], + "Unsupported iterable: all values must be ComplexWaveform with ComplexInt32DType.", + id="int16_complex_waveform", + ), + pytest.param( + [ + Spectrum.from_array_1d(np.array([1.0, 2.0])), + Vector([1.0, 2.0]), + ], + "Unsupported iterable: all values must be float64 Spectrum.", + id="spectrum", + ), + pytest.param( + [ + DigitalWaveform.from_lines([1], np.uint8), + Vector([1.0, 2.0]), + ], + "Unsupported iterable: all values must be DigitalWaveform.", + id="digital_waveform", + ), + pytest.param( + [ + XYData.from_arrays_1d([1.0], [2.0], np.float64), + Vector([1.0, 2.0]), + ], + "Unsupported iterable: all values must be float64 XYData.", + id="xydata", + ), + ], +) +def test___python_iterable_with_mismatched_second_element___populate_measurement_batch___raises_error( + values: list[object], error_message: str +) -> None: request = PublishMeasurementBatchRequest() - with pytest.raises(TypeError, match="Unsupported iterable"): + with pytest.raises(TypeError, match=error_message): populate_publish_measurement_batch_request_values(request, values) -def test___python_float64_xydata_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error() -> ( - None -): - values = [ - XYData.from_arrays_1d([1.0], [2.0], np.float64), - XYData.from_arrays_1d([3.0], [4.0], np.float32), - ] +@pytest.mark.parametrize( + "values, error_message", + [ + pytest.param( + [ + AnalogWaveform(sample_count=2, raw_data=np.array([1.25, -2.5], dtype=np.float64)), + AnalogWaveform(sample_count=3, raw_data=np.array([7, 0, -8], dtype=np.int16)), + ], + "Unsupported iterable: all values must be float64 AnalogWaveform.", + id="float64_analog_waveform", + ), + pytest.param( + [ + AnalogWaveform(sample_count=2, raw_data=np.array([12, -3], dtype=np.int16)), + AnalogWaveform( + sample_count=3, raw_data=np.array([3.5, 4.75, -6.0], dtype=np.float64) + ), + ], + "Unsupported iterable: all values must be int16 AnalogWaveform.", + id="int16_analog_waveform", + ), + pytest.param( + [ + ComplexWaveform( + sample_count=2, + raw_data=np.array([1.0 + 2.0j, -3.0 + 4.5j], dtype=np.complex128), + ), + ComplexWaveform( + sample_count=3, + raw_data=np.array([(-7, 4), (0, -6), (8, 3)], dtype=ComplexInt32DType), + ), + ], + "Unsupported iterable: all values must be complex128 ComplexWaveform.", + id="float64_complex_waveform", + ), + pytest.param( + [ + ComplexWaveform( + sample_count=2, + raw_data=np.array([(11, -2), (5, 9)], dtype=ComplexInt32DType), + ), + ComplexWaveform( + sample_count=3, + raw_data=np.array([0.5 - 1.5j, 2.25 + 0.75j, -4.0 - 2.0j], dtype=np.complex128), + ), + ], + "Unsupported iterable: all values must be ComplexWaveform with ComplexInt32DType.", + id="int16_complex_waveform", + ), + pytest.param( + [ + Spectrum.from_array_1d(np.array([1.0, 2.0])), + Spectrum.from_array_1d(np.array([3.0, 4.0], dtype=np.float32)), + ], + "Unsupported iterable: all values must be float64 Spectrum.", + id="spectrum", + ), + pytest.param( + [ + XYData.from_arrays_1d([1.0], [2.0], np.float64), + XYData.from_arrays_1d([3.0], [4.0], np.float32), + ], + "Unsupported iterable: all values must be float64 XYData.", + id="xydata", + ), + ], +) +def test___python_iterable_with_mismatched_second_dtype___populate_measurement_batch___raises_error( + values: list[object], error_message: str +) -> None: request = PublishMeasurementBatchRequest() - with pytest.raises(TypeError, match="Unsupported iterable"): + with pytest.raises(TypeError, match=error_message): populate_publish_measurement_batch_request_values(request, values) -def test___python_unsupported_dtype_xydata_iterable___populate_measurement_batch___raises_error() -> ( - None -): - values = [ - XYData.from_arrays_1d([1.0], [2.0], np.float32), - XYData.from_arrays_1d([3.0], [4.0], np.float32), - ] +@pytest.mark.parametrize( + "values, error_message", + [ + pytest.param( + [ + AnalogWaveform(sample_count=2, raw_data=np.array([1.25, -2.5], dtype=np.float32)), + AnalogWaveform( + sample_count=3, raw_data=np.array([3.5, 4.75, -6.0], dtype=np.float32) + ), + ], + "Unsupported AnalogWaveform dtype", + id="analog_waveform", + ), + pytest.param( + [ + ComplexWaveform( + sample_count=2, + raw_data=np.array([1.0 + 2.0j, -3.0 + 4.5j], dtype=np.complex64), + ), + ComplexWaveform( + sample_count=3, + raw_data=np.array([0.5 - 1.5j, 2.25 + 0.75j, -4.0 - 2.0j], dtype=np.complex64), + ), + ], + "Unsupported ComplexWaveform dtype", + id="complex_waveform", + ), + pytest.param( + [ + Spectrum.from_array_1d(np.array([1.0, 2.0], dtype=np.float32)), + Spectrum.from_array_1d(np.array([3.0, 4.0], dtype=np.float32)), + ], + "Unsupported Spectrum dtype", + id="spectrum", + ), + pytest.param( + [ + XYData.from_arrays_1d([1.0], [2.0], np.float32), + XYData.from_arrays_1d([3.0], [4.0], np.float32), + ], + "Unsupported XYData dtype", + id="xydata", + ), + ], +) +def test___python_unsupported_dtype_iterable___populate_measurement_batch___raises_error( + values: list[object], error_message: str +) -> None: request = PublishMeasurementBatchRequest() - with pytest.raises(TypeError, match="Unsupported XYData dtype"): + with pytest.raises(TypeError, match=error_message): populate_publish_measurement_batch_request_values(request, values) From 77d711493ec5242a3b99befd0dbb51aa4806c509 Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Wed, 20 May 2026 17:14:55 -0500 Subject: [PATCH 24/29] Review feedback - Split populate_publish_measurement_batch_request_values out into several helper methods. --- src/ni/datastore/data/_grpc_conversion.py | 237 +++++++++++++--------- 1 file changed, 138 insertions(+), 99 deletions(-) diff --git a/src/ni/datastore/data/_grpc_conversion.py b/src/ni/datastore/data/_grpc_conversion.py index aa82dbae..5bf026a3 100644 --- a/src/ni/datastore/data/_grpc_conversion.py +++ b/src/ni/datastore/data/_grpc_conversion.py @@ -50,6 +50,129 @@ _logger = logging.getLogger(__name__) +def _copy_batch_values( + repeated_field: Any, + batch_values: Iterable[object], + is_supported: Callable[[object], bool], + convert_value: Callable[[Any], Any], + error_message: str, +) -> None: + for value in batch_values: + if not is_supported(value): + raise TypeError(error_message) + repeated_field.add().CopyFrom(convert_value(value)) + + +def _populate_vector_batch_values( + publish_request: PublishMeasurementBatchRequest, values: Iterable[object] +) -> None: + _copy_batch_values( + publish_request.vector_values.vectors, + values, + lambda value: isinstance(value, Vector), + vector_to_protobuf, + "Unsupported iterable: all values must be Vector.", + ) + + +def _populate_analog_waveform_batch_values( + publish_request: PublishMeasurementBatchRequest, + first_value: AnalogWaveform, + values: Iterable[object], +) -> None: + if first_value.dtype == np.float64: + _copy_batch_values( + publish_request.double_analog_waveform_values.waveforms, + values, + lambda value: isinstance(value, AnalogWaveform) and value.dtype == np.float64, + float64_analog_waveform_to_protobuf, + "Unsupported iterable: all values must be float64 AnalogWaveform.", + ) + return + elif first_value.dtype == np.int16: + _copy_batch_values( + publish_request.i16_analog_waveform_values.waveforms, + values, + lambda value: isinstance(value, AnalogWaveform) and value.dtype == np.int16, + int16_analog_waveform_to_protobuf, + "Unsupported iterable: all values must be int16 AnalogWaveform.", + ) + return + raise TypeError(f"Unsupported AnalogWaveform dtype: {first_value.dtype}") + + +def _populate_complex_waveform_batch_values( + publish_request: PublishMeasurementBatchRequest, + first_value: ComplexWaveform, + values: Iterable[object], +) -> None: + if first_value.dtype == np.complex128: + _copy_batch_values( + publish_request.double_complex_waveform_values.waveforms, + values, + lambda value: isinstance(value, ComplexWaveform) and value.dtype == np.complex128, + float64_complex_waveform_to_protobuf, + "Unsupported iterable: all values must be complex128 ComplexWaveform.", + ) + return + if first_value.dtype == ComplexInt32DType: + _copy_batch_values( + publish_request.i16_complex_waveform_values.waveforms, + values, + lambda value: isinstance(value, ComplexWaveform) and value.dtype == ComplexInt32DType, + int16_complex_waveform_to_protobuf, + "Unsupported iterable: all values must be ComplexWaveform with ComplexInt32DType.", + ) + return + raise TypeError(f"Unsupported ComplexWaveform dtype: {first_value.dtype}") + + +def _populate_spectrum_batch_values( + publish_request: PublishMeasurementBatchRequest, + first_value: Spectrum, + values: Iterable[object], +) -> None: + if first_value.dtype != np.float64: + raise TypeError(f"Unsupported Spectrum dtype: {first_value.dtype}") + + _copy_batch_values( + publish_request.double_spectrum_values.waveforms, + values, + lambda value: isinstance(value, Spectrum) and value.dtype == np.float64, + float64_spectrum_to_protobuf, + "Unsupported iterable: all values must be float64 Spectrum.", + ) + + +def _populate_digital_waveform_batch_values( + publish_request: PublishMeasurementBatchRequest, values: Iterable[object] +) -> None: + _copy_batch_values( + publish_request.digital_waveform_values.waveforms, + values, + lambda value: isinstance(value, DigitalWaveform), + digital_waveform_to_protobuf, + "Unsupported iterable: all values must be DigitalWaveform.", + ) + + +def _populate_xydata_batch_values( + publish_request: PublishMeasurementBatchRequest, + first_value: XYData, + values: Iterable[object], +) -> None: + if first_value.dtype != np.float64: + raise TypeError(f"Unsupported XYData dtype: {first_value.dtype}") + + _copy_batch_values( + publish_request.x_y_data_values.x_y_data, + values, + lambda value: isinstance(value, XYData) and value.dtype == np.float64, + float64_xydata_to_protobuf, + "Unsupported iterable: all values must be float64 XYData.", + ) + + def populate_publish_condition_request_value( publish_request: PublishConditionRequest, value: object ) -> None: @@ -160,19 +283,6 @@ def populate_publish_measurement_batch_request_values( publish_request: PublishMeasurementBatchRequest, values: object ) -> None: """Assign a value to the appropriate field of the PublishMeasurementBatchRequest object.""" - - def copy_batch_values( - repeated_field: Any, - batch_values: Iterable[object], - is_supported: Callable[[object], bool], - convert_value: Callable[[Any], Any], - error_message: str, - ) -> None: - for value in batch_values: - if not is_supported(value): - raise TypeError(error_message) - repeated_field.add().CopyFrom(convert_value(value)) - if isinstance(values, Vector): publish_request.scalar_values.CopyFrom(vector_to_protobuf(values)) elif isinstance(values, Iterable): @@ -185,98 +295,27 @@ def copy_batch_values( all_values = chain([first_value], values_iterator) if isinstance(first_value, Vector): - copy_batch_values( - publish_request.vector_values.vectors, - all_values, - lambda value: isinstance(value, Vector), - vector_to_protobuf, - "Unsupported iterable: all values must be Vector.", - ) - return + _populate_vector_batch_values(publish_request, all_values) elif isinstance(first_value, AnalogWaveform): - if first_value.dtype == np.float64: - copy_batch_values( - publish_request.double_analog_waveform_values.waveforms, - all_values, - lambda value: isinstance(value, AnalogWaveform) and value.dtype == np.float64, - float64_analog_waveform_to_protobuf, - "Unsupported iterable: all values must be float64 AnalogWaveform.", - ) - return - elif first_value.dtype == np.int16: - copy_batch_values( - publish_request.i16_analog_waveform_values.waveforms, - all_values, - lambda value: isinstance(value, AnalogWaveform) and value.dtype == np.int16, - int16_analog_waveform_to_protobuf, - "Unsupported iterable: all values must be int16 AnalogWaveform.", - ) - return - raise TypeError(f"Unsupported AnalogWaveform dtype: {first_value.dtype}") + _populate_analog_waveform_batch_values(publish_request, first_value, all_values) elif isinstance(first_value, ComplexWaveform): - if first_value.dtype == np.complex128: - copy_batch_values( - publish_request.double_complex_waveform_values.waveforms, - all_values, - lambda value: isinstance(value, ComplexWaveform) - and value.dtype == np.complex128, - float64_complex_waveform_to_protobuf, - "Unsupported iterable: all values must be complex128 ComplexWaveform.", - ) - return - elif first_value.dtype == ComplexInt32DType: - copy_batch_values( - publish_request.i16_complex_waveform_values.waveforms, - all_values, - lambda value: isinstance(value, ComplexWaveform) - and value.dtype == ComplexInt32DType, - int16_complex_waveform_to_protobuf, - "Unsupported iterable: all values must be ComplexWaveform with ComplexInt32DType.", - ) - return - raise TypeError(f"Unsupported ComplexWaveform dtype: {first_value.dtype}") + _populate_complex_waveform_batch_values(publish_request, first_value, all_values) elif isinstance(first_value, Spectrum): - if first_value.dtype == np.float64: - copy_batch_values( - publish_request.double_spectrum_values.waveforms, - all_values, - lambda value: isinstance(value, Spectrum) and value.dtype == np.float64, - float64_spectrum_to_protobuf, - "Unsupported iterable: all values must be float64 Spectrum.", - ) - return - raise TypeError(f"Unsupported Spectrum dtype: {first_value.dtype}") + _populate_spectrum_batch_values(publish_request, first_value, all_values) elif isinstance(first_value, DigitalWaveform): - copy_batch_values( - publish_request.digital_waveform_values.waveforms, - all_values, - lambda value: isinstance(value, DigitalWaveform), - digital_waveform_to_protobuf, - "Unsupported iterable: all values must be DigitalWaveform.", - ) - return + _populate_digital_waveform_batch_values(publish_request, all_values) elif isinstance(first_value, XYData): - if first_value.dtype == np.float64: - copy_batch_values( - publish_request.x_y_data_values.x_y_data, - all_values, - lambda value: isinstance(value, XYData) and value.dtype == np.float64, - float64_xydata_to_protobuf, - "Unsupported iterable: all values must be float64 XYData.", + _populate_xydata_batch_values(publish_request, first_value, all_values) + else: + scalar_values = list(all_values) + try: + vector = Vector(cast(Iterable[bool | int | float | str], scalar_values)) + except (TypeError, ValueError): + raise TypeError( + f"Unsupported iterable. Subtype must be bool, float, int, string, Vector, " + "AnalogWaveform, ComplexWaveform, Spectrum, DigitalWaveform, or XYData." ) - return - raise TypeError(f"Unsupported XYData dtype: {first_value.dtype}") - - scalar_values = list(all_values) - try: - vector = Vector(cast(Iterable[bool | int | float | str], scalar_values)) - except (TypeError, ValueError): - raise TypeError( - f"Unsupported iterable. Subtype must be bool, float, int, string, Vector, " - "AnalogWaveform, ComplexWaveform, Spectrum, DigitalWaveform, or XYData." - ) - - publish_request.scalar_values.CopyFrom(vector_to_protobuf(vector)) + publish_request.scalar_values.CopyFrom(vector_to_protobuf(vector)) else: raise TypeError( f"Unsupported measurement values type: {type(values)}. Please consult the documentation." From 1c0f0cc742731e66865d6451ade756defcafea2d Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Wed, 20 May 2026 17:17:48 -0500 Subject: [PATCH 25/29] Fix code analysis (type hint) issue --- src/ni/datastore/data/_grpc_conversion.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ni/datastore/data/_grpc_conversion.py b/src/ni/datastore/data/_grpc_conversion.py index 5bf026a3..174c13a6 100644 --- a/src/ni/datastore/data/_grpc_conversion.py +++ b/src/ni/datastore/data/_grpc_conversion.py @@ -77,7 +77,7 @@ def _populate_vector_batch_values( def _populate_analog_waveform_batch_values( publish_request: PublishMeasurementBatchRequest, - first_value: AnalogWaveform, + first_value: AnalogWaveform[Any], values: Iterable[object], ) -> None: if first_value.dtype == np.float64: @@ -103,7 +103,7 @@ def _populate_analog_waveform_batch_values( def _populate_complex_waveform_batch_values( publish_request: PublishMeasurementBatchRequest, - first_value: ComplexWaveform, + first_value: ComplexWaveform[Any], values: Iterable[object], ) -> None: if first_value.dtype == np.complex128: @@ -129,7 +129,7 @@ def _populate_complex_waveform_batch_values( def _populate_spectrum_batch_values( publish_request: PublishMeasurementBatchRequest, - first_value: Spectrum, + first_value: Spectrum[Any], values: Iterable[object], ) -> None: if first_value.dtype != np.float64: @@ -158,7 +158,7 @@ def _populate_digital_waveform_batch_values( def _populate_xydata_batch_values( publish_request: PublishMeasurementBatchRequest, - first_value: XYData, + first_value: XYData[Any], values: Iterable[object], ) -> None: if first_value.dtype != np.float64: From 9440668b32d8dd547cada95c8d5ccbc111292400 Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Wed, 20 May 2026 17:29:00 -0500 Subject: [PATCH 26/29] Avoid instantiation of intermediary list from inputted values. --- src/ni/datastore/data/_grpc_conversion.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ni/datastore/data/_grpc_conversion.py b/src/ni/datastore/data/_grpc_conversion.py index 174c13a6..5caaac77 100644 --- a/src/ni/datastore/data/_grpc_conversion.py +++ b/src/ni/datastore/data/_grpc_conversion.py @@ -307,9 +307,8 @@ def populate_publish_measurement_batch_request_values( elif isinstance(first_value, XYData): _populate_xydata_batch_values(publish_request, first_value, all_values) else: - scalar_values = list(all_values) try: - vector = Vector(cast(Iterable[bool | int | float | str], scalar_values)) + vector = Vector(cast(Iterable[bool | int | float | str], values)) except (TypeError, ValueError): raise TypeError( f"Unsupported iterable. Subtype must be bool, float, int, string, Vector, " From f83e2582af2df860715a050279570b8c6ff4f5f3 Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Wed, 20 May 2026 17:34:45 -0500 Subject: [PATCH 27/29] Simplification of iteration logic in populate_publish_measurement_batch_request_values --- src/ni/datastore/data/_grpc_conversion.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/ni/datastore/data/_grpc_conversion.py b/src/ni/datastore/data/_grpc_conversion.py index 5caaac77..230e3f1d 100644 --- a/src/ni/datastore/data/_grpc_conversion.py +++ b/src/ni/datastore/data/_grpc_conversion.py @@ -4,7 +4,6 @@ import datetime as std_datetime import logging -from itertools import chain from typing import Any, Callable, Iterable, cast import hightime as ht @@ -292,23 +291,21 @@ def populate_publish_measurement_batch_request_values( except StopIteration as exc: raise ValueError("Cannot publish an empty Iterable.") from exc - all_values = chain([first_value], values_iterator) - if isinstance(first_value, Vector): - _populate_vector_batch_values(publish_request, all_values) + _populate_vector_batch_values(publish_request, values) elif isinstance(first_value, AnalogWaveform): - _populate_analog_waveform_batch_values(publish_request, first_value, all_values) + _populate_analog_waveform_batch_values(publish_request, first_value, values) elif isinstance(first_value, ComplexWaveform): - _populate_complex_waveform_batch_values(publish_request, first_value, all_values) + _populate_complex_waveform_batch_values(publish_request, first_value, values) elif isinstance(first_value, Spectrum): - _populate_spectrum_batch_values(publish_request, first_value, all_values) + _populate_spectrum_batch_values(publish_request, first_value, values) elif isinstance(first_value, DigitalWaveform): - _populate_digital_waveform_batch_values(publish_request, all_values) + _populate_digital_waveform_batch_values(publish_request, values) elif isinstance(first_value, XYData): - _populate_xydata_batch_values(publish_request, first_value, all_values) + _populate_xydata_batch_values(publish_request, first_value, values) else: try: - vector = Vector(cast(Iterable[bool | int | float | str], values)) + vector = Vector(values) except (TypeError, ValueError): raise TypeError( f"Unsupported iterable. Subtype must be bool, float, int, string, Vector, " From e2e0df8a5d1b93c26a7020854fadd877a7920503 Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Thu, 21 May 2026 11:25:14 -0500 Subject: [PATCH 28/29] Reversion of previous simplification to ensure support for Iterables that may only be iterated over once. --- src/ni/datastore/data/_grpc_conversion.py | 16 ++++++----- tests/unit/data/test_grpc_conversion.py | 35 ++++++++++++++++++++++- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/ni/datastore/data/_grpc_conversion.py b/src/ni/datastore/data/_grpc_conversion.py index 230e3f1d..130910fe 100644 --- a/src/ni/datastore/data/_grpc_conversion.py +++ b/src/ni/datastore/data/_grpc_conversion.py @@ -4,6 +4,7 @@ import datetime as std_datetime import logging +from itertools import chain from typing import Any, Callable, Iterable, cast import hightime as ht @@ -291,21 +292,22 @@ def populate_publish_measurement_batch_request_values( except StopIteration as exc: raise ValueError("Cannot publish an empty Iterable.") from exc + all_values = chain([first_value], values_iterator) if isinstance(first_value, Vector): - _populate_vector_batch_values(publish_request, values) + _populate_vector_batch_values(publish_request, all_values) elif isinstance(first_value, AnalogWaveform): - _populate_analog_waveform_batch_values(publish_request, first_value, values) + _populate_analog_waveform_batch_values(publish_request, first_value, all_values) elif isinstance(first_value, ComplexWaveform): - _populate_complex_waveform_batch_values(publish_request, first_value, values) + _populate_complex_waveform_batch_values(publish_request, first_value, all_values) elif isinstance(first_value, Spectrum): - _populate_spectrum_batch_values(publish_request, first_value, values) + _populate_spectrum_batch_values(publish_request, first_value, all_values) elif isinstance(first_value, DigitalWaveform): - _populate_digital_waveform_batch_values(publish_request, values) + _populate_digital_waveform_batch_values(publish_request, all_values) elif isinstance(first_value, XYData): - _populate_xydata_batch_values(publish_request, first_value, values) + _populate_xydata_batch_values(publish_request, first_value, all_values) else: try: - vector = Vector(values) + vector = Vector(cast(Iterable[bool | int | float | str], list(all_values))) except (TypeError, ValueError): raise TypeError( f"Unsupported iterable. Subtype must be bool, float, int, string, Vector, " diff --git a/tests/unit/data/test_grpc_conversion.py b/tests/unit/data/test_grpc_conversion.py index 30574a62..2409996a 100644 --- a/tests/unit/data/test_grpc_conversion.py +++ b/tests/unit/data/test_grpc_conversion.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Iterable import numpy as np import pytest @@ -423,6 +423,39 @@ def test___python_float64_xydata_iterable___populate_measurement_batch___measure assert list(request.x_y_data_values.x_y_data[1].y_data) == [4.0] +def test___python_scalar_generator_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( + None +): + def _values() -> Iterable[float]: + yield 1.5 + yield 2.5 + yield 3.5 + + request = PublishMeasurementBatchRequest() + populate_publish_measurement_batch_request_values(request, _values()) + + _assert_scalar_values(request, "double_array", [1.5, 2.5, 3.5]) + + +def test___python_non_scalar_generator_iterable___populate_measurement_batch___measurement_updated_correctly() -> ( + None +): + def _values() -> Iterable[AnalogWaveform[np.float64]]: + yield AnalogWaveform(sample_count=2, raw_data=np.array([1.25, -2.5], dtype=np.float64)) + yield AnalogWaveform(sample_count=3, raw_data=np.array([3.5, 4.75, -6.0], dtype=np.float64)) + + request = PublishMeasurementBatchRequest() + + populate_publish_measurement_batch_request_values(request, _values()) + + assert isinstance( + request.double_analog_waveform_values, waveform_wrappers_pb2.DoubleAnalogWaveformArrayValue + ) + assert len(request.double_analog_waveform_values.waveforms) == 2 + assert list(request.double_analog_waveform_values.waveforms[0].y_data) == [1.25, -2.5] + assert list(request.double_analog_waveform_values.waveforms[1].y_data) == [3.5, 4.75, -6.0] + + @pytest.mark.parametrize( "values, error_message", [ From 4daff9cb204b43582b0b8d9ae9b28f4db7420235 Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Thu, 21 May 2026 16:04:18 -0500 Subject: [PATCH 29/29] Fix case of populate_publish_condition_batch_request_values not supporting single-pass iterables. --- src/ni/datastore/data/_grpc_conversion.py | 11 ++++++++--- tests/unit/data/test_grpc_conversion.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/ni/datastore/data/_grpc_conversion.py b/src/ni/datastore/data/_grpc_conversion.py index 130910fe..f3ca4b52 100644 --- a/src/ni/datastore/data/_grpc_conversion.py +++ b/src/ni/datastore/data/_grpc_conversion.py @@ -5,7 +5,7 @@ import datetime as std_datetime import logging from itertools import chain -from typing import Any, Callable, Iterable, cast +from typing import Any, Callable, Iterable, Sequence, cast import hightime as ht import numpy as np @@ -202,11 +202,16 @@ def populate_publish_condition_batch_request_values( elif isinstance(values, Iterable): if not values: raise ValueError("Cannot publish an empty Iterable.") + + # Vector initialization requires the Iterable to be iterated over multiple times. + # We convert the Iterable to a list if we don't know that the Iterable type + # supports multiple iterations. + condition_values = values if isinstance(values, Sequence) else list(values) try: - vector = Vector(values) + vector = Vector(condition_values) except (TypeError, ValueError): raise TypeError( - f"Unsupported iterable: {values}. Subtype must be bool, float, int, or string." + f"Unsupported iterable: {condition_values}. Subtype must be bool, float, int, or string." ) publish_request.scalar_values.CopyFrom(vector_to_protobuf(vector)) diff --git a/tests/unit/data/test_grpc_conversion.py b/tests/unit/data/test_grpc_conversion.py index 2409996a..f5b930b4 100644 --- a/tests/unit/data/test_grpc_conversion.py +++ b/tests/unit/data/test_grpc_conversion.py @@ -76,6 +76,21 @@ def test___python_vector_object___populate_condition_batch___condition_updated_c assert request.scalar_values.attributes["NI_UnitDescription"].string_value == "amps" +def test___python_scalar_generator_iterable___populate_condition_batch___condition_updated_correctly() -> ( + None +): + def _values() -> Iterable[float]: + yield 1.5 + yield 2.5 + yield 3.5 + + request = PublishConditionBatchRequest() + populate_publish_condition_batch_request_values(request, _values()) + + assert isinstance(request.scalar_values, vector_pb2.Vector) + assert list(request.scalar_values.double_array.values) == [1.5, 2.5, 3.5] + + # ======================================================== # Populate Measurement # ========================================================