Coverage for python/astro_metadata_translator/observationGroup.py: 29%

76 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-03-31 09:56 +0000

1# This file is part of astro_metadata_translator. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the LICENSE file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# Use of this source code is governed by a 3-clause BSD-style 

10# license that can be found in the LICENSE file. 

11 

12from __future__ import annotations 

13 

14"""Represent a collection of translated headers""" 

15 

16__all__ = ("ObservationGroup",) 

17 

18import logging 

19from collections.abc import MutableSequence 

20from typing import ( 

21 TYPE_CHECKING, 

22 Any, 

23 Callable, 

24 Dict, 

25 Iterable, 

26 Iterator, 

27 List, 

28 MutableMapping, 

29 Optional, 

30 Set, 

31 Tuple, 

32 Type, 

33 Union, 

34) 

35 

36from .observationInfo import ObservationInfo 

37 

38if TYPE_CHECKING: 38 ↛ 39line 38 didn't jump to line 39, because the condition on line 38 was never true

39 from .translator import MetadataTranslator 

40 

41log = logging.getLogger(__name__) 

42 

43 

44class ObservationGroup(MutableSequence): 

45 """A collection of `ObservationInfo` headers. 

46 

47 Parameters 

48 ---------- 

49 members : iterable of `ObservationInfo` or `dict`-like 

50 `ObservationInfo` to seed the group membership. If `dict`-like 

51 values are used they will be passed to the `ObservationInfo` 

52 constructor. 

53 translator_class : `MetadataTranslator`-class, optional 

54 If any of the members is not an `ObservationInfo`, translator class 

55 to pass to the `ObservationInfo` constructor. If `None` the 

56 translation class will be determined automatically. 

57 pedantic : `bool`, optional 

58 If any of the members is not an `ObservationInfo`, passed to the 

59 `ObservationInfo` constructor to control whether 

60 a failed translation is fatal or not. `None` indicates that the 

61 `ObservationInfo` constructor default should be used. 

62 """ 

63 

64 def __init__( 

65 self, 

66 members: Iterable[Union[ObservationInfo, MutableMapping[str, Any]]], 

67 translator_class: Optional[Type[MetadataTranslator]] = None, 

68 pedantic: Optional[bool] = None, 

69 ) -> None: 

70 self._members = [ 

71 self._coerce_value(m, translator_class=translator_class, pedantic=pedantic) for m in members 

72 ] 

73 

74 # Cache of members in time order 

75 self._sorted: Optional[List[ObservationInfo]] = None 

76 

77 def __len__(self) -> int: 

78 return len(self._members) 

79 

80 def __delitem__(self, index: int) -> None: # type: ignore 

81 del self._members[index] 

82 self._sorted = None 

83 

84 def __getitem__(self, index: int) -> ObservationInfo: # type: ignore 

85 return self._members[index] 

86 

87 def __str__(self) -> str: 

88 results = [] 

89 for obs_info in self._members: 

90 results.append(f"({obs_info.instrument}, {obs_info.datetime_begin})") 

91 return "[" + ", ".join(results) + "]" 

92 

93 def _coerce_value( 

94 self, 

95 value: Union[ObservationInfo, MutableMapping[str, Any]], 

96 translator_class: Optional[Type[MetadataTranslator]] = None, 

97 pedantic: Optional[bool] = None, 

98 ) -> ObservationInfo: 

99 """Given a value, ensure it is an `ObservationInfo`. 

100 

101 Parameters 

102 ---------- 

103 value : `ObservationInfo` or `dict`-like 

104 Either an `ObservationInfo` or something that can be passed to 

105 an `ObservationInfo` constructor. 

106 translator_class : `MetadataTranslator`-class, optional 

107 If value is not an `ObservationInfo`, translator class to pass to 

108 the `ObservationInfo` constructor. If `None` the 

109 translation class will be determined automatically. 

110 pedantic : `bool`, optional 

111 If value is not an `ObservationInfo`, passed to the 

112 `ObservationInfo` constructor to control whether 

113 a failed translation is fatal or not. `None` indicates that the 

114 `ObservationInfo` constructor default should be used. 

115 

116 Raises 

117 ------ 

118 ValueError 

119 Raised if supplied value is not an `ObservationInfo` and can 

120 not be turned into one. 

121 """ 

122 if value is None: 

123 raise ValueError("An ObservationGroup cannot contain 'None'") 

124 

125 if not isinstance(value, ObservationInfo): 

126 try: 

127 kwargs: Dict[str, Any] = {"translator_class": translator_class} 

128 if pedantic is not None: 

129 kwargs["pedantic"] = pedantic 

130 value = ObservationInfo(value, **kwargs) 

131 except Exception as e: 

132 raise ValueError("Could not convert value to ObservationInfo") from e 

133 

134 return value 

135 

136 def __iter__(self) -> Iterator[ObservationInfo]: 

137 return iter(self._members) 

138 

139 def __eq__(self, other: Any) -> bool: 

140 """Compares equal if all the members are equal in the same order.""" 

141 if not isinstance(other, ObservationGroup): 

142 return NotImplemented 

143 

144 for info1, info2 in zip(self, other): 

145 if info1 != info2: 

146 return False 

147 return True 

148 

149 def __setitem__( # type: ignore 

150 self, index: int, value: Union[ObservationInfo, MutableMapping[str, Any]] 

151 ) -> None: 

152 """Store item in group. 

153 

154 Item must be an `ObservationInfo` or something that can be passed 

155 to an `ObservationInfo` constructor. 

156 """ 

157 value = self._coerce_value(value) 

158 self._members[index] = value 

159 self._sorted = None 

160 

161 def insert(self, index: int, value: Union[ObservationInfo, MutableMapping[str, Any]]) -> None: 

162 value = self._coerce_value(value) 

163 self._members.insert(index, value) 

164 self._sorted = None 

165 

166 def reverse(self) -> None: 

167 self._members.reverse() 

168 

169 def sort(self, key: Optional[Callable] = None, reverse: bool = False) -> None: 

170 self._members.sort(key=key, reverse=reverse) 

171 if key is None and not reverse and self._sorted is None: 

172 # Store sorted order in cache 

173 self._sorted = self._members.copy() 

174 

175 def extremes(self) -> Tuple[ObservationInfo, ObservationInfo]: 

176 """Return the oldest observation in the group and the newest. 

177 

178 If there is only one member of the group, the newest and oldest 

179 can be the same observation. 

180 

181 Returns 

182 ------- 

183 oldest : `ObservationInfo` 

184 Oldest observation. 

185 newest : `ObservationInfo` 

186 Newest observation. 

187 """ 

188 if self._sorted is None: 

189 self._sorted = sorted(self._members) 

190 return self._sorted[0], self._sorted[-1] 

191 

192 def newest(self) -> ObservationInfo: 

193 """Return the newest observation in the group. 

194 

195 Returns 

196 ------- 

197 newest : `ObservationInfo` 

198 The newest `ObservationInfo` in the `ObservationGroup`. 

199 """ 

200 return self.extremes()[1] 

201 

202 def oldest(self) -> ObservationInfo: 

203 """Return the oldest observation in the group. 

204 

205 Returns 

206 ------- 

207 oldest : `ObservationInfo` 

208 The oldest `ObservationInfo` in the `ObservationGroup`. 

209 """ 

210 return self.extremes()[0] 

211 

212 def property_values(self, property: str) -> Set[Any]: 

213 """Return a set of values associated with the specified property. 

214 

215 Parameters 

216 ---------- 

217 property : `str` 

218 Property of an `ObservationInfo` 

219 

220 Returns 

221 ------- 

222 values : `set` 

223 All the distinct values for that property within this group. 

224 """ 

225 return {getattr(obs_info, property) for obs_info in self} 

226 

227 def to_simple(self) -> List[MutableMapping[str, Any]]: 

228 """Convert the group to simplified form. 

229 

230 Returns 

231 ------- 

232 simple : `list` of `dict` 

233 Simple form is a list containing the simplified dict form of 

234 each `ObservationInfo`. 

235 """ 

236 return [obsinfo.to_simple() for obsinfo in self] 

237 

238 @classmethod 

239 def from_simple(cls, simple: List[Dict[str, Any]]) -> ObservationGroup: 

240 """Convert simplified form back to `ObservationGroup` 

241 

242 Parameters 

243 ---------- 

244 simple : `list` of `dict` 

245 Object returned by `to_simple`. 

246 

247 Returns 

248 ------- 

249 group : `ObservationGroup` 

250 Reconstructed group. 

251 """ 

252 return cls((ObservationInfo.from_simple(o) for o in simple))