def _update_keywords(self, **update_props): """ Update operation for ISO type-specific Keywords metadata: Theme or Place """ tree_to_update = update_props['tree_to_update'] prop = update_props['prop'] values = update_props['values'] keywords = [] if prop in KEYWORD_PROPS: xpath_root = self._data_map['_keywords_root'] xpath_map = self._data_structures[prop] xtype = xpath_map['keyword_type'] xroot = xpath_map['keyword_root'] xpath = xpath_map['keyword'] ktype = KEYWORD_TYPES[prop] # Remove descriptiveKeyword nodes according to type for element in get_elements(tree_to_update, xpath_root): if get_element_text(element, xtype).lower() == ktype.lower(): remove_element(tree_to_update, xpath_root) element = insert_element(tree_to_update, 0, xpath_root) insert_element(element, 0, xtype, ktype) # Add the type node keywords.extend(update_property(element, xroot, xpath, prop, values)) return keywords
def update_complex_list(tree_to_update, xpath_root, xpath_map, prop, values): """ Updates and returns the list of updated complex Elements parsed from tree_to_update. :param tree_to_update: the XML tree compatible with element_utils to be updated :param xpath_root: the XPATH location of each complex Element :param xpath_map: a Dictionary of XPATHs corresponding to the complex structure definition :param prop: the property identifying the complex structure to be serialized :param values: a List containing the updated complex structures as Dictionaries """ complex_list = [] remove_element(tree_to_update, xpath_root, True) if not values: # Returns the elements corresponding to property removed from the tree complex_list.append(update_property(tree_to_update, xpath_root, xpath_root, prop, values)) else: for idx, complex_struct in enumerate(wrap_value(values)): # Insert a new complex element root for each dict in the list complex_element = insert_element(tree_to_update, idx, xpath_root) for subprop, value in iteritems(complex_struct): xpath = get_xpath_branch(xpath_root, xpath_map[subprop]) value = get_default_for_complex_sub(prop, subprop, value, xpath) complex_list.append(update_property(complex_element, None, xpath, subprop, value)) return complex_list
def _update_keywords(self, **update_props): """ Update operation for ISO type-specific Keywords metadata: Theme or Place """ tree_to_update = update_props['tree_to_update'] prop = update_props['prop'] values = update_props['values'] keywords = [] if prop in [KEYWORDS_PLACE, KEYWORDS_THEME]: xpath_root = self._data_map['_keywords_root'] xpath_map = self._data_structures[update_props['prop']] xtype = xpath_map['keyword_type'] xroot = xpath_map['keyword_root'] xpath = xpath_map['keyword'] if prop == KEYWORDS_PLACE: ktype = KEYWORD_TYPE_PLACE elif prop == KEYWORDS_THEME: ktype = KEYWORD_TYPE_THEME # Remove descriptiveKeyword nodes according to type for element in get_elements(tree_to_update, xpath_root): if get_element_text(element, xtype).lower() == ktype.lower(): remove_element(tree_to_update, xpath_root) element = insert_element(tree_to_update, 0, xpath_root) insert_element(element, 0, xtype, ktype) # Add the type node keywords.extend(update_property(element, xroot, xpath, prop, values)) return keywords
def _update_dates(self, xpath_root=None, **update_props): """ Default update operation for Dates metadata :see: gis_metadata.utils._complex_definitions[DATES] """ tree_to_update = update_props['tree_to_update'] prop = update_props['prop'] values = (update_props['values'] or {}).get(DATE_VALUES) or u'' xpaths = self._data_structures[prop] if not self.dates: date_xpaths = xpath_root elif self.dates[DATE_TYPE] != DATE_TYPE_RANGE: date_xpaths = xpaths.get(self.dates[DATE_TYPE], u'') else: date_xpaths = [ xpaths[DATE_TYPE_RANGE_BEGIN], xpaths[DATE_TYPE_RANGE_END] ] if xpath_root: remove_element(tree_to_update, xpath_root) return update_property(tree_to_update, xpath_root, date_xpaths, prop, values)
def update_complex(tree_to_update, xpath_root, xpath_map, prop, values): """ Updates and returns the updated complex Element parsed from tree_to_update. :param tree_to_update: the XML tree compatible with element_utils to be updated :param xpath_root: the XPATH location of the root of the complex Element :param xpath_map: a Dictionary of XPATHs corresponding to the complex structure definition :param prop: the property identifying the complex structure to be serialized :param values: a Dictionary representing the complex structure to be updated """ remove_element(tree_to_update, xpath_root, True) values = reduce_value(values, {}) if not values: # Returns the elements corresponding to property removed from the tree updated = update_property(tree_to_update, xpath_root, xpath_root, prop, values) else: for subprop, value in iteritems(values): xpath = xpath_map[subprop] value = get_default_for_complex_sub(prop, subprop, value, xpath) update_property(tree_to_update, None, xpath, subprop, value) updated = get_element(tree_to_update, xpath_root) return updated
def update_element(elem, idx, root, path, vals): """ Internal helper function to encapsulate single item update """ has_root = bool(root and len(path) > len(root) and path.startswith(root)) path, attr = get_xpath_tuple(path) # 'path/@attr' to ('path', 'attr') if attr: removed = [get_element(elem, path)] remove_element_attributes(removed[0], attr) elif not has_root: removed = wrap_value(remove_element(elem, path)) else: path = get_xpath_branch(root, path) removed = [] if idx != 0 else [remove_element(e, path, True) for e in get_elements(elem, root)] if not vals: return removed items = [] for i, val in enumerate(wrap_value(vals)): elem_to_update = elem if has_root: elem_to_update = insert_element(elem, (i + idx), root) val = val.decode('utf-8') if not isinstance(val, string_types) else val if not attr: items.append(insert_element(elem_to_update, i, path, val)) else: items.append(insert_element(elem_to_update, i, path, **{attr: val})) return items
def update_complex_list(tree_to_update, xpath_root, xpath_map, prop, values): """ Updates and returns the list of updated complex Elements parsed from tree_to_update. :param tree_to_update: the XML tree compatible with element_utils to be updated :param xpath_root: the XPATH location of each complex Element :param xpath_map: a Dictionary of XPATHs corresponding to the complex structure definition :param prop: the property identifying the complex structure to be serialized :param values: a List containing the updated complex structures as Dictionaries """ complex_list = [] remove_element(tree_to_update, xpath_root, True) if not values: # Returns the elements corresponding to property removed from the tree complex_list.append( update_property(tree_to_update, xpath_root, xpath_root, prop, values)) else: for idx, complex_struct in enumerate(wrap_value(values)): # Insert a new complex element root for each dict in the list complex_element = insert_element(tree_to_update, idx, xpath_root) for subprop, value in iteritems(complex_struct): xpath = get_xpath_branch(xpath_root, xpath_map[subprop]) value = get_default_for_complex_sub(prop, subprop, value, xpath) complex_list.append( update_property(complex_element, None, xpath, subprop, value)) return complex_list
def _update_attribute_details(self, **update_props): """ Update operation for ISO Attribute Details metadata: write to "MD_Metadata/featureType" """ tree_to_update = update_props['tree_to_update'] xpath = self._data_map['_attr_citation'] # Cannot write to remote file: remove the featureCatalogueCitation element self._attr_details_file_url = None remove_element(tree_to_update, xpath, True) return self._update_complex_list(**update_props)
def update_element(elem, idx, root, path, vals): """ Internal helper function to encapsulate single item update """ has_root = bool(root and len(path) > len(root) and path.startswith(root)) path, attr = get_xpath_tuple(path) # 'path/@attr' to ('path', 'attr') if attr: removed = [get_element(elem, path)] remove_element_attributes(removed[0], attr) elif not has_root: removed = wrap_value(remove_element(elem, path)) else: path = get_xpath_branch(root, path) removed = [] if idx != 0 else [ remove_element(e, path, True) for e in get_elements(elem, root) ] if not vals: return removed items = [] for i, val in enumerate(wrap_value(vals)): elem_to_update = elem if has_root: elem_to_update = insert_element(elem, (i + idx), root) val = val.decode('utf-8') if not isinstance(val, string_types) else val if not attr: items.append(insert_element(elem_to_update, i, path, val)) elif path: items.append( insert_element(elem_to_update, i, path, **{attr: val})) else: set_element_attributes(elem_to_update, **{attr: val}) items.append(elem_to_update) return items
def _update_dates(self, **update_props): """ Update operation for ISO Dates metadata :see: gis_metadata.utils._complex_definitions[DATES] """ tree_to_update = update_props['tree_to_update'] xpath_root = self._data_map['_dates_root'] if self.dates: date_type = self.dates[DATE_TYPE] # First remove all date info from common root remove_element(tree_to_update, xpath_root) if date_type == DATE_TYPE_MULTIPLE: xpath_root += '/TimeInstant' elif date_type == DATE_TYPE_RANGE: xpath_root += '/TimePeriod' return super(IsoParser, self)._update_dates(xpath_root, **update_props)
def test_fgdc_parser(self): """ Tests behavior unique to the FGDC parser """ # Test dates structure defaults # Remove multiple dates to ensure range is queried fgdc_element = get_remote_element(self.fgdc_file) remove_element(fgdc_element, "idinfo/timeperd/timeinfo/mdattim", True) # Assert that the backup dates are read in successfully fgdc_parser = FgdcParser(element_to_string(fgdc_element)) self.assertEqual(fgdc_parser.dates, {"type": "range", "values": ["Date Range Start", "Date Range End"]}) # Test contact data structure defaults contacts_def = get_complex_definitions()[CONTACTS] # Remove the contact organization completely fgdc_element = get_remote_element(self.fgdc_file) for contact_element in get_elements(fgdc_element, "idinfo/ptcontac"): if element_exists(contact_element, "cntinfo/cntorgp"): clear_element(contact_element) # Assert that the contact organization has been read in fgdc_parser = FgdcParser(element_to_string(fgdc_element)) for key in contacts_def: for contact in fgdc_parser.contacts: self.assertIsNotNone(contact[key], "Failed to read contact.{0}".format(key)) # Remove the contact person completely fgdc_element = get_remote_element(self.fgdc_file) for contact_element in get_elements(fgdc_element, "idinfo/ptcontac"): if element_exists(contact_element, "cntinfo/cntperp"): clear_element(contact_element) # Assert that the contact organization has been read in fgdc_parser = FgdcParser(element_to_string(fgdc_element)) for key in contacts_def: for contact in fgdc_parser.contacts: self.assertIsNotNone(contact[key], "Failed to read updated contact.{0}".format(key))
def test_arcgis_parser(self): """ Tests behavior unique to the FGDC parser """ # Test dates structure defaults # Remove multiple dates to ensure range is queried arcgis_element = get_remote_element(self.arcgis_file) remove_element(arcgis_element, "dataIdInfo/dataExt/tempEle/TempExtent/exTemp/TM_Instant", True) # Assert that the backup dates are read in successfully arcgis_parser = ArcGISParser(element_to_string(arcgis_element)) self.assertEqual(arcgis_parser.dates, {"type": "range", "values": ["Date Range Start", "Date Range End"]}) # Remove one of the date range values and assert that only the end date is read in as a single remove_element(arcgis_element, "dataIdInfo/dataExt/tempEle/TempExtent/exTemp/TM_Period/tmBegin", True) arcgis_parser = ArcGISParser(element_to_string(arcgis_element)) self.assertEqual(arcgis_parser.dates, {"type": "single", "values": ["Date Range End"]}) # Remove the last of the date range values and assert that no dates are read in remove_element(arcgis_element, "dataIdInfo/dataExt/tempEle/TempExtent/exTemp/TM_Period", True) arcgis_parser = ArcGISParser(element_to_string(arcgis_element)) self.assertEqual(arcgis_parser.dates, {}) # Insert a single date value and assert that only it is read in single_path = "dataIdInfo/dataExt/tempEle/TempExtent/exTemp/TM_Instant/tmPosition" single_text = "Single Date" insert_element(arcgis_element, 0, single_path, single_text) arcgis_parser = ArcGISParser(element_to_string(arcgis_element)) self.assertEqual(arcgis_parser.dates, {"type": "single", "values": [single_text]})
def test_parser_values(self): """ Tests that parsers are populated with the expected values """ arcgis_element = get_remote_element(self.arcgis_file) arcgis_parser = ArcGISParser(element_to_string(arcgis_element)) arcgis_new = ArcGISParser(**TEST_METADATA_VALUES) # Test that the two ArcGIS parsers have the same data given the same input file self.assert_parsers_are_equal(arcgis_parser, arcgis_new) fgdc_element = get_remote_element(self.fgdc_file) fgdc_parser = FgdcParser(element_to_string(fgdc_element)) fgdc_new = FgdcParser(**TEST_METADATA_VALUES) # Test that the two FGDC parsers have the same data given the same input file self.assert_parsers_are_equal(fgdc_parser, fgdc_new) iso_element = get_remote_element(self.iso_file) remove_element(iso_element, _iso_tag_formats["_attr_citation"], True) iso_parser = IsoParser(element_to_string(iso_element)) iso_new = IsoParser(**TEST_METADATA_VALUES) # Test that the two ISO parsers have the same data given the same input file self.assert_parsers_are_equal(iso_parser, iso_new) # Test that all distinct parsers have the same data given equivalent input files self.assert_parsers_are_equal(arcgis_parser, fgdc_parser) self.assert_parsers_are_equal(fgdc_parser, iso_parser) self.assert_parsers_are_equal(iso_parser, arcgis_parser) # Test that each parser's values correspond to the target values for parser in (arcgis_parser, fgdc_parser, iso_parser): parser_type = type(parser) for prop, target in TEST_METADATA_VALUES.items(): self.assert_equal_for(parser_type, prop, getattr(parser, prop), target)