Coverage for cc_modules/cc_hl7.py : 13%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
3# noinspection HttpUrlsUsage
4"""
5camcops_server/cc_modules/cc_hl7.py
7===============================================================================
9 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com).
11 This file is part of CamCOPS.
13 CamCOPS is free software: you can redistribute it and/or modify
14 it under the terms of the GNU General Public License as published by
15 the Free Software Foundation, either version 3 of the License, or
16 (at your option) any later version.
18 CamCOPS is distributed in the hope that it will be useful,
19 but WITHOUT ANY WARRANTY; without even the implied warranty of
20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 GNU General Public License for more details.
23 You should have received a copy of the GNU General Public License
24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
26===============================================================================
28**Core HL7 functions, e.g. to build HL7 messages.**
30General HL7 sources:
32- https://python-hl7.readthedocs.org/en/latest/
33- http://www.interfaceware.com/manual/v3gen_python_library_details.html
34- http://www.interfaceware.com/hl7_video_vault.html#how
35- http://www.interfaceware.com/hl7-standard/hl7-segments.html
36- https://www.hl7.org/special/committees/vocab/v26_appendix_a.pdf
37- https://www.ncbi.nlm.nih.gov/pmc/articles/PMC130066/
39To consider
41- batched messages (HL7 batching protocol);
42 https://docs.oracle.com/cd/E23943_01/user.1111/e23486/app_hl7batching.htm
43- note: DG1 segment = diagnosis
45Basic HL7 message structure:
47- can package into HL7 2.X message as encapsulated PDF;
48 https://www.hl7standards.com/blog/2007/11/27/pdf-attachment-in-hl7-message/
49- message ORU^R01
50 https://www.corepointhealth.com/resource-center/hl7-resources/hl7-messages
51- MESSAGES: http://www.interfaceware.com/hl7-standard/hl7-messages.html
52- OBX segment = observation/result segment;
53 https://www.corepointhealth.com/resource-center/hl7-resources/hl7-obx-segment;
54 http://www.interfaceware.com/hl7-standard/hl7-segment-OBX.html
55- SEGMENTS:
56 https://www.corepointhealth.com/resource-center/hl7-resources/hl7-segments
57- ED field (= encapsulated data);
58 http://www.interfaceware.com/hl7-standard/hl7-fields.html
59- base-64 encoding
61We can then add an option for structure (XML), HTML, PDF export.
63"""
65import base64
66import logging
67import socket
68from typing import List, Optional, Tuple, TYPE_CHECKING, Union
70from cardinal_pythonlib.datetimefunc import format_datetime
71from cardinal_pythonlib.logs import BraceStyleAdapter
72import hl7
73from pendulum import Date, DateTime as Pendulum
75from camcops_server.cc_modules.cc_constants import DateFormat, FileType
76from camcops_server.cc_modules.cc_simpleobjects import HL7PatientIdentifier
78if TYPE_CHECKING:
79 from camcops_server.cc_modules.cc_request import CamcopsRequest
80 from camcops_server.cc_modules.cc_simpleobjects import TaskExportOptions
81 from camcops_server.cc_modules.cc_task import Task
83log = BraceStyleAdapter(logging.getLogger(__name__))
86# =============================================================================
87# Constants
88# =============================================================================
90# STRUCTURE OF HL7 MESSAGES
91# MESSAGE = list of segments, separated by carriage returns
92SEGMENT_SEPARATOR = "\r"
93# SEGMENT = list of fields (= composites), separated by pipes
94FIELD_SEPARATOR = "|"
95# FIELD (= COMPOSITE) = string, or list of components separated by carets
96COMPONENT_SEPARATOR = "^"
97# Component = string, or lists of subcomponents separated by ampersands
98SUBCOMPONENT_SEPARATOR = "&"
99# Subcomponents must be primitive data types (i.e. strings).
100# ... http://www.interfaceware.com/blog/hl7-composites/
102REPETITION_SEPARATOR = "~"
103ESCAPE_CHARACTER = "\\"
105# Fields are specified in terms of DATA TYPES:
106# http://www.corepointhealth.com/resource-center/hl7-resources/hl7-data-types
108# Some of those are COMPOSITE TYPES:
109# http://amisha.pragmaticdata.com/~gunther/oldhtml/composites.html#COMPOSITES
112# =============================================================================
113# HL7 helper functions
114# =============================================================================
116def get_mod11_checkdigit(strnum: str) -> str:
117 # noinspection HttpUrlsUsage
118 """
119 Input: string containing integer. Output: MOD11 check digit (string).
121 See:
123 - http://www.mexi.be/documents/hl7/ch200025.htm
124 - https://stackoverflow.com/questions/7006109
125 - http://www.pgrocer.net/Cis51/mod11.html
126 """
127 total = 0
128 multiplier = 2 # 2 for units digit, increases to 7, then resets to 2
129 try:
130 for i in reversed(range(len(strnum))):
131 total += int(strnum[i]) * multiplier
132 multiplier += 1
133 if multiplier == 8:
134 multiplier = 2
135 c = str(11 - (total % 11))
136 if c == "11":
137 c = "0"
138 elif c == "10":
139 c = "X"
140 return c
141 except (TypeError, ValueError):
142 # garbage in...
143 return ""
146def make_msh_segment(message_datetime: Pendulum,
147 message_control_id: str) -> hl7.Segment:
148 """
149 Creates an HL7 message header (MSH) segment.
151 - MSH: https://www.hl7.org/documentcenter/public/wg/conf/HL7MSH.htm
153 - We're making an ORU^R01 message = unsolicited result.
155 - ORU = Observational Report - Unsolicited
156 - ORU^R01 = Unsolicited transmission of an observation message
157 - https://www.corepointhealth.com/resource-center/hl7-resources/hl7-oru-message
158 - https://www.hl7kit.com/joomla/index.php/hl7resources/examples/107-orur01
159 """ # noqa
161 segment_id = "MSH"
162 encoding_characters = (COMPONENT_SEPARATOR + REPETITION_SEPARATOR +
163 ESCAPE_CHARACTER + SUBCOMPONENT_SEPARATOR)
164 sending_application = "CamCOPS"
165 sending_facility = ""
166 receiving_application = ""
167 receiving_facility = ""
168 date_time_of_message = format_datetime(message_datetime,
169 DateFormat.HL7_DATETIME)
170 security = ""
171 message_type = hl7.Field(COMPONENT_SEPARATOR, [
172 "ORU", # message type ID = Observ result/unsolicited
173 "R01" # trigger event ID = ORU/ACK - Unsolicited transmission
174 # of an observation message
175 ])
176 processing_id = "P" # production (processing mode: current)
177 version_id = "2.3" # HL7 version
178 sequence_number = ""
179 continuation_pointer = ""
180 accept_acknowledgement_type = ""
181 application_acknowledgement_type = "AL" # always
182 country_code = ""
183 character_set = "UNICODE UTF-8"
184 # http://wiki.hl7.org/index.php?title=Character_Set_used_in_v2_messages
185 principal_language_of_message = ""
187 fields = [
188 segment_id,
189 # field separator inserted automatically; HL7 standard considers it a
190 # field but the python-hl7 processor doesn't when it parses
191 encoding_characters,
192 sending_application,
193 sending_facility,
194 receiving_application,
195 receiving_facility,
196 date_time_of_message,
197 security,
198 message_type,
199 message_control_id,
200 processing_id,
201 version_id,
202 sequence_number,
203 continuation_pointer,
204 accept_acknowledgement_type,
205 application_acknowledgement_type,
206 country_code,
207 character_set,
208 principal_language_of_message,
209 ]
210 segment = hl7.Segment(FIELD_SEPARATOR, fields)
211 return segment
214def make_pid_segment(
215 forename: str,
216 surname: str,
217 dob: Date,
218 sex: str,
219 address: str,
220 patient_id_list: List[HL7PatientIdentifier] = None) -> hl7.Segment:
221 """
222 Creates an HL7 patient identification (PID) segment.
224 - https://www.corepointhealth.com/resource-center/hl7-resources/hl7-pid-segment
225 - https://www.hl7.org/documentcenter/public/wg/conf/Msgadt.pdf (s5.4.8)
227 - ID numbers...
228 https://www.cdc.gov/vaccines/programs/iis/technical-guidance/downloads/hl7guide-1-4-2012-08.pdf
229 """ # noqa
231 patient_id_list = patient_id_list or [] # type: List[HL7PatientIdentifier]
233 segment_id = "PID"
234 set_id = ""
236 # External ID
237 patient_external_id = ""
238 # ... this one is deprecated
239 # http://www.j4jayant.com/articles/hl7/16-patient-id
241 # Internal ID
242 internal_id_element_list = []
243 for i in range(len(patient_id_list)):
244 if not patient_id_list[i].pid:
245 continue
246 ptidentifier = patient_id_list[i]
247 pid = ptidentifier.pid
248 check_digit = get_mod11_checkdigit(pid)
249 check_digit_scheme = "M11" # Mod 11 algorithm
250 type_id = patient_id_list[i].id_type
251 assigning_authority = patient_id_list[i].assigning_authority
252 # Now, as per Table 4.6 "Extended composite ID" of
253 # hl7guide-1-4-2012-08.pdf:
254 internal_id_element = hl7.Field(COMPONENT_SEPARATOR, [
255 pid,
256 check_digit,
257 check_digit_scheme,
258 assigning_authority,
259 type_id # length "2..5" meaning 2-5
260 ])
261 internal_id_element_list.append(internal_id_element)
262 patient_internal_id = hl7.Field(REPETITION_SEPARATOR,
263 internal_id_element_list)
265 # Alternate ID
266 alternate_patient_id = ""
267 # ... this one is deprecated
268 # http://www.j4jayant.com/articles/hl7/16-patient-id
270 patient_name = hl7.Field(COMPONENT_SEPARATOR, [
271 forename, # surname
272 surname, # forename
273 "", # middle initial/name
274 "", # suffix (e.g. Jr, III)
275 "", # prefix (e.g. Dr)
276 "", # degree (e.g. MD)
277 ])
278 mothers_maiden_name = ""
279 date_of_birth = format_datetime(dob, DateFormat.HL7_DATE)
280 alias = ""
281 race = ""
282 country_code = ""
283 home_phone_number = ""
284 business_phone_number = ""
285 language = ""
286 marital_status = ""
287 religion = ""
288 account_number = ""
289 social_security_number = ""
290 drivers_license_number = ""
291 mother_identifier = ""
292 ethnic_group = ""
293 birthplace = ""
294 birth_order = ""
295 citizenship = ""
296 veterans_military_status = ""
298 fields = [
299 segment_id,
300 set_id, # PID.1
301 patient_external_id, # PID.2
302 patient_internal_id, # known as "PID-3" or "PID.3"
303 alternate_patient_id, # PID.4
304 patient_name,
305 mothers_maiden_name,
306 date_of_birth,
307 sex,
308 alias,
309 race,
310 address,
311 country_code,
312 home_phone_number,
313 business_phone_number,
314 language,
315 marital_status,
316 religion,
317 account_number,
318 social_security_number,
319 drivers_license_number,
320 mother_identifier,
321 ethnic_group,
322 birthplace,
323 birth_order,
324 citizenship,
325 veterans_military_status,
326 ]
327 segment = hl7.Segment(FIELD_SEPARATOR, fields)
328 return segment
331# noinspection PyUnusedLocal
332def make_obr_segment(task: "Task") -> hl7.Segment:
333 # noinspection HttpUrlsUsage
334 """
335 Creates an HL7 observation request (OBR) segment.
337 - http://hl7reference.com/HL7%20Specifications%20ORM-ORU.PDF
338 - Required in ORU^R01 message:
340 - https://www.corepointhealth.com/resource-center/hl7-resources/hl7-oru-message
341 - https://www.corepointhealth.com/resource-center/hl7-resources/hl7-obr-segment
342 """ # noqa
344 segment_id = "OBR"
345 set_id = "1"
346 placer_order_number = "CamCOPS"
347 filler_order_number = "CamCOPS"
348 universal_service_id = hl7.Field(COMPONENT_SEPARATOR, [
349 "CamCOPS",
350 "CamCOPS psychiatric/cognitive assessment"
351 ])
352 # unused below here, apparently
353 priority = ""
354 requested_date_time = ""
355 observation_date_time = ""
356 observation_end_date_time = ""
357 collection_volume = ""
358 collector_identifier = ""
359 specimen_action_code = ""
360 danger_code = ""
361 relevant_clinical_information = ""
362 specimen_received_date_time = ""
363 ordering_provider = ""
364 order_callback_phone_number = ""
365 placer_field_1 = ""
366 placer_field_2 = ""
367 filler_field_1 = ""
368 filler_field_2 = ""
369 results_report_status_change_date_time = ""
370 charge_to_practice = ""
371 diagnostic_service_section_id = ""
372 result_status = ""
373 parent_result = ""
374 quantity_timing = ""
375 result_copies_to = ""
376 parent = ""
377 transportation_mode = ""
378 reason_for_study = ""
379 principal_result_interpreter = ""
380 assistant_result_interpreter = ""
381 technician = ""
382 transcriptionist = ""
383 scheduled_date_time = ""
384 number_of_sample_containers = ""
385 transport_logistics_of_collected_samples = ""
386 collectors_comment = ""
387 transport_arrangement_responsibility = ""
388 transport_arranged = ""
389 escort_required = ""
390 planned_patient_transport_comment = ""
392 fields = [
393 segment_id,
394 set_id,
395 placer_order_number,
396 filler_order_number,
397 universal_service_id,
398 priority,
399 requested_date_time,
400 observation_date_time,
401 observation_end_date_time,
402 collection_volume,
403 collector_identifier,
404 specimen_action_code,
405 danger_code,
406 relevant_clinical_information,
407 specimen_received_date_time,
408 ordering_provider,
409 order_callback_phone_number,
410 placer_field_1,
411 placer_field_2,
412 filler_field_1,
413 filler_field_2,
414 results_report_status_change_date_time,
415 charge_to_practice,
416 diagnostic_service_section_id,
417 result_status,
418 parent_result,
419 quantity_timing,
420 result_copies_to,
421 parent,
422 transportation_mode,
423 reason_for_study,
424 principal_result_interpreter,
425 assistant_result_interpreter,
426 technician,
427 transcriptionist,
428 scheduled_date_time,
429 number_of_sample_containers,
430 transport_logistics_of_collected_samples,
431 collectors_comment,
432 transport_arrangement_responsibility,
433 transport_arranged,
434 escort_required,
435 planned_patient_transport_comment,
436 ]
437 segment = hl7.Segment(FIELD_SEPARATOR, fields)
438 return segment
441def make_obx_segment(req: "CamcopsRequest",
442 task: "Task",
443 task_format: str,
444 observation_identifier: str,
445 observation_datetime: Pendulum,
446 responsible_observer: str,
447 export_options: "TaskExportOptions") -> hl7.Segment:
448 # noinspection HttpUrlsUsage
449 """
450 Creates an HL7 observation result (OBX) segment.
452 - http://www.hl7standards.com/blog/2006/10/18/how-do-i-send-a-binary-file-inside-of-an-hl7-message
453 - http://www.hl7standards.com/blog/2007/11/27/pdf-attachment-in-hl7-message/
454 - http://www.hl7standards.com/blog/2006/12/01/sending-images-or-formatted-documents-via-hl7-messaging/
455 - https://www.hl7.org/documentcenter/public/wg/ca/HL7ClmAttIG.PDF
456 - type of data:
457 https://www.hl7.org/implement/standards/fhir/v2/0191/index.html
458 - subtype of data:
459 https://www.hl7.org/implement/standards/fhir/v2/0291/index.html
460 """ # noqa
462 segment_id = "OBX"
463 set_id = str(1)
465 source_application = "CamCOPS"
466 if task_format == FileType.PDF:
467 value_type = "ED" # Encapsulated data (ED) field
468 observation_value = hl7.Field(COMPONENT_SEPARATOR, [
469 source_application,
470 "Application", # type of data
471 "PDF", # data subtype
472 "Base64", # base 64 encoding
473 base64.standard_b64encode(task.get_pdf(req)) # data
474 ])
475 elif task_format == FileType.HTML:
476 value_type = "ED" # Encapsulated data (ED) field
477 observation_value = hl7.Field(COMPONENT_SEPARATOR, [
478 source_application,
479 "TEXT", # type of data
480 "HTML", # data subtype
481 "A", # no encoding (see table 0299), but need to escape
482 escape_hl7_text(task.get_html(req)) # data
483 ])
484 elif task_format == FileType.XML:
485 value_type = "ED" # Encapsulated data (ED) field
486 observation_value = hl7.Field(COMPONENT_SEPARATOR, [
487 source_application,
488 "TEXT", # type of data
489 "XML", # data subtype
490 "A", # no encoding (see table 0299), but need to escape
491 escape_hl7_text(task.get_xml(
492 req,
493 indent_spaces=0,
494 eol="",
495 options=export_options,
496 )) # data
497 ])
498 else:
499 raise AssertionError(
500 f"make_obx_segment: invalid task_format: {task_format}")
502 observation_sub_id = ""
503 units = ""
504 reference_range = ""
505 abnormal_flags = ""
506 probability = ""
507 nature_of_abnormal_test = ""
508 observation_result_status = ""
509 date_of_last_observation_normal_values = ""
510 user_defined_access_checks = ""
511 date_and_time_of_observation = format_datetime(
512 observation_datetime, DateFormat.HL7_DATETIME)
513 producer_id = ""
514 observation_method = ""
515 equipment_instance_identifier = ""
516 date_time_of_analysis = ""
518 fields = [
519 segment_id,
520 set_id,
521 value_type,
522 observation_identifier,
523 observation_sub_id,
524 observation_value,
525 units,
526 reference_range,
527 abnormal_flags,
528 probability,
529 nature_of_abnormal_test,
530 observation_result_status,
531 date_of_last_observation_normal_values,
532 user_defined_access_checks,
533 date_and_time_of_observation,
534 producer_id,
535 responsible_observer,
536 observation_method,
537 equipment_instance_identifier,
538 date_time_of_analysis,
539 ]
540 segment = hl7.Segment(FIELD_SEPARATOR, fields)
541 return segment
544def make_dg1_segment(set_id: int,
545 diagnosis_datetime: Pendulum,
546 coding_system: str,
547 diagnosis_identifier: str,
548 diagnosis_text: str,
549 alternate_coding_system: str = "",
550 alternate_diagnosis_identifier: str = "",
551 alternate_diagnosis_text: str = "",
552 diagnosis_type: str = "F",
553 diagnosis_classification: str = "D",
554 confidential_indicator: str = "N",
555 clinician_id_number: Union[str, int] = None,
556 clinician_surname: str = "",
557 clinician_forename: str = "",
558 clinician_middle_name_or_initial: str = "",
559 clinician_suffix: str = "",
560 clinician_prefix: str = "",
561 clinician_degree: str = "",
562 clinician_source_table: str = "",
563 clinician_assigning_authority: str = "",
564 clinician_name_type_code: str = "",
565 clinician_identifier_type_code: str = "",
566 clinician_assigning_facility: str = "",
567 attestation_datetime: Pendulum = None) \
568 -> hl7.Segment:
569 # noinspection HttpUrlsUsage
570 """
571 Creates an HL7 diagnosis (DG1) segment.
573 Args:
575 .. code-block:: none
577 set_id: Diagnosis sequence number, starting with 1 (use higher numbers
578 for >1 diagnosis).
579 diagnosis_datetime: Date/time diagnosis was made.
581 coding_system: E.g. "I9C" for ICD9-CM; "I10" for ICD10.
582 diagnosis_identifier: Code.
583 diagnosis_text: Text.
585 alternate_coding_system: Optional alternate coding system.
586 alternate_diagnosis_identifier: Optional alternate code.
587 alternate_diagnosis_text: Optional alternate text.
589 diagnosis_type: A admitting, W working, F final.
590 diagnosis_classification: C consultation, D diagnosis, M medication,
591 O other, R radiological scheduling, S sign and symptom,
592 T tissue diagnosis, I invasive procedure not classified elsewhere.
593 confidential_indicator: Y yes, N no
595 clinician_id_number: } Diagnosing clinician.
596 clinician_surname: }
597 clinician_forename: }
598 clinician_middle_name_or_initial: }
599 clinician_suffix: }
600 clinician_prefix: }
601 clinician_degree: }
602 clinician_source_table: }
603 clinician_assigning_authority: }
604 clinician_name_type_code: }
605 clinician_identifier_type_code: }
606 clinician_assigning_facility: }
608 attestation_datetime: Date/time the diagnosis was attested.
610 - http://www.mexi.be/documents/hl7/ch600012.htm
611 - https://www.hl7.org/special/committees/vocab/V26_Appendix_A.pdf
612 """
614 segment_id = "DG1"
615 try:
616 int(set_id)
617 set_id = str(set_id)
618 except Exception:
619 raise AssertionError("make_dg1_segment: set_id invalid")
620 diagnosis_coding_method = ""
621 diagnosis_code = hl7.Field(COMPONENT_SEPARATOR, [
622 diagnosis_identifier,
623 diagnosis_text,
624 coding_system,
625 alternate_diagnosis_identifier,
626 alternate_diagnosis_text,
627 alternate_coding_system,
628 ])
629 diagnosis_description = ""
630 diagnosis_datetime = format_datetime(diagnosis_datetime,
631 DateFormat.HL7_DATETIME)
632 if diagnosis_type not in ["A", "W", "F"]:
633 raise AssertionError("make_dg1_segment: diagnosis_type invalid")
634 major_diagnostic_category = ""
635 diagnostic_related_group = ""
636 drg_approval_indicator = ""
637 drg_grouper_review_code = ""
638 outlier_type = ""
639 outlier_days = ""
640 outlier_cost = ""
641 grouper_version_and_type = ""
642 diagnosis_priority = ""
644 try:
645 clinician_id_number = (
646 str(int(clinician_id_number))
647 if clinician_id_number is not None else ""
648 )
649 except Exception:
650 raise AssertionError("make_dg1_segment: diagnosing_clinician_id_number"
651 " invalid")
652 if clinician_id_number:
653 clinician_id_check_digit = get_mod11_checkdigit(clinician_id_number)
654 clinician_checkdigit_scheme = "M11" # Mod 11 algorithm
655 else:
656 clinician_id_check_digit = ""
657 clinician_checkdigit_scheme = ""
658 diagnosing_clinician = hl7.Field(COMPONENT_SEPARATOR, [
659 clinician_id_number,
660 clinician_surname or "",
661 clinician_forename or "",
662 clinician_middle_name_or_initial or "",
663 clinician_suffix or "",
664 clinician_prefix or "",
665 clinician_degree or "",
666 clinician_source_table or "",
667 clinician_assigning_authority or "",
668 clinician_name_type_code or "",
669 clinician_id_check_digit or "",
670 clinician_checkdigit_scheme or "",
671 clinician_identifier_type_code or "",
672 clinician_assigning_facility or "",
673 ])
675 if diagnosis_classification not in ["C", "D", "M", "O", "R", "S", "T",
676 "I"]:
677 raise AssertionError(
678 "make_dg1_segment: diagnosis_classification invalid")
679 if confidential_indicator not in ["Y", "N"]:
680 raise AssertionError(
681 "make_dg1_segment: confidential_indicator invalid")
682 attestation_datetime = (
683 format_datetime(attestation_datetime, DateFormat.HL7_DATETIME)
684 if attestation_datetime else ""
685 )
687 fields = [
688 segment_id,
689 set_id,
690 diagnosis_coding_method,
691 diagnosis_code,
692 diagnosis_description,
693 diagnosis_datetime,
694 diagnosis_type,
695 major_diagnostic_category,
696 diagnostic_related_group,
697 drg_approval_indicator,
698 drg_grouper_review_code,
699 outlier_type,
700 outlier_days,
701 outlier_cost,
702 grouper_version_and_type,
703 diagnosis_priority,
704 diagnosing_clinician,
705 diagnosis_classification,
706 confidential_indicator,
707 attestation_datetime,
708 ]
709 segment = hl7.Segment(FIELD_SEPARATOR, fields)
710 return segment
713def escape_hl7_text(s: str) -> str:
714 # noinspection HttpUrlsUsage
715 """
716 Escapes HL7 special characters.
718 - http://www.mexi.be/documents/hl7/ch200034.htm
719 - http://www.mexi.be/documents/hl7/ch200071.htm
720 """
721 esc_escape = ESCAPE_CHARACTER + ESCAPE_CHARACTER + ESCAPE_CHARACTER
722 esc_fieldsep = ESCAPE_CHARACTER + "F" + ESCAPE_CHARACTER
723 esc_componentsep = ESCAPE_CHARACTER + "S" + ESCAPE_CHARACTER
724 esc_subcomponentsep = ESCAPE_CHARACTER + "T" + ESCAPE_CHARACTER
725 esc_repetitionsep = ESCAPE_CHARACTER + "R" + ESCAPE_CHARACTER
727 # Linebreaks:
728 # http://www.healthintersections.com.au/?p=344
729 # https://groups.google.com/forum/#!topic/ensemble-in-healthcare/wP2DWMeFrPA # noqa
730 # http://www.hermetechnz.com/documentation/sqlschema/index.html?hl7_escape_rules.htm # noqa
731 esc_linebreak = ESCAPE_CHARACTER + ".br" + ESCAPE_CHARACTER
733 s = s.replace(ESCAPE_CHARACTER, esc_escape) # this one first!
734 s = s.replace(FIELD_SEPARATOR, esc_fieldsep)
735 s = s.replace(COMPONENT_SEPARATOR, esc_componentsep)
736 s = s.replace(SUBCOMPONENT_SEPARATOR, esc_subcomponentsep)
737 s = s.replace(REPETITION_SEPARATOR, esc_repetitionsep)
738 s = s.replace("\n", esc_linebreak)
739 return s
742def msg_is_successful_ack(msg: hl7.Message) -> Tuple[bool, Optional[str]]:
743 """
744 Checks whether msg represents a successful acknowledgement message.
746 - http://hl7reference.com/HL7%20Specifications%20ORM-ORU.PDF
747 """
749 if msg is None:
750 return False, "Reply is None"
752 # Get segments (MSH, MSA)
753 if len(msg) != 2:
754 return False, f"Reply doesn't have 2 segments (has {len(msg)})"
755 msh_segment = msg[0]
756 msa_segment = msg[1]
758 # Check MSH segment
759 if len(msh_segment) < 9:
760 return False, (
761 f"First (MSH) segment has <9 fields (has {len(msh_segment)})"
762 )
763 msh_segment_id = msh_segment[0]
764 msh_message_type = msh_segment[8]
765 if msh_segment_id != ["MSH"]:
766 return False, (
767 f"First (MSH) segment ID is not 'MSH' (is {msh_segment_id})"
768 )
769 if msh_message_type != ["ACK"]:
770 return False, (
771 f"MSH message type is not 'ACK' (is {msh_message_type})"
772 )
774 # Check MSA segment
775 if len(msa_segment) < 2:
776 return False, (
777 f"Second (MSA) segment has <2 fields (has {len(msa_segment)})"
778 )
779 msa_segment_id = msa_segment[0]
780 msa_acknowledgment_code = msa_segment[1]
781 if msa_segment_id != ["MSA"]:
782 return False, (
783 f"Second (MSA) segment ID is not 'MSA' (is {msa_segment_id})"
784 )
785 if msa_acknowledgment_code != ["AA"]:
786 # AA for success, AE for error
787 return False, (
788 f"MSA acknowledgement code is not 'AA' "
789 f"(is {msa_acknowledgment_code})"
790 )
792 return True, None
795# =============================================================================
796# MLLPTimeoutClient
797# =============================================================================
798# Modification of MLLPClient from python-hl7, to allow timeouts and failure.
800SB = '\x0b' # <SB>, vertical tab
801EB = '\x1c' # <EB>, file separator
802CR = '\x0d' # <CR>, \r
803FF = '\x0c' # <FF>, new page form feed
805RECV_BUFFER = 4096
808class MLLPTimeoutClient(object):
809 """
810 Class for MLLP TCP/IP transmission that implements timeouts.
811 """
813 def __init__(self, host: str, port: int, timeout_ms: int = None) -> None:
814 """Creates MLLP client and opens socket."""
815 self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
816 timeout_s = float(timeout_ms) / float(1000) \
817 if timeout_ms is not None else None
818 self.socket.settimeout(timeout_s)
819 self.socket.connect((host, port))
820 self.encoding = "utf-8"
822 def __enter__(self):
823 """
824 For use with "with" statement.
825 """
826 return self
828 # noinspection PyUnusedLocal
829 def __exit__(self, exc_type, exc_val, traceback):
830 """
831 For use with "with" statement.
832 """
833 self.close()
835 def close(self):
836 """
837 Release the socket connection.
838 """
839 self.socket.close()
841 def send_message(self, message: Union[str, hl7.Message]) \
842 -> Tuple[bool, Optional[str]]:
843 """
844 Wraps a string or :class:`hl7.Message` in a MLLP container
845 and sends the message to the server.
847 Returns ``success, ack_msg``.
848 """
849 if isinstance(message, hl7.Message):
850 message = str(message)
851 # wrap in MLLP message container
852 data = SB + message + CR + EB + CR
853 # ... the CR immediately after the message is my addition, because
854 # HL7 Inspector otherwise says: "Warning: last segment have no segment
855 # termination char 0x0d !" (sic).
856 return self.send(data.encode(self.encoding))
858 def send(self, data: bytes) -> Tuple[bool, Optional[str]]:
859 """
860 Low-level, direct access to the ``socket.send`` function (data must be
861 already wrapped in an MLLP container). Blocks until the server
862 returns.
864 Returns ``success, ack_msg``.
865 """
866 # upload the data
867 self.socket.send(data)
868 # wait for the ACK/NACK
869 try:
870 ack_msg = self.socket.recv(RECV_BUFFER).decode(self.encoding)
871 return True, ack_msg
872 except socket.timeout:
873 return False, None