Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HANS-298: HL7v2 Improvements #6

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 58 additions & 56 deletions src/convert_hl7v2_fhir/app.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import hl7
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.typing import LambdaContext
from boto3 import client
from botocore.exceptions import ClientError, NoRegionError
import hl7

from controllers.hl7builder import generate_ACK_message, v2ErrorCode, v2ErrorSeverity
from controllers.convertor import HL7v2ConversionController
from controllers.exceptions import (
from controllers.hl7.hl7_message_controller import HL7MessageController
from controllers.hl7.exceptions import (
InvalidNHSNumberError,
MissingNHSNumberError,
MissingFieldOrComponentError,
MissingSegmentError,
)
from internal_integrations.sqs.settings import SQSSettings
from controllers.hl7.hl7_builder import (
generate_ack_message,
HL7ErrorCode,
HL7ErrorSeverity,
)
from internal_integrations.sqs.settings import get_sqs_settings

_LOGGER = Logger()

Expand All @@ -22,52 +26,52 @@ def lambda_handler(event: dict, context: LambdaContext):

# hl7 messages expect \r rather than \r\n (and the parsing library)
# will reject otherwise (with a KeyError)
msg_parsed = hl7.parse(body.replace("\n", ""))
hl7_message = hl7.parse(body.replace("\n", ""))

ack = _create_ack(msg_parsed)
ack_message = _create_ack_message(hl7_message)

try:
fhir_json = _convert(msg_parsed)
_send_to_sqs(fhir_json)
fhir_bundle = HL7MessageController(hl7_message).to_fhir_bundle()
_send_to_sqs(fhir_bundle.json())
_LOGGER.info("Successfully processed message")
except InvalidNHSNumberError as ex:
_LOGGER.error(ex)
ack = _create_nak(
msg_parsed,
v2ErrorCode.DATA_TYPE_ERROR,
v2ErrorSeverity.ERROR,
ack_message = _create_nak(
hl7_message,
HL7ErrorCode.DATA_TYPE_ERROR,
HL7ErrorSeverity.ERROR,
"NHS Number in message was invalid",
)
except MissingNHSNumberError as ex:
_LOGGER.error(ex)
ack = _create_nak(
msg_parsed,
v2ErrorCode.UNKNOWN_KEY_IDENTIFIER,
v2ErrorSeverity.ERROR,
ack_message = _create_nak(
hl7_message,
HL7ErrorCode.UNKNOWN_KEY_IDENTIFIER,
HL7ErrorSeverity.ERROR,
"NHS Number missing from message",
)
except MissingSegmentError as ex:
_LOGGER.error(ex)
ack = _create_nak(
msg_parsed,
v2ErrorCode.SEGMENT_SEQUENCE_ERROR,
v2ErrorSeverity.ERROR,
ack_message = _create_nak(
hl7_message,
HL7ErrorCode.SEGMENT_SEQUENCE_ERROR,
HL7ErrorSeverity.ERROR,
"Required segment was missing: " + str(ex),
)
except MissingFieldOrComponentError as ex:
_LOGGER.error(ex)
ack = _create_nak(
msg_parsed,
v2ErrorCode.REQUIRED_FIELD_MISSING,
v2ErrorSeverity.ERROR,
ack_message = _create_nak(
hl7_message,
HL7ErrorCode.REQUIRED_FIELD_MISSING,
HL7ErrorSeverity.ERROR,
"Required field was missing: " + str(ex),
)
except (ClientError, NoRegionError) as ex:
_LOGGER.error(ex)
ack = _create_nak(
msg_parsed,
v2ErrorCode.APPLICATION_INTERNAL_ERROR,
v2ErrorSeverity.ERROR,
ack_message = _create_nak(
hl7_message,
HL7ErrorCode.APPLICATION_INTERNAL_ERROR,
HL7ErrorSeverity.ERROR,
"Issue reaching SQS service: " + str(ex),
)
except Exception as ex:
Expand All @@ -76,53 +80,51 @@ def lambda_handler(event: dict, context: LambdaContext):
# otherwise hospital system will not know we have had
# an internal server error - we will log as error though
_LOGGER.error(ex)
ack = _create_nak(
msg_parsed,
v2ErrorCode.APPLICATION_INTERNAL_ERROR,
v2ErrorSeverity.FATAL_ERROR,
ack_message = _create_nak(
hl7_message,
HL7ErrorCode.APPLICATION_INTERNAL_ERROR,
HL7ErrorSeverity.FATAL_ERROR,
str(ex),
)

return {
"statusCode": 200,
"headers": {"content-type": "x-application/hl7-v2+er; charset=utf-8"},
"body": ack,
"body": ack_message,
}


def _send_to_sqs(body: str):
sqs = client("sqs")
sqs_settings = SQSSettings()
sqs_settings = get_sqs_settings()
sqs.send_message(QueueUrl=sqs_settings.converted_queue_url, MessageBody=body)


def _create_nak(
msg_parsed: hl7.Message, err_code: str, err_sev: str, err_msg: str
hl7_message: hl7.Message,
error_code: HL7ErrorCode,
error_severity: HL7ErrorSeverity,
err_msg: str,
) -> str:
sending_app = msg_parsed["MSH"][0][3][0]
sending_facility = msg_parsed["MSH"][0][4][0]
msg_control_id = msg_parsed["MSH"][0][10][0]
return generate_ACK_message(
recipient_app=sending_app,
recipient_facility=sending_facility,
sending_application = hl7_message.extract_field("MSH", 0, 3, 0)
sending_facility = hl7_message.extract_field("MSH", 0, 4, 0)
msg_control_id = hl7_message.extract_field("MSH", 0, 10, 0)
return generate_ack_message(
receiving_application=sending_application,
receiving_facility=sending_facility,
replying_to_msgid=msg_control_id,
hl7_error_code=err_code,
error_severity=err_sev,
hl7_error_code=error_code,
error_severity=error_severity,
error_message=err_msg,
)


def _create_ack(msg_parsed: hl7.Message) -> str:
sending_app = msg_parsed["MSH"][0][3][0]
sending_facility = msg_parsed["MSH"][0][4][0]
msg_control_id = msg_parsed["MSH"][0][10][0]
return generate_ACK_message(
recipient_app=sending_app,
recipient_facility=sending_facility,
def _create_ack_message(hl7_message: hl7.Message) -> str:
sending_application = hl7_message.extract_field("MSH", 0, 3, 0)
sending_facility = hl7_message.extract_field("MSH", 0, 4, 0)
msg_control_id = hl7_message.extract_field("MSH", 0, 10, 0)
return generate_ack_message(
receiving_application=sending_application,
receiving_facility=sending_facility,
replying_to_msgid=msg_control_id,
)


def _convert(v2msg: str) -> str:
convertor = HL7v2ConversionController()
return convertor.convert(v2msg)
65 changes: 0 additions & 65 deletions src/convert_hl7v2_fhir/controllers/convertor.py

This file was deleted.

Empty file.
107 changes: 107 additions & 0 deletions src/convert_hl7v2_fhir/controllers/hl7/hl7_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from datetime import datetime
from enum import Enum, IntEnum
from typing import Optional
from uuid import uuid4, UUID


class HL7ErrorCode(IntEnum):
"""https://hl7-definition.caristix.com/v2/HL7v2.8/Tables/0357"""

ACCEPTED = 0
SEGMENT_SEQUENCE_ERROR = 100
REQUIRED_FIELD_MISSING = 101
DATA_TYPE_ERROR = 102
TABLE_VALUE_NOT_FOUND = 103
VALUE_TOO_LONG = 104
UNSUPPORTED_MESSAGE_TYPE = 200
UNSUPPORTED_EVENT_CODE = 201
UNSUPPORTED_PROCESSING_ID = 202
UNSUPPORTED_VERSION_ID = 203
UNKNOWN_KEY_IDENTIFIER = 204
DUPLICATE_KEY_IDENTIFIER = 205
APPLICATION_RECORD_LOCKED = 206
APPLICATION_INTERNAL_ERROR = 207


class HL7ErrorSeverity(str, Enum):
"""https://hl7-definition.caristix.com/v2/HL7v2.8/Tables/0516"""

ERROR = "E"
FATAL_ERROR = "F"
INFORMATION = "I"
WARNING = "W"


def generate_ack_message(
receiving_application: str,
receiving_facility: str,
replying_to_msgid: str,
care_provider_email: Optional[str] = None,
care_provider_orgname: Optional[str] = None,
hl7_error_code: Optional[HL7ErrorCode] = None,
error_severity: Optional[str] = None,
error_message: Optional[str] = None,
):
"""https://hl7-definition.caristix.com/v2/HL7v2.8/TriggerEvents/ACK"""

message_header = _generate_msh_segment(receiving_application, receiving_facility)

segment_err = ""
segment_zha = ""
accept_code = ""

if hl7_error_code:
# generate an ACK based on rejecting the message and processing no further
accept_code = "AR"
# for ERR layout, see https://hl7-definition.caristix.com/v2/HL7v2.8/Segments/ERR
segment_err = f"\rERR|||{hl7_error_code}|{error_severity}||||{error_message}"
else:
# generate an ACK based on accepting the message and sending care provider email as response
accept_code = "AA"
if care_provider_email:
# custom defined segment (see tech docs)
segment_zha = f"\rZHA|{care_provider_orgname}|{care_provider_email}"

# see https://hl7-definition.caristix.com/v2/HL7v2.8/Segments/MSA
segment_msa = f"MSA|{accept_code}|{replying_to_msgid}"
return message_header + "\r" + segment_msa + segment_err + segment_zha


def _generate_msh_segment(
receiving_application: str,
receiving_facility: str,
message_control_id: Optional[UUID] = None,
):
"""https://hl7-definition.caristix.com/v2/HL7v2.8/Segments/MSH"""

timestamp = datetime.now().strftime("%Y%m%d%H%M%S")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this format is right.. We're not specifying a timezone, which according to the spec means it defaults to being interpreted by anything that speaks HL7v2 as the local timezone. That means if we pass the output of this method to to_fhir_datetime below, which formats everything as UTC, we're actually an hour wrong for half the year. If we specify +0000 on the tail of the format string I think that removes any potential ambiguity.

msg_id = message_control_id or uuid4()
return f"MSH|^~\\&|HANS|NHSENGLAND|{receiving_application}|{receiving_facility}|{timestamp}||ACK^A01|{msg_id}|||||||||||||||"


def _generate_msa_segment(
accept_code: str,
message_control_id: str,
):
"""https://hl7-definition.caristix.com/v2/HL7v2.8/Segments/MSA"""

return f"MSA|{accept_code}|{message_control_id}"


def _generate_err_segment(
error_code: HL7ErrorCode,
error_severity: HL7ErrorSeverity,
error_message: str,
):
"""https://hl7-definition.caristix.com/v2/HL7v2.8/Segments/ERR"""

return f"ERR|||{error_code}|{error_severity}||||{error_message}"


def _generate_zha_segment(
care_provider_orgname: str,
care_provider_email: str,
):
"""custom defined segment (see tech docs)"""

return f"ZHA|{care_provider_orgname}|{care_provider_email}"
Loading