Files
mxaccessgw/clients/python
Joseph Doherty 6add4b4acc Python client: port bulk read/write SDK methods + CLI subcommands
Mirrors the .NET / Go ports of divergent branch commit f220908. HEAD's
Session class had only the subscribe-style bulks; this commit adds the
value-bulk SDK surface plus matching CLI subcommands and a
bench-read-bulk harness.

SDK (zb_mom_ww_mxgateway/session.py):
- async def write_bulk(server_handle, entries, *, correlation_id="")
  → list[pb.BulkWriteResult]
- async def write2_bulk(server_handle, entries, *, correlation_id="")
  → list[pb.BulkWriteResult]
- async def write_secured_bulk(server_handle, entries, *, correlation_id="")
  → list[pb.BulkWriteResult]
- async def write_secured2_bulk(server_handle, entries, *, correlation_id="")
  → list[pb.BulkWriteResult]
- async def read_bulk(server_handle, tag_addresses, *, timeout_ms=0,
  correlation_id="") → list[pb.BulkReadResult]

All five reuse the existing _ensure_bulk_size validator and route
through the existing invoke() pipeline. read_bulk additionally enforces
timeout_ms >= 0.

CLI (zb_mom_ww_mxgateway_cli/commands.py):
- read-bulk / write-bulk / write2-bulk / write-secured-bulk /
  write-secured2-bulk registered as click @main.command(...). The
  write families share a _build_write_bulk_entries() helper that parses
  --item-handles and --values with a single --type, validates count
  match, converts via to_mx_value, and assembles the correct per-entry
  proto message.
- bench-read-bulk: opens its own session, subscribes to --bulk-size
  TestMachine_NNN.TestChangingInt tags, runs warmup then steady-state
  ReadBulk for --duration-seconds with time.perf_counter() latency
  capture, and emits the shared JSON schema (language, durationMs,
  totalCalls, successfulCalls, failedCalls, totalReadResults,
  cachedReadResults, callsPerSecond, latencyMs:{p50,p95,p99,max,mean})
  so scripts/bench-read-bulk.ps1 collates Python alongside the four
  other clients. _percentile_summary + linear-interpolation
  _percentile helper match the Go / .NET implementations.

to_mx_value is added to the existing values-module import line in
commands.py since the bulk-write commands need it.

Verification: python -m pip install -e . --quiet --no-deps; pytest
42/42 passing. Manual smoke against live gateway on localhost:5120:
open-session → register → subscribe-bulk on two
TestMachine_NNN.TestChangingInt tags (both wasSuccessful=true) →
read-bulk (both wasSuccessful=true / wasCached=true / int32 values
present) → close-session SESSION_STATE_CLOSED.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:50:10 -04:00
..

Python Client

The Python client package contains generated MXAccess Gateway protobuf bindings, the async zb_mom_ww_mxgateway package, and the mxgw-py test CLI. The package uses the shared proto inputs documented in ../../docs/ClientProtoGeneration.md so gateway and client contracts stay in sync.

Layout

clients/python/
  pyproject.toml
  generate-proto.ps1
  src/zb_mom_ww_mxgateway/
  src/zb_mom_ww_mxgateway/generated/
  src/zb_mom_ww_mxgateway_cli/
  tests/

src/zb_mom_ww_mxgateway/generated contains code produced by grpc_tools.protoc. Do not edit generated files by hand.

Regenerating Protobuf Bindings

Run generation after the shared .proto files or the Python output path changes:

./generate-proto.ps1

The script uses the Python tool path recorded in ../../docs/ToolchainLinks.md.

Build And Test

Run the Python checks from clients/python:

python -m pip install -e ".[dev]"
python -m pytest
python -m pip wheel . --no-deps --wheel-dir "$env:TEMP\mxgateway-python-wheel"

The tests import the generated gateway and worker stubs, run fake async gateway stubs, verify API key metadata, exercise stream cancellation, load shared value and command fixtures, and check deterministic CLI output.

Packaging

Install the package in editable mode for local development:

python -m pip install -e ".[dev]"

Build a wheel from clients/python:

python -m pip wheel . --no-deps --wheel-dir "$env:TEMP\mxgateway-python-wheel"

Install the generated wheel into a target environment:

python -m pip install <wheel-path>

The wheel exposes the mxgw-py console script.

Library Usage

The library is async-first:

from zb_mom_ww_mxgateway import GatewayClient

async with await GatewayClient.connect(
    endpoint="localhost:5000",
    api_key="<gateway-api-key>",
    plaintext=True,
) as client:
    session = await client.open_session(client_session_name="python-client")
    try:
        server_handle = await session.register("python-client")
        item_handle = await session.add_item(server_handle, "Object.Attribute")
        await session.advise(server_handle, item_handle)
    finally:
        await session.close()

GatewayClient.open_session_raw, GatewayClient.invoke_raw, and GatewayClient.stream_events_raw keep the generated protobuf replies and events available for parity tests. Session helpers call the method-specific MXAccess commands and preserve raw replies on typed command exceptions.

Canceling a Python task cancels the client-side gRPC call or stream wait. It does not abort an in-flight MXAccess COM call inside the worker process.

Galaxy Repository Browse

The GalaxyRepositoryClient wraps the read-only GalaxyRepository gRPC service. It lets callers test connectivity to the AVEVA System Platform Galaxy Repository (ZB SQL database), read the last deploy timestamp, and enumerate the deployed object hierarchy plus each object's dynamic attributes:

from zb_mom_ww_mxgateway import GalaxyRepositoryClient

async with await GalaxyRepositoryClient.connect(
    endpoint="localhost:5000",
    api_key="<gateway-api-key>",
    plaintext=True,
) as galaxy:
    if not await galaxy.test_connection():
        raise RuntimeError("gateway cannot reach the Galaxy Repository DB")

    last_deploy = await galaxy.get_last_deploy_time()
    print(f"last deploy: {last_deploy}")

    for obj in await galaxy.discover_hierarchy():
        print(obj.tag_name, obj.contained_name)
        for attr in obj.attributes:
            print("  ", attr.attribute_name, "->", attr.full_tag_reference)

The methods return native Python types (bool, datetime | None, and a list[GalaxyObject] of generated proto messages) so callers can index into the hierarchy without learning the underlying stub class. The service requires the metadata:read scope on the API key.

Watching deploy events

GalaxyRepositoryClient.watch_deploy_events opens a server-streaming subscription that emits the current cached deploy state immediately and then one DeployEvent per new Galaxy deploy. sequence is monotonic per gateway start; gaps mean events were dropped from the per-subscriber buffer. Pass last_seen_deploy_time to suppress the bootstrap event when the caller already has the current state cached:

from datetime import datetime, timezone
from zb_mom_ww_mxgateway import DeployEvent, GalaxyRepositoryClient

async with await GalaxyRepositoryClient.connect(
    endpoint="localhost:5000",
    api_key="<gateway-api-key>",
    plaintext=True,
) as galaxy:
    last_seen: datetime | None = None
    async for event in galaxy.watch_deploy_events(last_seen_deploy_time=last_seen):
        assert isinstance(event, DeployEvent)
        print(
            f"#{event.sequence} deploy={event.time_of_last_deploy.ToDatetime(tzinfo=timezone.utc)} "
            f"objects={event.object_count} attributes={event.attribute_count}"
        )
        if event.time_of_last_deploy_present:
            last_seen = event.time_of_last_deploy.ToDatetime(tzinfo=timezone.utc)

The method returns an async iterator yielding the generated DeployEvent proto. Breaking out of the loop, calling aclose() on the iterator, or cancelling the surrounding task closes the underlying gRPC stream cleanly. The streaming RPC requires the same metadata:read scope as the other Galaxy methods. The CLI does not currently expose a streaming watch-deploy-events subcommand — use the library API directly when subscribing to deploy events from Python.

Authentication And TLS

ClientOptions.api_key adds this metadata to unary calls and streams:

authorization: Bearer <api-key>

The client supports plaintext channels for local development, TLS with system roots, TLS with a custom ca_file, and an optional test server name override. API keys are redacted from option repr output and CLI error output.

CLI

The CLI emits deterministic JSON for automation:

mxgw-py version --json
mxgw-py open-session --endpoint localhost:5000 --plaintext --json
mxgw-py register --session-id <id> --client-name python-client --json
mxgw-py add-item --session-id <id> --server-handle 1 --item Object.Attribute --json
mxgw-py advise --session-id <id> --server-handle 1 --item-handle 2 --json
mxgw-py stream-events --session-id <id> --max-events 1 --json
mxgw-py write --session-id <id> --server-handle 1 --item-handle 2 --type int32 --value 123 --json

Use --api-key or --api-key-env MXGATEWAY_API_KEY to attach API key metadata. smoke opens a session, registers, adds an item, advises, streams a bounded event count, and closes the session in a finally block.

Use TLS options for a secured gateway:

mxgw-py smoke --endpoint mxgateway.example.local:5001 --tls --ca-file C:\certs\mxgateway-ca.pem --server-name-override mxgateway.example.local --api-key-env MXGATEWAY_API_KEY --item Object.Attribute --json

Integration Checks

Run live checks only when a gateway and MXAccess-backed worker are available:

$env:MXGATEWAY_INTEGRATION = '1'
$env:MXGATEWAY_ENDPOINT = 'localhost:5000'
$env:MXGATEWAY_API_KEY = '<gateway-api-key>'
$env:MXGATEWAY_TEST_ITEM = 'Object.Attribute'
mxgw-py smoke --endpoint $env:MXGATEWAY_ENDPOINT --plaintext --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json