Coverage for python/lsst/daf/butler/dimensions/_database.py: 58%
113 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-05 10:00 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-05 10:00 +0000
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 software is dual licensed under the GNU General Public License and also
10# under a 3-clause BSD license. Recipients may choose which of these licenses
11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12# respectively. If you choose the GPL option then the following text applies
13# (but note that there is still no warranty even if you opt for BSD instead):
14#
15# This program is free software: you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation, either version 3 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program. If not, see <http://www.gnu.org/licenses/>.
28from __future__ import annotations
30__all__ = (
31 "DatabaseDimension",
32 "DatabaseDimensionCombination",
33 "DatabaseDimensionElement",
34 "DatabaseTopologicalFamily",
35)
37from collections.abc import Iterable, Mapping, Set
38from types import MappingProxyType
39from typing import TYPE_CHECKING
41from lsst.utils.classes import cached_getter
43from .._named import NamedValueAbstractSet, NamedValueSet
44from .._topology import TopologicalFamily, TopologicalSpace
45from ._elements import Dimension, DimensionCombination, DimensionElement, KeyColumnSpec, MetadataColumnSpec
46from .construction import DimensionConstructionBuilder, DimensionConstructionVisitor
48if TYPE_CHECKING:
49 from ._governor import GovernorDimension
50 from ._universe import DimensionUniverse
53class DatabaseTopologicalFamily(TopologicalFamily):
54 """Database topological family implementation.
56 A `TopologicalFamily` implementation for the `DatabaseDimension` and
57 `DatabaseDimensionCombination` objects that have direct database
58 representations.
60 Parameters
61 ----------
62 name : `str`
63 Name of the family.
64 space : `TopologicalSpace`
65 Space in which this family's regions live.
66 members : `NamedValueAbstractSet` [ `DimensionElement` ]
67 The members of this family, ordered according to the priority used
68 in `choose` (first-choice member first).
69 """
71 def __init__(
72 self,
73 name: str,
74 space: TopologicalSpace,
75 *,
76 members: NamedValueAbstractSet[DimensionElement],
77 ):
78 super().__init__(name, space)
79 self.members = members
81 def choose(self, endpoints: Set[str], universe: DimensionUniverse) -> DimensionElement:
82 # Docstring inherited from TopologicalFamily.
83 for member in self.members:
84 if member.name in endpoints:
85 return member
86 raise RuntimeError(f"No recognized endpoints for {self.name} in {endpoints}.")
88 @property
89 @cached_getter
90 def governor(self) -> GovernorDimension:
91 """Return `GovernorDimension` common to all members of this family.
93 (`GovernorDimension`).
94 """
95 governors = {m.governor for m in self.members}
96 if None in governors:
97 raise RuntimeError(
98 f"Bad {self.space.name} family definition {self.name}: at least one member "
99 f"in {self.members} has no GovernorDimension dependency."
100 )
101 try:
102 (result,) = governors
103 except ValueError:
104 raise RuntimeError(
105 f"Bad {self.space.name} family definition {self.name}: multiple governors {governors} "
106 f"in {self.members}."
107 ) from None
108 return result # type: ignore
110 members: NamedValueAbstractSet[DimensionElement]
111 """The members of this family, ordered according to the priority used in
112 `choose` (first-choice member first).
113 """
116class DatabaseTopologicalFamilyConstructionVisitor(DimensionConstructionVisitor):
117 """A construction visitor for `DatabaseTopologicalFamily`.
119 This visitor depends on (and is thus visited after) its members.
121 Parameters
122 ----------
123 space : `TopologicalSpace`
124 Space in which this family's regions live.
125 members : `~collections.abc.Iterable` [ `str` ]
126 The names of the members of this family, ordered according to the
127 priority used in `choose` (first-choice member first).
128 """
130 def __init__(self, space: TopologicalSpace, members: Iterable[str]):
131 self._space = space
132 self._members = tuple(members)
134 def has_dependencies_in(self, others: Set[str]) -> bool:
135 # Docstring inherited from DimensionConstructionVisitor.
136 return not others.isdisjoint(self._members)
138 def visit(self, name: str, builder: DimensionConstructionBuilder) -> None:
139 # Docstring inherited from DimensionConstructionVisitor.
140 members = NamedValueSet(builder.elements[member_name] for member_name in self._members)
141 family = DatabaseTopologicalFamily(name, self._space, members=members.freeze())
142 builder.topology[self._space].add(family)
143 for member in members:
144 assert isinstance(member, DatabaseDimension | DatabaseDimensionCombination)
145 other = member._topology.setdefault(self._space, family)
146 if other is not family:
147 raise RuntimeError(
148 f"{member.name} is declared to be a member of (at least) two "
149 f"{self._space.name} families: {other.name} and {family.name}."
150 )
153class DatabaseDimensionElement(DimensionElement):
154 """An intermediate base class for `DimensionElement` database classes.
156 Instances of these element classes map directly to a database table or
157 query.
159 Parameters
160 ----------
161 name : `str`
162 Name of the dimension.
163 implied : `NamedValueAbstractSet` [ `Dimension` ]
164 Other dimensions whose keys are included in this dimension's (logical)
165 table as foreign keys.
166 metadata_columns : `NamedValueAbstractSet` [ `MetadataColumnSpec` ]
167 Field specifications for all non-key fields in this dimension's table.
168 is_cached : `bool`
169 Whether this element's records should be persistently cached in the
170 client.
171 doc : `str`
172 Extended description of this element.
173 """
175 def __init__(
176 self,
177 name: str,
178 *,
179 implied: NamedValueAbstractSet[Dimension],
180 metadata_columns: NamedValueAbstractSet[MetadataColumnSpec],
181 is_cached: bool,
182 doc: str,
183 ):
184 self._name = name
185 self._implied = implied
186 self._metadata_columns = metadata_columns
187 self._topology: dict[TopologicalSpace, DatabaseTopologicalFamily] = {}
188 self._is_cached = is_cached
189 self._doc = doc
191 @property
192 def name(self) -> str:
193 # Docstring inherited from TopologicalRelationshipEndpoint.
194 return self._name
196 @property
197 def implied(self) -> NamedValueAbstractSet[Dimension]:
198 # Docstring inherited from DimensionElement.
199 return self._implied
201 @property
202 def metadata_columns(self) -> NamedValueAbstractSet[MetadataColumnSpec]:
203 # Docstring inherited from DimensionElement.
204 return self._metadata_columns
206 @property
207 def is_cached(self) -> bool:
208 # Docstring inherited.
209 return self._is_cached
211 @property
212 def documentation(self) -> str:
213 # Docstring inherited from DimensionElement.
214 return self._doc
216 @property
217 def topology(self) -> Mapping[TopologicalSpace, DatabaseTopologicalFamily]:
218 # Docstring inherited from TopologicalRelationshipEndpoint
219 return MappingProxyType(self._topology)
221 @property
222 def spatial(self) -> DatabaseTopologicalFamily | None:
223 # Docstring inherited from TopologicalRelationshipEndpoint
224 return self.topology.get(TopologicalSpace.SPATIAL)
226 @property
227 def temporal(self) -> DatabaseTopologicalFamily | None:
228 # Docstring inherited from TopologicalRelationshipEndpoint
229 return self.topology.get(TopologicalSpace.TEMPORAL)
232class DatabaseDimension(Dimension, DatabaseDimensionElement):
233 """A `Dimension` class that maps directly to a database table or query.
235 Parameters
236 ----------
237 name : `str`
238 Name of the dimension.
239 required : `NamedValueSet` [ `Dimension` ]
240 Other dimensions whose keys are part of the compound primary key for
241 this dimension's (logical) table, as well as references to their own
242 tables. The ``required`` parameter does not include ``self`` (it
243 can't, of course), but the corresponding attribute does - it is added
244 by the constructor.
245 implied : `NamedValueAbstractSet` [ `Dimension` ]
246 Other dimensions whose keys are included in this dimension's (logical)
247 table as foreign keys.
248 metadata_columns : `NamedValueAbstractSet` [ `MetadataColumnSpec` ]
249 Field specifications for all non-key fields in this dimension's table.
250 unique_keys : `NamedValueAbstractSet` [ `KeyColumnSpec` ]
251 Fields that can each be used to uniquely identify this dimension (given
252 values for all required dimensions). The first of these is used as
253 (part of) this dimension's table's primary key, while others are used
254 to define unique constraints.
255 implied_union_target : `str` or `None`
256 If not `None`, the name of an element whose implied values for
257 this element form the set of allowable values.
258 is_cached : `bool`
259 Whether this element's records should be persistently cached in the
260 client.
261 doc : `str`
262 Extended description of this element.
264 Notes
265 -----
266 `DatabaseDimension` objects may belong to a `TopologicalFamily`, but it is
267 the responsibility of `DatabaseTopologicalFamilyConstructionVisitor` to
268 update the `~TopologicalRelationshipEndpoint.topology` attribute of their
269 members.
270 """
272 def __init__(
273 self,
274 name: str,
275 *,
276 required: NamedValueSet[Dimension],
277 implied: NamedValueAbstractSet[Dimension],
278 metadata_columns: NamedValueAbstractSet[MetadataColumnSpec],
279 unique_keys: NamedValueAbstractSet[KeyColumnSpec],
280 implied_union_target: str | None,
281 is_cached: bool,
282 doc: str,
283 ):
284 super().__init__(
285 name, implied=implied, metadata_columns=metadata_columns, is_cached=is_cached, doc=doc
286 )
287 required.add(self)
288 self._required = required.freeze()
289 self._unique_keys = unique_keys
290 self._implied_union_target = implied_union_target
292 @property
293 def required(self) -> NamedValueAbstractSet[Dimension]:
294 # Docstring inherited from DimensionElement.
295 return self._required
297 @property
298 def unique_keys(self) -> NamedValueAbstractSet[KeyColumnSpec]:
299 # Docstring inherited from Dimension.
300 return self._unique_keys
302 @property
303 def implied_union_target(self) -> DimensionElement | None:
304 # Docstring inherited from DimensionElement.
305 # This is a bit encapsulation-breaking, but it'll all be cleaned up
306 # soon when we get rid of the storage objects entirely.
307 return self.universe[self._implied_union_target] if self._implied_union_target is not None else None
310class DatabaseDimensionCombination(DimensionCombination, DatabaseDimensionElement):
311 """A combination class that maps directly to a database table or query.
313 Parameters
314 ----------
315 name : `str`
316 Name of the dimension.
317 required : `NamedValueAbstractSet` [ `Dimension` ]
318 Dimensions whose keys define the compound primary key for this
319 combinations's (logical) table, as well as references to their own
320 tables.
321 implied : `NamedValueAbstractSet` [ `Dimension` ]
322 Dimensions whose keys are included in this combinations's (logical)
323 table as foreign keys.
324 metadata_columns : `NamedValueAbstractSet` [ `MetadataColumnSpec` ]
325 Field specifications for all non-key fields in this combination's
326 table.
327 is_cached : `bool`
328 Whether this element's records should be persistently cached in the
329 client.
330 always_join : `bool`, optional
331 If `True`, always include this element in any query or data ID in
332 which its ``required`` dimensions appear, because it defines a
333 relationship between those dimensions that must always be satisfied.
334 populated_by : `Dimension` or `None`
335 The dimension that this element's records are always inserted,
336 exported, and imported alongside.
337 doc : `str`
338 Extended description of this element.
340 Notes
341 -----
342 `DatabaseDimensionCombination` objects may belong to a `TopologicalFamily`,
343 but it is the responsibility of
344 `DatabaseTopologicalFamilyConstructionVisitor` to update the
345 `~TopologicalRelationshipEndpoint.topology` attribute of their members.
347 This class has a lot in common with `DatabaseDimension`, but they are
348 expected to diverge in future changes, and the only way to make them share
349 method implementations would be via multiple inheritance. Given the
350 trivial nature of all of those implementations, this does not seem worth
351 the drawbacks (particularly the constraints it imposes on constructor
352 signatures).
353 """
355 def __init__(
356 self,
357 name: str,
358 *,
359 required: NamedValueAbstractSet[Dimension],
360 implied: NamedValueAbstractSet[Dimension],
361 metadata_columns: NamedValueAbstractSet[MetadataColumnSpec],
362 is_cached: bool,
363 always_join: bool,
364 populated_by: Dimension | None,
365 doc: str,
366 ):
367 super().__init__(
368 name, implied=implied, metadata_columns=metadata_columns, is_cached=is_cached, doc=doc
369 )
370 self._required = required
371 self._always_join = always_join
372 self._populated_by = populated_by
374 @property
375 def required(self) -> NamedValueAbstractSet[Dimension]:
376 # Docstring inherited from DimensionElement.
377 return self._required
379 @property
380 def alwaysJoin(self) -> bool:
381 # Docstring inherited from DimensionElement.
382 return self._always_join
384 @property
385 def defines_relationships(self) -> bool:
386 # Docstring inherited from DimensionElement.
387 return self._always_join or bool(self.implied)
389 @property
390 def populated_by(self) -> Dimension | None:
391 # Docstring inherited.
392 return self._populated_by