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

76 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-15 08:03 +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 

12"""Represent a collection of translated headers.""" 

13 

14from __future__ import annotations 

15 

16__all__ = ("ObservationGroup",) 

17 

18import logging 

19from collections.abc import Callable, Iterable, Iterator, MutableMapping, MutableSequence 

20from typing import TYPE_CHECKING, Any 

21 

22from .observationInfo import ObservationInfo 

23 

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

25 from .translator import MetadataTranslator 

26 

27log = logging.getLogger(__name__) 

28 

29 

30class ObservationGroup(MutableSequence): 

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

32 

33 Parameters 

34 ---------- 

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

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

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

38 constructor. 

39 translator_class : `MetadataTranslator`-class, optional 

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

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

42 translation class will be determined automatically. 

43 pedantic : `bool`, optional 

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

45 `ObservationInfo` constructor to control whether 

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

47 `ObservationInfo` constructor default should be used. 

48 """ 

49 

50 def __init__( 

51 self, 

52 members: Iterable[ObservationInfo | MutableMapping[str, Any]], 

53 translator_class: type[MetadataTranslator] | None = None, 

54 pedantic: bool | None = None, 

55 ) -> None: 

56 self._members = [ 

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

58 ] 

59 

60 # Cache of members in time order 

61 self._sorted: list[ObservationInfo] | None = None 

62 

63 def __len__(self) -> int: 

64 return len(self._members) 

65 

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

67 del self._members[index] 

68 self._sorted = None 

69 

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

71 return self._members[index] 

72 

73 def __str__(self) -> str: 

74 results = [] 

75 for obs_info in self._members: 

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

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

78 

79 def _coerce_value( 

80 self, 

81 value: ObservationInfo | MutableMapping[str, Any], 

82 translator_class: type[MetadataTranslator] | None = None, 

83 pedantic: bool | None = None, 

84 ) -> ObservationInfo: 

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

86 

87 Parameters 

88 ---------- 

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

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

91 an `ObservationInfo` constructor. 

92 translator_class : `MetadataTranslator`-class, optional 

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

94 the `ObservationInfo` constructor. If `None` the 

95 translation class will be determined automatically. 

96 pedantic : `bool`, optional 

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

98 `ObservationInfo` constructor to control whether 

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

100 `ObservationInfo` constructor default should be used. 

101 

102 Raises 

103 ------ 

104 ValueError 

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

106 not be turned into one. 

107 """ 

108 if value is None: 

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

110 

111 if not isinstance(value, ObservationInfo): 

112 try: 

113 kwargs: dict[str, Any] = {"translator_class": translator_class} 

114 if pedantic is not None: 

115 kwargs["pedantic"] = pedantic 

116 value = ObservationInfo(value, **kwargs) 

117 except Exception as e: 

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

119 

120 return value 

121 

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

123 return iter(self._members) 

124 

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

126 """Check equality with another group. 

127 

128 Compare equal if all the members are equal in the same order. 

129 """ 

130 if not isinstance(other, ObservationGroup): 

131 return NotImplemented 

132 

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

134 if info1 != info2: 

135 return False 

136 return True 

137 

138 def __setitem__( # type: ignore 

139 self, index: int, value: ObservationInfo | MutableMapping[str, Any] 

140 ) -> None: 

141 """Store item in group. 

142 

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

144 to an `ObservationInfo` constructor. 

145 """ 

146 value = self._coerce_value(value) 

147 self._members[index] = value 

148 self._sorted = None 

149 

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

151 value = self._coerce_value(value) 

152 self._members.insert(index, value) 

153 self._sorted = None 

154 

155 def reverse(self) -> None: 

156 self._members.reverse() 

157 

158 def sort(self, key: Callable | None = None, reverse: bool = False) -> None: 

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

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

161 # Store sorted order in cache 

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

163 

164 def extremes(self) -> tuple[ObservationInfo, ObservationInfo]: 

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

166 

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

168 can be the same observation. 

169 

170 Returns 

171 ------- 

172 oldest : `ObservationInfo` 

173 Oldest observation. 

174 newest : `ObservationInfo` 

175 Newest observation. 

176 """ 

177 if self._sorted is None: 

178 self._sorted = sorted(self._members) 

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

180 

181 def newest(self) -> ObservationInfo: 

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

183 

184 Returns 

185 ------- 

186 newest : `ObservationInfo` 

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

188 """ 

189 return self.extremes()[1] 

190 

191 def oldest(self) -> ObservationInfo: 

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

193 

194 Returns 

195 ------- 

196 oldest : `ObservationInfo` 

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

198 """ 

199 return self.extremes()[0] 

200 

201 def property_values(self, property: str) -> set[Any]: 

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

203 

204 Parameters 

205 ---------- 

206 property : `str` 

207 Property of an `ObservationInfo` 

208 

209 Returns 

210 ------- 

211 values : `set` 

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

213 """ 

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

215 

216 def to_simple(self) -> list[MutableMapping[str, Any]]: 

217 """Convert the group to simplified form. 

218 

219 Returns 

220 ------- 

221 simple : `list` of `dict` 

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

223 each `ObservationInfo`. 

224 """ 

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

226 

227 @classmethod 

228 def from_simple(cls, simple: list[dict[str, Any]]) -> ObservationGroup: 

229 """Convert simplified form back to `ObservationGroup`. 

230 

231 Parameters 

232 ---------- 

233 simple : `list` of `dict` 

234 Object returned by `to_simple`. 

235 

236 Returns 

237 ------- 

238 group : `ObservationGroup` 

239 Reconstructed group. 

240 """ 

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