Files
mxaccessgw/clients/python/src/zb_mom_ww_mxgateway/values.py
T
Joseph Doherty 397d3c5c4f rename: apply ZB.MOM.WW prefix to all client SDKs + fix pre-existing alarm-RPC breaks
Rename across every client surface using each language's idiomatic convention:

  * .NET   clients/dotnet/MxGateway.Client[.Cli|.Tests]/
             -> clients/dotnet/ZB.MOM.WW.MxGateway.Client[.Cli|.Tests]/
             namespaces -> ZB.MOM.WW.MxGateway.Client[.Cli|.Tests]
             contracts ProjectReference repointed to ZB.MOM.WW.MxGateway.Contracts
             sln migrated to slnx (dotnet sln migrate)
  * Python src/mxgateway -> src/zb_mom_ww_mxgateway
             src/mxgateway_cli -> src/zb_mom_ww_mxgateway_cli
             distribution: mxaccess-gateway-client -> zb-mom-ww-mxaccess-gateway-client
  * Rust   crate: mxgateway-client -> zb-mom-ww-mxgateway-client
             build.rs proto path repointed
  * Java   subprojects: mxgateway-{client,cli} -> zb-mom-ww-mxgateway-{client,cli}
             packages com.dohertylan.mxgateway -> com.zb.mom.ww.mxgateway
             group   com.dohertylan.mxgateway -> com.zb.mom.ww.mxgateway
             rootProject mxaccessgw-java -> zb-mom-ww-mxaccessgw-java
  * Go     generate-proto.ps1 proto path repointed; module path and
             package mxgateway kept (Go convention).
  * proto-inputs.json: generatedOutputs.python updated to new package path.
  * scripts/run-client-e2e-tests.ps1: Java CLI install path + gradle task
             updated to zb-mom-ww-mxgateway-cli.

CLI binary names (mxgw, mxgw-py, mxgw-go, mxgateway-cli) and wire-level
identifiers (MXGATEWAY_* env vars, the mxgw_<id>_<secret> API key
prefix, protobuf package names like mxaccess_gateway.v1, all MXAccess
references) intentionally NOT renamed.

Fix pre-existing alarms-over-gateway breaks unblocked by the rename:

  * mxaccess_gateway.proto: add missing public message QueryActiveAlarmsRequest
    {session_id, client_correlation_id, alarm_filter_prefix} and missing
    rpc QueryActiveAlarms(QueryActiveAlarmsRequest) returns
    (stream ActiveAlarmSnapshot). All four typed clients referenced
    these but they were absent from the proto.
  * MxAccessGatewayService.QueryActiveAlarms: implement the new RPC on
    the server, streaming from IGatewayAlarmService.CurrentAlarms with
    optional alarm_filter_prefix filter.
  * clients/dotnet/.../DiscoverHierarchyOptions.cs: add the hand-written
    .NET POCO that wraps DiscoverHierarchyRequest (referenced by
    GalaxyRepositoryClient.DiscoverHierarchyAsync but never authored).
  * Drop retired session_id field references from
    AcknowledgeAlarmRequest/AcknowledgeAlarmReply test fixtures across
    .NET, Rust, Go, and Python clients.
  * Rust integration test: add the missing stream_alarms impl on the
    fake MxAccessGateway server (the trait gained the method, fake
    didn't).
  * Rust CLI test: bump expected gatewayProtocolVersion 2 -> 3.

Regenerated artifacts updated in this commit:
  * src/ZB.MOM.WW.MxGateway.Contracts/Generated/{MxaccessGateway,MxaccessGatewayGrpc}.cs
  * clients/python/src/zb_mom_ww_mxgateway/generated/*_pb2{,_grpc}.py
  * clients/go/internal/generated/*.pb.go
(C# regenerated by Grpc.Tools on contracts build; Python and Go via
their generate-proto.ps1 scripts; Rust regenerates from .proto via
tonic-build at compile time so no checked-in artefact.)

Verification: 472 server tests, 275 worker tests (9 dev-rig skipped),
18 integration tests (live MxAccess + LDAP + Galaxy), 57 .NET client
tests, 32 Rust workspace tests, 39 Python tests, all Go packages, and
gradle build for Java all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 19:09:34 -04:00

235 lines
7.2 KiB
Python

"""MXAccess value conversion helpers."""
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any
from google.protobuf.timestamp_pb2 import Timestamp
from .generated import mxaccess_gateway_pb2 as pb
MxValueInput = bool | int | float | str | datetime | bytes | None | Sequence[Any]
@dataclass(frozen=True)
class MxValueView:
"""Typed projection of a raw `MxValue` protobuf message."""
value: Any
kind: str
raw: pb.MxValue
def to_mx_value(value: MxValueInput, *, data_type: str | None = None) -> pb.MxValue:
"""Convert a Python value into the public protobuf `MxValue` union."""
if isinstance(value, pb.MxValue):
return value
if value is None:
return pb.MxValue(
data_type=pb.MX_DATA_TYPE_NO_DATA,
variant_type="VT_EMPTY",
is_null=True,
raw_data_type=pb.MX_DATA_TYPE_NO_DATA,
)
if isinstance(value, bool):
return pb.MxValue(
data_type=_data_type(data_type, pb.MX_DATA_TYPE_BOOLEAN),
variant_type="VT_BOOL",
bool_value=value,
)
if isinstance(value, int):
if -(2**31) <= value <= (2**31 - 1):
return pb.MxValue(
data_type=_data_type(data_type, pb.MX_DATA_TYPE_INTEGER),
variant_type="VT_I4",
int32_value=value,
)
return pb.MxValue(
data_type=_data_type(data_type, pb.MX_DATA_TYPE_INTEGER),
variant_type="VT_I8",
int64_value=value,
)
if isinstance(value, float):
return pb.MxValue(
data_type=_data_type(data_type, pb.MX_DATA_TYPE_DOUBLE),
variant_type="VT_R8",
double_value=value,
)
if isinstance(value, str):
return pb.MxValue(
data_type=_data_type(data_type, pb.MX_DATA_TYPE_STRING),
variant_type="VT_BSTR",
string_value=value,
)
if isinstance(value, datetime):
return pb.MxValue(
data_type=_data_type(data_type, pb.MX_DATA_TYPE_TIME),
variant_type="VT_DATE",
timestamp_value=_timestamp_from_datetime(value),
)
if isinstance(value, bytes):
return pb.MxValue(
data_type=_data_type(data_type, pb.MX_DATA_TYPE_UNKNOWN),
variant_type="VT_RECORD",
raw_value=value,
)
if isinstance(value, Sequence):
return _sequence_to_mx_value(value, data_type=data_type)
raise TypeError(f"unsupported MxValue input type: {type(value).__name__}")
def from_mx_value(value: pb.MxValue) -> MxValueView:
"""Project a protobuf `MxValue` into an idiomatic Python value."""
kind = value.WhichOneof("kind")
if kind is None:
return MxValueView(None, "none", value)
if kind == "timestamp_value":
return MxValueView(
value.timestamp_value.ToDatetime().replace(tzinfo=timezone.utc),
kind,
value,
)
if kind == "array_value":
return MxValueView(from_mx_array(value.array_value), kind, value)
return MxValueView(getattr(value, kind), kind, value)
def from_mx_array(array: pb.MxArray) -> list[Any]:
"""Project a protobuf `MxArray` into a Python list."""
kind = array.WhichOneof("values")
if kind is None:
return []
values = list(getattr(array, kind).values)
if kind == "timestamp_values":
return [
timestamp.ToDatetime().replace(tzinfo=timezone.utc)
for timestamp in values
]
return values
def _sequence_to_mx_value(
values: Sequence[Any],
*,
data_type: str | None,
) -> pb.MxValue:
sequence = list(values)
if not sequence:
return pb.MxValue(
data_type=_data_type(data_type, pb.MX_DATA_TYPE_UNKNOWN),
array_value=pb.MxArray(
element_data_type=pb.MX_DATA_TYPE_UNKNOWN,
dimensions=[0],
),
)
first = sequence[0]
dimensions = [len(sequence)]
if all(isinstance(item, bool) for item in sequence):
array = pb.MxArray(
element_data_type=pb.MX_DATA_TYPE_BOOLEAN,
variant_type="VT_ARRAY|VT_BOOL",
dimensions=dimensions,
bool_values=pb.BoolArray(values=sequence),
)
return pb.MxValue(data_type=pb.MX_DATA_TYPE_BOOLEAN, array_value=array)
if all(isinstance(item, int) and not isinstance(item, bool) for item in sequence):
use_int32 = all(-(2**31) <= item <= (2**31 - 1) for item in sequence)
if use_int32:
array = pb.MxArray(
element_data_type=pb.MX_DATA_TYPE_INTEGER,
variant_type="VT_ARRAY|VT_I4",
dimensions=dimensions,
int32_values=pb.Int32Array(values=sequence),
)
else:
array = pb.MxArray(
element_data_type=pb.MX_DATA_TYPE_INTEGER,
variant_type="VT_ARRAY|VT_I8",
dimensions=dimensions,
int64_values=pb.Int64Array(values=sequence),
)
return pb.MxValue(data_type=pb.MX_DATA_TYPE_INTEGER, array_value=array)
if all(isinstance(item, float) for item in sequence):
array = pb.MxArray(
element_data_type=pb.MX_DATA_TYPE_DOUBLE,
variant_type="VT_ARRAY|VT_R8",
dimensions=dimensions,
double_values=pb.DoubleArray(values=sequence),
)
return pb.MxValue(data_type=pb.MX_DATA_TYPE_DOUBLE, array_value=array)
if all(isinstance(item, str) for item in sequence):
array = pb.MxArray(
element_data_type=pb.MX_DATA_TYPE_STRING,
variant_type="VT_ARRAY|VT_BSTR",
dimensions=dimensions,
string_values=pb.StringArray(values=sequence),
)
return pb.MxValue(data_type=pb.MX_DATA_TYPE_STRING, array_value=array)
if all(isinstance(item, datetime) for item in sequence):
array = pb.MxArray(
element_data_type=pb.MX_DATA_TYPE_TIME,
variant_type="VT_ARRAY|VT_DATE",
dimensions=dimensions,
timestamp_values=pb.TimestampArray(
values=[_timestamp_from_datetime(item) for item in sequence],
),
)
return pb.MxValue(data_type=pb.MX_DATA_TYPE_TIME, array_value=array)
if all(isinstance(item, bytes) for item in sequence):
array = pb.MxArray(
element_data_type=pb.MX_DATA_TYPE_UNKNOWN,
variant_type="VT_ARRAY|VT_VARIANT",
dimensions=dimensions,
raw_values=pb.RawArray(values=sequence),
)
return pb.MxValue(data_type=pb.MX_DATA_TYPE_UNKNOWN, array_value=array)
raise TypeError(
"MxValue array inputs must use one supported element type; "
f"first element was {type(first).__name__}"
)
def _timestamp_from_datetime(value: datetime) -> Timestamp:
timestamp = Timestamp()
if value.tzinfo is None:
value = value.replace(tzinfo=timezone.utc)
timestamp.FromDatetime(value.astimezone(timezone.utc))
return timestamp
def _data_type(name: str | None, default: int) -> int:
if name is None:
return default
return pb.MxDataType.Value(name)