1
2
3 __doc__ = """GNUmed DICOM handling middleware"""
4
5 __license__ = "GPL v2 or later"
6 __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>"
7
8
9
10 import io
11 import os
12 import sys
13 import re as regex
14 import logging
15 import http.client
16 import socket
17 import httplib2
18 import json
19 import zipfile
20 import shutil
21 import time
22 import datetime as pydt
23 from urllib.parse import urlencode
24 import distutils.version as version
25
26
27
28 if __name__ == '__main__':
29 sys.path.insert(0, '../../')
30 from Gnumed.pycommon import gmTools
31 from Gnumed.pycommon import gmShellAPI
32 from Gnumed.pycommon import gmMimeLib
33
34
35
36 _log = logging.getLogger('gm.dicom')
37
38 _map_gender_gm2dcm = {
39 'm': 'M',
40 'f': 'F',
41 'tm': 'M',
42 'tf': 'F',
43 'h': None
44 }
45
46
48
49
50
51
52
53
54
55
56
57
58 - def connect(self, host, port, user, password, expected_minimal_version=None, expected_name=None, expected_aet=None):
59 try:
60 int(port)
61 except Exception:
62 _log.error('invalid port [%s]', port)
63 return False
64 if (host is None) or (host.strip() == ''):
65 host = 'localhost'
66 try:
67 self.__server_url = str('http://%s:%s' % (host, port))
68 except Exception:
69 _log.exception('cannot create server url from: host [%s] and port [%s]', host, port)
70 return False
71 self.__user = user
72 self.__password = password
73 _log.info('connecting as [%s] to Orthanc server at [%s]', self.__user, self.__server_url)
74 cache_dir = os.path.join(gmTools.gmPaths().user_tmp_dir, '.orthanc2gm-cache')
75 gmTools.mkdir(cache_dir)
76 _log.debug('using cache directory: %s', cache_dir)
77 self.__conn = httplib2.Http(cache = cache_dir)
78 self.__conn.add_credentials(self.__user, self.__password)
79 _log.debug('connected to server: %s', self.server_identification)
80 self.connect_error = ''
81 if self.server_identification is False:
82 self.connect_error += 'retrieving server identification failed'
83 return False
84 if expected_minimal_version is not None:
85 if version.LooseVersion(self.server_identification['Version']) < version.LooseVersion(expected_min_version):
86 _log.error('server too old, needed [%s]', expected_min_version)
87 self.connect_error += 'server too old, needed version [%s]' % expected_min_version
88 return False
89 if expected_name is not None:
90 if self.server_identification['Name'] != expected_name:
91 _log.error('wrong server name, expected [%s]', expected_name)
92 self.connect_error += 'wrong server name, expected [%s]' % expected_name
93 return False
94 if expected_aet is not None:
95 if self.server_identification['DicomAet'] != expected_name:
96 _log.error('wrong server AET, expected [%s]', expected_aet)
97 self.connect_error += 'wrong server AET, expected [%s]' % expected_aet
98 return False
99 return True
100
101
103 try:
104 return self.__server_identification
105 except AttributeError:
106 pass
107 system_data = self.__run_GET(url = '%s/system' % self.__server_url)
108 if system_data is False:
109 _log.error('unable to get server identification')
110 return False
111 _log.debug('server: %s', system_data)
112 self.__server_identification = system_data
113
114 self.__initial_orthanc_encoding = self.__run_GET(url = '%s/tools/default-encoding' % self.__server_url)
115 _log.debug('initial Orthanc encoding: %s', self.__initial_orthanc_encoding)
116
117
118 tolerance = 60
119 client_now_as_utc = pydt.datetime.utcnow()
120 start = time.time()
121 orthanc_now_str = self.__run_GET(url = '%s/tools/now' % self.__server_url)
122 end = time.time()
123 query_duration = end - start
124 orthanc_now_unknown_tz = pydt.datetime.strptime(orthanc_now_str, '%Y%m%dT%H%M%S')
125 _log.debug('GNUmed "now" (UTC): %s', client_now_as_utc)
126 _log.debug('Orthanc "now" (UTC): %s', orthanc_now_unknown_tz)
127 _log.debug('wire roundtrip (seconds): %s', query_duration)
128 _log.debug('maximum skew tolerance (seconds): %s', tolerance)
129 if query_duration > tolerance:
130 _log.info('useless to check GNUmed/Orthanc time skew, wire roundtrip (%s) > tolerance (%s)', query_duration, tolerance)
131 else:
132 if orthanc_now_unknown_tz > client_now_as_utc:
133 real_skew = orthanc_now_unknown_tz - client_now_as_utc
134 else:
135 real_skew = client_now_as_utc - orthanc_now_unknown_tz
136 _log.info('GNUmed/Orthanc time skew: %s', real_skew)
137 if real_skew > pydt.timedelta(seconds = tolerance):
138 _log.error('GNUmed/Orthanc time skew > tolerance (may be due to timezone differences on Orthanc < v1.3.2)')
139
140 return self.__server_identification
141
142 server_identification = property(_get_server_identification, lambda x:x)
143
144
146
147 return 'Orthanc::%(Name)s::%(DicomAet)s' % self.__server_identification
148
149 as_external_id_issuer = property(_get_as_external_id_issuer, lambda x:x)
150
151
153 if self.__user is None:
154 return self.__server_url
155 return self.__server_url.replace('http://', 'http://%s@' % self.__user)
156
157 url_browse_patients = property(_get_url_browse_patients, lambda x:x)
158
159
163
164
168
169
170
171
173 _log.info('searching for Orthanc patients matching %s', person)
174
175
176 pacs_ids = person.get_external_ids(id_type = 'PACS', issuer = self.as_external_id_issuer)
177 if len(pacs_ids) > 1:
178 _log.error('GNUmed patient has more than one ID for this PACS: %s', pacs_ids)
179 _log.error('the PACS ID is expected to be unique per PACS')
180 return []
181
182 pacs_ids2use = []
183
184 if len(pacs_ids) == 1:
185 pacs_ids2use.append(pacs_ids[0]['value'])
186 pacs_ids2use.extend(person.suggest_external_ids(target = 'PACS'))
187
188 for pacs_id in pacs_ids2use:
189 _log.debug('using PACS ID [%s]', pacs_id)
190 pats = self.get_patients_by_external_id(external_id = pacs_id)
191 if len(pats) > 1:
192 _log.warning('more than one Orthanc patient matches PACS ID: %s', pacs_id)
193 if len(pats) > 0:
194 return pats
195
196 _log.debug('no matching patient found in PACS')
197
198
199
200
201
202
203 return []
204
205
207 matching_patients = []
208 _log.info('searching for patients with external ID >>>%s<<<', external_id)
209
210
211 search_data = {
212 'Level': 'Patient',
213 'CaseSensitive': False,
214 'Expand': True,
215 'Query': {'PatientID': external_id.strip('*')}
216 }
217 _log.info('server-side C-FIND SCU over REST search, mogrified search data: %s', search_data)
218 matches = self.__run_POST(url = '%s/tools/find' % self.__server_url, data = search_data)
219
220
221 for match in matches:
222 self.protect_patient(orthanc_id = match['ID'])
223 return matches
224
225
226
227
228
229
230
231
232
233
234
235
236
237
239 _log.info('name parts %s, gender [%s], dob [%s], fuzzy: %s', name_parts, gender, dob, fuzzy)
240 if len(name_parts) > 1:
241 return self.get_patients_by_name_parts(name_parts = name_parts, gender = gender, dob = dob, fuzzy = fuzzy)
242 if not fuzzy:
243 search_term = name_parts[0].strip('*')
244 else:
245 search_term = name_parts[0]
246 if not search_term.endswith('*'):
247 search_term += '*'
248 search_data = {
249 'Level': 'Patient',
250 'CaseSensitive': False,
251 'Expand': True,
252 'Query': {'PatientName': search_term}
253 }
254 if gender is not None:
255 gender = _map_gender_gm2dcm[gender.lower()]
256 if gender is not None:
257 search_data['Query']['PatientSex'] = gender
258 if dob is not None:
259 search_data['Query']['PatientBirthDate'] = dob.strftime('%Y%m%d')
260 _log.info('server-side C-FIND SCU over REST search, mogrified search data: %s', search_data)
261 matches = self.__run_POST(url = '%s/tools/find' % self.__server_url, data = search_data)
262 return matches
263
264
266
267 matching_patients = []
268 clean_parts = []
269 for part in name_parts:
270 if part.strip() == '':
271 continue
272 clean_parts.append(part.lower().strip())
273 _log.info('client-side patient search, scrubbed search terms: %s', clean_parts)
274 pat_ids = self.__run_GET(url = '%s/patients' % self.__server_url)
275 if pat_ids is False:
276 _log.error('cannot retrieve patients')
277 return []
278 for pat_id in pat_ids:
279 orthanc_pat = self.__run_GET(url = '%s/patients/%s' % (self.__server_url, pat_id))
280 if orthanc_pat is False:
281 _log.error('cannot retrieve patient')
282 continue
283 orthanc_name = orthanc_pat['MainDicomTags']['PatientName'].lower().strip()
284 if not fuzzy:
285 orthanc_name = orthanc_name.replace(' ', ',').replace('^', ',').split(',')
286 parts_in_orthanc_name = 0
287 for part in clean_parts:
288 if part in orthanc_name:
289 parts_in_orthanc_name += 1
290 if parts_in_orthanc_name == len(clean_parts):
291 _log.debug('name match: "%s" contains all of %s', orthanc_name, clean_parts)
292 if gender is not None:
293 gender = _map_gender_gm2dcm[gender.lower()]
294 if gender is not None:
295 if orthanc_pat['MainDicomTags']['PatientSex'].lower() != gender:
296 _log.debug('gender mismatch: dicom=[%s] gnumed=[%s], skipping', orthanc_pat['MainDicomTags']['PatientSex'], gender)
297 continue
298 if dob is not None:
299 if orthanc_pat['MainDicomTags']['PatientBirthDate'] != dob.strftime('%Y%m%d'):
300 _log.debug('dob mismatch: dicom=[%s] gnumed=[%s], skipping', orthanc_pat['MainDicomTags']['PatientBirthDate'], dob)
301 continue
302 matching_patients.append(orthanc_pat)
303 else:
304 _log.debug('name mismatch: "%s" does not contain all of %s', orthanc_name, clean_parts)
305 return matching_patients
306
307
312
313
318
319
328
329
338
339
349
350
352
353 if filename is None:
354 filename = gmTools.get_unique_filename(prefix = r'DCM-', suffix = r'.zip', tmp_dir = target_dir)
355
356
357 if study_ids is None:
358 _log.info('exporting all studies of patient [%s] into [%s]', patient_id, filename)
359 f = io.open(filename, 'wb')
360 url = '%s/patients/%s/media' % (self.__server_url, str(patient_id))
361 _log.debug(url)
362 f.write(self.__run_GET(url = url, allow_cached = True))
363 f.close()
364 if create_zip:
365 return filename
366 if target_dir is None:
367 target_dir = gmTools.mk_sandbox_dir(prefix = 'dcm-')
368 if not gmTools.unzip_archive(filename, target_dir = target_dir, remove_archive = True):
369 return False
370 return target_dir
371
372
373 dicomdir_cmd = 'gm-create_dicomdir'
374 found, external_cmd = gmShellAPI.detect_external_binary(dicomdir_cmd)
375 if not found:
376 _log.error('[%s] not found', dicomdir_cmd)
377 return False
378
379 if create_zip:
380 sandbox_dir = gmTools.mk_sandbox_dir(prefix = 'dcm-')
381 _log.info('exporting studies [%s] into [%s] (sandbox [%s])', study_ids, filename, sandbox_dir)
382 else:
383 sandbox_dir = target_dir
384 _log.info('exporting studies [%s] into [%s]', study_ids, sandbox_dir)
385 _log.debug('sandbox dir: %s', sandbox_dir)
386 idx = 0
387 for study_id in study_ids:
388 study_zip_name = gmTools.get_unique_filename(prefix = 'dcm-', suffix = '.zip')
389
390 study_zip_name = self.get_study_as_zip_with_dicomdir(study_id = study_id, filename = study_zip_name)
391
392 idx += 1
393 study_unzip_dir = os.path.join(sandbox_dir, 'STUDY%s' % idx)
394 _log.debug('study [%s] -> %s -> %s', study_id, study_zip_name, study_unzip_dir)
395
396
397 if not gmTools.unzip_archive(study_zip_name, target_dir = study_unzip_dir, remove_archive = True):
398 return False
399
400
401
402 target_dicomdir_name = os.path.join(sandbox_dir, 'DICOMDIR')
403 gmTools.remove_file(target_dicomdir_name, log_error = False)
404 _log.debug('generating [%s]', target_dicomdir_name)
405 cmd = '%(cmd)s %(DICOMDIR)s %(startdir)s' % {
406 'cmd': external_cmd,
407 'DICOMDIR': target_dicomdir_name,
408 'startdir': sandbox_dir
409 }
410 success = gmShellAPI.run_command_in_shell (
411 command = cmd,
412 blocking = True
413 )
414 if not success:
415 _log.error('problem running [gm-create_dicomdir]')
416 return False
417
418 try:
419 io.open(target_dicomdir_name)
420 except Exception:
421 _log.error('[%s] not generated, aborting', target_dicomdir_name)
422 return False
423
424
425 if not create_zip:
426 return sandbox_dir
427
428
429 studies_zip = shutil.make_archive (
430 gmTools.fname_stem_with_path(filename),
431 'zip',
432 root_dir = gmTools.parent_dir(sandbox_dir),
433 base_dir = gmTools.dirname_stem(sandbox_dir),
434 logger = _log
435 )
436 _log.debug('archived all studies with one DICOMDIR into: %s', studies_zip)
437
438 gmTools.rmdir(sandbox_dir)
439 return studies_zip
440
441
443
444 if filename is None:
445 filename = gmTools.get_unique_filename(prefix = r'DCM-', suffix = r'.zip', tmp_dir = target_dir)
446
447
448 if study_ids is None:
449 if patient_id is None:
450 raise ValueError('<patient_id> must be defined if <study_ids> is None')
451 _log.info('exporting all studies of patient [%s] into [%s]', patient_id, filename)
452 f = io.open(filename, 'wb')
453 url = '%s/patients/%s/media' % (self.__server_url, str(patient_id))
454 _log.debug(url)
455 f.write(self.__run_GET(url = url, allow_cached = True))
456 f.close()
457 if create_zip:
458 return filename
459 if target_dir is None:
460 target_dir = gmTools.mk_sandbox_dir(prefix = 'dcm-')
461 if not gmTools.unzip_archive(filename, target_dir = target_dir, remove_archive = True):
462 return False
463 return target_dir
464
465
466 _log.info('exporting %s studies into [%s]', len(study_ids), filename)
467 _log.debug('studies: %s', study_ids)
468 f = io.open(filename, 'wb')
469
470
471
472
473
474 url = '%s/tools/create-media-extended' % self.__server_url
475 _log.debug(url)
476 try:
477 downloaded = self.__run_POST(url = url, data = study_ids, output_file = f)
478 if not downloaded:
479 _log.error('this Orthanc version probably does not support "create-media-extended"')
480 except TypeError:
481 f.close()
482 _log.exception('cannot retrieve multiple studies as one archive with DICOMDIR, probably not supported by this Orthanc version')
483 return False
484
485 if not downloaded:
486 url = '%s/tools/create-media' % self.__server_url
487 _log.debug('retrying: %s', url)
488 try:
489 downloaded = self.__run_POST(url = url, data = study_ids, output_file = f)
490 if not downloaded:
491 return False
492 except TypeError:
493 _log.exception('cannot retrieve multiple studies as one archive with DICOMDIR, probably not supported by this Orthanc version')
494 return False
495 finally:
496 f.close()
497 if create_zip:
498 return filename
499 if target_dir is None:
500 target_dir = gmTools.mk_sandbox_dir(prefix = 'dcm-')
501 _log.debug('exporting studies into [%s]', target_dir)
502 if not gmTools.unzip_archive(filename, target_dir = target_dir, remove_archive = True):
503 return False
504 return target_dir
505
506
514
515
526
527
538
539
540
541
543 url = '%s/patients/%s/protected' % (self.__server_url, str(orthanc_id))
544 if self.__run_GET(url) == 1:
545 _log.debug('patient already protected: %s', orthanc_id)
546 return True
547 _log.warning('patient [%s] not protected against recycling, enabling protection now', orthanc_id)
548 self.__run_PUT(url = url, data = '1')
549 if self.__run_GET(url) == 1:
550 return True
551 _log.error('cannot protect patient [%s] against recycling', orthanc_id)
552 return False
553
554
556 url = '%s/patients/%s/protected' % (self.__server_url, str(orthanc_id))
557 if self.__run_GET(url) == 0:
558 return True
559 _log.info('patient [%s] protected against recycling, disabling protection now', orthanc_id)
560 self.__run_PUT(url = url, data = '0')
561 if self.__run_GET(url) == 0:
562 return True
563 _log.error('cannot unprotect patient [%s] against recycling', orthanc_id)
564 return False
565
566
568 url = '%s/patients/%s/protected' % (self.__server_url, str(orthanc_id))
569 return (self.__run_GET(url) == 1)
570
571
573 _log.info('verifying DICOM data of patient [%s]', orthanc_id)
574 bad_data = []
575 instances_url = '%s/patients/%s/instances' % (self.__server_url, orthanc_id)
576 instances = self.__run_GET(instances_url)
577 for instance in instances:
578 instance_id = instance['ID']
579 attachments_url = '%s/instances/%s/attachments' % (self.__server_url, instance_id)
580 attachments = self.__run_GET(attachments_url, allow_cached = True)
581 for attachment in attachments:
582 verify_url = '%s/%s/verify-md5' % (attachments_url, attachment)
583
584
585
586 if self.__run_POST(verify_url) is not False:
587 continue
588 _log.error('bad MD5 of DICOM file at url [%s]: patient=%s, attachment_type=%s', verify_url, orthanc_id, attachment)
589 bad_data.append({'patient': orthanc_id, 'instance': instance_id, 'type': attachment, 'orthanc': '%s [%s]' % (self.server_identification, self.__server_url)})
590
591 return bad_data
592
593
595
596 if old_patient_id == new_patient_id:
597 return True
598
599 modify_data = {
600 'Replace': {
601 'PatientID': new_patient_id
602
603
604 }
605 , 'Force': True
606
607
608 }
609 o_pats = self.get_patients_by_external_id(external_id = old_patient_id)
610 all_modified = True
611 for o_pat in o_pats:
612 _log.info('modifying Orthanc patient [%s]: DICOM ID [%s] -> [%s]', o_pat['ID'], old_patient_id, new_patient_id)
613 if self.patient_is_protected(o_pat['ID']):
614 _log.debug('patient protected: %s, unprotecting for modification', o_pat['ID'])
615 if not self.unprotect_patient(o_pat['ID']):
616 _log.error('cannot unlock patient [%s], skipping', o_pat['ID'])
617 all_modified = False
618 continue
619 was_protected = True
620 else:
621 was_protected = False
622 pat_url = '%s/patients/%s' % (self.__server_url, o_pat['ID'])
623 modify_url = '%s/modify' % pat_url
624 result = self.__run_POST(modify_url, data = modify_data)
625 _log.debug('modified: %s', result)
626 if result is False:
627 _log.error('cannot modify patient [%s]', o_pat['ID'])
628 all_modified = False
629 continue
630 newly_created_patient_id = result['ID']
631 _log.debug('newly created Orthanc patient ID: %s', newly_created_patient_id)
632 _log.debug('deleting archived patient: %s', self.__run_DELETE(pat_url))
633 if was_protected:
634 if not self.protect_patient(newly_created_patient_id):
635 _log.error('cannot re-lock (new) patient [%s]', newly_created_patient_id)
636
637 return all_modified
638
639
640
641
643 if gmTools.fname_stem(filename) == 'DICOMDIR':
644 _log.debug('ignoring [%s], no use uploading DICOMDIR files to Orthanc', filename)
645 return True
646
647 if check_mime_type:
648 if gmMimeLib.guess_mimetype(filename) != 'application/dicom':
649 _log.error('not considered a DICOM file: %s', filename)
650 return False
651 try:
652 f = io.open(filename, 'rb')
653 except Exception:
654 _log.exception('cannot open [%s]', filename)
655 return False
656 dcm_data = f.read()
657 f.close()
658 _log.debug('uploading [%s]', filename)
659 upload_url = '%s/instances' % self.__server_url
660 uploaded = self.__run_POST(upload_url, data = dcm_data, content_type = 'application/dicom')
661 if uploaded is False:
662 _log.error('cannot upload [%s]', filename)
663 return False
664 _log.debug(uploaded)
665 if uploaded['Status'] == 'AlreadyStored':
666
667 available_fields_url = '%s%s/attachments/dicom' % (self.__server_url, uploaded['Path'])
668 available_fields = self.__run_GET(available_fields_url, allow_cached = True)
669 if 'md5' not in available_fields:
670 _log.debug('md5 of instance not available in Orthanc, cannot compare against file md5, trusting Orthanc')
671 return True
672 md5_url = '%s/md5' % available_fields_url
673 md5_db = self.__run_GET(md5_url)
674 md5_file = gmTools.file2md5(filename)
675 if md5_file != md5_db:
676 _log.error('local md5: %s', md5_file)
677 _log.error('in-db md5: %s', md5_db)
678 _log.error('MD5 mismatch !')
679 return False
680 _log.error('MD5 match between file and database')
681 return True
682
683
685 uploaded = []
686 not_uploaded = []
687 for filename in files:
688 success = self.upload_dicom_file(filename, check_mime_type = check_mime_type)
689 if success:
690 uploaded.append(filename)
691 continue
692 not_uploaded.append(filename)
693
694 if len(not_uploaded) > 0:
695 _log.error('not all files uploaded')
696 return (uploaded, not_uploaded)
697
698
699 - def upload_from_directory(self, directory=None, recursive=False, check_mime_type=False, ignore_other_files=True):
700
701
702 def _on_error(exc):
703 _log.error('DICOM (?) file not accessible: %s', exc.filename)
704 _log.error(exc)
705
706
707 _log.debug('uploading DICOM files from [%s]', directory)
708 if not recursive:
709 files2try = os.listdir(directory)
710 _log.debug('found %s files', len(files2try))
711 if ignore_other_files:
712 files2try = [ f for f in files2try if gmMimeLib.guess_mimetype(f) == 'application/dicom' ]
713 _log.debug('DICOM files therein: %s', len(files2try))
714 return self.upload_dicom_files(files = files2try, check_mime_type = check_mime_type)
715
716 _log.debug('recursing for DICOM files')
717 uploaded = []
718 not_uploaded = []
719 for curr_root, curr_root_subdirs, curr_root_files in os.walk(directory, onerror = _on_error):
720 _log.debug('recursing into [%s]', curr_root)
721 files2try = [ os.path.join(curr_root, f) for f in curr_root_files ]
722 _log.debug('found %s files', len(files2try))
723 if ignore_other_files:
724 files2try = [ f for f in files2try if gmMimeLib.guess_mimetype(f) == 'application/dicom' ]
725 _log.debug('DICOM files therein: %s', len(files2try))
726 up, not_up = self.upload_dicom_files (
727 files = files2try,
728 check_mime_type = check_mime_type
729 )
730 uploaded.extend(up)
731 not_uploaded.extend(not_up)
732
733 return (uploaded, not_uploaded)
734
735
738
739
740
741
743
744 study_keys2hide = ['ModifiedFrom', 'Type', 'ID', 'ParentPatient', 'Series']
745 series_keys2hide = ['ModifiedFrom', 'Type', 'ID', 'ParentStudy', 'Instances']
746
747 studies_by_patient = []
748 series_keys = {}
749 series_keys_m = {}
750
751
752 for pat in orthanc_patients:
753 pat_dict = {
754 'orthanc_id': pat['ID'],
755 'name': None,
756 'external_id': None,
757 'date_of_birth': None,
758 'gender': None,
759 'studies': []
760 }
761 try:
762 pat_dict['name'] = pat['MainDicomTags']['PatientName'].strip()
763 except KeyError:
764 pass
765 try:
766 pat_dict['external_id'] = pat['MainDicomTags']['PatientID'].strip()
767 except KeyError:
768 pass
769 try:
770 pat_dict['date_of_birth'] = pat['MainDicomTags']['PatientBirthDate'].strip()
771 except KeyError:
772 pass
773 try:
774 pat_dict['gender'] = pat['MainDicomTags']['PatientSex'].strip()
775 except KeyError:
776 pass
777 for key in pat_dict:
778 if pat_dict[key] in ['unknown', '(null)', '']:
779 pat_dict[key] = None
780 pat_dict[key] = cleanup_dicom_string(pat_dict[key])
781 studies_by_patient.append(pat_dict)
782
783
784 orth_studies = self.__run_GET(url = '%s/patients/%s/studies' % (self.__server_url, pat['ID']))
785 if orth_studies is False:
786 _log.error('cannot retrieve studies')
787 return []
788 for orth_study in orth_studies:
789 study_dict = {
790 'orthanc_id': orth_study['ID'],
791 'date': None,
792 'time': None,
793 'description': None,
794 'referring_doc': None,
795 'requesting_doc': None,
796 'radiology_org': None,
797 'operator_name': None,
798 'series': []
799 }
800 try:
801 study_dict['date'] = orth_study['MainDicomTags']['StudyDate'].strip()
802 except KeyError:
803 pass
804 try:
805 study_dict['time'] = orth_study['MainDicomTags']['StudyTime'].strip()
806 except KeyError:
807 pass
808 try:
809 study_dict['description'] = orth_study['MainDicomTags']['StudyDescription'].strip()
810 except KeyError:
811 pass
812 try:
813 study_dict['referring_doc'] = orth_study['MainDicomTags']['ReferringPhysicianName'].strip()
814 except KeyError:
815 pass
816 try:
817 study_dict['requesting_doc'] = orth_study['MainDicomTags']['RequestingPhysician'].strip()
818 except KeyError:
819 pass
820 try:
821 study_dict['radiology_org'] = orth_study['MainDicomTags']['InstitutionName'].strip()
822 except KeyError:
823 pass
824 for key in study_dict:
825 if study_dict[key] in ['unknown', '(null)', '']:
826 study_dict[key] = None
827 study_dict[key] = cleanup_dicom_string(study_dict[key])
828 study_dict['all_tags'] = {}
829 try:
830 orth_study['PatientMainDicomTags']
831 except KeyError:
832 orth_study['PatientMainDicomTags'] = pat['MainDicomTags']
833 for key in orth_study.keys():
834 if key == 'MainDicomTags':
835 for mkey in orth_study['MainDicomTags'].keys():
836 study_dict['all_tags'][mkey] = orth_study['MainDicomTags'][mkey].strip()
837 continue
838 if key == 'PatientMainDicomTags':
839 for pkey in orth_study['PatientMainDicomTags'].keys():
840 study_dict['all_tags'][pkey] = orth_study['PatientMainDicomTags'][pkey].strip()
841 continue
842 study_dict['all_tags'][key] = orth_study[key]
843 _log.debug('study: %s', study_dict['all_tags'].keys())
844 for key in study_keys2hide:
845 try: del study_dict['all_tags'][key]
846 except KeyError: pass
847 pat_dict['studies'].append(study_dict)
848
849
850 for orth_series_id in orth_study['Series']:
851 orth_series = self.__run_GET(url = '%s/series/%s' % (self.__server_url, orth_series_id))
852
853 ordered_slices = self.__run_GET(url = '%s/series/%s/ordered-slices' % (self.__server_url, orth_series_id))
854 slices = [ s[0] for s in ordered_slices['SlicesShort'] ]
855 if orth_series is False:
856 _log.error('cannot retrieve series')
857 return []
858 series_dict = {
859 'orthanc_id': orth_series['ID'],
860 'instances': slices,
861 'modality': None,
862 'date': None,
863 'time': None,
864 'description': None,
865 'body_part': None,
866 'protocol': None,
867 'performed_procedure_step_description': None,
868 'acquisition_device_processing_description': None,
869 'operator_name': None
870 }
871 try:
872 series_dict['modality'] = orth_series['MainDicomTags']['Modality'].strip()
873 except KeyError:
874 pass
875 try:
876 series_dict['date'] = orth_series['MainDicomTags']['SeriesDate'].strip()
877 except KeyError:
878 pass
879 try:
880 series_dict['description'] = orth_series['MainDicomTags']['SeriesDescription'].strip()
881 except KeyError:
882 pass
883 try:
884 series_dict['time'] = orth_series['MainDicomTags']['SeriesTime'].strip()
885 except KeyError:
886 pass
887 try:
888 series_dict['body_part'] = orth_series['MainDicomTags']['BodyPartExamined'].strip()
889 except KeyError:
890 pass
891 try:
892 series_dict['protocol'] = orth_series['MainDicomTags']['ProtocolName'].strip()
893 except KeyError:
894 pass
895 try:
896 series_dict['performed_procedure_step_description'] = orth_series['MainDicomTags']['PerformedProcedureStepDescription'].strip()
897 except KeyError:
898 pass
899 try:
900 series_dict['acquisition_device_processing_description'] = orth_series['MainDicomTags']['AcquisitionDeviceProcessingDescription'].strip()
901 except KeyError:
902 pass
903 try:
904 series_dict['operator_name'] = orth_series['MainDicomTags']['OperatorsName'].strip()
905 except KeyError:
906 pass
907 for key in series_dict:
908 if series_dict[key] in ['unknown', '(null)', '']:
909 series_dict[key] = None
910 if series_dict['description'] == series_dict['protocol']:
911 _log.debug('<series description> matches <series protocol>, ignoring protocol')
912 series_dict['protocol'] = None
913 if series_dict['performed_procedure_step_description'] in [series_dict['description'], series_dict['protocol']]:
914 series_dict['performed_procedure_step_description'] = None
915 if series_dict['performed_procedure_step_description'] is not None:
916 if regex.match ('[.,/\|\-\s\d]+', series_dict['performed_procedure_step_description'], flags = regex.UNICODE):
917 series_dict['performed_procedure_step_description'] = None
918 if series_dict['acquisition_device_processing_description'] in [series_dict['description'], series_dict['protocol']]:
919 series_dict['acquisition_device_processing_description'] = None
920 if series_dict['acquisition_device_processing_description'] is not None:
921 if regex.match ('[.,/\|\-\s\d]+', series_dict['acquisition_device_processing_description'], flags = regex.UNICODE):
922 series_dict['acquisition_device_processing_description'] = None
923 if series_dict['date'] == study_dict['date']:
924 _log.debug('<series date> matches <study date>, ignoring date')
925 series_dict['date'] = None
926 if series_dict['time'] == study_dict['time']:
927 _log.debug('<series time> matches <study time>, ignoring time')
928 series_dict['time'] = None
929 for key in series_dict:
930 series_dict[key] = cleanup_dicom_string(series_dict[key])
931 series_dict['all_tags'] = {}
932 for key in orth_series.keys():
933 if key == 'MainDicomTags':
934 for mkey in orth_series['MainDicomTags'].keys():
935 series_dict['all_tags'][mkey] = orth_series['MainDicomTags'][mkey].strip()
936 continue
937 series_dict['all_tags'][key] = orth_series[key]
938 _log.debug('series: %s', series_dict['all_tags'].keys())
939 for key in series_keys2hide:
940 try: del series_dict['all_tags'][key]
941 except KeyError: pass
942 study_dict['operator_name'] = series_dict['operator_name']
943 study_dict['series'].append(series_dict)
944
945 return studies_by_patient
946
947
948
949
950 - def __run_GET(self, url=None, data=None, allow_cached=False):
951 if data is None:
952 data = {}
953 headers = {}
954 if not allow_cached:
955 headers['cache-control'] = 'no-cache'
956 params = ''
957 if len(data.keys()) > 0:
958 params = '?' + urlencode(data)
959 url_with_params = url + params
960
961 try:
962 response, content = self.__conn.request(url_with_params, 'GET', headers = headers)
963 except (socket.error, http.client.ResponseNotReady, http.client.InvalidURL, OverflowError, httplib2.ServerNotFoundError):
964 _log.exception('exception in GET')
965 _log.debug(' url: %s', url_with_params)
966 _log.debug(' headers: %s', headers)
967 return False
968
969 if response.status not in [ 200 ]:
970 _log.error('GET returned non-OK status: %s', response.status)
971 _log.debug(' url: %s', url_with_params)
972 _log.debug(' headers: %s', headers)
973 _log.error(' response: %s', response)
974 _log.debug(' content: %s', content)
975 return False
976
977
978
979
980 if response['content-type'].startswith('text/plain'):
981
982
983
984
985 return content.decode('utf8')
986
987 if response['content-type'].startswith('application/json'):
988 try:
989 return json.loads(content)
990 except Exception:
991 return content
992
993 return content
994
995
996 - def __run_POST(self, url=None, data=None, content_type=None, output_file=None):
997
998 body = data
999 headers = {'content-type' : content_type}
1000 if isinstance(data, str):
1001 if content_type is None:
1002 headers['content-type'] = 'text/plain'
1003 elif isinstance(data, bytes):
1004 if content_type is None:
1005 headers['content-type'] = 'application/octet-stream'
1006 else:
1007 body = json.dumps(data)
1008 headers['content-type'] = 'application/json'
1009
1010 _log.info(body)
1011 _log.info(headers)
1012
1013 try:
1014 try:
1015 response, content = self.__conn.request(url, 'POST', body = body, headers = headers)
1016 except BrokenPipeError:
1017 response, content = self.__conn.request(url, 'POST', body = body, headers = headers)
1018 except (socket.error, http.client.ResponseNotReady, OverflowError):
1019 _log.exception('exception in POST')
1020 _log.debug(' url: %s', url)
1021 _log.debug(' headers: %s', headers)
1022 _log.debug(' body: %s', body[:256])
1023 return False
1024
1025 if response.status == 404:
1026 _log.debug('no data, response: %s', response)
1027 if output_file is None:
1028 return []
1029 return False
1030 if response.status not in [ 200, 302 ]:
1031 _log.error('POST returned non-OK status: %s', response.status)
1032 _log.debug(' url: %s', url)
1033 _log.debug(' headers: %s', headers)
1034 _log.debug(' body: %s', body[:256])
1035 _log.error(' response: %s', response)
1036 _log.debug(' content: %s', content)
1037 return False
1038
1039 try:
1040 content = json.loads(content)
1041 except Exception:
1042 pass
1043 if output_file is None:
1044 return content
1045 output_file.write(content)
1046 return True
1047
1048
1049 - def __run_PUT(self, url=None, data=None, content_type=None):
1050
1051 body = data
1052 headers = {'content-type' : content_type}
1053 if isinstance(data, str):
1054 if content_type is None:
1055 headers['content-type'] = 'text/plain'
1056 elif isinstance(data, bytes):
1057 if content_type is None:
1058 headers['content-type'] = 'application/octet-stream'
1059 else:
1060 body = json.dumps(data)
1061 headers['content-type'] = 'application/json'
1062
1063 try:
1064 try:
1065 response, content = self.__conn.request(url, 'PUT', body = body, headers = headers)
1066 except BrokenPipeError:
1067 response, content = self.__conn.request(url, 'PUT', body = body, headers = headers)
1068 except (socket.error, http.client.ResponseNotReady, OverflowError):
1069 _log.exception('exception in PUT')
1070 _log.debug(' url: %s', url)
1071 _log.debug(' headers: %s', headers)
1072 _log.debug(' body: %s', body[:256])
1073 return False
1074
1075 if response.status == 404:
1076 _log.debug('no data, response: %s', response)
1077 return []
1078 if response.status not in [ 200, 302 ]:
1079 _log.error('PUT returned non-OK status: %s', response.status)
1080 _log.debug(' url: %s', url)
1081 _log.debug(' headers: %s', headers)
1082 _log.debug(' body: %s', body[:256])
1083 _log.error(' response: %s', response)
1084 _log.debug(' content: %s', content)
1085 return False
1086
1087 if response['content-type'].startswith('text/plain'):
1088
1089
1090
1091
1092 return content.decode('utf8')
1093
1094 if response['content-type'].startswith('application/json'):
1095 try:
1096 return json.loads(content)
1097 except Exception:
1098 return content
1099
1100 return content
1101
1102
1104 try:
1105 response, content = self.__conn.request(url, 'DELETE')
1106 except (http.client.ResponseNotReady, socket.error, OverflowError):
1107 _log.exception('exception in DELETE')
1108 _log.debug(' url: %s', url)
1109 return False
1110
1111 if response.status not in [ 200 ]:
1112 _log.error('DELETE returned non-OK status: %s', response.status)
1113 _log.debug(' url: %s', url)
1114 _log.error(' response: %s', response)
1115 _log.debug(' content: %s', content)
1116 return False
1117
1118 if response['content-type'].startswith('text/plain'):
1119
1120
1121
1122
1123 return content.decode('utf8')
1124
1125 if response['content-type'].startswith('application/json'):
1126 try:
1127 return json.loads(content)
1128 except Exception:
1129 return content
1130
1131 return content
1132
1133
1135 if not isinstance(dicom_str, str):
1136 return dicom_str
1137 return regex.sub('\^+', ' ', dicom_str.strip('^'))
1138
1139
1140
1141
1142 if __name__ == "__main__":
1143
1144 if len(sys.argv) == 1:
1145 sys.exit()
1146
1147 if sys.argv[1] != 'test':
1148 sys.exit()
1149
1150
1151
1152 from Gnumed.pycommon import gmLog2
1153
1154
1156 orthanc = cOrthancServer()
1157 if not orthanc.connect(host, port, user = None, password = None):
1158 print('error connecting to server:', orthanc.connect_error)
1159 return False
1160 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s] - API [%s])' % (
1161 orthanc.server_identification['Name'],
1162 orthanc.server_identification['DicomAet'],
1163 orthanc.server_identification['Version'],
1164 orthanc.server_identification['DatabaseVersion'],
1165 orthanc.server_identification['ApiVersion']
1166 ))
1167 print('')
1168 print('Please enter patient name parts, separated by SPACE.')
1169
1170 while True:
1171 entered_name = gmTools.prompted_input(prompt = "\nEnter person search term or leave blank to exit")
1172 if entered_name in ['exit', 'quit', 'bye', None]:
1173 print("user cancelled patient search")
1174 break
1175
1176 pats = orthanc.get_patients_by_external_id(external_id = entered_name)
1177 if len(pats) > 0:
1178 print('Patients found:')
1179 for pat in pats:
1180 print(' -> ', pat)
1181 continue
1182
1183 pats = orthanc.get_patients_by_name(name_parts = entered_name.split(), fuzzy = True)
1184 print('Patients found:')
1185 for pat in pats:
1186 print(' -> ', pat)
1187 print(' verifying ...')
1188 bad_data = orthanc.verify_patient_data(pat['ID'])
1189 print(' bad data:')
1190 for bad in bad_data:
1191 print(' -> ', bad)
1192 continue
1193
1194 continue
1195
1196 pats = orthanc.get_studies_list_by_patient_name(name_parts = entered_name.split(), fuzzy = True)
1197 print('Patients found from studies list:')
1198 for pat in pats:
1199 print(' -> ', pat['name'])
1200 for study in pat['studies']:
1201 print(' ', gmTools.format_dict_like(study, relevant_keys = ['orthanc_id', 'date', 'time'], template = 'study [%%(orthanc_id)s] at %%(date)s %%(time)s contains %s series' % len(study['series'])))
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214 print('--------')
1215
1216
1218 try:
1219 host = sys.argv[2]
1220 except IndexError:
1221 host = None
1222 try:
1223 port = sys.argv[3]
1224 except IndexError:
1225 port = '8042'
1226
1227 orthanc_console(host, port)
1228
1229
1231 try:
1232 host = sys.argv[2]
1233 port = sys.argv[3]
1234 except IndexError:
1235 host = None
1236 port = '8042'
1237 orthanc = cOrthancServer()
1238 if not orthanc.connect(host, port, user = None, password = None):
1239 print('error connecting to server:', orthanc.connect_error)
1240 return False
1241 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s])' % (
1242 orthanc.server_identification['Name'],
1243 orthanc.server_identification['DicomAet'],
1244 orthanc.server_identification['Version'],
1245 orthanc.server_identification['DatabaseVersion']
1246 ))
1247 print('')
1248 print('Please enter patient name parts, separated by SPACE.')
1249
1250 entered_name = gmTools.prompted_input(prompt = "\nEnter person search term or leave blank to exit")
1251 if entered_name in ['exit', 'quit', 'bye', None]:
1252 print("user cancelled patient search")
1253 return
1254
1255 pats = orthanc.get_patients_by_name(name_parts = entered_name.split(), fuzzy = True)
1256 if len(pats) == 0:
1257 print('no patient found')
1258 return
1259
1260 pat = pats[0]
1261 print('test patient:')
1262 print(pat)
1263 old_id = pat['MainDicomTags']['PatientID']
1264 new_id = old_id + '-1'
1265 print('setting [%s] to [%s]:' % (old_id, new_id), orthanc.modify_patient_id(old_id, new_id))
1266
1267
1269
1270
1271
1272
1273 host = None
1274 port = '8042'
1275
1276 orthanc = cOrthancServer()
1277 if not orthanc.connect(host, port, user = None, password = None):
1278 print('error connecting to server:', orthanc.connect_error)
1279 return False
1280 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s] - REST API [%s])' % (
1281 orthanc.server_identification['Name'],
1282 orthanc.server_identification['DicomAet'],
1283 orthanc.server_identification['Version'],
1284 orthanc.server_identification['DatabaseVersion'],
1285 orthanc.server_identification['ApiVersion']
1286 ))
1287 print('')
1288
1289
1290 orthanc.upload_from_directory(directory = sys.argv[2], recursive = True, check_mime_type = False, ignore_other_files = True)
1291
1292
1311
1312
1333
1334
1335
1336 run_console()
1337
1338
1339
1340
1341