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