Last active
February 4, 2023 21:48
-
-
Save dmfigol/26dcac2b13322957ba47a79ced07a135 to your computer and use it in GitHub Desktop.
Convert Python dictionary to NETCONF XML payload
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from typing import Any, Union, Optional | |
from lxml import etree | |
__license__ = "MIT" | |
# Feel free to re-use the code snippet for any projects without attribution | |
def dict_to_xml( | |
data: Any, root: Union[None, str, etree._Element] = None, attr_marker: str = "_" | |
) -> etree.Element: | |
"""Converts Python dictionary with YANG data to lxml etree.Element object. | |
XML attributes must be represented in nested dictionary, which is accessed by the | |
element name. Attribute keys must be prepended with underscore. | |
Common use-cases: | |
* operation attribute. For example: | |
{"vrf": {"_operation": "replace"}} -> <vrf operation="replace"></vrf> | |
* changing default namespace. For example: | |
{"native": {"hostname": "R1", "_xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XE-native"}} -> | |
<native xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-native"><hostname>R1</hostname></native> | |
Empty XML tags (including self-closing tags) are represented with value `None`: | |
{"address-family": {"ipv4": None}} -> <address-family><ipv4/></address-family> | |
When an element has attributes, it can't be simply represented with key-value pair, | |
instead use #text key, for example: | |
{"bundle-name": { | |
"_xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XE-ppp", | |
"#text": "authenticated" | |
}} -> | |
<bundle-name xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-ppp">authenticated</bundle-name> | |
Namespaces with prefix: | |
1. They need to be defined under the top-level key "_namespaces" in the dictionary | |
in the form prefix:namespace. E.g.: | |
{"_namespaces": {"ianaift": "urn:ietf:params:xml:ns:yang:iana-if-type"}} | |
2. Use the form `element-name+prefix` to use it for a specific element. E.g.: | |
{"type+ianaift": "ianaift:ethernetCsmacd"} -> | |
<type ianaift="urn:ietf:params:xml:ns:yang:iana-if-type">ianaift:ethernetCsmacd</type> | |
""" | |
namespaces = data.pop("_namespaces", {}) | |
def _dict_to_xml(data_: Any, parent: Optional[etree._Element] = None) -> None: | |
nonlocal root | |
if not isinstance(data_, dict): | |
raise ValueError("provided data must be a dictionary") | |
for key, value in data_.items(): | |
if key.startswith(attr_marker): | |
# handle keys starting with attr_marker as tag attributes | |
attr_name = key.lstrip(attr_marker) | |
parent.attrib[attr_name] = value | |
elif key == "#text": | |
parent.text = value | |
else: | |
if "+" in key: | |
key, *_namespaces = key.split("+") | |
nsmap = {ns: namespaces[ns] for ns in _namespaces} | |
else: | |
nsmap = None | |
element = etree.Element(key, nsmap=nsmap) | |
if root is None: | |
root = element | |
if parent is not None and not isinstance(value, list): | |
parent.append(element) | |
if isinstance(value, dict): | |
_dict_to_xml(value, element) | |
elif isinstance(value, list): | |
for item in value: | |
list_key = etree.Element(key) | |
parent.append(list_key) | |
_dict_to_xml(item, list_key) | |
else: | |
if value is True or value is False: | |
value = str(value).lower() | |
elif value is not None and not isinstance(value, str): | |
value = str(value) | |
element.text = value | |
if isinstance(root, str): | |
root = etree.Element(root) | |
_dict_to_xml(data, root) | |
return root |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment