Coverage for python/lsst/daf/butler/core/dimensions/_elements.py : 47%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of daf_butler.
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 COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
22from __future__ import annotations
24__all__ = (
25 "Dimension",
26 "DimensionCombination",
27 "DimensionElement",
28)
30from abc import abstractmethod
32from typing import (
33 Any,
34 Optional,
35 Type,
36 TYPE_CHECKING,
37)
39from ..named import NamedValueAbstractSet, NamedValueSet
40from ..utils import cached_getter
41from .. import ddl
42from .._topology import TopologicalRelationshipEndpoint
45if TYPE_CHECKING: # Imports needed only for type annotations; may be circular. 45 ↛ 46line 45 didn't jump to line 46, because the condition on line 45 was never true
46 from ._universe import DimensionUniverse
47 from ._governor import GovernorDimension
48 from ._graph import DimensionGraph
49 from ._records import DimensionRecord
52class DimensionElement(TopologicalRelationshipEndpoint):
53 """A named data-organization concept that defines a label and/or metadata
54 in the dimensions system.
56 A `DimensionElement` instance typically corresponds to a _logical_ table in
57 the `Registry`: either an actual database table or a way of generating rows
58 on-the-fly that can similarly participate in queries. The rows in that
59 table are represented by instances of a `DimensionRecord` subclass. Most
60 `DimensionElement` instances are instances of its `Dimension` subclass,
61 which is used for elements that can be used as data ID keys.
63 Notes
64 -----
65 `DimensionElement` instances should always be constructed by and retreived
66 from a `DimensionUniverse`. They are immutable after they are fully
67 constructed, and should never be copied.
69 Pickling a `DimensionElement` just records its name and universe;
70 unpickling one actually just looks up the element via the singleton
71 dictionary of all universes. This allows pickle to be used to transfer
72 elements between processes, but only when each process initializes its own
73 instance of the same `DimensionUniverse`.
74 """
76 def __str__(self) -> str:
77 return self.name
79 def __repr__(self) -> str:
80 return f"{type(self).__name__}({self.name})"
82 def __eq__(self, other: Any) -> bool:
83 try:
84 return self.name == other.name
85 except AttributeError:
86 # TODO: try removing this fallback; it's not really consistent with
87 # base class intent, and it could be confusing
88 return self.name == other
90 def __hash__(self) -> int:
91 return hash(self.name)
93 # TODO: try removing comparison operators; DimensionUniverse.sorted should
94 # be adequate.
96 def __lt__(self, other: DimensionElement) -> bool:
97 try:
98 return self.universe.getElementIndex(self.name) < self.universe.getElementIndex(other.name)
99 except KeyError:
100 return NotImplemented
102 def __le__(self, other: DimensionElement) -> bool:
103 try:
104 return self.universe.getElementIndex(self.name) <= self.universe.getElementIndex(other.name)
105 except KeyError:
106 return NotImplemented
108 def __gt__(self, other: DimensionElement) -> bool:
109 try:
110 return self.universe.getElementIndex(self.name) > self.universe.getElementIndex(other.name)
111 except KeyError:
112 return NotImplemented
114 def __ge__(self, other: DimensionElement) -> bool:
115 try:
116 return self.universe.getElementIndex(self.name) >= self.universe.getElementIndex(other.name)
117 except KeyError:
118 return NotImplemented
120 @classmethod
121 def _unpickle(cls, universe: DimensionUniverse, name: str) -> DimensionElement:
122 """Callable used for unpickling.
124 For internal use only.
125 """
126 return universe[name]
128 def __reduce__(self) -> tuple:
129 return (self._unpickle, (self.universe, self.name))
131 def __deepcopy__(self, memo: dict) -> DimensionElement:
132 # DimensionElement is recursively immutable; see note in @immutable
133 # decorator.
134 return self
136 def hasTable(self) -> bool:
137 """Return `True` if this element is associated with a table
138 (even if that table "belongs" to another element).
139 """
140 return True
142 universe: DimensionUniverse
143 """The universe of all compatible dimensions with which this element is
144 associated (`DimensionUniverse`).
145 """
147 @property # type: ignore
148 @cached_getter
149 def governor(self) -> Optional[GovernorDimension]:
150 """The `GovernorDimension` that is a required dependency of this
151 element, or `None` if there is no such dimension (`GovernorDimension`
152 or `None`).
153 """
154 if len(self.graph.governors) == 1:
155 (result,) = self.graph.governors
156 return result
157 elif len(self.graph.governors) > 1:
158 raise RuntimeError(
159 f"Dimension element {self.name} has multiple governors: {self.graph.governors}."
160 )
161 else:
162 return None
164 @property
165 @abstractmethod
166 def required(self) -> NamedValueAbstractSet[Dimension]:
167 """Dimensions that are necessary to uniquely identify a record of this
168 dimension element.
170 For elements with a database representation, these dimension are
171 exactly those used to form the (possibly compound) primary key, and all
172 dimensions here that are not ``self`` are also used to form foreign
173 keys.
175 For `Dimension` instances, this should be exactly the same as
176 ``graph.required``, but that may not be true for `DimensionElement`
177 instances in general. When they differ, there are multiple
178 combinations of dimensions that uniquely identify this element, but
179 this one is more direct.
180 """
181 raise NotImplementedError()
183 @property
184 @abstractmethod
185 def implied(self) -> NamedValueAbstractSet[Dimension]:
186 """Other dimensions that are uniquely identified directly by a record
187 of this dimension element.
189 For elements with a database representation, these are exactly the
190 dimensions used to form foreign key constraints whose fields are not
191 (wholly) also part of the primary key.
193 Unlike ``self.graph.implied``, this set is not expanded recursively.
194 """
195 raise NotImplementedError()
197 @property # type: ignore
198 @cached_getter
199 def dimensions(self) -> NamedValueAbstractSet[Dimension]:
200 """The union of `required` and `implied`, with all elements in
201 `required` before any elements in `implied`.
203 This differs from ``self.graph.dimensions`` both in order and in
204 content:
206 - as in ``self.implied``, implied dimensions are not expanded
207 recursively here;
208 - implied dimensions appear after required dimensions here, instead of
209 being topologically ordered.
211 As a result, this set is ordered consistently with
212 ``self.RecordClass.fields``.
213 """
214 return NamedValueSet(list(self.required) + list(self.implied)).freeze()
216 @property # type: ignore
217 @cached_getter
218 def graph(self) -> DimensionGraph:
219 """Minimal graph that includes this element (`DimensionGraph`).
221 ``self.graph.required`` includes all dimensions whose primary key
222 values are sufficient (often necessary) to uniquely identify ``self``
223 (including ``self`` if ``isinstance(self, Dimension)``.
224 ``self.graph.implied`` includes all dimensions also identified
225 (possibly recursively) by this set.
226 """
227 return self.universe.extract(self.dimensions.names)
229 @property # type: ignore
230 @cached_getter
231 def RecordClass(self) -> Type[DimensionRecord]:
232 """The `DimensionRecord` subclass used to hold records for this element
233 (`type`).
235 Because `DimensionRecord` subclasses are generated dynamically, this
236 type cannot be imported directly and hence can only be obtained from
237 this attribute.
238 """
239 from ._records import _subclassDimensionRecord
240 return _subclassDimensionRecord(self)
242 @property
243 @abstractmethod
244 def metadata(self) -> NamedValueAbstractSet[ddl.FieldSpec]:
245 """Additional metadata fields included in this element's table
246 (`NamedValueSet` of `FieldSpec`).
247 """
248 raise NotImplementedError()
250 @property
251 def viewOf(self) -> Optional[str]:
252 """Name of another table this element's records are drawn from (`str`
253 or `None`).
254 """
255 return None
257 @property
258 def alwaysJoin(self) -> bool:
259 """If `True`, always include this element in any query or data ID in
260 which its ``required`` dimensions appear, because it defines a
261 relationship between those dimensions that must always be satisfied.
262 """
263 return False
266class Dimension(DimensionElement):
267 """A named data-organization concept that can be used as a key in a data
268 ID.
269 """
271 @property
272 @abstractmethod
273 def uniqueKeys(self) -> NamedValueAbstractSet[ddl.FieldSpec]:
274 """All fields that can individually be used to identify records of this
275 element, given the primary keys of all required dependencies
276 (`NamedValueAbstractSet` of `FieldSpec`).
277 """
278 raise NotImplementedError()
280 @property # type: ignore
281 @cached_getter
282 def primaryKey(self) -> ddl.FieldSpec:
283 """The primary key field for this dimension (`FieldSpec`).
285 Note that the database primary keys for dimension tables are in general
286 compound; this field is the only field in the database primary key that
287 is not also a foreign key (to a required dependency dimension table).
288 """
289 primaryKey, *_ = self.uniqueKeys
290 return primaryKey
292 @property # type: ignore
293 @cached_getter
294 def alternateKeys(self) -> NamedValueAbstractSet[ddl.FieldSpec]:
295 """Additional unique key fields for this dimension that are not the
296 primary key (`NamedValueAbstractSet` of `FieldSpec`).
298 If this dimension has required dependencies, the keys of those
299 dimensions are also included in the unique constraints defined for
300 these alternate keys.
301 """
302 _, *alternateKeys = self.uniqueKeys
303 return NamedValueSet(alternateKeys).freeze()
306class DimensionCombination(DimensionElement):
307 """A `DimensionElement` that provides extra metadata and/or relationship
308 endpoint information for a combination of dimensions.
309 """
310 pass