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

74 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-13 01:21 -0800

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 itertools import zip_longest 

21from typing import TYPE_CHECKING, Any 

22 

23from .observationInfo import ObservationInfo 

24 

25if TYPE_CHECKING: 

26 from .translator import MetadataTranslator 

27 

28log = logging.getLogger(__name__) 

29 

30 

31class ObservationGroup(MutableSequence): 

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

33 

34 Parameters 

35 ---------- 

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

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

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

39 constructor. 

40 translator_class : `MetadataTranslator`-class, optional 

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

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

43 translation class will be determined automatically. 

44 pedantic : `bool`, optional 

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

46 `ObservationInfo` constructor to control whether 

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

48 `ObservationInfo` constructor default should be used. 

49 """ 

50 

51 def __init__( 

52 self, 

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

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

55 pedantic: bool | None = None, 

56 ) -> None: 

57 self._members = [ 

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

59 ] 

60 

61 # Cache of members in time order 

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

63 

64 def __len__(self) -> int: 

65 return len(self._members) 

66 

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

68 del self._members[index] 

69 self._sorted = None 

70 

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

72 return self._members[index] 

73 

74 def __str__(self) -> str: 

75 results = [] 

76 for obs_info in self._members: 

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

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

79 

80 def _coerce_value( 

81 self, 

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

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

84 pedantic: bool | None = None, 

85 ) -> ObservationInfo: 

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

87 

88 Parameters 

89 ---------- 

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

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

92 an `ObservationInfo` constructor. 

93 translator_class : `MetadataTranslator`-class, optional 

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

95 the `ObservationInfo` constructor. If `None` the 

96 translation class will be determined automatically. 

97 pedantic : `bool`, optional 

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

99 `ObservationInfo` constructor to control whether 

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

101 `ObservationInfo` constructor default should be used. 

102 

103 Raises 

104 ------ 

105 ValueError 

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

107 not be turned into one. 

108 """ 

109 if value is None: 

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

111 

112 if not isinstance(value, ObservationInfo): 

113 try: 

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

115 if pedantic is not None: 

116 kwargs["pedantic"] = pedantic 

117 value = ObservationInfo(value, **kwargs) 

118 except Exception as e: 

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

120 

121 return value 

122 

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

124 return iter(self._members) 

125 

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

127 """Check equality with another group. 

128 

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

130 

131 Parameters 

132 ---------- 

133 other : `typing.Any` 

134 Thing to compare with current group. 

135 """ 

136 if not isinstance(other, ObservationGroup): 

137 return NotImplemented 

138 

139 for info1, info2 in zip_longest(self, other): 

140 if info1 != info2: 

141 return False 

142 return True 

143 

144 def __setitem__( # type: ignore 

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

146 ) -> None: 

147 """Store item in group. 

148 

149 Parameters 

150 ---------- 

151 index : `int` 

152 Index to use to store the item. 

153 value : `ObservationInfo` or `~collections.abc.MutableMapping` 

154 Information to store in group. Item must be an `ObservationInfo` 

155 or something that can be passed to an `ObservationInfo` 

156 constructor. 

157 """ 

158 value = self._coerce_value(value) 

159 self._members[index] = value 

160 self._sorted = None 

161 

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

163 value = self._coerce_value(value) 

164 self._members.insert(index, value) 

165 self._sorted = None 

166 

167 def reverse(self) -> None: 

168 self._members.reverse() 

169 

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

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

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

173 # Store sorted order in cache 

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

175 

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

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

178 

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

180 can be the same observation. 

181 

182 Returns 

183 ------- 

184 oldest : `ObservationInfo` 

185 Oldest observation. 

186 newest : `ObservationInfo` 

187 Newest observation. 

188 """ 

189 if self._sorted is None: 

190 self._sorted = sorted(self._members) 

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

192 

193 def newest(self) -> ObservationInfo: 

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

195 

196 Returns 

197 ------- 

198 newest : `ObservationInfo` 

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

200 """ 

201 return self.extremes()[1] 

202 

203 def oldest(self) -> ObservationInfo: 

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

205 

206 Returns 

207 ------- 

208 oldest : `ObservationInfo` 

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

210 """ 

211 return self.extremes()[0] 

212 

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

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

215 

216 Parameters 

217 ---------- 

218 property : `str` 

219 Property of an `ObservationInfo`. 

220 

221 Returns 

222 ------- 

223 values : `set` 

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

225 """ 

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

227 

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

229 """Convert the group to simplified form. 

230 

231 Returns 

232 ------- 

233 simple : `list` of `dict` 

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

235 each `ObservationInfo`. 

236 """ 

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

238 

239 @classmethod 

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

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

242 

243 Parameters 

244 ---------- 

245 simple : `list` of `dict` 

246 Object returned by `to_simple`. 

247 

248 Returns 

249 ------- 

250 group : `ObservationGroup` 

251 Reconstructed group. 

252 """ 

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