text: import datetime
  import os
  from itertools import chain, starmap
  
  
  def dict_compare(old_dict, new_dict, nested=None):
      """ Compare two dictionaries
          Only 1 level, ignoring attributes starting with '_'
      """
      key_prefix = nested + '|' if nested else ''
      intersect_keys = old_dict.keys() & new_dict.keys()
      modified = {key_prefix + k: dict(old=old_dict[k], new=new_dict[k], action='mod') for k in intersect_keys
                          if k[0] != '_'
                          and old_dict[k] != new_dict[k]
                          and not (isinstance(old_dict[k], dict) and isinstance(new_dict[k], dict))}
  
      nested_keys = [k for k in intersect_keys if k[0] != '_' and isinstance(old_dict[k], dict) and isinstance(new_dict[k], dict)]
      for k in nested_keys:
          x = dict_compare(old_dict[k], new_dict[k], key_prefix + k)
          if x:
              modified.update(x)
  
      added = new_dict.keys() - old_dict.keys()
      modified.update({key_prefix + k: dict(new=new_dict[k], action='add') for k in added if k[0] != '_'})
  
      deleted = old_dict.keys() - new_dict.keys()
      modified.update({key_prefix + k: dict(old=old_dict[k], action='del') for k in deleted if k[0] != '_'})
      if modified:
          return modified
  
  
  def running_in_gcf():
      """ Determine if code is running in GCF using GCP_PROJECT
      """
      return os.getenv('GCP_PROJECT') is not None
  
  
  def default_object(o):
      """ Default handler for json.dumps()"""
      if isinstance(o, (datetime.date, datetime.datetime)):
          return o.isoformat()
      raise TypeError("Type %s not serializable" % type(o))
  
  
  # def buid_dict(composed_key, key_value):
  #     def recursive_buid_dict(keys, value, nest_level=0):
  #         if nest_level < len(keys):
  #             return recursive_buid_dict(keys, value={keys[nest_level]: value}, nest_level=nest_level + 1)
  #         elif nest_level == len(keys):
  #             return value
  #
  #     return recursive_buid_dict(keys=list(reversed(composed_key.split('.'))), value=key_value)
  #
  # from collections import ChainMap
  #
  # def merge_dicts(dict_a, dict_b):
  #     new_dict = dict()
  #     for key in ChainMap(dict_a, dict_b):
  #         val_a , val_b = dict_a.get(key), dict_b.get(key)
  #         if isinstance(val_a, dict) and isinstance(val_b, dict):
  #             new_dict[key] = merge_dicts(val_a, val_b)
  #         elif val_a is None and val_b is None:
  #             continue
  #         elif val_a is None:
  #             new_dict[key] = val_b
  #         elif val_b is None or val_a == val_b:
  #             new_dict[key] = val_a
  #         else:
  #             new_dict[key] = [val_a, val_b]
  #     return new_dict
  
  class Compare(object):
  
      def __init__(self, old_dictionary=None, new_dictionary=None, ignore_starting=None, ignore_ending=None):
          self.old_dict = old_dictionary
          self.new_dict = new_dictionary
          self.ignore_starting = ignore_starting
          self.ignore_ending = ignore_ending
          self.result = self.deepcompare(old_dict=old_dictionary,
                                         new_dict=new_dictionary,
                                         ignore_starting=self.ignore_starting,
                                         ignore_ending=self.ignore_starting)
          self.flatresult = self.flat()
  
      @staticmethod
      def intersect_lists(left_list, right_list):
          """Intersect two list and return difference as tuple (left, middle, right)
  
          USAGE:  
              l, _, _ = intersect_list(left_list, right_list)
                  return list of values in left list not matched with right list
              
              _, m, _ = intersect_list(left_list, right_list)
                  return list of values in matched in left list and right list
              
              _, _, r = intersect_list(left_list, right_list)
                  return list of values in right list not matched with left list
              
              l, m, r = intersect_list(left_list, right_list)
                  return all three above mentioned results
  
          EXAMPLE:
              l, m, r = intersect_list([1, 2, 3], [2, 3, 4])
              l = [1]
              m = [2, 3]
              r = [4]
          """
          l = [value for value in left_list if value not in right_list]
          m = [value for value in left_list if value in right_list]
          r = [value for value in right_list if value not in left_list]
          return l, m, r
  
      @staticmethod
      def unnest(dictionary, separator='.'):
          """Flatten a nested dictionary structure
          
          EXAMPLE:
          test_unnest = {'a': {'b': {'c': {'d': ['e', 'f'], 'g': 5}}}}
          unnest(test_unnest)
              returns {'a.b.c.d': ['e', 'f'], 'a.b.c.g': 5}
          """
  
          def unpack(parent_key, parent_value):
              """Unpack one level of nesting in a dictionary"""
              try:
                  for key, value in parent_value.items():
                      yield (f'{parent_key}{separator}{key}', value)
              except AttributeError:
                  # parent_value was not a dict, no need to flatten
                  yield (f'{parent_key}', parent_value)
  
          if not isinstance(dictionary, dict):
              return dictionary
          while any(isinstance(value, dict) for value in dictionary.values()): # TODO secure breaking
              # Keep unpacking the dictionary until all value's are not dictionary's
              try:
                  dictionary = dict(chain.from_iterable(starmap(unpack, dictionary.items())))
              except AttributeError:
                  break
          return dictionary
  
      @staticmethod
      def exclude_keys(set_of_keys, starting=None, ending=None):
          """Remove keys from set of keys starting and/or ending with string
          :param set_of_keys: a set of string keys to be iterated for ignoring keys
          :param starting: keys starting with string to be removed from a set of keys (default None)
          :param ending: keys ending with string to be removed from a set of keys (default None)
          """
  
          try:
              clean_keys = set()
              for key in set_of_keys:
                  if starting and ending and key.startswith(starting) and key.endswith(ending):
                      continue
                  elif starting and key.startswith(starting):
                      continue
                  elif ending and key.endswith(ending):
                      continue
                  else:
                      clean_keys.add(key)
              return clean_keys
          except TypeError as te:
              raise te
  
      def listtodict(self, lst):
          """Convert list of dictionaries to dictionary of dictionaries using index of list
          converts list of objects to dictionary of objects
          :param lst: list or dictionary to be converted to dictionary of objects
          """
          #NOTE: if object is not a dictionary it will be converted using default key as parent and index as child key
          if not isinstance(lst, (list, dict)):
              return lst
          try:
              result = {key: self.listtodict(value) for key, value in lst.items()}
          except AttributeError:
              result = {f'{index}': self.listtodict(value) for index, value in enumerate(lst)}
          return result
  
      def deepcompare(self, old_dict, new_dict, ignore_starting=None, ignore_ending=None):
          """Compare nested dictionaries, including lists recursively
          :param old_dict: the dictionary to be compared with
          :param new_dict: the dictionary to compare 
          :param ignore_starting: keys to be ignored starting with string (default None)
          :param ignore_ending: keys to be ignored ending with string (default None)
          :param unnested_result: boolean if return unnest(normalized) result
          :param separator: if unnested_result separator used to normalize keys
          """
          if old_dict == new_dict:
              return
          result = {}
          try:
              old = self.exclude_keys(old_dict.keys(), ignore_starting, ignore_ending)
              new = self.exclude_keys(new_dict.keys(), ignore_starting, ignore_ending)
              overlapping_keys = old & new
              old_keys = old - new
              new_keys = new - old
              if old_keys:
                  for key in old_keys:
                      result.update({key: {'action': 'del', 'old': old_dict[key]}})
              if new_keys:
                  for key in new_keys:
                      result.update({key: {'action': 'add', 'new': new_dict[key]}})
              for key in overlapping_keys:
                  if isinstance(old_dict[key], list) and isinstance(new_dict[key], list):
                      old_d = self.listtodict(old_dict[key])
                      new_d = self.listtodict(new_dict[key])
                      value = self.deepcompare(old_d, new_d)
                      if value is None:
                          continue
                  else:
                      value = self.deepcompare(old_dict[key], new_dict[key])
                  if value is None:
                      continue
                  if all(f'{index}' == key for index, key in enumerate(sorted(value.keys()))):
                      value = [v for _, v in value.items()]
                  result.update({key: value})
          except AttributeError:
              if old_dict != new_dict:
                  result = {'action': 'mod', 'old': old_dict, 'new': new_dict}
          finally:
              if result:
                  return result
  
      def flat(self, dictionary=None, separator='.', unpack_lists=False):
          if unpack_lists == True:
              return self.unnest(self.listtodict(dictionary or self.result), separator=separator)
          else:
              return self.unnest(dictionary or self.result, separator=separator)