Skip to content
This repository has been archived by the owner on Oct 17, 2024. It is now read-only.

Commit

Permalink
johnpaulett#44 client code updated
Browse files Browse the repository at this point in the history
* mllp_client code updated to react on timeouts from socket, use of argparse instead of obsolete optparse
* tests updated to follow new features and convetions
  • Loading branch information
cezio committed Aug 16, 2022
1 parent 6a87eff commit 6f2eae5
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 107 deletions.
119 changes: 70 additions & 49 deletions hl7/client.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import io
import logging
import os.path
import socket
import sys
import time
from optparse import OptionParser
import logging
from argparse import ArgumentParser

import hl7
from hl7.exceptions import CLIException

SB = b"\x0b" # <SB>, vertical tab
EB = b"\x1c" # <EB>, file separator
Expand All @@ -18,6 +19,7 @@

log = logging.getLogger(__name__)


class MLLPException(Exception):
pass

Expand All @@ -39,20 +41,22 @@ class MLLPClient(object):
with MLLPClient(host, port) as client:
client.send_message('MSH|...')
MLLPClient takes an optional ``encoding`` parameter, defaults to UTF-8,
for encoding unicode messages [#]_.
MLLPClient takes optional parameters:
`timeout` in seconds will be used to wait for full response from the server.
* ``encoding``, defaults to UTF-8, for encoding unicode messages [#]_.
* ``timeout`` in seconds, timeout for socket operations [_t]_.
* ``deadline`` in seconds, will be used by the client to determine how long it should wait for full response.
.. [#] http://wiki.hl7.org/index.php?title=Character_Set_used_in_v2_messages
.. [_t] https://docs.python.org/3/library/socket.html#socket.socket.settimeout
"""

def __init__(self, host, port, encoding="utf-8", timeout=10, deadline=3):
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.connect((host, port))
self.encoding = encoding
self.timeout = timeout # seconds
self.deadline = deadline # seconds
self.timeout = timeout # seconds for socket timeout
self.deadline = deadline # seconds for max time client will wait for a response

def __enter__(self):
return self
Expand Down Expand Up @@ -91,24 +95,27 @@ def send(self, data):
wrapped in an MLLP container). Blocks until the server returns.
"""
# upload the data
log.debug(f'sending {(data,)}')
log.debug(f"sending {(data,)}")
self.socket.send(data)
# wait for the ACK/NACK
self.socket.settimeout(self.timeout)
buff = b''
buff = b""
# the whole message should be received within this deadline
deadline = (time.time() + self.deadline)
deadline = time.time() + self.deadline

# This will read data until deadline is reached.
# some HL7 counterparts may send responses in chunks, so we should wait some
# and read from the socket until we probably receive full message.
# timeout/deadline are configurable on client creation.
while deadline > time.time():
data = self.socket.recv(RECV_BUFFER)
try:
data = self.socket.recv(RECV_BUFFER)
except TimeoutError:
data = None
if not data:
continue
buff += data
log.debug(f'received {(buff,)}')
log.debug(f"received {(buff,)}")
return buff


Expand Down Expand Up @@ -143,7 +150,7 @@ def read_stream(stream):
if not data:
break
if isinstance(data, str):
data = data.encode('utf-8')
data = data.encode("utf-8")
# usually should be broken up by EB, but I have seen FF separating
# messages
messages = (_buffer + data).split(EB if FF not in data else FF)
Expand Down Expand Up @@ -190,95 +197,109 @@ def read_loose(stream):
yield START_BLOCK + m


def mllp_send():
def mllp_send(in_args=None):
"""Command line tool to send messages to an MLLP server"""
# set up the command line options
script_name = os.path.basename(sys.argv[0])
parser = OptionParser(usage=script_name + " [options] <server>")
parser.add_option(
in_args = in_args or sys.argv
script_name = os.path.basename(in_args[0])
parser = ArgumentParser(usage=script_name + " [options] <server>")
parser.add_argument("host", action="store", nargs=1, help="Host to connect to")
parser.add_argument(
"--version",
action="store_true",
dest="version",
default=False,
help="print current version and exit",
)
parser.add_option(
parser.add_argument(
"-p",
"--port",
action="store",
type="int",
type=int,
required=False,
dest="port",
default=6661,
help="port to connect to",
)
parser.add_option(
parser.add_argument(
"-f",
"--file",
required=False,
dest="filename",
default=None,
help="read from FILE instead of stdin",
metavar="FILE",
)
parser.add_option(
parser.add_argument(
"-q",
"--quiet",
action="store_true",
action="store_false",
dest="verbose",
default=True,
default=True,
help="do not print status messages to stdout",
)
parser.add_option(
parser.add_argument(
"--loose",
action="store_true",
dest="loose",
default=False,
help=(
"allow file to be a HL7-like object (\\r\\n instead "
"of \\r). Requires that messages start with "
'"MSH|^~\\&|". Requires --file option (no stdin)'
),
)
parser.add_argument(
"--timeout",
action="store",
dest="timeout",
required=False,
default=10,
type=float,
help="number of seconds for socket operations to timeout",
)
parser.add_argument(
"--deadline",
action="store",
required=False,
dest="deadline",
default=3,
type=float,
help="number of seconds for the client to receive full response",
)

(options, args) = parser.parse_args()

if options.version:
args = parser.parse_args(in_args[1:])
if args.version:
import hl7

stdout(hl7.__version__)
return

if len(args) == 1:
host = args[0]
else:
# server not present
parser.print_usage()
stderr().write("server required\n")
sys.exit(1)
return # for testing when sys.exit mocked
host = args.host[0]

if options.filename is not None:
if args.filename is not None:
# Previously set stream to the open() handle, but then we did not
# close the open file handle. This new approach consumes the entire
# file into memory before starting to process, which is not required
# or ideal, since we can handle a stream
with open(options.filename, "rb") as f:
with open(args.filename, "rb") as f:
stream = io.BytesIO(f.read())
else:
if options.loose:
if args.loose:
stderr().write("--loose requires --file\n")
sys.exit(1)
return # for testing when sys.exit mocked
raise CLIException(1)

stream = stdin()

with MLLPClient(host, options.port) as client:
message_stream = (
read_stream(stream) if not options.loose else read_loose(stream)
)
with MLLPClient(
host, args.port, deadline=args.deadline, timeout=args.timeout
) as client:
message_stream = read_stream(stream) if not args.loose else read_loose(stream)
for message in message_stream:
result = client.send_message(message)
if options.verbose:
if args.verbose:
stdout(result)


if __name__ == "__main__":
mllp_send()
try:
mllp_send(sys.argv)
except CLIException as err:
sys.exit(err.exit_code)
6 changes: 6 additions & 0 deletions hl7/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ class MalformedFileException(HL7Exception):

class ParseException(HL7Exception):
pass


class CLIException(HL7Exception):
""" An exception to propagate expected exit code from cli script"""
def __init__(self, exit_code):
self.exit_code = exit_code
Loading

0 comments on commit 6f2eae5

Please sign in to comment.