Coverage for C: \ Users \ peaco \ OneDrive \ Documents \ GitHub \ mt_metadata \ mt_metadata \ transfer_functions \ io \ emtfxml \ metadata \ helpers.py: 98%

90 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-10 00:11 -0800

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))