Package Gnumed :: Package business :: Module gmDICOM
[frames] | no frames]

Source Code for Module Gnumed.business.gmDICOM

   1  # -*- coding: utf-8 -*- 
   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  # stdlib 
  10  import io 
  11  import os 
  12  import sys 
  13  import re as regex 
  14  import logging 
  15  import http.client              # exception names used by httplib2 
  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  # GNUmed modules 
  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  #from Gnumed.pycommon import gmHooks 
  34  #from Gnumed.pycommon import gmDispatcher 
  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  #============================================================ 
47 -class cOrthancServer:
48 # REST API access to Orthanc DICOM servers 49 50 # def __init__(self): 51 # self.__server_identification = None 52 # self.__user = None 53 # self.__password = None 54 # self.__conn = None 55 # self.__server_url = None 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 #--------------------------------------------------------
102 - def _get_server_identification(self):
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 # check time skew 118 tolerance = 60 # seconds 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) # 20180208T165832 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 #--------------------------------------------------------
145 - def _get_as_external_id_issuer(self):
146 # fixed type :: user level instance name :: DICOM AET 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 #--------------------------------------------------------
152 - def _get_url_browse_patients(self):
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 #--------------------------------------------------------
160 - def get_url_browse_patient(self, patient_id):
161 # http://localhost:8042/#patient?uuid=0da01e38-cf792452-65c1e6af-b77faf5a-b637a05b 162 return '%s/#patient?uuid=%s' % (self.url_browse_patients, patient_id)
163 164 #--------------------------------------------------------
165 - def get_url_browse_study(self, study_id):
166 # http://localhost:8042/#study?uuid=0da01e38-cf792452-65c1e6af-b77faf5a-b637a05b 167 return '%s/#study?uuid=%s' % (self.url_browse_patients, study_id)
168 169 #-------------------------------------------------------- 170 # download API 171 #--------------------------------------------------------
172 - def get_matching_patients(self, person):
173 _log.info('searching for Orthanc patients matching %s', person) 174 175 # look for patient by external ID first 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 # return find type ? especially useful for non-matches on ID 198 199 # search by name 200 201 # # then look for name parts 202 # name = person.get_active_name() 203 return []
204 205 #--------------------------------------------------------
206 - def get_patients_by_external_id(self, external_id=None):
207 matching_patients = [] 208 _log.info('searching for patients with external ID >>>%s<<<', external_id) 209 210 # elegant server-side approach: 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 # paranoia 221 for match in matches: 222 self.protect_patient(orthanc_id = match['ID']) 223 return matches
224 225 # # recursive brute force approach: 226 # for pat_id in self.__run_GET(url = '%s/patients' % self.__server_url): 227 # orthanc_pat = self.__run_GET(url = '%s/patients/%s' % (self.__server_url, pat_id)) 228 # orthanc_external_id = orthanc_pat['MainDicomTags']['PatientID'] 229 # if orthanc_external_id != external_id: 230 # continue 231 # _log.debug(u'match: %s (name=[%s], orthanc_id=[%s])', orthanc_external_id, orthanc_pat['MainDicomTags']['PatientName'], orthanc_pat['ID']) 232 # matching_patients.append(orthanc_pat) 233 # if len(matching_patients) == 0: 234 # _log.debug(u'no matches') 235 # return matching_patients 236 237 #--------------------------------------------------------
238 - def get_patients_by_name(self, name_parts=None, gender=None, dob=None, fuzzy=False):
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 #--------------------------------------------------------
265 - def get_patients_by_name_parts(self, name_parts=None, gender=None, dob=None, fuzzy=False):
266 # fuzzy: allow partial/substring matches (but not across name part boundaries ',' or '^') 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 #--------------------------------------------------------
308 - def get_studies_list_by_patient_name(self, name_parts=None, gender=None, dob=None, fuzzy=False):
309 return self.get_studies_list_by_orthanc_patient_list ( 310 orthanc_patients = self.get_patients_by_name(name_parts = name_parts, gender = gender, dob = dob, fuzzy = fuzzy) 311 )
312 313 #--------------------------------------------------------
314 - def get_studies_list_by_external_id(self, external_id=None):
315 return self.get_studies_list_by_orthanc_patient_list ( 316 orthanc_patients = self.get_patients_by_external_id(external_id = external_id) 317 )
318 319 #--------------------------------------------------------
320 - def get_study_as_zip(self, study_id=None, filename=None):
321 if filename is None: 322 filename = gmTools.get_unique_filename(prefix = r'DCM-', suffix = r'.zip') 323 _log.info('exporting study [%s] into [%s]', study_id, filename) 324 f = io.open(filename, 'wb') 325 f.write(self.__run_GET(url = '%s/studies/%s/archive' % (self.__server_url, str(study_id)), allow_cached = True)) 326 f.close() 327 return filename
328 329 #--------------------------------------------------------
330 - def get_study_as_zip_with_dicomdir(self, study_id=None, filename=None):
331 if filename is None: 332 filename = gmTools.get_unique_filename(prefix = r'DCM-', suffix = r'.zip') 333 _log.info('exporting study [%s] into [%s]', study_id, filename) 334 f = io.open(filename, 'wb') 335 f.write(self.__run_GET(url = '%s/studies/%s/media' % (self.__server_url, str(study_id)), allow_cached = True)) 336 f.close() 337 return filename
338 339 #--------------------------------------------------------
340 - def get_studies_as_zip(self, study_ids=None, patient_id=None, filename=None):
341 if filename is None: 342 filename = gmTools.get_unique_filename(prefix = r'DCM-', suffix = r'.zip') 343 if study_ids is None: 344 _log.info('exporting all studies of patient [%s] into [%s]', patient_id, filename) 345 f = io.open(filename, 'wb') 346 f.write(self.__run_GET(url = '%s/patients/%s/archive' % (self.__server_url, str(patient_id)), allow_cached = True)) 347 f.close() 348 return filename
349 350 #--------------------------------------------------------
351 - def _manual_get_studies_with_dicomdir(self, study_ids=None, patient_id=None, target_dir=None, filename=None, create_zip=False):
352 353 if filename is None: 354 filename = gmTools.get_unique_filename(prefix = r'DCM-', suffix = r'.zip', tmp_dir = target_dir) 355 356 # all studies 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 # a selection of studies 373 dicomdir_cmd = 'gm-create_dicomdir' # args: 1) name of DICOMDIR to create 2) base directory where to start recursing for DICOM files 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 # getting with DICOMDIR returns DICOMDIR compatible subdirs and filenames 390 study_zip_name = self.get_study_as_zip_with_dicomdir(study_id = study_id, filename = study_zip_name) 391 # non-beautiful per-study dir name required by subsequent DICOMDIR generation 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 # need to extract into per-study subdir because get-with-dicomdir 396 # returns identical-across-studies subdirs / filenames 397 if not gmTools.unzip_archive(study_zip_name, target_dir = study_unzip_dir, remove_archive = True): 398 return False 399 400 # create DICOMDIR across all studies, 401 # we simply ignore the already existing per-study DICOMDIR files 402 target_dicomdir_name = os.path.join(sandbox_dir, 'DICOMDIR') 403 gmTools.remove_file(target_dicomdir_name, log_error = False) # better safe than sorry 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 # paranoia 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 # return path to extracted studies 425 if not create_zip: 426 return sandbox_dir 427 428 # else return ZIP of all studies 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 # studies can be _large_ so attempt to get rid of intermediate files 438 gmTools.rmdir(sandbox_dir) 439 return studies_zip
440 441 #--------------------------------------------------------
442 - def get_studies_with_dicomdir(self, study_ids=None, patient_id=None, target_dir=None, filename=None, create_zip=False):
443 444 if filename is None: 445 filename = gmTools.get_unique_filename(prefix = r'DCM-', suffix = r'.zip', tmp_dir = target_dir) 446 447 # all studies 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 # selection of studies 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 # You have to make a POST request against URI "/tools/create-media", with a 470 # JSON body that contains the array of the resources of interest (as Orthanc 471 # identifiers). Here is a sample command-line: 472 # curl -X POST http://localhost:8042/tools/create-media -d '["8c4663df-c3e66066-9e20a8fc-dd14d1e5-251d3d84","2cd4848d-02f0005f-812ffef6-a210bbcf-3f01a00a","6eeded74-75005003-c3ae9738-d4a06a4f-6beedeb8","8a622020-c058291c-7693b63f-bc67aa2e-0a02e69c"]' -v > /tmp/a.zip 473 # (this will not create duplicates but will also not check for single-patient-ness) 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 # retry with old URL 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 #--------------------------------------------------------
507 - def get_instance_dicom_tags(self, instance_id, simplified=True):
508 _log.debug('retrieving DICOM tags for instance [%s]', instance_id) 509 if simplified: 510 download_url = '%s/instances/%s/simplified-tags' % (self.__server_url, instance_id) 511 else: 512 download_url = '%s/instances/%s/tags' % (self.__server_url, instance_id) 513 return self.__run_GET(url = download_url, allow_cached = True)
514 515 #--------------------------------------------------------
516 - def get_instance_preview(self, instance_id, filename=None):
517 if filename is None: 518 filename = gmTools.get_unique_filename(suffix = '.png') 519 520 _log.debug('exporting preview for instance [%s] into [%s]', instance_id, filename) 521 download_url = '%s/instances/%s/preview' % (self.__server_url, instance_id) 522 f = io.open(filename, 'wb') 523 f.write(self.__run_GET(url = download_url, allow_cached = True)) 524 f.close() 525 return filename
526 527 #--------------------------------------------------------
528 - def get_instance(self, instance_id, filename=None):
529 if filename is None: 530 filename = gmTools.get_unique_filename(suffix = '.dcm') 531 532 _log.debug('exporting instance [%s] into [%s]', instance_id, filename) 533 download_url = '%s/instances/%s/attachments/dicom/data' % (self.__server_url, instance_id) 534 f = io.open(filename, 'wb') 535 f.write(self.__run_GET(url = download_url, allow_cached = True)) 536 f.close() 537 return filename
538 539 #-------------------------------------------------------- 540 # server-side API 541 #--------------------------------------------------------
542 - def protect_patient(self, orthanc_id):
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 #--------------------------------------------------------
555 - def unprotect_patient(self, orthanc_id):
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 #--------------------------------------------------------
567 - def patient_is_protected(self, orthanc_id):
568 url = '%s/patients/%s/protected' % (self.__server_url, str(orthanc_id)) 569 return (self.__run_GET(url) == 1)
570 571 #--------------------------------------------------------
572 - def verify_patient_data(self, orthanc_id):
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 # False, success = "{}" 584 #2018-02-08 19:11:27 ERROR gm.dicom [-1211701504 MainThread] (gmDICOM.py::__run_POST() #986): cannot POST: http://localhost:8042/instances/5a8206f4-24619e76-6650d9cd-792cdf25-039e96e6/attachments/dicom-as-json/verify-md5 585 #2018-02-08 19:11:27 ERROR gm.dicom [-1211701504 MainThread] (gmDICOM.py::__run_POST() #987): response: {'status': '400', 'content-length': '0'} 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 #--------------------------------------------------------
594 - def modify_patient_id(self, old_patient_id, new_patient_id):
595 596 if old_patient_id == new_patient_id: 597 return True 598 599 modify_data = { 600 'Replace': { 601 'PatientID': new_patient_id 602 #,u'0010,0021': praxis.name / "GNUmed vX.X.X" 603 #,u'0010,1002': series of (old) patient IDs 604 } 605 , 'Force': True 606 # "Keep" doesn't seem to do what it suggests ATM 607 #, u'Keep': True 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 # upload API 641 #--------------------------------------------------------
642 - def upload_dicom_file(self, filename, check_mime_type=False):
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 # paranoia, as is our custom 667 available_fields_url = '%s%s/attachments/dicom' % (self.__server_url, uploaded['Path']) # u'Path': u'/instances/1440110e-9cd02a98-0b1c0452-087d35db-3fd5eb05' 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 #--------------------------------------------------------
684 - def upload_dicom_files(self, files=None, check_mime_type=False):
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 #--------------------------------------------------------
736 - def upload_by_DICOMDIR(self, DICOMDIR=None):
737 pass
738 739 #-------------------------------------------------------- 740 # helper functions 741 #--------------------------------------------------------
742 - def get_studies_list_by_orthanc_patient_list(self, orthanc_patients=None):
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 # loop over patients 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 # loop over studies of patient 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 # loop over series in study 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 #slices = orth_series['Instances'] 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'] # will collapse all operators into that of the last series 943 study_dict['series'].append(series_dict) 944 945 return studies_by_patient
946 947 #-------------------------------------------------------- 948 # generic REST helpers 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 # _log.error(' response: %s', response) 978 # _log.error(' content type: %s', type(content)) 979 980 if response['content-type'].startswith('text/plain'): 981 # utf8 ? 982 # urldecode ? 983 # latin1 = Orthanc default = tools/default-encoding ? 984 # ascii ? 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 # utf8 ? 1089 # urldecode ? 1090 # latin1 = Orthanc default = tools/default-encoding ? 1091 # ascii ? 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 #--------------------------------------------------------
1103 - def __run_DELETE(self, url=None):
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 # utf8 ? 1120 # urldecode ? 1121 # latin1 = Orthanc default = tools/default-encoding ? 1122 # ascii ? 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 #------------------------------------------------------------
1134 -def cleanup_dicom_string(dicom_str):
1135 if not isinstance(dicom_str, str): 1136 return dicom_str 1137 return regex.sub('\^+', ' ', dicom_str.strip('^'))
1138 1139 #============================================================ 1140 # main 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 # if __name__ == '__main__': 1151 # sys.path.insert(0, '../../') 1152 from Gnumed.pycommon import gmLog2 1153 1154 #--------------------------------------------------------
1155 - def orthanc_console(host, port):
1156 orthanc = cOrthancServer() 1157 if not orthanc.connect(host, port, user = None, password = None): #, expected_aet = 'another AET' 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 # for series in study['series']: 1203 # print ( 1204 # u' ', 1205 # gmTools.format_dict_like ( 1206 # series, 1207 # relevant_keys = ['orthanc_id', 'date', 'time', 'modality', 'instances', 'body_part', 'protocol', 'description', 'station'], 1208 # template = u'series [%(orthanc_id)s] at %(date)s %(time)s: "%(description)s" %(modality)s@%(station)s (%(protocol)s) of body part "%(body_part)s" holds images:\n%(instances)s' 1209 # ) 1210 # ) 1211 # print(orthanc.get_studies_with_dicomdir(study_ids = [study['orthanc_id']], filename = 'study_%s.zip' % study['orthanc_id'], create_zip = True)) 1212 #print(orthanc.get_study_as_zip(study_id = study['orthanc_id'], filename = 'study_%s.zip' % study['orthanc_id'])) 1213 #print(orthanc.get_studies_as_zip_with_dicomdir(study_ids = [ s['orthanc_id'] for s in pat['studies'] ], filename = 'studies_of_%s.zip' % pat['orthanc_id'])) 1214 print('--------')
1215 1216 #--------------------------------------------------------
1217 - def run_console():
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 #--------------------------------------------------------
1230 - def test_modify_patient_id():
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): #, expected_aet = 'another AET' 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 #--------------------------------------------------------
1268 - def test_upload_files():
1269 # try: 1270 # host = sys.argv[2] 1271 # port = sys.argv[3] 1272 # except IndexError: 1273 host = None 1274 port = '8042' 1275 1276 orthanc = cOrthancServer() 1277 if not orthanc.connect(host, port, user = None, password = None): #, expected_aet = 'another AET' 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 #orthanc.upload_dicom_file(sys.argv[2]) 1290 orthanc.upload_from_directory(directory = sys.argv[2], recursive = True, check_mime_type = False, ignore_other_files = True)
1291 1292 #--------------------------------------------------------
1293 - def test_get_instance_preview():
1294 host = None 1295 port = '8042' 1296 1297 orthanc = cOrthancServer() 1298 if not orthanc.connect(host, port, user = None, password = None): #, expected_aet = 'another AET' 1299 print('error connecting to server:', orthanc.connect_error) 1300 return False 1301 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s])' % ( 1302 orthanc.server_identification['Name'], 1303 orthanc.server_identification['DicomAet'], 1304 orthanc.server_identification['Version'], 1305 orthanc.server_identification['DatabaseVersion'] 1306 )) 1307 print('') 1308 1309 print(orthanc.get_instance_preview('f4f07d22-0d8265ef-112ea4e9-dc140e13-350c06d1')) 1310 print(orthanc.get_instance('f4f07d22-0d8265ef-112ea4e9-dc140e13-350c06d1'))
1311 1312 #--------------------------------------------------------
1313 - def test_get_instance_tags():
1314 host = None 1315 port = '8042' 1316 1317 orthanc = cOrthancServer() 1318 if not orthanc.connect(host, port, user = None, password = None): #, expected_aet = 'another AET' 1319 print('error connecting to server:', orthanc.connect_error) 1320 return False 1321 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s])' % ( 1322 orthanc.server_identification['Name'], 1323 orthanc.server_identification['DicomAet'], 1324 orthanc.server_identification['Version'], 1325 orthanc.server_identification['DatabaseVersion'] 1326 )) 1327 print('') 1328 1329 instance_id = 'f4f07d22-0d8265ef-112ea4e9-dc140e13-350c06d1' 1330 for key, value in orthanc.get_instance_dicom_tags(instance_id, simplified = False).items(): 1331 print(key, ':', value) 1332 print()
1333 #print(orthanc.get_instance_dicom_tags(instance_id, simplified = True)) 1334 1335 #-------------------------------------------------------- 1336 run_console() 1337 #test_modify_patient_id() 1338 #test_upload_files() 1339 #test_get_instance_preview() 1340 #test_get_instance_tags() 1341