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

76 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-27 02:38 -0700

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 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 """Compares equal if all the members are equal in the same order.""" 

127 if not isinstance(other, ObservationGroup): 

128 return NotImplemented 

129 

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

131 if info1 != info2: 

132 return False 

133 return True 

134 

135 def __setitem__( # type: ignore 

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

137 ) -> None: 

138 """Store item in group. 

139 

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

141 to an `ObservationInfo` constructor. 

142 """ 

143 value = self._coerce_value(value) 

144 self._members[index] = value 

145 self._sorted = None 

146 

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

148 value = self._coerce_value(value) 

149 self._members.insert(index, value) 

150 self._sorted = None 

151 

152 def reverse(self) -> None: 

153 self._members.reverse() 

154 

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

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

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

158 # Store sorted order in cache 

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

160 

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

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

163 

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

165 can be the same observation. 

166 

167 Returns 

168 ------- 

169 oldest : `ObservationInfo` 

170 Oldest observation. 

171 newest : `ObservationInfo` 

172 Newest observation. 

173 """ 

174 if self._sorted is None: 

175 self._sorted = sorted(self._members) 

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

177 

178 def newest(self) -> ObservationInfo: 

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

180 

181 Returns 

182 ------- 

183 newest : `ObservationInfo` 

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

185 """ 

186 return self.extremes()[1] 

187 

188 def oldest(self) -> ObservationInfo: 

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

190 

191 Returns 

192 ------- 

193 oldest : `ObservationInfo` 

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

195 """ 

196 return self.extremes()[0] 

197 

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

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

200 

201 Parameters 

202 ---------- 

203 property : `str` 

204 Property of an `ObservationInfo` 

205 

206 Returns 

207 ------- 

208 values : `set` 

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

210 """ 

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

212 

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

214 """Convert the group to simplified form. 

215 

216 Returns 

217 ------- 

218 simple : `list` of `dict` 

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

220 each `ObservationInfo`. 

221 """ 

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

223 

224 @classmethod 

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

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

227 

228 Parameters 

229 ---------- 

230 simple : `list` of `dict` 

231 Object returned by `to_simple`. 

232 

233 Returns 

234 ------- 

235 group : `ObservationGroup` 

236 Reconstructed group. 

237 """ 

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