102 lines
3.9 KiB
Python
102 lines
3.9 KiB
Python
from lxml import etree
|
|
|
|
|
|
def dict_to_xml(node, *, nsmap={}, template=None, render_empty_nodes=False, tag=None, path=None):
|
|
""" Helper to render a Python dict as an XML node.
|
|
|
|
The dict is expected to be of the form:
|
|
{
|
|
# Special keys:
|
|
'_tag': 'tag_name', # '_tag' is rendered as the node's tag
|
|
'_text': 'content', # '_text' is rendered as the node's text content
|
|
'_dummy': 'dummy_value', # Keys starting with '_' are not rendered
|
|
|
|
# Simple values are rendered as attributes
|
|
'attribute_name': 'attribute_value',
|
|
|
|
# Dicts are rendered as child nodes
|
|
'child_tag': {
|
|
'_text': 'content',
|
|
'attribute_name': 'attribute_value',
|
|
},
|
|
|
|
# Lists of dicts are also rendered as child nodes
|
|
'child_tag': [
|
|
{
|
|
'_text': 'content',
|
|
'attribute_name': 'attribute_value',
|
|
},
|
|
],
|
|
}
|
|
|
|
:param node: The Python dict to render.
|
|
:param nsmap: (optional) A dict of namespaces to be used for rendering the node.
|
|
:param template: (optional) A Python dict providing default values and an order of keys for rendering the node.
|
|
:param render_empty_nodes: (optional) If True, empty nodes will be rendered in the XML tree.
|
|
:param tag: (optional) The tag of the node to render (needed only for recursive calls).
|
|
:param path: (optional) The path of the currently rendered node in the XML tree (needed only for recursive calls).
|
|
:return: The rendered XML node as an lxml.Element.
|
|
"""
|
|
def convert_tag_to_lxml_convention(tag):
|
|
if ':' in tag:
|
|
namespace, local_name = tag.split(':')
|
|
if namespace in nsmap:
|
|
return etree.QName(nsmap[namespace], local_name).text
|
|
return tag
|
|
|
|
if template is not None:
|
|
# Ensure order of keys
|
|
node = dict.fromkeys(template) | node
|
|
|
|
tag = node.get('_tag') or (template or {}).get('_tag', tag)
|
|
|
|
if tag is None:
|
|
raise ValueError(f"No tag was specified for node: {str(node)[:20]}")
|
|
|
|
if path is None:
|
|
path = tag
|
|
|
|
element = etree.Element(convert_tag_to_lxml_convention(tag), nsmap=nsmap)
|
|
|
|
# Add attributes
|
|
for attr_name, attr_value in node.items():
|
|
if not attr_name.startswith('_') and not isinstance(attr_value, (dict, list)) and attr_value is not None and attr_value is not False:
|
|
element.set(convert_tag_to_lxml_convention(attr_name), str(attr_value))
|
|
|
|
# Add text content if present
|
|
text = node.get('_text')
|
|
if text is not None and text is not False:
|
|
element.text = str(text)
|
|
|
|
# Add child nodes
|
|
for child_tag, child in node.items():
|
|
if not child_tag.startswith('_') and isinstance(child, (dict, list)):
|
|
child_template = (template or {}).get(child_tag)
|
|
child_is_empty = True
|
|
if isinstance(child, dict):
|
|
child = [child]
|
|
|
|
# child is a list (of dicts)
|
|
for sub_child in child:
|
|
if sub_child is not None:
|
|
child_element = dict_to_xml(
|
|
sub_child,
|
|
nsmap=nsmap,
|
|
template=child_template,
|
|
render_empty_nodes=render_empty_nodes,
|
|
tag=child_tag,
|
|
path=f'{path}/{child_tag}',
|
|
)
|
|
if child_element is not None:
|
|
element.append(child_element)
|
|
child_is_empty = False
|
|
|
|
# Check that all non-empty child nodes are defined in the template
|
|
if template is not None and child_tag not in template and not child_is_empty:
|
|
raise ValueError(f"The following child node is not defined in the template: {path}/{child_tag}")
|
|
|
|
if not render_empty_nodes and not element.attrib and not element.text and len(element) == 0:
|
|
return None
|
|
|
|
return element
|