Files
mxaccessgw/clients/python/PythonClientDesign.md
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

5.2 KiB

Python Client Detailed Design

Purpose

Provide an async Python client package for MXAccess Gateway, plus a test CLI and unit tests. The Python client should be useful for automation, diagnostics, and test harnesses.

Follow the Python Style Guide for handwritten code and the Protobuf Style Guide for generated contract inputs.

Package Layout

Recommended layout:

clients/python/
  pyproject.toml
  src/zb_mom_ww_mxgateway/
    __init__.py
    client.py
    session.py
    options.py
    auth.py
    values.py
    errors.py
    generated/
  src/zb_mom_ww_mxgateway_cli/
    __main__.py
    commands.py
  tests/

Expected dependencies:

  • grpcio
  • grpcio-tools
  • protobuf
  • click or typer
  • pytest
  • pytest-asyncio

Library API

Use async-first API. A sync wrapper can be added later if needed.

Suggested API:

client = await GatewayClient.connect(
    endpoint="localhost:5000",
    api_key=api_key,
    plaintext=True,
)

session = await client.open_session()
server = await session.register("python-client")
item = await session.add_item(server, "TestChildObject.TestInt")
await session.advise(server, item)

async for event in session.stream_events():
    ...

await session.close()
await client.close()

Classes:

class GatewayClient:
    @classmethod
    async def connect(cls, options: ClientOptions) -> "GatewayClient": ...
    async def open_session(self, options: OpenSessionOptions | None = None) -> "Session": ...
    async def invoke(self, request: MxCommandRequest) -> MxCommandReply: ...
    async def close(self) -> None: ...

class Session:
    session_id: str
    async def register(self, client_name: str) -> int: ...
    async def add_item(self, server_handle: int, item: str) -> int: ...
    async def add_item2(self, server_handle: int, item: str, context: str) -> int: ...
    async def advise(self, server_handle: int, item_handle: int) -> None: ...
    async def add_item_bulk(self, server_handle: int, tag_addresses: Sequence[str]) -> list[SubscribeResult]: ...
    async def advise_item_bulk(self, server_handle: int, item_handles: Sequence[int]) -> list[SubscribeResult]: ...
    async def remove_item_bulk(self, server_handle: int, item_handles: Sequence[int]) -> list[SubscribeResult]: ...
    async def unadvise_item_bulk(self, server_handle: int, item_handles: Sequence[int]) -> list[SubscribeResult]: ...
    async def subscribe_bulk(self, server_handle: int, tag_addresses: Sequence[str]) -> list[SubscribeResult]: ...
    async def unsubscribe_bulk(self, server_handle: int, item_handles: Sequence[int]) -> list[SubscribeResult]: ...
    async def write(self, server_handle: int, item_handle: int, value: MxValueInput, user_id: int = 0) -> None: ...
    async def stream_events(self) -> AsyncIterator[MxEvent]: ...
    async def close(self) -> None: ...

Authentication

Use gRPC metadata:

metadata = (("authorization", f"Bearer {api_key}"),)

Provide a metadata helper that all unary and streaming calls use. Redact API keys in exceptions and CLI output.

TLS

Support:

  • insecure channel for local development,
  • TLS channel with default roots,
  • custom root certificate file.

Streaming

Expose stream_events as an async iterator. Canceling the task should cancel the gRPC stream.

Do not hide stream errors. Convert common auth/session errors into typed exceptions.

Error Handling

Define typed exceptions:

MxGatewayError
MxGatewayTransportError
MxGatewayAuthenticationError
MxGatewayAuthorizationError
MxGatewaySessionError
MxGatewayWorkerError
MxGatewayCommandError
MxAccessError

MxGatewayCommandError should include the raw protobuf reply when available.

Test CLI

Entry point:

mxgw-py

Recommended commands:

mxgw-py version
mxgw-py smoke --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --item TestChildObject.TestInt
mxgw-py stream-events --session-id <id> --json
mxgw-py write --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123

Use click or typer. JSON output should be stable for test automation.

Unit Tests

Use pytest and pytest-asyncio.

Use fake generated stubs or an in-process test gRPC server where practical.

Required tests:

  • API key metadata injection,
  • API key redaction,
  • insecure and TLS channel option construction,
  • request construction for method helpers,
  • value conversion from Python values,
  • status/error mapping,
  • async event iteration,
  • stream cancellation,
  • CLI parsing,
  • JSON output.

Integration Tests

Skip unless:

MXGATEWAY_INTEGRATION=1

Use bounded smoke flow and always attempt close_session in finally.

Packaging

Use pyproject.toml. Publishable package name should be stable, for example:

mxaccess-gateway-client

Generated protobuf code should be regenerated through a documented command, not edited by hand.