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

# Source Code for Module Gnumed.business.gmDICOM

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