Hide keyboard shortcuts

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 

2 

3# noinspection HttpUrlsUsage 

4""" 

5camcops_server/cc_modules/cc_hl7.py 

6 

7=============================================================================== 

8 

9 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

10 

11 This file is part of CamCOPS. 

12 

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. 

17 

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. 

22 

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/>. 

25 

26=============================================================================== 

27 

28**Core HL7 functions, e.g. to build HL7 messages.** 

29 

30General HL7 sources: 

31 

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/ 

38 

39To consider 

40 

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 

44 

45Basic HL7 message structure: 

46 

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 

60 

61We can then add an option for structure (XML), HTML, PDF export. 

62 

63""" 

64 

65import base64 

66import logging 

67import socket 

68from typing import List, Optional, Tuple, TYPE_CHECKING, Union 

69 

70from cardinal_pythonlib.datetimefunc import format_datetime 

71from cardinal_pythonlib.logs import BraceStyleAdapter 

72import hl7 

73from pendulum import Date, DateTime as Pendulum 

74 

75from camcops_server.cc_modules.cc_constants import DateFormat, FileType 

76from camcops_server.cc_modules.cc_simpleobjects import HL7PatientIdentifier 

77 

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 

82 

83log = BraceStyleAdapter(logging.getLogger(__name__)) 

84 

85 

86# ============================================================================= 

87# Constants 

88# ============================================================================= 

89 

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/ 

101 

102REPETITION_SEPARATOR = "~" 

103ESCAPE_CHARACTER = "\\" 

104 

105# Fields are specified in terms of DATA TYPES: 

106# http://www.corepointhealth.com/resource-center/hl7-resources/hl7-data-types 

107 

108# Some of those are COMPOSITE TYPES: 

109# http://amisha.pragmaticdata.com/~gunther/oldhtml/composites.html#COMPOSITES 

110 

111 

112# ============================================================================= 

113# HL7 helper functions 

114# ============================================================================= 

115 

116def get_mod11_checkdigit(strnum: str) -> str: 

117 # noinspection HttpUrlsUsage 

118 """ 

119 Input: string containing integer. Output: MOD11 check digit (string). 

120 

121 See: 

122 

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 "" 

144 

145 

146def make_msh_segment(message_datetime: Pendulum, 

147 message_control_id: str) -> hl7.Segment: 

148 """ 

149 Creates an HL7 message header (MSH) segment. 

150 

151 - MSH: https://www.hl7.org/documentcenter/public/wg/conf/HL7MSH.htm 

152 

153 - We're making an ORU^R01 message = unsolicited result. 

154 

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 

160 

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 = "" 

186 

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 

212 

213 

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. 

223 

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) 

226 

227 - ID numbers... 

228 https://www.cdc.gov/vaccines/programs/iis/technical-guidance/downloads/hl7guide-1-4-2012-08.pdf 

229 """ # noqa 

230 

231 patient_id_list = patient_id_list or [] # type: List[HL7PatientIdentifier] 

232 

233 segment_id = "PID" 

234 set_id = "" 

235 

236 # External ID 

237 patient_external_id = "" 

238 # ... this one is deprecated 

239 # http://www.j4jayant.com/articles/hl7/16-patient-id 

240 

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) 

264 

265 # Alternate ID 

266 alternate_patient_id = "" 

267 # ... this one is deprecated 

268 # http://www.j4jayant.com/articles/hl7/16-patient-id 

269 

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 = "" 

297 

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 

329 

330 

331# noinspection PyUnusedLocal 

332def make_obr_segment(task: "Task") -> hl7.Segment: 

333 # noinspection HttpUrlsUsage 

334 """ 

335 Creates an HL7 observation request (OBR) segment. 

336 

337 - http://hl7reference.com/HL7%20Specifications%20ORM-ORU.PDF 

338 - Required in ORU^R01 message: 

339 

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 

343 

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 = "" 

391 

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 

439 

440 

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. 

451 

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 

461 

462 segment_id = "OBX" 

463 set_id = str(1) 

464 

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}") 

501 

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 = "" 

517 

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 

542 

543 

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. 

572 

573 Args: 

574 

575 .. code-block:: none 

576 

577 set_id: Diagnosis sequence number, starting with 1 (use higher numbers 

578 for >1 diagnosis). 

579 diagnosis_datetime: Date/time diagnosis was made. 

580 

581 coding_system: E.g. "I9C" for ICD9-CM; "I10" for ICD10. 

582 diagnosis_identifier: Code. 

583 diagnosis_text: Text. 

584 

585 alternate_coding_system: Optional alternate coding system. 

586 alternate_diagnosis_identifier: Optional alternate code. 

587 alternate_diagnosis_text: Optional alternate text. 

588 

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 

594 

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: } 

607 

608 attestation_datetime: Date/time the diagnosis was attested. 

609 

610 - http://www.mexi.be/documents/hl7/ch600012.htm 

611 - https://www.hl7.org/special/committees/vocab/V26_Appendix_A.pdf 

612 """ 

613 

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 = "" 

643 

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 ]) 

674 

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 ) 

686 

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 

711 

712 

713def escape_hl7_text(s: str) -> str: 

714 # noinspection HttpUrlsUsage 

715 """ 

716 Escapes HL7 special characters. 

717 

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 

726 

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 

732 

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 

740 

741 

742def msg_is_successful_ack(msg: hl7.Message) -> Tuple[bool, Optional[str]]: 

743 """ 

744 Checks whether msg represents a successful acknowledgement message. 

745 

746 - http://hl7reference.com/HL7%20Specifications%20ORM-ORU.PDF 

747 """ 

748 

749 if msg is None: 

750 return False, "Reply is None" 

751 

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] 

757 

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 ) 

773 

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 ) 

791 

792 return True, None 

793 

794 

795# ============================================================================= 

796# MLLPTimeoutClient 

797# ============================================================================= 

798# Modification of MLLPClient from python-hl7, to allow timeouts and failure. 

799 

800SB = '\x0b' # <SB>, vertical tab 

801EB = '\x1c' # <EB>, file separator 

802CR = '\x0d' # <CR>, \r 

803FF = '\x0c' # <FF>, new page form feed 

804 

805RECV_BUFFER = 4096 

806 

807 

808class MLLPTimeoutClient(object): 

809 """ 

810 Class for MLLP TCP/IP transmission that implements timeouts. 

811 """ 

812 

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" 

821 

822 def __enter__(self): 

823 """ 

824 For use with "with" statement. 

825 """ 

826 return self 

827 

828 # noinspection PyUnusedLocal 

829 def __exit__(self, exc_type, exc_val, traceback): 

830 """ 

831 For use with "with" statement. 

832 """ 

833 self.close() 

834 

835 def close(self): 

836 """ 

837 Release the socket connection. 

838 """ 

839 self.socket.close() 

840 

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. 

846 

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)) 

857 

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. 

863 

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