Client-server subpackage (ska_ser_devices.client_server)

This subpackage supports client-server communication.

A key design element is that received data is provided to the application layer via a bytestring iterator:

Receipt of an application payload may be split across multiple buffers, and the manner by which the application layer decides that it has received enough bytes to comprise a full application payload is exclusively determined by the application.

For example, the application layer might deal in “lines”, and receiving a “line” might involve continually receiving bytestrings until a line terminator character sequence such as “rn” is encountered.

Using TCP as an example for how transport protocol details (e.g. socket reads) are commonly mixed with application-level decision logic, the concatenation of bytestrings into an application payload is typically implemented like:

data = my_socket.recv(1024)
while not data.endswith(b"\r\n"):
    data = data + my_socket.recv(1024)

Instead, this module handles this situation by passing a bytestring iterator to the application layer. Thus, it is the application layer’s job to iterate over bytestrings until it has constructed a full application payload. Meanwhile, the transport protocol details remain hidden, and the application remains agnostic of any transport mechanism.

data = next(bytes_iterator)
while not data.endswith(b"\r\n"):
    data = data + next(bytes_iterator)
class ska_ser_devices.client_server.ApplicationClient(bytes_client, request_marshaller, response_unmarshaller)

An application-layer client.

It uses an underlying transport-layer client (such as a TCP client), but requests are application payloads rather than bytestrings. These must be marshalled into bytestrings before sending to the server. When a response is received from the server, the received bytes are unmarshalled into an application payload which is returned to the caller. Thus, the caller deals only in application payloads, and does not know any details about how the transport layer transmits those payloads,

__call__(request, expect_response=True)

Call the client with a request.

The client marshalls the request (an application payload) down to a bytestring, then hands the bytestring down to the bytestring TCP client for sending to the server. Upon receive of a bytestring response, this client unmarshalls it into an application payload, which is returned to the caller.

Parameters
  • request (TypeVar(RequestPayloadT)) – the payload to be sent to the server

  • expect_response (bool) – whether to wait for a response from the server. Defaults to True. In the unusual case where the client does not expect a response to their request, set this to False.

Return type

Optional[TypeVar(ResponsePayloadT)]

Returns

a response payload

__init__(bytes_client, request_marshaller, response_unmarshaller)

Initialise a new instance.

Parameters
  • bytes_client (TransportClientProtocol) – the bytes-level client to use.

  • request_marshaller (Callable[[TypeVar(RequestPayloadT)], bytes]) – a callable that marshalls application-layer request payloads into bytes.

  • response_unmarshaller (Callable[[Iterator[bytes]], TypeVar(ResponsePayloadT)]) – a callable that unmarshalls byte into application-layer response payloads.

__weakref__

list of weak references to the object (if defined)

class ska_ser_devices.client_server.ApplicationServer(request_unmarshaller, response_marshaller, callback)

A server of application payloads.

It uses an underlying transport-layer server (such as a TCP server), but requests are unmarshalled into application-layer payloads before being delivered to the application-layer server backend. Responses from the server backend are application-layer payloads that this server marshals down to a bytestring and returns to the client. Thus, the server backend deals only in application payloads, and does not know any details about how the transport layer transmits those payloads.

__call__(bytes_iterator)

Handle receipt of bytes from the transport layer.

When the transport layer server receives some bytes, it calls this callback with a bytestring iterator. This callback uses that iterator to ingest bytestrings until it has enough bytes to unmarshall them into a complete application payload. The payload is passed to the server backend. Once this callback receives a response from the backend, it marshalls that response down to a bytestring, and returns it to the transport-layer server for returning to the client.

Parameters

bytes_iterator (Iterator[bytes]) – a bytestring iterator.

Return type

Optional[bytes]

Returns

the bytes to be returned to the client.

__init__(request_unmarshaller, response_marshaller, callback)

Initialise a new instance.

Parameters
  • request_unmarshaller (Callable[[Iterator[bytes]], TypeVar(RequestPayloadT)]) – a callable that unmarshalls byte into application-layer request payloads.

  • response_marshaller (Callable[[TypeVar(ResponsePayloadT)], bytes]) – a callable that marshalls application-layer response payloads into bytes

  • callback (Callable[[TypeVar(RequestPayloadT)], Optional[TypeVar(ResponsePayloadT)]]) – callback to the application layer. When this server receives a request payload, it calls the application layer with the request, and expects to receive a response payload back.

__weakref__

list of weak references to the object (if defined)

class ska_ser_devices.client_server.FixedLengthBytesMarshaller(length, logger=None)

A bytes marshaller for bytestrings of fixed, known length.

__init__(length, logger=None)

Initialise a new instance.

Parameters
  • length (int) – the length of the payload

  • logger (Optional[Logger]) – a python standard logger

__weakref__

list of weak references to the object (if defined)

marshall(payload)

Marshall application-layer payload bytes into bytes to be transmitted.

This class simply appends the sentinel character sequence.

Parameters

payload (bytes) – the application-layer payload bytes.

Return type

bytes

Returns

the bytes to be transmitted.

Raises

ValueError – if the received bytestring is not of the correct length

unmarshall(bytes_iterator)

Unmarshall transmitted bytes into application-layer payload bytes.

This method is implemented to continually receive bytestrings until it receives a bytestring terminated by the sentinel. It then strips the sentinel off, and returns the rest.

Parameters

bytes_iterator (Iterator[bytes]) – an iterator of bytestrings received by the server

Return type

bytes

Returns

the application-layer bytestring.

Raises

ValueError – if the received bytestring is not of the correct length

class ska_ser_devices.client_server.SentinelBytesMarshaller(sentinel, logger=None)

A bytes marshaller that marshalls and unmarshalls terminated bytestrings.

That is, the application-layer payload is a bytestring, the end of which is demarcated by a special character sequence. For example, the payload might be

  • A c-style string, terminated by the NUL character;

  • A line of text, terminated by an end-of-line sequence, such as “rn”.

  • A file, terminated by an EOF byte.

__init__(sentinel, logger=None)

Initialise a new instance.

Parameters
  • sentinel (bytes) – the sentinel character that marks the end of the payload

  • logger (Optional[Logger]) – a python standard logger

__weakref__

list of weak references to the object (if defined)

marshall(payload)

Marshall application-layer payload bytes into bytes to be transmitted.

This class simply appends the sentinel character sequence.

Parameters

payload (bytes) – the application-layer payload bytes.

Return type

bytes

Returns

the bytes to be transmitted.

unmarshall(bytes_iterator)

Unmarshall transmitted bytes into application-layer payload bytes.

This method is implemented to continually receive bytestrings until it receives a bytestring terminated by the sentinel. It then strips the sentinel off, and returns the rest.

Parameters

bytes_iterator (Iterator[bytes]) – an iterator of bytestrings received by the server

Return type

bytes

Returns

the application-layer bytestring, minus the terminator.

class ska_ser_devices.client_server.TcpClient(host, port, timeout=None, buffer_size=1024, logger=None)

A TCP client that operates at the bytestring level.

It handles client requests by sending the request bytes straight off to the server. However, when it receives a response, it creates a bytestring iterator and hands it up to the application layer, so that the application layer can receive as many bytestrings as it needs to constitute an application payload.

__init__(host, port, timeout=None, buffer_size=1024, logger=None)

Initialise a new instance.

Parameters
  • host (Union[str, bytes, bytearray]) – host name or IP address of the server.

  • port (int) – port on which the server is running.

  • timeout (Optional[float]) – how long to wait when attempting to send or receive data, in seconds. If None, the socket blocks indefinitely.

  • buffer_size (int) – maximum size of a bytestring.

  • logger (Optional[Logger]) – a python standard logger

__weakref__

list of weak references to the object (if defined)

request(request)

Initiate a new client request.

Call this method with

with tcp_client.request(request_bytes) as bytes_iterator:
    response_bytes = next(bytes_iterator)
    if not response_bytes.endswith(b"\r\n"):
        response_bytes += next(bytes_iterator)

That is,

  • First enter into a session with the TCP server by establishing a connection and sending the request data.

  • Since only the calling application can know when it has received enough bytes for a complete response, the session context returns a bytestring iterator for the application layer to use to retrieve blocks of bytes. (In this example, the application layer knows that the response is terminated by “rn”, so it keeps receiving data until it encounters that sequence and the end of a block.)

  • Upon exiting the session context, the session is closed.

Parameters

request (bytes) – request bytes.

Yields

a bytestring iterator.

Return type

Iterator[Iterator[bytes]]

class ska_ser_devices.client_server.TcpServer(host, port, callback, buffer_size=1024, logger=None)

A TCP server that operates at the bytestring level.

It handles client requests by creating a bytestring iterator, and handing it up to the application layer, so that the application layer can receive as many bytestrings as it needs to constitute an application payload. When the application layer returns a response, that response is sent back to the client.

__init__(host, port, callback, buffer_size=1024, logger=None)

Initialise a new instance.

Parameters
  • host (str) – host name or IP address of the server.

  • port (int) – port on which the server is running.

  • callback (Callable[[Iterator[bytes]], Optional[bytes]]) – the application layer callback to call when bytes are received

  • buffer_size (int) – maximum size of a bytestring.

  • logger (Optional[Logger]) – a python standard logger

class ska_ser_devices.client_server.TelnetClient(host, port, timeout=None, logger=None)

A Telnet client.

It handles client requests by sending the request bytes straight off to the server. However, when it receives a response, it creates a bytestring iterator and hands it up to the application layer, so that the application layer can receive as many bytestrings as it needs to constitute an application payload.

__init__(host, port, timeout=None, logger=None)

Initialise a new instance.

Parameters
  • host (str) – host name or IP address of the server.

  • port (int) – port on which the server is running.

  • timeout (Optional[float]) – how long to wait when attempting to send or receive data.

  • logger (Optional[Logger]) – a python standard logger

__weakref__

list of weak references to the object (if defined)

request(request)

Initiate a new client request.

Call this method like

with telnet_client.request(request_bytes) as bytes_iterator:
    response_bytes = next(bytes_iterator)
    if not response_bytes.endswith(b"\r\n"):
        response_bytes += next(bytes_iterator)

That is,

  • First enter into a session with the Telnet server by establishing a connection and sending the request data.

  • Since only the calling application can know when it has received enough bytes for a complete response, the session context returns a bytestring iterator for the application layer to use to retrieve blocks of bytes. (In this example, the application layer knows that the response is terminated by “rn”, so it keeps receiving data until it encounters that sequence and the end of a block.)

  • Upon exiting the session context, the session is closed.

Parameters

request (bytes) – request bytes.

Yields

a bytestring iterator.

Return type

Iterator[Iterator[bytes]]

class ska_ser_devices.client_server.TransportClientProtocol(*args, **kwargs)

Structural subtyping protocol for supported transport protocols.

That is, in order for a transport protocol client to be supported by this module, it must provide a request method that is implemented to:

  • enter a session context with the server,

  • issue the client request,

  • return an iterator, for use by the application layer to read as many bytestrings as necessary in order to construct the complete response payload.

  • exit the session context.

__init__(*args, **kwargs)
__subclasshook__()

Abstract classes can override this to customize issubclass().

This is invoked early on by abc.ABCMeta.__subclasscheck__(). It should return True, False or NotImplemented. If it returns NotImplemented, the normal algorithm is used. Otherwise, it overrides the normal algorithm (and the outcome is cached).

__weakref__

list of weak references to the object (if defined)

request(request)

Transact a client request.

Enter a session context with the server, issue the request, and return an iterator for use by the application layer to read as many bytestrings as necessary to construct the complete response payload.

Parameters

request (bytes) – the request bytes

Yield

a bytes iterator by which to construct the response

Return type

Iterator[Iterator[bytes]]