Gnumed.business.gmDICOM

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