feat(client-python): add write_array_elements default-fill helper and document semantics
Regenerate Python proto bindings to pick up MxSparseArray/MxSparseElement/
sparse_array_value from the shared mxaccess_gateway.proto. Add
Session.write_array_elements which builds an MxValue(sparse_array_value=…)
from a {index→scalar} dict and delegates to the existing write(). Add 8 pytest
tests covering builder correctness and full round-trip wire shape. Update
README with a default-fill semantics paragraph and bare-name array-write note.
This commit is contained in:
@@ -148,6 +148,33 @@ the unchanged elements included. For example, to change 2 elements of a
|
|||||||
the 2 new ones). Sending only the 2 changed values overwrites the attribute
|
the 2 new ones). Sending only the 2 changed values overwrites the attribute
|
||||||
with a 2-element array.
|
with a 2-element array.
|
||||||
|
|
||||||
|
### Default-fill partial array writes
|
||||||
|
|
||||||
|
`Session.write_array_elements` lets you write only the indices you care about.
|
||||||
|
The gateway fills every unmentioned position with the type default for the
|
||||||
|
declared `element_data_type` (0, `False`, `""`, Unix epoch for timestamps).
|
||||||
|
The previous value at those positions is **not** preserved — the gateway expands
|
||||||
|
the sparse map to a full array before forwarding the write to MXAccess, so this
|
||||||
|
is still a full replacement:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Write indices 0 and 5 of a 10-element integer array.
|
||||||
|
# Positions 1-4 and 6-9 become 0, not their previous values.
|
||||||
|
await session.write_array_elements(
|
||||||
|
server_handle=server_handle,
|
||||||
|
item_handle=item_handle,
|
||||||
|
element_data_type=pb.MX_DATA_TYPE_INTEGER,
|
||||||
|
total_length=10,
|
||||||
|
elements={0: 100, 5: 500},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Bare-name array items (e.g. `Object.ArrayAttr` without an index suffix) added
|
||||||
|
via `add_item` auto-normalize to `[]` — they refer to the whole array, not a
|
||||||
|
single element. Writes through such handles must cover the full array or use
|
||||||
|
`write_array_elements` to supply `total_length` and let the gateway fill
|
||||||
|
defaults for the rest.
|
||||||
|
|
||||||
## Galaxy Repository Browse
|
## Galaxy Repository Browse
|
||||||
|
|
||||||
The `GalaxyRepositoryClient` wraps the read-only `GalaxyRepository` gRPC
|
The `GalaxyRepositoryClient` wraps the read-only `GalaxyRepository` gRPC
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
# NO CHECKED-IN PROTOBUF GENCODE
|
# NO CHECKED-IN PROTOBUF GENCODE
|
||||||
# source: galaxy_repository.proto
|
# source: galaxy_repository.proto
|
||||||
# Protobuf Python Version: 6.31.1
|
# Protobuf Python Version: 6.33.5
|
||||||
"""Generated protocol buffer code."""
|
"""Generated protocol buffer code."""
|
||||||
from google.protobuf import descriptor as _descriptor
|
from google.protobuf import descriptor as _descriptor
|
||||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||||
@@ -12,8 +12,8 @@ from google.protobuf.internal import builder as _builder
|
|||||||
_runtime_version.ValidateProtobufRuntimeVersion(
|
_runtime_version.ValidateProtobufRuntimeVersion(
|
||||||
_runtime_version.Domain.PUBLIC,
|
_runtime_version.Domain.PUBLIC,
|
||||||
6,
|
6,
|
||||||
31,
|
33,
|
||||||
1,
|
5,
|
||||||
'',
|
'',
|
||||||
'galaxy_repository.proto'
|
'galaxy_repository.proto'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import warnings
|
|||||||
|
|
||||||
import galaxy_repository_pb2 as galaxy__repository__pb2
|
import galaxy_repository_pb2 as galaxy__repository__pb2
|
||||||
|
|
||||||
GRPC_GENERATED_VERSION = '1.80.0'
|
GRPC_GENERATED_VERSION = '1.81.1'
|
||||||
GRPC_VERSION = grpc.__version__
|
GRPC_VERSION = grpc.__version__
|
||||||
_version_not_supported = False
|
_version_not_supported = False
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ if _version_not_supported:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class GalaxyRepositoryStub(object):
|
class GalaxyRepositoryStub:
|
||||||
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
|
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
|
||||||
additively only. Never renumber or repurpose an existing field number or
|
additively only. Never renumber or repurpose an existing field number or
|
||||||
enum value. When a field or enum value is removed, add a `reserved` range
|
enum value. When a field or enum value is removed, add a `reserved` range
|
||||||
@@ -72,7 +72,7 @@ class GalaxyRepositoryStub(object):
|
|||||||
_registered_method=True)
|
_registered_method=True)
|
||||||
|
|
||||||
|
|
||||||
class GalaxyRepositoryServicer(object):
|
class GalaxyRepositoryServicer:
|
||||||
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
|
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
|
||||||
additively only. Never renumber or repurpose an existing field number or
|
additively only. Never renumber or repurpose an existing field number or
|
||||||
enum value. When a field or enum value is removed, add a `reserved` range
|
enum value. When a field or enum value is removed, add a `reserved` range
|
||||||
@@ -162,7 +162,7 @@ def add_GalaxyRepositoryServicer_to_server(servicer, server):
|
|||||||
|
|
||||||
|
|
||||||
# This class is part of an EXPERIMENTAL API.
|
# This class is part of an EXPERIMENTAL API.
|
||||||
class GalaxyRepository(object):
|
class GalaxyRepository:
|
||||||
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
|
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
|
||||||
additively only. Never renumber or repurpose an existing field number or
|
additively only. Never renumber or repurpose an existing field number or
|
||||||
enum value. When a field or enum value is removed, add a `reserved` range
|
enum value. When a field or enum value is removed, add a `reserved` range
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@ import warnings
|
|||||||
|
|
||||||
import mxaccess_gateway_pb2 as mxaccess__gateway__pb2
|
import mxaccess_gateway_pb2 as mxaccess__gateway__pb2
|
||||||
|
|
||||||
GRPC_GENERATED_VERSION = '1.80.0'
|
GRPC_GENERATED_VERSION = '1.81.1'
|
||||||
GRPC_VERSION = grpc.__version__
|
GRPC_VERSION = grpc.__version__
|
||||||
_version_not_supported = False
|
_version_not_supported = False
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ if _version_not_supported:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MxAccessGatewayStub(object):
|
class MxAccessGatewayStub:
|
||||||
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
|
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
|
||||||
additively only. Never renumber or repurpose an existing field number or
|
additively only. Never renumber or repurpose an existing field number or
|
||||||
enum value. When a field or enum value is removed, add a `reserved` range
|
enum value. When a field or enum value is removed, add a `reserved` range
|
||||||
@@ -78,7 +78,7 @@ class MxAccessGatewayStub(object):
|
|||||||
_registered_method=True)
|
_registered_method=True)
|
||||||
|
|
||||||
|
|
||||||
class MxAccessGatewayServicer(object):
|
class MxAccessGatewayServicer:
|
||||||
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
|
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
|
||||||
additively only. Never renumber or repurpose an existing field number or
|
additively only. Never renumber or repurpose an existing field number or
|
||||||
enum value. When a field or enum value is removed, add a `reserved` range
|
enum value. When a field or enum value is removed, add a `reserved` range
|
||||||
@@ -189,7 +189,7 @@ def add_MxAccessGatewayServicer_to_server(servicer, server):
|
|||||||
|
|
||||||
|
|
||||||
# This class is part of an EXPERIMENTAL API.
|
# This class is part of an EXPERIMENTAL API.
|
||||||
class MxAccessGateway(object):
|
class MxAccessGateway:
|
||||||
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
|
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
|
||||||
additively only. Never renumber or repurpose an existing field number or
|
additively only. Never renumber or repurpose an existing field number or
|
||||||
enum value. When a field or enum value is removed, add a `reserved` range
|
enum value. When a field or enum value is removed, add a `reserved` range
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
# NO CHECKED-IN PROTOBUF GENCODE
|
# NO CHECKED-IN PROTOBUF GENCODE
|
||||||
# source: mxaccess_worker.proto
|
# source: mxaccess_worker.proto
|
||||||
# Protobuf Python Version: 6.31.1
|
# Protobuf Python Version: 6.33.5
|
||||||
"""Generated protocol buffer code."""
|
"""Generated protocol buffer code."""
|
||||||
from google.protobuf import descriptor as _descriptor
|
from google.protobuf import descriptor as _descriptor
|
||||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||||
@@ -12,8 +12,8 @@ from google.protobuf.internal import builder as _builder
|
|||||||
_runtime_version.ValidateProtobufRuntimeVersion(
|
_runtime_version.ValidateProtobufRuntimeVersion(
|
||||||
_runtime_version.Domain.PUBLIC,
|
_runtime_version.Domain.PUBLIC,
|
||||||
6,
|
6,
|
||||||
31,
|
33,
|
||||||
1,
|
5,
|
||||||
'',
|
'',
|
||||||
'mxaccess_worker.proto'
|
'mxaccess_worker.proto'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import grpc
|
|||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
GRPC_GENERATED_VERSION = '1.80.0'
|
GRPC_GENERATED_VERSION = '1.81.1'
|
||||||
GRPC_VERSION = grpc.__version__
|
GRPC_VERSION = grpc.__version__
|
||||||
_version_not_supported = False
|
_version_not_supported = False
|
||||||
|
|
||||||
|
|||||||
@@ -489,6 +489,56 @@ class Session:
|
|||||||
correlation_id=correlation_id,
|
correlation_id=correlation_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def write_array_elements(
|
||||||
|
self,
|
||||||
|
server_handle: int,
|
||||||
|
item_handle: int,
|
||||||
|
element_data_type: "pb.MxDataType.ValueType",
|
||||||
|
total_length: int,
|
||||||
|
elements: dict[int, MxValueInput],
|
||||||
|
*,
|
||||||
|
user_id: int = 0,
|
||||||
|
correlation_id: str = "",
|
||||||
|
) -> None:
|
||||||
|
"""Write a partial array by specifying only the indices you want to set.
|
||||||
|
|
||||||
|
The gateway expands the sparse representation into a full ``total_length``
|
||||||
|
array before forwarding the write to MXAccess. Indices not listed in
|
||||||
|
*elements* are filled with the type default for *element_data_type* (0,
|
||||||
|
False, empty string, Unix epoch for timestamps, etc.). The previous
|
||||||
|
value at those positions is **not** preserved — this is a full array
|
||||||
|
replacement, not a patch.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
server_handle: Handle returned by :meth:`register`.
|
||||||
|
item_handle: Handle returned by :meth:`add_item`.
|
||||||
|
element_data_type: ``pb.MX_DATA_TYPE_*`` enum value for the scalar
|
||||||
|
element type of the target array attribute.
|
||||||
|
total_length: Total number of elements in the written array. Must
|
||||||
|
be > 0 and large enough to contain every index in *elements*.
|
||||||
|
elements: Mapping of zero-based element index to scalar value.
|
||||||
|
Values are converted with :func:`~zb_mom_ww_mxgateway.values.to_mx_value`.
|
||||||
|
user_id: Galaxy user id to stamp on the write (requires a prior
|
||||||
|
supervisory advise to take effect — see README).
|
||||||
|
correlation_id: Optional client-supplied correlation token echoed
|
||||||
|
in the command reply.
|
||||||
|
"""
|
||||||
|
sparse = pb.MxSparseArray(
|
||||||
|
element_data_type=element_data_type,
|
||||||
|
total_length=total_length,
|
||||||
|
elements=[
|
||||||
|
pb.MxSparseElement(index=idx, value=to_mx_value(val))
|
||||||
|
for idx, val in elements.items()
|
||||||
|
],
|
||||||
|
)
|
||||||
|
await self.write(
|
||||||
|
server_handle,
|
||||||
|
item_handle,
|
||||||
|
pb.MxValue(sparse_array_value=sparse),
|
||||||
|
user_id=user_id,
|
||||||
|
correlation_id=correlation_id,
|
||||||
|
)
|
||||||
|
|
||||||
async def write2(
|
async def write2(
|
||||||
self,
|
self,
|
||||||
server_handle: int,
|
server_handle: int,
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
"""Tests for Session.write_array_elements default-fill sparse-array helper."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from zb_mom_ww_mxgateway import ClientOptions, GatewayClient
|
||||||
|
from zb_mom_ww_mxgateway.generated import mxaccess_gateway_pb2 as pb
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _make_sparse_mx_value(
|
||||||
|
element_data_type: "pb.MxDataType.ValueType",
|
||||||
|
total_length: int,
|
||||||
|
elements: dict[int, Any],
|
||||||
|
) -> pb.MxValue:
|
||||||
|
"""Build an MxValue wrapping an MxSparseArray from Python primitives.
|
||||||
|
|
||||||
|
Mirrors the logic inside Session.write_array_elements so tests can assert
|
||||||
|
the exact wire shape the helper produces without going through the full
|
||||||
|
gRPC stack.
|
||||||
|
"""
|
||||||
|
from zb_mom_ww_mxgateway.values import to_mx_value
|
||||||
|
|
||||||
|
return pb.MxValue(
|
||||||
|
sparse_array_value=pb.MxSparseArray(
|
||||||
|
element_data_type=element_data_type,
|
||||||
|
total_length=total_length,
|
||||||
|
elements=[
|
||||||
|
pb.MxSparseElement(index=idx, value=to_mx_value(val))
|
||||||
|
for idx, val in elements.items()
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Fake stub (minimal — only needs Invoke / OpenSession)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeUnary:
|
||||||
|
def __init__(self, replies: list[Any]) -> None:
|
||||||
|
self.replies = list(replies)
|
||||||
|
self.requests: list[Any] = []
|
||||||
|
self.metadata: tuple[tuple[str, str], ...] | None = None
|
||||||
|
|
||||||
|
async def __call__(
|
||||||
|
self,
|
||||||
|
request: Any,
|
||||||
|
*,
|
||||||
|
metadata: tuple[tuple[str, str], ...],
|
||||||
|
) -> Any:
|
||||||
|
self.requests.append(request)
|
||||||
|
self.metadata = metadata
|
||||||
|
return self.replies.pop(0)
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeStub:
|
||||||
|
"""Minimal stub that satisfies GatewayClient for a single invoke round-trip."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
ok = pb.ProtocolStatus(code=pb.PROTOCOL_STATUS_CODE_OK)
|
||||||
|
self.open_session = _FakeUnary([pb.OpenSessionReply(session_id="s1", protocol_status=ok)])
|
||||||
|
self.invoke = _FakeUnary(
|
||||||
|
[
|
||||||
|
pb.MxCommandReply(
|
||||||
|
session_id="s1",
|
||||||
|
kind=pb.MX_COMMAND_KIND_WRITE,
|
||||||
|
protocol_status=ok,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.OpenSession = self.open_session
|
||||||
|
self.Invoke = self.invoke
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Unit tests
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_sparse_mx_value_builder_sets_correct_oneof() -> None:
|
||||||
|
"""Builder helper must produce an MxValue with kind == 'sparse_array_value'."""
|
||||||
|
mv = _make_sparse_mx_value(pb.MX_DATA_TYPE_INTEGER, 5, {0: 10, 3: 30})
|
||||||
|
assert mv.WhichOneof("kind") == "sparse_array_value"
|
||||||
|
|
||||||
|
|
||||||
|
def test_sparse_mx_value_builder_total_length() -> None:
|
||||||
|
"""total_length must equal the value passed to the builder."""
|
||||||
|
mv = _make_sparse_mx_value(pb.MX_DATA_TYPE_INTEGER, 20, {1: 7})
|
||||||
|
assert mv.sparse_array_value.total_length == 20
|
||||||
|
|
||||||
|
|
||||||
|
def test_sparse_mx_value_builder_element_count_and_values() -> None:
|
||||||
|
"""Elements list length and scalar values must match the input dict."""
|
||||||
|
mv = _make_sparse_mx_value(pb.MX_DATA_TYPE_INTEGER, 10, {0: 11, 4: 55, 9: 99})
|
||||||
|
sa = mv.sparse_array_value
|
||||||
|
assert len(sa.elements) == 3
|
||||||
|
by_index = {e.index: e.value for e in sa.elements}
|
||||||
|
assert by_index[0].int32_value == 11
|
||||||
|
assert by_index[4].int32_value == 55
|
||||||
|
assert by_index[9].int32_value == 99
|
||||||
|
|
||||||
|
|
||||||
|
def test_sparse_mx_value_builder_element_data_type() -> None:
|
||||||
|
"""element_data_type must be forwarded verbatim."""
|
||||||
|
mv = _make_sparse_mx_value(pb.MX_DATA_TYPE_FLOAT, 3, {})
|
||||||
|
assert mv.sparse_array_value.element_data_type == pb.MX_DATA_TYPE_FLOAT
|
||||||
|
|
||||||
|
|
||||||
|
def test_sparse_mx_value_builder_empty_elements() -> None:
|
||||||
|
"""An empty elements dict must still produce a valid MxSparseArray."""
|
||||||
|
mv = _make_sparse_mx_value(pb.MX_DATA_TYPE_BOOLEAN, 8, {})
|
||||||
|
sa = mv.sparse_array_value
|
||||||
|
assert len(sa.elements) == 0
|
||||||
|
assert sa.total_length == 8
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Integration-level: write_array_elements routes through Session.write
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_write_array_elements_sends_sparse_array_write_command() -> None:
|
||||||
|
"""write_array_elements must send a WRITE command whose value is sparse_array_value."""
|
||||||
|
stub = _FakeStub()
|
||||||
|
client = await GatewayClient.connect(
|
||||||
|
ClientOptions(endpoint="fake", api_key="mxgw_test_secret", plaintext=True),
|
||||||
|
stub=stub,
|
||||||
|
)
|
||||||
|
session = await client.open_session()
|
||||||
|
|
||||||
|
await session.write_array_elements(
|
||||||
|
server_handle=1,
|
||||||
|
item_handle=2,
|
||||||
|
element_data_type=pb.MX_DATA_TYPE_INTEGER,
|
||||||
|
total_length=10,
|
||||||
|
elements={0: 100, 5: 500},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(stub.invoke.requests) == 1
|
||||||
|
cmd_req: pb.MxCommandRequest = stub.invoke.requests[0]
|
||||||
|
cmd = cmd_req.command
|
||||||
|
assert cmd.kind == pb.MX_COMMAND_KIND_WRITE
|
||||||
|
mv = cmd.write.value
|
||||||
|
assert mv.WhichOneof("kind") == "sparse_array_value"
|
||||||
|
|
||||||
|
sa = mv.sparse_array_value
|
||||||
|
assert sa.element_data_type == pb.MX_DATA_TYPE_INTEGER
|
||||||
|
assert sa.total_length == 10
|
||||||
|
assert len(sa.elements) == 2
|
||||||
|
by_index = {e.index: e.value for e in sa.elements}
|
||||||
|
assert by_index[0].int32_value == 100
|
||||||
|
assert by_index[5].int32_value == 500
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_write_array_elements_forwards_user_id() -> None:
|
||||||
|
"""user_id must reach the WriteCommand."""
|
||||||
|
stub = _FakeStub()
|
||||||
|
client = await GatewayClient.connect(
|
||||||
|
ClientOptions(endpoint="fake", api_key="mxgw_test_secret", plaintext=True),
|
||||||
|
stub=stub,
|
||||||
|
)
|
||||||
|
session = await client.open_session()
|
||||||
|
|
||||||
|
await session.write_array_elements(
|
||||||
|
server_handle=1,
|
||||||
|
item_handle=2,
|
||||||
|
element_data_type=pb.MX_DATA_TYPE_BOOLEAN,
|
||||||
|
total_length=4,
|
||||||
|
elements={},
|
||||||
|
user_id=42,
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd = stub.invoke.requests[0].command
|
||||||
|
assert cmd.write.user_id == 42
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_write_array_elements_string_elements() -> None:
|
||||||
|
"""String element values must be encoded as string_value scalars."""
|
||||||
|
stub = _FakeStub()
|
||||||
|
client = await GatewayClient.connect(
|
||||||
|
ClientOptions(endpoint="fake", api_key="mxgw_test_secret", plaintext=True),
|
||||||
|
stub=stub,
|
||||||
|
)
|
||||||
|
session = await client.open_session()
|
||||||
|
|
||||||
|
await session.write_array_elements(
|
||||||
|
server_handle=1,
|
||||||
|
item_handle=2,
|
||||||
|
element_data_type=pb.MX_DATA_TYPE_STRING,
|
||||||
|
total_length=3,
|
||||||
|
elements={1: "hello", 2: "world"},
|
||||||
|
)
|
||||||
|
|
||||||
|
sa = stub.invoke.requests[0].command.write.value.sparse_array_value
|
||||||
|
by_index = {e.index: e.value for e in sa.elements}
|
||||||
|
assert by_index[1].string_value == "hello"
|
||||||
|
assert by_index[2].string_value == "world"
|
||||||
Reference in New Issue
Block a user