1# -*- coding: utf-8 -*-
2"""
3Created on Wed Mar 8 19:53:04 2023
4
5@author: jpeacock
6"""
7
8# =============================================================================
9# Imports
10# =============================================================================
11from collections import OrderedDict
12from xml.etree import cElementTree as et
13
14from loguru import logger
15
16from mt_metadata import NULL_VALUES
17from mt_metadata.base.helpers import element_to_string
18from mt_metadata.utils.validators import validate_attribute
19
20
21# =============================================================================
22
23
24def _get_attributes(cls) -> list[str]:
25 return [f for f in cls.__dict__.keys() if f[0] != "_" and f not in ["logger"]]
26
27
28def _capwords(value: str) -> str:
29 """
30 Convert a string to capwords format.
31
32 Could use string.capwords, but this seems
33 easy enough
34
35 Parameters
36 ----------
37 value : str
38 The input string to convert.
39
40 Returns
41 -------
42 str
43 The converted string in capwords format.
44 """
45
46 if value.count("_") > 0:
47 return value.replace("_", " ").title().replace(" ", "")
48 elif sum(1 for c in value if c.isupper()) == 0:
49 return value.title()
50
51 return value
52
53
54def _convert_tag_to_capwords(element: et.Element) -> et.Element:
55 """
56 Convert back to capwords representation for the tag.
57
58 Parameters
59 ----------
60 element : et.Element
61 The XML element to convert.
62
63 Returns
64 -------
65 et.Element
66 The converted XML element.
67 """
68
69 for item in element.iter():
70 if item.tag != "value":
71 item.tag = _capwords(item.tag)
72
73 return element
74
75
76def _read_single(cls: type, root_dict: dict, key: str) -> None:
77 """
78 Read a single value from a dictionary into a class attribute.
79
80 Parameters
81 ----------
82 cls : type
83 The class to update.
84 root_dict : dict
85 The dictionary containing the data.
86 key : str
87 The key to read from the dictionary.
88 """
89
90 try:
91 setattr(cls, key, root_dict[key])
92 except KeyError:
93 logger.debug("no description in xml")
94
95
96def _write_single(
97 parent: et.Element, key: str, value: str, attributes: dict = {}
98) -> et.Element:
99 """
100 Write a single value to an XML element.
101
102 Parameters
103 ----------
104 parent : et.Element
105 The parent XML element to append the new element to.
106 key : str
107 The key for the new XML element.
108 value : str
109 The value for the new XML element.
110 attributes : dict, optional
111 Additional attributes for the new XML element, by default {}
112
113 Returns
114 -------
115 et.Element
116 The newly created XML element.
117 """
118
119 element = et.SubElement(parent, _capwords(key), attributes)
120 if value not in NULL_VALUES:
121 element.text = str(value)
122 return element
123
124
125def _read_element(cls: type, root_dict: dict, element_name: str) -> None:
126 """
127 Read an XML element into a class instance.
128
129 Parameters
130 ----------
131 cls : type
132 The class to update.
133 root_dict : dict
134 The dictionary containing the data.
135 element_name : str
136 The name of the XML element to read.
137 """
138
139 try:
140 element_dict = {element_name: root_dict[element_name]}
141 cls.from_dict(element_dict)
142
143 except KeyError:
144 logger.warning(f"No {element_name} in EMTF XML")
145
146
147def _convert_keys_to_lower_case(root_dict: dict) -> OrderedDict:
148 """
149 Convert all keys in the dictionary to lower case.
150
151 Parameters
152 ----------
153 root_dict : dict
154 The dictionary to convert.
155
156 Returns
157 -------
158 OrderedDict
159 The converted dictionary with lower case keys.
160 """
161
162 res = OrderedDict()
163 if isinstance(root_dict, (dict, OrderedDict)):
164 for key in root_dict.keys():
165 new_key = validate_attribute(key)
166 res[new_key] = root_dict[key]
167 if isinstance(res[new_key], (dict, OrderedDict, list)):
168 res[new_key] = _convert_keys_to_lower_case(res[new_key])
169 elif isinstance(root_dict, list):
170 res = []
171 for item in root_dict:
172 item = _convert_keys_to_lower_case(item)
173 res.append(item)
174 return res
175
176
177def _remove_null_values(element: et.Element, replace: str = "") -> et.Element:
178 """
179 Remove null values from an XML element.
180
181 Parameters
182 ----------
183 element : et.Element
184 The XML element to process.
185 replace : str, optional
186 The value to replace null values with, by default "".
187
188 Returns
189 -------
190 et.Element
191 The processed XML element.
192 """
193
194 for item in element.iter():
195 if item.text in NULL_VALUES:
196 if replace == False:
197 element.remove(item)
198 else:
199 item.text = replace
200 for key, value in item.attrib.items():
201 if value in NULL_VALUES:
202 if replace == False:
203 element.remove(item)
204 else:
205 item.attrib[key] = replace
206
207 return element
208
209
210def to_xml(cls, string=False, required=True, order=None) -> str | et.Element:
211 """
212 Convert a class instance to an XML element.
213
214 Parameters
215 ----------
216 string : bool, optional
217 Whether to return the XML as a string, by default False
218 required : bool, optional
219 Whether the XML element is required, by default True
220 order : list, optional
221 The order of attributes to include, by default None
222
223 Returns
224 -------
225 str | et.Element
226 The XML representation of the class instance.
227 """
228
229 root = et.Element(cls.__class__.__name__)
230
231 if order is None:
232 order = _get_attributes(cls)
233 for attr in order:
234 c_attr = getattr(cls, attr)
235 if c_attr is None:
236 continue
237 if hasattr(c_attr, "to_xml") and callable(getattr(c_attr, "to_xml")):
238 element = c_attr.to_xml(required=required)
239 if isinstance(element, list):
240 for item in element:
241 root.append(item)
242 else:
243 root.append(element)
244 elif isinstance(c_attr, list):
245 if len(c_attr) == 0:
246 continue
247 if hasattr(c_attr[0], "to_xml") and callable(getattr(c_attr[0], "to_xml")):
248 # If the first item has a to_xml method, assume all items do
249 # and call to_xml on each item
250 for item in c_attr:
251 if isinstance(item, et.Element):
252 root.append(item)
253 else:
254 root.append(item.to_xml(required=required))
255 elif isinstance(c_attr[0], str):
256 # If the first item is a string, write it directly
257 value = " ".join(c_attr)
258 _write_single(root, attr, value)
259
260 else:
261 _write_single(root, attr, c_attr)
262
263 if not string:
264 return root
265 else:
266 return element_to_string(_remove_null_values(root))