Coverage for C: \ Users \ peaco \ OneDrive \ Documents \ GitHub \ mt_metadata \ mt_metadata \ common \ comment.py: 95%

108 statements  

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

1# ===================================================== 

2# Imports 

3# ===================================================== 

4from typing import Annotated 

5from xml.etree import ElementTree as et 

6 

7import numpy as np 

8import pandas as pd 

9from loguru import logger 

10from pydantic import ( 

11 Field, 

12 field_validator, 

13 model_validator, 

14 ValidationError, 

15 ValidationInfo, 

16) 

17from typing_extensions import Self 

18 

19from mt_metadata.base import MetadataBase 

20from mt_metadata.base.helpers import element_to_string 

21from mt_metadata.common.mttime import MTime 

22 

23 

24# ===================================================== 

25class Comment(MetadataBase): 

26 author: Annotated[ 

27 str | None, 

28 Field( 

29 default=None, 

30 description="person who authored the comment", 

31 alias=None, 

32 json_schema_extra={ 

33 "units": None, 

34 "required": False, 

35 "examples": ["J. Pedantic"], 

36 }, 

37 ), 

38 ] 

39 

40 time_stamp: Annotated[ 

41 float | int | np.datetime64 | pd.Timestamp | str | MTime | None, 

42 Field( 

43 default_factory=lambda: MTime(time_stamp="1980-01-01T00:00:00+00:00"), 

44 description="Date and time of in UTC of when comment was made.", 

45 alias=None, 

46 json_schema_extra={ 

47 "units": None, 

48 "required": False, 

49 "examples": ["2020-02-01T09:23:45.453670+00:00"], 

50 }, 

51 ), 

52 ] 

53 

54 value: Annotated[ 

55 str | list | None, 

56 Field( 

57 default=None, 

58 description="comment string", 

59 alias=None, 

60 json_schema_extra={ 

61 "units": None, 

62 "required": False, 

63 "examples": ["failure at midnight."], 

64 }, 

65 ), 

66 ] = None 

67 

68 @field_validator("time_stamp", mode="before") 

69 @classmethod 

70 def validate_time(cls, value, info: ValidationInfo) -> MTime: 

71 """ 

72 Validate that the value is a valid time. 

73 """ 

74 return MTime(time_stamp=value) 

75 

76 @field_validator("value", mode="before") 

77 @classmethod 

78 def validate_value(cls, value, info: ValidationInfo) -> str | list | None: 

79 """ 

80 Validate that the value is a valid string or list. 

81 """ 

82 if isinstance(value, str): 

83 return value 

84 elif isinstance(value, list): 

85 return ",".join([v.strip() for v in value if isinstance(v, str)]) 

86 elif value is None: 

87 return None 

88 else: 

89 raise TypeError(f"Invalid type for value: {type(value)}") 

90 

91 @model_validator(mode="after") 

92 def set_variables(self) -> Self: 

93 """ 

94 Validate that the value is a valid string. 

95 """ 

96 if self.value is not None: 

97 if "|" in self.value: 

98 parts = [ss.strip() for ss in self.value.split("|")] 

99 self.value = parts[-1] 

100 if len(parts) == 3: 

101 self.time_stamp = parts[0] 

102 self.author = parts[1] 

103 elif len(parts) == 2: 

104 try: 

105 self.time_stamp = parts[0] 

106 except ValidationError: 

107 self.author = parts[0] 

108 return self 

109 

110 # need to override __eq__ to compare the values of the object 

111 # otherwise the __eq__ from MetadataBase will be used which 

112 # assumes an object. 

113 def __eq__(self, other: object) -> bool: 

114 """ 

115 Check if two Comment objects are equal. 

116 

117 Parameters 

118 ---------- 

119 other : object 

120 The object to compare with. 

121 

122 Returns 

123 ------- 

124 bool 

125 True if the objects are equal, False otherwise. 

126 """ 

127 if other is None: 

128 return Comment(value=None) # type: ignore 

129 if isinstance(other, str): 

130 other = Comment(value=other) # type: ignore 

131 elif isinstance(other, dict): 

132 other = Comment(**other) 

133 return ( 

134 self.author == other.author 

135 and self.time_stamp == other.time_stamp 

136 and self.value == other.value 

137 ) 

138 

139 def as_string(self) -> str: 

140 """ 

141 Returns the comment as "{time_stamp} | {author} | {comment}" 

142 

143 Returns 

144 ------- 

145 str 

146 formatted comment 

147 """ 

148 if self.value is None: 

149 return "" 

150 

151 if self.time_stamp == "1980-01-01T00:00:00+00:00": 

152 if self.author in [None, ""]: 

153 return self.value 

154 return f" {self.author} | {self.value}" 

155 if self.author in [None, ""]: 

156 return f"{self.time_stamp} | {self.value}" 

157 return f"{self.time_stamp} | {self.author} | {self.value}" 

158 

159 def from_dict( 

160 self, 

161 value: str | dict, 

162 skip_none=False, 

163 ) -> None: 

164 """ 

165 Parse input comment assuming "{time_stamp} | {author} | {comment}" 

166 

167 Parameters 

168 ---------- 

169 value : str 

170 _description_ 

171 skip_none : bool, optional 

172 _description_, by default False 

173 """ 

174 if isinstance(value, str): 

175 self.value = value 

176 elif isinstance(value, dict): 

177 if len(value.keys()) > 1: 

178 self.time_stamp = value.get("time_stamp", None) 

179 self.author = value.get("author", None) 

180 self.value = value.get("value", None) 

181 

182 elif len(value.keys()) == 1: 

183 key = list(value.keys())[0] 

184 value = value[key] 

185 if isinstance(value, dict): 

186 self.time_stamp = value.get("time_stamp", None) 

187 self.author = value.get("author", None) 

188 self.value = value.get("value", None) 

189 elif isinstance(value, str): 

190 self.value = value 

191 

192 # this only happens on instance creation, so we can ignore it 

193 else: 

194 pass 

195 

196 else: 

197 raise TypeError(f"Cannot parse type {type(value)}") 

198 

199 def read_dict(self, input_dict: dict) -> None: 

200 """ 

201 

202 can probably use from_dict method instead, but to keep consistency in EMTF XML 

203 metadata, this method is used to read the comment from a dictionary. 

204 

205 :param input_dict: input dictionary containing comment data 

206 :type input_dict: dict 

207 :return: None 

208 :rtype: None 

209 

210 """ 

211 key = input_dict["comments"] 

212 if isinstance(key, str): 

213 self.value = key 

214 elif isinstance(key, dict): 

215 try: 

216 self.value = key["value"] 

217 except KeyError: 

218 logger.debug("No value in comment") 

219 

220 try: 

221 self.author = key["author"] 

222 except KeyError: 

223 logger.debug("No author of comment") 

224 try: 

225 self.time_stamp = key["date"] 

226 except KeyError: 

227 logger.debug("No date for comment") 

228 else: 

229 raise TypeError(f"Comment cannot parse type {type(key)}") 

230 

231 def to_xml(self, string: bool = False, required: bool = True) -> str | et.Element: 

232 """ 

233 Convert the Comment instance to XML format. 

234 

235 :param string: If True, return the XML as a string. If False, return an ElementTree Element. 

236 :type string: bool, optional 

237 :param required: If True, include all required fields. 

238 :type required: bool, optional 

239 :return: XML representation of the Comment. 

240 :rtype: str | et.Element 

241 """ 

242 

243 if self.author is None: 

244 self.author = "" 

245 root = et.Element(self.__class__.__name__ + "s", {"author": self.author}) 

246 if self.value is None: 

247 self.value = "" 

248 root.text = self.value 

249 

250 if string: 

251 return element_to_string(root) 

252 return root