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
« 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.
12from __future__ import annotations
14"""Represent a collection of translated headers"""
16__all__ = ("ObservationGroup",)
18import logging
19from collections.abc import Callable, Iterable, Iterator, MutableMapping, MutableSequence
20from typing import TYPE_CHECKING, Any
22from .observationInfo import ObservationInfo
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
27log = logging.getLogger(__name__)
30class ObservationGroup(MutableSequence):
31 """A collection of `ObservationInfo` headers.
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 """
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 ]
60 # Cache of members in time order
61 self._sorted: list[ObservationInfo] | None = None
63 def __len__(self) -> int:
64 return len(self._members)
66 def __delitem__(self, index: int) -> None: # type: ignore
67 del self._members[index]
68 self._sorted = None
70 def __getitem__(self, index: int) -> ObservationInfo: # type: ignore
71 return self._members[index]
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) + "]"
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`.
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.
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'")
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
120 return value
122 def __iter__(self) -> Iterator[ObservationInfo]:
123 return iter(self._members)
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
130 for info1, info2 in zip(self, other):
131 if info1 != info2:
132 return False
133 return True
135 def __setitem__( # type: ignore
136 self, index: int, value: ObservationInfo | MutableMapping[str, Any]
137 ) -> None:
138 """Store item in group.
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
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
152 def reverse(self) -> None:
153 self._members.reverse()
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()
161 def extremes(self) -> tuple[ObservationInfo, ObservationInfo]:
162 """Return the oldest observation in the group and the newest.
164 If there is only one member of the group, the newest and oldest
165 can be the same observation.
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]
178 def newest(self) -> ObservationInfo:
179 """Return the newest observation in the group.
181 Returns
182 -------
183 newest : `ObservationInfo`
184 The newest `ObservationInfo` in the `ObservationGroup`.
185 """
186 return self.extremes()[1]
188 def oldest(self) -> ObservationInfo:
189 """Return the oldest observation in the group.
191 Returns
192 -------
193 oldest : `ObservationInfo`
194 The oldest `ObservationInfo` in the `ObservationGroup`.
195 """
196 return self.extremes()[0]
198 def property_values(self, property: str) -> set[Any]:
199 """Return a set of values associated with the specified property.
201 Parameters
202 ----------
203 property : `str`
204 Property of an `ObservationInfo`
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}
213 def to_simple(self) -> list[MutableMapping[str, Any]]:
214 """Convert the group to simplified form.
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]
224 @classmethod
225 def from_simple(cls, simple: list[dict[str, Any]]) -> ObservationGroup:
226 """Convert simplified form back to `ObservationGroup`
228 Parameters
229 ----------
230 simple : `list` of `dict`
231 Object returned by `to_simple`.
233 Returns
234 -------
235 group : `ObservationGroup`
236 Reconstructed group.
237 """
238 return cls(ObservationInfo.from_simple(o) for o in simple)