diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index d2f7718..77a27e7 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.6.8, 3.7, 3.8, 3.9] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v2 diff --git a/acsys/__init__.py b/acsys/__init__.py index 9348c59..02d0159 100644 --- a/acsys/__init__.py +++ b/acsys/__init__.py @@ -1,829 +1,196 @@ -"""This module provides access to the ACSys Control System allowing -Python scripts to communicate with ACSys services and use ACSys -resources. Historically, all these resources were accessed using the -ACNET protocol. Many services are being updated to using standard -transports and protocols. The APIs that pass around an -`acsys.Connection` object are still using ACNET to transfer data. - -To use this library, your main function should be marked `async` and -take a single parameter which will be the ACSys Connection object. -Your function should get passed to `acsys.run_client()`. - -This library writes to the 'acsys' logger. Your script can configure -the logger as it sees fit. - -NOTE: Due to security concerns, you cannot access the control system -offsite unless you use Fermi's VPN. - -NOTE: When developing scripts, you may find it useful to put the async -scheduler in "debug mode". How to do it and what it does is described -here: - - https://docs.python.org/3/library/asyncio-dev.html#asyncio-debug-mode - - -EXAMPLE #1: Specifying your script's starting function. - -This simple example displays the ACSys handle that is assigned to the -script when it connects to ACSys. It shows how to register a starting -function and shows how it receives a Connection object you can use. - - import acsys - - async def main(con): - print(f'assigned handle: {con.handle}') - - acsys.run_client(main) - -Your function can create as many asynchronous tasks as it wants. -However, when the primary function returns, all other tasks will be -stopped and your script will continue execution after the -`acsys.run_client()` call. - -The Connection object provides a low-level API to ACSys. Most Python -libraries will take this object and wrap an API around it when -supporting a popular ACSys service (e.g. DPM, LOOKUP, etc.) - - -EXAMPLE #2: Using Connection's low-level API to do node/name - translations. - -This example shows how to translate node names to and from node -addresses using the ACSys service with which the script is associated. - - import acsys - - async def my_client(con): - # Look-up address of node CENTRA. - - name = 'CENTRA' - addr = await con.get_addr(name) - print(f'node {name} has address {addr}') - - # Do reverse look-up of CENTRA's address. - - name = await con.get_name(addr) - print(f'node {addr} has name {name}') - - acsys.run_client(my_client) - - -EXAMPLE #3: Making a request for a single reply. - -This snippet shows how a request is made to another ACSys task. - - import acsys - - async def my_client(con): - - # Send an ACSys "ping" message. This message is supported by - # the ACSys task on every node. - - snd, msg = await con.request_reply('ACNET@CENTRA', b'\\x00\\x00') - snd = await con.get_name(snd) - print(f'reply from {snd}: {msg}') - - acsys.run_client(my_client) - - -EXAMPLE #4: Making simultaneous requests - -This snippet looks up the addresses of three ACSys nodes -simultaneously. - - import asyncio - import acsys - - async def my_client(con): - results = await asyncio.gather( - con.get_addr('CENTRA'), - con.get_addr('CENTRY'), - con.get_addr('CLXSRV') - ) - - for ii in results: - print(ii) - - acsys.run_client(my_client) - -""" - -import asyncio -import logging -import array -import socket -import struct -import nest_asyncio -import acsys.status as status -from acsys.status import AcnetReplyTaskDisconnected - -# https://packaging.python.org/guides/single-sourcing-package-version/#single-sourcing-the-version -try: - from importlib import metadata -except ImportError: - # Running on pre-3.8 Python; use importlib-metadata package - import importlib_metadata as metadata - -__version__ = metadata.version('acsys') -__all__ = [ - '__version__', - 'Connection', -] - -nest_asyncio.apply() - -_log = logging.getLogger(__name__) - -# This map and the two following functions define a framework which -# decodes incoming ACK packets. - -_ackMap = { - 0: lambda buf: struct.unpack('>2xHh', buf), - 1: lambda buf: struct.unpack('>2xHhBI', buf), - 2: lambda buf: struct.unpack('>2xHhH', buf), - 4: lambda buf: struct.unpack('>2xHhBB', buf), - 5: lambda buf: struct.unpack('>2xHhI', buf), - 16: lambda buf: struct.unpack('>2xHhHI', buf) +from dataclasses import dataclass +from typing import Optional, Union, Iterable +from python_graphql_client import GraphqlClient + +class ACSysApiError(Exception): + def __init__(self, message: str): + super().__init__(message) + +# A GraphQL query to return a "one-shot" read. The list of device +# names can only represent device information -- not sample event or +# data logger parameters. + +_READ_DEVICES_ = """ +query ReadDevices ($devs: [String!]!) { + acceleratorData (deviceList: $devs) { + data { + timestamp + result { + ... on Scalar { + scalarValue + } + ... on ScalarArray { + scalarArrayValue + } + ... on Raw { + rawValue + } + } + } + } } +""" +@dataclass(frozen=True) +class Reading: + """Holds information related to a device reading. -def _throw_bug(_): - raise status.AcnetRequestTimeOutQueuedAtDestination() - - -def _decode_ack(buf): - return (_ackMap.get(buf[2] * 256 + buf[3], _throw_bug))(buf) - -# This class defines the communication protocol between the client and -# acnetd. - - -class _AcnetdProtocol(asyncio.Protocol): - def __init__(self): - super().__init__() - self.transport = None - self.buffer = bytearray() - self.q_cmd = asyncio.Queue(100) - self._rpy_map = {} - self._rpy_queue = [] - - def __del__(self): - self.end() - - def end(self): - if self.transport: - self.transport.close() - self.transport = None - - def add_handler(self, reqid, handler): - self._rpy_map[reqid] = handler - - def _get_packet(self, view): - if len(view) >= 4: - total = (view[0] << 24) + (view[1] << 16) + \ - (view[2] << 8) + view[3] - if len(view) >= total + 4: - return (view[4:(total + 4)], view[(total + 4):]) - return (None, view) - - def pop_reqid(self, reqid): - items = [] - rest = [] - for rpy in self._rpy_queue: - rpy_id, _, _, _, _ = rpy - if rpy_id == reqid: - items.append(rpy) - else: - rest.append(rpy) - self._rpy_queue = rest - return items - - def data_received(self, data): - - # Append to buffer and determine if enough data has arrived. - - self.buffer += data - - pkt, rest = self._get_packet(memoryview(self.buffer)) - - while pkt is not None: - pkt_type = pkt[0] * 256 + pkt[1] - - # Type 2 packets are ACKs for commands. There should - # always be an element in the queue when we receive an - # ACK. - - if pkt_type == 2: - self.q_cmd.get_nowait().set_result(bytes(pkt)) - - # Type 3 packets are ACSys reply traffic. - - elif pkt_type == 3: - - # Split out the interesting fields of the ACSys header. - - sts = pkt[5] * 256 + pkt[4] - if sts >= 0x8000: - sts -= 0x10000 - replier = pkt[6] * 256 + pkt[7] - reqid = pkt[17] * 256 + pkt[16] - last = (pkt[2] & 1) == 0 - sts = status.Status.create(sts) - - if sts != status.ACNET_PEND: - - # Check to see if there's a function associated - # with the request ID - - func = self._rpy_map.get(reqid) - if func is not None: - # If bit 0 is clear, this is the last reply so - # we remove the entry from the map. - - if last: - del self._rpy_map[reqid] - - # Send the 3-tuple, (sender, status, message) - # to the recipient. - - func((replier, sts, bytes(pkt[20:])), last) - else: - self._rpy_queue.append((reqid, replier, sts, - bytes(pkt[20:]), last)) - pkt, rest = self._get_packet(rest) - self.buffer = bytearray(rest) - - # Gets called when the transport successfully connects. We send - # out the RAW header to tell acnetd we're using the TCP socket in - # RAW mode (instead of WebSocket mode.) - - def connection_made(self, transport): - self.transport = transport - self.transport.write(b'RAW\r\n\r\n') - _log.debug('connected to ACSys') - - def connection_lost(self, exc): - self.end() - if exc is not None: - _log.warning('lost connection with ACSys') - - # Loop through all active requests and send a message - # indicating the request is done. - - msg = (0, AcnetReplyTaskDisconnected(), b'') - for _, func in self._rpy_map.items(): - func(msg, True) - self._rpy_map = {} - - # Send an error to all pending ACKs. The '\xde\x01' value is - # ACNET_DISCONNECTED. - - msg = b'\x00\x00\xde\x01' - while not self.q_cmd.empty(): - self.q_cmd.get_nowait().set_result(msg) - - def error_received(self): - _log.error('ACSys socket error', exc_info=True) - - async def xact(self, buf): - ack_fut = asyncio.get_event_loop().create_future() - await self.q_cmd.put(ack_fut) - if self.transport is not None: - self.transport.write(buf) - return _decode_ack(await ack_fut) - - raise AcnetReplyTaskDisconnected() - -# This class manages the connection between the client and acnetd. It -# defines the public API. - - -class Connection: - """Manages a connection to the ACSys control system. + A reading consists of a timestamp and value. `timestamp` is in UTC + time and is seconds (and fractional seconds) since the UNIX Epoch. + `value` is the device value when it was sampled. It can be of any + type that devices can return. -In addition to methods that make requests, this object has -methods that directly interact with the local ACSys service. """ - _char_index = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ$.%0123456789' - _rad50_chars = array.array( - 'B', bytes(_char_index, 'utf8')) - - def __init__(self): - """Constructor. - -Creates a disconnected instance of a Connection object. This -instance can't be properly used until further steps are -completed. SCRIPTS SHOULDN'T CREATE CONNECTIONS; they should -receive a properly created one indirectly through -`acsys.run_client()`. - - """ - self._raw_handle = 0 - self.handle = None - self.protocol = None - - def __del__(self): - if self.protocol is not None: - self.protocol.end() - - # Convert rad50 value to a string - - @staticmethod - def __rtoa(r50): - result = array.array('B', b' ') - chars = Connection._rad50_chars - - first_bit = r50 & 0xffff - second_bit = (r50 >> 16) & 0xffff - - for index in range(0, 3): - result[int(2 - index)] = chars[int(first_bit % 40)] - first_bit /= 40 - result[int(5 - index)] = chars[int(second_bit % 40)] - second_bit /= 40 - - return str.strip(result.tobytes().decode('ascii')) - - # Convert a string to rad50 value - - @staticmethod - def __ator(input_string): - def char_to_index(char): - try: - return Connection._char_index.index(char.upper()) - except ValueError: - return 0 - - first_bit = 0 - second_bit = 0 - s_len = len(input_string) - for index in range(0, 6): - char = input_string[index] if index < s_len else ' ' - - if index < (6 / 2): - first_bit *= 40 - first_bit += char_to_index(char) - else: - second_bit *= 40 - second_bit += char_to_index(char) - - return (second_bit << 16) | first_bit - - async def _xact(self, buf): - if self.protocol is not None: - while True: - try: - return await self.protocol.xact(buf) - except status.AcnetReplyTaskDisconnected: - if self.protocol is not None: - self.protocol = None - raise - else: - raise AcnetReplyTaskDisconnected() - - # Used to tell acnetd to cancel a specific request ID. This method - # doesn't return an error; if the request ID existed, it'll be - # gone and if it didn't, it's still gone. - - async def _cancel(self, reqid): - buf = struct.pack('>I2H2IH', 14, 1, 8, self._raw_handle, 0, reqid) - try: - await self._xact(buf) - except Exception: - pass - - # acnetd needs to know when a client is ready to receive replies - # to a request. This method informs acnetd which request has been - # prepared. - - async def _ack_request(self, reqid): - buf = struct.pack('>I2H2IH', 14, 1, 9, self._raw_handle, 0, reqid) - await self._xact(buf) - - # Finish initializing a Connection object. The construction can't - # block for the CONNECT command so we have to initialize in two - # steps. - - async def _connect(self, proto): - - # Send a CONNECT command requesting an anonymous handle and - # get the reply. Use 'proto' directly to call '.xact()' since - # 'self.protocol' hasn't been assigned yet. This prevents - # other clients from using the Connection until we register - # and get a handle. - - _log.debug('registering with ACSys') - buf = struct.pack('>I2H3IH', 18, 1, 1, self._raw_handle, 0, 0, 0) - res = await proto.xact(buf) - sts = status.Status.create(res[1]) - - # A good reply is a tuple with 4 elements. - - if sts.is_success and len(res) == 4: - self.protocol = proto - self._raw_handle = res[3] - self.handle = Connection.__rtoa(res[3]) - _log.info('connected to ACSys with handle %s', self.handle) - else: - raise sts - - @staticmethod - async def create(): - proto = await _create_socket() - if proto is not None: - con = Connection() - try: - await con._connect(proto) - return con - except Exception: - del con - raise - else: - _log.error('*** unable to connect to ACSys') - raise AcnetReplyTaskDisconnected() - - async def get_name(self, addr): - """Look-up node name. - -Returns the ACSys node name associated with the ACSys node -address, `addr`. - - """ - if isinstance(addr, int) and 0 <= addr <= 0x10000: - buf = struct.pack('>I2H2IH', 14, 1, 12, self._raw_handle, 0, addr) - res = await self._xact(buf) - sts = status.Status.create(res[1]) - - # A good reply is a tuple with 3 elements. - - if sts.is_success and len(res) == 3: - return Connection.__rtoa(res[2]) - - raise sts - - raise ValueError( - 'addr must be in the range of a 16-bit, signed integer') - - async def get_addr(self, name): - """Look-up node address. - -Returns the ACSys trunk/node node address associated with -the ACSys node name, `name`. - - """ - if isinstance(name, str) and len(name) <= 6: - buf = struct.pack('>I2H3I', 16, 1, 11, self._raw_handle, 0, - Connection.__ator(name)) - res = await self._xact(buf) - sts = status.Status.create(res[1]) - - # A good reply is a tuple with 4 elements. - - if sts.is_success and len(res) == 4: - return res[2] * 256 + res[3] - - raise sts - - raise ValueError( - 'name must be a string of no more than 6 characters') - - async def get_local_node(self): - """Return the node name associated with this connection. - -Python scripts and web applications gain access to the -control system through a pool of ACNET nodes. This method -returns which node of the pool is being used for the -connection. - - """ - buf = struct.pack('>I2H2I', 12, 1, 13, self._raw_handle, 0) - res = await self._xact(buf) - sts = status.Status.create(res[1]) - - # A good reply is a tuple with 4 elements. - - if sts.is_success and len(res) == 4: - addr = res[2] * 256 + res[3] - return await self.get_name(addr) - - raise sts - - async def _to_trunknode(self, node): - if isinstance(node, str): - return await self.get_addr(node) - if not isinstance(node, int): - raise ValueError('node should be an integer or string') - - return node - - async def _to_nodename(self, node): - if isinstance(node, int): - return await self.get_name(node) - if not isinstance(node, str): - raise ValueError('node should be an integer or string') - - return node - - async def make_canonical_taskname(self, taskname): - """Return an efficient form of taskname. - -This library uses the 'HANDLE@NODE' format to refer to -remote tasks. The internals of ACNET actually use trunk/node -addresses and an integer form of the handle name when -routing messages. This means the convenient form requires a -look-up call to the ACNET service to get the underlying -address of the node. - -If few requests are made, this overhead is negligible. If -frequent requests are made to the same task, however, the -overhead can be avoided by converting the convenient format -into this efficient format. - - """ - - if isinstance(taskname, str): - part = taskname.split('@', 1) - if len(part) == 2: - addr = await self.get_addr(part[1]) - return (Connection.__ator(part[0]), addr) + timestamp: float + value: float - raise ValueError('taskname has bad format') +# Class that interacts with the ACSys GraphQL API. - if isinstance(taskname, tuple) and len(taskname) == 2: - if isinstance(taskname[0], int): - if isinstance(taskname[1], int): - return taskname +class ACSys: + """Interact with Fermilab's ACSys GraphQL API - return (taskname[0], await self.get_addr(taskname[1])) + This object cn be used to access accelerator information including + device meta-information, readings, and historical data. If proper + credentials are provided, it also allows control of devices. - handle = Connection.__ator(taskname[0]) - if isinstance(taskname[1], int): - return (handle, taskname[1]) - - return (handle, await self.get_addr(taskname[1])) - - raise ValueError('invalid taskname') - - async def _mk_req(self, remtsk, message, mult, proto, timeout): - # If a protocol module name was provided, verify the message - # object has a '.marshal()' method. If it does, use it to - # create a bytearray. - - if proto is not None: - if hasattr(message, 'marshal'): - message = bytearray(message.marshal()) - else: - raise ValueError( - 'message wasn''t created by the protocol compiler') - - # Make sure the message is some sort of binary and the timeout - # is an integer. - - if isinstance(message, (bytes, bytearray)) \ - and isinstance(timeout, int): - task, node = await self.make_canonical_taskname(remtsk) - buf = struct.pack('>I2H3I2HI', 24 + len(message), 1, 18, - self._raw_handle, 0, task, node, mult, - timeout) + message - res = await self._xact(buf) - sts = status.Status.create(res[1]) - - # A good reply is a tuple with 3 elements. The last - # element will be the request ID, which is what we return - # to the caller. - - if sts.is_success and len(res) == 3: - return res[2] - - raise sts - - raise ValueError('message must be a binary') - - async def request_reply( - self, - remtsk, - message, - *, - proto=None, - timeout=1000 - ): - """Requests a single reply from an ACSys task. - -This function sends a request to an ACSys task and returns a -future which will be resolved with the reply. The reply is a -2-tuple where the first element is the trunk/node address of -the sender and the second is the reply data. - -The ACSys status will always be good (i.e. success or -warning); receiving a fatal status results in the future -throwing an exception. - -'remtsk' is a string representing the remote ACSys task in -the format "TASKNAME@NODENAME". - -'message' is either a bytes type, or a type that's an -acceptable value for a protocol (specified by the 'proto' -parameter.) - -'proto' is an optional, named parameter. If omitted, the -message must be a bytes type. If specified, it should be the -name of a module generated by the Protocol Compiler. + """ -'timeout' is an optional field which sets the timeout for -the request. If the reply doesn't arrive in time, an -ACNET_UTIME status will be raised. + def __init__(self, jwt: Optional[str] = None): + """Creates an instance of `ACSys`. -If the message is in an incorrect format or the timeout -parameter isn't an integer, ValueError is raised. + Arg: + jwt (str): A Javascript Web Token (JWT). How this token is + obtained is beyond the scope of this package. The JWT + holds authorization infomation for a user. """ - def process_reply(reply): - assert isinstance(reply, tuple) and len(reply) == 3 - - replier, sts, data = reply - if not sts.is_fatal: - if (proto is not None) and len(data) > 0: - data = proto.unmarshal_reply(iter(data)) - return (replier, data) - - raise sts - - reqid = await self._mk_req(remtsk, message, 0, proto, timeout) - # Save the handler in the map and return the future. BTW, we - # don't have to test for the validity of 'self.protocol' here - # because, to reach this point, the previous call to - # `._mk_req` didn't throw an exception (which it would have if - # `self.protocol` was None. + # If the caller has a JWT, use it for all the requests. - replies = self.protocol.pop_reqid(reqid) - if len(replies) == 0: - - # Create a future which will eventually resolve to the - # reply. - - loop = asyncio.get_event_loop() - rpy_fut = loop.create_future() - - # Define a function we can use to stuff the future with - # the reply. If the status is fatal, this function will - # resolve the future with an exception. Otherwise the - # reply message is set as the result. - - def reply_handler(reply, _): - try: - rpy_fut.set_result(process_reply(reply)) - except Exception as exception: - rpy_fut.set_exception(exception) + if jwt is None or (not isinstance(jwt, str)): + headers = {} + else: + headers = { 'Authorization': f"Bearer {jwt}" } - self.protocol.add_handler(reqid, reply_handler) - return await rpy_fut + # Create the two connections to the GraphQL service. - _, replier, sts, msg, _ = replies[0] - return process_reply((replier, sts, msg)) + self._query = GraphqlClient( + endpoint="https://acsys-proxy.fnal.gov:8001/acsys", + headers=headers + ) + self._subscription = GraphqlClient( + endpoint="https://acsys-proxy.fnal.gov:8001/acsys/s", + headers=headers + ) - async def request_stream( - self, - remtsk, - message, - *, - proto=None, - timeout=1000 - ): - """Requests a stream of replies from an ACSys task. + # Private method to convert an item of a reading reply. It knows + # all the possible types that can be returned an converts them + # into native Python types. + + def _convertItem(item): + timestamp = item['timestamp'] + result = item['result'] + + if 'scalarValue' in result: + value = result['scalarValue'] + elif 'scalarArrayValue' in result: + value = result['scalarArrayValue'] + elif 'rawValue' in result: + value = bytearray(result['rawValue']) + elif 'textValue' in result: + value = result['textValue'] + elif 'textArrayValue' in result: + value = result['textArrayValue'] + else: + value = None -This function sends a request to an ACSys task and returns -an async generator which returns the stream of replies. Each -reply is a 2-tuple where the first element is the trunk/node -address of the sender and the second is the reply data. + return Reading(timestamp = timestamp, value = value) -The ACSys status in each reply will always be good (i.e. -success or warning); receiving a fatal status results in the -generator throwing an exception. + # Private method to convert the entire reading reply. This is a + # generator function. -'remtsk' is a string representing the remote ACSys task in -the format "TASKNAME@NODENAME". + def _convertReply(reply): + if 'acceleratorData' in reply['data']: + for item in reply['data']['acceleratorData']: + data = item['data'] -'message' is either a bytes type, or a type that's an -acceptable value for a protocol (specified by the 'proto' -parameter.) + if len(data) == 1: + yield ACSys._convertItem(data[0]) + elif len(data) > 1: + yield [ACSys._convertItem(point) for point in data] + else: + yield None + else: + raise ACSysApiError(message=reply['error']) -'proto' is an optional, named parameter. If omitted, the -message must be a bytes type. If specified, it should be the -name of a module generated by the Protocol Compiler. + # Method that does a "one-shot" on a set of devices. -'timeout' is an optional field which sets the timeout -between each reply. If any reply doesn't arrive in time, an -ACNET_UTIME status will be raised. + def readDevices(self, devices: Union[str, Iterable[str]]) -> Union[Reading, tuple[Reading, ...]]: + """Return the current reading for one or more devices. -If the message is in an incorrect format or the timeout -parameter isn't an integer, ValueError is raised. + This function returns the latest reading for the specified + devices. `devices` can be a list, a tuple, or an iterator of + strings. If `devices` is a string, it specifies a single + device to read. - """ - try: - reqid = await self._mk_req(remtsk, message, 1, proto, timeout) - rpy_q = asyncio.Queue() + Each element of `devices` is a "device specification" as + defined in the DRF spec. Only the device portion of DRF is + used -- no event or data logger specification is allowed. This + means you can use the array notation for array devices, you + can specify different properties (used by ACNET devices), use + PV names (for EPICS devices), and use field names. - def handler(rpy, last): - replier, sts, msg = rpy - rpy_q.put_nowait((replier, sts, msg, last)) + Examples: - # Pre-stuff the queue with replies that may already have - # arrived. BTW, we don't have to test for the validity of - # 'self.protocol' here because, to reach this point, the - # previous call to `._mk_req` didn't throw an exception - # (which it would have if `self.protocol` was None. + acsys = ACSys() - for _, snd, sts, pkt, last in self.protocol.pop_reqid(reqid): - handler((snd, sts, pkt), last) + # Read outdoor temperature - # Save the handler in the map. + temp = acsys.readDevices("M:OUTTMP") + print(f"{temp.timestamp} : {temp.value} F") - self.protocol.add_handler(reqid, handler) - await self._ack_request(reqid) + # Read all elements of Z:CUBE - # This section implements the async generator. + cube = acsys.readDevices("Z:CUBE[]") + print(f"{cube.timestamp} : {cube.value}") - done = False - while not done: - snd, sts, msg, done = await rpy_q.get() - if not sts.is_fatal: - if (proto is not None) and len(msg) > 0: - msg = proto.unmarshal_reply(iter(msg)) - yield (snd, msg) - else: - raise sts - finally: - # If this generator exits for any reason, cancel the - # associated request. + # Read both with one request (much more efficient than + # making separate requests!) - if not done: - _log.debug('canceling request %d', reqid) - loop = asyncio.get_event_loop() - loop.run_until_complete(self._cancel(reqid)) + (temp, cube) = acsys.readDevices(("M:OUTTMP", "Z:CUBE[]")) - async def ping(self, node): - """Pings an ACSys node. + Arg: + devices: A list of strings or a single string, each + representing a device specification. -Uses the Level2 protocol to perform an ACSys ping request. -Returns True if the node responded or False if it didn't. A -node is given 1/4 second to respond. If the Connection has -problems, this method will raise an ACSys Status code. + Returns: + tuple: A tuple containing the readings. The size of the + tuple will match the number of device specifications. + If there is only one device, it returns the reading + instead of 1-tuple. """ - node = await self._to_nodename(node) - try: - await self.request_reply(f'ACNET@{node}', b'\x00\x00', timeout=250) - return True - except status.AcnetRequestTimeOutQueuedAtDestination: - return False - - -async def _create_socket(): - try: - acsys_socket = socket.create_connection(('acsys-proxy.fnal.gov', 6802), 0.25) - except socket.timeout: - _log.warning('timeout connecting to ACSys') - return None - else: - loop = asyncio.get_event_loop() - _log.debug('creating ACSys transport') - _, proto = await loop.create_connection(_AcnetdProtocol, sock=acsys_socket) - return proto - -async def __client_main(main, **kwargs): - con = await Connection.create() - try: - await main(con, **kwargs) - finally: - del con + # If the parameter is a string, we need to wrap it in a + # list. Strings are iterable so, if we don't do this, we end + # up with an iterator yielding device names consisting of + # single characters. This is not what the user wants. + if isinstance(devices, str): + devices = [devices] -def run_client(main, **kwargs): - """Starts an asynchronous session for ACSys clients. + # Perform the query and process the results. -This function starts up an ACSys session. The parameter, -`main`, is an async function with the signature: - - async def main(con, **kwargs): - -This function will be passed `con` -- a fully initialized -`Connection` object. It will also get passed `kwargs`. + reply = self._query.execute( + query=_READ_DEVICES_, + variables={ "devs": list(devices) } + ) + result = tuple(ACSys._convertReply(reply)) -When 'main()' resolves, `run_client()` will return the value -returned by `main()`. + # Don't return a 1-tuple. - """ - loop = asyncio.get_event_loop() - client_fut = asyncio.Task(__client_main(main, **kwargs)) - try: - loop.run_until_complete(client_fut) - except Exception: - client_fut.cancel() - try: - loop.run_until_complete(client_fut) - except Exception: - pass - raise + if len(result) == 1: + return result[0] + else: + return result diff --git a/acsys/daq_lib/__init__.py b/acsys/daq_lib/__init__.py deleted file mode 100644 index ed88f7f..0000000 --- a/acsys/daq_lib/__init__.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python3 - - -""""daq_lib is a module that provides a high-level interface to ACSys device - data acquisition. It is designed to be used both functionally and object - oriented with the DeviceDataAcquisition class. The DeviceDataAcquisition - class is a wrapper around the ACSys device data acquisition library.""" - -from datetime import date -from typing import ( - AsyncIterable, - AsyncIterator, - Callable, - Iterable, - Iterator, - List, - Optional, - Union -) - -from grpc import Channel - - -class DeviceReading: - """A reading from a device""" - time: int - value: float - - -class Device(Iterable, AsyncIterable): - """Device class doc""" - - def __init__( - self, - data_source_channel: Channel, - request_string: str, - roles: Optional[List[str]] = None - ) -> Iterator[DeviceReading]: - self.channel = data_source_channel - self.request_string = request_string - self.roles = roles if roles is not None else [] - - raise NotImplementedError - - def __iter__(self) -> Iterator[DeviceReading]: - """Provide data in an iterator.""" - raise NotImplementedError - - def __next__(self) -> Iterator[DeviceReading]: - """Provide data in an iterator.""" - raise NotImplementedError - - def __aiter__(self) -> AsyncIterator[DeviceReading]: - """Provide data in an iterator.""" - raise NotImplementedError - - def __anext__(self) -> AsyncIterator[DeviceReading]: - """Provide data in an iterator.""" - raise NotImplementedError - - def read(self) -> DeviceReading: - """Get reading of a device.""" - raise NotImplementedError - - def set(self, value: float) -> None: - """Set the value of a device.""" - raise NotImplementedError - - def handle_readings(self, callback: Callable[[DeviceReading], None]) -> None: - """Handle readings of a device.""" - raise NotImplementedError - - -class DeviceDataAcquisition: - """DeviceDataAcquisition class doc""" - - def __init__(self, roles: Optional[List[str]] = None) -> None: - self.roles = roles if roles is not None else [] - - def read( - self, - request_string: str, - start: Optional[date] = None, - end: Optional[date] = None - ) -> DeviceReading: - """Get reading of a device.""" - raise NotImplementedError - - def set( - self, - device_name: str, - value: float, - roles: Optional[List[str]] = None - ) -> None: - """Set the value of a device.""" - raise NotImplementedError - - def start( - self, - request_string: str, - start: Optional[date] = None, - end: Optional[date] = None - ) -> Iterator[DeviceReading]: - """Start reading a device.""" - raise NotImplementedError - - -class DeviceError(Exception): - """DeviceError class doc""" - ... - - -def enter_acquisition(entry_fn: Callable[..., None]) -> None: - """Enter acquisition mode""" - raise NotImplementedError - - -def handle_readings( - data_source_channel: Channel, - request_string: str, - callback: Callable[[DeviceReading], None] -) -> None: - """Enter acquisition mode""" - raise NotImplementedError - - -def apply_settings( - # There's not good support for zip typing here. I think Iterator works. - device_list: Union[List[Device], Iterator[Device]], - roles: Optional[List[str]] = None -) -> None: - """Apply settings to devices""" - raise NotImplementedError diff --git a/acsys/defaults/config.py b/acsys/defaults/config.py deleted file mode 100644 index 4d0b57c..0000000 --- a/acsys/defaults/config.py +++ /dev/null @@ -1,223 +0,0 @@ -from dataclasses import dataclass -from dataclasses import field -from dataclasses import InitVar - - -@dataclass -class Defaults: - """ Defaults is n class that when instantiated, contains defaults for - external parameters such as the hostname for the public proxy to - connect to. Parameters can be accessed and modified as properties - of the class. - - Additional config items can be passed via a keyword argument to the - constructor (init). Example: - - config = Defaults(additional_config={'foo': 'bar'}) - - NB: We need to override the "setter" for the environment data field. - This is because we want to set defaults for all the other data - fields when the environment changes. There is no easy way to - override a Dataclass "setter". We need to define a property, but - that isn't an intuititive process. See: - - https://florimond.dev/en/posts/2018/10/reconciling-dataclasses-and-properties-in-python/ - - for an explanation of the technique used in this class. - """ - _environment: str = field(init=False, repr=False) - proxy_hostname: str = field(default="") - proxy_port: int = field(default=0) - request_timeout_ms: int = field(default=0) - async_timeout_ms: int = field(default=0) - additional_config: dict = field(default_factory=dict) - default_environment: InitVar[str] = "production" - - """ An enumeration of default configuration items. - The top level key is the environment. Switching environments will - overwrite all config values with the defaults for the selected - environment. Items are exposed as properties and can be individually - overridden using the property syntax. - - example: - config = Defaults() - # read a property: - print(config.environment) - - # override a single value - config.hostname = "foo" - - # switch environments (reset all values to default for environment) - config.environment = "test" - """ - default_enumeration = { - "production": { - "proxy_hostname": "prod.fnal.gov", - "proxy_port": 80, - "request_timeout_ms": 2000, - "async_timeout_ms": 1000 - }, - "test": { - "proxy_hostname": "test.fnal.gov", - "proxy_port": 80, - "request_timeout_ms": 2000, - "async_timeout_ms": 1000 - } - } - - """ A list of supported output types to return the contents of the current - configuration state - """ - dump_formats = ['json'] - - def __post_init__(self, default_environment, additional_config={}): - """ Initializes the configuration object with defaults - - If environment is not specified in the init arguments, the environment - will default to "production". - """ - self.hostname = "" - self.port = 0 - self.request_timeout = 0 - self.async_timeout = 0 - self.environment = default_environment - - [setattr(self, k, v) for k, v in self.additional_config.items()] - - def dump(self, format="json"): - """ Outputs the current state of the configuration in the format specified - The default format is JSON. The method will check for valid format types. - NB: Currently the ONLY supported output format is JSON - """ - output = "" - output_format = format.lower() - - if output_format not in Defaults.dump_formats: - raise ValueError(f"Argument value for format, '{format}', is not a valid value") - - if output_format == "json": - output = f"{{ '{self.environment}': {{ 'proxy_hostname': '{self.hostname}', 'proxy_port': {self.port}, " \ - f"'request_timeout_ms': {self.request_timeout}, 'async_timeout_ms': {self.async_timeout} }} }}" - - return output - - @property - def environment(self): - return self._environment - - @environment.setter - def environment(self, environment): - """ Sets the environment property. - Switching environments will overwrite all config values with the - defaults for the selected environment. Items are exposed as properties - and can be individually overridden using the property syntax. - """ - try: - default_env = Defaults.default_enumeration[environment] - self._environment = environment - self.hostname = default_env["proxy_hostname"] - self.port = default_env["proxy_port"] - self.request_timeout = default_env["request_timeout_ms"] - self.async_timeout = default_env["async_timeout_ms"] - except KeyError as ke: - raise ValueError(f"Environment '{environment}' is not a valid environment") from ke - - -def test_read_property(): - config = Defaults() - assert config.hostname == "prod.fnal.gov", "test_read_property: Incorrect hostname" - assert config.port == 80, "test_read_property: Incorrect port" - assert config.request_timeout == 2000, "test_read_property: Incorrect request_timeout" - assert config.async_timeout == 1000, "test_read_property: async_timeout hostname" - - -def test_set_property(): - """ Test that all properties are set correctly - (except the environment property, this is a special case) - """ - config = Defaults() - - # Test hostname - assert config.hostname == "prod.fnal.gov", "test_set_property: Incorrect hostname (initial value)" - config.hostname = "whatever.fnal.gov" - assert config.hostname == "whatever.fnal.gov", "test_set_property: Incorrect hostname (post value)" - - # Test port - assert config.port == 80, "test_set_property: Incorrect port (initial value)" - config.port = 81 - assert config.port == 81, "test_set_property: Incorrect port (post value)" - - # Test request_timeout - assert config.request_timeout == 2000, "test_set_property: Incorrect request_timeout (initial value)" - config.request_timeout = 2001 - assert config.request_timeout == 2001, "test_set_property: Incorrect request_timeout (post value)" - - # Test async_timeout - assert config.async_timeout == 1000, "test_set_property: Incorrect async_timeout (initial value)" - config.async_timeout = 1001 - assert config.async_timeout == 1001, "test_set_property: Incorrect async_timeout (post value)" - - -def test_dump_valid_format(): - """Do we want to be more pedantic with this test? - e.g., do we want to validate the syntax of the format selected? - """ - config = Defaults() - expected_output = "{ 'production': { 'proxy_hostname': 'prod.fnal.gov', 'proxy_port': 80, 'request_timeout_ms': 2000," \ - " 'async_timeout_ms': 1000 } }" - assert config.dump(format="json") == expected_output, "test_dump_valid_format: JSON output did not match expected output" - - -def test_dump_invalid_format(): - """ Test that an exception is thrown when an invalid format is specified """ - config = Defaults() - try: - config.dump(format="garbage") - assert False, "test_dump_invalid_format: did not throw an exception as expected" - except ValueError: - assert True - - -def test_set_valid_environment(): - config = Defaults() - config.environment = "test" - - assert config.environment == "test", "test_set_valid_environment: Incorrect environment" - assert config.hostname == "test.fnal.gov", "test_set_valid_environment: Incorrect hostname" - assert config.port == 80, "test_set_valid_environment: Incorrect port" - assert config.request_timeout == 2000, "test_set_valid_environment: Incorrect request_timeout" - assert config.async_timeout == 1000, "test_set_valid_environment: Incorrect async_timeout" - - -def test_set_invalid_environment(): - config = Defaults() - try: - config.environment = "garbage" - assert False, "test_set_invalid_environment: did not throw an exception as expected" - except ValueError: - assert True - - -def test_set_additional_config(): - config = Defaults(additional_config={'foo': 'bar'}) - assert (config.foo == 'bar'), "test_set_additional_config: Additional config 'baz' did not equal 'bar'" - - -def test_set_initial_environment(): - config = Defaults(default_environment="test") - assert config.environment == "test", "test_set_valid_environment: Incorrect environment" - assert config.hostname == "test.fnal.gov", "test_set_valid_environment: Incorrect hostname" - assert config.port == 80, "test_set_valid_environment: Incorrect port" - assert config.request_timeout == 2000, "test_set_valid_environment: Incorrect request_timeout" - assert config.async_timeout == 1000, "test_set_valid_environment: Incorrect async_timeout" - - -if __name__ == "__main__": - # Run tests - test_read_property() - test_set_property() - test_dump_valid_format() - test_dump_invalid_format() - test_set_valid_environment() - test_set_invalid_environment() - test_set_additional_config() diff --git a/acsys/dpm/__init__.py b/acsys/dpm/__init__.py deleted file mode 100644 index 886f518..0000000 --- a/acsys/dpm/__init__.py +++ /dev/null @@ -1,853 +0,0 @@ -import asyncio -import importlib -import logging -import getpass -import os -import ssl -import sys -from datetime import (timezone, datetime, timedelta) -import acsys.status -from acsys.dpm.dpm_protocol import (ServiceDiscovery_request, - AddToList_request, - RemoveFromList_request, - StartList_request, - StopList_request, - OpenList_reply, - ClearList_request, - RawSetting_struct, - TextSetting_struct, - ScaledSetting_struct, - ApplySettings_request, - Status_reply, AnalogAlarm_reply, - BasicStatus_reply, - DigitalAlarm_reply, - DeviceInfo_reply, Raw_reply, - ScalarArray_reply, Scalar_reply, - TextArray_reply, Text_reply, - ListStatus_reply, - ApplySettings_reply, - Authenticate_request, - EnableSettings_request, - TimedScalarArray_reply, - Authenticate_reply, - unmarshal_reply) - -_log = logging.getLogger(__name__) - - -class ItemData: - """An object that holds a reading from a device. - -DPM delivers device data using a stream of ItemData objects. - -The 'tag' field corresponds to the tag parameter specified when -the '.add_entry()' method added the device to the list. - -'cycle' holds the cycle number. This field can be used to -correlate readings from different devices. - -'status' usually contains a success status. Some devices, -however, could add a warning status to indicate more information -to the caller. Some devices may be disabled and would set this -status field to a value indicating the data is stale, for -instance. - -'meta' is a dictionary containing device information. - -'data' is an array of timestamp/data pairs. - - """ - - def __init__(self, tag, cycle, stamp, status, data, micros=None, meta=None): - self._tag = tag - self._cycle = cycle - self._status = status - self._meta = (meta or {}) - base_time = datetime(1970, 1, 1, tzinfo=timezone.utc) - if micros is None: - self._data = [(base_time + timedelta(milliseconds=stamp), data)] - else: - self._data = list(zip([base_time + timedelta(microseconds=stamp) for stamp in micros], data)) - - @property - def tag(self): - """Corresponds to the ref ID that was associated with the DRF -request string. - """ - return self._tag - - @property - def data(self): - """An array of timestamp/value pairs. The value will be of the type -asked in the corresponding DRF2 (specified in the call to the -'.add_entry()' method.) For instance, if .RAW was specified, the -value field will contain a bytes(). Otherwise it will contain a -scaled, floating point value (or an array, if it's an array -device), or a dictionary -- in the case of basic status or alarm -blocks. - - """ - return self._data - - @property - def status(self): - return self._status - - @property - def cycle(self): - return self._cycle - - @property - def stamp(self): - return self._data[0][0] - - @property - def value(self): - return self._data[0][1] - - @property - def meta(self): - """Contains a dictionary of extra information about the device. The -'name' key holds the device name. 'di' contains the device index. -If the device has scaling, a 'units' key will be present and hold -the engineering units of the reading. - - """ - return self._meta - - def __str__(self): - return f'{{ tag: {self.tag}, data: {self.data}, meta: {self.meta} }}' - - def is_reading_for(self, *tags): - """Returns True if this ItemData instance is associated with any of -the tag values in the parameter list. - """ - return self.tag in tags - - def is_status_for(self, *_tags): - """Returns False.""" - return False - - -class ItemStatus: - """An object reporting status of an item in a DPM list. - -If there was an error in a request, this object will be in the -stream instead of a ItemData object. The 'tag' field corresponds -to the tag parameter used in the call to the '.add_entry()' -method. - -If this message appears as a result of a reading request, there -will never be an ItemData object for the 'tag' until the error -condition is fixed and the list restarted. - -There will always be one of these objects generated to indicate -the result of a setting. - """ - - def __init__(self, tag, status): - self._tag = tag - self._status = acsys.status.Status.create(status) - - @property - def tag(self): - """Corresponds to the ref ID that was associated with the DRF -request string. - """ - return self._tag - - @property - def status(self): - """Describes the error that occurred with this item.""" - return self._status - - def __str__(self): - return f'{{ tag: {self.tag}, status: {self.status} }}' - - def is_reading_for(self, *_tags): - """Returns False.""" - return False - - def is_status_for(self, *tags): - """Returns True if this ItemStatus instance is associated with any -of the tag values in the parameter list. - - """ - return self.tag in tags - - -class DPM(asyncio.Protocol): - """An object that manages a connection to a remote Data Pool Manager -(DPM). Instances of this class should be obtained by a -'DPMContext' object used in an 'async-with' statement. - -Creating an instance results in a dormant object. To activate it, -it needs to be passed as the 'protocol' argument to -'asyncio.create_connection()'. - -'DPMContext's in an `async-statement' perform this initialization -for you as well as clean-up properly. - - """ - - # Constructor -- this simply defines the object instance and puts - # it in a dormant state. Passing it to 'asyncio.create_connection()' - # will activate it. - - def __init__(self, port): - super().__init__() - - # These properties can be accessed without owning '_state_sem' - # because they're either constant or concurrent-safe or - # they're not manipulated across 'await' statements. - - self.meta = {} - self.port = port - self.buf = bytearray() - self._qdata = asyncio.Queue() # queue to hold incoming - # acquisition replies - - self._state_sem = asyncio.Semaphore() - - # When accessing these properties, '_state_sem' must be owned. - - self._transport = asyncio.get_event_loop().create_future() - self._dev_list = {} # set of staged DRF requests - self._active_dev_list = {} # DRFs being used in active - # acquisition - self._qrpy = [] # queue holding futures to be - # resolved when a reply is - # received - self.active = False # flag indicating acquisition status - self.can_set = False # flag indicating settings are enabled - self.model = None # default model to be used by - # connection - self.req_tmo = 2000 # ms timeout for replies - - # The '._transport' field of a DPM instance is always a - # 'Future'. The constructor sets it to an unresolved future. The - # '.connection_made()' callback will resolve it with the actual transport. - - async def _get_transport(self): - try: - return await asyncio.wait_for(self._transport, timeout=None) - except asyncio.TimeoutError as exc: - raise acsys.status.AcnetReplyTaskDisconnected() from exc - - # We use the state of the DPM object to initialize the state of - # the connection. This means a new DPM object will do the minimum - # to prepare the connection's state whereas a connection returning - # data will get its DRF requests setup and started. - - async def _setup_state(self, transport): - async with self._state_sem as lock: - _log.debug('setting up DPM state') - self._transport.set_result(transport) - - # The pending replies from a previous connection should - # have been cleared out before we start a new one. - - assert self._qrpy == [] - - await self._add_to_list(lock, 0, f'#USER:{getpass.getuser()}') - await self._add_to_list(lock, 0, f'#PID:{os.getpid()}') - await self._add_to_list(lock, 0, '#TYPE:Python3/TCP') - - # Did we have settings enabled in a previous connection? - # If so, enable them again. - - if self.can_set: - _log.debug('re-enabling settings') - await self.enable_settings() - - # Load up all the DRF requests. - - for tag, drf in self._active_dev_list.items(): - await self._add_to_list(lock, tag, drf) - - # If the previous connection was running acquisition, - # start it up again. - - if self.active: - _log.debug('re-activating data acquisition') - await self.start(self.model) - - # Called when the connection is lost or when closing out a DPM - # connection. It closes the connection to DPM, if it was open, and - # then makes sure all queue are drained so connecting to another - # DPM will start with initialized state. All DRF lists are - # preserved so that the DPM state can be restored. - - async def _shutdown_state(self, restart=False): - _log.debug('shutting down DPM state') - async with self._state_sem: - - # Clean up the transport resources. - - (await self._get_transport()).abort() - self._transport = asyncio.get_event_loop().create_future() - - # Loop through all pending requests and resolve them with - # exceptions. - - for ii in self._qrpy: - ii.set_exception(acsys.status.ACNET_DISCONNECTED) - self._qrpy = [] - - # Must do this outside the previous block because tasks - # reading the queue contents may have to grab the semaphore - # and we don't want to dead-lock. - - _log.debug('waiting for reply queue to be drained') - await asyncio.wait_for(self._qdata.join(), 1000) - - if restart: - self.connect() - - # Called when the TCP socket has connected to the remote machine. - # The 'transport' parameter is the object to use to write data to - # the remote end. A future is scheduled to initialize the state of - # the DPM object. - - def connection_made(self, transport): - self.buf = bytearray() - _log.info('connected to DPM') - transport.write(b'GET /dpm HTTP/1.1\r\n') - transport.write(b'content-type: application/pc\r\n\r\n') - asyncio.ensure_future(self._setup_state(transport)) - - # Called when we lose our connection to DPM. - - def connection_lost(self, exc): - if exc is not None: - _log.warning('lost connection to DPM, %s', exc) - asyncio.ensure_future(self._shutdown_state(restart=True)) - else: - _log.debug('closed connection to DPM') - - @staticmethod - def _get_packet(buf): - if len(buf) >= 4: - total = (buf[0] << 24) + (buf[1] << 16) + \ - (buf[2] << 8) + buf[3] - if len(buf) >= total + 4: - return (iter(memoryview(buf)[4:(total + 4)]), - buf[(total + 4):]) - return (None, buf) - - def data_received(self, data): - self.buf += data - pkt, rest = DPM._get_packet(self.buf) - while pkt is not None: - msg = self._xlat_reply(unmarshal_reply(pkt)) - - if isinstance(msg, list): - for ii in msg: - self._qdata.put_nowait(ii) - elif isinstance(msg, (ItemData, ItemStatus)): - self._qdata.put_nowait(msg) - elif msg is not None: - self._qrpy[0].set_result(msg) - self._qrpy.pop() - - pkt, rest = DPM._get_packet(rest) - self.buf = rest - - def _xlat_reply(self, msg): - if isinstance(msg, Status_reply): - return ItemStatus(msg.ref_id, msg.status) - if isinstance(msg, (AnalogAlarm_reply, - DigitalAlarm_reply, - BasicStatus_reply)): - return ItemData(msg.ref_id, msg.cycle, msg.timestamp, - acsys.status.Status.create(msg.status), - msg.__dict__, meta=self.meta.get(msg.ref_id, {})) - if isinstance(msg, (Raw_reply, - ScalarArray_reply, - Scalar_reply, - TextArray_reply, - Text_reply)): - return ItemData(msg.ref_id, msg.cycle, msg.timestamp, - acsys.status.Status.create(msg.status), - msg.data, meta=self.meta.get(msg.ref_id, {})) - if isinstance(msg, ApplySettings_reply): - return [ItemStatus(reply.ref_id, reply.status) - for reply in msg.status] - if isinstance(msg, (ListStatus_reply, - OpenList_reply)): - return None - if isinstance(msg, DeviceInfo_reply): - self.meta[msg.ref_id] = \ - {'di': msg.di, 'name': msg.name, - 'desc': msg.description, - 'units': msg.units if hasattr(msg, 'units') else None, - 'format_hint': msg.format_hint if hasattr(msg, 'format_hint') - else None} - return None - if isinstance(msg, TimedScalarArray_reply): - return ItemData(msg.ref_id, msg.cycle, msg.timestamp, - acsys.status.Status.create(msg.status), - msg.data, meta=self.meta.get(msg.ref_id, {}), - micros=msg.micros) - return msg - - async def connect(self): - assert ssl.HAS_ECDH - - loop = asyncio.get_event_loop() - - # Create SSL context. Use defaults and then shut off TLS 1.0 - # and 1.1. - - ssl_ctx = ssl.create_default_context() - ssl_ctx.options |= ssl.OP_NO_TLSv1 - ssl_ctx.options |= ssl.OP_NO_TLSv1_1 - ssl_ctx.options |= ssl.OP_NO_SSLv2 - ssl_ctx.options |= ssl.OP_NO_SSLv3 - - # Create the connection to the server and wait for the future - # to complete. We give it 2 seconds to connect. - - con_fut = loop.create_connection(lambda: self, - host='acsys-proxy.fnal.gov', - port=self.port, - ssl=ssl_ctx) - await asyncio.wait_for(con_fut, 2000) - - # To reach this spot, two things will have happened: 1) we - # actually made a connection to a remote DPM. If we didn't, an - # exception would be raised. And 2) the .connect_made() method - # has been called and finished executing. At this point, - # ._setup_state() has been scheduled, but might not have - # completed yet. We can't return yet because the user's script - # may try to send requests to DPM. So we block here until the - # future that holds the transport gets resolved. - - await self._transport - - async def replies(self, tmo=None, model=None): - """Returns a generator of replies. - -Returns an async generator which yields each reply from DPM. The -optional `tmo` parameter indicates how long to wait between -replies before an `asyncio.TimeoutError` is raised. - - """ - should_stop = False - - try: - # If we're not active, then the user is expexting this - # generator to do the start/stop management. - - if not self.active: - await self.start(model) - should_stop = True - - while True: - pkt = await asyncio.wait_for(self._qdata.get(), tmo) - self._qdata.task_done() - yield pkt - finally: - # The generator has been exited. If this function started - # acquisition, then it should stop it. When we get a reply - # for the `.stop()`, we know we won't get any more data - # replies. `._qdata` may contain some stale entries so we - # throw it away and create a new, empty queue. - - if should_stop: - await self.stop() - self._qdata = asyncio.Queue() - - async def get_entry(self, tag, active=False): - """Returns the DRF string associated with the 'tag'. - -The DPM object keeps track of two sets of DRF requests; one set -represents requests which become active after a call to -'DPM.start()'. The other set is defined when data acquisition is -active. - -If the 'active' parameter is False or data acquisition isn't -happening, the staged requests are searched. If 'active' is True -and acquisition is happening, the active requests are searched. - - """ - async with self._state_sem: - if active and self.active: - return self._active_dev_list.get(tag) - return self._dev_list.get(tag) - - # Makes a request to DPM. - - async def _mk_request(self, _lock, msg, wait=False): - msg = bytes(msg.marshal()) - xport = await self._get_transport() - xport.writelines([len(msg).to_bytes(4, byteorder='big'), msg]) - if wait: - fut = asyncio.get_event_loop().create_future() - self._qrpy.append(fut) - return fut - return None - - async def clear_list(self): - """Clears all entries in the tag/drf dictionary. - -Clearing the list doesn't stop incoming data acquisition, it -clears the list to be sent with the next call to 'dpm.start()'. -To stop data acquisition, call 'dpm.stop()'. - - """ - msg = ClearList_request() - msg.list_id = 0 # ignored in TCP connections - _log.debug('clearing list') - - async with self._state_sem as lock: - await self._mk_request(lock, msg) - self._dev_list = {} - - async def _add_to_list(self, lock, tag, drf): - msg = AddToList_request() - - msg.list_id = 0 # ignored in TCP connections - msg.ref_id = tag - msg.drf_request = drf - - # Perform the request. If the request returns a fatal error, - # the status will be raised for us. If the DPM returns a fatal - # status in the reply message, we raise it ourselves. - - _log.debug('adding tag:%d, drf:%s', tag, drf) - await self._mk_request(lock, msg) - - # DPM has been updated so we can safely add the entry to our - # device list. (We don't add entries starting with "#" because - # those are property hacks. - - if drf[0] != "#": - self._dev_list[tag] = drf - - async def add_entry(self, tag, drf): - """Add an entry to the list of devices to be acquired. - -This updates the list of device requests. The 'tag' parameter is -used to mark this request's device data. When the script starts -receiving ItemData objects, it can correlate the data using the -'tag' field. The 'tag' must be an integer -- the method will -raise a ValueError if it's not. - -The 'drf' parameter is a DRF2 string representing the data to be -read along with the sampling event. If it isn't a string, -ValueError will be raised. - -If this method is called with a tag that was previously used, it -replaces the previous request. If data is currently being -returned, it won't reflect the new entry until the 'start' method -is called. - -If simultaneous calls are made to this method and all are using -the same 'tag', which 'drf' string is ultimately associated with -the tag is non-deterministic. - - """ - - # Make sure the tag parameter is an integer and the drf - # parameter is a string. Otherwise throw a ValueError - # exception. - - if isinstance(tag, int): - if isinstance(drf, str): - async with self._state_sem as lock: - await self._add_to_list(lock, tag, drf) - else: - raise ValueError('drf must be a string') - else: - raise ValueError('tag must be an integer') - - async def add_entries(self, entries): - """Adds multiple entries. - -This is a convenience function to add a list of tag/drf pairs to -DPM's request list. - - """ - - # Validate the array of parameters. - - for tag, drf in entries: - if not isinstance(tag, int): - raise ValueError(f'tag must be an integer -- found {tag}') - if not isinstance(drf, str): - raise ValueError('drf must be a string -- found {drf}') - - async with self._state_sem as lock: - for tag, drf in entries: - await self._add_to_list(lock, tag, drf) - - async def remove_entry(self, tag): - """Removes an entry from the list of devices to be acquired. - -This updates the list of device requests. The 'tag' parameter is -used to specify which request should be removed from the list. -The 'tag' must be an integer -- the method will raise a -`ValueError` if it's not. - -Data associated with the 'tag' will continue to be returned until -the '.start()' method is called. - - """ - - # Make sure the tag parameter is an integer and the drf - # parameter is a string. Otherwise throw a ValueError - # exception. - - if isinstance(tag, int): - # Create the message and set the fields appropriately. - - msg = RemoveFromList_request() - msg.list_id = 0 # ignored in TCP connections - msg.ref_id = tag - _log.debug('removing tag:%d', tag) - - async with self._state_sem as lock: - await self._mk_request(lock, msg) - - # DPM has been updated so we can safely remove the - # entry from our device list. - - del self._dev_list[tag] - else: - raise ValueError('tag must be an integer') - - async def start(self, model=None): - """Start/restart data acquisition using the current request list. - -Calls to '.add_entry()' and '.remove_entry()' make changes to the -list of requests but don't actually affect data acquisition until -this method is called. This allows a script to make major -adjustments and then enable the changes all at once. - - """ - _log.debug('starting DPM list') - self.model = model - msg = StartList_request() - msg.list_id = 0 # ignored in TCP connections - - if self.model: - msg.model = self.model - - async with self._state_sem as lock: - fut = await self._mk_request(lock, msg, wait=True) - self.active = True - self._active_dev_list = self._dev_list.copy() - await fut - - async def stop(self): - """Stops data acquisition. - -This method stops data acquisition. The list of requests is -unaffected so a call to '.start()' will restart the list. - -Due to the asynchronous nature of network communications, after -calling this method, a few readings may still get delivered. - - """ - - msg = StopList_request() - msg.list_id = 0 - _log.debug('stopping DPM list') - - async with self._state_sem as lock: - await self._mk_request(lock, msg) - self.active = False - self._active_dev_list = {} - - @staticmethod - def _build_struct(ref_id, value): - if isinstance(value, (bytearray, bytes)): - set_struct = RawSetting_struct() - elif isinstance(value, str): - if not isinstance(value, list): - value = [value] - set_struct = TextSetting_struct() - else: - if not isinstance(value, list): - value = [value] - set_struct = ScaledSetting_struct() - - set_struct.ref_id = ref_id - set_struct.data = value - return set_struct - - # Performs one round-trip of the Kerberos validation. - - async def _auth_step(self, lock, tok): - msg = Authenticate_request() - msg.list_id = 0 - if tok is not None: - msg.token = tok - - fut = await self._mk_request(lock, msg, wait=True) - - msg = await fut - if not isinstance(msg, Authenticate_reply): - raise TypeError(f'unexpected protocol message: %{msg}') - return msg - - async def enable_settings(self, role=None): - """Enable settings for the current DPM session. - -This method exchanges credentials with the DPM. If successful, -the session is allowed to make settings. The script must be -running in an environment with a valid Kerberos ticket. The -ticket must part of the FNAL.GOV realm and can't be expired. - -The credentials are valid as long as this session is maintained. - -The `role` parameter indicates in what role your script will be -running. Your Kerberos principal should be authorized to operate -in the role. - - """ - - # Lazy load the gssapi library so that this doesn't block users - # who are only doing readings. - - spec = importlib.util.find_spec('gssapi') - if spec is None: - _log.error('Cannot find the gssapi module') - print('To enable settings, the "gssapi" module must be installed.') - print(('Run `pip install "acsys[settings]"` ' - 'to install the required library.')) - sys.exit(1) - - # Perform the actual import - - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - gssapi = importlib.import_module('gssapi') - requirement_flag = gssapi.raw.types.RequirementFlag - - # Get the user's Kerberos credentials. Make sure they are from, - # the FNAL.GOV realm and they haven't expired. - - creds = gssapi.creds.Credentials(usage='initiate') - principal = str(creds.name).split('@') - - if principal[1] != 'FNAL.GOV': - raise ValueError('invalid Kerberos realm') - if creds.lifetime <= 0: - raise ValueError('Kerberos ticket expired') - - try: - async with self._state_sem as lock: - - # Create a security context used to sign messages. - - msg = await self._auth_step(lock, None) - service_name = gssapi.Name(msg.serviceName.translate( - {ord('@'): '/', ord('\\'): None})) - _log.info('service name: %s', service_name) - ctx = gssapi.SecurityContext(name=service_name, usage='initiate', - creds=creds, - flags=[requirement_flag.replay_detection, - requirement_flag.integrity, - requirement_flag.out_of_sequence_detection], - mech=gssapi.MechType.kerberos) - try: - if role is not None: - await self._add_to_list(lock, 0, f'#ROLE:{role}') - - # Enter a loop which steps the security context to - # completion (or an error occurs.) - - in_tok = None - while not ctx.complete: - msg = await self._auth_step(lock, bytes(ctx.step(in_tok))) - - if not hasattr(msg, 'token'): - break - - in_tok = msg.token - - # Now that the context has been validated, send - # the 'EnableSettings' request with a signed - # message. - - msg = EnableSettings_request() - msg.list_id = 0 - msg.message = b'1234' - msg.MIC = ctx.get_signature(msg.message) - - await self._mk_request(lock, msg) - self.can_set = True - _log.info('settings enabled') - finally: - del ctx - finally: - del creds - - async def apply_settings(self, input_array): - """A placeholder for apply setting docstring - """ - - async with self._state_sem as lock: - if not self.can_set: - raise RuntimeError('settings are disabled') - - if not isinstance(input_array, list): - input_array = [input_array] - - msg = ApplySettings_request() - - for ref_id, input_val in input_array: - if self._dev_list.get(ref_id) is None: - raise ValueError(f'setting for undefined ref_id, {ref_id}') - - dpm_struct = DPM._build_struct(ref_id, input_val) - if isinstance(dpm_struct, RawSetting_struct): - if not hasattr(msg, 'raw_array'): - msg.raw_array = [] - msg.raw_array.append(dpm_struct) - elif isinstance(dpm_struct, TextSetting_struct): - if not hasattr(msg, 'text_array'): - msg.text_array = [] - msg.text_array.append(dpm_struct) - else: - if not hasattr(msg, 'scaled_array'): - msg.scaled_array = [] - msg.scaled_array.append(dpm_struct) - - msg.list_id = 0 - await self._mk_request(lock, msg) - - -class DPMContext: - """Defines a scope for communicating with DPM.. - -Creates a communication context with one DPM (of a pool of DPMs.) -This context must be used in an `async-with-statement` so that -resources are properly released when the block is exited. - - async with DpmContext() as dpm: - # 'dpm' is an instance of DPM and is usable while - # in this block. - -Creating a DPM context isn't necessarily a trivial process, so it -should be done at a higher level - preferrably as the script -starts up. Future versions of this package may move the Kerberos -negotiation into this section as well, instead of hiding it in -`.settings_enable()`, so it will be even more expensive. - - """ - - def __init__(self, *, port=6802): - self.dpm = DPM(port) - - async def __aenter__(self): - _log.debug('entering DPM context') - await self.dpm.connect() - return self.dpm - - async def __aexit__(self, exc_type, exc, trace_back): - _log.debug('exiting DPM context') - await self.dpm._shutdown_state() - return False diff --git a/acsys/dpm/dpm_protocol.py b/acsys/dpm/dpm_protocol.py deleted file mode 100644 index ff0c599..0000000 --- a/acsys/dpm/dpm_protocol.py +++ /dev/null @@ -1,1806 +0,0 @@ -# Generated by the protocol compiler version 1.3.8 -# DO NOT EDIT THIS FILE DIRECTLY! - -__doc__ = 'Message serializer for the DPM protocol.' - -from itertools import chain, islice - -__all__ = ['ProtocolError', - 'unmarshal_request', - 'unmarshal_reply', - 'RawSetting_struct', - 'ScaledSetting_struct', - 'TextSetting_struct', - 'SettingStatus_struct', - 'ServiceDiscovery_request', - 'OpenList_request', - 'AddToList_request', - 'Authenticate_request', - 'EnableSettings_request', - 'RemoveFromList_request', - 'StartList_request', - 'ClearList_request', - 'StopList_request', - 'ApplySettings_request', - 'ServiceDiscovery_reply', - 'OpenList_reply', - 'AddToList_reply', - 'RemoveFromList_reply', - 'StartList_reply', - 'ListStatus_reply', - 'Status_reply', - 'DeviceInfo_reply', - 'Scalar_reply', - 'ScalarArray_reply', - 'Raw_reply', - 'Text_reply', - 'TextArray_reply', - 'AnalogAlarm_reply', - 'DigitalAlarm_reply', - 'BasicStatus_reply', - 'TimedScalarArray_reply', - 'ApplySettings_reply', - 'Authenticate_reply'] - -import struct - -class ProtocolError(Exception): - """Exception class that gets raised when there's a problem marshaling - or unmarshaling a message from the DPM protocol.""" - - def __init__(self, reason): - self.reason = reason - - def __str__(self): - return repr(self.reason) - -# -- Internal marshalling routines -- - -def emitRawInt(tag, val): - def emitEach(buf, n): - curr = (val >> (n * 8)) & 0xff - next = val >> ((n + 1) * 8) - if (next == 0 and (curr & 0x80) != 0x80) or \ - (next == -1 and (curr & 0x80) == 0x80): - buf.append(tag + n + 1) - else: - emitEach(buf, n + 1) - buf.append(curr) - tmp = bytearray() - emitEach(tmp, 0) - return tmp - -def marshal_bool(val): - yield (0x71 if val else 0x70) - -def marshal_int16(val): - if isinstance(val, int): - if val < 32768 and val > -32769: - return emitRawInt(0x10, val) - else: - raise ProtocolError("value out of range for int16") - else: - raise ProtocolError("expected integer type") - -def marshal_int32(val): - if isinstance(val, int): - if int(-2147483648) <= val <= int(2147483647): - return emitRawInt(0x10, val) - else: - raise ProtocolError("value out of range for int32") - else: - raise ProtocolError("expected integer type") - -def marshal_int64(val): - if isinstance(val, int): - if int(-9223372036854775808) <= val <= int(9223372036854775807): - return emitRawInt(0x10, val) - else: - raise ProtocolError("value out of range for int64") - else: - raise ProtocolError("expected integer type") - -def marshal_double(val): - return chain(b'\x28', (ii for ii in struct.pack(">d", float(val)))) - -def marshal_string(val): - if isinstance(val, str): - return chain(emitRawInt(0x40, len(val)),\ - (ord(ii) for ii in val)) - else: - raise ProtocolError("expected string type") - -def marshal_binary(val): - if isinstance(val, (bytearray, bytes)): - return chain(emitRawInt(0x30, len(val)), val) - else: - raise ProtocolError("expected bytearray or bytes type") - -def marshal_array(fn, val): - if isinstance(val, list): - return chain(emitRawInt(0x50, len(val)),\ - chain.from_iterable((fn(v) for v in val))) - else: - raise ProtocolError("expected list type") - -class RawSetting_struct: - def __init__(self): - self.ref_id = int(0) - self.data = b'' - - def __eq__(self, other): - return self.ref_id == other.ref_id and \ - self.data == other.data - - def __ne__(self, other): - return not self.__eq__(other) - -def marshal_RawSetting_struct(val): - return chain(b'\x51\x04\x12\x1e\xab', - marshal_int64(val.ref_id), - b'\x12\x7f\x38', - marshal_binary(val.data)) - -class ScaledSetting_struct: - def __init__(self): - self.ref_id = int(0) - self.data = [] - - def __eq__(self, other): - return self.ref_id == other.ref_id and \ - self.data == other.data - - def __ne__(self, other): - return not self.__eq__(other) - -def marshal_ScaledSetting_struct(val): - return chain(b'\x51\x04\x12\x1e\xab', - marshal_int64(val.ref_id), - b'\x12\x7f\x38', - marshal_array(marshal_double, val.data)) - -class TextSetting_struct: - def __init__(self): - self.ref_id = int(0) - self.data = [] - - def __eq__(self, other): - return self.ref_id == other.ref_id and \ - self.data == other.data - - def __ne__(self, other): - return not self.__eq__(other) - -def marshal_TextSetting_struct(val): - return chain(b'\x51\x04\x12\x1e\xab', - marshal_int64(val.ref_id), - b'\x12\x7f\x38', - marshal_array(marshal_string, val.data)) - -class SettingStatus_struct: - def __init__(self): - self.ref_id = int(0) - self.status = int(0) - - def __eq__(self, other): - return self.ref_id == other.ref_id and \ - self.status == other.status - - def __ne__(self, other): - return not self.__eq__(other) - -def marshal_SettingStatus_struct(val): - return chain(b'\x51\x04\x12\x1e\xab', - marshal_int64(val.ref_id), - b'\x12\x44\x54', - marshal_int16(val.status)) - -class ServiceDiscovery_request: - def __eq__(self, other): - return True - - def __ne__(self, other): - return False - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of ServiceDiscovery_request.""" - return b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\xdf\xda\x51\x00' - -class OpenList_request: - - def __eq__(self, other): - return ((not hasattr(self, 'location') and not hasattr(other, 'location')) or \ - (hasattr(self, 'location') and hasattr(other, 'location') and \ - self.location == other.location)) - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of OpenList_request.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\x08\x1c', - emitRawInt(0x50, 0 \ - + (2 if hasattr(self, 'location') else 0)), - chain(b'\x12\x9d\xe0', - marshal_string(self.location)) \ - if hasattr(self, 'location') else b'') - -class AddToList_request: - def __init__(self): - self.list_id = int(0) - self.ref_id = int(0) - self.drf_request = '' - - def __eq__(self, other): - return self.list_id == other.list_id and \ - self.ref_id == other.ref_id and \ - self.drf_request == other.drf_request - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of AddToList_request.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x11\x61\x51\x06\x12\xe8\x20', - marshal_int32(self.list_id), - b'\x12\x1e\xab', - marshal_int64(self.ref_id), - b'\x12\x63\x24', - marshal_string(self.drf_request)) - -class Authenticate_request: - def __init__(self): - self.list_id = int(0) - self.token = b'' - - def __eq__(self, other): - return self.list_id == other.list_id and \ - self.token == other.token - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of Authenticate_request.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\xc4\x53\x51\x04\x12\xe8\x20', - marshal_int32(self.list_id), - b'\x12\x8d\xa1', - marshal_binary(self.token)) - -class EnableSettings_request: - def __init__(self): - self.list_id = int(0) - self.MIC = b'' - self.message = b'' - - def __eq__(self, other): - return self.list_id == other.list_id and \ - self.MIC == other.MIC and \ - self.message == other.message - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of EnableSettings_request.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\x53\x62\x51\x06\x12\xe8\x20', - marshal_int32(self.list_id), - b'\x12\x1f\xcd', - marshal_binary(self.MIC), - b'\x12\x31\x02', - marshal_binary(self.message)) - -class RemoveFromList_request: - def __init__(self): - self.list_id = int(0) - self.ref_id = int(0) - - def __eq__(self, other): - return self.list_id == other.list_id and \ - self.ref_id == other.ref_id - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of RemoveFromList_request.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\xf1\x21\x51\x04\x12\xe8\x20', - marshal_int32(self.list_id), - b'\x12\x1e\xab', - marshal_int64(self.ref_id)) - -class StartList_request: - def __init__(self): - self.list_id = int(0) - - def __eq__(self, other): - return self.list_id == other.list_id and \ - ((not hasattr(self, 'model') and not hasattr(other, 'model')) or \ - (hasattr(self, 'model') and hasattr(other, 'model') and \ - self.model == other.model)) - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of StartList_request.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\xdd\xb5', - emitRawInt(0x50, 2 \ - + (2 if hasattr(self, 'model') else 0)), - b'\x12\xe8\x20', - marshal_int32(self.list_id), - chain(b'\x12\x5e\x63', - marshal_string(self.model)) \ - if hasattr(self, 'model') else b'') - -class ClearList_request: - def __init__(self): - self.list_id = int(0) - - def __eq__(self, other): - return self.list_id == other.list_id - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of ClearList_request.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\xc3\x09\x51\x02\x12\xe8\x20', - marshal_int32(self.list_id)) - -class StopList_request: - def __init__(self): - self.list_id = int(0) - - def __eq__(self, other): - return self.list_id == other.list_id - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of StopList_request.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\x53\xa9\x51\x02\x12\xe8\x20', - marshal_int32(self.list_id)) - -class ApplySettings_request: - def __init__(self): - self.user_name = '' - self.list_id = int(0) - - def __eq__(self, other): - return self.user_name == other.user_name and \ - self.list_id == other.list_id and \ - ((not hasattr(self, 'raw_array') and not hasattr(other, 'raw_array')) or \ - (hasattr(self, 'raw_array') and hasattr(other, 'raw_array') and \ - self.raw_array == other.raw_array)) and \ - ((not hasattr(self, 'scaled_array') and not hasattr(other, 'scaled_array')) or \ - (hasattr(self, 'scaled_array') and hasattr(other, 'scaled_array') and \ - self.scaled_array == other.scaled_array)) and \ - ((not hasattr(self, 'text_array') and not hasattr(other, 'text_array')) or \ - (hasattr(self, 'text_array') and hasattr(other, 'text_array') and \ - self.text_array == other.text_array)) - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of ApplySettings_request.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\xc6\x78', - emitRawInt(0x50, 4 \ - + (2 if hasattr(self, 'raw_array') else 0) \ - + (2 if hasattr(self, 'scaled_array') else 0) \ - + (2 if hasattr(self, 'text_array') else 0)), - b'\x12\x56\x48', - marshal_string(self.user_name), - b'\x12\xe8\x20', - marshal_int32(self.list_id), - chain(b'\x12\x03\x01', - marshal_array(marshal_RawSetting_struct, self.raw_array)) \ - if hasattr(self, 'raw_array') else b'', - chain(b'\x12\x63\xce', - marshal_array(marshal_ScaledSetting_struct, self.scaled_array)) \ - if hasattr(self, 'scaled_array') else b'', - chain(b'\x12\xce\x8f', - marshal_array(marshal_TextSetting_struct, self.text_array)) \ - if hasattr(self, 'text_array') else b'') - -class ServiceDiscovery_reply: - def __init__(self): - self.load = int(0) - self.serviceLocation = '' - - def __eq__(self, other): - return self.load == other.load and \ - self.serviceLocation == other.serviceLocation - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of ServiceDiscovery_reply.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\xcd\x7e\x51\x04\x12\x1e\xb3', - marshal_int16(self.load), - b'\x12\x11\xaf', - marshal_string(self.serviceLocation)) - -class OpenList_reply: - def __init__(self): - self.list_id = int(0) - - def __eq__(self, other): - return self.list_id == other.list_id - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of OpenList_reply.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\x34\x9e\x51\x02\x12\xe8\x20', - marshal_int32(self.list_id)) - -class AddToList_reply: - def __init__(self): - self.list_id = int(0) - self.ref_id = int(0) - self.status = int(0) - - def __eq__(self, other): - return self.list_id == other.list_id and \ - self.ref_id == other.ref_id and \ - self.status == other.status - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of AddToList_reply.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\x8b\xac\x51\x06\x12\xe8\x20', - marshal_int32(self.list_id), - b'\x12\x1e\xab', - marshal_int64(self.ref_id), - b'\x12\x44\x54', - marshal_int16(self.status)) - -class RemoveFromList_reply: - def __init__(self): - self.list_id = int(0) - self.ref_id = int(0) - self.status = int(0) - - def __eq__(self, other): - return self.list_id == other.list_id and \ - self.ref_id == other.ref_id and \ - self.status == other.status - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of RemoveFromList_reply.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\xf4\x1a\x51\x06\x12\xe8\x20', - marshal_int32(self.list_id), - b'\x12\x1e\xab', - marshal_int64(self.ref_id), - b'\x12\x44\x54', - marshal_int16(self.status)) - -class StartList_reply: - def __init__(self): - self.list_id = int(0) - self.status = int(0) - - def __eq__(self, other): - return self.list_id == other.list_id and \ - self.status == other.status - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of StartList_reply.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\x96\x00\x51\x04\x12\xe8\x20', - marshal_int32(self.list_id), - b'\x12\x44\x54', - marshal_int16(self.status)) - -class ListStatus_reply: - def __init__(self): - self.list_id = int(0) - self.status = int(0) - - def __eq__(self, other): - return self.list_id == other.list_id and \ - self.status == other.status - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of ListStatus_reply.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\x73\x70\x51\x04\x12\xe8\x20', - marshal_int32(self.list_id), - b'\x12\x44\x54', - marshal_int16(self.status)) - -class Status_reply: - def __init__(self): - self.ref_id = int(0) - self.timestamp = int(0) - self.cycle = int(0) - self.status = int(0) - - def __eq__(self, other): - return self.ref_id == other.ref_id and \ - self.timestamp == other.timestamp and \ - self.cycle == other.cycle and \ - self.status == other.status - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of Status_reply.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\xd6\xad\x51\x08\x12\x1e\xab', - marshal_int64(self.ref_id), - b'\x12\xd5\x5b', - marshal_int64(self.timestamp), - b'\x12\x07\x40', - marshal_int64(self.cycle), - b'\x12\x44\x54', - marshal_int16(self.status)) - -class DeviceInfo_reply: - def __init__(self): - self.ref_id = int(0) - self.di = int(0) - self.name = '' - self.description = '' - - def __eq__(self, other): - return self.ref_id == other.ref_id and \ - self.di == other.di and \ - self.name == other.name and \ - self.description == other.description and \ - ((not hasattr(self, 'units') and not hasattr(other, 'units')) or \ - (hasattr(self, 'units') and hasattr(other, 'units') and \ - self.units == other.units)) and \ - ((not hasattr(self, 'format_hint') and not hasattr(other, 'format_hint')) or \ - (hasattr(self, 'format_hint') and hasattr(other, 'format_hint') and \ - self.format_hint == other.format_hint)) - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of DeviceInfo_reply.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\x6f\xed', - emitRawInt(0x50, 8 \ - + (2 if hasattr(self, 'units') else 0) \ - + (2 if hasattr(self, 'format_hint') else 0)), - b'\x12\x1e\xab', - marshal_int64(self.ref_id), - b'\x12\x82\xdd', - marshal_int32(self.di), - b'\x12\x93\x1c', - marshal_string(self.name), - b'\x12\xf9\x2c', - marshal_string(self.description), - chain(b'\x12\x3d\xfb', - marshal_string(self.units)) \ - if hasattr(self, 'units') else b'', - chain(b'\x12\x7e\xc2', - marshal_int16(self.format_hint)) \ - if hasattr(self, 'format_hint') else b'') - -class Scalar_reply: - def __init__(self): - self.ref_id = int(0) - self.timestamp = int(0) - self.cycle = int(0) - self.status = int(0) - self.data = float(0.0) - - def __eq__(self, other): - return self.ref_id == other.ref_id and \ - self.timestamp == other.timestamp and \ - self.cycle == other.cycle and \ - self.status == other.status and \ - self.data == other.data - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of Scalar_reply.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\xd6\xab\x51\x0a\x12\x1e\xab', - marshal_int64(self.ref_id), - b'\x12\xd5\x5b', - marshal_int64(self.timestamp), - b'\x12\x07\x40', - marshal_int64(self.cycle), - b'\x12\x44\x54', - marshal_int16(self.status), - b'\x12\x7f\x38', - marshal_double(self.data)) - -class ScalarArray_reply: - def __init__(self): - self.ref_id = int(0) - self.timestamp = int(0) - self.cycle = int(0) - self.status = int(0) - self.data = [] - - def __eq__(self, other): - return self.ref_id == other.ref_id and \ - self.timestamp == other.timestamp and \ - self.cycle == other.cycle and \ - self.status == other.status and \ - self.data == other.data - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of ScalarArray_reply.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\x59\xfc\x51\x0a\x12\x1e\xab', - marshal_int64(self.ref_id), - b'\x12\xd5\x5b', - marshal_int64(self.timestamp), - b'\x12\x07\x40', - marshal_int64(self.cycle), - b'\x12\x44\x54', - marshal_int16(self.status), - b'\x12\x7f\x38', - marshal_array(marshal_double, self.data)) - -class Raw_reply: - def __init__(self): - self.ref_id = int(0) - self.timestamp = int(0) - self.cycle = int(0) - self.status = int(0) - self.data = b'' - - def __eq__(self, other): - return self.ref_id == other.ref_id and \ - self.timestamp == other.timestamp and \ - self.cycle == other.cycle and \ - self.status == other.status and \ - self.data == other.data - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of Raw_reply.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\xcf\x5e\x51\x0a\x12\x1e\xab', - marshal_int64(self.ref_id), - b'\x12\xd5\x5b', - marshal_int64(self.timestamp), - b'\x12\x07\x40', - marshal_int64(self.cycle), - b'\x12\x44\x54', - marshal_int16(self.status), - b'\x12\x7f\x38', - marshal_binary(self.data)) - -class Text_reply: - def __init__(self): - self.ref_id = int(0) - self.timestamp = int(0) - self.cycle = int(0) - self.status = int(0) - self.data = '' - - def __eq__(self, other): - return self.ref_id == other.ref_id and \ - self.timestamp == other.timestamp and \ - self.cycle == other.cycle and \ - self.status == other.status and \ - self.data == other.data - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of Text_reply.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\x8f\x32\x51\x0a\x12\x1e\xab', - marshal_int64(self.ref_id), - b'\x12\xd5\x5b', - marshal_int64(self.timestamp), - b'\x12\x07\x40', - marshal_int64(self.cycle), - b'\x12\x44\x54', - marshal_int16(self.status), - b'\x12\x7f\x38', - marshal_string(self.data)) - -class TextArray_reply: - def __init__(self): - self.ref_id = int(0) - self.timestamp = int(0) - self.cycle = int(0) - self.status = int(0) - self.data = [] - - def __eq__(self, other): - return self.ref_id == other.ref_id and \ - self.timestamp == other.timestamp and \ - self.cycle == other.cycle and \ - self.status == other.status and \ - self.data == other.data - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of TextArray_reply.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\x32\x7b\x51\x0a\x12\x1e\xab', - marshal_int64(self.ref_id), - b'\x12\xd5\x5b', - marshal_int64(self.timestamp), - b'\x12\x07\x40', - marshal_int64(self.cycle), - b'\x12\x44\x54', - marshal_int16(self.status), - b'\x12\x7f\x38', - marshal_array(marshal_string, self.data)) - -class AnalogAlarm_reply: - def __init__(self): - self.ref_id = int(0) - self.timestamp = int(0) - self.cycle = int(0) - self.minimum = float(0.0) - self.maximum = float(0.0) - self.alarm_enable = bool(False) - self.alarm_status = bool(False) - self.abort = bool(False) - self.abort_inhibit = bool(False) - self.tries_needed = int(0) - self.tries_now = int(0) - - def __eq__(self, other): - return self.ref_id == other.ref_id and \ - self.timestamp == other.timestamp and \ - self.cycle == other.cycle and \ - self.minimum == other.minimum and \ - self.maximum == other.maximum and \ - self.alarm_enable == other.alarm_enable and \ - self.alarm_status == other.alarm_status and \ - self.abort == other.abort and \ - self.abort_inhibit == other.abort_inhibit and \ - self.tries_needed == other.tries_needed and \ - self.tries_now == other.tries_now - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of AnalogAlarm_reply.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\x75\x88\x51\x16\x12\x1e\xab', - marshal_int64(self.ref_id), - b'\x12\xd5\x5b', - marshal_int64(self.timestamp), - b'\x12\x07\x40', - marshal_int64(self.cycle), - b'\x12\x23\x4c', - marshal_double(self.minimum), - b'\x12\xe2\x88', - marshal_double(self.maximum), - b'\x12\x8f\x20', - marshal_bool(self.alarm_enable), - b'\x12\x60\x36', - marshal_bool(self.alarm_status), - b'\x12\x4a\x1c', - marshal_bool(self.abort), - b'\x12\x81\x72', - marshal_bool(self.abort_inhibit), - b'\x12\x01\x32', - marshal_int32(self.tries_needed), - b'\x12\x2b\x3e', - marshal_int32(self.tries_now)) - -class DigitalAlarm_reply: - def __init__(self): - self.ref_id = int(0) - self.timestamp = int(0) - self.cycle = int(0) - self.nominal = int(0) - self.mask = int(0) - self.alarm_enable = bool(False) - self.alarm_status = bool(False) - self.abort = bool(False) - self.abort_inhibit = bool(False) - self.tries_needed = int(0) - self.tries_now = int(0) - - def __eq__(self, other): - return self.ref_id == other.ref_id and \ - self.timestamp == other.timestamp and \ - self.cycle == other.cycle and \ - self.nominal == other.nominal and \ - self.mask == other.mask and \ - self.alarm_enable == other.alarm_enable and \ - self.alarm_status == other.alarm_status and \ - self.abort == other.abort and \ - self.abort_inhibit == other.abort_inhibit and \ - self.tries_needed == other.tries_needed and \ - self.tries_now == other.tries_now - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of DigitalAlarm_reply.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\xc7\x9b\x51\x16\x12\x1e\xab', - marshal_int64(self.ref_id), - b'\x12\xd5\x5b', - marshal_int64(self.timestamp), - b'\x12\x07\x40', - marshal_int64(self.cycle), - b'\x12\x4f\x1d', - marshal_int32(self.nominal), - b'\x12\x11\xeb', - marshal_int32(self.mask), - b'\x12\x8f\x20', - marshal_bool(self.alarm_enable), - b'\x12\x60\x36', - marshal_bool(self.alarm_status), - b'\x12\x4a\x1c', - marshal_bool(self.abort), - b'\x12\x81\x72', - marshal_bool(self.abort_inhibit), - b'\x12\x01\x32', - marshal_int32(self.tries_needed), - b'\x12\x2b\x3e', - marshal_int32(self.tries_now)) - -class BasicStatus_reply: - def __init__(self): - self.ref_id = int(0) - self.timestamp = int(0) - self.cycle = int(0) - - def __eq__(self, other): - return self.ref_id == other.ref_id and \ - self.timestamp == other.timestamp and \ - self.cycle == other.cycle and \ - ((not hasattr(self, 'on') and not hasattr(other, 'on')) or \ - (hasattr(self, 'on') and hasattr(other, 'on') and \ - self.on == other.on)) and \ - ((not hasattr(self, 'ready') and not hasattr(other, 'ready')) or \ - (hasattr(self, 'ready') and hasattr(other, 'ready') and \ - self.ready == other.ready)) and \ - ((not hasattr(self, 'remote') and not hasattr(other, 'remote')) or \ - (hasattr(self, 'remote') and hasattr(other, 'remote') and \ - self.remote == other.remote)) and \ - ((not hasattr(self, 'positive') and not hasattr(other, 'positive')) or \ - (hasattr(self, 'positive') and hasattr(other, 'positive') and \ - self.positive == other.positive)) and \ - ((not hasattr(self, 'ramp') and not hasattr(other, 'ramp')) or \ - (hasattr(self, 'ramp') and hasattr(other, 'ramp') and \ - self.ramp == other.ramp)) - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of BasicStatus_reply.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\xf2\xf9', - emitRawInt(0x50, 6 \ - + (2 if hasattr(self, 'on') else 0) \ - + (2 if hasattr(self, 'ready') else 0) \ - + (2 if hasattr(self, 'remote') else 0) \ - + (2 if hasattr(self, 'positive') else 0) \ - + (2 if hasattr(self, 'ramp') else 0)), - b'\x12\x1e\xab', - marshal_int64(self.ref_id), - b'\x12\xd5\x5b', - marshal_int64(self.timestamp), - b'\x12\x07\x40', - marshal_int64(self.cycle), - chain(b'\x12\x5c\x01', - marshal_bool(self.on)) \ - if hasattr(self, 'on') else b'', - chain(b'\x12\xab\x23', - marshal_bool(self.ready)) \ - if hasattr(self, 'ready') else b'', - chain(b'\x12\xe4\x86', - marshal_bool(self.remote)) \ - if hasattr(self, 'remote') else b'', - chain(b'\x12\x27\x16', - marshal_bool(self.positive)) \ - if hasattr(self, 'positive') else b'', - chain(b'\x12\x92\x38', - marshal_bool(self.ramp)) \ - if hasattr(self, 'ramp') else b'') - -class TimedScalarArray_reply: - def __init__(self): - self.ref_id = int(0) - self.timestamp = int(0) - self.cycle = int(0) - self.status = int(0) - self.data = [] - self.micros = [] - - def __eq__(self, other): - return self.ref_id == other.ref_id and \ - self.timestamp == other.timestamp and \ - self.cycle == other.cycle and \ - self.status == other.status and \ - self.data == other.data and \ - self.micros == other.micros - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of TimedScalarArray_reply.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\x7a\x32\x51\x0c\x12\x1e\xab', - marshal_int64(self.ref_id), - b'\x12\xd5\x5b', - marshal_int64(self.timestamp), - b'\x12\x07\x40', - marshal_int64(self.cycle), - b'\x12\x44\x54', - marshal_int16(self.status), - b'\x12\x7f\x38', - marshal_array(marshal_double, self.data), - b'\x12\x34\x2d', - marshal_array(marshal_int64, self.micros)) - -class ApplySettings_reply: - def __init__(self): - self.status = [] - - def __eq__(self, other): - return self.status == other.status - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of ApplySettings_reply.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\x43\xc0\x51\x02\x12\x44\x54', - marshal_array(marshal_SettingStatus_struct, self.status)) - -class Authenticate_reply: - - def __eq__(self, other): - return ((not hasattr(self, 'serviceName') and not hasattr(other, 'serviceName')) or \ - (hasattr(self, 'serviceName') and hasattr(other, 'serviceName') and \ - self.serviceName == other.serviceName)) and \ - ((not hasattr(self, 'token') and not hasattr(other, 'token')) or \ - (hasattr(self, 'token') and hasattr(other, 'token') and \ - self.token == other.token)) - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of Authenticate_reply.""" - return chain(b'SDD\x02\x51\x03\x14\xb1\x3a\x70\x3a\x12\x1c\x76', - emitRawInt(0x50, 0 \ - + (2 if hasattr(self, 'serviceName') else 0) \ - + (2 if hasattr(self, 'token') else 0)), - chain(b'\x12\x63\x38', - marshal_string(self.serviceName)) \ - if hasattr(self, 'serviceName') else b'', - chain(b'\x12\x8d\xa1', - marshal_binary(self.token)) \ - if hasattr(self, 'token') else b'') - -def marshal_request(val): - return val.marshal() - -def marshal_reply(val): - return val.marshal() - -# -- Internal unmarshalling routines -- - -from itertools import islice - -def consumeRawInt(it, tag): - itTag = next(it) - itLen = itTag & 0xf - if (itTag & 0xf0) == tag and itLen > 0 and itLen <= 8: - val = int.from_bytes(islice(it, 0, itLen), 'big') - if val > (1 << (itLen * 8 - 1)): - val -= 1 << (itLen * 8) - return val - else: - raise ProtocolError("bad tag or length") - -def unmarshal_bool(ii): - val = next(ii) - if val == 112: - return False - elif val == 113: - return True - else: - raise ProtocolError("expected boolean value") - -def unmarshal_int16(ii): - tag = next(ii) - if tag == 0x11: - val = next(ii) - return val if val < 128 else val - 256 - elif tag == 0x12: - hi = next(ii) - return (next(ii) + (hi if hi < 128 else hi - 256) * 256) - elif (tag & 0xf0) == 0x10: - raise ProtocolError("value out of range for int16") - else: - raise ProtocolError("unknown field found") - -def unmarshal_int32(ii): - val = consumeRawInt(ii, 0x10) - if int(-2147483648) <= val <= int(2147483647): - return int(val) - else: - raise ProtocolError("value out of range for int32") - -def unmarshal_int64(ii): - return consumeRawInt(ii, 0x10) - -_unpack_float = struct.Struct('>xd').unpack - -def unmarshal_double(it): - raw = bytes(islice(it, 9)) - if raw[0] == 40: - v, = _unpack_float(raw) - return v - else: - raise ProtocolError("expected tag for double") - -def unmarshal_string(ii): - return bytes(islice(ii, consumeRawInt(ii, 0x40))).decode('utf-8') - -def unmarshal_binary(ii): - return bytes(islice(ii, consumeRawInt(ii, 0x30))) - -def unmarshal_array(ii, fn): - return [fn(ii) for x in range(consumeRawInt(ii, 0x50))] - -def unmarshal_header(ii): - if next(ii) != 83 or next(ii) != 68 or \ - next(ii) != 68 or next(ii) != 2 or \ - consumeRawInt(ii, 0x50) != 3: - raise ProtocolError("invalid header") - elif consumeRawInt(ii, 0x10) != -1321570246: - raise ProtocolError("incorrect protocol specified") - -def unmarshal_RawSetting_struct(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 4: - raise ProtocolError("incorrect number of fields") - else: - tmp = RawSetting_struct() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == 7851: - tmp.ref_id = unmarshal_int64(ii) - elif fld == 32568: - tmp.data = unmarshal_binary(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_ScaledSetting_struct(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 4: - raise ProtocolError("incorrect number of fields") - else: - tmp = ScaledSetting_struct() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == 7851: - tmp.ref_id = unmarshal_int64(ii) - elif fld == 32568: - tmp.data = unmarshal_array(ii, unmarshal_double) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_TextSetting_struct(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 4: - raise ProtocolError("incorrect number of fields") - else: - tmp = TextSetting_struct() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == 7851: - tmp.ref_id = unmarshal_int64(ii) - elif fld == 32568: - tmp.data = unmarshal_array(ii, unmarshal_string) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_SettingStatus_struct(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 4: - raise ProtocolError("incorrect number of fields") - else: - tmp = SettingStatus_struct() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == 7851: - tmp.ref_id = unmarshal_int64(ii) - elif fld == 17492: - tmp.status = unmarshal_int16(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_ServiceDiscovery_request(ii): - if consumeRawInt(ii, 0x50) != 0: - raise ProtocolError("incorrect number of fields") - else: - return ServiceDiscovery_request() - -def unmarshal_OpenList_request(ii): - nFlds = consumeRawInt(ii, 0x50) - if (nFlds % 2) != 0 or nFlds < 0 or nFlds > 2: - raise ProtocolError("incorrect number of fields") - else: - tmp = OpenList_request() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == -25120: - tmp.location = unmarshal_string(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_AddToList_request(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 6: - raise ProtocolError("incorrect number of fields") - else: - tmp = AddToList_request() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == -6112: - tmp.list_id = unmarshal_int32(ii) - elif fld == 7851: - tmp.ref_id = unmarshal_int64(ii) - elif fld == 25380: - tmp.drf_request = unmarshal_string(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_Authenticate_request(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 4: - raise ProtocolError("incorrect number of fields") - else: - tmp = Authenticate_request() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == -6112: - tmp.list_id = unmarshal_int32(ii) - elif fld == -29279: - tmp.token = unmarshal_binary(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_EnableSettings_request(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 6: - raise ProtocolError("incorrect number of fields") - else: - tmp = EnableSettings_request() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == -6112: - tmp.list_id = unmarshal_int32(ii) - elif fld == 8141: - tmp.MIC = unmarshal_binary(ii) - elif fld == 12546: - tmp.message = unmarshal_binary(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_RemoveFromList_request(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 4: - raise ProtocolError("incorrect number of fields") - else: - tmp = RemoveFromList_request() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == -6112: - tmp.list_id = unmarshal_int32(ii) - elif fld == 7851: - tmp.ref_id = unmarshal_int64(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_StartList_request(ii): - nFlds = consumeRawInt(ii, 0x50) - if (nFlds % 2) != 0 or nFlds < 2 or nFlds > 4: - raise ProtocolError("incorrect number of fields") - else: - tmp = StartList_request() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == -6112: - tmp.list_id = unmarshal_int32(ii) - elif fld == 24163: - tmp.model = unmarshal_string(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_ClearList_request(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 2: - raise ProtocolError("incorrect number of fields") - else: - tmp = ClearList_request() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == -6112: - tmp.list_id = unmarshal_int32(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_StopList_request(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 2: - raise ProtocolError("incorrect number of fields") - else: - tmp = StopList_request() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == -6112: - tmp.list_id = unmarshal_int32(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_ApplySettings_request(ii): - nFlds = consumeRawInt(ii, 0x50) - if (nFlds % 2) != 0 or nFlds < 4 or nFlds > 10: - raise ProtocolError("incorrect number of fields") - else: - tmp = ApplySettings_request() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == 22088: - tmp.user_name = unmarshal_string(ii) - elif fld == -6112: - tmp.list_id = unmarshal_int32(ii) - elif fld == 769: - tmp.raw_array = unmarshal_array(ii, unmarshal_RawSetting_struct) - elif fld == 25550: - tmp.scaled_array = unmarshal_array(ii, unmarshal_ScaledSetting_struct) - elif fld == -12657: - tmp.text_array = unmarshal_array(ii, unmarshal_TextSetting_struct) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_ServiceDiscovery_reply(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 4: - raise ProtocolError("incorrect number of fields") - else: - tmp = ServiceDiscovery_reply() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == 7859: - tmp.load = unmarshal_int16(ii) - elif fld == 4527: - tmp.serviceLocation = unmarshal_string(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_OpenList_reply(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 2: - raise ProtocolError("incorrect number of fields") - else: - tmp = OpenList_reply() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == -6112: - tmp.list_id = unmarshal_int32(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_AddToList_reply(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 6: - raise ProtocolError("incorrect number of fields") - else: - tmp = AddToList_reply() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == -6112: - tmp.list_id = unmarshal_int32(ii) - elif fld == 7851: - tmp.ref_id = unmarshal_int64(ii) - elif fld == 17492: - tmp.status = unmarshal_int16(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_RemoveFromList_reply(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 6: - raise ProtocolError("incorrect number of fields") - else: - tmp = RemoveFromList_reply() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == -6112: - tmp.list_id = unmarshal_int32(ii) - elif fld == 7851: - tmp.ref_id = unmarshal_int64(ii) - elif fld == 17492: - tmp.status = unmarshal_int16(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_StartList_reply(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 4: - raise ProtocolError("incorrect number of fields") - else: - tmp = StartList_reply() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == -6112: - tmp.list_id = unmarshal_int32(ii) - elif fld == 17492: - tmp.status = unmarshal_int16(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_ListStatus_reply(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 4: - raise ProtocolError("incorrect number of fields") - else: - tmp = ListStatus_reply() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == -6112: - tmp.list_id = unmarshal_int32(ii) - elif fld == 17492: - tmp.status = unmarshal_int16(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_Status_reply(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 8: - raise ProtocolError("incorrect number of fields") - else: - tmp = Status_reply() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == 7851: - tmp.ref_id = unmarshal_int64(ii) - elif fld == -10917: - tmp.timestamp = unmarshal_int64(ii) - elif fld == 1856: - tmp.cycle = unmarshal_int64(ii) - elif fld == 17492: - tmp.status = unmarshal_int16(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_DeviceInfo_reply(ii): - nFlds = consumeRawInt(ii, 0x50) - if (nFlds % 2) != 0 or nFlds < 8 or nFlds > 12: - raise ProtocolError("incorrect number of fields") - else: - tmp = DeviceInfo_reply() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == 7851: - tmp.ref_id = unmarshal_int64(ii) - elif fld == -32035: - tmp.di = unmarshal_int32(ii) - elif fld == -27876: - tmp.name = unmarshal_string(ii) - elif fld == -1748: - tmp.description = unmarshal_string(ii) - elif fld == 15867: - tmp.units = unmarshal_string(ii) - elif fld == 32450: - tmp.format_hint = unmarshal_int16(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_Scalar_reply(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 10: - raise ProtocolError("incorrect number of fields") - else: - tmp = Scalar_reply() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == 7851: - tmp.ref_id = unmarshal_int64(ii) - elif fld == -10917: - tmp.timestamp = unmarshal_int64(ii) - elif fld == 1856: - tmp.cycle = unmarshal_int64(ii) - elif fld == 17492: - tmp.status = unmarshal_int16(ii) - elif fld == 32568: - tmp.data = unmarshal_double(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_ScalarArray_reply(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 10: - raise ProtocolError("incorrect number of fields") - else: - tmp = ScalarArray_reply() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == 7851: - tmp.ref_id = unmarshal_int64(ii) - elif fld == -10917: - tmp.timestamp = unmarshal_int64(ii) - elif fld == 1856: - tmp.cycle = unmarshal_int64(ii) - elif fld == 17492: - tmp.status = unmarshal_int16(ii) - elif fld == 32568: - tmp.data = unmarshal_array(ii, unmarshal_double) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_Raw_reply(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 10: - raise ProtocolError("incorrect number of fields") - else: - tmp = Raw_reply() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == 7851: - tmp.ref_id = unmarshal_int64(ii) - elif fld == -10917: - tmp.timestamp = unmarshal_int64(ii) - elif fld == 1856: - tmp.cycle = unmarshal_int64(ii) - elif fld == 17492: - tmp.status = unmarshal_int16(ii) - elif fld == 32568: - tmp.data = unmarshal_binary(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_Text_reply(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 10: - raise ProtocolError("incorrect number of fields") - else: - tmp = Text_reply() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == 7851: - tmp.ref_id = unmarshal_int64(ii) - elif fld == -10917: - tmp.timestamp = unmarshal_int64(ii) - elif fld == 1856: - tmp.cycle = unmarshal_int64(ii) - elif fld == 17492: - tmp.status = unmarshal_int16(ii) - elif fld == 32568: - tmp.data = unmarshal_string(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_TextArray_reply(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 10: - raise ProtocolError("incorrect number of fields") - else: - tmp = TextArray_reply() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == 7851: - tmp.ref_id = unmarshal_int64(ii) - elif fld == -10917: - tmp.timestamp = unmarshal_int64(ii) - elif fld == 1856: - tmp.cycle = unmarshal_int64(ii) - elif fld == 17492: - tmp.status = unmarshal_int16(ii) - elif fld == 32568: - tmp.data = unmarshal_array(ii, unmarshal_string) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_AnalogAlarm_reply(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 22: - raise ProtocolError("incorrect number of fields") - else: - tmp = AnalogAlarm_reply() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == 7851: - tmp.ref_id = unmarshal_int64(ii) - elif fld == -10917: - tmp.timestamp = unmarshal_int64(ii) - elif fld == 1856: - tmp.cycle = unmarshal_int64(ii) - elif fld == 9036: - tmp.minimum = unmarshal_double(ii) - elif fld == -7544: - tmp.maximum = unmarshal_double(ii) - elif fld == -28896: - tmp.alarm_enable = unmarshal_bool(ii) - elif fld == 24630: - tmp.alarm_status = unmarshal_bool(ii) - elif fld == 18972: - tmp.abort = unmarshal_bool(ii) - elif fld == -32398: - tmp.abort_inhibit = unmarshal_bool(ii) - elif fld == 306: - tmp.tries_needed = unmarshal_int32(ii) - elif fld == 11070: - tmp.tries_now = unmarshal_int32(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_DigitalAlarm_reply(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 22: - raise ProtocolError("incorrect number of fields") - else: - tmp = DigitalAlarm_reply() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == 7851: - tmp.ref_id = unmarshal_int64(ii) - elif fld == -10917: - tmp.timestamp = unmarshal_int64(ii) - elif fld == 1856: - tmp.cycle = unmarshal_int64(ii) - elif fld == 20253: - tmp.nominal = unmarshal_int32(ii) - elif fld == 4587: - tmp.mask = unmarshal_int32(ii) - elif fld == -28896: - tmp.alarm_enable = unmarshal_bool(ii) - elif fld == 24630: - tmp.alarm_status = unmarshal_bool(ii) - elif fld == 18972: - tmp.abort = unmarshal_bool(ii) - elif fld == -32398: - tmp.abort_inhibit = unmarshal_bool(ii) - elif fld == 306: - tmp.tries_needed = unmarshal_int32(ii) - elif fld == 11070: - tmp.tries_now = unmarshal_int32(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_BasicStatus_reply(ii): - nFlds = consumeRawInt(ii, 0x50) - if (nFlds % 2) != 0 or nFlds < 6 or nFlds > 16: - raise ProtocolError("incorrect number of fields") - else: - tmp = BasicStatus_reply() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == 7851: - tmp.ref_id = unmarshal_int64(ii) - elif fld == -10917: - tmp.timestamp = unmarshal_int64(ii) - elif fld == 1856: - tmp.cycle = unmarshal_int64(ii) - elif fld == 23553: - tmp.on = unmarshal_bool(ii) - elif fld == -21725: - tmp.ready = unmarshal_bool(ii) - elif fld == -7034: - tmp.remote = unmarshal_bool(ii) - elif fld == 10006: - tmp.positive = unmarshal_bool(ii) - elif fld == -28104: - tmp.ramp = unmarshal_bool(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_TimedScalarArray_reply(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 12: - raise ProtocolError("incorrect number of fields") - else: - tmp = TimedScalarArray_reply() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == 7851: - tmp.ref_id = unmarshal_int64(ii) - elif fld == -10917: - tmp.timestamp = unmarshal_int64(ii) - elif fld == 1856: - tmp.cycle = unmarshal_int64(ii) - elif fld == 17492: - tmp.status = unmarshal_int16(ii) - elif fld == 32568: - tmp.data = unmarshal_array(ii, unmarshal_double) - elif fld == 13357: - tmp.micros = unmarshal_array(ii, unmarshal_int64) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_ApplySettings_reply(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 2: - raise ProtocolError("incorrect number of fields") - else: - tmp = ApplySettings_reply() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == 17492: - tmp.status = unmarshal_array(ii, unmarshal_SettingStatus_struct) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_Authenticate_reply(ii): - nFlds = consumeRawInt(ii, 0x50) - if (nFlds % 2) != 0 or nFlds < 0 or nFlds > 4: - raise ProtocolError("incorrect number of fields") - else: - tmp = Authenticate_reply() - for xx in range(nFlds // 2): - fld = unmarshal_int16(ii) - if fld == 25400: - tmp.serviceName = unmarshal_string(ii) - elif fld == -29279: - tmp.token = unmarshal_binary(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_request(ii): - """Attempts to unmarshal a request message from the specified - generator, ii. If an error occurs, the ProtocolError exception - will be raised.""" - try: - unmarshal_header(ii) - msg = unmarshal_int16(ii) - if msg == -8230: - return unmarshal_ServiceDiscovery_request(ii) - elif msg == 2076: - return unmarshal_OpenList_request(ii) - elif msg == 97: - return unmarshal_AddToList_request(ii) - elif msg == -15277: - return unmarshal_Authenticate_request(ii) - elif msg == 21346: - return unmarshal_EnableSettings_request(ii) - elif msg == -3807: - return unmarshal_RemoveFromList_request(ii) - elif msg == -8779: - return unmarshal_StartList_request(ii) - elif msg == -15607: - return unmarshal_ClearList_request(ii) - elif msg == 21417: - return unmarshal_StopList_request(ii) - elif msg == -14728: - return unmarshal_ApplySettings_request(ii) - else: - raise ProtocolError("unknown request type") - except StopIteration: - raise ProtocolError("unexpected end of input") - -def unmarshal_reply(ii): - """Attempts to unmarshal a reply message from the specified - generator, ii. If an error occurs, the ProtocolError exception - will be raised.""" - try: - unmarshal_header(ii) - msg = unmarshal_int16(ii) - if msg == -12930: - return unmarshal_ServiceDiscovery_reply(ii) - elif msg == 13470: - return unmarshal_OpenList_reply(ii) - elif msg == -29780: - return unmarshal_AddToList_reply(ii) - elif msg == -3046: - return unmarshal_RemoveFromList_reply(ii) - elif msg == -27136: - return unmarshal_StartList_reply(ii) - elif msg == 29552: - return unmarshal_ListStatus_reply(ii) - elif msg == -10579: - return unmarshal_Status_reply(ii) - elif msg == 28653: - return unmarshal_DeviceInfo_reply(ii) - elif msg == -10581: - return unmarshal_Scalar_reply(ii) - elif msg == 23036: - return unmarshal_ScalarArray_reply(ii) - elif msg == -12450: - return unmarshal_Raw_reply(ii) - elif msg == -28878: - return unmarshal_Text_reply(ii) - elif msg == 12923: - return unmarshal_TextArray_reply(ii) - elif msg == 30088: - return unmarshal_AnalogAlarm_reply(ii) - elif msg == -14437: - return unmarshal_DigitalAlarm_reply(ii) - elif msg == -3335: - return unmarshal_BasicStatus_reply(ii) - elif msg == 31282: - return unmarshal_TimedScalarArray_reply(ii) - elif msg == 17344: - return unmarshal_ApplySettings_reply(ii) - elif msg == 7286: - return unmarshal_Authenticate_reply(ii) - else: - raise ProtocolError("unknown reply type") - except StopIteration: - raise ProtocolError("unexpected end of input") diff --git a/acsys/scaling/__init__.py b/acsys/scaling/__init__.py deleted file mode 100644 index 36d5398..0000000 --- a/acsys/scaling/__init__.py +++ /dev/null @@ -1,86 +0,0 @@ -import logging -import acsys.status -from acsys.scaling.scaling_protocol import (ServiceDiscovery_request, - Scale_request) - -_log = logging.getLogger(__name__) - - -async def find_service(con, node=None): - """Use Service Discovery to find an available SCALE service. - """ - - task = f'SCALE@{node or "MCAST"}' - msg = ServiceDiscovery_request() - try: - replier, _ = await con.request_reply( - task, msg, timeout=150, proto=acsys.scaling.scaling_protocol) - node = await con.get_name(replier) - _log.debug('found SCALE service at node %s', node) - return node - except acsys.status.AcnetRequestTimeOutQueuedAtDestination as exception: - raise acsys.status.AcnetNoSuchRequestOrReply() from exception - except acsys.status.AcnetUserGeneratedNetworkTimeout: - return None - - -async def convert_data(con, drf, data, node=None): - """Returns converted data. - -'con' is an acsys.Connection object. 'drf' is the DRF request that -describes what conversion is desired. If the DRF string contains -".RAW", it is assumed 'data' is an array of floating point values -which will be unscaled into a binary buffer. If the DRF string -specifies a scaled request, then 'data' is assumed to be a binary -buffer that needs to be scaled. - -The scaling service will use the length fields in the database to -determine how the binary data is formatted. - -The return value is an array of floats, if scaling from a binary -buffer, or a binary buffer, if unscaling a list of floats.. - -If no SCALE services are found, this function raises -`acsys.status.ACNET_NO_NODE`. A slow SCALE service could also result -in `acsys.status.ACNET_UTIME` being raised. - - """ - - # Verify parameters. - - assert isinstance(con, acsys.Connection) - assert isinstance(drf, str) - assert isinstance(data, (bytes, bytearray, list, float, int)) - - # Build request. - - msg = Scale_request() - msg.drf_request = drf - - if isinstance(data, (float, int)): - data = [float(data)] - - if isinstance(data, list): - _log.debug('converting %s scaled data, %s, to unscaled', drf, data) - msg.scaled = data - else: - _log.debug('converting %s unscaled data, %s, to scaled', drf, data) - msg.raw = data - - if node is None: - node = await find_service(con) - if node is None: - raise acsys.status.AcnetNoSuchLogicalNode() - - _, reply = await con.request_reply(f'SCALE@{node}', msg, timeout=1000, - proto=acsys.scaling.scaling_protocol) - - status = acsys.status.Status.create(reply.status) - - if status.is_success: - if hasattr(reply, 'raw'): - return reply.raw - - return reply.scaled - - raise status diff --git a/acsys/scaling/scaling_protocol.py b/acsys/scaling/scaling_protocol.py deleted file mode 100644 index ceb4a66..0000000 --- a/acsys/scaling/scaling_protocol.py +++ /dev/null @@ -1,323 +0,0 @@ -# Generated by the protocol compiler version 1.3.8 -# DO NOT EDIT THIS FILE DIRECTLY! - -__doc__ = 'Message serializer for the Scaling protocol.' - -from itertools import chain, islice - -__all__ = ['ProtocolError', - 'unmarshal_request', - 'unmarshal_reply', - 'ServiceDiscovery_request', - 'Scale_request', - 'ServiceDiscovery_reply', - 'Scale_reply'] - -import struct - -class ProtocolError(Exception): - """Exception class that gets raised when there's a problem marshaling - or unmarshaling a message from the Scaling protocol.""" - - def __init__(self, reason): - self.reason = reason - - def __str__(self): - return repr(self.reason) - -# -- Internal marshalling routines -- - -def emitRawInt(tag, val): - def emitEach(buf, n): - curr = (val >> (n * 8)) & 0xff - next = val >> ((n + 1) * 8) - if (next == 0 and (curr & 0x80) != 0x80) or \ - (next == -1 and (curr & 0x80) == 0x80): - buf.append(tag + n + 1) - else: - emitEach(buf, n + 1) - buf.append(curr) - tmp = bytearray() - emitEach(tmp, 0) - return bytes(tmp) - -def marshal_int16(val): - if isinstance(val, int): - if val < 32768 and val > -32769: - return emitRawInt(0x10, val) - else: - raise ProtocolError("value out of range for int16") - else: - raise ProtocolError("expected integer type") - -def marshal_int32(val): - if isinstance(val, int): - if int(-2147483648) <= val <= int(2147483647): - return emitRawInt(0x10, val) - else: - raise ProtocolError("value out of range for int32") - else: - raise ProtocolError("expected integer type") - -def marshal_double(val): - return chain(b'\x28', (ii for ii in struct.pack(">d", float(val)))) - -def marshal_string(val): - if isinstance(val, str): - return chain(emitRawInt(0x40, len(val)),\ - (ord(ii) for ii in val)) - else: - raise ProtocolError("expected string type") - -def marshal_binary(val): - if isinstance(val, (bytearray, bytes)): - return chain(emitRawInt(0x30, len(val)), val) - else: - raise ProtocolError("expected bytearray type") - -def marshal_array(fn, val): - if isinstance(val, list): - return chain(emitRawInt(0x50, len(val)),\ - chain.from_iterable((fn(v) for v in val))) - else: - raise ProtocolError("expected list type") - -class ServiceDiscovery_request: - def __eq__(self, other): - return True - - def __ne__(self, other): - return False - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of ServiceDiscovery_request.""" - return chain(b'SDD\x02\x51\x03\x14\xbc\x96\x7d\xc2\x12\xdf\xda\x51\x00') - -class Scale_request: - def __init__(self): - self.drf_request = '' - - def __eq__(self, other): - return self.drf_request == other.drf_request and \ - ((not hasattr(self, 'raw') and not hasattr(other, 'raw')) or \ - (hasattr(self, 'raw') and hasattr(other, 'raw') and \ - self.raw == other.raw)) and \ - ((not hasattr(self, 'scaled') and not hasattr(other, 'scaled')) or \ - (hasattr(self, 'scaled') and hasattr(other, 'scaled') and \ - self.scaled == other.scaled)) - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of Scale_request.""" - return chain(b'SDD\x02\x51\x03\x14\xbc\x96\x7d\xc2\x12\xbe\xf8', - emitRawInt(0x50, 2 \ - + (2 if hasattr(self, 'raw') else 0) \ - + (2 if hasattr(self, 'scaled') else 0)), - b'\x12\x63\x24', - marshal_string(self.drf_request), - chain(b'\x12\x66\xaf', - marshal_binary(self.raw)) \ - if hasattr(self, 'raw') else bytearray(), - chain(b'\x12\xa9\x9b', - marshal_array(marshal_double, self.scaled)) \ - if hasattr(self, 'scaled') else bytearray()) - -class ServiceDiscovery_reply: - def __init__(self): - self.serviceLocation = '' - - def __eq__(self, other): - return self.serviceLocation == other.serviceLocation - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of ServiceDiscovery_reply.""" - return chain(b'SDD\x02\x51\x03\x14\xbc\x96\x7d\xc2\x12\xcd\x7e\x51\x02\x12\x11\xaf', - marshal_string(self.serviceLocation)) - -class Scale_reply: - def __init__(self): - self.status = int(0) - - def __eq__(self, other): - return self.status == other.status and \ - ((not hasattr(self, 'raw') and not hasattr(other, 'raw')) or \ - (hasattr(self, 'raw') and hasattr(other, 'raw') and \ - self.raw == other.raw)) and \ - ((not hasattr(self, 'scaled') and not hasattr(other, 'scaled')) or \ - (hasattr(self, 'scaled') and hasattr(other, 'scaled') and \ - self.scaled == other.scaled)) - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of Scale_reply.""" - return chain(b'SDD\x02\x51\x03\x14\xbc\x96\x7d\xc2\x12\x44\xf9', - emitRawInt(0x50, 2 \ - + (2 if hasattr(self, 'raw') else 0) \ - + (2 if hasattr(self, 'scaled') else 0)), - b'\x12\x44\x54', - marshal_int16(self.status), - chain(b'\x12\x66\xaf', - marshal_binary(self.raw)) \ - if hasattr(self, 'raw') else bytearray(), - chain(b'\x12\xa9\x9b', - marshal_array(marshal_double, self.scaled)) \ - if hasattr(self, 'scaled') else bytearray()) - -def marshal_request(val): - return val.marshal() - -def marshal_reply(val): - return val.marshal() - -# -- Internal unmarshalling routines -- - -def consumeRawInt(ii, tag): - iiTag = (ii.__next__()) - iiLen = iiTag & 0xf - if (iiTag & 0xf0) == (tag & 0xf0) and iiLen > 0 and iiLen <= 8: - firstByte = (ii.__next__()) - retVal = (0 if (0x80 & firstByte) == 0 else -256) | firstByte - while iiLen > 1: - retVal = (retVal << 8) | (ii.__next__()) - iiLen = iiLen - 1 - return int(retVal) - else: - raise ProtocolError("bad tag or length") - -def unmarshal_int16(ii): - val = consumeRawInt(ii, 0x10) - if val >= -0x8000 and val < 0x8000: - return int(val) - else: - raise ProtocolError("value out of range for int16") - -def unmarshal_int32(ii): - val = consumeRawInt(ii, 0x10) - if int(-2147483648) <= val <= int(2147483647): - return int(val) - else: - raise ProtocolError("value out of range for int32") - -def unmarshal_double(ii): - if ii.__next__() == 40: - raw = bytearray(islice(ii, 8)) - v, = struct.unpack(">d", raw) - return v - else: - raise ProtocolError("expected tag for double") - -def unmarshal_string(ii): - return bytearray(islice(ii, consumeRawInt(ii, 0x40))).decode('utf-8') - -def unmarshal_binary(ii): - return bytearray(islice(ii, consumeRawInt(ii, 0x30))) - -def unmarshal_array(ii, fn): - return [fn(ii) for x in range(consumeRawInt(ii, 0x50))] - -def unmarshal_header(ii): - if ii.__next__() != 83 or ii.__next__() != 68 or \ - ii.__next__() != 68 or ii.__next__() != 2 or \ - consumeRawInt(ii, 0x50) != 3: - raise ProtocolError("invalid header") - elif consumeRawInt(ii, 0x10) != -1130988094: - raise ProtocolError("incorrect protocol specified") - -def unmarshal_ServiceDiscovery_request(ii): - if consumeRawInt(ii, 0x50) != 0: - raise ProtocolError("incorrect number of fields") - else: - return ServiceDiscovery_request() - -def unmarshal_Scale_request(ii): - nFlds = consumeRawInt(ii, 0x50) - if (nFlds % 2) != 0 or nFlds < 2 or nFlds > 6: - raise ProtocolError("incorrect number of fields") - else: - tmp = Scale_request() - for xx in range(nFlds // 2): - fld = consumeRawInt(ii, 0x10) - if fld == 25380: - tmp.drf_request = unmarshal_string(ii) - elif fld == 26287: - tmp.raw = unmarshal_binary(ii) - elif fld == -22117: - tmp.scaled = unmarshal_array(ii, unmarshal_double) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_ServiceDiscovery_reply(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 2: - raise ProtocolError("incorrect number of fields") - else: - tmp = ServiceDiscovery_reply() - for xx in range(nFlds // 2): - fld = consumeRawInt(ii, 0x10) - if fld == 4527: - tmp.serviceLocation = unmarshal_string(ii) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_Scale_reply(ii): - nFlds = consumeRawInt(ii, 0x50) - if (nFlds % 2) != 0 or nFlds < 2 or nFlds > 6: - raise ProtocolError("incorrect number of fields") - else: - tmp = Scale_reply() - for xx in range(nFlds // 2): - fld = consumeRawInt(ii, 0x10) - if fld == 17492: - tmp.status = unmarshal_int16(ii) - elif fld == 26287: - tmp.raw = unmarshal_binary(ii) - elif fld == -22117: - tmp.scaled = unmarshal_array(ii, unmarshal_double) - else: - raise ProtocolError("unknown field found") - return tmp - -def unmarshal_request(ii): - """Attempts to unmarshal a request message from the specified - generator, ii. If an error occurs, the ProtocolError exception - will be raised.""" - try: - unmarshal_header(ii) - msg = consumeRawInt(ii, 0x10) - if msg == -8230: - return unmarshal_ServiceDiscovery_request(ii) - elif msg == -16648: - return unmarshal_Scale_request(ii) - else: - raise ProtocolError("unknown request type") - except StopIteration: - raise ProtocolError("unexpected end of input") - -def unmarshal_reply(ii): - """Attempts to unmarshal a reply message from the specified - generator, ii. If an error occurs, the ProtocolError exception - will be raised.""" - try: - unmarshal_header(ii) - msg = consumeRawInt(ii, 0x10) - if msg == -12930: - return unmarshal_ServiceDiscovery_reply(ii) - elif msg == 17657: - return unmarshal_Scale_reply(ii) - else: - raise ProtocolError("unknown reply type") - except StopIteration: - raise ProtocolError("unexpected end of input") diff --git a/acsys/status.py b/acsys/status.py deleted file mode 100644 index bc63b2c..0000000 --- a/acsys/status.py +++ /dev/null @@ -1,515 +0,0 @@ -from abc import abstractmethod, ABC -from enum import IntEnum - -class Status(ABC): - """An ACSys status type.""" - class Codes(IntEnum): - """ACNET common status codes""" - ACNET_SUCCESS = 1 + 256 * 0 - """ACNET success code""" - ACNET_PEND = 1 + 256 * 1 - """operation is pending""" - ACNET_ENDMULT = 1 + 256 * 2 - """end multiple replies (on final reply)""" - ACNET_RETRY = 1 + 256 * -1 - """retryable I/O error""" - ACNET_NOLCLMEM = 1 + 256 * -2 - """no local memory available""" - ACNET_NOREMMEM = 1 + 256 * -3 - """no remote memory available""" - ACNET_RPLYPACK = 1 + 256 * -4 - """reply message packet assembly error""" - ACNET_REQPACK = 1 + 256 * -5 - """request message packet assembly error""" - ACNET_REQTMO = 1 + 256 * -6 - """request timeout with queued at destination""" - ACNET_QUEFULL = 1 + 256 * -7 - """request failed, destination queue full""" - ACNET_BUSY = 1 + 256 * -8 - """request failed, destination task busy""" - ACNET_NOT_CONNECTED = 1 + 256 * -21 - """not connected to the network""" - ACNET_ARG = 1 + 256 * -22 - """missing arguments""" - ACNET_IVM = 1 + 256 * -23 - """invalid message length or buffer address""" - ACNET_NO_SUCH = 1 + 256 * -24 - """no such request or reply""" - ACNET_REQREJ = 1 + 256 * -25 - """request to destination task was rejected""" - ACNET_CANCELLED = 1 + 256 * -26 - """request has been cancelled""" - ACNET_NAME_IN_USE = 1 + 256 * -27 - """task name already in use""" - ACNET_NCR = 1 + 256 * -28 - """not connected as a RUM task""" - ACNET_NO_NODE = 1 + 256 * -30 - """no such logical node""" - ACNET_TRUNC_REQUEST = 1 + 256 * -31 - """truncated request""" - ACNET_TRUNC_REPLY = 1 + 256 * -32 - """truncated reply""" - ACNET_NO_TASK = 1 + 256 * -33 - """no such destination task""" - ACNET_DISCONNECTED = 1 + 256 * -34 - """replier task being disconnected""" - ACNET_LEVEL2 = 1 + 256 * -35 - """ACNET level II function error""" - ACNET_HARD_IO = 1 + 256 * -41 - """hard I/O error""" - ACNET_NODE_DOWN = 1 + 256 * -42 - """logical node down or offline""" - ACNET_SYS = 1 + 256 * -43 - """system service error""" - ACNET_NXE = 1 + 256 * -44 - """untranslatable error""" - ACNET_BUG = 1 + 256 * -45 - """network internal error""" - ACNET_NE1 = 1 + 256 * -46 - """VMS exceeded some quota or limit""" - ACNET_NE2 = 1 + 256 * -47 - """VMS no address for request/reply ID word""" - ACNET_NE3 = 1 + 256 * -48 - """VMS buffer/control block vector in use or block already locked""" - ACNET_UTIME = 1 + 256 * -49 - """user-generated network timeout""" - ACNET_INVARG = 1 + 256 * -50 - """invalid argument passed""" - ACNET_MEMFAIL = 1 + 256 * -51 - """memory allocation failed""" - ACNET_NO_HANDLE = 1 + 256 * -52 - """requested handle doesn't exist""" - - @abstractmethod - def __init__(self) -> None: - """ - Use Status.create(val), or specific AcnetXXX() class instead. - """ - pass - - def _set_val(self, val): - """Set's supplied value, which must be in the range of signed, 16-bit - integers. - Do not use this method directly, use Status.create(val), or - specific AcnetXXX() class instead. - """ - if -0x8000 < val <= 0x7fff: - self.value = val - else: - raise ValueError('raw status values are 16-bit, signed integers') - - @property - def facility(self): - """Returns the 'facility' code of a status value.""" - return self.value & 255 - - @property - def err_code(self): - """Returns the 'error' code of a status value.""" - return self.value // 256 - - @property - def is_success(self): - """Returns True if the status represents a success status.""" - return self.err_code == 0 - - @property - def is_fatal(self): - """Returns True if the status represents a fatal status.""" - return self.err_code < 0 - - @property - def is_warning(self): - """Returns True if the status represents a warning status.""" - return self.err_code > 0 - - def __eq__(self, other): - return self.value == other.value \ - if isinstance(other, Status) else False - - def __ne__(self, other): - return self.value != other.value \ - if isinstance(other, Status) else True - - def __str__(self): - return f'[{self.facility} {self.err_code}]' - - @staticmethod - def create(val): - """ - Factory method to build ACNET Status codes. If given an error - code, it will build a raisable (AcnetException) object. - """ - # In case zero is given as a value / zero facility code - if val == 0: - st = _SuccessStatus() - st._set_val(Status.Codes.ACNET_SUCCESS) - return st - # Non-error codes - elif val == Status.Codes.ACNET_SUCCESS or \ - val == Status.Codes.ACNET_PEND or \ - val == Status.Codes.ACNET_ENDMULT: - st = _SuccessStatus() - st._set_val(val) - return st - # Error codes build an exception - elif val == Status.Codes.ACNET_RETRY: - return AcnetRetryIOError() - elif val == Status.Codes.ACNET_NOLCLMEM: - return AcnetNoLocalMemory() - elif val == Status.Codes.ACNET_NOREMMEM: - return AcnetNoRemoteMemory() - elif val == Status.Codes.ACNET_RPLYPACK: - return AcnetReplyMessagePacketAssemblyError() - elif val == Status.Codes.ACNET_REQPACK: - return AcnetRequestMessagePacketAssemblyError() - elif val == Status.Codes.ACNET_REQTMO: - return AcnetRequestTimeOutQueuedAtDestination() - elif val == Status.Codes.ACNET_QUEFULL: - return AcnetDestinationQueueFull() - elif val == Status.Codes.ACNET_BUSY: - return AcnetDestinationBusy() - elif val == Status.Codes.ACNET_NOT_CONNECTED: - return AcnetNotConnected() - elif val == Status.Codes.ACNET_ARG: - return AcnetMissingArguments() - elif val == Status.Codes.ACNET_IVM: - return AcnetInvalidMessageLengthOrBufferAddress() - elif val == Status.Codes.ACNET_NO_SUCH: - return AcnetNoSuchRequestOrReply() - elif val == Status.Codes.ACNET_REQREJ: - return RequestToDestinationTaskRejected() - elif val == Status.Codes.ACNET_CANCELLED: - return RequestedCancelled() - elif val == Status.Codes.ACNET_NAME_IN_USE: - return AcnetNameAlreadyInUse() - elif val == Status.Codes.ACNET_NCR: - return AcnetNotConnectedAsRumTask() - elif val == Status.Codes.ACNET_NO_NODE: - return AcnetNoSuchLogicalNode() - elif val == Status.Codes.ACNET_TRUNC_REQUEST: - return AcnetTruncatedRequest() - elif val == Status.Codes.ACNET_TRUNC_REPLY: - return AcnetTruncatedReply() - elif val == Status.Codes.ACNET_NO_TASK: - return AcnetNoSuchDestinationTask() - elif val == Status.Codes.ACNET_DISCONNECTED: - return AcnetReplyTaskDisconnected() - elif val == Status.Codes.ACNET_LEVEL2: - return AcnetLevel2FunctionError() - elif val == Status.Codes.ACNET_HARD_IO: - return AcnetHardIOError() - elif val == Status.Codes.ACNET_NODE_DOWN: - return AcnetLogicalNodeDownOffline() - elif val == Status.Codes.ACNET_SYS: - return AcnetSystemServiceError() - elif val == Status.Codes.ACNET_NXE: - return AcnetUntranslatableError() - elif val == Status.Codes.ACNET_BUG: - return AcnetNetworkInternalError() - elif val == Status.Codes.ACNET_NE1: - return AcnetNE1_VMSExceededQuota() - elif val == Status.Codes.ACNET_NE2: - return AcnetNE2_VMSNoAdressForRequestOrReply() - elif val == Status.Codes.ACNET_NE3: - return AcnetNE3_VMSBufferInUse() - elif val == Status.Codes.ACNET_UTIME: - return AcnetUserGeneratedNetworkTimeout() - elif val == Status.Codes.ACNET_INVARG: - return AcnetInvalidArgumentPassed() - elif val == Status.Codes.ACNET_MEMFAIL: - return AcnetMemoryAllocationFailed() - elif val == Status.Codes.ACNET_NO_HANDLE: - return AcnetNoRequestHandle() - else: - raise ValueError(f"Invalid ACNET Status code: {val}") - -class _SuccessStatus(Status): - def __init__(self) -> None: - pass - -# Clases that specialize Status - based on code, that can be captured on different except clauses -class AcnetException(Status, Exception): - """Use this to capture error statuses""" - pass - -class AcnetRetryIOError(AcnetException): - """retryable I/O error""" - def __init__(self): - self._set_val(Status.Codes.ACNET_RETRY) - Exception.__init__(self) - -class AcnetNoLocalMemory(AcnetException): - """no local memory available""" - def __init__(self): - self._set_val(Status.Codes.ACNET_NOLCLMEM) - Exception.__init__(self) - -class AcnetNoRemoteMemory(AcnetException): - """no remote memory available""" - def __init__(self): - self._set_val(Status.Codes.ACNET_NOREMMEM) - Exception.__init__(self) - -class AcnetReplyMessagePacketAssemblyError(AcnetException): - """reply message packet assembly error""" - def __init__(self): - self._set_val(Status.Codes.ACNET_RPLYPACK) - Exception.__init__(self) - -class AcnetRequestMessagePacketAssemblyError(AcnetException): - """request message packet assembly error""" - def __init__(self): - self._set_val(Status.Codes.ACNET_REQPACK) - Exception.__init__(self) - -class AcnetRequestTimeOutQueuedAtDestination(AcnetException): - """request timeout with queued at destination""" - def __init__(self): - self._set_val(Status.Codes.ACNET_REQTMO) - Exception.__init__(self) - -class AcnetDestinationQueueFull(AcnetException): - """request failed, destination queue full""" - def __init__(self): - self._set_val(Status.Codes.ACNET_QUEFULL) - Exception.__init__(self) - -class AcnetDestinationBusy(AcnetException): - """request failed, destination task busy""" - def __init__(self): - self._set_val(Status.Codes.ACNET_BUSY) - Exception.__init__(self) - -class AcnetNotConnected(AcnetException): - """not connected to the network""" - def __init__(self): - self._set_val(Status.Codes.ACNET_NOT_CONNECTED) - Exception.__init__(self) - -class AcnetMissingArguments(AcnetException): - """missing arguments""" - def __init__(self): - self._set_val(Status.Codes.ACNET_ARG) - Exception.__init__(self) - -class AcnetInvalidMessageLengthOrBufferAddress(AcnetException): - """invalid message length or buffer address""" - def __init__(self): - self._set_val(Status.Codes.ACNET_IVM) - Exception.__init__(self) - -class AcnetNoSuchRequestOrReply(AcnetException): - """no such request or reply""" - def __init__(self): - self._set_val(Status.Codes.ACNET_NO_SUCH) - Exception.__init__(self) - -class RequestToDestinationTaskRejected(AcnetException): - """request to destination task was rejected""" - def __init__(self): - self._set_val(Status.Codes.ACNET_REQREJ) - Exception.__init__(self) - -class RequestedCancelled(AcnetException): - """request has been cancelled""" - def __init__(self): - self._set_val(Status.Codes.ACNET_CANCELLED) - Exception.__init__(self) - -class AcnetNameAlreadyInUse(AcnetException): - """task name already in use""" - def __init__(self): - self._set_val(Status.Codes.ACNET_NAME_IN_USE) - Exception.__init__(self) - -class AcnetNotConnectedAsRumTask(AcnetException): - """not connected as a RUM task""" - def __init__(self): - self._set_val(Status.Codes.ACNET_NCR) - Exception.__init__(self) - -class AcnetNoSuchLogicalNode(AcnetException): - """no such logical node""" - def __init__(self): - self._set_val(Status.Codes.ACNET_NO_NODE) - Exception.__init__(self) - -class AcnetTruncatedRequest(AcnetException): - """truncated request""" - def __init__(self): - self._set_val(Status.Codes.ACNET_TRUNC_REQUEST) - Exception.__init__(self) - -class AcnetTruncatedReply(AcnetException): - """truncated reply""" - def __init__(self): - self._set_val(Status.Codes.ACNET_TRUNC_REPLY) - Exception.__init__(self) - -class AcnetNoSuchDestinationTask(AcnetException): - """no such destination task""" - def __init__(self): - self._set_val(Status.Codes.ACNET_NO_TASK) - Exception.__init__(self) - -class AcnetReplyTaskDisconnected(AcnetException): - """replier task being disconnected""" - def __init__(self): - self._set_val(Status.Codes.ACNET_DISCONNECTED) - Exception.__init__(self) - -class AcnetLevel2FunctionError(AcnetException): - """ACNET level II function error""" - def __init__(self): - self._set_val(Status.Codes.ACNET_LEVEL2) - Exception.__init__(self) - -class AcnetHardIOError(AcnetException): - """hard I/O error""" - def __init__(self): - self._set_val(Status.Codes.ACNET_HARD_IO) - Exception.__init__(self) - -class AcnetLogicalNodeDownOffline(AcnetException): - """logical node down or offline""" - def __init__(self): - self._set_val(Status.Codes.ACNET_NODE_DOWN) - Exception.__init__(self) - -class AcnetSystemServiceError(AcnetException): - """system service error""" - def __init__(self): - self._set_val(Status.Codes.ACNET_SYS) - Exception.__init__(self) - -class AcnetUntranslatableError(AcnetException): - """untranslatable error""" - def __init__(self): - self._set_val(Status.Codes.ACNET_NXE) - Exception.__init__(self) - -class AcnetNetworkInternalError(AcnetException): - """network internal error""" - def __init__(self): - self._set_val(Status.Codes.ACNET_BUG) - Exception.__init__(self) - -class AcnetNE1_VMSExceededQuota(AcnetException): - """VMS exceeded some quota or limit""" - def __init__(self): - self._set_val(Status.Codes.ACNET_NE1) - Exception.__init__(self) - -class AcnetNE2_VMSNoAdressForRequestOrReply(AcnetException): - """VMS no address for request/reply ID word""" - def __init__(self): - self._set_val(Status.Codes.ACNET_NE2) - Exception.__init__(self) - -class AcnetNE3_VMSBufferInUse(AcnetException): - """VMS buffer/control block vector in use or block already locked""" - def __init__(self): - self._set_val(Status.Codes.ACNET_NE3) - Exception.__init__(self) - -class AcnetUserGeneratedNetworkTimeout(AcnetException): - """user-generated network timeout""" - def __init__(self): - self._set_val(Status.Codes.ACNET_UTIME) - Exception.__init__(self) - -class AcnetInvalidArgumentPassed(AcnetException): - """invalid argument passed""" - def __init__(self): - self._set_val(Status.Codes.ACNET_INVARG) - Exception.__init__(self) - -class AcnetMemoryAllocationFailed(AcnetException): - """memory allocation failed""" - def __init__(self): - self._set_val(Status.Codes.ACNET_MEMFAIL) - Exception.__init__(self) - -class AcnetNoRequestHandle(AcnetException): - """requested handle doesn't exist""" - def __init__(self): - self._set_val(Status.Codes.ACNET_NO_HANDLE) - Exception.__init__(self) - -# Objects that represent an instance of each status, so they can be compared -# inside an except clause. -ACNET_SUCCESS = Status.create(Status.Codes.ACNET_SUCCESS) -ACNET_PEND = Status.create(Status.Codes.ACNET_PEND) -ACNET_ENDMULT = Status.create(Status.Codes.ACNET_ENDMULT) - -# TODO these globals can be used but are redundant. -# Should be deleted on subsequent versions -ACNET_RETRY = Status.create(Status.Codes.ACNET_RETRY) -""" @deprecated: Use AcnetRetryIOError() instead """ -ACNET_NOLCLMEM = Status.create(Status.Codes.ACNET_NOLCLMEM) -""" @deprecated: Use AcnetNoLocalMemory() instead """ -ACNET_NOREMMEM = Status.create(Status.Codes.ACNET_NOREMMEM) -""" @deprecated: Use AcnetNoRemoteMemory() instead """ -ACNET_RPLYPACK = Status.create(Status.Codes.ACNET_RPLYPACK) -""" @deprecated: Use AcnetReplyMessagePacketAssemblyError() instead """ -ACNET_REQPACK = Status.create(Status.Codes.ACNET_REQPACK) -""" @deprecated: Use AcnetRequestMessagePacketAssemblyError() instead """ -ACNET_REQTMO = Status.create(Status.Codes.ACNET_REQTMO) -""" @deprecated: Use AcnetRequestTimeOutQueuedAtDestination() instead """ -ACNET_QUEFULL = Status.create(Status.Codes.ACNET_QUEFULL) -""" @deprecated: Use AcnetDestinationQueueFull() instead """ -ACNET_BUSY = Status.create(Status.Codes.ACNET_BUSY) -""" @deprecated: Use AcnetDestinationBusy() instead """ -ACNET_NOT_CONNECTED = Status.create(Status.Codes.ACNET_NOT_CONNECTED) -""" @deprecated: Use AcnetNotConnected() instead """ -ACNET_ARG = Status.create(Status.Codes.ACNET_ARG) -""" @deprecated: Use AcnetMissingArguments() instead """ -ACNET_IVM = Status.create(Status.Codes.ACNET_IVM) -""" @deprecated: Use AcnetInvalidMessageLengthOrBufferAddress() instead """ -ACNET_NO_SUCH = Status.create(Status.Codes.ACNET_NO_SUCH) -""" @deprecated: Use AcnetNoSuchRequestOrReply() instead """ -ACNET_REQREJ = Status.create(Status.Codes.ACNET_REQREJ) -""" @deprecated: Use RequestToDestinationTaskRejected() instead """ -ACNET_CANCELLED = Status.create(Status.Codes.ACNET_CANCELLED) -""" @deprecated: Use RequestedCancelled() instead """ -ACNET_NAME_IN_USE = Status.create(Status.Codes.ACNET_NAME_IN_USE) -""" @deprecated: Use AcnetNameAlreadyInUse() instead """ -ACNET_NCR = Status.create(Status.Codes.ACNET_NCR) -""" @deprecated: Use AcnetNotConnectedAsRumTask() instead """ -ACNET_NO_NODE = Status.create(Status.Codes.ACNET_NO_NODE) -""" @deprecated: Use AcnetNoSuchLogicalNode() instead """ -ACNET_TRUNC_REQUEST = Status.create(Status.Codes.ACNET_TRUNC_REQUEST) -""" @deprecated: Use AcnetTruncatedRequest() instead """ -ACNET_TRUNC_REPLY = Status.create(Status.Codes.ACNET_TRUNC_REPLY) -""" @deprecated: Use AcnetTruncatedReply() instead """ -ACNET_NO_TASK = Status.create(Status.Codes.ACNET_NO_TASK) -""" @deprecated: Use AcnetNoSuchDestinationTask() instead """ -ACNET_DISCONNECTED = Status.create(Status.Codes.ACNET_DISCONNECTED) -""" @deprecated: Use AcnetReplyTaskDisconnected() instead """ -ACNET_LEVEL2 = Status.create(Status.Codes.ACNET_LEVEL2) -""" @deprecated: Use AcnetLevel2FunctionError() instead """ -ACNET_HARD_IO = Status.create(Status.Codes.ACNET_HARD_IO) -""" @deprecated: Use AcnetHardIOError() instead """ -ACNET_NODE_DOWN = Status.create(Status.Codes.ACNET_NODE_DOWN) -""" @deprecated: Use AcnetLogicalNodeDownOffline() instead """ -ACNET_SYS = Status.create(Status.Codes.ACNET_SYS) -""" @deprecated: Use AcnetSystemServiceError() instead """ -ACNET_NXE = Status.create(Status.Codes.ACNET_NXE) -""" @deprecated: Use AcnetUntranslatableError() instead """ -ACNET_BUG = Status.create(Status.Codes.ACNET_BUG) -""" @deprecated: Use AcnetNetworkInternalError() instead """ -ACNET_NE1 = Status.create(Status.Codes.ACNET_NE1) -""" @deprecated: Use AcnetNE1_VMSExceededQuota() instead """ -ACNET_NE2 = Status.create(Status.Codes.ACNET_NE2) -""" @deprecated: Use AcnetNE2_VMSNoAdressForRequestOrReply() instead """ -ACNET_NE3 = Status.create(Status.Codes.ACNET_NE3) -""" @deprecated: Use AcnetNE3_VMSBufferInUse() instead """ -ACNET_UTIME = Status.create(Status.Codes.ACNET_UTIME) -""" @deprecated: Use AcnetUserGeneratedNetworkTimeout() instead """ -ACNET_INVARG = Status.create(Status.Codes.ACNET_INVARG) -""" @deprecated: Use AcnetInvalidArgumentPassed() instead """ -ACNET_MEMFAIL = Status.create(Status.Codes.ACNET_MEMFAIL) -""" @deprecated: Use AcnetMemoryAllocationFailed() instead """ -ACNET_NO_HANDLE = Status.create(Status.Codes.ACNET_NO_HANDLE) -""" @deprecated: Use AcnetNoRequestHandle() instead """ diff --git a/acsys/sync/__init__.py b/acsys/sync/__init__.py deleted file mode 100644 index 3f1f6d4..0000000 --- a/acsys/sync/__init__.py +++ /dev/null @@ -1,155 +0,0 @@ -import datetime -import asyncio -import logging -import acsys.status -from acsys.sync.syncd_protocol import (Clock_Tclk, Discover_request, - Register_request, Report_reply) - -_log = logging.getLogger(__name__) - - -class ClockEvent: - """Simple class that holds clock event information.""" - - def __init__(self, stamp, event, number): - self._stamp = stamp - self._event = event - self._number = number - - @property - def stamp(self): - """Returns the timestamp of the clock event.""" - return self._stamp - - @property - def event(self): - """Returns the event number.""" - return self._event - - @property - def number(self): - """Returns the instance number of the event.""" - return self._number - - def __str__(self): - return f'' - - -class StateEvent: - """Simple class that holds state event information.""" - - def __init__(self, stamp, device_index, value): - self._stamp = stamp - self._device_index = device_index - self._value = value - - @property - def stamp(self): - """Returns the timestamp of the state event.""" - return self._stamp - - @property - def device_index(self): - """Returns the device index assicated with the state event.""" - return self._device_index - - @property - def value(self): - """Returns the state device value that caused the event to fire.""" - return self._value - - def __str__(self): - return f'' - - -async def find_service(con, clock=Clock_Tclk, node=None): - """Use Service Discovery to find an available SYNCD service. - - """ - - task = f'SYNC@{node or "MCAST"}' - msg = Discover_request() - msg.clock = clock - try: - replier, _ = await con.request_reply(task, msg, timeout=150, - proto=acsys.sync.syncd_protocol) - return await con.get_name(replier) - except acsys.status.AcnetUserGeneratedNetworkTimeout: - return None - - -async def get_available(con, clock=Clock_Tclk): - """Find active SYNCD services. - -Returns a list containing ACNET nodes tha support the SYNC service. If -no nodes are found, an empty list is returned. - - """ - result = [] - msg = Discover_request() - msg.clock = clock - gen = con.request_stream( - 'SYNC@MCAST', msg, proto=acsys.sync.syncd_protocol, timeout=150) - try: - async for replier, _ in gen: - result.append(await con.get_name(replier)) - except acsys.status.AcnetUserGeneratedNetworkTimeout: - return result - - - -async def get_events(con, ev_str, sync_node=None, clock=Clock_Tclk): - """Returns an async generator that yields event information. - -'con' is an acsys.Connection object. 'ev_str' is a list of -strings. Each string describes an event to monitor, using DRF2 format -for events. 'sync_node', if specified, is a string indicating a -particular ACNET node to use for the SYNC service. If not provided, -one will be chosen. - -The generator will yield ClockEvent and StateEvent objects as they -occur. - - """ - - # Verify parameters - - assert isinstance(con, acsys.Connection) - assert isinstance(ev_str, list) - assert sync_node is None or isinstance(sync_node, str) - - # Build the request message. - - msg = Register_request() - msg.evTclk = ev_str - - while True: - if sync_node is None: - node = await find_service(con, clock=clock, node=sync_node) - if node is None: - raise acsys.status.AcnetNoSuchLogicalNode() - else: - node = sync_node - - _log.info('using SYNC service on %s', node) - - try: - utc_timezone = datetime.timezone.utc - gen = con.request_stream(f'SYNC@{node}', msg, proto=acsys.sync.syncd_protocol) - async for _, reply in gen: - assert isinstance(reply, Report_reply) - - for event in reply.events: - delta = datetime.timedelta(milliseconds=event.stamp) - stamp = datetime.datetime(1970, 1, 1, tzinfo=utc_timezone) + delta - - if hasattr(event, 'state'): - yield StateEvent(stamp, event.state.device_index, - event.state.value) - else: - yield ClockEvent(stamp, event.clock.event, event.clock.number) - except acsys.status.AcnetReplyTaskDisconnected: - raise - except acsys.status.AcnetException as exception: - _log.warning(f'lost connection with SYNC service: {exception!r}') - await asyncio.sleep(0.5) diff --git a/acsys/sync/syncd_protocol.py b/acsys/sync/syncd_protocol.py deleted file mode 100644 index f52ec1e..0000000 --- a/acsys/sync/syncd_protocol.py +++ /dev/null @@ -1,435 +0,0 @@ -# Generated by the protocol compiler version 1.3.9 -# DO NOT EDIT THIS FILE DIRECTLY! - -__doc__ = 'Message serializer for the Syncd protocol.' - -from itertools import chain, islice - -__all__ = ['ProtocolError', - 'unmarshal_request', - 'unmarshal_reply', - 'EvState_struct', - 'EvClock_struct', - 'Event_struct', - 'Discover_request', - 'Register_request', - 'Instance_reply', - 'Report_reply'] - -class ProtocolError(Exception): - """Exception class that gets raised when there's a problem marshaling - or unmarshaling a message from the Syncd protocol.""" - - def __init__(self, reason): - self.reason = reason - - def __str__(self): - return repr(self.reason) - -# -- Internal marshalling routines -- - -def emitRawInt(tag, val): - def emitEach(buf, n): - curr = (val >> (n * 8)) & 0xff - next = val >> ((n + 1) * 8) - if (next == 0 and (curr & 0x80) != 0x80) or \ - (next == -1 and (curr & 0x80) == 0x80): - buf.append(tag + n + 1) - else: - emitEach(buf, n + 1) - buf.append(curr) - tmp = bytearray() - emitEach(tmp, 0) - return tmp - -def marshal_int16(val): - if isinstance(val, int): - if val < 32768 and val > -32769: - return emitRawInt(0x10, val) - else: - raise ProtocolError('value out of range for int16') - else: - raise ProtocolError('expected integer type') - -def marshal_int32(val): - if isinstance(val, int): - if int(-2147483648) <= val <= int(2147483647): - return emitRawInt(0x10, val) - else: - raise ProtocolError('value out of range for int32') - else: - raise ProtocolError('expected integer type') - -def marshal_int64(val): - if isinstance(val, int): - if int(-9223372036854775808) <= val <= int(9223372036854775807): - return emitRawInt(0x10, val) - else: - raise ProtocolError('value out of range for int64') - else: - raise ProtocolError('expected integer type') - -def marshal_string(val): - if isinstance(val, str): - return chain(emitRawInt(0x40, len(val)),\ - (ord(ii) for ii in val)) - else: - raise ProtocolError('expected string type') - -def marshal_array(fn, val): - if isinstance(val, list): - return chain(emitRawInt(0x50, len(val)),\ - chain.from_iterable((fn(v) for v in val))) - else: - raise ProtocolError('expected list type') - -__all__.append('Clock_Tclk') -Clock_Tclk = 8267 -__all__.append('Clock_Test') -Clock_Test = 26129 -__all__.append('Clock_CMTF') -Clock_CMTF = -27901 -__all__.append('Clock_NML') -Clock_NML = 24689 - -def marshal_Clock_enum(val): - if int(val) == 8267: - return b'\x82\x20\x4b' - elif int(val) == 26129: - return b'\x82\x66\x11' - elif int(val) == -27901: - return b'\x82\x93\x03' - elif int(val) == 24689: - return b'\x82\x60\x71' - else: - raise ProtocolError("invalid value for enum 'Clock'") - -class EvState_struct: - def __init__(self): - self.device_index = int(0) - self.value = int(0) - - def __eq__(self, other): - return self.device_index == other.device_index and \ - self.value == other.value - - def __ne__(self, other): - return not self.__eq__(other) - -def marshal_EvState_struct(val): - return chain(b'\x51\x04\x12\x05\xad', - marshal_int32(val.device_index), - b'\x12\xc1\x60', - marshal_int32(val.value)) - -class EvClock_struct: - def __init__(self): - self.event = int(0) - self.number = int(0) - - def __eq__(self, other): - return self.event == other.event and \ - self.number == other.number - - def __ne__(self, other): - return not self.__eq__(other) - -def marshal_EvClock_struct(val): - return chain(b'\x51\x04\x12\x63\x90', - marshal_int16(val.event), - b'\x12\x24\x8a', - marshal_int32(val.number)) - -class Event_struct: - def __init__(self): - self.stamp = int(0) - - def __eq__(self, other): - return self.stamp == other.stamp and \ - ((not hasattr(self, 'state') and not hasattr(other, 'state')) or \ - (hasattr(self, 'state') and hasattr(other, 'state') and \ - self.state == other.state)) and \ - ((not hasattr(self, 'clock') and not hasattr(other, 'clock')) or \ - (hasattr(self, 'clock') and hasattr(other, 'clock') and \ - self.clock == other.clock)) - - def __ne__(self, other): - return not self.__eq__(other) - -def marshal_Event_struct(val): - return chain(emitRawInt(0x50, 2 \ - + (2 if hasattr(val, 'state') else 0) \ - + (2 if hasattr(val, 'clock') else 0)), - b'\x12\xc7\x8d', - marshal_int64(val.stamp), - chain(b'\x12\x9e\x2e', - marshal_EvState_struct(val.state)) \ - if hasattr(val, 'state') else bytearray(), - chain(b'\x12\x1b\x19', - marshal_EvClock_struct(val.clock)) \ - if hasattr(val, 'clock') else bytearray()) - -class Discover_request: - def __init__(self): - self.clock = Clock_Tclk - - def __eq__(self, other): - return self.clock == other.clock - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of Discover_request.""" - return chain(b'SDD\x02\x51\x03\x14\xc7\xf5\x7b\x8d\x12\x97\x3c\x51\x02\x12\x1b\x19', - marshal_Clock_enum(self.clock)) - -class Register_request: - def __init__(self): - self.evTclk = [] - - def __eq__(self, other): - return self.evTclk == other.evTclk - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of Register_request.""" - return chain(b'SDD\x02\x51\x03\x14\xc7\xf5\x7b\x8d\x12\x12\xd6\x51\x02\x12\x99\xdf', - marshal_array(marshal_string, self.evTclk)) - -class Instance_reply: - def __eq__(self, other): - return True - - def __ne__(self, other): - return False - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of Instance_reply.""" - return b'SDD\x02\x51\x03\x14\xc7\xf5\x7b\x8d\x12\xb7\x31\x51\x00' - -class Report_reply: - def __init__(self): - self.seq = int(0) - self.events = [] - - def __eq__(self, other): - return self.seq == other.seq and \ - self.events == other.events - - def __ne__(self, other): - return not self.__eq__(other) - - def marshal(self): - """Returns a generator that emits a character stream representing - the marshaled contents of Report_reply.""" - return chain(b'SDD\x02\x51\x03\x14\xc7\xf5\x7b\x8d\x12\xed\x8e\x51\x04\x12\xc2\xde', - marshal_int16(self.seq), - b'\x12\x8b\x06', - marshal_array(marshal_Event_struct, self.events)) - -def marshal_request(val): - return val.marshal() - -def marshal_reply(val): - return val.marshal() - -# -- Internal unmarshalling routines -- - -def consumeRawInt(ii, tag): - iiTag = (ii.__next__()) - iiLen = iiTag & 0xf - if (iiTag & 0xf0) == (tag & 0xf0) and iiLen > 0 and iiLen <= 8: - firstByte = (ii.__next__()) - retVal = (0 if (0x80 & firstByte) == 0 else -256) | firstByte - while iiLen > 1: - retVal = (retVal << 8) | (ii.__next__()) - iiLen = iiLen - 1 - return int(retVal) - else: - raise ProtocolError('bad tag or length') - -def unmarshal_int16(ii): - val = consumeRawInt(ii, 0x10) - if val >= -0x8000 and val < 0x8000: - return int(val) - else: - raise ProtocolError('value out of range for int16') - -def unmarshal_int32(ii): - val = consumeRawInt(ii, 0x10) - if int(-2147483648) <= val <= int(2147483647): - return int(val) - else: - raise ProtocolError('value out of range for int32') - -def unmarshal_int64(ii): - val = consumeRawInt(ii, 0x10) - if int(-9223372036854775808) <= val <= int(9223372036854775807): - return val - else: - raise ProtocolError('value out of range for int64') - -def unmarshal_string(ii): - return bytearray(islice(ii, consumeRawInt(ii, 0x40))).decode('utf-8') - -def unmarshal_array(ii, fn): - return [fn(ii) for x in range(consumeRawInt(ii, 0x50))] - -def unmarshal_header(ii): - if ii.__next__() != 83 or ii.__next__() != 68 or \ - ii.__next__() != 68 or ii.__next__() != 2 or \ - consumeRawInt(ii, 0x50) != 3: - raise ProtocolError('invalid header') - elif consumeRawInt(ii, 0x10) != -940213363: - raise ProtocolError('incorrect protocol specified') - -def unmarshal_Clock_enum(ii): - val = consumeRawInt(ii, 0x80) - if val == 8267: - return Clock_Tclk - elif val == 26129: - return Clock_Test - elif val == -27901: - return Clock_CMTF - elif val == 24689: - return Clock_NML - else: - raise ProtocolError("invalid value for enum 'Clock'") - -def unmarshal_EvState_struct(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 4: - raise ProtocolError('incorrect number of fields') - else: - tmp = EvState_struct() - for xx in range(nFlds // 2): - fld = consumeRawInt(ii, 0x10) - if fld == 1453: - tmp.device_index = unmarshal_int32(ii) - elif fld == -16032: - tmp.value = unmarshal_int32(ii) - else: - raise ProtocolError('unknown field found') - return tmp - -def unmarshal_EvClock_struct(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 4: - raise ProtocolError('incorrect number of fields') - else: - tmp = EvClock_struct() - for xx in range(nFlds // 2): - fld = consumeRawInt(ii, 0x10) - if fld == 25488: - tmp.event = unmarshal_int16(ii) - elif fld == 9354: - tmp.number = unmarshal_int32(ii) - else: - raise ProtocolError('unknown field found') - return tmp - -def unmarshal_Event_struct(ii): - nFlds = consumeRawInt(ii, 0x50) - if (nFlds % 2) != 0 or nFlds < 2 or nFlds > 6: - raise ProtocolError('incorrect number of fields') - else: - tmp = Event_struct() - for xx in range(nFlds // 2): - fld = consumeRawInt(ii, 0x10) - if fld == -14451: - tmp.stamp = unmarshal_int64(ii) - elif fld == -25042: - tmp.state = unmarshal_EvState_struct(ii) - elif fld == 6937: - tmp.clock = unmarshal_EvClock_struct(ii) - else: - raise ProtocolError('unknown field found') - return tmp - -def unmarshal_Discover_request(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 2: - raise ProtocolError('incorrect number of fields') - else: - tmp = Discover_request() - for xx in range(nFlds // 2): - fld = consumeRawInt(ii, 0x10) - if fld == 6937: - tmp.clock = unmarshal_Clock_enum(ii) - else: - raise ProtocolError('unknown field found') - return tmp - -def unmarshal_Register_request(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 2: - raise ProtocolError('incorrect number of fields') - else: - tmp = Register_request() - for xx in range(nFlds // 2): - fld = consumeRawInt(ii, 0x10) - if fld == -26145: - tmp.evTclk = unmarshal_array(ii, unmarshal_string) - else: - raise ProtocolError('unknown field found') - return tmp - -def unmarshal_Instance_reply(ii): - if consumeRawInt(ii, 0x50) != 0: - raise ProtocolError('incorrect number of fields') - else: - return Instance_reply() - -def unmarshal_Report_reply(ii): - nFlds = consumeRawInt(ii, 0x50) - if nFlds != 4: - raise ProtocolError('incorrect number of fields') - else: - tmp = Report_reply() - for xx in range(nFlds // 2): - fld = consumeRawInt(ii, 0x10) - if fld == -15650: - tmp.seq = unmarshal_int16(ii) - elif fld == -29946: - tmp.events = unmarshal_array(ii, unmarshal_Event_struct) - else: - raise ProtocolError('unknown field found') - return tmp - -def unmarshal_request(ii): - """Attempts to unmarshal a request message from the specified - generator, ii. If an error occurs, the ProtocolError exception - will be raised.""" - try: - unmarshal_header(ii) - msg = consumeRawInt(ii, 0x10) - if msg == -26820: - return unmarshal_Discover_request(ii) - elif msg == 4822: - return unmarshal_Register_request(ii) - else: - raise ProtocolError('unknown request type') - except StopIteration: - raise ProtocolError('unexpected end of input') - -def unmarshal_reply(ii): - """Attempts to unmarshal a reply message from the specified - generator, ii. If an error occurs, the ProtocolError exception - will be raised.""" - try: - unmarshal_header(ii) - msg = consumeRawInt(ii, 0x10) - if msg == -18639: - return unmarshal_Instance_reply(ii) - elif msg == -4722: - return unmarshal_Report_reply(ii) - else: - raise ProtocolError('unknown reply type') - except StopIteration: - raise ProtocolError('unexpected end of input') diff --git a/requirements.txt b/requirements.txt index a9a91f9..579ff65 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,2 @@ gssapi -nest_asyncio importlib-metadata -dataclasses>='0.7';python_version<'3.7' diff --git a/setup.py b/setup.py index 3ea1218..0b2ba76 100644 --- a/setup.py +++ b/setup.py @@ -21,10 +21,10 @@ install_requires=[ 'gssapi', 'nest_asyncio', - 'importlib-metadata; python_version < "3.8"', + 'importlib-metadata; python_version >= "3.9"', ], extras_require={ # Optional 'settings': ['gssapi'] }, - python_requires='>=3.6', + python_requires='>=3.9', )