diff --git a/docs/reference/ni-data-store-v2.md b/docs/reference/ni-data-store-v2.md index b8f7f1e..37a04c7 100644 --- a/docs/reference/ni-data-store-v2.md +++ b/docs/reference/ni-data-store-v2.md @@ -57,6 +57,12 @@ extensions as dictionaries. Queries accept an OData expression and optional `MeasurementField`, for example `read_fields=[StepField.CONDITIONS]`; field names as strings are also accepted. +`Step.has_children` is output-only and can be requested with +`read_fields=[StepField.HAS_CHILDREN]`. It becomes true after the step has had a +committed direct child and remains true even if that child is later deleted. +The installed V2 proto package must include the field for its value to be read; +older generated packages report it as false. + Updates change only the fields named in the protobuf update mask, and the client builds the mask for you. Pass a `Step`, or its ID, with keyword fields, an update builder, or the whole object: diff --git a/src/ni/datastore/data/v2/_types/_read_fields.py b/src/ni/datastore/data/v2/_types/_read_fields.py index 1f33c60..dbbf4f4 100644 --- a/src/ni/datastore/data/v2/_types/_read_fields.py +++ b/src/ni/datastore/data/v2/_types/_read_fields.py @@ -72,6 +72,7 @@ class StepField(str, Enum): CONDITIONS = "conditions" MEASUREMENTS = "measurements" CHILDREN = "children" + HAS_CHILDREN = "has_children" TEST = "test" diff --git a/src/ni/datastore/data/v2/_types/_step.py b/src/ni/datastore/data/v2/_types/_step.py index 48637c0..a7d9deb 100644 --- a/src/ni/datastore/data/v2/_types/_step.py +++ b/src/ni/datastore/data/v2/_types/_step.py @@ -58,6 +58,7 @@ class Step: "_children", "_path", "_path_ids", + "has_children", "created_at", "updated_at", "created_by", @@ -125,6 +126,7 @@ def __init__( children: Iterable[Step] | None = None, path: Iterable[str] | None = None, path_ids: Iterable[str] | None = None, + has_children: bool = False, created_at: ht.datetime | None = None, updated_at: ht.datetime | None = None, created_by: str = "", @@ -158,6 +160,7 @@ def __init__( children (optional): Child steps created with the step. path (optional): Output only. The step-name path, including this step. path_ids (optional): Output only. The step-identifier path, including this step. + has_children (optional): Output only. Whether this step has ever had a child step. created_at (optional): Output only. When the step was created. updated_at (optional): Output only. When the step was last updated. created_by (optional): Output only. Who created the step. @@ -190,6 +193,7 @@ def __init__( self._children: MutableSequence[Step] = list(children) if children is not None else [] self._path: MutableSequence[str] = string_list(path, "path") self._path_ids: MutableSequence[str] = string_list(path_ids, "path_ids") + self.has_children = has_children self.created_at = created_at self.updated_at = updated_at self.created_by = created_by @@ -228,6 +232,7 @@ def from_protobuf(step_proto: StepProto) -> "Step": children=[Step.from_protobuf(child) for child in step_proto.children], path=step_proto.path, path_ids=step_proto.path_ids, + has_children=getattr(step_proto, "has_children", False), created_at=timestamp_from_protobuf(step_proto, "created_at"), updated_at=timestamp_from_protobuf(step_proto, "updated_at"), created_by=step_proto.created_by, @@ -293,6 +298,7 @@ def __eq__(self, other: object) -> bool: and self.children == other.children and self.path == other.path and self.path_ids == other.path_ids + and self.has_children == other.has_children and self.created_at == other.created_at and self.updated_at == other.updated_at and self.created_by == other.created_by diff --git a/tests/unit/data/test_v2_data_store_client.py b/tests/unit/data/test_v2_data_store_client.py index abe1863..ad237b3 100644 --- a/tests/unit/data/test_v2_data_store_client.py +++ b/tests/unit/data/test_v2_data_store_client.py @@ -1225,9 +1225,15 @@ def test___get_step___sends_read_field_enums_as_field_names( ) -> None: service_client.get_step.return_value = data_store_pb2.Step(id="step-id") - client.get_step("step-id", read_fields=[StepField.NAME, StepField.CONDITIONS]) + client.get_step( + "step-id", read_fields=[StepField.NAME, StepField.CONDITIONS, StepField.HAS_CHILDREN] + ) - assert list(_sent(service_client.get_step).read_mask.paths) == ["name", "conditions"] + assert list(_sent(service_client.get_step).read_mask.paths) == [ + "name", + "conditions", + "has_children", + ] def test___query_measurements___accepts_read_field_enums_and_plain_names( diff --git a/tests/unit/data/test_v2_types.py b/tests/unit/data/test_v2_types.py index 4af066e..45094fa 100644 --- a/tests/unit/data/test_v2_types.py +++ b/tests/unit/data/test_v2_types.py @@ -7,7 +7,7 @@ import uuid from collections.abc import Callable from enum import Enum -from typing import Any +from typing import Any, cast import pytest from google.protobuf import json_format @@ -137,6 +137,34 @@ def test___step___round_trips_through_protobuf_including_conditions() -> None: assert Step.from_protobuf(proto) == step +def test___step___reads_has_children_from_protobuf() -> None: + class StepProtoWithHasChildren: + def __init__(self, step_proto: data_store_pb2.Step) -> None: + self._step_proto = step_proto + + def __getattr__(self, name: str) -> Any: + if name == "has_children": + return True + return getattr(self._step_proto, name) + + proto = cast( + data_store_pb2.Step, + StepProtoWithHasChildren(data_store_pb2.Step(name="parent")), + ) + + step = Step.from_protobuf(proto) + + assert step.has_children is True + + +def test___step___does_not_write_has_children_to_protobuf() -> None: + step = Step("parent", has_children=True) + + proto = step.to_protobuf() + + assert proto == data_store_pb2.Step(name="parent") + + def test___measurement___reads_metadata_and_conditions_from_protobuf() -> None: proto = data_store_pb2.Measurement( id="measurement-id", @@ -340,8 +368,11 @@ def test___read_field_enums___match_protobuf_fields( field_type: type[Enum], message_type: Any ) -> None: field_names = sorted(field.value for field in field_type) + protobuf_field_names = sorted(field.name for field in message_type.DESCRIPTOR.fields) + if field_type is StepField and "has_children" not in protobuf_field_names: + field_names.remove(StepField.HAS_CHILDREN.value) - assert field_names == sorted(field.name for field in message_type.DESCRIPTOR.fields) + assert field_names == protobuf_field_names @pytest.mark.parametrize( @@ -546,7 +577,14 @@ def test___client___wraps_every_low_level_method() -> None: assert sorted(low_level_methods - set(dir(DataStoreClient))) == [] -_OUTPUT_ONLY_INIT_PARAMETERS = {"id", "created_at", "updated_at", "created_by", "updated_by"} +_OUTPUT_ONLY_INIT_PARAMETERS = { + "id", + "created_at", + "updated_at", + "created_by", + "updated_by", + "has_children", +} @pytest.mark.parametrize(