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>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
"""MXAccess Gateway Python client package."""
|
||||
|
||||
from .auth import ApiKey, auth_metadata
|
||||
from .client import GatewayClient
|
||||
from .galaxy import GalaxyRepositoryClient
|
||||
from .generated.galaxy_repository_pb2 import (
|
||||
DeployEvent,
|
||||
GalaxyAttribute,
|
||||
GalaxyObject,
|
||||
WatchDeployEventsRequest,
|
||||
)
|
||||
from .errors import (
|
||||
MxAccessError,
|
||||
MxGatewayAuthenticationError,
|
||||
MxGatewayAuthorizationError,
|
||||
MxGatewayCommandError,
|
||||
MxGatewayError,
|
||||
MxGatewaySessionError,
|
||||
MxGatewayTransportError,
|
||||
MxGatewayWorkerError,
|
||||
)
|
||||
from .options import ClientOptions
|
||||
from .session import Session
|
||||
from .values import MxValueView, from_mx_value, to_mx_value
|
||||
from .version import __version__
|
||||
|
||||
__all__ = [
|
||||
"ApiKey",
|
||||
"ClientOptions",
|
||||
"DeployEvent",
|
||||
"GalaxyAttribute",
|
||||
"GalaxyObject",
|
||||
"GalaxyRepositoryClient",
|
||||
"GatewayClient",
|
||||
"MxAccessError",
|
||||
"MxGatewayAuthenticationError",
|
||||
"MxGatewayAuthorizationError",
|
||||
"MxGatewayCommandError",
|
||||
"MxGatewayError",
|
||||
"MxGatewaySessionError",
|
||||
"MxGatewayTransportError",
|
||||
"MxGatewayWorkerError",
|
||||
"MxValueView",
|
||||
"Session",
|
||||
"WatchDeployEventsRequest",
|
||||
"__version__",
|
||||
"auth_metadata",
|
||||
"from_mx_value",
|
||||
"to_mx_value",
|
||||
]
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Authentication metadata helpers for MXAccess Gateway clients."""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
|
||||
AUTHORIZATION_HEADER = "authorization"
|
||||
REDACTED = "[redacted]"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ApiKey:
|
||||
"""API key wrapper that avoids leaking the secret through repr output."""
|
||||
|
||||
value: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Validate that the API key value is non-empty."""
|
||||
if not self.value:
|
||||
raise ValueError("api_key must not be empty")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a repr that redacts the secret value."""
|
||||
return f"{type(self).__name__}({REDACTED!r})"
|
||||
|
||||
def bearer_value(self) -> str:
|
||||
"""Return the value formatted as an HTTP `Bearer` token."""
|
||||
return f"Bearer {self.value}"
|
||||
|
||||
|
||||
def auth_metadata(api_key: str | ApiKey | None) -> tuple[tuple[str, str], ...]:
|
||||
"""Return gRPC metadata for API key auth."""
|
||||
|
||||
if api_key is None:
|
||||
return ()
|
||||
|
||||
key = api_key.value if isinstance(api_key, ApiKey) else api_key
|
||||
if not key:
|
||||
return ()
|
||||
|
||||
return ((AUTHORIZATION_HEADER, f"Bearer {key}"),)
|
||||
|
||||
|
||||
def merge_metadata(
|
||||
api_key: str | ApiKey | None,
|
||||
metadata: Sequence[tuple[str, str]] | None = None,
|
||||
) -> tuple[tuple[str, str], ...]:
|
||||
"""Merge caller metadata with API key metadata."""
|
||||
|
||||
merged = list(metadata or ())
|
||||
merged.extend(auth_metadata(api_key))
|
||||
return tuple(merged)
|
||||
|
||||
|
||||
def redact_secret(text: str, secrets: Sequence[str | None]) -> str:
|
||||
"""Replace known secret values with a stable redaction marker."""
|
||||
|
||||
redacted = text
|
||||
for secret in secrets:
|
||||
if secret:
|
||||
redacted = redacted.replace(secret, REDACTED)
|
||||
return redacted
|
||||
@@ -0,0 +1,225 @@
|
||||
"""Async MXAccess Gateway client wrapper."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator, Sequence
|
||||
from typing import Any
|
||||
|
||||
import grpc
|
||||
|
||||
from .auth import merge_metadata
|
||||
from .errors import ensure_protocol_success, map_rpc_error
|
||||
from .generated import mxaccess_gateway_pb2 as pb
|
||||
from .generated import mxaccess_gateway_pb2_grpc as pb_grpc
|
||||
from .options import ClientOptions, create_channel
|
||||
|
||||
|
||||
class GatewayClient:
|
||||
"""Async client for the public MXAccess Gateway gRPC API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
options: ClientOptions,
|
||||
stub: Any,
|
||||
channel: grpc.aio.Channel | None = None,
|
||||
) -> None:
|
||||
"""Initialize the client with resolved options and a gRPC stub."""
|
||||
self.options = options
|
||||
self.raw_stub = stub
|
||||
self._channel = channel
|
||||
self._closed = False
|
||||
|
||||
@classmethod
|
||||
async def connect(
|
||||
cls,
|
||||
options: ClientOptions | None = None,
|
||||
*,
|
||||
endpoint: str | None = None,
|
||||
api_key: str | None = None,
|
||||
plaintext: bool = False,
|
||||
ca_file: str | None = None,
|
||||
server_name_override: str | None = None,
|
||||
stub: Any | None = None,
|
||||
) -> "GatewayClient":
|
||||
"""Create a client with either a real async channel or a supplied fake stub."""
|
||||
|
||||
resolved = options or ClientOptions(
|
||||
endpoint=endpoint or "",
|
||||
api_key=api_key,
|
||||
plaintext=plaintext,
|
||||
ca_file=ca_file,
|
||||
server_name_override=server_name_override,
|
||||
)
|
||||
|
||||
if stub is not None:
|
||||
return cls(options=resolved, stub=stub)
|
||||
|
||||
channel = create_channel(resolved)
|
||||
return cls(
|
||||
options=resolved,
|
||||
stub=pb_grpc.MxAccessGatewayStub(channel),
|
||||
channel=channel,
|
||||
)
|
||||
|
||||
async def __aenter__(self) -> "GatewayClient":
|
||||
"""Return self to support ``async with`` usage."""
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_exc_info: object) -> None:
|
||||
"""Close the client when leaving an ``async with`` block."""
|
||||
await self.close()
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Close the owned gRPC channel."""
|
||||
|
||||
if self._closed:
|
||||
return
|
||||
|
||||
if self._channel is not None:
|
||||
await self._channel.close()
|
||||
self._closed = True
|
||||
|
||||
async def open_session(
|
||||
self,
|
||||
request: pb.OpenSessionRequest | None = None,
|
||||
*,
|
||||
requested_backend: str = "",
|
||||
client_session_name: str = "",
|
||||
client_correlation_id: str = "",
|
||||
) -> "Session":
|
||||
"""Open a gateway session and return a high-level session wrapper."""
|
||||
|
||||
from .session import Session
|
||||
|
||||
raw_request = request or pb.OpenSessionRequest(
|
||||
requested_backend=requested_backend,
|
||||
client_session_name=client_session_name,
|
||||
client_correlation_id=client_correlation_id,
|
||||
)
|
||||
reply = await self.open_session_raw(raw_request)
|
||||
return Session(client=self, session_id=reply.session_id, open_reply=reply)
|
||||
|
||||
async def open_session_raw(self, request: pb.OpenSessionRequest) -> pb.OpenSessionReply:
|
||||
"""Send an `OpenSession` RPC and return the raw reply."""
|
||||
reply = await self._unary("open session", self.raw_stub.OpenSession, request)
|
||||
ensure_protocol_success("open session", reply.protocol_status, reply)
|
||||
return reply
|
||||
|
||||
async def close_session_raw(
|
||||
self,
|
||||
request: pb.CloseSessionRequest,
|
||||
) -> pb.CloseSessionReply:
|
||||
"""Send a `CloseSession` RPC and return the raw reply."""
|
||||
reply = await self._unary("close session", self.raw_stub.CloseSession, request)
|
||||
ensure_protocol_success("close session", reply.protocol_status, reply)
|
||||
return reply
|
||||
|
||||
async def invoke_raw(self, request: pb.MxCommandRequest) -> pb.MxCommandReply:
|
||||
"""Send an `Invoke` RPC and return the raw reply."""
|
||||
reply = await self._unary("invoke", self.raw_stub.Invoke, request)
|
||||
ensure_protocol_success("invoke", reply.protocol_status, reply)
|
||||
return reply
|
||||
|
||||
def stream_events_raw(
|
||||
self,
|
||||
request: pb.StreamEventsRequest,
|
||||
*,
|
||||
metadata: Sequence[tuple[str, str]] | None = None,
|
||||
) -> AsyncIterator[pb.MxEvent]:
|
||||
"""Return an async event iterator and cancel the stream when iteration stops."""
|
||||
|
||||
kwargs: dict[str, Any] = {"metadata": merge_metadata(self.options.api_key, metadata)}
|
||||
if self.options.stream_timeout is not None:
|
||||
kwargs["timeout"] = self.options.stream_timeout
|
||||
call = self.raw_stub.StreamEvents(request, **kwargs)
|
||||
return _canceling_iterator(call)
|
||||
|
||||
async def acknowledge_alarm(
|
||||
self,
|
||||
request: pb.AcknowledgeAlarmRequest,
|
||||
) -> pb.AcknowledgeAlarmReply:
|
||||
"""Acknowledge an active MXAccess alarm condition through the gateway.
|
||||
|
||||
The gateway authenticates the request against the API key's
|
||||
``invoke:alarm-ack`` scope and forwards the acknowledge to the worker's
|
||||
MXAccess session; the resulting native ``MxStatus`` is returned in the
|
||||
reply. Acks are idempotent — re-acking an already-acked condition is a
|
||||
no-op at the MxAccess layer.
|
||||
"""
|
||||
reply = await self._unary("acknowledge alarm", self.raw_stub.AcknowledgeAlarm, request)
|
||||
ensure_protocol_success("acknowledge alarm", reply.protocol_status, reply)
|
||||
return reply
|
||||
|
||||
def query_active_alarms(
|
||||
self,
|
||||
request: pb.QueryActiveAlarmsRequest,
|
||||
*,
|
||||
metadata: Sequence[tuple[str, str]] | None = None,
|
||||
) -> AsyncIterator[pb.ActiveAlarmSnapshot]:
|
||||
"""Stream a snapshot of all alarms currently Active or ActiveAcked.
|
||||
|
||||
The gateway's ConditionRefresh equivalent. Use after reconnect to seed
|
||||
local Part 9 state, or to reconcile alarms that may have been missed
|
||||
during a transport blip. Optionally scoped by alarm-reference prefix
|
||||
(``request.alarm_filter_prefix``) so a partial refresh can target an
|
||||
equipment sub-tree.
|
||||
"""
|
||||
kwargs: dict[str, Any] = {"metadata": merge_metadata(self.options.api_key, metadata)}
|
||||
if self.options.stream_timeout is not None:
|
||||
kwargs["timeout"] = self.options.stream_timeout
|
||||
call = self.raw_stub.QueryActiveAlarms(request, **kwargs)
|
||||
return _canceling_active_alarms_iterator(call)
|
||||
|
||||
async def _unary(
|
||||
self,
|
||||
operation: str,
|
||||
method: Any,
|
||||
request: Any,
|
||||
*,
|
||||
metadata: Sequence[tuple[str, str]] | None = None,
|
||||
) -> Any:
|
||||
kwargs: dict[str, Any] = {"metadata": merge_metadata(self.options.api_key, metadata)}
|
||||
if self.options.call_timeout is not None:
|
||||
kwargs["timeout"] = self.options.call_timeout
|
||||
try:
|
||||
call = method(request, **kwargs)
|
||||
except TypeError as error:
|
||||
if "timeout" not in kwargs or "unexpected keyword argument 'timeout'" not in str(error):
|
||||
raise
|
||||
kwargs.pop("timeout")
|
||||
call = method(request, **kwargs)
|
||||
try:
|
||||
return await call
|
||||
except asyncio.CancelledError:
|
||||
cancel = getattr(call, "cancel", None)
|
||||
if cancel is not None:
|
||||
cancel()
|
||||
raise
|
||||
except grpc.RpcError as error:
|
||||
raise map_rpc_error(operation, error) from error
|
||||
|
||||
|
||||
async def _canceling_iterator(call: Any) -> AsyncIterator[pb.MxEvent]:
|
||||
try:
|
||||
async for event in call:
|
||||
yield event
|
||||
except grpc.RpcError as error:
|
||||
raise map_rpc_error("stream events", error) from error
|
||||
finally:
|
||||
cancel = getattr(call, "cancel", None)
|
||||
if cancel is not None:
|
||||
cancel()
|
||||
|
||||
|
||||
async def _canceling_active_alarms_iterator(call: Any) -> AsyncIterator[pb.ActiveAlarmSnapshot]:
|
||||
try:
|
||||
async for snapshot in call:
|
||||
yield snapshot
|
||||
except grpc.RpcError as error:
|
||||
raise map_rpc_error("query active alarms", error) from error
|
||||
finally:
|
||||
cancel = getattr(call, "cancel", None)
|
||||
if cancel is not None:
|
||||
cancel()
|
||||
@@ -0,0 +1,158 @@
|
||||
"""Typed exception model for MXAccess Gateway Python clients."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import grpc
|
||||
|
||||
from .generated import mxaccess_gateway_pb2 as pb
|
||||
|
||||
|
||||
class MxGatewayError(Exception):
|
||||
"""Base class for client wrapper errors."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
*,
|
||||
protocol_status: pb.ProtocolStatus | None = None,
|
||||
raw_reply: Any | None = None,
|
||||
) -> None:
|
||||
"""Initialize with a message and the optional raw protocol context."""
|
||||
super().__init__(message)
|
||||
self.protocol_status = protocol_status
|
||||
self.raw_reply = raw_reply
|
||||
|
||||
|
||||
class MxGatewayTransportError(MxGatewayError):
|
||||
"""Transport-level gRPC failure."""
|
||||
|
||||
|
||||
class MxGatewayAuthenticationError(MxGatewayTransportError):
|
||||
"""Authentication failure reported by gRPC."""
|
||||
|
||||
|
||||
class MxGatewayAuthorizationError(MxGatewayTransportError):
|
||||
"""Authorization failure reported by gRPC."""
|
||||
|
||||
|
||||
class MxGatewaySessionError(MxGatewayError):
|
||||
"""Gateway session failure."""
|
||||
|
||||
|
||||
class MxGatewayWorkerError(MxGatewayError):
|
||||
"""Gateway worker process or protocol failure."""
|
||||
|
||||
|
||||
class MxGatewayCommandError(MxGatewayError):
|
||||
"""Command failure that preserves the raw protobuf reply."""
|
||||
|
||||
|
||||
class MxAccessError(MxGatewayCommandError):
|
||||
"""MXAccess HRESULT or status failure."""
|
||||
|
||||
|
||||
def map_rpc_error(operation: str, error: grpc.RpcError) -> MxGatewayTransportError:
|
||||
"""Map a generated gRPC exception to the client exception hierarchy."""
|
||||
|
||||
code = error.code() if hasattr(error, "code") else None
|
||||
details = error.details() if hasattr(error, "details") else str(error)
|
||||
message = f"{operation} failed: {details}"
|
||||
|
||||
if code == grpc.StatusCode.UNAUTHENTICATED:
|
||||
return MxGatewayAuthenticationError(message)
|
||||
if code == grpc.StatusCode.PERMISSION_DENIED:
|
||||
return MxGatewayAuthorizationError(message)
|
||||
|
||||
return MxGatewayTransportError(message)
|
||||
|
||||
|
||||
def ensure_protocol_success(
|
||||
operation: str,
|
||||
protocol_status: pb.ProtocolStatus | None,
|
||||
raw_reply: Any | None = None,
|
||||
) -> Any | None:
|
||||
"""Raise typed gateway errors for non-OK protocol statuses."""
|
||||
|
||||
code = (
|
||||
protocol_status.code
|
||||
if protocol_status is not None
|
||||
else pb.PROTOCOL_STATUS_CODE_UNSPECIFIED
|
||||
)
|
||||
|
||||
if code in (
|
||||
pb.PROTOCOL_STATUS_CODE_OK,
|
||||
pb.PROTOCOL_STATUS_CODE_MXACCESS_FAILURE,
|
||||
):
|
||||
return raw_reply
|
||||
|
||||
message_text = protocol_status.message if protocol_status else ""
|
||||
message = f"{operation} failed: {message_text or pb.ProtocolStatusCode.Name(code)}"
|
||||
|
||||
if code in (
|
||||
pb.PROTOCOL_STATUS_CODE_SESSION_NOT_FOUND,
|
||||
pb.PROTOCOL_STATUS_CODE_SESSION_NOT_READY,
|
||||
):
|
||||
raise MxGatewaySessionError(
|
||||
message,
|
||||
protocol_status=protocol_status,
|
||||
raw_reply=raw_reply,
|
||||
)
|
||||
|
||||
if code in (
|
||||
pb.PROTOCOL_STATUS_CODE_WORKER_UNAVAILABLE,
|
||||
pb.PROTOCOL_STATUS_CODE_TIMEOUT,
|
||||
pb.PROTOCOL_STATUS_CODE_CANCELED,
|
||||
pb.PROTOCOL_STATUS_CODE_PROTOCOL_VIOLATION,
|
||||
):
|
||||
raise MxGatewayWorkerError(
|
||||
message,
|
||||
protocol_status=protocol_status,
|
||||
raw_reply=raw_reply,
|
||||
)
|
||||
|
||||
raise MxGatewayCommandError(
|
||||
message,
|
||||
protocol_status=protocol_status,
|
||||
raw_reply=raw_reply,
|
||||
)
|
||||
|
||||
|
||||
def ensure_mxaccess_success(operation: str, reply: pb.MxCommandReply) -> pb.MxCommandReply:
|
||||
"""Raise `MxAccessError` when MXAccess returned HRESULT or status failure."""
|
||||
|
||||
status = reply.protocol_status
|
||||
if status.code == pb.PROTOCOL_STATUS_CODE_MXACCESS_FAILURE:
|
||||
raise MxAccessError(
|
||||
_mxaccess_message(operation, reply),
|
||||
protocol_status=status,
|
||||
raw_reply=reply,
|
||||
)
|
||||
|
||||
if reply.HasField("hresult") and reply.hresult < 0:
|
||||
raise MxAccessError(
|
||||
_mxaccess_message(operation, reply),
|
||||
protocol_status=status,
|
||||
raw_reply=reply,
|
||||
)
|
||||
|
||||
for mx_status in reply.statuses:
|
||||
if mx_status.success == 0:
|
||||
raise MxAccessError(
|
||||
_mxaccess_message(operation, reply),
|
||||
protocol_status=status,
|
||||
raw_reply=reply,
|
||||
)
|
||||
|
||||
return reply
|
||||
|
||||
|
||||
def _mxaccess_message(operation: str, reply: pb.MxCommandReply) -> str:
|
||||
status_text = reply.protocol_status.message or "MXAccess command failed"
|
||||
hresult = reply.hresult if reply.HasField("hresult") else None
|
||||
return (
|
||||
f"{operation} failed: {status_text}; "
|
||||
f"session={reply.session_id}; correlation={reply.correlation_id}; "
|
||||
f"hresult={hresult}; statuses={len(reply.statuses)}"
|
||||
)
|
||||
@@ -0,0 +1,219 @@
|
||||
"""Async Galaxy Repository client wrapper.
|
||||
|
||||
Wraps the read-only ``GalaxyRepository`` gRPC service exposed by the
|
||||
MxAccess Gateway. The service lets callers test connectivity to the AVEVA
|
||||
System Platform Galaxy Repository (ZB SQL database), read the last
|
||||
deployment timestamp, and enumerate the deployed object hierarchy plus the
|
||||
attributes on each object.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator, Sequence
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import grpc
|
||||
from google.protobuf.timestamp_pb2 import Timestamp
|
||||
|
||||
from .auth import merge_metadata
|
||||
from .errors import MxGatewayError, map_rpc_error
|
||||
from .generated import galaxy_repository_pb2 as galaxy_pb
|
||||
from .generated import galaxy_repository_pb2_grpc as galaxy_pb_grpc
|
||||
from .options import ClientOptions, create_channel
|
||||
|
||||
_DISCOVER_HIERARCHY_PAGE_SIZE = 5000
|
||||
|
||||
|
||||
class GalaxyRepositoryClient:
|
||||
"""Async client for the Galaxy Repository gRPC service."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
options: ClientOptions,
|
||||
stub: Any,
|
||||
channel: grpc.aio.Channel | None = None,
|
||||
) -> None:
|
||||
"""Initialize the client with resolved options and a gRPC stub."""
|
||||
self.options = options
|
||||
self.raw_stub = stub
|
||||
self._channel = channel
|
||||
self._closed = False
|
||||
|
||||
@classmethod
|
||||
async def connect(
|
||||
cls,
|
||||
options: ClientOptions | None = None,
|
||||
*,
|
||||
endpoint: str | None = None,
|
||||
api_key: str | None = None,
|
||||
plaintext: bool = False,
|
||||
ca_file: str | None = None,
|
||||
server_name_override: str | None = None,
|
||||
stub: Any | None = None,
|
||||
) -> "GalaxyRepositoryClient":
|
||||
"""Create a client with either a real async channel or a supplied fake stub."""
|
||||
|
||||
resolved = options or ClientOptions(
|
||||
endpoint=endpoint or "",
|
||||
api_key=api_key,
|
||||
plaintext=plaintext,
|
||||
ca_file=ca_file,
|
||||
server_name_override=server_name_override,
|
||||
)
|
||||
|
||||
if stub is not None:
|
||||
return cls(options=resolved, stub=stub)
|
||||
|
||||
channel = create_channel(resolved)
|
||||
return cls(
|
||||
options=resolved,
|
||||
stub=galaxy_pb_grpc.GalaxyRepositoryStub(channel),
|
||||
channel=channel,
|
||||
)
|
||||
|
||||
async def __aenter__(self) -> "GalaxyRepositoryClient":
|
||||
"""Return self to support ``async with`` usage."""
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_exc_info: object) -> None:
|
||||
"""Close the client when leaving an ``async with`` block."""
|
||||
await self.close()
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Close the owned gRPC channel."""
|
||||
|
||||
if self._closed:
|
||||
return
|
||||
|
||||
if self._channel is not None:
|
||||
await self._channel.close()
|
||||
self._closed = True
|
||||
|
||||
async def test_connection(self) -> bool:
|
||||
"""Return ``True`` when the gateway can reach the Galaxy Repository DB."""
|
||||
|
||||
reply = await self._unary(
|
||||
"test connection",
|
||||
self.raw_stub.TestConnection,
|
||||
galaxy_pb.TestConnectionRequest(),
|
||||
)
|
||||
return bool(reply.ok)
|
||||
|
||||
async def get_last_deploy_time(self) -> datetime | None:
|
||||
"""Return the last Galaxy deploy timestamp or ``None`` when unset."""
|
||||
|
||||
reply = await self._unary(
|
||||
"get last deploy time",
|
||||
self.raw_stub.GetLastDeployTime,
|
||||
galaxy_pb.GetLastDeployTimeRequest(),
|
||||
)
|
||||
if not reply.present:
|
||||
return None
|
||||
return reply.time_of_last_deploy.ToDatetime()
|
||||
|
||||
async def discover_hierarchy(self) -> list[galaxy_pb.GalaxyObject]:
|
||||
"""Return the deployed Galaxy object hierarchy as raw proto messages."""
|
||||
|
||||
objects: list[galaxy_pb.GalaxyObject] = []
|
||||
seen_page_tokens: set[str] = set()
|
||||
page_token = ""
|
||||
while True:
|
||||
reply = await self._unary(
|
||||
"discover hierarchy",
|
||||
self.raw_stub.DiscoverHierarchy,
|
||||
galaxy_pb.DiscoverHierarchyRequest(
|
||||
page_size=_DISCOVER_HIERARCHY_PAGE_SIZE,
|
||||
page_token=page_token,
|
||||
),
|
||||
)
|
||||
objects.extend(reply.objects)
|
||||
page_token = reply.next_page_token
|
||||
if not page_token:
|
||||
return objects
|
||||
if page_token in seen_page_tokens:
|
||||
raise MxGatewayError(
|
||||
f"galaxy discover hierarchy returned repeated page token {page_token!r}"
|
||||
)
|
||||
seen_page_tokens.add(page_token)
|
||||
|
||||
def watch_deploy_events(
|
||||
self,
|
||||
last_seen_deploy_time: datetime | None = None,
|
||||
*,
|
||||
metadata: Sequence[tuple[str, str]] | None = None,
|
||||
) -> AsyncIterator[galaxy_pb.DeployEvent]:
|
||||
"""Stream Galaxy deploy events.
|
||||
|
||||
On subscribe the gateway emits the current cached state and then one
|
||||
event per new deploy time. ``sequence`` is monotonic per server start;
|
||||
gaps mean events were dropped from the per-subscriber buffer. When
|
||||
``last_seen_deploy_time`` is supplied and matches the current cached
|
||||
deploy time the bootstrap event is suppressed.
|
||||
"""
|
||||
|
||||
request = galaxy_pb.WatchDeployEventsRequest()
|
||||
if last_seen_deploy_time is not None:
|
||||
timestamp = Timestamp()
|
||||
timestamp.FromDatetime(last_seen_deploy_time)
|
||||
request.last_seen_deploy_time.CopyFrom(timestamp)
|
||||
|
||||
kwargs: dict[str, Any] = {"metadata": merge_metadata(self.options.api_key, metadata)}
|
||||
if self.options.stream_timeout is not None:
|
||||
kwargs["timeout"] = self.options.stream_timeout
|
||||
try:
|
||||
call = self.raw_stub.WatchDeployEvents(request, **kwargs)
|
||||
except TypeError as error:
|
||||
if "timeout" not in kwargs or "unexpected keyword argument 'timeout'" not in str(error):
|
||||
raise
|
||||
kwargs.pop("timeout")
|
||||
call = self.raw_stub.WatchDeployEvents(request, **kwargs)
|
||||
|
||||
return _canceling_iterator(call)
|
||||
|
||||
async def _unary(
|
||||
self,
|
||||
operation: str,
|
||||
method: Any,
|
||||
request: Any,
|
||||
*,
|
||||
metadata: Sequence[tuple[str, str]] | None = None,
|
||||
) -> Any:
|
||||
kwargs: dict[str, Any] = {"metadata": merge_metadata(self.options.api_key, metadata)}
|
||||
if self.options.call_timeout is not None:
|
||||
kwargs["timeout"] = self.options.call_timeout
|
||||
try:
|
||||
call = method(request, **kwargs)
|
||||
except TypeError as error:
|
||||
if "timeout" not in kwargs or "unexpected keyword argument 'timeout'" not in str(error):
|
||||
raise
|
||||
kwargs.pop("timeout")
|
||||
call = method(request, **kwargs)
|
||||
try:
|
||||
return await call
|
||||
except asyncio.CancelledError:
|
||||
cancel = getattr(call, "cancel", None)
|
||||
if cancel is not None:
|
||||
cancel()
|
||||
raise
|
||||
except grpc.RpcError as error:
|
||||
raise map_rpc_error(operation, error) from error
|
||||
|
||||
|
||||
async def _canceling_iterator(call: Any) -> AsyncIterator[galaxy_pb.DeployEvent]:
|
||||
try:
|
||||
async for event in call:
|
||||
yield event
|
||||
except asyncio.CancelledError:
|
||||
cancel = getattr(call, "cancel", None)
|
||||
if cancel is not None:
|
||||
cancel()
|
||||
raise
|
||||
except grpc.RpcError as error:
|
||||
raise map_rpc_error("watch deploy events", error) from error
|
||||
finally:
|
||||
cancel = getattr(call, "cancel", None)
|
||||
if cancel is not None:
|
||||
cancel()
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Generated protobuf and gRPC modules for MXAccess Gateway.
|
||||
|
||||
The Python protobuf generator emits absolute imports between generated modules.
|
||||
This package initializer registers package-local aliases so callers can import
|
||||
the generated stubs through `zb_mom_ww_mxgateway.generated` without moving the modules to
|
||||
the top-level import namespace.
|
||||
"""
|
||||
|
||||
from importlib import import_module
|
||||
import sys
|
||||
|
||||
mxaccess_gateway_pb2 = import_module(f"{__name__}.mxaccess_gateway_pb2")
|
||||
sys.modules.setdefault("mxaccess_gateway_pb2", mxaccess_gateway_pb2)
|
||||
|
||||
mxaccess_gateway_pb2_grpc = import_module(f"{__name__}.mxaccess_gateway_pb2_grpc")
|
||||
sys.modules.setdefault("mxaccess_gateway_pb2_grpc", mxaccess_gateway_pb2_grpc)
|
||||
|
||||
mxaccess_worker_pb2 = import_module(f"{__name__}.mxaccess_worker_pb2")
|
||||
sys.modules.setdefault("mxaccess_worker_pb2", mxaccess_worker_pb2)
|
||||
|
||||
mxaccess_worker_pb2_grpc = import_module(f"{__name__}.mxaccess_worker_pb2_grpc")
|
||||
sys.modules.setdefault("mxaccess_worker_pb2_grpc", mxaccess_worker_pb2_grpc)
|
||||
|
||||
galaxy_repository_pb2 = import_module(f"{__name__}.galaxy_repository_pb2")
|
||||
sys.modules.setdefault("galaxy_repository_pb2", galaxy_repository_pb2)
|
||||
|
||||
galaxy_repository_pb2_grpc = import_module(f"{__name__}.galaxy_repository_pb2_grpc")
|
||||
sys.modules.setdefault("galaxy_repository_pb2_grpc", galaxy_repository_pb2_grpc)
|
||||
|
||||
__all__ = [
|
||||
"galaxy_repository_pb2",
|
||||
"galaxy_repository_pb2_grpc",
|
||||
"mxaccess_gateway_pb2",
|
||||
"mxaccess_gateway_pb2_grpc",
|
||||
"mxaccess_worker_pb2",
|
||||
"mxaccess_worker_pb2_grpc",
|
||||
]
|
||||
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# NO CHECKED-IN PROTOBUF GENCODE
|
||||
# source: galaxy_repository.proto
|
||||
# Protobuf Python Version: 6.31.1
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import runtime_version as _runtime_version
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf.internal import builder as _builder
|
||||
_runtime_version.ValidateProtobufRuntimeVersion(
|
||||
_runtime_version.Domain.PUBLIC,
|
||||
6,
|
||||
31,
|
||||
1,
|
||||
'',
|
||||
'galaxy_repository.proto'
|
||||
)
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
|
||||
from google.protobuf import wrappers_pb2 as google_dot_protobuf_dot_wrappers__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17galaxy_repository.proto\x12\x14galaxy_repository.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\"\x17\n\x15TestConnectionRequest\"!\n\x13TestConnectionReply\x12\n\n\x02ok\x18\x01 \x01(\x08\"\x1a\n\x18GetLastDeployTimeRequest\"b\n\x16GetLastDeployTimeReply\x12\x0f\n\x07present\x18\x01 \x01(\x08\x12\x37\n\x13time_of_last_deploy\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x87\x03\n\x18\x44iscoverHierarchyRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\x12\x19\n\x0froot_gobject_id\x18\x03 \x01(\x05H\x00\x12\x17\n\rroot_tag_name\x18\x04 \x01(\tH\x00\x12\x1d\n\x13root_contained_path\x18\x05 \x01(\tH\x00\x12.\n\tmax_depth\x18\x06 \x01(\x0b\x32\x1b.google.protobuf.Int32Value\x12\x14\n\x0c\x63\x61tegory_ids\x18\x07 \x03(\x05\x12\x1f\n\x17template_chain_contains\x18\x08 \x03(\t\x12\x15\n\rtag_name_glob\x18\t \x01(\t\x12\x1f\n\x12include_attributes\x18\n \x01(\x08H\x01\x88\x01\x01\x12\x1a\n\x12\x61larm_bearing_only\x18\x0b \x01(\x08\x12\x17\n\x0fhistorized_only\x18\x0c \x01(\x08\x42\x06\n\x04rootB\x15\n\x13_include_attributes\"\x82\x01\n\x16\x44iscoverHierarchyReply\x12\x33\n\x07objects\x18\x01 \x03(\x0b\x32\".galaxy_repository.v1.GalaxyObject\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\x12\x1a\n\x12total_object_count\x18\x03 \x01(\x05\"U\n\x18WatchDeployEventsRequest\x12\x39\n\x15last_seen_deploy_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xdd\x01\n\x0b\x44\x65ployEvent\x12\x10\n\x08sequence\x18\x01 \x01(\x04\x12/\n\x0bobserved_at\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x37\n\x13time_of_last_deploy\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12#\n\x1btime_of_last_deploy_present\x18\x04 \x01(\x08\x12\x14\n\x0cobject_count\x18\x05 \x01(\x05\x12\x17\n\x0f\x61ttribute_count\x18\x06 \x01(\x05\"\x93\x02\n\x0cGalaxyObject\x12\x12\n\ngobject_id\x18\x01 \x01(\x05\x12\x10\n\x08tag_name\x18\x02 \x01(\t\x12\x16\n\x0e\x63ontained_name\x18\x03 \x01(\t\x12\x13\n\x0b\x62rowse_name\x18\x04 \x01(\t\x12\x19\n\x11parent_gobject_id\x18\x05 \x01(\x05\x12\x0f\n\x07is_area\x18\x06 \x01(\x08\x12\x13\n\x0b\x63\x61tegory_id\x18\x07 \x01(\x05\x12\x1c\n\x14hosted_by_gobject_id\x18\x08 \x01(\x05\x12\x16\n\x0etemplate_chain\x18\t \x03(\t\x12\x39\n\nattributes\x18\n \x03(\x0b\x32%.galaxy_repository.v1.GalaxyAttribute\"\xa8\x02\n\x0fGalaxyAttribute\x12\x16\n\x0e\x61ttribute_name\x18\x01 \x01(\t\x12\x1a\n\x12\x66ull_tag_reference\x18\x02 \x01(\t\x12\x14\n\x0cmx_data_type\x18\x03 \x01(\x05\x12\x16\n\x0e\x64\x61ta_type_name\x18\x04 \x01(\t\x12\x10\n\x08is_array\x18\x05 \x01(\x08\x12\x17\n\x0f\x61rray_dimension\x18\x06 \x01(\x05\x12\x1f\n\x17\x61rray_dimension_present\x18\x07 \x01(\x08\x12\x1d\n\x15mx_attribute_category\x18\x08 \x01(\x05\x12\x1f\n\x17security_classification\x18\t \x01(\x05\x12\x15\n\ris_historized\x18\n \x01(\x08\x12\x10\n\x08is_alarm\x18\x0b \x01(\x08\x32\xcc\x03\n\x10GalaxyRepository\x12h\n\x0eTestConnection\x12+.galaxy_repository.v1.TestConnectionRequest\x1a).galaxy_repository.v1.TestConnectionReply\x12q\n\x11GetLastDeployTime\x12..galaxy_repository.v1.GetLastDeployTimeRequest\x1a,.galaxy_repository.v1.GetLastDeployTimeReply\x12q\n\x11\x44iscoverHierarchy\x12..galaxy_repository.v1.DiscoverHierarchyRequest\x1a,.galaxy_repository.v1.DiscoverHierarchyReply\x12h\n\x11WatchDeployEvents\x12..galaxy_repository.v1.WatchDeployEventsRequest\x1a!.galaxy_repository.v1.DeployEvent0\x01\x42-\xaa\x02*ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxyb\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'galaxy_repository_pb2', _globals)
|
||||
if not _descriptor._USE_C_DESCRIPTORS:
|
||||
_globals['DESCRIPTOR']._loaded_options = None
|
||||
_globals['DESCRIPTOR']._serialized_options = b'\252\002*ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy'
|
||||
_globals['_TESTCONNECTIONREQUEST']._serialized_start=114
|
||||
_globals['_TESTCONNECTIONREQUEST']._serialized_end=137
|
||||
_globals['_TESTCONNECTIONREPLY']._serialized_start=139
|
||||
_globals['_TESTCONNECTIONREPLY']._serialized_end=172
|
||||
_globals['_GETLASTDEPLOYTIMEREQUEST']._serialized_start=174
|
||||
_globals['_GETLASTDEPLOYTIMEREQUEST']._serialized_end=200
|
||||
_globals['_GETLASTDEPLOYTIMEREPLY']._serialized_start=202
|
||||
_globals['_GETLASTDEPLOYTIMEREPLY']._serialized_end=300
|
||||
_globals['_DISCOVERHIERARCHYREQUEST']._serialized_start=303
|
||||
_globals['_DISCOVERHIERARCHYREQUEST']._serialized_end=694
|
||||
_globals['_DISCOVERHIERARCHYREPLY']._serialized_start=697
|
||||
_globals['_DISCOVERHIERARCHYREPLY']._serialized_end=827
|
||||
_globals['_WATCHDEPLOYEVENTSREQUEST']._serialized_start=829
|
||||
_globals['_WATCHDEPLOYEVENTSREQUEST']._serialized_end=914
|
||||
_globals['_DEPLOYEVENT']._serialized_start=917
|
||||
_globals['_DEPLOYEVENT']._serialized_end=1138
|
||||
_globals['_GALAXYOBJECT']._serialized_start=1141
|
||||
_globals['_GALAXYOBJECT']._serialized_end=1416
|
||||
_globals['_GALAXYATTRIBUTE']._serialized_start=1419
|
||||
_globals['_GALAXYATTRIBUTE']._serialized_end=1715
|
||||
_globals['_GALAXYREPOSITORY']._serialized_start=1718
|
||||
_globals['_GALAXYREPOSITORY']._serialized_end=2178
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
@@ -0,0 +1,265 @@
|
||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
import warnings
|
||||
|
||||
import galaxy_repository_pb2 as galaxy__repository__pb2
|
||||
|
||||
GRPC_GENERATED_VERSION = '1.80.0'
|
||||
GRPC_VERSION = grpc.__version__
|
||||
_version_not_supported = False
|
||||
|
||||
try:
|
||||
from grpc._utilities import first_version_is_lower
|
||||
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
|
||||
except ImportError:
|
||||
_version_not_supported = True
|
||||
|
||||
if _version_not_supported:
|
||||
raise RuntimeError(
|
||||
f'The grpc package installed is at version {GRPC_VERSION},'
|
||||
+ ' but the generated code in galaxy_repository_pb2_grpc.py depends on'
|
||||
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
|
||||
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
|
||||
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
|
||||
)
|
||||
|
||||
|
||||
class GalaxyRepositoryStub(object):
|
||||
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
|
||||
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
|
||||
(and `reserved` name) covering it in the same change so a future editor
|
||||
cannot accidentally reuse the retired tag. There are no `reserved`
|
||||
declarations today because no field or enum value has ever been removed.
|
||||
|
||||
Read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL
|
||||
database). Lets clients enumerate the deployed object hierarchy and each
|
||||
object's dynamic attributes so they know what tag references to subscribe
|
||||
to via the MxAccessGateway service.
|
||||
"""
|
||||
|
||||
def __init__(self, channel):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
channel: A grpc.Channel.
|
||||
"""
|
||||
self.TestConnection = channel.unary_unary(
|
||||
'/galaxy_repository.v1.GalaxyRepository/TestConnection',
|
||||
request_serializer=galaxy__repository__pb2.TestConnectionRequest.SerializeToString,
|
||||
response_deserializer=galaxy__repository__pb2.TestConnectionReply.FromString,
|
||||
_registered_method=True)
|
||||
self.GetLastDeployTime = channel.unary_unary(
|
||||
'/galaxy_repository.v1.GalaxyRepository/GetLastDeployTime',
|
||||
request_serializer=galaxy__repository__pb2.GetLastDeployTimeRequest.SerializeToString,
|
||||
response_deserializer=galaxy__repository__pb2.GetLastDeployTimeReply.FromString,
|
||||
_registered_method=True)
|
||||
self.DiscoverHierarchy = channel.unary_unary(
|
||||
'/galaxy_repository.v1.GalaxyRepository/DiscoverHierarchy',
|
||||
request_serializer=galaxy__repository__pb2.DiscoverHierarchyRequest.SerializeToString,
|
||||
response_deserializer=galaxy__repository__pb2.DiscoverHierarchyReply.FromString,
|
||||
_registered_method=True)
|
||||
self.WatchDeployEvents = channel.unary_stream(
|
||||
'/galaxy_repository.v1.GalaxyRepository/WatchDeployEvents',
|
||||
request_serializer=galaxy__repository__pb2.WatchDeployEventsRequest.SerializeToString,
|
||||
response_deserializer=galaxy__repository__pb2.DeployEvent.FromString,
|
||||
_registered_method=True)
|
||||
|
||||
|
||||
class GalaxyRepositoryServicer(object):
|
||||
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
|
||||
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
|
||||
(and `reserved` name) covering it in the same change so a future editor
|
||||
cannot accidentally reuse the retired tag. There are no `reserved`
|
||||
declarations today because no field or enum value has ever been removed.
|
||||
|
||||
Read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL
|
||||
database). Lets clients enumerate the deployed object hierarchy and each
|
||||
object's dynamic attributes so they know what tag references to subscribe
|
||||
to via the MxAccessGateway service.
|
||||
"""
|
||||
|
||||
def TestConnection(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def GetLastDeployTime(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def DiscoverHierarchy(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def WatchDeployEvents(self, request, context):
|
||||
"""Server-stream of deploy events. The server emits the current state immediately
|
||||
on subscribe (so clients can bootstrap their cache without waiting for the next
|
||||
deploy), then emits one event each time the gateway's hierarchy cache observes
|
||||
a new galaxy.time_of_last_deploy. The sequence field is monotonically
|
||||
increasing per server start; gaps indicate the per-subscriber buffer dropped
|
||||
older events because the client was too slow.
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
|
||||
def add_GalaxyRepositoryServicer_to_server(servicer, server):
|
||||
rpc_method_handlers = {
|
||||
'TestConnection': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.TestConnection,
|
||||
request_deserializer=galaxy__repository__pb2.TestConnectionRequest.FromString,
|
||||
response_serializer=galaxy__repository__pb2.TestConnectionReply.SerializeToString,
|
||||
),
|
||||
'GetLastDeployTime': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.GetLastDeployTime,
|
||||
request_deserializer=galaxy__repository__pb2.GetLastDeployTimeRequest.FromString,
|
||||
response_serializer=galaxy__repository__pb2.GetLastDeployTimeReply.SerializeToString,
|
||||
),
|
||||
'DiscoverHierarchy': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.DiscoverHierarchy,
|
||||
request_deserializer=galaxy__repository__pb2.DiscoverHierarchyRequest.FromString,
|
||||
response_serializer=galaxy__repository__pb2.DiscoverHierarchyReply.SerializeToString,
|
||||
),
|
||||
'WatchDeployEvents': grpc.unary_stream_rpc_method_handler(
|
||||
servicer.WatchDeployEvents,
|
||||
request_deserializer=galaxy__repository__pb2.WatchDeployEventsRequest.FromString,
|
||||
response_serializer=galaxy__repository__pb2.DeployEvent.SerializeToString,
|
||||
),
|
||||
}
|
||||
generic_handler = grpc.method_handlers_generic_handler(
|
||||
'galaxy_repository.v1.GalaxyRepository', rpc_method_handlers)
|
||||
server.add_generic_rpc_handlers((generic_handler,))
|
||||
server.add_registered_method_handlers('galaxy_repository.v1.GalaxyRepository', rpc_method_handlers)
|
||||
|
||||
|
||||
# This class is part of an EXPERIMENTAL API.
|
||||
class GalaxyRepository(object):
|
||||
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
|
||||
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
|
||||
(and `reserved` name) covering it in the same change so a future editor
|
||||
cannot accidentally reuse the retired tag. There are no `reserved`
|
||||
declarations today because no field or enum value has ever been removed.
|
||||
|
||||
Read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL
|
||||
database). Lets clients enumerate the deployed object hierarchy and each
|
||||
object's dynamic attributes so they know what tag references to subscribe
|
||||
to via the MxAccessGateway service.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def TestConnection(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/galaxy_repository.v1.GalaxyRepository/TestConnection',
|
||||
galaxy__repository__pb2.TestConnectionRequest.SerializeToString,
|
||||
galaxy__repository__pb2.TestConnectionReply.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def GetLastDeployTime(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/galaxy_repository.v1.GalaxyRepository/GetLastDeployTime',
|
||||
galaxy__repository__pb2.GetLastDeployTimeRequest.SerializeToString,
|
||||
galaxy__repository__pb2.GetLastDeployTimeReply.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def DiscoverHierarchy(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/galaxy_repository.v1.GalaxyRepository/DiscoverHierarchy',
|
||||
galaxy__repository__pb2.DiscoverHierarchyRequest.SerializeToString,
|
||||
galaxy__repository__pb2.DiscoverHierarchyReply.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def WatchDeployEvents(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_stream(
|
||||
request,
|
||||
target,
|
||||
'/galaxy_repository.v1.GalaxyRepository/WatchDeployEvents',
|
||||
galaxy__repository__pb2.WatchDeployEventsRequest.SerializeToString,
|
||||
galaxy__repository__pb2.DeployEvent.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,386 @@
|
||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
import warnings
|
||||
|
||||
import mxaccess_gateway_pb2 as mxaccess__gateway__pb2
|
||||
|
||||
GRPC_GENERATED_VERSION = '1.80.0'
|
||||
GRPC_VERSION = grpc.__version__
|
||||
_version_not_supported = False
|
||||
|
||||
try:
|
||||
from grpc._utilities import first_version_is_lower
|
||||
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
|
||||
except ImportError:
|
||||
_version_not_supported = True
|
||||
|
||||
if _version_not_supported:
|
||||
raise RuntimeError(
|
||||
f'The grpc package installed is at version {GRPC_VERSION},'
|
||||
+ ' but the generated code in mxaccess_gateway_pb2_grpc.py depends on'
|
||||
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
|
||||
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
|
||||
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
|
||||
)
|
||||
|
||||
|
||||
class MxAccessGatewayStub(object):
|
||||
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
|
||||
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
|
||||
(and `reserved` name) covering it in the same change so a future editor
|
||||
cannot accidentally reuse the retired tag.
|
||||
|
||||
Public client API for MXAccess sessions hosted by the gateway.
|
||||
"""
|
||||
|
||||
def __init__(self, channel):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
channel: A grpc.Channel.
|
||||
"""
|
||||
self.OpenSession = channel.unary_unary(
|
||||
'/mxaccess_gateway.v1.MxAccessGateway/OpenSession',
|
||||
request_serializer=mxaccess__gateway__pb2.OpenSessionRequest.SerializeToString,
|
||||
response_deserializer=mxaccess__gateway__pb2.OpenSessionReply.FromString,
|
||||
_registered_method=True)
|
||||
self.CloseSession = channel.unary_unary(
|
||||
'/mxaccess_gateway.v1.MxAccessGateway/CloseSession',
|
||||
request_serializer=mxaccess__gateway__pb2.CloseSessionRequest.SerializeToString,
|
||||
response_deserializer=mxaccess__gateway__pb2.CloseSessionReply.FromString,
|
||||
_registered_method=True)
|
||||
self.Invoke = channel.unary_unary(
|
||||
'/mxaccess_gateway.v1.MxAccessGateway/Invoke',
|
||||
request_serializer=mxaccess__gateway__pb2.MxCommandRequest.SerializeToString,
|
||||
response_deserializer=mxaccess__gateway__pb2.MxCommandReply.FromString,
|
||||
_registered_method=True)
|
||||
self.StreamEvents = channel.unary_stream(
|
||||
'/mxaccess_gateway.v1.MxAccessGateway/StreamEvents',
|
||||
request_serializer=mxaccess__gateway__pb2.StreamEventsRequest.SerializeToString,
|
||||
response_deserializer=mxaccess__gateway__pb2.MxEvent.FromString,
|
||||
_registered_method=True)
|
||||
self.AcknowledgeAlarm = channel.unary_unary(
|
||||
'/mxaccess_gateway.v1.MxAccessGateway/AcknowledgeAlarm',
|
||||
request_serializer=mxaccess__gateway__pb2.AcknowledgeAlarmRequest.SerializeToString,
|
||||
response_deserializer=mxaccess__gateway__pb2.AcknowledgeAlarmReply.FromString,
|
||||
_registered_method=True)
|
||||
self.StreamAlarms = channel.unary_stream(
|
||||
'/mxaccess_gateway.v1.MxAccessGateway/StreamAlarms',
|
||||
request_serializer=mxaccess__gateway__pb2.StreamAlarmsRequest.SerializeToString,
|
||||
response_deserializer=mxaccess__gateway__pb2.AlarmFeedMessage.FromString,
|
||||
_registered_method=True)
|
||||
self.QueryActiveAlarms = channel.unary_stream(
|
||||
'/mxaccess_gateway.v1.MxAccessGateway/QueryActiveAlarms',
|
||||
request_serializer=mxaccess__gateway__pb2.QueryActiveAlarmsRequest.SerializeToString,
|
||||
response_deserializer=mxaccess__gateway__pb2.ActiveAlarmSnapshot.FromString,
|
||||
_registered_method=True)
|
||||
|
||||
|
||||
class MxAccessGatewayServicer(object):
|
||||
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
|
||||
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
|
||||
(and `reserved` name) covering it in the same change so a future editor
|
||||
cannot accidentally reuse the retired tag.
|
||||
|
||||
Public client API for MXAccess sessions hosted by the gateway.
|
||||
"""
|
||||
|
||||
def OpenSession(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def CloseSession(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def Invoke(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def StreamEvents(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def AcknowledgeAlarm(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def StreamAlarms(self, request, context):
|
||||
"""Session-less central alarm feed. The stream opens with the current
|
||||
active-alarm snapshot (one `active_alarm` per alarm), then a single
|
||||
`snapshot_complete`, then a `transition` for every subsequent change.
|
||||
Served by the gateway's always-on alarm monitor; any number of clients
|
||||
fan out from the single monitor without opening a worker session.
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def QueryActiveAlarms(self, request, context):
|
||||
"""Point-in-time snapshot of the currently-active alarm set served from the
|
||||
gateway's always-on alarm monitor cache (session-less). Used after a
|
||||
reconnect to seed Part 9 client state, or to reconcile alarms that may
|
||||
have been missed during a transport blip. Streamed so callers can
|
||||
begin processing without buffering the full set.
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
|
||||
def add_MxAccessGatewayServicer_to_server(servicer, server):
|
||||
rpc_method_handlers = {
|
||||
'OpenSession': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.OpenSession,
|
||||
request_deserializer=mxaccess__gateway__pb2.OpenSessionRequest.FromString,
|
||||
response_serializer=mxaccess__gateway__pb2.OpenSessionReply.SerializeToString,
|
||||
),
|
||||
'CloseSession': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.CloseSession,
|
||||
request_deserializer=mxaccess__gateway__pb2.CloseSessionRequest.FromString,
|
||||
response_serializer=mxaccess__gateway__pb2.CloseSessionReply.SerializeToString,
|
||||
),
|
||||
'Invoke': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.Invoke,
|
||||
request_deserializer=mxaccess__gateway__pb2.MxCommandRequest.FromString,
|
||||
response_serializer=mxaccess__gateway__pb2.MxCommandReply.SerializeToString,
|
||||
),
|
||||
'StreamEvents': grpc.unary_stream_rpc_method_handler(
|
||||
servicer.StreamEvents,
|
||||
request_deserializer=mxaccess__gateway__pb2.StreamEventsRequest.FromString,
|
||||
response_serializer=mxaccess__gateway__pb2.MxEvent.SerializeToString,
|
||||
),
|
||||
'AcknowledgeAlarm': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.AcknowledgeAlarm,
|
||||
request_deserializer=mxaccess__gateway__pb2.AcknowledgeAlarmRequest.FromString,
|
||||
response_serializer=mxaccess__gateway__pb2.AcknowledgeAlarmReply.SerializeToString,
|
||||
),
|
||||
'StreamAlarms': grpc.unary_stream_rpc_method_handler(
|
||||
servicer.StreamAlarms,
|
||||
request_deserializer=mxaccess__gateway__pb2.StreamAlarmsRequest.FromString,
|
||||
response_serializer=mxaccess__gateway__pb2.AlarmFeedMessage.SerializeToString,
|
||||
),
|
||||
'QueryActiveAlarms': grpc.unary_stream_rpc_method_handler(
|
||||
servicer.QueryActiveAlarms,
|
||||
request_deserializer=mxaccess__gateway__pb2.QueryActiveAlarmsRequest.FromString,
|
||||
response_serializer=mxaccess__gateway__pb2.ActiveAlarmSnapshot.SerializeToString,
|
||||
),
|
||||
}
|
||||
generic_handler = grpc.method_handlers_generic_handler(
|
||||
'mxaccess_gateway.v1.MxAccessGateway', rpc_method_handlers)
|
||||
server.add_generic_rpc_handlers((generic_handler,))
|
||||
server.add_registered_method_handlers('mxaccess_gateway.v1.MxAccessGateway', rpc_method_handlers)
|
||||
|
||||
|
||||
# This class is part of an EXPERIMENTAL API.
|
||||
class MxAccessGateway(object):
|
||||
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
|
||||
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
|
||||
(and `reserved` name) covering it in the same change so a future editor
|
||||
cannot accidentally reuse the retired tag.
|
||||
|
||||
Public client API for MXAccess sessions hosted by the gateway.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def OpenSession(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/mxaccess_gateway.v1.MxAccessGateway/OpenSession',
|
||||
mxaccess__gateway__pb2.OpenSessionRequest.SerializeToString,
|
||||
mxaccess__gateway__pb2.OpenSessionReply.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def CloseSession(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/mxaccess_gateway.v1.MxAccessGateway/CloseSession',
|
||||
mxaccess__gateway__pb2.CloseSessionRequest.SerializeToString,
|
||||
mxaccess__gateway__pb2.CloseSessionReply.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def Invoke(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/mxaccess_gateway.v1.MxAccessGateway/Invoke',
|
||||
mxaccess__gateway__pb2.MxCommandRequest.SerializeToString,
|
||||
mxaccess__gateway__pb2.MxCommandReply.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def StreamEvents(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_stream(
|
||||
request,
|
||||
target,
|
||||
'/mxaccess_gateway.v1.MxAccessGateway/StreamEvents',
|
||||
mxaccess__gateway__pb2.StreamEventsRequest.SerializeToString,
|
||||
mxaccess__gateway__pb2.MxEvent.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def AcknowledgeAlarm(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/mxaccess_gateway.v1.MxAccessGateway/AcknowledgeAlarm',
|
||||
mxaccess__gateway__pb2.AcknowledgeAlarmRequest.SerializeToString,
|
||||
mxaccess__gateway__pb2.AcknowledgeAlarmReply.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def StreamAlarms(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_stream(
|
||||
request,
|
||||
target,
|
||||
'/mxaccess_gateway.v1.MxAccessGateway/StreamAlarms',
|
||||
mxaccess__gateway__pb2.StreamAlarmsRequest.SerializeToString,
|
||||
mxaccess__gateway__pb2.AlarmFeedMessage.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def QueryActiveAlarms(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_stream(
|
||||
request,
|
||||
target,
|
||||
'/mxaccess_gateway.v1.MxAccessGateway/QueryActiveAlarms',
|
||||
mxaccess__gateway__pb2.QueryActiveAlarmsRequest.SerializeToString,
|
||||
mxaccess__gateway__pb2.ActiveAlarmSnapshot.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# NO CHECKED-IN PROTOBUF GENCODE
|
||||
# source: mxaccess_worker.proto
|
||||
# Protobuf Python Version: 6.31.1
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import runtime_version as _runtime_version
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf.internal import builder as _builder
|
||||
_runtime_version.ValidateProtobufRuntimeVersion(
|
||||
_runtime_version.Domain.PUBLIC,
|
||||
6,
|
||||
31,
|
||||
1,
|
||||
'',
|
||||
'mxaccess_worker.proto'
|
||||
)
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2
|
||||
from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
|
||||
import mxaccess_gateway_pb2 as mxaccess__gateway__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15mxaccess_worker.proto\x12\x12mxaccess_worker.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x16mxaccess_gateway.proto\"\x95\x06\n\x0eWorkerEnvelope\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x12\n\nsession_id\x18\x02 \x01(\t\x12\x10\n\x08sequence\x18\x03 \x01(\x04\x12\x16\n\x0e\x63orrelation_id\x18\x04 \x01(\t\x12\x39\n\rgateway_hello\x18\n \x01(\x0b\x32 .mxaccess_worker.v1.GatewayHelloH\x00\x12\x37\n\x0cworker_hello\x18\x0b \x01(\x0b\x32\x1f.mxaccess_worker.v1.WorkerHelloH\x00\x12\x37\n\x0cworker_ready\x18\x0c \x01(\x0b\x32\x1f.mxaccess_worker.v1.WorkerReadyH\x00\x12;\n\x0eworker_command\x18\r \x01(\x0b\x32!.mxaccess_worker.v1.WorkerCommandH\x00\x12\x46\n\x14worker_command_reply\x18\x0e \x01(\x0b\x32&.mxaccess_worker.v1.WorkerCommandReplyH\x00\x12\x39\n\rworker_cancel\x18\x0f \x01(\x0b\x32 .mxaccess_worker.v1.WorkerCancelH\x00\x12=\n\x0fworker_shutdown\x18\x10 \x01(\x0b\x32\".mxaccess_worker.v1.WorkerShutdownH\x00\x12\x44\n\x13worker_shutdown_ack\x18\x11 \x01(\x0b\x32%.mxaccess_worker.v1.WorkerShutdownAckH\x00\x12\x37\n\x0cworker_event\x18\x12 \x01(\x0b\x32\x1f.mxaccess_worker.v1.WorkerEventH\x00\x12?\n\x10worker_heartbeat\x18\x13 \x01(\x0b\x32#.mxaccess_worker.v1.WorkerHeartbeatH\x00\x12\x37\n\x0cworker_fault\x18\x14 \x01(\x0b\x32\x1f.mxaccess_worker.v1.WorkerFaultH\x00\x42\x06\n\x04\x62ody\"Z\n\x0cGatewayHello\x12\"\n\x1asupported_protocol_version\x18\x01 \x01(\r\x12\r\n\x05nonce\x18\x02 \x01(\t\x12\x17\n\x0fgateway_version\x18\x03 \x01(\t\"i\n\x0bWorkerHello\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\r\n\x05nonce\x18\x02 \x01(\t\x12\x19\n\x11worker_process_id\x18\x03 \x01(\x05\x12\x16\n\x0eworker_version\x18\x04 \x01(\t\"\x8e\x01\n\x0bWorkerReady\x12\x19\n\x11worker_process_id\x18\x01 \x01(\x05\x12\x17\n\x0fmxaccess_progid\x18\x02 \x01(\t\x12\x16\n\x0emxaccess_clsid\x18\x03 \x01(\t\x12\x33\n\x0fready_timestamp\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"w\n\rWorkerCommand\x12/\n\x07\x63ommand\x18\x01 \x01(\x0b\x32\x1e.mxaccess_gateway.v1.MxCommand\x12\x35\n\x11\x65nqueue_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x81\x01\n\x12WorkerCommandReply\x12\x32\n\x05reply\x18\x01 \x01(\x0b\x32#.mxaccess_gateway.v1.MxCommandReply\x12\x37\n\x13\x63ompleted_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x1e\n\x0cWorkerCancel\x12\x0e\n\x06reason\x18\x01 \x01(\t\"Q\n\x0eWorkerShutdown\x12/\n\x0cgrace_period\x18\x01 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x0e\n\x06reason\x18\x02 \x01(\t\"H\n\x11WorkerShutdownAck\x12\x33\n\x06status\x18\x01 \x01(\x0b\x32#.mxaccess_gateway.v1.ProtocolStatus\":\n\x0bWorkerEvent\x12+\n\x05\x65vent\x18\x01 \x01(\x0b\x32\x1c.mxaccess_gateway.v1.MxEvent\"\xa5\x02\n\x0fWorkerHeartbeat\x12\x19\n\x11worker_process_id\x18\x01 \x01(\x05\x12.\n\x05state\x18\x02 \x01(\x0e\x32\x1f.mxaccess_worker.v1.WorkerState\x12?\n\x1blast_sta_activity_timestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x1d\n\x15pending_command_count\x18\x04 \x01(\r\x12\"\n\x1aoutbound_event_queue_depth\x18\x05 \x01(\r\x12\x1b\n\x13last_event_sequence\x18\x06 \x01(\x04\x12&\n\x1e\x63urrent_command_correlation_id\x18\x07 \x01(\t\"\xf4\x01\n\x0bWorkerFault\x12\x39\n\x08\x63\x61tegory\x18\x01 \x01(\x0e\x32\'.mxaccess_worker.v1.WorkerFaultCategory\x12\x16\n\x0e\x63ommand_method\x18\x02 \x01(\t\x12\x14\n\x07hresult\x18\x03 \x01(\x05H\x00\x88\x01\x01\x12\x16\n\x0e\x65xception_type\x18\x04 \x01(\t\x12\x1a\n\x12\x64iagnostic_message\x18\x05 \x01(\t\x12<\n\x0fprotocol_status\x18\x06 \x01(\x0b\x32#.mxaccess_gateway.v1.ProtocolStatusB\n\n\x08_hresult*\x97\x02\n\x0bWorkerState\x12\x1c\n\x18WORKER_STATE_UNSPECIFIED\x10\x00\x12\x19\n\x15WORKER_STATE_STARTING\x10\x01\x12\x1c\n\x18WORKER_STATE_HANDSHAKING\x10\x02\x12!\n\x1dWORKER_STATE_INITIALIZING_STA\x10\x03\x12\x16\n\x12WORKER_STATE_READY\x10\x04\x12\"\n\x1eWORKER_STATE_EXECUTING_COMMAND\x10\x05\x12\x1e\n\x1aWORKER_STATE_SHUTTING_DOWN\x10\x06\x12\x18\n\x14WORKER_STATE_STOPPED\x10\x07\x12\x18\n\x14WORKER_STATE_FAULTED\x10\x08*\xc7\x04\n\x13WorkerFaultCategory\x12%\n!WORKER_FAULT_CATEGORY_UNSPECIFIED\x10\x00\x12+\n\'WORKER_FAULT_CATEGORY_INVALID_ARGUMENTS\x10\x01\x12\x37\n3WORKER_FAULT_CATEGORY_GATEWAY_AUTHENTICATION_FAILED\x10\x02\x12+\n\'WORKER_FAULT_CATEGORY_PROTOCOL_MISMATCH\x10\x03\x12,\n(WORKER_FAULT_CATEGORY_PROTOCOL_VIOLATION\x10\x04\x12+\n\'WORKER_FAULT_CATEGORY_PIPE_DISCONNECTED\x10\x05\x12\x32\n.WORKER_FAULT_CATEGORY_MXACCESS_CREATION_FAILED\x10\x06\x12\x31\n-WORKER_FAULT_CATEGORY_MXACCESS_COMMAND_FAILED\x10\x07\x12:\n6WORKER_FAULT_CATEGORY_MXACCESS_EVENT_CONVERSION_FAILED\x10\x08\x12\"\n\x1eWORKER_FAULT_CATEGORY_STA_HUNG\x10\t\x12(\n$WORKER_FAULT_CATEGORY_QUEUE_OVERFLOW\x10\n\x12*\n&WORKER_FAULT_CATEGORY_SHUTDOWN_TIMEOUT\x10\x0b\x42&\xaa\x02#ZB.MOM.WW.MxGateway.Contracts.Protob\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'mxaccess_worker_pb2', _globals)
|
||||
if not _descriptor._USE_C_DESCRIPTORS:
|
||||
_globals['DESCRIPTOR']._loaded_options = None
|
||||
_globals['DESCRIPTOR']._serialized_options = b'\252\002#ZB.MOM.WW.MxGateway.Contracts.Proto'
|
||||
_globals['_WORKERSTATE']._serialized_start=2316
|
||||
_globals['_WORKERSTATE']._serialized_end=2595
|
||||
_globals['_WORKERFAULTCATEGORY']._serialized_start=2598
|
||||
_globals['_WORKERFAULTCATEGORY']._serialized_end=3181
|
||||
_globals['_WORKERENVELOPE']._serialized_start=135
|
||||
_globals['_WORKERENVELOPE']._serialized_end=924
|
||||
_globals['_GATEWAYHELLO']._serialized_start=926
|
||||
_globals['_GATEWAYHELLO']._serialized_end=1016
|
||||
_globals['_WORKERHELLO']._serialized_start=1018
|
||||
_globals['_WORKERHELLO']._serialized_end=1123
|
||||
_globals['_WORKERREADY']._serialized_start=1126
|
||||
_globals['_WORKERREADY']._serialized_end=1268
|
||||
_globals['_WORKERCOMMAND']._serialized_start=1270
|
||||
_globals['_WORKERCOMMAND']._serialized_end=1389
|
||||
_globals['_WORKERCOMMANDREPLY']._serialized_start=1392
|
||||
_globals['_WORKERCOMMANDREPLY']._serialized_end=1521
|
||||
_globals['_WORKERCANCEL']._serialized_start=1523
|
||||
_globals['_WORKERCANCEL']._serialized_end=1553
|
||||
_globals['_WORKERSHUTDOWN']._serialized_start=1555
|
||||
_globals['_WORKERSHUTDOWN']._serialized_end=1636
|
||||
_globals['_WORKERSHUTDOWNACK']._serialized_start=1638
|
||||
_globals['_WORKERSHUTDOWNACK']._serialized_end=1710
|
||||
_globals['_WORKEREVENT']._serialized_start=1712
|
||||
_globals['_WORKEREVENT']._serialized_end=1770
|
||||
_globals['_WORKERHEARTBEAT']._serialized_start=1773
|
||||
_globals['_WORKERHEARTBEAT']._serialized_end=2066
|
||||
_globals['_WORKERFAULT']._serialized_start=2069
|
||||
_globals['_WORKERFAULT']._serialized_end=2313
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
import warnings
|
||||
|
||||
|
||||
GRPC_GENERATED_VERSION = '1.80.0'
|
||||
GRPC_VERSION = grpc.__version__
|
||||
_version_not_supported = False
|
||||
|
||||
try:
|
||||
from grpc._utilities import first_version_is_lower
|
||||
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
|
||||
except ImportError:
|
||||
_version_not_supported = True
|
||||
|
||||
if _version_not_supported:
|
||||
raise RuntimeError(
|
||||
f'The grpc package installed is at version {GRPC_VERSION},'
|
||||
+ ' but the generated code in mxaccess_worker_pb2_grpc.py depends on'
|
||||
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
|
||||
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
|
||||
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
"""Client connection options for the async Python wrapper."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import grpc
|
||||
|
||||
from .auth import REDACTED, ApiKey
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ClientOptions:
|
||||
"""Connection settings for `GatewayClient.connect`."""
|
||||
|
||||
endpoint: str
|
||||
api_key: str | ApiKey | None = None
|
||||
plaintext: bool = False
|
||||
ca_file: str | None = None
|
||||
server_name_override: str | None = None
|
||||
call_timeout: float | None = 30.0
|
||||
stream_timeout: float | None = None
|
||||
max_grpc_message_bytes: int = 16 * 1024 * 1024
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Validate options; raise `ValueError` for invalid combinations."""
|
||||
if not self.endpoint:
|
||||
raise ValueError("endpoint must not be empty")
|
||||
|
||||
if self.plaintext and self.ca_file:
|
||||
raise ValueError("ca_file cannot be used with plaintext connections")
|
||||
if self.call_timeout is not None and self.call_timeout <= 0:
|
||||
raise ValueError("call_timeout must be greater than zero")
|
||||
if self.stream_timeout is not None and self.stream_timeout <= 0:
|
||||
raise ValueError("stream_timeout must be greater than zero")
|
||||
if self.max_grpc_message_bytes <= 0:
|
||||
raise ValueError("max_grpc_message_bytes must be greater than zero")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a repr that redacts the API key value."""
|
||||
api_key = REDACTED if self.api_key else None
|
||||
return (
|
||||
f"{type(self).__name__}(endpoint={self.endpoint!r}, "
|
||||
f"api_key={api_key!r}, plaintext={self.plaintext!r}, "
|
||||
f"ca_file={self.ca_file!r}, "
|
||||
f"server_name_override={self.server_name_override!r}, "
|
||||
f"call_timeout={self.call_timeout!r}, "
|
||||
f"stream_timeout={self.stream_timeout!r}, "
|
||||
f"max_grpc_message_bytes={self.max_grpc_message_bytes!r})"
|
||||
)
|
||||
|
||||
|
||||
def create_channel(options: ClientOptions) -> grpc.aio.Channel:
|
||||
"""Create a plaintext or TLS `grpc.aio` channel from client options."""
|
||||
|
||||
channel_options: list[tuple[str, str | int]] = [
|
||||
("grpc.max_receive_message_length", options.max_grpc_message_bytes),
|
||||
("grpc.max_send_message_length", options.max_grpc_message_bytes),
|
||||
]
|
||||
if options.server_name_override:
|
||||
channel_options.append(("grpc.ssl_target_name_override", options.server_name_override))
|
||||
|
||||
if options.plaintext:
|
||||
return grpc.aio.insecure_channel(options.endpoint, options=channel_options)
|
||||
|
||||
root_certificates = None
|
||||
if options.ca_file:
|
||||
root_certificates = Path(options.ca_file).read_bytes()
|
||||
|
||||
credentials = grpc.ssl_channel_credentials(root_certificates=root_certificates)
|
||||
return grpc.aio.secure_channel(
|
||||
options.endpoint,
|
||||
credentials,
|
||||
options=channel_options,
|
||||
)
|
||||
@@ -0,0 +1,404 @@
|
||||
"""Async session wrapper for MXAccess Gateway commands."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncIterator, Sequence
|
||||
|
||||
from .errors import ensure_mxaccess_success
|
||||
from .generated import mxaccess_gateway_pb2 as pb
|
||||
from .values import MxValueInput, to_mx_value
|
||||
|
||||
MAX_BULK_ITEMS = 1000
|
||||
|
||||
|
||||
class Session:
|
||||
"""A single gateway-backed MXAccess session."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
client: "GatewayClient",
|
||||
session_id: str,
|
||||
open_reply: pb.OpenSessionReply | None = None,
|
||||
) -> None:
|
||||
"""Initialize a session bound to a client and gateway session id."""
|
||||
self.client = client
|
||||
self.session_id = session_id
|
||||
self.open_reply = open_reply
|
||||
self._closed = False
|
||||
|
||||
async def __aenter__(self) -> "Session":
|
||||
"""Return self to support ``async with`` usage."""
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_exc_info: object) -> None:
|
||||
"""Close the session when leaving an ``async with`` block."""
|
||||
await self.close()
|
||||
|
||||
async def close(self, *, client_correlation_id: str = "") -> pb.CloseSessionReply:
|
||||
"""Close the gateway session. Repeated calls return a local closed reply."""
|
||||
|
||||
if self._closed:
|
||||
return pb.CloseSessionReply(
|
||||
session_id=self.session_id,
|
||||
final_state=pb.SESSION_STATE_CLOSED,
|
||||
protocol_status=pb.ProtocolStatus(code=pb.PROTOCOL_STATUS_CODE_OK),
|
||||
)
|
||||
|
||||
reply = await self.client.close_session_raw(
|
||||
pb.CloseSessionRequest(
|
||||
session_id=self.session_id,
|
||||
client_correlation_id=client_correlation_id,
|
||||
),
|
||||
)
|
||||
self._closed = True
|
||||
return reply
|
||||
|
||||
async def invoke(self, command: pb.MxCommand, *, correlation_id: str = "") -> pb.MxCommandReply:
|
||||
"""Invoke a raw command and enforce gateway and MXAccess success."""
|
||||
|
||||
reply = await self.invoke_raw(command, correlation_id=correlation_id)
|
||||
return ensure_mxaccess_success("invoke", reply)
|
||||
|
||||
async def invoke_raw(
|
||||
self,
|
||||
command: pb.MxCommand,
|
||||
*,
|
||||
correlation_id: str = "",
|
||||
) -> pb.MxCommandReply:
|
||||
"""Invoke a raw command and preserve the raw reply."""
|
||||
|
||||
return await self.client.invoke_raw(
|
||||
pb.MxCommandRequest(
|
||||
session_id=self.session_id,
|
||||
client_correlation_id=correlation_id,
|
||||
command=command,
|
||||
),
|
||||
)
|
||||
|
||||
async def register(self, client_name: str, *, correlation_id: str = "") -> int:
|
||||
"""Invoke MXAccess `Register` and return the new `ServerHandle`."""
|
||||
reply = await self.invoke(
|
||||
pb.MxCommand(
|
||||
kind=pb.MX_COMMAND_KIND_REGISTER,
|
||||
register=pb.RegisterCommand(client_name=client_name),
|
||||
),
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
return reply.register.server_handle
|
||||
|
||||
async def unregister(self, server_handle: int, *, correlation_id: str = "") -> None:
|
||||
"""Invoke MXAccess `Unregister` for a previously registered `ServerHandle`."""
|
||||
await self.invoke(
|
||||
pb.MxCommand(
|
||||
kind=pb.MX_COMMAND_KIND_UNREGISTER,
|
||||
unregister=pb.UnregisterCommand(server_handle=server_handle),
|
||||
),
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
|
||||
async def remove_item(
|
||||
self,
|
||||
server_handle: int,
|
||||
item_handle: int,
|
||||
*,
|
||||
correlation_id: str = "",
|
||||
) -> None:
|
||||
"""Invoke MXAccess `RemoveItem` for the given `ItemHandle`."""
|
||||
await self.invoke(
|
||||
pb.MxCommand(
|
||||
kind=pb.MX_COMMAND_KIND_REMOVE_ITEM,
|
||||
remove_item=pb.RemoveItemCommand(
|
||||
server_handle=server_handle,
|
||||
item_handle=item_handle,
|
||||
),
|
||||
),
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
|
||||
async def add_item(
|
||||
self,
|
||||
server_handle: int,
|
||||
item_definition: str,
|
||||
*,
|
||||
correlation_id: str = "",
|
||||
) -> int:
|
||||
"""Invoke MXAccess `AddItem` and return the new `ItemHandle`."""
|
||||
reply = await self.invoke(
|
||||
pb.MxCommand(
|
||||
kind=pb.MX_COMMAND_KIND_ADD_ITEM,
|
||||
add_item=pb.AddItemCommand(
|
||||
server_handle=server_handle,
|
||||
item_definition=item_definition,
|
||||
),
|
||||
),
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
return reply.add_item.item_handle
|
||||
|
||||
async def add_item2(
|
||||
self,
|
||||
server_handle: int,
|
||||
item_definition: str,
|
||||
item_context: str,
|
||||
*,
|
||||
correlation_id: str = "",
|
||||
) -> int:
|
||||
"""Invoke MXAccess `AddItem2` with item context and return the new `ItemHandle`."""
|
||||
reply = await self.invoke(
|
||||
pb.MxCommand(
|
||||
kind=pb.MX_COMMAND_KIND_ADD_ITEM2,
|
||||
add_item2=pb.AddItem2Command(
|
||||
server_handle=server_handle,
|
||||
item_definition=item_definition,
|
||||
item_context=item_context,
|
||||
),
|
||||
),
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
return reply.add_item2.item_handle
|
||||
|
||||
async def advise(
|
||||
self,
|
||||
server_handle: int,
|
||||
item_handle: int,
|
||||
*,
|
||||
correlation_id: str = "",
|
||||
) -> None:
|
||||
"""Invoke MXAccess `Advise` to subscribe an existing `ItemHandle` to events."""
|
||||
await self.invoke(
|
||||
pb.MxCommand(
|
||||
kind=pb.MX_COMMAND_KIND_ADVISE,
|
||||
advise=pb.AdviseCommand(
|
||||
server_handle=server_handle,
|
||||
item_handle=item_handle,
|
||||
),
|
||||
),
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
|
||||
async def unadvise(
|
||||
self,
|
||||
server_handle: int,
|
||||
item_handle: int,
|
||||
*,
|
||||
correlation_id: str = "",
|
||||
) -> None:
|
||||
"""Invoke MXAccess `UnAdvise` to stop event delivery for an `ItemHandle`."""
|
||||
await self.invoke(
|
||||
pb.MxCommand(
|
||||
kind=pb.MX_COMMAND_KIND_UN_ADVISE,
|
||||
un_advise=pb.UnAdviseCommand(
|
||||
server_handle=server_handle,
|
||||
item_handle=item_handle,
|
||||
),
|
||||
),
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
|
||||
async def add_item_bulk(
|
||||
self,
|
||||
server_handle: int,
|
||||
tag_addresses: Sequence[str],
|
||||
*,
|
||||
correlation_id: str = "",
|
||||
) -> list[pb.SubscribeResult]:
|
||||
"""Invoke MXAccess `AddItemBulk` and return one result per tag address."""
|
||||
if tag_addresses is None:
|
||||
raise TypeError("tag_addresses is required")
|
||||
_ensure_bulk_size("tag_addresses", len(tag_addresses))
|
||||
reply = await self.invoke(
|
||||
pb.MxCommand(
|
||||
kind=pb.MX_COMMAND_KIND_ADD_ITEM_BULK,
|
||||
add_item_bulk=pb.AddItemBulkCommand(
|
||||
server_handle=server_handle,
|
||||
tag_addresses=tag_addresses,
|
||||
),
|
||||
),
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
return list(reply.add_item_bulk.results)
|
||||
|
||||
async def advise_item_bulk(
|
||||
self,
|
||||
server_handle: int,
|
||||
item_handles: Sequence[int],
|
||||
*,
|
||||
correlation_id: str = "",
|
||||
) -> list[pb.SubscribeResult]:
|
||||
"""Invoke MXAccess `AdviseItemBulk` and return one result per item handle."""
|
||||
if item_handles is None:
|
||||
raise TypeError("item_handles is required")
|
||||
_ensure_bulk_size("item_handles", len(item_handles))
|
||||
reply = await self.invoke(
|
||||
pb.MxCommand(
|
||||
kind=pb.MX_COMMAND_KIND_ADVISE_ITEM_BULK,
|
||||
advise_item_bulk=pb.AdviseItemBulkCommand(
|
||||
server_handle=server_handle,
|
||||
item_handles=item_handles,
|
||||
),
|
||||
),
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
return list(reply.advise_item_bulk.results)
|
||||
|
||||
async def remove_item_bulk(
|
||||
self,
|
||||
server_handle: int,
|
||||
item_handles: Sequence[int],
|
||||
*,
|
||||
correlation_id: str = "",
|
||||
) -> list[pb.SubscribeResult]:
|
||||
"""Invoke MXAccess `RemoveItemBulk` and return one result per item handle."""
|
||||
if item_handles is None:
|
||||
raise TypeError("item_handles is required")
|
||||
_ensure_bulk_size("item_handles", len(item_handles))
|
||||
reply = await self.invoke(
|
||||
pb.MxCommand(
|
||||
kind=pb.MX_COMMAND_KIND_REMOVE_ITEM_BULK,
|
||||
remove_item_bulk=pb.RemoveItemBulkCommand(
|
||||
server_handle=server_handle,
|
||||
item_handles=item_handles,
|
||||
),
|
||||
),
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
return list(reply.remove_item_bulk.results)
|
||||
|
||||
async def unadvise_item_bulk(
|
||||
self,
|
||||
server_handle: int,
|
||||
item_handles: Sequence[int],
|
||||
*,
|
||||
correlation_id: str = "",
|
||||
) -> list[pb.SubscribeResult]:
|
||||
"""Invoke MXAccess `UnAdviseItemBulk` and return one result per item handle."""
|
||||
if item_handles is None:
|
||||
raise TypeError("item_handles is required")
|
||||
_ensure_bulk_size("item_handles", len(item_handles))
|
||||
reply = await self.invoke(
|
||||
pb.MxCommand(
|
||||
kind=pb.MX_COMMAND_KIND_UN_ADVISE_ITEM_BULK,
|
||||
un_advise_item_bulk=pb.UnAdviseItemBulkCommand(
|
||||
server_handle=server_handle,
|
||||
item_handles=item_handles,
|
||||
),
|
||||
),
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
return list(reply.un_advise_item_bulk.results)
|
||||
|
||||
async def subscribe_bulk(
|
||||
self,
|
||||
server_handle: int,
|
||||
tag_addresses: Sequence[str],
|
||||
*,
|
||||
correlation_id: str = "",
|
||||
) -> list[pb.SubscribeResult]:
|
||||
"""Invoke MXAccess `SubscribeBulk` and return one result per tag address."""
|
||||
if tag_addresses is None:
|
||||
raise TypeError("tag_addresses is required")
|
||||
_ensure_bulk_size("tag_addresses", len(tag_addresses))
|
||||
reply = await self.invoke(
|
||||
pb.MxCommand(
|
||||
kind=pb.MX_COMMAND_KIND_SUBSCRIBE_BULK,
|
||||
subscribe_bulk=pb.SubscribeBulkCommand(
|
||||
server_handle=server_handle,
|
||||
tag_addresses=tag_addresses,
|
||||
),
|
||||
),
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
return list(reply.subscribe_bulk.results)
|
||||
|
||||
async def unsubscribe_bulk(
|
||||
self,
|
||||
server_handle: int,
|
||||
item_handles: Sequence[int],
|
||||
*,
|
||||
correlation_id: str = "",
|
||||
) -> list[pb.SubscribeResult]:
|
||||
"""Invoke MXAccess `UnsubscribeBulk` and return one result per item handle."""
|
||||
if item_handles is None:
|
||||
raise TypeError("item_handles is required")
|
||||
_ensure_bulk_size("item_handles", len(item_handles))
|
||||
reply = await self.invoke(
|
||||
pb.MxCommand(
|
||||
kind=pb.MX_COMMAND_KIND_UNSUBSCRIBE_BULK,
|
||||
unsubscribe_bulk=pb.UnsubscribeBulkCommand(
|
||||
server_handle=server_handle,
|
||||
item_handles=item_handles,
|
||||
),
|
||||
),
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
return list(reply.unsubscribe_bulk.results)
|
||||
|
||||
async def write(
|
||||
self,
|
||||
server_handle: int,
|
||||
item_handle: int,
|
||||
value: MxValueInput,
|
||||
*,
|
||||
user_id: int = 0,
|
||||
correlation_id: str = "",
|
||||
) -> None:
|
||||
"""Invoke MXAccess `Write` for an `ItemHandle` with the converted value."""
|
||||
await self.invoke(
|
||||
pb.MxCommand(
|
||||
kind=pb.MX_COMMAND_KIND_WRITE,
|
||||
write=pb.WriteCommand(
|
||||
server_handle=server_handle,
|
||||
item_handle=item_handle,
|
||||
value=to_mx_value(value),
|
||||
user_id=user_id,
|
||||
),
|
||||
),
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
|
||||
async def write2(
|
||||
self,
|
||||
server_handle: int,
|
||||
item_handle: int,
|
||||
value: MxValueInput,
|
||||
timestamp_value: MxValueInput,
|
||||
*,
|
||||
user_id: int = 0,
|
||||
correlation_id: str = "",
|
||||
) -> None:
|
||||
"""Invoke MXAccess `Write2` with both a value and a client-supplied timestamp."""
|
||||
await self.invoke(
|
||||
pb.MxCommand(
|
||||
kind=pb.MX_COMMAND_KIND_WRITE2,
|
||||
write2=pb.Write2Command(
|
||||
server_handle=server_handle,
|
||||
item_handle=item_handle,
|
||||
value=to_mx_value(value),
|
||||
timestamp_value=to_mx_value(timestamp_value),
|
||||
user_id=user_id,
|
||||
),
|
||||
),
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
|
||||
def stream_events(
|
||||
self,
|
||||
*,
|
||||
after_worker_sequence: int = 0,
|
||||
) -> AsyncIterator[pb.MxEvent]:
|
||||
"""Return an async iterator of `MxEvent` messages for this session."""
|
||||
return self.client.stream_events_raw(
|
||||
pb.StreamEventsRequest(
|
||||
session_id=self.session_id,
|
||||
after_worker_sequence=after_worker_sequence,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _ensure_bulk_size(name: str, count: int) -> None:
|
||||
if count > MAX_BULK_ITEMS:
|
||||
raise ValueError(f"{name} bulk commands are limited to {MAX_BULK_ITEMS} item(s)")
|
||||
|
||||
|
||||
from .client import GatewayClient # noqa: E402
|
||||
@@ -0,0 +1,234 @@
|
||||
"""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)
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Package version information."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
Reference in New Issue
Block a user