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

1#!/usr/bin/env python 

2 

3# noinspection HttpUrlsUsage 

4""" 

5camcops_server/cc_modules/cc_hl7.py 

6 

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

8 

9 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

10 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

11 

12 This file is part of CamCOPS. 

13 

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. 

18 

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. 

23 

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

26 

27=============================================================================== 

28 

29**Core HL7 functions, e.g. to build HL7 v2 messages.** 

30 

31General HL7 sources: 

32 

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/ 

39 

40To consider 

41 

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 

45 

46Basic HL7 message structure: 

47 

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 

61 

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

63 

64""" 

65 

66import base64 

67import logging 

68import socket 

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

70 

71from cardinal_pythonlib.datetimefunc import format_datetime 

72from cardinal_pythonlib.logs import BraceStyleAdapter 

73import hl7 

74from pendulum import Date, DateTime as Pendulum 

75 

76from camcops_server.cc_modules.cc_constants import DateFormat, FileType 

77from camcops_server.cc_modules.cc_simpleobjects import HL7PatientIdentifier 

78 

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 

83 

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

85 

86 

87# ============================================================================= 

88# Constants 

89# ============================================================================= 

90 

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/ 

102 

103REPETITION_SEPARATOR = "~" 

104ESCAPE_CHARACTER = "\\" 

105 

106# Fields are specified in terms of DATA TYPES: 

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

108 

109# Some of those are COMPOSITE TYPES: 

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

111 

112 

113# ============================================================================= 

114# HL7 helper functions 

115# ============================================================================= 

116 

117 

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

119 # noinspection HttpUrlsUsage 

120 """ 

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

122 

123 See: 

124 

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

146 

147 

148def make_msh_segment( 

149 message_datetime: Pendulum, message_control_id: str 

150) -> hl7.Segment: 

151 """ 

152 Creates an HL7 message header (MSH) segment. 

153 

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

155 

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

157 

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 

163 

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

197 

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 

223 

224 

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. 

235 

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) 

238 

239 - ID numbers... 

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

241 """ # noqa 

242 

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

244 

245 segment_id = "PID" 

246 set_id = "" 

247 

248 # External ID 

249 patient_external_id = "" 

250 # ... this one is deprecated 

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

252 

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 ) 

280 

281 # Alternate ID 

282 alternate_patient_id = "" 

283 # ... this one is deprecated 

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

285 

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

316 

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 

348 

349 

350# noinspection PyUnusedLocal 

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

352 # noinspection HttpUrlsUsage 

353 """ 

354 Creates an HL7 observation request (OBR) segment. 

355 

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

357 - Required in ORU^R01 message: 

358 

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 

362 

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

410 

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 

458 

459 

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. 

472 

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 

482 

483 segment_id = "OBX" 

484 set_id = str(1) 

485 

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 ) 

531 

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

548 

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 

573 

574 

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. 

604 

605 Args: 

606 

607 .. code-block:: none 

608 

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

610 for >1 diagnosis). 

611 diagnosis_datetime: Date/time diagnosis was made. 

612 

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

614 diagnosis_identifier: Code. 

615 diagnosis_text: Text. 

616 

617 alternate_coding_system: Optional alternate coding system. 

618 alternate_diagnosis_identifier: Optional alternate code. 

619 alternate_diagnosis_text: Optional alternate text. 

620 

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 

626 

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

639 

640 attestation_datetime: Date/time the diagnosis was attested. 

641 

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

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

644 """ 

645 

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

679 

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 ) 

715 

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 ) 

738 

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 

763 

764 

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

766 # noinspection HttpUrlsUsage 

767 """ 

768 Escapes HL7 special characters. 

769 

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 

778 

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 

784 

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 

792 

793 

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. 

798 

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

800 """ 

801 

802 if msg is None: 

803 return False, "Reply is None" 

804 

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] 

810 

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 ) 

829 

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 ) 

852 

853 return True, None 

854 

855 

856# ============================================================================= 

857# MLLPTimeoutClient 

858# ============================================================================= 

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

860 

861SB = "\x0b" # <SB>, vertical tab 

862EB = "\x1c" # <EB>, file separator 

863CR = "\x0d" # <CR>, \r 

864FF = "\x0c" # <FF>, new page form feed 

865 

866RECV_BUFFER = 4096 

867 

868 

869class MLLPTimeoutClient(object): 

870 """ 

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

872 """ 

873 

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" 

883 

884 def __enter__(self): 

885 """ 

886 For use with "with" statement. 

887 """ 

888 return self 

889 

890 # noinspection PyUnusedLocal 

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

892 """ 

893 For use with "with" statement. 

894 """ 

895 self.close() 

896 

897 def close(self): 

898 """ 

899 Release the socket connection. 

900 """ 

901 self.socket.close() 

902 

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. 

909 

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

920 

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. 

926 

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