Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 13 additions & 20 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ python = "^3.10"
protobuf = {version=">=4.21"}
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" }
ni-protobuf-types = { version = ">=1.2.0dev1", allow-prereleases = true }
Comment thread
dixonjoel marked this conversation as resolved.
hightime = { version = ">=1.0.0" }

[tool.poetry.group.dev.dependencies]
Expand Down
31 changes: 18 additions & 13 deletions src/ni/datastore/data/_grpc_conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import datetime as std_datetime
import logging
from itertools import chain
from typing import Any, Callable, Iterable, Sequence, cast
from typing import Any, Callable, cast, Iterable

import hightime as ht
import numpy as np
Expand Down Expand Up @@ -200,18 +200,18 @@ def populate_publish_condition_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)
Comment thread
dixonjoel marked this conversation as resolved.
try:
first_value = next(values_iterator)
except StopIteration as exc:
raise ValueError("Cannot publish an empty Iterable.") from exc

# 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)
all_values = chain([first_value], values_iterator)
try:
vector = Vector(condition_values)
vector = Vector(cast(Iterable[bool | int | float | str], all_values))
except (TypeError, ValueError):
raise TypeError(
f"Unsupported iterable: {condition_values}. Subtype must be bool, float, int, or string."
f"Unsupported iterable: {values}. Subtype must be bool, float, int, or string."
)

publish_request.scalar_values.CopyFrom(vector_to_protobuf(vector))
Expand Down Expand Up @@ -268,10 +268,15 @@ def populate_publish_measurement_request_value(
else:
raise TypeError(f"Unsupported XYData dtype: {value.dtype}")
elif isinstance(value, Iterable):
if not value:
raise ValueError("Cannot publish an empty Iterable.")
value_iterator = iter(value)
try:
first_item = next(value_iterator)
except StopIteration as exc:
raise ValueError("Cannot publish an empty Iterable.") from exc

all_items = chain([first_item], value_iterator)
try:
vector = Vector(value)
vector = Vector(cast(Iterable[bool | int | float | str], all_items))
except (TypeError, ValueError):
raise TypeError(
f"Unsupported iterable: {value}. Subtype must be bool, float, int, or string."
Expand Down Expand Up @@ -312,7 +317,7 @@ def populate_publish_measurement_batch_request_values(
_populate_xydata_batch_values(publish_request, first_value, all_values)
else:
try:
vector = Vector(cast(Iterable[bool | int | float | str], list(all_values)))
vector = Vector(cast(Iterable[bool | int | float | str], all_values))
except (TypeError, ValueError):
raise TypeError(
f"Unsupported iterable. Subtype must be bool, float, int, string, Vector, "
Expand Down
21 changes: 18 additions & 3 deletions tests/acceptance/test_publish_measurement_and_read_data.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
"""Acceptance tests that publish various values then reads the data back."""

import hightime as ht
import numpy as np
from nitypes.scalar import Scalar
from nitypes.vector import Vector
from nitypes.waveform import AnalogWaveform, ComplexWaveform, DigitalWaveform, Spectrum
from nitypes.waveform import (
AnalogWaveform,
ComplexWaveform,
DigitalWaveform,
SampleIntervalMode,
Spectrum,
Timing,
)
Comment thread
Copilot marked this conversation as resolved.
from nitypes.xy_data import XYData
from utilities import DataStoreContext

Expand Down Expand Up @@ -113,6 +121,7 @@ def test___publish_analog_waveform___read_measurement_value_returns_analog_wavef
expected_waveform = AnalogWaveform(
sample_count=3,
raw_data=np.array([1.0, 2.0, 3.0]),
timing=Timing(SampleIntervalMode.NONE, time_offset=ht.timedelta()),
)

published_measurement_id = data_store_client.publish_measurement(
Expand All @@ -133,7 +142,10 @@ def test___publish_digital_waveform___read_measurement_value_returns_digital_wav
) -> None:
with DataStoreClient() as data_store_client:
step_id = _create_step(data_store_client, "digital waveform")
expected_waveform = DigitalWaveform(10)
expected_waveform = DigitalWaveform(
10,
timing=Timing(SampleIntervalMode.NONE, time_offset=ht.timedelta()),
)
published_measurement_id = data_store_client.publish_measurement(
name="python publish digital waveform",
value=expected_waveform,
Expand All @@ -152,7 +164,10 @@ def test___publish_complex_waveform___read_measurement_value_returns_complex_wav
) -> None:
with DataStoreClient() as data_store_client:
step_id = _create_step(data_store_client, "complex waveform")
expected_waveform = ComplexWaveform(10)
expected_waveform = ComplexWaveform(
10,
timing=Timing(SampleIntervalMode.NONE, time_offset=ht.timedelta()),
)
published_measurement_id = data_store_client.publish_measurement(
name="python publish complex waveform",
value=expected_waveform,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Acceptance tests that publish various batch measurement values then reads the data back."""

import hightime as ht
import numpy as np
from nitypes.vector import Vector
from nitypes.waveform import AnalogWaveform
from nitypes.waveform import AnalogWaveform, NoneScaleMode, SampleIntervalMode, Timing
from utilities import DataStoreContext

from ni.datastore.data import (
Expand Down Expand Up @@ -84,8 +85,18 @@ def test___publish_batch_double_analog_waveforms___read_measurement_value_return
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)),
AnalogWaveform(
sample_count=3,
raw_data=np.array([1.0, 2.0, 3.0]),
scale_mode=NoneScaleMode(),
timing=Timing(SampleIntervalMode.NONE, time_offset=ht.timedelta()),
),
AnalogWaveform(
sample_count=3,
raw_data=np.array([4.0, 5.0, 6.0]),
scale_mode=NoneScaleMode(),
timing=Timing(SampleIntervalMode.NONE, time_offset=ht.timedelta()),
),
]
step = Step(name="Initial step", test_result_id=test_result_id)
step_id = data_store_client.create_step(step)
Expand Down
4 changes: 3 additions & 1 deletion tests/acceptance/test_publish_with_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import hightime as ht
import numpy as np
from nitypes.waveform import AnalogWaveform
from nitypes.waveform import AnalogWaveform, NoneScaleMode, Timing, SampleIntervalMode
from utilities import DataStoreContext

from ni.datastore.data import (
Expand Down Expand Up @@ -155,6 +155,8 @@ def test___waveform_with_all_metadata___publish___query_read_returns_correct_dat
expected_waveform = AnalogWaveform(
sample_count=3,
raw_data=np.array([1.0, 2.0, 3.0]),
scale_mode=NoneScaleMode(),
timing=Timing(SampleIntervalMode.NONE, time_offset=ht.timedelta()),
Comment thread
dixonjoel marked this conversation as resolved.
)

# Metadata: Test
Expand Down
52 changes: 52 additions & 0 deletions tests/unit/data/test_grpc_conversion.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from collections.abc import Generator
from typing import Any, Iterable

import numpy as np
Expand Down Expand Up @@ -109,6 +110,16 @@ def test___empty_iterable___populate_condition_batch___raises_error() -> None:
populate_publish_condition_batch_request_values(request, [])


def test___empty_generator___populate_condition_batch___raises_error() -> None:
Comment thread
dixonjoel marked this conversation as resolved.
def _values() -> Generator[float, None, None]:
yield from []

request = PublishConditionBatchRequest()

with pytest.raises(ValueError, match="Cannot publish an empty Iterable."):
populate_publish_condition_batch_request_values(request, _values())


def test___python_unsupported_iterable___populate_condition_batch___raises_error() -> None:
values = [object(), object()]
request = PublishConditionBatchRequest()
Expand Down Expand Up @@ -265,6 +276,37 @@ def test___python_float64_xydata___populate_measurement___measurement_updated_co
assert request.x_y_data.attributes["NI_UnitDescription_Y"].string_value == "Seconds"


@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___measurement_updated_correctly(
values: list[object], attribute_name: str
) -> None:
request = PublishMeasurementRequest()
populate_publish_measurement_request_value(request, values)

assert isinstance(request.vector, vector_pb2.Vector)
assert list(getattr(request.vector, attribute_name).values) == values


def test___empty_iterable___populate_measurement___raises_value_error() -> None:
request = PublishMeasurementRequest()
with pytest.raises(ValueError, match="Cannot publish an empty Iterable."):
populate_publish_measurement_request_value(request, [])


def test___unsupported_iterable___populate_measurement___raises_type_error() -> None:
request = PublishMeasurementRequest()
with pytest.raises(TypeError, match="Unsupported iterable"):
populate_publish_measurement_request_value(request, [object(), object()])


# ========================================================
# Populate Measurement Batch
# ========================================================
Expand Down Expand Up @@ -732,6 +774,16 @@ def test___empty_iterable___populate_measurement_batch___raises_error() -> None:
populate_publish_measurement_batch_request_values(request, [])


Comment thread
dixonjoel marked this conversation as resolved.
def test___empty_generator___populate_measurement_batch___raises_error() -> None:
def _values() -> Generator[float, None, None]:
yield from []

request = PublishMeasurementBatchRequest()

with pytest.raises(ValueError, match="Cannot publish an empty Iterable."):
populate_publish_measurement_batch_request_values(request, _values())


def test___python_unsupported_iterable___populate_measurement_batch___raises_error() -> None:
values = [object(), object()]
request = PublishMeasurementBatchRequest()
Expand Down
Loading