Esempio n. 1
0
    def map(self, xml_etree: ElementTree,
            json: Union[Dict, List, None],
            xml_namespaces: Dict = None,
            ignore_empty: bool = True) -> Dict:
        """
        Maps the XMLNode from xml_etree into the given json serializable dictionary.
        :param xml_etree: An XML ElementTree from which the input XMLNode is to be taken. If the
                          XMLNode is not found in the xml_etree it will be defaulted to None.
        :param json: The JSON serializable dictionary onto which the input is to be mapped.
        :param xml_namespaces: A dictionary defining the XML namespaces used in the xml_etree, if
                               they are used they must be provided to find the XMLNode via its XPath
                               expression.
        :param ignore_empty: A boolean indicating if a missing XML Node is to be mapped into the target JSON.
        :return: The JSON serializable dictionary with the input XMLNode mapped.
        """
        if self.from_xml_node.node_type == XMLNodeType.ATTRIBUTE:
            input = self._get_attribute(xml_etree, xml_namespaces)
        elif self.from_xml_node.node_type == XMLNodeType.VALUE:
            input = self._get_element_value(xml_etree, xml_namespaces)
        elif self.from_xml_node.node_type == XMLNodeType.SEQUENCE:
            if not self.item_mappings:
                raise ValueError(
                    "An item_mapping must be provided for an XML node of type 'sequence'.")
            input_sequence = xml_etree.findall(str(self.from_xml_node.path), xml_namespaces)
            if not input_sequence and ignore_empty:
                return json

            if self.to_json_node.node_type == JSONNodeType.ARRAY:
                items = []
                for element in input_sequence:
                    item = None
                    for mapping in self.item_mappings:
                        item = mapping.map(element, item, xml_namespaces, ignore_empty=ignore_empty)
                    items.append(item)
                return write_item_in_path(items, JSONPath(self.to_json_node.path), json)
            else:
                raise ValueError("Cannot map an XML sequence into {}".format(self.to_json_node.node_type))
        elif self.from_xml_node.node_type == XMLNodeType.ELEMENT:
            xml_element = xml_etree.find(str(self.from_xml_node.path), xml_namespaces)
            item = {}
            for mapping in self.item_mappings:
                item = mapping.map(xml_element, item, xml_namespaces, ignore_empty=ignore_empty)
            return write_item_in_path(item, JSONPath(self.to_json_node.path), json)
        else:
            raise ValueError(f"Not supported node type {self.from_xml_node.node_type.name}")
        if not input and ignore_empty:
            return json
        else:
            return self._map_input(input, json, ignore_empty)
Esempio n. 2
0
    def test_sequence_path(self):
        with self.subTest():
            xpath = XPath("/adrmsg:ADRMessage/adrmsg:hasMember[1]/aixm:Route/@gml:id")
            reference = JSONPath("$.adrmsg:ADRMessage.adrmsg:hasMember[0].aixm:Route._gml:id")
            self.assertEqual(reference, xpath.to_json_path(attributes="_", with_namespaces=True))

        with self.subTest():
            xpath = XPath("/adrmsg:ADRMessage/adrmsg:hasMember[1]/aixm:Route/@gml:id")
            reference = JSONPath("$.ADRMessage.hasMember[0].Route.id")
            self.assertEqual(reference, xpath.to_json_path(attributes="", with_namespaces=False))

        with self.subTest():
            xpath = XPath("/adrmsg:ADRMessage/adrmsg:hasMember[-1]/aixm:Route/@gml:id")
            reference = JSONPath("$.ADRMessage.hasMember[-1].Route.id")
            self.assertRaises(ValueError)
Esempio n. 3
0
    def test_change_attribute_tag(self):
        with self.subTest():
            xpath = XPath('/adrmsg:ADRMessage/adrmsg:hasMember/aixm:Route/@gml:id')
            reference = JSONPath('$.ADRMessage.hasMember.Route._id')
            self.assertEqual(reference, xpath.to_json_path(attributes='_', with_namespaces=False))

        with self.subTest():
            xpath = XPath('/adrmsg:ADRMessage/adrmsg:hasMember/aixm:Route/@gml:id')
            reference = JSONPath('$.ADRMessage.hasMember.Route.id')
            self.assertEqual(reference, xpath.to_json_path(attributes='', with_namespaces=False))

        with self.subTest():
            xpath = XPath('/adrmsg:ADRMessage/adrmsg:hasMember/aixm:Route/@gml:id')
            reference = JSONPath('$.adrmsg:ADRMessage.adrmsg:hasMember.aixm:Route.attrib_gml:id')
            self.assertEqual(reference, xpath.to_json_path(attributes='attrib_', with_namespaces=True))
Esempio n. 4
0
    def to_json_path(self,
                     attributes: str = '',
                     with_namespaces: bool = True) -> JSONPath:
        """
        Infers a JSONPath representation for the XPath.

        :param attributes: Defines the tag that will precede an XML attribute name in JSONPath.
                           It defaults to an empty string, resulting in no difference in the
                           representation of XML elements and attributes into JSON. Can be used to
                           define your own convention.

                           E.g. the following XPath '/element/subelement/@attribute will be
                           transformed as follows:
                               '': $.element.subelement.attribute
                               '_': $.element.subelement._attribute
                               '@': $.element.subelement.@attribute

        :param with_namespaces: Defines how XML namespaces will be handled. It defaults to True,
                                resulting in shortened namespaces being kept in the JSONPath. A
                                False will drop them in the conversion to JSONPath and it should be
                                used with caution as it may result in name collisions.

                                E.g. the following XPath '/ns:element/nss:subelement will be
                                transformed as follows:

                                    'True': $.ns:element.nss:subelement
                                    'False': $.element.subelement
        :return: A JSONPath representation of the XPath.
        """
        json_path = self.raw_xpath if with_namespaces else re.sub(
            r'[a-zA-Z]+:', '', self.raw_xpath)

        json_path = re.sub(r'/@', '/' + attributes, json_path)
        json_path = re.sub(r'^\./', '@/', json_path)
        json_path = re.sub(r'^/', '$/', json_path)
        json_path = re.sub(r'/', '.', json_path)
        json_path = JSONPath(json_path)
        json_path_structure = []
        for path_key in json_path.json_path_structure:
            if isinstance(path_key, int):
                if path_key <= 0:
                    raise ValueError(
                        f"An XPath expression cannot contain an index <= 0, xpath= {self}"
                    )
                path_key += -1
            json_path_structure.append(path_key)
        return JSONPath.from_json_path_structure(json_path_structure)
Esempio n. 5
0
    def map(self, input_json: Union[Dict, List],
            output_json: Union[Dict, List, None],
            ignore_empty: bool = True) -> Union[Dict, List]:
        input = self._get_attribute(input_json)
        if self.transform:
            input = self.transform(input)

        if not input:
            if ignore_empty:
                return output_json

        return write_item_in_path(input, in_path=JSONPath(self.to_json_node.path), json=output_json)
Esempio n. 6
0
def infer_path_type(path: str) -> Union[XPath, JSONPath]:
    """
    Infers the type of a path (XPath or JSONPath) based on its syntax.
    It performs some basic sanity checks to differentiate a JSONPath from an XPath.
    :param path: A valid XPath or JSONPath string.
    :return: An instance of JSONPath or XPath
    """
    if not path:
        raise ValueError("No path given")
    if path[0] in ['$', '@']:
        return JSONPath(path)
    else:
        if path[0] in ['.', '/']:
            return XPath(path)
        else:
            raise ValueError("Couldn't identify the path type for {}".format(path))
Esempio n. 7
0
 def test_ignore_namespaces(self):
     xpath = XPath('/adrmsg:ADRMessage/adrmsg:hasMember/aixm:Route/@gml:id')
     reference = JSONPath('$.ADRMessage.hasMember.Route.@id')
     self.assertEqual(reference, xpath.to_json_path(attributes='@', with_namespaces=False))
Esempio n. 8
0
 def test_relative_path(self):
     xpath = XPath('./adrmsg:ADRMessage/adrmsg:hasMember/aixm:Route/@gml:id')
     reference = JSONPath('@.adrmsg:ADRMessage.adrmsg:hasMember.aixm:Route.@gml:id')
     self.assertEqual(reference, xpath.to_json_path(attributes='@', with_namespaces=True))
Esempio n. 9
0
 def _get_attribute(self, json: Union[Dict, List]) -> Optional[Any]:
     try:
         return get_item_from_json_path(JSONPath(self.from_json_node.path), json)
     except (KeyError, TypeError, IndexError) as e:
         logger.debug("Attribute at path {} doesn't exist".format(self.from_json_node.path))
         return None
Esempio n. 10
0
    def _map_input(self, input: Optional[str], json: Union[Dict, List, None], ignore_empty: bool = True) -> Union[Dict, List]:
        logger.debug("Mapping {} into {}".format(self.from_xml_node.path, self.to_json_node.path))
        if ignore_empty:
            logger.debug("ignore_empty=True")
            if isinstance(input, str):
                input = re.sub(r"^\s*$", "", input)
            if input is None or input == '':
                logger.debug("input is empty. Ignoring...")
                return json

        if self.to_json_node.node_type == JSONNodeType.STRING:
            logger.debug("Mapping input to string")
            return write_item_in_path(input, JSONPath(self.to_json_node.path), json)

        if self.to_json_node.node_type == JSONNodeType.INTEGER:
            logger.debug("Mapping input to integer")
            try:
                casted_value = int(input)
                return write_item_in_path(casted_value, JSONPath(self.to_json_node.path), json)
            except ValueError as e:
                raise ValueError(
                    f'The node at {self.from_xml_node.path} is not castable into int', e.args[0])

        if self.to_json_node.node_type == JSONNodeType.NUMBER:
            logger.debug("Mapping input to number")
            try:
                casted_value = float(input)
                return write_item_in_path(casted_value, JSONPath(self.to_json_node.path), json)
            except ValueError as e:
                raise ValueError(
                    f'The node at {self.from_xml_node.path} is not castable into float', e.args[0])

        if self.to_json_node.node_type == JSONNodeType.BOOLEAN:
            logger.debug("Mapping input to boolean")
            if input == 'true':
                casted_value = True
            elif input == 'false':
                casted_value = False
            else:
                raise ValueError(f'The node at {self.from_xml_node.path} with value {input} '
                                 f'is not castable into a boolean. '
                                 f'Only "true" and "false" are valid XML boolean values.')

            return write_item_in_path(casted_value, JSONPath(self.to_json_node.path), json)

        if self.to_json_node.node_type == JSONNodeType.NULL:
            logger.debug("Mapping input to null")
            return write_item_in_path(None, JSONPath(self.to_json_node.path), json)

        if self.to_json_node.node_type == JSONNodeType.INFER:
            try:
                inferred_json_type = infer_json_type(input)
            except ValueError:
                raise ValueError('Unable to infer JSON type for the value at {}'.format(self.from_xml_node.path))

            if inferred_json_type == JSONNodeType.BOOLEAN:
                if input == 'true':
                    casted_value = True
                elif input == 'false':
                    casted_value = False
                else:
                    casted_value = bool(input)
                return write_item_in_path(casted_value, JSONPath(self.to_json_node.path), json)

            elif inferred_json_type == JSONNodeType.NUMBER:
                casted_value = float(input)
                return write_item_in_path(casted_value, JSONPath(self.to_json_node.path), json)

            elif inferred_json_type == JSONNodeType.INTEGER:
                casted_value = int(input)
                return write_item_in_path(casted_value, JSONPath(self.to_json_node.path), json)

            elif inferred_json_type == JSONNodeType.STRING:
                return write_item_in_path(input, JSONPath(self.to_json_node.path), json)

            elif inferred_json_type == JSONNodeType.NULL:
                return write_item_in_path(None, JSONPath(self.to_json_node.path), json)

            elif inferred_json_type == JSONNodeType.ARRAY:
                return write_item_in_path(input, JSONPath(self.to_json_node.path), json)

            elif inferred_json_type == JSONNodeType.OBJECT:
                return write_item_in_path(input, JSONPath(self.to_json_node.path), json)
            else:
                raise ValueError("Input map for JSONNodeType of type {} not implemented".format(inferred_json_type))