Skip to content
Draft
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
6 changes: 6 additions & 0 deletions docs/reference/ni-data-store-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions src/ni/datastore/data/v2/_types/_read_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class StepField(str, Enum):
CONDITIONS = "conditions"
MEASUREMENTS = "measurements"
CHILDREN = "children"
HAS_CHILDREN = "has_children"
TEST = "test"


Expand Down
6 changes: 6 additions & 0 deletions src/ni/datastore/data/v2/_types/_step.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class Step:
"_children",
"_path",
"_path_ids",
"has_children",
"created_at",
"updated_at",
"created_by",
Expand Down Expand Up @@ -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 = "",
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions tests/unit/data/test_v2_data_store_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
44 changes: 41 additions & 3 deletions tests/unit/data/test_v2_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
Loading