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': 'O' 
  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, 0o700) 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 'performing_doc': None, 797 'operator_name': None, 798 'radiographer_code': None, 799 'radiology_org': None, 800 'radiology_dept': None, 801 'radiology_org_addr': None, 802 'station_name': None, 803 'series': [] 804 } 805 try: 806 study_dict['date'] = orth_study['MainDicomTags']['StudyDate'].strip() 807 except KeyError: 808 pass 809 try: 810 study_dict['time'] = orth_study['MainDicomTags']['StudyTime'].strip() 811 except KeyError: 812 pass 813 try: 814 study_dict['description'] = orth_study['MainDicomTags']['StudyDescription'].strip() 815 except KeyError: 816 pass 817 try: 818 study_dict['referring_doc'] = orth_study['MainDicomTags']['ReferringPhysicianName'].strip() 819 except KeyError: 820 pass 821 try: 822 study_dict['requesting_doc'] = orth_study['MainDicomTags']['RequestingPhysician'].strip() 823 except KeyError: 824 pass 825 try: 826 study_dict['radiology_org_addr'] = orth_study['MainDicomTags']['InstitutionAddress'].strip() 827 except KeyError: 828 pass 829 try: 830 study_dict['radiology_org'] = orth_study['MainDicomTags']['InstitutionName'].strip() 831 if study_dict['radiology_org_addr'] is not None: 832 if study_dict['radiology_org'] in study_dict['radiology_org_addr']: 833 study_dict['radiology_org'] = None 834 except KeyError: 835 pass 836 try: 837 study_dict['radiology_dept'] = orth_study['MainDicomTags']['InstitutionalDepartmentName'].strip() 838 if study_dict['radiology_org'] is not None: 839 if study_dict['radiology_dept'] in study_dict['radiology_org']: 840 study_dict['radiology_dept'] = None 841 if study_dict['radiology_org_addr'] is not None: 842 if study_dict['radiology_dept'] in study_dict['radiology_org_addr']: 843 study_dict['radiology_dept'] = None 844 except KeyError: 845 pass 846 try: 847 study_dict['station_name'] = orth_study['MainDicomTags']['StationName'].strip() 848 if study_dict['radiology_org'] is not None: 849 if study_dict['station_name'] in study_dict['radiology_org']: 850 study_dict['station_name'] = None 851 if study_dict['radiology_org_addr'] is not None: 852 if study_dict['station_name'] in study_dict['radiology_org_addr']: 853 study_dict['station_name'] = None 854 if study_dict['radiology_dept'] is not None: 855 if study_dict['station_name'] in study_dict['radiology_dept']: 856 study_dict['station_name'] = None 857 except KeyError: 858 pass 859 for key in study_dict: 860 if study_dict[key] in ['unknown', '(null)', '']: 861 study_dict[key] = None 862 study_dict[key] = cleanup_dicom_string(study_dict[key]) 863 study_dict['all_tags'] = {} 864 try: 865 orth_study['PatientMainDicomTags'] 866 except KeyError: 867 orth_study['PatientMainDicomTags'] = pat['MainDicomTags'] 868 for key in orth_study.keys(): 869 if key == 'MainDicomTags': 870 for mkey in orth_study['MainDicomTags'].keys(): 871 study_dict['all_tags'][mkey] = orth_study['MainDicomTags'][mkey].strip() 872 continue 873 if key == 'PatientMainDicomTags': 874 for pkey in orth_study['PatientMainDicomTags'].keys(): 875 study_dict['all_tags'][pkey] = orth_study['PatientMainDicomTags'][pkey].strip() 876 continue 877 study_dict['all_tags'][key] = orth_study[key] 878 _log.debug('study: %s', study_dict['all_tags'].keys()) 879 for key in study_keys2hide: 880 try: del study_dict['all_tags'][key] 881 except KeyError: pass 882 pat_dict['studies'].append(study_dict) 883 884 # loop over series in study 885 for orth_series_id in orth_study['Series']: 886 orth_series = self.__run_GET(url = '%s/series/%s' % (self.__server_url, orth_series_id)) 887 #slices = orth_series['Instances'] 888 ordered_slices = self.__run_GET(url = '%s/series/%s/ordered-slices' % (self.__server_url, orth_series_id)) 889 slices = [ s[0] for s in ordered_slices['SlicesShort'] ] 890 if orth_series is False: 891 _log.error('cannot retrieve series') 892 return [] 893 series_dict = { 894 'orthanc_id': orth_series['ID'], 895 'instances': slices, 896 'modality': None, 897 'date': None, 898 'time': None, 899 'description': None, 900 'body_part': None, 901 'protocol': None, 902 'performed_procedure_step_description': None, 903 'acquisition_device_processing_description': None, 904 'operator_name': None, 905 'radiographer_code': None, 906 'performing_doc': None 907 } 908 try: 909 series_dict['modality'] = orth_series['MainDicomTags']['Modality'].strip() 910 except KeyError: 911 pass 912 try: 913 series_dict['date'] = orth_series['MainDicomTags']['SeriesDate'].strip() 914 except KeyError: 915 pass 916 try: 917 series_dict['description'] = orth_series['MainDicomTags']['SeriesDescription'].strip() 918 except KeyError: 919 pass 920 try: 921 series_dict['time'] = orth_series['MainDicomTags']['SeriesTime'].strip() 922 except KeyError: 923 pass 924 try: 925 series_dict['body_part'] = orth_series['MainDicomTags']['BodyPartExamined'].strip() 926 except KeyError: 927 pass 928 try: 929 series_dict['protocol'] = orth_series['MainDicomTags']['ProtocolName'].strip() 930 except KeyError: 931 pass 932 try: 933 series_dict['performed_procedure_step_description'] = orth_series['MainDicomTags']['PerformedProcedureStepDescription'].strip() 934 except KeyError: 935 pass 936 try: 937 series_dict['acquisition_device_processing_description'] = orth_series['MainDicomTags']['AcquisitionDeviceProcessingDescription'].strip() 938 except KeyError: 939 pass 940 try: 941 series_dict['operator_name'] = orth_series['MainDicomTags']['OperatorsName'].strip() 942 except KeyError: 943 pass 944 try: 945 series_dict['radiographer_code'] = orth_series['MainDicomTags']['RadiographersCode'].strip() 946 except KeyError: 947 pass 948 try: 949 series_dict['performing_doc'] = orth_series['MainDicomTags']['PerformingPhysicianName'].strip() 950 except KeyError: 951 pass 952 for key in series_dict: 953 if series_dict[key] in ['unknown', '(null)', '']: 954 series_dict[key] = None 955 if series_dict['description'] == series_dict['protocol']: 956 _log.debug('<series description> matches <series protocol>, ignoring protocol') 957 series_dict['protocol'] = None 958 if series_dict['performed_procedure_step_description'] in [series_dict['description'], series_dict['protocol']]: 959 series_dict['performed_procedure_step_description'] = None 960 if series_dict['performed_procedure_step_description'] is not None: 961 # weed out "numeric" only 962 if regex.match ('[.,/\|\-\s\d]+', series_dict['performed_procedure_step_description'], flags = regex.UNICODE): 963 series_dict['performed_procedure_step_description'] = None 964 if series_dict['acquisition_device_processing_description'] in [series_dict['description'], series_dict['protocol']]: 965 series_dict['acquisition_device_processing_description'] = None 966 if series_dict['acquisition_device_processing_description'] is not None: 967 # weed out "numeric" only 968 if regex.match ('[.,/\|\-\s\d]+', series_dict['acquisition_device_processing_description'], flags = regex.UNICODE): 969 series_dict['acquisition_device_processing_description'] = None 970 if series_dict['date'] == study_dict['date']: 971 _log.debug('<series date> matches <study date>, ignoring date') 972 series_dict['date'] = None 973 if series_dict['time'] == study_dict['time']: 974 _log.debug('<series time> matches <study time>, ignoring time') 975 series_dict['time'] = None 976 for key in series_dict: 977 series_dict[key] = cleanup_dicom_string(series_dict[key]) 978 series_dict['all_tags'] = {} 979 for key in orth_series.keys(): 980 if key == 'MainDicomTags': 981 for mkey in orth_series['MainDicomTags'].keys(): 982 series_dict['all_tags'][mkey] = orth_series['MainDicomTags'][mkey].strip() 983 continue 984 series_dict['all_tags'][key] = orth_series[key] 985 _log.debug('series: %s', series_dict['all_tags'].keys()) 986 for key in series_keys2hide: 987 try: del series_dict['all_tags'][key] 988 except KeyError: pass 989 study_dict['operator_name'] = series_dict['operator_name'] # will collapse all operators into that of the last series 990 study_dict['radiographer_code'] = series_dict['radiographer_code'] # will collapse all into that of the last series 991 study_dict['performing_doc'] = series_dict['performing_doc'] # will collapse all into that of the last series 992 study_dict['series'].append(series_dict) 993 994 return studies_by_patient
995 996 #-------------------------------------------------------- 997 # generic REST helpers 998 #--------------------------------------------------------
999 - def __run_GET(self, url=None, data=None, allow_cached=False):
1000 if data is None: 1001 data = {} 1002 headers = {} 1003 if not allow_cached: 1004 headers['cache-control'] = 'no-cache' 1005 params = '' 1006 if len(data.keys()) > 0: 1007 params = '?' + urlencode(data) 1008 url_with_params = url + params 1009 1010 try: 1011 response, content = self.__conn.request(url_with_params, 'GET', headers = headers) 1012 except (socket.error, http.client.ResponseNotReady, http.client.InvalidURL, OverflowError, httplib2.ServerNotFoundError): 1013 _log.exception('exception in GET') 1014 _log.debug(' url: %s', url_with_params) 1015 _log.debug(' headers: %s', headers) 1016 return False 1017 1018 if response.status not in [ 200 ]: 1019 _log.error('GET returned non-OK status: %s', response.status) 1020 _log.debug(' url: %s', url_with_params) 1021 _log.debug(' headers: %s', headers) 1022 _log.error(' response: %s', response) 1023 _log.debug(' content: %s', content) 1024 return False 1025 1026 # _log.error(' response: %s', response) 1027 # _log.error(' content type: %s', type(content)) 1028 1029 if response['content-type'].startswith('text/plain'): 1030 # utf8 ? 1031 # urldecode ? 1032 # latin1 = Orthanc default = tools/default-encoding ? 1033 # ascii ? 1034 return content.decode('utf8') 1035 1036 if response['content-type'].startswith('application/json'): 1037 try: 1038 return json.loads(content) 1039 except Exception: 1040 return content 1041 1042 return content
1043 1044 #--------------------------------------------------------
1045 - def __run_POST(self, url=None, data=None, content_type=None, output_file=None):
1046 1047 body = data 1048 headers = {'content-type' : content_type} 1049 if isinstance(data, str): 1050 if content_type is None: 1051 headers['content-type'] = 'text/plain' 1052 elif isinstance(data, bytes): 1053 if content_type is None: 1054 headers['content-type'] = 'application/octet-stream' 1055 else: 1056 body = json.dumps(data) 1057 headers['content-type'] = 'application/json' 1058 1059 try: 1060 try: 1061 response, content = self.__conn.request(url, 'POST', body = body, headers = headers) 1062 except BrokenPipeError: 1063 response, content = self.__conn.request(url, 'POST', body = body, headers = headers) 1064 except (socket.error, http.client.ResponseNotReady, OverflowError): 1065 _log.exception('exception in POST') 1066 _log.debug(' url: %s', url) 1067 _log.debug(' headers: %s', headers) 1068 _log.debug(' body: %s', body[:16]) 1069 return False 1070 1071 if response.status == 404: 1072 _log.debug('no data, response: %s', response) 1073 if output_file is None: 1074 return [] 1075 return False 1076 if response.status not in [ 200, 302 ]: 1077 _log.error('POST returned non-OK status: %s', response.status) 1078 _log.debug(' url: %s', url) 1079 _log.debug(' headers: %s', headers) 1080 _log.debug(' body: %s', body[:16]) 1081 _log.error(' response: %s', response) 1082 _log.debug(' content: %s', content) 1083 return False 1084 1085 try: 1086 content = json.loads(content) 1087 except Exception: 1088 pass 1089 if output_file is None: 1090 return content 1091 output_file.write(content) 1092 return True
1093 1094 #--------------------------------------------------------
1095 - def __run_PUT(self, url=None, data=None, content_type=None):
1096 1097 body = data 1098 headers = {'content-type' : content_type} 1099 if isinstance(data, str): 1100 if content_type is None: 1101 headers['content-type'] = 'text/plain' 1102 elif isinstance(data, bytes): 1103 if content_type is None: 1104 headers['content-type'] = 'application/octet-stream' 1105 else: 1106 body = json.dumps(data) 1107 headers['content-type'] = 'application/json' 1108 1109 try: 1110 try: 1111 response, content = self.__conn.request(url, 'PUT', body = body, headers = headers) 1112 except BrokenPipeError: 1113 response, content = self.__conn.request(url, 'PUT', body = body, headers = headers) 1114 except (socket.error, http.client.ResponseNotReady, OverflowError): 1115 _log.exception('exception in PUT') 1116 _log.debug(' url: %s', url) 1117 _log.debug(' headers: %s', headers) 1118 _log.debug(' body: %s', body[:16]) 1119 return False 1120 1121 if response.status == 404: 1122 _log.debug('no data, response: %s', response) 1123 return [] 1124 if response.status not in [ 200, 302 ]: 1125 _log.error('PUT returned non-OK status: %s', response.status) 1126 _log.debug(' url: %s', url) 1127 _log.debug(' headers: %s', headers) 1128 _log.debug(' body: %s', body[:16]) 1129 _log.error(' response: %s', response) 1130 _log.debug(' content: %s', content) 1131 return False 1132 1133 if response['content-type'].startswith('text/plain'): 1134 # utf8 ? 1135 # urldecode ? 1136 # latin1 = Orthanc default = tools/default-encoding ? 1137 # ascii ? 1138 return content.decode('utf8') 1139 1140 if response['content-type'].startswith('application/json'): 1141 try: 1142 return json.loads(content) 1143 except Exception: 1144 return content 1145 1146 return content
1147 1148 #--------------------------------------------------------
1149 - def __run_DELETE(self, url=None):
1150 try: 1151 response, content = self.__conn.request(url, 'DELETE') 1152 except (http.client.ResponseNotReady, socket.error, OverflowError): 1153 _log.exception('exception in DELETE') 1154 _log.debug(' url: %s', url) 1155 return False 1156 1157 if response.status not in [ 200 ]: 1158 _log.error('DELETE returned non-OK status: %s', response.status) 1159 _log.debug(' url: %s', url) 1160 _log.error(' response: %s', response) 1161 _log.debug(' content: %s', content) 1162 return False 1163 1164 if response['content-type'].startswith('text/plain'): 1165 # utf8 ? 1166 # urldecode ? 1167 # latin1 = Orthanc default = tools/default-encoding ? 1168 # ascii ? 1169 return content.decode('utf8') 1170 1171 if response['content-type'].startswith('application/json'): 1172 try: 1173 return json.loads(content) 1174 except Exception: 1175 return content 1176 1177 return content
1178 1179 #------------------------------------------------------------
1180 -def cleanup_dicom_string(dicom_str):
1181 if not isinstance(dicom_str, str): 1182 return dicom_str 1183 dicom_str = regex.sub('\^+', ' ', dicom_str.strip('^')) 1184 #dicom_str = dicom_str.replace('\r\n', ' [CR] ') 1185 return dicom_str
1186 1187 #---------------------------------------------------------------------------
1188 -def dicomize_pdf(pdf_name=None, title=None, person=None, dcm_name=None, verbose=False):
1189 assert (pdf_name is not None), '<pdfname> must not be None' 1190 assert (person is not None), '<person> must not be None' 1191 1192 if title is None: 1193 title = pdf_name 1194 if dcm_name is None: 1195 dcm_name = gmTools.get_unique_filename(suffix = '.dcm') 1196 name = person.active_name 1197 cmd_line = [ 1198 'pdf2dcm', 1199 '--patient-id', person.suggest_external_id(target = 'PACS'), 1200 '--patient-name', ('%s^%s' % (name['lastnames'], name['firstnames'])).replace(' ', '^'), 1201 '--title', title, 1202 '--log-level', 'trace' 1203 ] 1204 if person['dob'] is not None: 1205 cmd_line.append('--patient-birthdate') 1206 cmd_line.append(person.get_formatted_dob(format = '%Y%m%d', honor_estimation = False)) 1207 if person['gender'] is not None: 1208 cmd_line.append('--patient-sex') 1209 cmd_line.append(_map_gender_gm2dcm[person['gender']]) 1210 cmd_line.append(pdf_name) 1211 cmd_line.append(dcm_name) 1212 success, exit_code, stdout = gmShellAPI.run_process(cmd_line = cmd_line, encoding = 'utf8', verbose = verbose) 1213 if success: 1214 return dcm_name 1215 return None
1216 1217 #============================================================ 1218 # main 1219 #------------------------------------------------------------ 1220 if __name__ == "__main__": 1221 1222 if len(sys.argv) == 1: 1223 sys.exit() 1224 1225 if sys.argv[1] != 'test': 1226 sys.exit() 1227 1228 # if __name__ == '__main__': 1229 # sys.path.insert(0, '../../') 1230 from Gnumed.pycommon import gmLog2 1231 1232 #--------------------------------------------------------
1233 - def orthanc_console(host, port):
1234 orthanc = cOrthancServer() 1235 if not orthanc.connect(host, port, user = None, password = None): #, expected_aet = 'another AET' 1236 print('error connecting to server:', orthanc.connect_error) 1237 return False 1238 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s] - API [%s])' % ( 1239 orthanc.server_identification['Name'], 1240 orthanc.server_identification['DicomAet'], 1241 orthanc.server_identification['Version'], 1242 orthanc.server_identification['DatabaseVersion'], 1243 orthanc.server_identification['ApiVersion'] 1244 )) 1245 print('') 1246 print('Please enter patient name parts, separated by SPACE.') 1247 1248 while True: 1249 entered_name = gmTools.prompted_input(prompt = "\nEnter person search term or leave blank to exit") 1250 if entered_name in ['exit', 'quit', 'bye', None]: 1251 print("user cancelled patient search") 1252 break 1253 1254 pats = orthanc.get_patients_by_external_id(external_id = entered_name) 1255 if len(pats) > 0: 1256 print('Patients found:') 1257 for pat in pats: 1258 print(' -> ', pat) 1259 continue 1260 1261 pats = orthanc.get_patients_by_name(name_parts = entered_name.split(), fuzzy = True) 1262 print('Patients found:') 1263 for pat in pats: 1264 print(' -> ', pat) 1265 print(' verifying ...') 1266 bad_data = orthanc.verify_patient_data(pat['ID']) 1267 print(' bad data:') 1268 for bad in bad_data: 1269 print(' -> ', bad) 1270 continue 1271 1272 continue 1273 1274 pats = orthanc.get_studies_list_by_patient_name(name_parts = entered_name.split(), fuzzy = True) 1275 print('Patients found from studies list:') 1276 for pat in pats: 1277 print(' -> ', pat['name']) 1278 for study in pat['studies']: 1279 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']))) 1280 # for series in study['series']: 1281 # print ( 1282 # u' ', 1283 # gmTools.format_dict_like ( 1284 # series, 1285 # relevant_keys = ['orthanc_id', 'date', 'time', 'modality', 'instances', 'body_part', 'protocol', 'description', 'station'], 1286 # 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' 1287 # ) 1288 # ) 1289 # print(orthanc.get_studies_with_dicomdir(study_ids = [study['orthanc_id']], filename = 'study_%s.zip' % study['orthanc_id'], create_zip = True)) 1290 #print(orthanc.get_study_as_zip(study_id = study['orthanc_id'], filename = 'study_%s.zip' % study['orthanc_id'])) 1291 #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'])) 1292 print('--------')
1293 1294 #--------------------------------------------------------
1295 - def run_console():
1296 try: 1297 host = sys.argv[2] 1298 except IndexError: 1299 host = None 1300 try: 1301 port = sys.argv[3] 1302 except IndexError: 1303 port = '8042' 1304 1305 orthanc_console(host, port)
1306 1307 #--------------------------------------------------------
1308 - def test_modify_patient_id():
1309 try: 1310 host = sys.argv[2] 1311 port = sys.argv[3] 1312 except IndexError: 1313 host = None 1314 port = '8042' 1315 orthanc = cOrthancServer() 1316 if not orthanc.connect(host, port, user = None, password = None): #, expected_aet = 'another AET' 1317 print('error connecting to server:', orthanc.connect_error) 1318 return False 1319 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s])' % ( 1320 orthanc.server_identification['Name'], 1321 orthanc.server_identification['DicomAet'], 1322 orthanc.server_identification['Version'], 1323 orthanc.server_identification['DatabaseVersion'] 1324 )) 1325 print('') 1326 print('Please enter patient name parts, separated by SPACE.') 1327 1328 entered_name = gmTools.prompted_input(prompt = "\nEnter person search term or leave blank to exit") 1329 if entered_name in ['exit', 'quit', 'bye', None]: 1330 print("user cancelled patient search") 1331 return 1332 1333 pats = orthanc.get_patients_by_name(name_parts = entered_name.split(), fuzzy = True) 1334 if len(pats) == 0: 1335 print('no patient found') 1336 return 1337 1338 pat = pats[0] 1339 print('test patient:') 1340 print(pat) 1341 old_id = pat['MainDicomTags']['PatientID'] 1342 new_id = old_id + '-1' 1343 print('setting [%s] to [%s]:' % (old_id, new_id), orthanc.modify_patient_id(old_id, new_id))
1344 1345 #--------------------------------------------------------
1346 - def test_upload_files():
1347 # try: 1348 # host = sys.argv[2] 1349 # port = sys.argv[3] 1350 # except IndexError: 1351 host = None 1352 port = '8042' 1353 1354 orthanc = cOrthancServer() 1355 if not orthanc.connect(host, port, user = None, password = None): #, expected_aet = 'another AET' 1356 print('error connecting to server:', orthanc.connect_error) 1357 return False 1358 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s] - REST API [%s])' % ( 1359 orthanc.server_identification['Name'], 1360 orthanc.server_identification['DicomAet'], 1361 orthanc.server_identification['Version'], 1362 orthanc.server_identification['DatabaseVersion'], 1363 orthanc.server_identification['ApiVersion'] 1364 )) 1365 print('') 1366 1367 #orthanc.upload_dicom_file(sys.argv[2]) 1368 orthanc.upload_from_directory(directory = sys.argv[2], recursive = True, check_mime_type = False, ignore_other_files = True)
1369 1370 #--------------------------------------------------------
1371 - def test_get_instance_preview():
1372 host = None 1373 port = '8042' 1374 1375 orthanc = cOrthancServer() 1376 if not orthanc.connect(host, port, user = None, password = None): #, expected_aet = 'another AET' 1377 print('error connecting to server:', orthanc.connect_error) 1378 return False 1379 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s])' % ( 1380 orthanc.server_identification['Name'], 1381 orthanc.server_identification['DicomAet'], 1382 orthanc.server_identification['Version'], 1383 orthanc.server_identification['DatabaseVersion'] 1384 )) 1385 print('') 1386 1387 print(orthanc.get_instance_preview('f4f07d22-0d8265ef-112ea4e9-dc140e13-350c06d1')) 1388 print(orthanc.get_instance('f4f07d22-0d8265ef-112ea4e9-dc140e13-350c06d1'))
1389 1390 #--------------------------------------------------------
1391 - def test_get_instance_tags():
1392 host = None 1393 port = '8042' 1394 1395 orthanc = cOrthancServer() 1396 if not orthanc.connect(host, port, user = None, password = None): #, expected_aet = 'another AET' 1397 print('error connecting to server:', orthanc.connect_error) 1398 return False 1399 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s])' % ( 1400 orthanc.server_identification['Name'], 1401 orthanc.server_identification['DicomAet'], 1402 orthanc.server_identification['Version'], 1403 orthanc.server_identification['DatabaseVersion'] 1404 )) 1405 print('') 1406 1407 instance_id = 'f4f07d22-0d8265ef-112ea4e9-dc140e13-350c06d1' 1408 for key, value in orthanc.get_instance_dicom_tags(instance_id, simplified = False).items(): 1409 print(key, ':', value) 1410 print()
1411 #print(orthanc.get_instance_dicom_tags(instance_id, simplified = True)) 1412 1413 #--------------------------------------------------------
1414 - def test_pdf2dcm():
1415 #print(pdf2dcm(filename = filename, patient_id = 'ID::abcABC', dob = '19900101')) 1416 from Gnumed.business import gmPerson 1417 pers = gmPerson.cPerson(12) 1418 print(dicomize_pdf(pdf_name = sys.argv[2], person = pers, dcm_name = None, verbose = True))#, title = 'test'))
1419 1420 #-------------------------------------------------------- 1421 #run_console() 1422 #test_modify_patient_id() 1423 #test_upload_files() 1424 #test_get_instance_preview() 1425 #test_get_instance_tags() 1426 test_pdf2dcm() 1427