docs: def get_chain_details(self, pdb_id, chain = None, internal_function_call = False, pfam_scop_mapping = {}):
          ''' Returns a dict pdb_id -> chain(s) -> chain and SCOPe details.
              This is the main function for getting details for a PDB chain. If there is an associated SCOPe entry for this
              chain then this function returns the most information.
              internal_function_call is used to prevent potential infinite loops
          '''
  
          query = '''
              SELECT DISTINCT scop_node.id AS scop_node_id, scop_node.*, pdb_entry.code, pdb_chain_id, pdb_chain.chain, pdb_chain.is_polypeptide, pdb_entry.description AS ChainDescription, pdb_release.resolution
              FROM `link_pdb`
              INNER JOIN scop_node on node_id=scop_node.id
              INNER JOIN pdb_chain ON pdb_chain_id = pdb_chain.id
              INNER JOIN pdb_release ON pdb_release_id = pdb_release.id
              INNER JOIN pdb_entry ON pdb_entry_id = pdb_entry.id
              WHERE pdb_entry.code=%s'''
          if chain:
              query += ' AND pdb_chain.chain=%s'
              parameters=(pdb_id, chain)
          else:
              parameters = (pdb_id, )
          query += ' ORDER BY release_id DESC'
  
          results = self.execute_select(query, parameters = parameters)
          if not results:
              if self.fallback_on_failures and not internal_function_call:
                  # Fallback - use any Pfam accession numbers associated with the chain to get partial information
                  #            Note: this fallback has another fallback in case none of the Pfam entries exist in SCOPe
                  searched_deeper = True
                  return self.get_chain_details_by_pfam(pdb_id, chain)
              else:
                  return None
  
          # I am making the assumption here that sids are consistent through releases i.e. that if d1aqt_1 is used in release
          # 3 then it will be used for any other releases where the domain is named
          sid_map = {}
          for r in results:
              sid = r['sid']
              c_id = r['chain']
              if not(sid_map.get(sid)) or sid_map[sid] == ' ':
                  sid_map[sid] = c_id
          chain_to_sid_map = {}
          for k, v in sid_map.iteritems():
              chain_to_sid_map[v] = chain_to_sid_map.get(v, set())
              chain_to_sid_map[v].add(k)
  
          leaf_node_chains = set()
          searched_deeper = False
          if pdb_id and chain:
              leaf_node_chains.add(chain)
          else:
              pdb_chain_ids = self.get_list_of_pdb_chains(pdb_id)
              if pdb_chain_ids:
                  leaf_node_chains = pdb_chain_ids
              else:
                  return None
  
          leaf_nodes = {}
          for c in leaf_node_chains:
              if c in chain_to_sid_map:
                  for sid in chain_to_sid_map[c]:
                      leaf_nodes[(c, sid)] = None
  
  
          # Only consider the most recent records
          for r in results:
              chain_id = r['chain']
              sid = r['sid']
              k = (chain_id, sid)
              if (not leaf_nodes.get(k)) or (r['release_id'] > leaf_nodes[k]['release_id']):
                  leaf_nodes[k] = r
  
          # Older revisions of SCOPe have blank chain IDs for some records while newer revisions have the chain ID
          # The best solution to avoid redundant results seems to be to remove all blank chain records if at least one
          # more recent named chain exists. There could be some nasty cases - we only keep the most recent unnamed chain
          # but this may correspond to many chains if the PDB has multiple chains since we only look at the chain ID.
          # I think that it should be *unlikely* that we will have much if any bad behavior though.
          for k1, v2 in leaf_nodes.iteritems():
              if k1[0] == ' ':
                  release_id_of_blank_record = leaf_nodes[k1]['release_id']
                  for k2, v2 in leaf_nodes.iteritems():
                      if k2[0] != ' ':
                          assert(k2[0].isalpha() and len(k2[0]) == 1)
                          if v2['release_id'] > release_id_of_blank_record:
                              del leaf_nodes[k1] # we are modifying a structure while iterating over it but we break immediately afterwards
                              break
  
          d = {}
          for chain_sid_pair, details in leaf_nodes.iteritems():
              chain_id = chain_sid_pair[0]
              sid = chain_sid_pair[1]
              if sid.strip() == '':
                  colortext.warning('FOUND AN EMPTY SID FIELD')
              assert(sid == details['sid'])
  
              # Get the details for all chains
              if details:
  
                  if d.get('resolution'):
                      assert(d['resolution'] == details['resolution'])
                  else:
                      d['resolution'] = details['resolution']
  
                  d['chains'] = d.get('chains', {})
                  if d['chains'].get(chain_id):
                      assert(d['chains'][chain_id]['is_polypeptide'] == details['is_polypeptide'])
                      assert(d['chains'][chain_id]['chain_description'] == details['ChainDescription'])
                  else:
                      d['chains'][chain_id] = {}
                      d['chains'][chain_id]['is_polypeptide'] = details['is_polypeptide']
                      d['chains'][chain_id]['chain_description'] = details['ChainDescription']
  
                  d['chains'][chain_id]['domains'] = d['chains'][chain_id].get('domains', {})
                  domain_information = dict(
                      #pdb_id = details['code'],
                      #chain = details['chain'],
                      #is_polypeptide = details['is_polypeptide'],
                      #chain_description = details['ChainDescription'],
                      sunid = details['sunid'],
                      sccs = details['sccs'],
                      sid = details['sid'],
                      scop_release_id = details['release_id'],
                      SCOPe_sources = 'SCOPe',
                      SCOPe_search_fields = 'link_pdb.pdb_chain_id',
                      SCOPe_trust_level = 1
                  )
  
                  for k, v in sorted(self.levels.iteritems()):
                      domain_information[v] = None
  
                  pfam = None
                  level, parent_node_id = details['level_id'], details['parent_node_id']
                  pfam = pfam or self.get_pfam_for_node(details['scop_node_id'])
  
                  # Store the top-level description
                  domain_information[self.levels[level]] = details['description']
  
                  # Wind up the level hierarchy and retrieve the descriptions
                  c = 0
                  while level > 2:
                      parent_details = self.execute_select('SELECT * FROM scop_node WHERE id=%s', parameters = (parent_node_id,))
                      assert(len(parent_details) <= 1)
                      if parent_details:
                          parent_details = parent_details[0]
                          level, parent_node_id = parent_details['level_id'], parent_details['parent_node_id']
                          pfam = pfam or self.get_pfam_for_node(parent_details['id'])
                          domain_information[self.levels[level]] = parent_details['description']
                      else:
                          break
                      # This should never trigger but just in case...
                      c += 1
                      if c > 20:
                          raise Exception('There is a logical error in the script or database which may result in an infinite lookup loop.')
                  domain_information['Pfam'] = pfam
  
                  # Fill in the residue range data
                  domain_information['pdbe_residue_range'] = None
                  sifts_object = self.get_sifts(pdb_id)
                  if sifts_object:
                      colortext.message(pdb_id)
                      region_mapping = sifts_object.region_mapping
                      ps_map = sifts_object.pfam_scop_mapping or {}
                      for k, v in ps_map.iteritems():
                          pfam_scop_mapping[k] = pfam_scop_mapping.get(k, set())
                          pfam_scop_mapping[k] = pfam_scop_mapping[k].union(v.get_matches('SCOP'))
  
                      residue_ranges = region_mapping.get(chain_id, {}).get('SCOP', {}).get(str(details['sunid']))
                      if residue_ranges:
                          residue_ranges = ', '.join(['%d-%d' % (t[0], t[1]) for t in residue_ranges])
                      domain_information['pdbe_residue_range'] = residue_ranges
  
                  d['chains'][chain_id]['domains'][sid] = domain_information
  
              else:
                  if self.fallback_on_failures and not(internal_function_call) and not(searched_deeper):
                      fallback_results = self.get_chain_details_by_pfam(pdb_id, chain_id)
                      if fallback_results and fallback_results.get(chain_id):
                          domain_information = fallback_results[chain_id]
          return d