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
« 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
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
19from mt_metadata.base import MetadataBase
20from mt_metadata.base.helpers import element_to_string
21from mt_metadata.common.mttime import MTime
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 ]
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 ]
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
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)
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)}")
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
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.
117 Parameters
118 ----------
119 other : object
120 The object to compare with.
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 )
139 def as_string(self) -> str:
140 """
141 Returns the comment as "{time_stamp} | {author} | {comment}"
143 Returns
144 -------
145 str
146 formatted comment
147 """
148 if self.value is None:
149 return ""
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}"
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}"
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)
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
192 # this only happens on instance creation, so we can ignore it
193 else:
194 pass
196 else:
197 raise TypeError(f"Cannot parse type {type(value)}")
199 def read_dict(self, input_dict: dict) -> None:
200 """
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.
205 :param input_dict: input dictionary containing comment data
206 :type input_dict: dict
207 :return: None
208 :rtype: None
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")
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)}")
231 def to_xml(self, string: bool = False, required: bool = True) -> str | et.Element:
232 """
233 Convert the Comment instance to XML format.
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 """
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
250 if string:
251 return element_to_string(root)
252 return root