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 gmDateTime 
  34  #from Gnumed.pycommon import gmHooks 
  35  #from Gnumed.pycommon import gmDispatcher 
  36   
  37  _log = logging.getLogger('gm.dicom') 
  38   
  39  _map_gender_gm2dcm = { 
  40          'm': 'M', 
  41          'f': 'F', 
  42          'tm': 'M', 
  43          'tf': 'F', 
  44          'h': 'O' 
  45  } 
  46   
  47  #============================================================ 
48 -class cOrthancServer:
49 # REST API access to Orthanc DICOM servers 50 51 # def __init__(self): 52 # self.__server_identification = None 53 # self.__user = None 54 # self.__password = None 55 # self.__conn = None 56 # self.__server_url = None 57 58 #--------------------------------------------------------
59 - def connect(self, host, port, user, password, expected_minimal_version=None, expected_name=None, expected_aet=None):
60 try: 61 int(port) 62 except Exception: 63 _log.error('invalid port [%s]', port) 64 return False 65 if (host is None) or (host.strip() == ''): 66 host = 'localhost' 67 try: 68 self.__server_url = str('http://%s:%s' % (host, port)) 69 except Exception: 70 _log.exception('cannot create server url from: host [%s] and port [%s]', host, port) 71 return False 72 self.__user = user 73 self.__password = password 74 _log.info('connecting as [%s] to Orthanc server at [%s]', self.__user, self.__server_url) 75 cache_dir = os.path.join(gmTools.gmPaths().user_tmp_dir, '.orthanc2gm-cache') 76 gmTools.mkdir(cache_dir, 0o700) 77 _log.debug('using cache directory: %s', cache_dir) 78 self.__conn = httplib2.Http(cache = cache_dir) 79 self.__conn.add_credentials(self.__user, self.__password) 80 _log.debug('connected to server: %s', self.server_identification) 81 self.connect_error = '' 82 if self.server_identification is False: 83 self.connect_error += 'retrieving server identification failed' 84 return False 85 if expected_minimal_version is not None: 86 if version.LooseVersion(self.server_identification['Version']) < version.LooseVersion(expected_min_version): 87 _log.error('server too old, needed [%s]', expected_min_version) 88 self.connect_error += 'server too old, needed version [%s]' % expected_min_version 89 return False 90 if expected_name is not None: 91 if self.server_identification['Name'] != expected_name: 92 _log.error('wrong server name, expected [%s]', expected_name) 93 self.connect_error += 'wrong server name, expected [%s]' % expected_name 94 return False 95 if expected_aet is not None: 96 if self.server_identification['DicomAet'] != expected_name: 97 _log.error('wrong server AET, expected [%s]', expected_aet) 98 self.connect_error += 'wrong server AET, expected [%s]' % expected_aet 99 return False 100 return True
101 102 #--------------------------------------------------------
103 - def _get_server_identification(self):
104 try: 105 return self.__server_identification 106 except AttributeError: 107 pass 108 system_data = self.__run_GET(url = '%s/system' % self.__server_url) 109 if system_data is False: 110 _log.error('unable to get server identification') 111 return False 112 _log.debug('server: %s', system_data) 113 self.__server_identification = system_data 114 115 self.__initial_orthanc_encoding = self.__run_GET(url = '%s/tools/default-encoding' % self.__server_url) 116 _log.debug('initial Orthanc encoding: %s', self.__initial_orthanc_encoding) 117 118 # check time skew 119 tolerance = 60 # seconds 120 client_now_as_utc = pydt.datetime.utcnow() 121 start = time.time() 122 orthanc_now_str = self.__run_GET(url = '%s/tools/now' % self.__server_url) # 20180208T165832 123 end = time.time() 124 query_duration = end - start 125 orthanc_now_unknown_tz = pydt.datetime.strptime(orthanc_now_str, '%Y%m%dT%H%M%S') 126 _log.debug('GNUmed "now" (UTC): %s', client_now_as_utc) 127 _log.debug('Orthanc "now" (UTC): %s', orthanc_now_unknown_tz) 128 _log.debug('wire roundtrip (seconds): %s', query_duration) 129 _log.debug('maximum skew tolerance (seconds): %s', tolerance) 130 if query_duration > tolerance: 131 _log.info('useless to check GNUmed/Orthanc time skew, wire roundtrip (%s) > tolerance (%s)', query_duration, tolerance) 132 else: 133 if orthanc_now_unknown_tz > client_now_as_utc: 134 real_skew = orthanc_now_unknown_tz - client_now_as_utc 135 else: 136 real_skew = client_now_as_utc - orthanc_now_unknown_tz 137 _log.info('GNUmed/Orthanc time skew: %s', real_skew) 138 if real_skew > pydt.timedelta(seconds = tolerance): 139 _log.error('GNUmed/Orthanc time skew > tolerance (may be due to timezone differences on Orthanc < v1.3.2)') 140 141 return self.__server_identification
142 143 server_identification = property(_get_server_identification, lambda x:x) 144 145 #--------------------------------------------------------
146 - def _get_as_external_id_issuer(self):
147 # fixed type :: user level instance name :: DICOM AET 148 return 'Orthanc::%(Name)s::%(DicomAet)s' % self.__server_identification
149 150 as_external_id_issuer = property(_get_as_external_id_issuer, lambda x:x) 151 152 #--------------------------------------------------------
153 - def _get_url_browse_patients(self):
154 if self.__user is None: 155 return self.__server_url 156 return self.__server_url.replace('http://', 'http://%s@' % self.__user)
157 158 url_browse_patients = property(_get_url_browse_patients, lambda x:x) 159 160 #--------------------------------------------------------
161 - def get_url_browse_patient(self, patient_id):
162 # http://localhost:8042/#patient?uuid=0da01e38-cf792452-65c1e6af-b77faf5a-b637a05b 163 return '%s/#patient?uuid=%s' % (self.url_browse_patients, patient_id)
164 165 #--------------------------------------------------------
166 - def get_url_browse_study(self, study_id):
167 # http://localhost:8042/#study?uuid=0da01e38-cf792452-65c1e6af-b77faf5a-b637a05b 168 return '%s/#study?uuid=%s' % (self.url_browse_patients, study_id)
169 170 #-------------------------------------------------------- 171 # download API 172 #--------------------------------------------------------
173 - def get_matching_patients(self, person):
174 _log.info('searching for Orthanc patients matching %s', person) 175 176 # look for patient by external ID first 177 pacs_ids = person.get_external_ids(id_type = 'PACS', issuer = self.as_external_id_issuer) 178 if len(pacs_ids) > 1: 179 _log.error('GNUmed patient has more than one ID for this PACS: %s', pacs_ids) 180 _log.error('the PACS ID is expected to be unique per PACS') 181 return [] 182 183 pacs_ids2use = [] 184 185 if len(pacs_ids) == 1: 186 pacs_ids2use.append(pacs_ids[0]['value']) 187 pacs_ids2use.extend(person.suggest_external_ids(target = 'PACS')) 188 189 for pacs_id in pacs_ids2use: 190 _log.debug('using PACS ID [%s]', pacs_id) 191 pats = self.get_patients_by_external_id(external_id = pacs_id) 192 if len(pats) > 1: 193 _log.warning('more than one Orthanc patient matches PACS ID: %s', pacs_id) 194 if len(pats) > 0: 195 return pats 196 197 _log.debug('no matching patient found in PACS') 198 # return find type ? especially useful for non-matches on ID 199 200 # search by name 201 202 # # then look for name parts 203 # name = person.get_active_name() 204 return []
205 206 #--------------------------------------------------------
207 - def get_patients_by_external_id(self, external_id=None):
208 matching_patients = [] 209 _log.info('searching for patients with external ID >>>%s<<<', external_id) 210 211 # elegant server-side approach: 212 search_data = { 213 'Level': 'Patient', 214 'CaseSensitive': False, 215 'Expand': True, 216 'Query': {'PatientID': external_id.strip('*')} 217 } 218 _log.info('server-side C-FIND SCU over REST search, mogrified search data: %s', search_data) 219 matches = self.__run_POST(url = '%s/tools/find' % self.__server_url, data = search_data) 220 221 # paranoia 222 for match in matches: 223 self.protect_patient(orthanc_id = match['ID']) 224 return matches
225 226 # # recursive brute force approach: 227 # for pat_id in self.__run_GET(url = '%s/patients' % self.__server_url): 228 # orthanc_pat = self.__run_GET(url = '%s/patients/%s' % (self.__server_url, pat_id)) 229 # orthanc_external_id = orthanc_pat['MainDicomTags']['PatientID'] 230 # if orthanc_external_id != external_id: 231 # continue 232 # _log.debug(u'match: %s (name=[%s], orthanc_id=[%s])', orthanc_external_id, orthanc_pat['MainDicomTags']['PatientName'], orthanc_pat['ID']) 233 # matching_patients.append(orthanc_pat) 234 # if len(matching_patients) == 0: 235 # _log.debug(u'no matches') 236 # return matching_patients 237 238 #--------------------------------------------------------
239 - def get_patients_by_name(self, name_parts=None, gender=None, dob=None, fuzzy=False):
240 _log.info('name parts %s, gender [%s], dob [%s], fuzzy: %s', name_parts, gender, dob, fuzzy) 241 if len(name_parts) > 1: 242 return self.get_patients_by_name_parts(name_parts = name_parts, gender = gender, dob = dob, fuzzy = fuzzy) 243 if not fuzzy: 244 search_term = name_parts[0].strip('*') 245 else: 246 search_term = name_parts[0] 247 if not search_term.endswith('*'): 248 search_term += '*' 249 search_data = { 250 'Level': 'Patient', 251 'CaseSensitive': False, 252 'Expand': True, 253 'Query': {'PatientName': search_term} 254 } 255 if gender is not None: 256 gender = _map_gender_gm2dcm[gender.lower()] 257 if gender is not None: 258 search_data['Query']['PatientSex'] = gender 259 if dob is not None: 260 search_data['Query']['PatientBirthDate'] = dob.strftime('%Y%m%d') 261 _log.info('server-side C-FIND SCU over REST search, mogrified search data: %s', search_data) 262 matches = self.__run_POST(url = '%s/tools/find' % self.__server_url, data = search_data) 263 return matches
264 265 #--------------------------------------------------------
266 - def get_patients_by_name_parts(self, name_parts=None, gender=None, dob=None, fuzzy=False):
267 # fuzzy: allow partial/substring matches (but not across name part boundaries ',' or '^') 268 matching_patients = [] 269 clean_parts = [] 270 for part in name_parts: 271 if part.strip() == '': 272 continue 273 clean_parts.append(part.lower().strip()) 274 _log.info('client-side patient search, scrubbed search terms: %s', clean_parts) 275 pat_ids = self.__run_GET(url = '%s/patients' % self.__server_url) 276 if pat_ids is False: 277 _log.error('cannot retrieve patients') 278 return [] 279 for pat_id in pat_ids: 280 orthanc_pat = self.__run_GET(url = '%s/patients/%s' % (self.__server_url, pat_id)) 281 if orthanc_pat is False: 282 _log.error('cannot retrieve patient') 283 continue 284 orthanc_name = orthanc_pat['MainDicomTags']['PatientName'].lower().strip() 285 if not fuzzy: 286 orthanc_name = orthanc_name.replace(' ', ',').replace('^', ',').split(',') 287 parts_in_orthanc_name = 0 288 for part in clean_parts: 289 if part in orthanc_name: 290 parts_in_orthanc_name += 1 291 if parts_in_orthanc_name == len(clean_parts): 292 _log.debug('name match: "%s" contains all of %s', orthanc_name, clean_parts) 293 if gender is not None: 294 gender = _map_gender_gm2dcm[gender.lower()] 295 if gender is not None: 296 if orthanc_pat['MainDicomTags']['PatientSex'].lower() != gender: 297 _log.debug('gender mismatch: dicom=[%s] gnumed=[%s], skipping', orthanc_pat['MainDicomTags']['PatientSex'], gender) 298 continue 299 if dob is not None: 300 if orthanc_pat['MainDicomTags']['PatientBirthDate'] != dob.strftime('%Y%m%d'): 301 _log.debug('dob mismatch: dicom=[%s] gnumed=[%s], skipping', orthanc_pat['MainDicomTags']['PatientBirthDate'], dob) 302 continue 303 matching_patients.append(orthanc_pat) 304 else: 305 _log.debug('name mismatch: "%s" does not contain all of %s', orthanc_name, clean_parts) 306 return matching_patients
307 308 #--------------------------------------------------------
309 - def get_studies_list_by_patient_name(self, name_parts=None, gender=None, dob=None, fuzzy=False):
310 return self.get_studies_list_by_orthanc_patient_list ( 311 orthanc_patients = self.get_patients_by_name(name_parts = name_parts, gender = gender, dob = dob, fuzzy = fuzzy) 312 )
313 314 #--------------------------------------------------------
315 - def get_studies_list_by_external_id(self, external_id=None):
316 return self.get_studies_list_by_orthanc_patient_list ( 317 orthanc_patients = self.get_patients_by_external_id(external_id = external_id) 318 )
319 320 #--------------------------------------------------------
321 - def get_study_as_zip(self, study_id=None, filename=None):
322 if filename is None: 323 filename = gmTools.get_unique_filename(prefix = r'DCM-', suffix = r'.zip') 324 _log.info('exporting study [%s] into [%s]', study_id, filename) 325 f = io.open(filename, 'wb') 326 f.write(self.__run_GET(url = '%s/studies/%s/archive' % (self.__server_url, str(study_id)), allow_cached = True)) 327 f.close() 328 return filename
329 330 #--------------------------------------------------------
331 - def get_study_as_zip_with_dicomdir(self, study_id=None, filename=None):
332 if filename is None: 333 filename = gmTools.get_unique_filename(prefix = r'DCM-', suffix = r'.zip') 334 _log.info('exporting study [%s] into [%s]', study_id, filename) 335 f = io.open(filename, 'wb') 336 f.write(self.__run_GET(url = '%s/studies/%s/media' % (self.__server_url, str(study_id)), allow_cached = True)) 337 f.close() 338 return filename
339 340 #--------------------------------------------------------
341 - def get_studies_as_zip(self, study_ids=None, patient_id=None, filename=None):
342 if filename is None: 343 filename = gmTools.get_unique_filename(prefix = r'DCM-', suffix = r'.zip') 344 if study_ids is None: 345 _log.info('exporting all studies of patient [%s] into [%s]', patient_id, filename) 346 f = io.open(filename, 'wb') 347 f.write(self.__run_GET(url = '%s/patients/%s/archive' % (self.__server_url, str(patient_id)), allow_cached = True)) 348 f.close() 349 return filename
350 351 #--------------------------------------------------------
352 - def _manual_get_studies_with_dicomdir(self, study_ids=None, patient_id=None, target_dir=None, filename=None, create_zip=False):
353 354 if filename is None: 355 filename = gmTools.get_unique_filename(prefix = r'DCM-', suffix = r'.zip', tmp_dir = target_dir) 356 357 # all studies 358 if study_ids is None: 359 _log.info('exporting all studies of patient [%s] into [%s]', patient_id, filename) 360 f = io.open(filename, 'wb') 361 url = '%s/patients/%s/media' % (self.__server_url, str(patient_id)) 362 _log.debug(url) 363 f.write(self.__run_GET(url = url, allow_cached = True)) 364 f.close() 365 if create_zip: 366 return filename 367 if target_dir is None: 368 target_dir = gmTools.mk_sandbox_dir(prefix = 'dcm-') 369 if not gmTools.unzip_archive(filename, target_dir = target_dir, remove_archive = True): 370 return False 371 return target_dir 372 373 # a selection of studies 374 dicomdir_cmd = 'gm-create_dicomdir' # args: 1) name of DICOMDIR to create 2) base directory where to start recursing for DICOM files 375 found, external_cmd = gmShellAPI.detect_external_binary(dicomdir_cmd) 376 if not found: 377 _log.error('[%s] not found', dicomdir_cmd) 378 return False 379 380 if create_zip: 381 sandbox_dir = gmTools.mk_sandbox_dir(prefix = 'dcm-') 382 _log.info('exporting studies [%s] into [%s] (sandbox [%s])', study_ids, filename, sandbox_dir) 383 else: 384 sandbox_dir = target_dir 385 _log.info('exporting studies [%s] into [%s]', study_ids, sandbox_dir) 386 _log.debug('sandbox dir: %s', sandbox_dir) 387 idx = 0 388 for study_id in study_ids: 389 study_zip_name = gmTools.get_unique_filename(prefix = 'dcm-', suffix = '.zip') 390 # getting with DICOMDIR returns DICOMDIR compatible subdirs and filenames 391 study_zip_name = self.get_study_as_zip_with_dicomdir(study_id = study_id, filename = study_zip_name) 392 # non-beautiful per-study dir name required by subsequent DICOMDIR generation 393 idx += 1 394 study_unzip_dir = os.path.join(sandbox_dir, 'STUDY%s' % idx) 395 _log.debug('study [%s] -> %s -> %s', study_id, study_zip_name, study_unzip_dir) 396 # need to extract into per-study subdir because get-with-dicomdir 397 # returns identical-across-studies subdirs / filenames 398 if not gmTools.unzip_archive(study_zip_name, target_dir = study_unzip_dir, remove_archive = True): 399 return False 400 401 # create DICOMDIR across all studies, 402 # we simply ignore the already existing per-study DICOMDIR files 403 target_dicomdir_name = os.path.join(sandbox_dir, 'DICOMDIR') 404 gmTools.remove_file(target_dicomdir_name, log_error = False) # better safe than sorry 405 _log.debug('generating [%s]', target_dicomdir_name) 406 cmd = '%(cmd)s %(DICOMDIR)s %(startdir)s' % { 407 'cmd': external_cmd, 408 'DICOMDIR': target_dicomdir_name, 409 'startdir': sandbox_dir 410 } 411 success = gmShellAPI.run_command_in_shell ( 412 command = cmd, 413 blocking = True 414 ) 415 if not success: 416 _log.error('problem running [gm-create_dicomdir]') 417 return False 418 # paranoia 419 try: 420 io.open(target_dicomdir_name) 421 except Exception: 422 _log.error('[%s] not generated, aborting', target_dicomdir_name) 423 return False 424 425 # return path to extracted studies 426 if not create_zip: 427 return sandbox_dir 428 429 # else return ZIP of all studies 430 studies_zip = shutil.make_archive ( 431 gmTools.fname_stem_with_path(filename), 432 'zip', 433 root_dir = gmTools.parent_dir(sandbox_dir), 434 base_dir = gmTools.dirname_stem(sandbox_dir), 435 logger = _log 436 ) 437 _log.debug('archived all studies with one DICOMDIR into: %s', studies_zip) 438 # studies can be _large_ so attempt to get rid of intermediate files 439 gmTools.rmdir(sandbox_dir) 440 return studies_zip
441 442 #--------------------------------------------------------
443 - def get_studies_with_dicomdir(self, study_ids=None, patient_id=None, target_dir=None, filename=None, create_zip=False):
444 445 if filename is None: 446 filename = gmTools.get_unique_filename(prefix = r'DCM-', suffix = r'.zip', tmp_dir = target_dir) 447 448 # all studies 449 if study_ids is None: 450 if patient_id is None: 451 raise ValueError('<patient_id> must be defined if <study_ids> is None') 452 _log.info('exporting all studies of patient [%s] into [%s]', patient_id, filename) 453 f = io.open(filename, 'wb') 454 url = '%s/patients/%s/media' % (self.__server_url, str(patient_id)) 455 _log.debug(url) 456 f.write(self.__run_GET(url = url, allow_cached = True)) 457 f.close() 458 if create_zip: 459 return filename 460 if target_dir is None: 461 target_dir = gmTools.mk_sandbox_dir(prefix = 'dcm-') 462 if not gmTools.unzip_archive(filename, target_dir = target_dir, remove_archive = True): 463 return False 464 return target_dir 465 466 # selection of studies 467 _log.info('exporting %s studies into [%s]', len(study_ids), filename) 468 _log.debug('studies: %s', study_ids) 469 f = io.open(filename, 'wb') 470 # You have to make a POST request against URI "/tools/create-media", with a 471 # JSON body that contains the array of the resources of interest (as Orthanc 472 # identifiers). Here is a sample command-line: 473 # 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 474 # (this will not create duplicates but will also not check for single-patient-ness) 475 url = '%s/tools/create-media-extended' % self.__server_url 476 _log.debug(url) 477 try: 478 downloaded = self.__run_POST(url = url, data = study_ids, output_file = f) 479 if not downloaded: 480 _log.error('this Orthanc version probably does not support "create-media-extended"') 481 except TypeError: 482 f.close() 483 _log.exception('cannot retrieve multiple studies as one archive with DICOMDIR, probably not supported by this Orthanc version') 484 return False 485 # retry with old URL 486 if not downloaded: 487 url = '%s/tools/create-media' % self.__server_url 488 _log.debug('retrying: %s', url) 489 try: 490 downloaded = self.__run_POST(url = url, data = study_ids, output_file = f) 491 if not downloaded: 492 return False 493 except TypeError: 494 _log.exception('cannot retrieve multiple studies as one archive with DICOMDIR, probably not supported by this Orthanc version') 495 return False 496 finally: 497 f.close() 498 if create_zip: 499 return filename 500 if target_dir is None: 501 target_dir = gmTools.mk_sandbox_dir(prefix = 'dcm-') 502 _log.debug('exporting studies into [%s]', target_dir) 503 if not gmTools.unzip_archive(filename, target_dir = target_dir, remove_archive = True): 504 return False 505 return target_dir
506 507 #--------------------------------------------------------
508 - def get_instance_dicom_tags(self, instance_id, simplified=True):
509 _log.debug('retrieving DICOM tags for instance [%s]', instance_id) 510 if simplified: 511 download_url = '%s/instances/%s/simplified-tags' % (self.__server_url, instance_id) 512 else: 513 download_url = '%s/instances/%s/tags' % (self.__server_url, instance_id) 514 return self.__run_GET(url = download_url, allow_cached = True)
515 516 #--------------------------------------------------------
517 - def get_instance_preview(self, instance_id, filename=None):
518 if filename is None: 519 filename = gmTools.get_unique_filename(suffix = '.png') 520 521 _log.debug('exporting preview for instance [%s] into [%s]', instance_id, filename) 522 download_url = '%s/instances/%s/preview' % (self.__server_url, instance_id) 523 f = io.open(filename, 'wb') 524 f.write(self.__run_GET(url = download_url, allow_cached = True)) 525 f.close() 526 return filename
527 528 #--------------------------------------------------------
529 - def get_instance(self, instance_id, filename=None):
530 if filename is None: 531 filename = gmTools.get_unique_filename(suffix = '.dcm') 532 533 _log.debug('exporting instance [%s] into [%s]', instance_id, filename) 534 download_url = '%s/instances/%s/attachments/dicom/data' % (self.__server_url, instance_id) 535 f = io.open(filename, 'wb') 536 f.write(self.__run_GET(url = download_url, allow_cached = True)) 537 f.close() 538 return filename
539 540 #-------------------------------------------------------- 541 # server-side API 542 #--------------------------------------------------------
543 - def protect_patient(self, orthanc_id):
544 url = '%s/patients/%s/protected' % (self.__server_url, str(orthanc_id)) 545 if self.__run_GET(url) == 1: 546 _log.debug('patient already protected: %s', orthanc_id) 547 return True 548 _log.warning('patient [%s] not protected against recycling, enabling protection now', orthanc_id) 549 self.__run_PUT(url = url, data = '1') 550 if self.__run_GET(url) == 1: 551 return True 552 _log.error('cannot protect patient [%s] against recycling', orthanc_id) 553 return False
554 555 #--------------------------------------------------------
556 - def unprotect_patient(self, orthanc_id):
557 url = '%s/patients/%s/protected' % (self.__server_url, str(orthanc_id)) 558 if self.__run_GET(url) == 0: 559 return True 560 _log.info('patient [%s] protected against recycling, disabling protection now', orthanc_id) 561 self.__run_PUT(url = url, data = '0') 562 if self.__run_GET(url) == 0: 563 return True 564 _log.error('cannot unprotect patient [%s] against recycling', orthanc_id) 565 return False
566 567 #--------------------------------------------------------
568 - def patient_is_protected(self, orthanc_id):
569 url = '%s/patients/%s/protected' % (self.__server_url, str(orthanc_id)) 570 return (self.__run_GET(url) == 1)
571 572 #--------------------------------------------------------
573 - def verify_patient_data(self, orthanc_id):
574 _log.info('verifying DICOM data of patient [%s]', orthanc_id) 575 bad_data = [] 576 instances_url = '%s/patients/%s/instances' % (self.__server_url, orthanc_id) 577 instances = self.__run_GET(instances_url) 578 for instance in instances: 579 instance_id = instance['ID'] 580 attachments_url = '%s/instances/%s/attachments' % (self.__server_url, instance_id) 581 attachments = self.__run_GET(attachments_url, allow_cached = True) 582 for attachment in attachments: 583 verify_url = '%s/%s/verify-md5' % (attachments_url, attachment) 584 # False, success = "{}" 585 #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 586 #2018-02-08 19:11:27 ERROR gm.dicom [-1211701504 MainThread] (gmDICOM.py::__run_POST() #987): response: {'status': '400', 'content-length': '0'} 587 if self.__run_POST(verify_url) is not False: 588 continue 589 _log.error('bad MD5 of DICOM file at url [%s]: patient=%s, attachment_type=%s', verify_url, orthanc_id, attachment) 590 bad_data.append({'patient': orthanc_id, 'instance': instance_id, 'type': attachment, 'orthanc': '%s [%s]' % (self.server_identification, self.__server_url)}) 591 592 return bad_data
593 594 #--------------------------------------------------------
595 - def modify_patient_id(self, old_patient_id, new_patient_id):
596 597 if old_patient_id == new_patient_id: 598 return True 599 600 modify_data = { 601 'Replace': { 602 'PatientID': new_patient_id 603 #,u'0010,0021': praxis.name / "GNUmed vX.X.X" 604 #,u'0010,1002': series of (old) patient IDs 605 } 606 , 'Force': True 607 # "Keep" doesn't seem to do what it suggests ATM 608 #, u'Keep': True 609 } 610 o_pats = self.get_patients_by_external_id(external_id = old_patient_id) 611 all_modified = True 612 for o_pat in o_pats: 613 _log.info('modifying Orthanc patient [%s]: DICOM ID [%s] -> [%s]', o_pat['ID'], old_patient_id, new_patient_id) 614 if self.patient_is_protected(o_pat['ID']): 615 _log.debug('patient protected: %s, unprotecting for modification', o_pat['ID']) 616 if not self.unprotect_patient(o_pat['ID']): 617 _log.error('cannot unlock patient [%s], skipping', o_pat['ID']) 618 all_modified = False 619 continue 620 was_protected = True 621 else: 622 was_protected = False 623 pat_url = '%s/patients/%s' % (self.__server_url, o_pat['ID']) 624 modify_url = '%s/modify' % pat_url 625 result = self.__run_POST(modify_url, data = modify_data) 626 _log.debug('modified: %s', result) 627 if result is False: 628 _log.error('cannot modify patient [%s]', o_pat['ID']) 629 all_modified = False 630 continue 631 newly_created_patient_id = result['ID'] 632 _log.debug('newly created Orthanc patient ID: %s', newly_created_patient_id) 633 _log.debug('deleting archived patient: %s', self.__run_DELETE(pat_url)) 634 if was_protected: 635 if not self.protect_patient(newly_created_patient_id): 636 _log.error('cannot re-lock (new) patient [%s]', newly_created_patient_id) 637 638 return all_modified
639 640 #-------------------------------------------------------- 641 # upload API 642 #--------------------------------------------------------
643 - def upload_dicom_file(self, filename, check_mime_type=False):
644 if gmTools.fname_stem(filename) == 'DICOMDIR': 645 _log.debug('ignoring [%s], no use uploading DICOMDIR files to Orthanc', filename) 646 return True 647 648 if check_mime_type: 649 if gmMimeLib.guess_mimetype(filename) != 'application/dicom': 650 _log.error('not considered a DICOM file: %s', filename) 651 return False 652 try: 653 f = io.open(filename, 'rb') 654 except Exception: 655 _log.exception('cannot open [%s]', filename) 656 return False 657 dcm_data = f.read() 658 f.close() 659 _log.debug('uploading [%s]', filename) 660 upload_url = '%s/instances' % self.__server_url 661 uploaded = self.__run_POST(upload_url, data = dcm_data, content_type = 'application/dicom') 662 if uploaded is False: 663 _log.error('cannot upload [%s]', filename) 664 return False 665 _log.debug(uploaded) 666 if uploaded['Status'] == 'AlreadyStored': 667 # paranoia, as is our custom 668 available_fields_url = '%s%s/attachments/dicom' % (self.__server_url, uploaded['Path']) # u'Path': u'/instances/1440110e-9cd02a98-0b1c0452-087d35db-3fd5eb05' 669 available_fields = self.__run_GET(available_fields_url, allow_cached = True) 670 if 'md5' not in available_fields: 671 _log.debug('md5 of instance not available in Orthanc, cannot compare against file md5, trusting Orthanc') 672 return True 673 md5_url = '%s/md5' % available_fields_url 674 md5_db = self.__run_GET(md5_url) 675 md5_file = gmTools.file2md5(filename) 676 if md5_file != md5_db: 677 _log.error('local md5: %s', md5_file) 678 _log.error('in-db md5: %s', md5_db) 679 _log.error('MD5 mismatch !') 680 return False 681 _log.error('MD5 match between file and database') 682 return True
683 684 #--------------------------------------------------------
685 - def upload_dicom_files(self, files=None, check_mime_type=False):
686 uploaded = [] 687 not_uploaded = [] 688 for filename in files: 689 success = self.upload_dicom_file(filename, check_mime_type = check_mime_type) 690 if success: 691 uploaded.append(filename) 692 continue 693 not_uploaded.append(filename) 694 695 if len(not_uploaded) > 0: 696 _log.error('not all files uploaded') 697 return (uploaded, not_uploaded)
698 699 #--------------------------------------------------------
700 - def upload_from_directory(self, directory=None, recursive=False, check_mime_type=False, ignore_other_files=True):
701 702 #-------------------- 703 def _on_error(exc): 704 _log.error('DICOM (?) file not accessible: %s', exc.filename) 705 _log.error(exc)
706 #-------------------- 707 708 _log.debug('uploading DICOM files from [%s]', directory) 709 if not recursive: 710 files2try = os.listdir(directory) 711 _log.debug('found %s files', len(files2try)) 712 if ignore_other_files: 713 files2try = [ f for f in files2try if gmMimeLib.guess_mimetype(f) == 'application/dicom' ] 714 _log.debug('DICOM files therein: %s', len(files2try)) 715 return self.upload_dicom_files(files = files2try, check_mime_type = check_mime_type) 716 717 _log.debug('recursing for DICOM files') 718 uploaded = [] 719 not_uploaded = [] 720 for curr_root, curr_root_subdirs, curr_root_files in os.walk(directory, onerror = _on_error): 721 _log.debug('recursing into [%s]', curr_root) 722 files2try = [ os.path.join(curr_root, f) for f in curr_root_files ] 723 _log.debug('found %s files', len(files2try)) 724 if ignore_other_files: 725 files2try = [ f for f in files2try if gmMimeLib.guess_mimetype(f) == 'application/dicom' ] 726 _log.debug('DICOM files therein: %s', len(files2try)) 727 up, not_up = self.upload_dicom_files ( 728 files = files2try, 729 check_mime_type = check_mime_type 730 ) 731 uploaded.extend(up) 732 not_uploaded.extend(not_up) 733 734 return (uploaded, not_uploaded)
735 736 #--------------------------------------------------------
737 - def upload_by_DICOMDIR(self, DICOMDIR=None):
738 pass
739 740 #-------------------------------------------------------- 741 # helper functions 742 #--------------------------------------------------------
743 - def get_studies_list_by_orthanc_patient_list(self, orthanc_patients=None):
744 745 study_keys2hide = ['ModifiedFrom', 'Type', 'ID', 'ParentPatient', 'Series'] 746 series_keys2hide = ['ModifiedFrom', 'Type', 'ID', 'ParentStudy', 'Instances'] 747 748 studies_by_patient = [] 749 series_keys = {} 750 series_keys_m = {} 751 752 # loop over patients 753 for pat in orthanc_patients: 754 pat_dict = { 755 'orthanc_id': pat['ID'], 756 'name': None, 757 'external_id': None, 758 'date_of_birth': None, 759 'gender': None, 760 'studies': [] 761 } 762 try: 763 pat_dict['name'] = pat['MainDicomTags']['PatientName'].strip() 764 except KeyError: 765 pass 766 try: 767 pat_dict['external_id'] = pat['MainDicomTags']['PatientID'].strip() 768 except KeyError: 769 pass 770 try: 771 pat_dict['date_of_birth'] = pat['MainDicomTags']['PatientBirthDate'].strip() 772 except KeyError: 773 pass 774 try: 775 pat_dict['gender'] = pat['MainDicomTags']['PatientSex'].strip() 776 except KeyError: 777 pass 778 for key in pat_dict: 779 if pat_dict[key] in ['unknown', '(null)', '']: 780 pat_dict[key] = None 781 pat_dict[key] = cleanup_dicom_string(pat_dict[key]) 782 studies_by_patient.append(pat_dict) 783 784 # loop over studies of patient 785 orth_studies = self.__run_GET(url = '%s/patients/%s/studies' % (self.__server_url, pat['ID'])) 786 if orth_studies is False: 787 _log.error('cannot retrieve studies') 788 return [] 789 for orth_study in orth_studies: 790 study_dict = { 791 'orthanc_id': orth_study['ID'], 792 'date': None, 793 'time': None, 794 'description': None, 795 'referring_doc': None, 796 'requesting_doc': None, 797 'performing_doc': None, 798 'operator_name': None, 799 'radiographer_code': None, 800 'radiology_org': None, 801 'radiology_dept': None, 802 'radiology_org_addr': None, 803 'station_name': None, 804 'series': [] 805 } 806 try: 807 study_dict['date'] = orth_study['MainDicomTags']['StudyDate'].strip() 808 except KeyError: 809 pass 810 try: 811 study_dict['time'] = orth_study['MainDicomTags']['StudyTime'].strip() 812 except KeyError: 813 pass 814 try: 815 study_dict['description'] = orth_study['MainDicomTags']['StudyDescription'].strip() 816 except KeyError: 817 pass 818 try: 819 study_dict['referring_doc'] = orth_study['MainDicomTags']['ReferringPhysicianName'].strip() 820 except KeyError: 821 pass 822 try: 823 study_dict['requesting_doc'] = orth_study['MainDicomTags']['RequestingPhysician'].strip() 824 except KeyError: 825 pass 826 try: 827 study_dict['radiology_org_addr'] = orth_study['MainDicomTags']['InstitutionAddress'].strip() 828 except KeyError: 829 pass 830 try: 831 study_dict['radiology_org'] = orth_study['MainDicomTags']['InstitutionName'].strip() 832 if study_dict['radiology_org_addr'] is not None: 833 if study_dict['radiology_org'] in study_dict['radiology_org_addr']: 834 study_dict['radiology_org'] = None 835 except KeyError: 836 pass 837 try: 838 study_dict['radiology_dept'] = orth_study['MainDicomTags']['InstitutionalDepartmentName'].strip() 839 if study_dict['radiology_org'] is not None: 840 if study_dict['radiology_dept'] in study_dict['radiology_org']: 841 study_dict['radiology_dept'] = None 842 if study_dict['radiology_org_addr'] is not None: 843 if study_dict['radiology_dept'] in study_dict['radiology_org_addr']: 844 study_dict['radiology_dept'] = None 845 except KeyError: 846 pass 847 try: 848 study_dict['station_name'] = orth_study['MainDicomTags']['StationName'].strip() 849 if study_dict['radiology_org'] is not None: 850 if study_dict['station_name'] in study_dict['radiology_org']: 851 study_dict['station_name'] = None 852 if study_dict['radiology_org_addr'] is not None: 853 if study_dict['station_name'] in study_dict['radiology_org_addr']: 854 study_dict['station_name'] = None 855 if study_dict['radiology_dept'] is not None: 856 if study_dict['station_name'] in study_dict['radiology_dept']: 857 study_dict['station_name'] = None 858 except KeyError: 859 pass 860 for key in study_dict: 861 if study_dict[key] in ['unknown', '(null)', '']: 862 study_dict[key] = None 863 study_dict[key] = cleanup_dicom_string(study_dict[key]) 864 study_dict['all_tags'] = {} 865 try: 866 orth_study['PatientMainDicomTags'] 867 except KeyError: 868 orth_study['PatientMainDicomTags'] = pat['MainDicomTags'] 869 for key in orth_study.keys(): 870 if key == 'MainDicomTags': 871 for mkey in orth_study['MainDicomTags'].keys(): 872 study_dict['all_tags'][mkey] = orth_study['MainDicomTags'][mkey].strip() 873 continue 874 if key == 'PatientMainDicomTags': 875 for pkey in orth_study['PatientMainDicomTags'].keys(): 876 study_dict['all_tags'][pkey] = orth_study['PatientMainDicomTags'][pkey].strip() 877 continue 878 study_dict['all_tags'][key] = orth_study[key] 879 _log.debug('study: %s', study_dict['all_tags'].keys()) 880 for key in study_keys2hide: 881 try: del study_dict['all_tags'][key] 882 except KeyError: pass 883 pat_dict['studies'].append(study_dict) 884 885 # loop over series in study 886 for orth_series_id in orth_study['Series']: 887 orth_series = self.__run_GET(url = '%s/series/%s' % (self.__server_url, orth_series_id)) 888 #slices = orth_series['Instances'] 889 ordered_slices = self.__run_GET(url = '%s/series/%s/ordered-slices' % (self.__server_url, orth_series_id)) 890 slices = [ s[0] for s in ordered_slices['SlicesShort'] ] 891 if orth_series is False: 892 _log.error('cannot retrieve series') 893 return [] 894 series_dict = { 895 'orthanc_id': orth_series['ID'], 896 'instances': slices, 897 'modality': None, 898 'date': None, 899 'time': None, 900 'description': None, 901 'body_part': None, 902 'protocol': None, 903 'performed_procedure_step_description': None, 904 'acquisition_device_processing_description': None, 905 'operator_name': None, 906 'radiographer_code': None, 907 'performing_doc': None 908 } 909 try: 910 series_dict['modality'] = orth_series['MainDicomTags']['Modality'].strip() 911 except KeyError: 912 pass 913 try: 914 series_dict['date'] = orth_series['MainDicomTags']['SeriesDate'].strip() 915 except KeyError: 916 pass 917 try: 918 series_dict['description'] = orth_series['MainDicomTags']['SeriesDescription'].strip() 919 except KeyError: 920 pass 921 try: 922 series_dict['time'] = orth_series['MainDicomTags']['SeriesTime'].strip() 923 except KeyError: 924 pass 925 try: 926 series_dict['body_part'] = orth_series['MainDicomTags']['BodyPartExamined'].strip() 927 except KeyError: 928 pass 929 try: 930 series_dict['protocol'] = orth_series['MainDicomTags']['ProtocolName'].strip() 931 except KeyError: 932 pass 933 try: 934 series_dict['performed_procedure_step_description'] = orth_series['MainDicomTags']['PerformedProcedureStepDescription'].strip() 935 except KeyError: 936 pass 937 try: 938 series_dict['acquisition_device_processing_description'] = orth_series['MainDicomTags']['AcquisitionDeviceProcessingDescription'].strip() 939 except KeyError: 940 pass 941 try: 942 series_dict['operator_name'] = orth_series['MainDicomTags']['OperatorsName'].strip() 943 except KeyError: 944 pass 945 try: 946 series_dict['radiographer_code'] = orth_series['MainDicomTags']['RadiographersCode'].strip() 947 except KeyError: 948 pass 949 try: 950 series_dict['performing_doc'] = orth_series['MainDicomTags']['PerformingPhysicianName'].strip() 951 except KeyError: 952 pass 953 for key in series_dict: 954 if series_dict[key] in ['unknown', '(null)', '']: 955 series_dict[key] = None 956 if series_dict['description'] == series_dict['protocol']: 957 _log.debug('<series description> matches <series protocol>, ignoring protocol') 958 series_dict['protocol'] = None 959 if series_dict['performed_procedure_step_description'] in [series_dict['description'], series_dict['protocol']]: 960 series_dict['performed_procedure_step_description'] = None 961 if series_dict['performed_procedure_step_description'] is not None: 962 # weed out "numeric" only 963 if regex.match ('[.,/\|\-\s\d]+', series_dict['performed_procedure_step_description'], flags = regex.UNICODE): 964 series_dict['performed_procedure_step_description'] = None 965 if series_dict['acquisition_device_processing_description'] in [series_dict['description'], series_dict['protocol']]: 966 series_dict['acquisition_device_processing_description'] = None 967 if series_dict['acquisition_device_processing_description'] is not None: 968 # weed out "numeric" only 969 if regex.match ('[.,/\|\-\s\d]+', series_dict['acquisition_device_processing_description'], flags = regex.UNICODE): 970 series_dict['acquisition_device_processing_description'] = None 971 if series_dict['date'] == study_dict['date']: 972 _log.debug('<series date> matches <study date>, ignoring date') 973 series_dict['date'] = None 974 if series_dict['time'] == study_dict['time']: 975 _log.debug('<series time> matches <study time>, ignoring time') 976 series_dict['time'] = None 977 for key in series_dict: 978 series_dict[key] = cleanup_dicom_string(series_dict[key]) 979 series_dict['all_tags'] = {} 980 for key in orth_series.keys(): 981 if key == 'MainDicomTags': 982 for mkey in orth_series['MainDicomTags'].keys(): 983 series_dict['all_tags'][mkey] = orth_series['MainDicomTags'][mkey].strip() 984 continue 985 series_dict['all_tags'][key] = orth_series[key] 986 _log.debug('series: %s', series_dict['all_tags'].keys()) 987 for key in series_keys2hide: 988 try: del series_dict['all_tags'][key] 989 except KeyError: pass 990 study_dict['operator_name'] = series_dict['operator_name'] # will collapse all operators into that of the last series 991 study_dict['radiographer_code'] = series_dict['radiographer_code'] # will collapse all into that of the last series 992 study_dict['performing_doc'] = series_dict['performing_doc'] # will collapse all into that of the last series 993 study_dict['series'].append(series_dict) 994 995 return studies_by_patient
996 997 #-------------------------------------------------------- 998 # generic REST helpers 999 #--------------------------------------------------------
1000 - def __run_GET(self, url=None, data=None, allow_cached=False):
1001 if data is None: 1002 data = {} 1003 headers = {} 1004 if not allow_cached: 1005 headers['cache-control'] = 'no-cache' 1006 params = '' 1007 if len(data.keys()) > 0: 1008 params = '?' + urlencode(data) 1009 url_with_params = url + params 1010 1011 try: 1012 response, content = self.__conn.request(url_with_params, 'GET', headers = headers) 1013 except (socket.error, http.client.ResponseNotReady, http.client.InvalidURL, OverflowError, httplib2.ServerNotFoundError): 1014 _log.exception('exception in GET') 1015 _log.debug(' url: %s', url_with_params) 1016 _log.debug(' headers: %s', headers) 1017 return False 1018 1019 if response.status not in [ 200 ]: 1020 _log.error('GET returned non-OK status: %s', response.status) 1021 _log.debug(' url: %s', url_with_params) 1022 _log.debug(' headers: %s', headers) 1023 _log.error(' response: %s', response) 1024 _log.debug(' content: %s', content) 1025 return False 1026 1027 # _log.error(' response: %s', response) 1028 # _log.error(' content type: %s', type(content)) 1029 1030 if response['content-type'].startswith('text/plain'): 1031 # utf8 ? 1032 # urldecode ? 1033 # latin1 = Orthanc default = tools/default-encoding ? 1034 # ascii ? 1035 return content.decode('utf8') 1036 1037 if response['content-type'].startswith('application/json'): 1038 try: 1039 return json.loads(content) 1040 except Exception: 1041 return content 1042 1043 return content
1044 1045 #--------------------------------------------------------
1046 - def __run_POST(self, url=None, data=None, content_type=None, output_file=None):
1047 1048 body = data 1049 headers = {'content-type' : content_type} 1050 if isinstance(data, str): 1051 if content_type is None: 1052 headers['content-type'] = 'text/plain' 1053 elif isinstance(data, bytes): 1054 if content_type is None: 1055 headers['content-type'] = 'application/octet-stream' 1056 else: 1057 body = json.dumps(data) 1058 headers['content-type'] = 'application/json' 1059 1060 try: 1061 try: 1062 response, content = self.__conn.request(url, 'POST', body = body, headers = headers) 1063 except BrokenPipeError: 1064 response, content = self.__conn.request(url, 'POST', body = body, headers = headers) 1065 except (socket.error, http.client.ResponseNotReady, OverflowError): 1066 _log.exception('exception in POST') 1067 _log.debug(' url: %s', url) 1068 _log.debug(' headers: %s', headers) 1069 _log.debug(' body: %s', body[:16]) 1070 return False 1071 1072 if response.status == 404: 1073 _log.debug('no data, response: %s', response) 1074 if output_file is None: 1075 return [] 1076 return False 1077 if response.status not in [ 200, 302 ]: 1078 _log.error('POST returned non-OK status: %s', response.status) 1079 _log.debug(' url: %s', url) 1080 _log.debug(' headers: %s', headers) 1081 _log.debug(' body: %s', body[:16]) 1082 _log.error(' response: %s', response) 1083 _log.debug(' content: %s', content) 1084 return False 1085 1086 try: 1087 content = json.loads(content) 1088 except Exception: 1089 pass 1090 if output_file is None: 1091 return content 1092 output_file.write(content) 1093 return True
1094 1095 #--------------------------------------------------------
1096 - def __run_PUT(self, url=None, data=None, content_type=None):
1097 1098 body = data 1099 headers = {'content-type' : content_type} 1100 if isinstance(data, str): 1101 if content_type is None: 1102 headers['content-type'] = 'text/plain' 1103 elif isinstance(data, bytes): 1104 if content_type is None: 1105 headers['content-type'] = 'application/octet-stream' 1106 else: 1107 body = json.dumps(data) 1108 headers['content-type'] = 'application/json' 1109 1110 try: 1111 try: 1112 response, content = self.__conn.request(url, 'PUT', body = body, headers = headers) 1113 except BrokenPipeError: 1114 response, content = self.__conn.request(url, 'PUT', body = body, headers = headers) 1115 except (socket.error, http.client.ResponseNotReady, OverflowError): 1116 _log.exception('exception in PUT') 1117 _log.debug(' url: %s', url) 1118 _log.debug(' headers: %s', headers) 1119 _log.debug(' body: %s', body[:16]) 1120 return False 1121 1122 if response.status == 404: 1123 _log.debug('no data, response: %s', response) 1124 return [] 1125 if response.status not in [ 200, 302 ]: 1126 _log.error('PUT returned non-OK status: %s', response.status) 1127 _log.debug(' url: %s', url) 1128 _log.debug(' headers: %s', headers) 1129 _log.debug(' body: %s', body[:16]) 1130 _log.error(' response: %s', response) 1131 _log.debug(' content: %s', content) 1132 return False 1133 1134 if response['content-type'].startswith('text/plain'): 1135 # utf8 ? 1136 # urldecode ? 1137 # latin1 = Orthanc default = tools/default-encoding ? 1138 # ascii ? 1139 return content.decode('utf8') 1140 1141 if response['content-type'].startswith('application/json'): 1142 try: 1143 return json.loads(content) 1144 except Exception: 1145 return content 1146 1147 return content
1148 1149 #--------------------------------------------------------
1150 - def __run_DELETE(self, url=None):
1151 try: 1152 response, content = self.__conn.request(url, 'DELETE') 1153 except (http.client.ResponseNotReady, socket.error, OverflowError): 1154 _log.exception('exception in DELETE') 1155 _log.debug(' url: %s', url) 1156 return False 1157 1158 if response.status not in [ 200 ]: 1159 _log.error('DELETE returned non-OK status: %s', response.status) 1160 _log.debug(' url: %s', url) 1161 _log.error(' response: %s', response) 1162 _log.debug(' content: %s', content) 1163 return False 1164 1165 if response['content-type'].startswith('text/plain'): 1166 # utf8 ? 1167 # urldecode ? 1168 # latin1 = Orthanc default = tools/default-encoding ? 1169 # ascii ? 1170 return content.decode('utf8') 1171 1172 if response['content-type'].startswith('application/json'): 1173 try: 1174 return json.loads(content) 1175 except Exception: 1176 return content 1177 1178 return content
1179 1180 #------------------------------------------------------------
1181 -def cleanup_dicom_string(dicom_str):
1182 if not isinstance(dicom_str, str): 1183 return dicom_str 1184 dicom_str = regex.sub('\^+', ' ', dicom_str.strip('^')) 1185 #dicom_str = dicom_str.replace('\r\n', ' [CR] ') 1186 return dicom_str
1187 1188 #---------------------------------------------------------------------------
1189 -def dicomize_pdf(pdf_name=None, title=None, person=None, dcm_name=None, verbose=False, dcm_template_file=None):
1190 assert (pdf_name is not None), '<pdfname> must not be None' 1191 assert (not ((person is None) and (dcm_template_file is None))), '<person> or <dcm_template_file> must not be None' 1192 1193 _log.debug('pdf: %s', pdf_name) 1194 if title is None: 1195 title = pdf_name 1196 if dcm_name is None: 1197 dcm_name = gmTools.get_unique_filename(suffix = '.dcm') 1198 now = gmDateTime.pydt_now_here() 1199 cmd_line = [ 1200 'pdf2dcm', 1201 '--title', title, 1202 '--key', '0008,0020=%s' % now.strftime('%Y%M%d'), # StudyDate 1203 '--key', '0008,0023=%s' % now.strftime('%H%m%s.0'), # ContentDate 1204 '--key', '0008,0030=%s' % now.strftime('%H%m%s.0'), # StudyTime 1205 '--key', '0008,0033=%s' % now.strftime('%H%m%s.0') # ContentTime 1206 ] 1207 if dcm_template_file is None: 1208 name = person.active_name 1209 cmd_line.append('--patient-id') 1210 cmd_line.append(person.suggest_external_id(target = 'PACS')) 1211 cmd_line.append('--patient-name') 1212 cmd_line.append(('%s^%s' % (name['lastnames'], name['firstnames'])).replace(' ', '^')) 1213 if person['dob'] is not None: 1214 cmd_line.append('--patient-birthdate') 1215 cmd_line.append(person.get_formatted_dob(format = '%Y%m%d', honor_estimation = False)) 1216 if person['gender'] is not None: 1217 cmd_line.append('--patient-sex') 1218 cmd_line.append(_map_gender_gm2dcm[person['gender']]) 1219 else: 1220 _log.debug('DCM template file: %s', dcm_template_file) 1221 cmd_line.append('--series-from') 1222 cmd_line.append(dcm_template_file) 1223 1224 if verbose: 1225 cmd_line.append('--log-level') 1226 cmd_line.append('trace') 1227 cmd_line.append(pdf_name) 1228 cmd_line.append(dcm_name) 1229 success, exit_code, stdout = gmShellAPI.run_process(cmd_line = cmd_line, encoding = 'utf8', verbose = verbose) 1230 if success: 1231 return dcm_name 1232 return None
1233 1234 #============================================================ 1235 # main 1236 #------------------------------------------------------------ 1237 if __name__ == "__main__": 1238 1239 if len(sys.argv) == 1: 1240 sys.exit() 1241 1242 if sys.argv[1] != 'test': 1243 sys.exit() 1244 1245 # if __name__ == '__main__': 1246 # sys.path.insert(0, '../../') 1247 from Gnumed.pycommon import gmLog2 1248 1249 #--------------------------------------------------------
1250 - def orthanc_console(host, port):
1251 orthanc = cOrthancServer() 1252 if not orthanc.connect(host, port, user = None, password = None): #, expected_aet = 'another AET' 1253 print('error connecting to server:', orthanc.connect_error) 1254 return False 1255 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s] - API [%s])' % ( 1256 orthanc.server_identification['Name'], 1257 orthanc.server_identification['DicomAet'], 1258 orthanc.server_identification['Version'], 1259 orthanc.server_identification['DatabaseVersion'], 1260 orthanc.server_identification['ApiVersion'] 1261 )) 1262 print('') 1263 print('Please enter patient name parts, separated by SPACE.') 1264 1265 while True: 1266 entered_name = gmTools.prompted_input(prompt = "\nEnter person search term or leave blank to exit") 1267 if entered_name in ['exit', 'quit', 'bye', None]: 1268 print("user cancelled patient search") 1269 break 1270 1271 pats = orthanc.get_patients_by_external_id(external_id = entered_name) 1272 if len(pats) > 0: 1273 print('Patients found:') 1274 for pat in pats: 1275 print(' -> ', pat) 1276 continue 1277 1278 pats = orthanc.get_patients_by_name(name_parts = entered_name.split(), fuzzy = True) 1279 print('Patients found:') 1280 for pat in pats: 1281 print(' -> ', pat) 1282 print(' verifying ...') 1283 bad_data = orthanc.verify_patient_data(pat['ID']) 1284 print(' bad data:') 1285 for bad in bad_data: 1286 print(' -> ', bad) 1287 continue 1288 1289 continue 1290 1291 pats = orthanc.get_studies_list_by_patient_name(name_parts = entered_name.split(), fuzzy = True) 1292 print('Patients found from studies list:') 1293 for pat in pats: 1294 print(' -> ', pat['name']) 1295 for study in pat['studies']: 1296 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']))) 1297 # for series in study['series']: 1298 # print ( 1299 # u' ', 1300 # gmTools.format_dict_like ( 1301 # series, 1302 # relevant_keys = ['orthanc_id', 'date', 'time', 'modality', 'instances', 'body_part', 'protocol', 'description', 'station'], 1303 # 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' 1304 # ) 1305 # ) 1306 # print(orthanc.get_studies_with_dicomdir(study_ids = [study['orthanc_id']], filename = 'study_%s.zip' % study['orthanc_id'], create_zip = True)) 1307 #print(orthanc.get_study_as_zip(study_id = study['orthanc_id'], filename = 'study_%s.zip' % study['orthanc_id'])) 1308 #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'])) 1309 print('--------')
1310 1311 #--------------------------------------------------------
1312 - def run_console():
1313 try: 1314 host = sys.argv[2] 1315 except IndexError: 1316 host = None 1317 try: 1318 port = sys.argv[3] 1319 except IndexError: 1320 port = '8042' 1321 1322 orthanc_console(host, port)
1323 1324 #--------------------------------------------------------
1325 - def test_modify_patient_id():
1326 try: 1327 host = sys.argv[2] 1328 port = sys.argv[3] 1329 except IndexError: 1330 host = None 1331 port = '8042' 1332 orthanc = cOrthancServer() 1333 if not orthanc.connect(host, port, user = None, password = None): #, expected_aet = 'another AET' 1334 print('error connecting to server:', orthanc.connect_error) 1335 return False 1336 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s])' % ( 1337 orthanc.server_identification['Name'], 1338 orthanc.server_identification['DicomAet'], 1339 orthanc.server_identification['Version'], 1340 orthanc.server_identification['DatabaseVersion'] 1341 )) 1342 print('') 1343 print('Please enter patient name parts, separated by SPACE.') 1344 1345 entered_name = gmTools.prompted_input(prompt = "\nEnter person search term or leave blank to exit") 1346 if entered_name in ['exit', 'quit', 'bye', None]: 1347 print("user cancelled patient search") 1348 return 1349 1350 pats = orthanc.get_patients_by_name(name_parts = entered_name.split(), fuzzy = True) 1351 if len(pats) == 0: 1352 print('no patient found') 1353 return 1354 1355 pat = pats[0] 1356 print('test patient:') 1357 print(pat) 1358 old_id = pat['MainDicomTags']['PatientID'] 1359 new_id = old_id + '-1' 1360 print('setting [%s] to [%s]:' % (old_id, new_id), orthanc.modify_patient_id(old_id, new_id))
1361 1362 #--------------------------------------------------------
1363 - def test_upload_files():
1364 # try: 1365 # host = sys.argv[2] 1366 # port = sys.argv[3] 1367 # except IndexError: 1368 host = None 1369 port = '8042' 1370 1371 orthanc = cOrthancServer() 1372 if not orthanc.connect(host, port, user = None, password = None): #, expected_aet = 'another AET' 1373 print('error connecting to server:', orthanc.connect_error) 1374 return False 1375 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s] - REST API [%s])' % ( 1376 orthanc.server_identification['Name'], 1377 orthanc.server_identification['DicomAet'], 1378 orthanc.server_identification['Version'], 1379 orthanc.server_identification['DatabaseVersion'], 1380 orthanc.server_identification['ApiVersion'] 1381 )) 1382 print('') 1383 1384 #orthanc.upload_dicom_file(sys.argv[2]) 1385 orthanc.upload_from_directory(directory = sys.argv[2], recursive = True, check_mime_type = False, ignore_other_files = True)
1386 1387 #--------------------------------------------------------
1388 - def test_get_instance_preview():
1389 host = None 1390 port = '8042' 1391 1392 orthanc = cOrthancServer() 1393 if not orthanc.connect(host, port, user = None, password = None): #, expected_aet = 'another AET' 1394 print('error connecting to server:', orthanc.connect_error) 1395 return False 1396 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s])' % ( 1397 orthanc.server_identification['Name'], 1398 orthanc.server_identification['DicomAet'], 1399 orthanc.server_identification['Version'], 1400 orthanc.server_identification['DatabaseVersion'] 1401 )) 1402 print('') 1403 1404 print(orthanc.get_instance_preview('f4f07d22-0d8265ef-112ea4e9-dc140e13-350c06d1')) 1405 print(orthanc.get_instance('f4f07d22-0d8265ef-112ea4e9-dc140e13-350c06d1'))
1406 1407 #--------------------------------------------------------
1408 - def test_get_instance_tags():
1409 host = None 1410 port = '8042' 1411 1412 orthanc = cOrthancServer() 1413 if not orthanc.connect(host, port, user = None, password = None): #, expected_aet = 'another AET' 1414 print('error connecting to server:', orthanc.connect_error) 1415 return False 1416 print('Connected to Orthanc server "%s" (AET [%s] - version [%s] - DB [%s])' % ( 1417 orthanc.server_identification['Name'], 1418 orthanc.server_identification['DicomAet'], 1419 orthanc.server_identification['Version'], 1420 orthanc.server_identification['DatabaseVersion'] 1421 )) 1422 print('') 1423 1424 instance_id = 'f4f07d22-0d8265ef-112ea4e9-dc140e13-350c06d1' 1425 for key, value in orthanc.get_instance_dicom_tags(instance_id, simplified = False).items(): 1426 print(key, ':', value) 1427 print()
1428 #print(orthanc.get_instance_dicom_tags(instance_id, simplified = True)) 1429 1430 #--------------------------------------------------------
1431 - def test_pdf2dcm():
1432 #print(pdf2dcm(filename = filename, patient_id = 'ID::abcABC', dob = '19900101')) 1433 from Gnumed.business import gmPerson 1434 pers = gmPerson.cPerson(12) 1435 try: 1436 print(dicomize_pdf(pdf_name = sys.argv[2], person = pers, dcm_name = None, verbose = True, dcm_template_file = sys.argv[3]))#, title = 'test')) 1437 except IndexError: 1438 print(dicomize_pdf(pdf_name = sys.argv[2], person = pers, dcm_name = None, verbose = True))#, title = 'test'))
1439 1440 #-------------------------------------------------------- 1441 #run_console() 1442 #test_modify_patient_id() 1443 #test_upload_files() 1444 #test_get_instance_preview() 1445 #test_get_instance_tags() 1446 test_pdf2dcm() 1447