Files
mxaccessgw/docs/clients-python-design.md
T
2026-04-26 15:19:17 -04:00

4.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/mxgateway/
    __init__.py
    client.py
    session.py
    options.py
    auth.py
    values.py
    errors.py
    generated/
  src/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 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.