Coverage for python/lsst/daf/butler/registry/interfaces/_dimensions.py : 38%

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/>.
21from __future__ import annotations
23__all__ = ["DimensionRecordStorage", "DimensionRecordStorageManager"]
25from abc import ABC, abstractmethod
26from typing import Optional, Type, TYPE_CHECKING
28import sqlalchemy
30from ...core import SkyPixDimension
32if TYPE_CHECKING: 32 ↛ 33line 32 didn't jump to line 33, because the condition on line 32 was never true
33 from ...core import (
34 DataId,
35 DimensionElement,
36 DimensionRecord,
37 DimensionUniverse,
38 Timespan,
39 )
40 from ...core.utils import NamedKeyDict
41 from ..queries import QueryBuilder
42 from ._database import Database, StaticTablesContext
45class DimensionRecordStorage(ABC):
46 """An abstract base class that represents a way of storing the records
47 associated with a single `DimensionElement`.
49 Concrete `DimensionRecordStorage` instances should generally be constructed
50 via a call to `setupDimensionStorage`, which selects the appropriate
51 subclass for each element according to its configuration.
53 All `DimensionRecordStorage` methods are pure abstract, even though in some
54 cases a reasonable default implementation might be possible, in order to
55 better guarantee all methods are correctly overridden. All of these
56 potentially-defaultable implementations are extremely trivial, so asking
57 subclasses to provide them is not a significant burden.
58 """
60 @classmethod
61 @abstractmethod
62 def initialize(cls, db: Database, element: DimensionElement, *,
63 context: Optional[StaticTablesContext] = None) -> DimensionRecordStorage:
64 """Construct an instance of this class using a standardized interface.
66 Parameters
67 ----------
68 db : `Database`
69 Interface to the underlying database engine and namespace.
70 element : `DimensionElement`
71 Dimension element the new instance will manage records for.
72 context : `StaticTablesContext`, optional
73 If provided, an object to use to create any new tables. If not
74 provided, ``db.ensureTableExists`` should be used instead.
76 Returns
77 -------
78 storage : `DimensionRecordStorage`
79 A new `DimensionRecordStorage` subclass instance.
80 """
81 raise NotImplementedError()
83 @staticmethod
84 def getDefaultImplementation(element: DimensionElement, ignoreCached: bool = False
85 ) -> Type[DimensionRecordStorage]:
86 """Return the default `DimensionRecordStorage` implementation for the
87 given `DimensionElement`.
89 Parameters
90 ----------
91 element : `DimensionElement`
92 The element whose properties should be examined to determine the
93 appropriate default implementation class.
94 ignoreCached : `bool`, optional
95 If `True`, ignore `DimensionElement.cached` and always return the
96 storage implementation that would be used without caching.
98 Returns
99 -------
100 cls : `type`
101 A concrete subclass of `DimensionRecordStorage`.
103 Notes
104 -----
105 At present, these defaults are always used, but we may add support for
106 explicitly setting the class to use in configuration in the future.
107 """
108 if not ignoreCached and element.cached:
109 from ..dimensions.caching import CachingDimensionRecordStorage
110 return CachingDimensionRecordStorage
111 elif element.hasTable():
112 if element.viewOf is not None:
113 if element.spatial:
114 raise NotImplementedError("Spatial view dimension storage is not supported.")
115 from ..dimensions.query import QueryDimensionRecordStorage
116 return QueryDimensionRecordStorage
117 elif element.spatial:
118 from ..dimensions.spatial import SpatialDimensionRecordStorage
119 return SpatialDimensionRecordStorage
120 else:
121 from ..dimensions.table import TableDimensionRecordStorage
122 return TableDimensionRecordStorage
123 elif isinstance(element, SkyPixDimension):
124 from ..dimensions.skypix import SkyPixDimensionRecordStorage
125 return SkyPixDimensionRecordStorage
126 raise NotImplementedError(f"No default DimensionRecordStorage class for {element}.")
128 @property
129 @abstractmethod
130 def element(self) -> DimensionElement:
131 """The element whose records this instance holds (`DimensionElement`).
132 """
133 raise NotImplementedError()
135 @abstractmethod
136 def clearCaches(self):
137 """Clear any in-memory caches held by the storage instance.
139 This is called by `Registry` when transactions are rolled back, to
140 avoid in-memory caches from ever containing records that are not
141 present in persistent storage.
142 """
143 raise NotImplementedError()
145 @abstractmethod
146 def join(
147 self,
148 builder: QueryBuilder, *,
149 regions: Optional[NamedKeyDict[DimensionElement, sqlalchemy.sql.ColumnElement]] = None,
150 timespans: Optional[NamedKeyDict[DimensionElement, Timespan[sqlalchemy.sql.ColumnElement]]] = None,
151 ):
152 """Add the dimension element's logical table to a query under
153 construction.
155 This is a visitor pattern interface that is expected to be called only
156 by `QueryBuilder.joinDimensionElement`.
158 Parameters
159 ----------
160 builder : `QueryBuilder`
161 Builder for the query that should contain this element.
162 regions : `NamedKeyDict`, optional
163 A mapping from `DimensionElement` to a SQLAlchemy column containing
164 the region for that element, which should be updated to include a
165 region column for this element if one exists. If `None`,
166 ``self.element`` is not being included in the query via a spatial
167 join.
168 timespan : `NamedKeyDict`, optional
169 A mapping from `DimensionElement` to a `Timespan` of SQLALchemy
170 columns containing the timespan for that element, which should be
171 updated to include timespan columns for this element if they exist.
172 If `None`, ``self.element`` is not being included in the query via
173 a temporal join.
175 Returns
176 -------
177 fromClause : `sqlalchemy.sql.FromClause`
178 Table or clause for the element which is joined.
180 Notes
181 -----
182 Elements are only included in queries via spatial and/or temporal joins
183 when necessary to connect them to other elements in the query, so
184 ``regions`` and ``timespans`` cannot be assumed to be not `None` just
185 because an element has a region or timespan.
186 """
187 raise NotImplementedError()
189 @abstractmethod
190 def insert(self, *records: DimensionRecord):
191 """Insert one or more records into storage.
193 Parameters
194 ----------
195 records
196 One or more instances of the `DimensionRecord` subclass for the
197 element this storage is associated with.
199 Raises
200 ------
201 TypeError
202 Raised if the element does not support record insertion.
203 sqlalchemy.exc.IntegrityError
204 Raised if one or more records violate database integrity
205 constraints.
207 Notes
208 -----
209 As `insert` is expected to be called only by a `Registry`, we rely
210 on `Registry` to provide transactionality, both by using a SQLALchemy
211 connection shared with the `Registry` and by relying on it to call
212 `clearCaches` when rolling back transactions.
213 """
214 raise NotImplementedError()
216 @abstractmethod
217 def fetch(self, dataId: DataId) -> Optional[DimensionRecord]:
218 """Retrieve a record from storage.
220 Parameters
221 ----------
222 dataId : `DataId`
223 A data ID that identifies the record to be retrieved. This may
224 be an informal data ID dict or a validated `DataCoordinate`.
226 Returns
227 -------
228 record : `DimensionRecord` or `None`
229 A record retrieved from storage, or `None` if there is no such
230 record.
231 """
232 raise NotImplementedError()
235class DimensionRecordStorageManager(ABC):
236 """An interface for managing the dimension records in a `Registry`.
238 `DimensionRecordStorageManager` primarily serves as a container and factory
239 for `DimensionRecordStorage` instances, which each provide access to the
240 records for a different `DimensionElement`.
242 Parameters
243 ----------
244 universe : `DimensionUniverse`
245 Universe of all dimensions and dimension elements known to the
246 `Registry`.
248 Notes
249 -----
250 In a multi-layer `Registry`, many dimension elements will only have
251 records in one layer (often the base layer). The union of the records
252 across all layers forms the logical table for the full `Registry`.
253 """
254 def __init__(self, *, universe: DimensionUniverse):
255 self.universe = universe
257 @classmethod
258 @abstractmethod
259 def initialize(cls, db: Database, context: StaticTablesContext, *,
260 universe: DimensionUniverse) -> DimensionRecordStorageManager:
261 """Construct an instance of the manager.
263 Parameters
264 ----------
265 db : `Database`
266 Interface to the underlying database engine and namespace.
267 context : `StaticTablesContext`
268 Context object obtained from `Database.declareStaticTables`; used
269 to declare any tables that should always be present in a layer
270 implemented with this manager.
271 universe : `DimensionUniverse`
272 Universe graph containing dimensions known to this `Registry`.
274 Returns
275 -------
276 manager : `DimensionRecordStorageManager`
277 An instance of a concrete `DimensionRecordStorageManager` subclass.
278 """
279 raise NotImplementedError()
281 @abstractmethod
282 def refresh(self):
283 """Ensure all other operations on this manager are aware of any
284 dataset types that may have been registered by other clients since
285 it was initialized or last refreshed.
286 """
287 raise NotImplementedError()
289 def __getitem__(self, element: DimensionElement) -> DimensionRecordStorage:
290 """Interface to `get` that raises `LookupError` instead of returning
291 `None` on failure.
292 """
293 r = self.get(element)
294 if r is None:
295 raise LookupError(f"No dimension element '{element.name}' found in this registry layer.")
296 return r
298 @abstractmethod
299 def get(self, element: DimensionElement) -> Optional[DimensionRecordStorage]:
300 """Return an object that provides access to the records associated with
301 the given element, if one exists in this layer.
303 Parameters
304 ----------
305 element : `DimensionElement`
306 Element for which records should be returned.
308 Returns
309 -------
310 records : `DimensionRecordStorage` or `None`
311 The object representing the records for the given element in this
312 layer, or `None` if there are no records for that element in this
313 layer.
315 Notes
316 -----
317 Dimension elements registered by another client of the same layer since
318 the last call to `initialize` or `refresh` may not be found.
319 """
320 raise NotImplementedError()
322 @abstractmethod
323 def register(self, element: DimensionElement) -> DimensionRecordStorage:
324 """Ensure that this layer can hold records for the given element,
325 creating new tables as necessary.
327 Parameters
328 ----------
329 element : `DimensionElement`
330 Element for which a table should created (as necessary) and
331 an associated `DimensionRecordStorage` returned.
333 Returns
334 -------
335 records : `DimensionRecordStorage`
336 The object representing the records for the given element in this
337 layer.
339 Raises
340 ------
341 TransactionInterruption
342 Raised if this operation is invoked within a `Database.transaction`
343 context.
344 """
345 raise NotImplementedError()
347 @abstractmethod
348 def clearCaches(self):
349 """Clear any in-memory caches held by nested `DimensionRecordStorage`
350 instances.
352 This is called by `Registry` when transactions are rolled back, to
353 avoid in-memory caches from ever containing records that are not
354 present in persistent storage.
355 """
356 raise NotImplementedError()
358 universe: DimensionUniverse
359 """Universe of all dimensions and dimension elements known to the
360 `Registry` (`DimensionUniverse`).
361 """