def _validate_and_adapt_config_file()

in python/de-identifier/research_pacs/de_identifier/dicom.py [0:0]


  def _validate_and_adapt_config_file(self):
    """
    Validate the content of the config file, and prepare it for the following computation like 
    translating DICOM query filters to JSONPath queries.
    
    Labels: list                          List of labels
      - Name: str                         Label name
        DICOMQueryFilter: str             [Optional] DICOM query filter similar to searching or 
                                          exporting DICOM instances. If you don't provide a query 
                                          or if the query is empty, the label matches all DICOM 
                                          instances
    Categories: list                      List of categories. A category is a set of labels
      - Name: str                         Category name
        Labels: list                      List of labels associated with this category
    ScopeToForward: dict                  List of labels or categories that should be forwarded to 
                                          the target Orthanc server
      Labels: str or list
      ExceptLabels: str or list
      Categories: str or list
      ExceptCategories: str or list
    Transformations: list                 List of transformations to apply
      - Scope: dict                       Scope to which the transformation specified in this item 
                                          should apply. Similar to "ScopeToForward"
        [See below]                       See inline comments for the possible types of 
                                          transformations
        
    """
    VALID_REUSE_VALUE = ('Always', 'SamePatient', 'SameStudy', 'SameSeries', 'SameInstance')
    
    def label_exists(label_name):
      for label in self._config['Labels']:
        if label['Name'] == label_name:
          return True
      return False
      
    def category_exists(category_name):
      if not 'Categories' in self._config:
        return False
      for category in self._config['Categories']:
        if category['Name'] == category_name:
          return True
      return False
      
    def check_scope_rules(rules, path):
      """
      Args:
        rules (dict): Dict that can contain `Labels`, `ExceptLabels`, `Categories`, 
          `ExceptCategories` attributes
        path (str): Path to this dict in the config file
      
      """
      for rule_type in ('Labels', 'ExceptLabels', 'Categories', 'ExceptCategories'):
        if rule_type in rules:
          if isinstance(rules[rule_type], str):
            rules[rule_type] = [rules[rule_type]]
          assert isinstance(rules[rule_type], list), f'{path}["{rule_type}"] is not a string or a list of strings'
          rpacs_v.check_list_item_type(rules[rule_type], str, f'{path}["{rule_type}"]')
          for item in rules[rule_type]:
            if rule_type in ('Labels', 'ExceptLabels'):
              assert item == 'ALL' or label_exists(item), f'"{item}" is not a valid label. Make sure it exists in config["Labels"]'
            else:
              assert category_exists(item), f'"{item}" is not a valid category. Make sure it exists in config["Categories"]'      

    def check_tag_patterns_exist(element, path):
      """
      Check if the element contains an attribute "TagPatterns" that is a path pattern or a list of 
      path patterns, and an optional "ExceptTagPatterns".
      
      Args:
        element (dict)
        path (str)
      
      """
      assert 'TagPatterns' in element, f'{path}["TagPatterns"] is missing'
      element['TagPatterns'] = rpacs_v.check_or_form_list_of_str(element['TagPatterns'], f'{path}["TagPatterns"]')
      for i_tag_pattern, tag_pattern in enumerate(element['TagPatterns']):
        assert dicom_tpp.is_tag_path_pattern(tag_pattern), f'{path}["TagPatterns"][{i_tag_pattern}] is not a valid tag pattern'
      
      if 'ExceptTagPatterns' in element:
        element['ExceptTagPatterns'] = rpacs_v.check_or_form_list_of_str(element['ExceptTagPatterns'], f'{path}["ExceptTagPatterns"]')
        for i_tag_pattern, tag_pattern in enumerate(element['ExceptTagPatterns']):
          assert dicom_tpp.is_tag_path_pattern(tag_pattern), f'{path}["ExceptTagPatterns"][{i_tag_pattern}] is not a valid tag pattern'
      else:
        element['ExceptTagPatterns'] = []

    # Labels
    rpacs_v.check_dict_attribute_exists_and_type(self._config, 'Labels', list, 'config')
    for i_label, label in rpacs_v.enumerate_list_and_check_item_type(self._config['Labels'], dict, 'config["Labels"]'):
      rpacs_v.check_dict_attribute_exists_and_type(label, 'Name', str, f'config["Labels"][{i_label}]')

      # Check that `DICOMQueryFilter` is valid, if it is specified and not empty. Translate and 
      # store the associated JSON Path query into `label['JSONPathQuery']`
      if 'DICOMQueryFilter' in label and label['DICOMQueryFilter'] != '':
        try:
          label['JSONPathQuery'] = rpacs_dicom_json.translate_query_to_jsonpath(label['DICOMQueryFilter'])
        except:
          raise Exception(f'label["Labels"][{i_label}]["DICOMQueryFilter"] is not a valid query')
    
    # Categories
    if rpacs_v.check_dict_attribute_exists_and_type(self._config, 'Categories', list, 'config', optional=True) is True:
      for i_category, category in rpacs_v.enumerate_list_and_check_item_type(self._config['Categories'], dict, 'config["Categories"]'):
        rpacs_v.check_dict_attribute_exists_and_type(category, 'Name', str, f'config["Categories"][{i_category}]')
        rpacs_v.check_dict_attribute_exists_and_type(category, 'Labels', list, f'config["Categories"][{i_category}]')
        rpacs_v.check_list_item_type(category['Labels'], str, f'config["Categories"][{i_category}]["Labels"]')

    # Scope to forward
    rpacs_v.check_dict_attribute_exists_and_type(self._config, 'ScopeToForward', dict, 'config')
    check_scope_rules(self._config['ScopeToForward'], 'config["ScopeToForward"]')

    # Transformations
    rpacs_v.check_dict_attribute_exists_and_type(self._config, 'Transformations', list, 'config')
    for i_t, t in rpacs_v.enumerate_list_and_check_item_type(self._config['Transformations'], dict, 'config["Transformations"]'):
      t_path = f'config["Transformations"][{i_t}]'
      rpacs_v.check_dict_attribute_exists_and_type(t, 'Scope', dict, t_path)
      check_scope_rules(t['Scope'], f'{t_path}["Scope"]')

      # ShiftDateTime
      #   - TagPatterns: str or list              List of UID tag patterns to shift
      #     ExceptTagPatterns: str or list        Except this list of tag patterns
      #     ShiftBy: int                          Will shift by a random number of days (if Date) 
      #                                           or seconds (if DateTime or Time) between
      #                                           `-ShiftBy` and `+ShiftBy`
      #     ReuseMapping: str                     [Optional] Scope of the mapping
      if rpacs_v.check_dict_attribute_exists_and_type(t, 'ShiftDateTime', list, t_path, optional=True) is True:
        for i_element, element in rpacs_v.enumerate_list_and_check_item_type(t['ShiftDateTime'], dict, f'{t_path}["ShiftDateTime"]'):
          check_tag_patterns_exist(element, f'{t_path}["ShiftDateTime"][{i_element}]')
          rpacs_v.check_dict_attribute_exists_and_type(element, 'ShiftBy', int, f'{t_path}["ShiftDateTime"]')
          if rpacs_v.check_dict_attribute_exists_and_type(element, 'ReuseMapping', str, f'{t_path}["ShiftDateTime"]', optional=True) is True:
            assert element['ReuseMapping'] in VALID_REUSE_VALUE, f'{t_path}["ShiftDateTime"][{i_element}]["ReuseMapping"] is invalid'

      # RandomizeText
      #   - TagPatterns: str or list              List of UID tag patterns to shift
      #     ExceptTagPatterns: str or list        Except this list of tag patterns
      #     Split: str                            Split the element value on `Split` and randomize
      #                                           each item obtained separately
      #     IgnoreCase: bool                      Specified whether the original value must be 
      #                                           lowercased before being randomized. Default is
      #                                           `False`.
      #     ReuseMapping: str                     [Optional] Scope of the mapping
      if rpacs_v.check_dict_attribute_exists_and_type(t, 'RandomizeText', list, t_path, optional=True) is True:
        for i_element, element in rpacs_v.enumerate_list_and_check_item_type(t['RandomizeText'], dict, f'{t_path}["RandomizeText"]'):
          check_tag_patterns_exist(element, f'{t_path}["RandomizeText"][{i_element}]')
          if rpacs_v.check_dict_attribute_exists_and_type(element, 'Split', str, f'{t_path}["RandomizeText"]', optional=True) is False:
            element['Split'] = None
          if rpacs_v.check_dict_attribute_exists_and_type(element, 'IgnoreCase', bool, f'{t_path}["RandomizeText"]', optional=True) is False:
            element['IgnoreCase'] = False
          if rpacs_v.check_dict_attribute_exists_and_type(element, 'ReuseMapping', str, f'{t_path}["RandomizeText"]', optional=True) is True:
            assert element['ReuseMapping'] in VALID_REUSE_VALUE, f'{t_path}["RandomizeText"][{i_element}]["ReuseMapping"] is invalid'

      # RandomizeUID:
      #   - TagPatterns: str or list              List of UID tag patterns to randomize
      #     ExceptTagPatterns: str or list        Except this list of tag patterns
      #     PrefixUID: str                        [Optional] UID prefix to use when creating the 
      #                                           UID. Default is the pydicom root UID
      if rpacs_v.check_dict_attribute_exists_and_type(t, 'RandomizeUID', list, t_path, optional=True) is True:
        for i_element, element in rpacs_v.enumerate_list_and_check_item_type(t['RandomizeUID'], dict, f'{t_path}["RandomizeUID"]'):
          check_tag_patterns_exist(element, f'{t_path}["RandomizeUID"][{i_element}]')
          rpacs_v.check_dict_attribute_exists_and_type(element, 'Prefix', str, f'{t_path}["RandomizeUID"]', optional=True)
      
      # AddTags
      #   - Tag: str                              Tag path
      #     VR: str                               Value Representation of the tag
      #     Value: str                            Value of the tag to create
      #     OverwriteIfExists                     If the tag already exists, set `True` to 
      #                                           overwrite its value. Default is `False`
      if rpacs_v.check_dict_attribute_exists_and_type(t, 'AddTags', list, t_path, optional=True) is True:
        for i_element, element in rpacs_v.enumerate_list_and_check_item_type(t['AddTags'], dict, f'{t_path}["AddTags"]'):
          rpacs_v.check_dict_attribute_exists_and_type(element, 'Tag', str, f'{t_path}["AddTags"]')
          assert dicom_tp.is_tag_path(element['Tag']), f'{t_path}["AddTags"][{i_element}]["Tag"] is not a valid tag path'
          rpacs_v.check_dict_attribute_exists_and_type(element, 'VR', str, f'{t_path}["AddTags"]')
          rpacs_v.check_dict_attribute_exists_and_type(element, 'Value', str, f'{t_path}["AddTags"]')
          if rpacs_v.check_dict_attribute_exists_and_type(element, 'OverwriteIfExists', bool, f'{t_path}["AddTags"]', optional=True) is False:
            element['OverwriteIfExists'] = False
      
      # RemoveBurnedInAnnotations:
      #   - Type: str                     OCR or Manual
      #     BoxCoordinates: list          [Conditional] Provide a list of box coordinates. Each 
      #                                   box coordinate is a 4-element list with integer (left, 
      #                                   top, right, bottom)
      if rpacs_v.check_dict_attribute_exists_and_type(t, 'RemoveBurnedInAnnotations', list, t_path, optional=True) is True:
        for i_element, element in rpacs_v.enumerate_list_and_check_item_type(t['RemoveBurnedInAnnotations'], dict, f'{t_path}["RemoveBurnedInAnnotations"]'):
          rpacs_v.check_dict_attribute_exists_and_type(element, 'Type', str, f'{t_path}["RemoveBurnedInAnnotations"][{i_element}]')
          assert element['Type'] in ('OCR', 'Manual'), f'{t_path}["RemoveBurnedInAnnotations"][{i_element}]["Type"] must be equal to "OCR" or "Manual"'
          
          if element['Type'] == 'Manual':
            rpacs_v.check_dict_attribute_exists_and_type(element, 'BoxCoordinates', list, f'{t_path}["RemoveBurnedInAnnotations"][{i_element}]')
            for i_box, box in rpacs_v.enumerate_list_and_check_item_type(element['BoxCoordinates'], list, f'{t_path}["RemoveBurnedInAnnotations"][{i_element}]["BoxCoordinates"]'):
              rpacs_v.check_list_item_type(box, int, f'{t_path}["RemoveBurnedInAnnotations"][{i_element}]["BoxCoordinates"][{i_box}]')
              assert len(box) == 4, f'{t_path}["RemoveBurnedInAnnotations"][{i_element}]["BoxCoordinates"][{i_box}] is not a 4-element list'
              assert box[0] < box[2] and box[1] < box[3], f'{t_path}["RemoveBurnedInAnnotations"][{i_element}]["BoxCoordinates"][{i_box}] contains invalid coordinates'

      # DeleteTags:
      #   - TagPatterns: str or list                      List of tag patterns to remove
      #     ExceptTagPatterns: str or list                List of tag patterns to retain
      #     Action: str                                   Remove or Empty
      if rpacs_v.check_dict_attribute_exists_and_type(t, 'DeleteTags', list, t_path, optional=True) is True:
        for i_element, element in rpacs_v.enumerate_list_and_check_item_type(t['DeleteTags'], dict, f'{t_path}["DeleteTags"]'):
          check_tag_patterns_exist(element, f'{t_path}["DeleteTags"][{i_element}]')
          rpacs_v.check_dict_attribute_exists_and_type(element, 'Action', str, f'{t_path}["DeleteTags"][{i_element}]')
          assert element['Action'] in ('Remove', 'Empty'), f'{t_path}["DeleteTags"][{i_element}]["Action"] must be equal to "Remove" or "Empty"'

      # Transcode: str                    Transfer syntax UID to which the de-identified DICOM
      #                                   file should be transcoded. If not provided, the 
      #                                   de-identified DICOM file will use the same transfer
      #                                   syntax than the original DICOM file.
      rpacs_v.check_dict_attribute_exists_and_type(t, 'Transcode', str, t_path, optional=True)