Coverage for python/lsst/daf/butler/core/dimensions/records.py : 28%

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__ = ("DimensionRecord",)
26from typing import (
27 Any,
28 ClassVar,
29 Dict,
30 Mapping,
31 Tuple,
32 TYPE_CHECKING,
33 Type,
34)
36from ..timespan import Timespan
37from .elements import Dimension
39if TYPE_CHECKING: # Imports needed only for type annotations; may be circular. 39 ↛ 40line 39 didn't jump to line 40, because the condition on line 39 was never true
40 import astropy.time
41 from .elements import DimensionElement
42 from .coordinate import DataCoordinate
45def _reconstructDimensionRecord(definition: DimensionElement, *args: Any) -> DimensionRecord:
46 """Unpickle implementation for `DimensionRecord` subclasses.
48 For internal use by `DimensionRecord`.
49 """
50 return definition.RecordClass(*args)
53def _makeTimespanFromRecord(record: DimensionRecord) -> Timespan[astropy.time.Time]:
54 """Extract a `Timespan` object from the appropriate endpoint attributes.
56 For internal use by `DimensionRecord`.
57 """
58 from ..timespan import TIMESPAN_FIELD_SPECS
59 return Timespan(
60 begin=getattr(record, TIMESPAN_FIELD_SPECS.begin.name),
61 end=getattr(record, TIMESPAN_FIELD_SPECS.end.name),
62 )
65def _subclassDimensionRecord(definition: DimensionElement) -> Type[DimensionRecord]:
66 """Create a dynamic subclass of `DimensionRecord` for the given
67 `DimensionElement`.
69 For internal use by `DimensionRecord`.
70 """
71 from .schema import makeDimensionElementTableSpec
72 fields = tuple(makeDimensionElementTableSpec(definition).fields.names)
73 d = {
74 "definition": definition,
75 "__slots__": fields,
76 "fields": fields,
77 }
78 if definition.temporal:
79 d["timespan"] = property(_makeTimespanFromRecord)
80 return type(definition.name + ".RecordClass", (DimensionRecord,), d)
83class DimensionRecord:
84 """Base class for the Python representation of database records for
85 a `DimensionElement`.
87 Parameters
88 ----------
89 args
90 Field values for this record, ordered to match ``__slots__``.
92 Notes
93 -----
94 `DimensionRecord` subclasses are created dynamically for each
95 `DimensionElement` in a `DimensionUniverse`, and are accessible via the
96 `DimensionElement.RecordClass` attribute. The `DimensionRecord` base class
97 itself is pure abstract, but does not use the `abc` module to indicate this
98 because it does not have overridable methods.
100 Record classes have attributes that correspond exactly to the fields in the
101 related database table, a few additional methods inherited from the
102 `DimensionRecord` base class, and two additional injected attributes:
104 - ``definition`` is a class attribute that holds the `DimensionElement`;
106 - ``timespan`` is a property that returns a `Timespan`, present only
107 on record classes that correspond to temporal elements.
109 The field attributes are defined via the ``__slots__`` mechanism, and the
110 ``__slots__`` tuple itself is considered the public interface for obtaining
111 the list of fields.
113 Instances are usually obtained from a `Registry`, but in the rare cases
114 where they are constructed directly in Python (usually for insertion into
115 a `Registry`), the `fromDict` method should generally be used.
117 `DimensionRecord` instances are immutable.
118 """
120 # Derived classes are required to define __slots__ as well, and it's those
121 # derived-class slots that other methods on the base class expect to see
122 # when they access self.__slots__.
123 __slots__ = ("dataId",)
125 def __init__(self, *args: Any):
126 for attrName, value in zip(self.__slots__, args):
127 object.__setattr__(self, attrName, value)
128 from .coordinate import DataCoordinate
129 if self.definition.required.names == self.definition.graph.required.names:
130 dataId = DataCoordinate.fromRequiredValues(
131 self.definition.graph,
132 args[:len(self.definition.required.names)]
133 )
134 else:
135 assert not isinstance(self.definition, Dimension)
136 dataId = DataCoordinate.fromRequiredValues(
137 self.definition.graph,
138 tuple(getattr(self, name) for name in self.definition.required.names)
139 )
140 object.__setattr__(self, "dataId", dataId)
142 @classmethod
143 def fromDict(cls, mapping: Mapping[str, Any]) -> DimensionRecord:
144 """Construct a `DimensionRecord` subclass instance from a mapping
145 of field values.
147 Parameters
148 ----------
149 mapping : `~collections.abc.Mapping`
150 Field values, keyed by name. The keys must match those in
151 ``__slots__``, with the exception that a dimension name
152 may be used in place of the primary key name - for example,
153 "tract" may be used instead of "id" for the "id" primary key
154 field of the "tract" dimension.
156 Returns
157 -------
158 record : `DimensionRecord`
159 An instance of this subclass of `DimensionRecord`.
160 """
161 # If the name of the dimension is present in the given dict, use it
162 # as the primary key value instead of expecting the field name.
163 # For example, allow {"instrument": "HSC", ...} instead of
164 # {"name": "HSC", ...} when building a record for instrument dimension.
165 primaryKey = mapping.get(cls.definition.name)
166 d: Mapping[str, Any]
167 if primaryKey is not None and isinstance(cls.definition, Dimension):
168 d = dict(mapping)
169 d[cls.definition.primaryKey.name] = primaryKey
170 else:
171 d = mapping
172 values = tuple(d.get(k) for k in cls.__slots__)
173 return cls(*values)
175 def __str__(self) -> str:
176 lines = [f"{self.definition.name}:"]
177 lines.extend(f" {field}: {getattr(self, field)!r}" for field in self.fields)
178 return "\n".join(lines)
180 def __repr__(self) -> str:
181 return "{}.RecordClass({})".format(
182 self.definition.name,
183 ", ".join(repr(getattr(self, field)) for field in self.fields)
184 )
186 def __reduce__(self) -> tuple:
187 args = tuple(getattr(self, name) for name in self.__slots__)
188 return (_reconstructDimensionRecord, (self.definition,) + args)
190 def toDict(self) -> Dict[str, Any]:
191 """Return a vanilla `dict` representation of this record.
192 """
193 return {name: getattr(self, name) for name in self.__slots__}
195 # Class attributes below are shadowed by instance attributes, and are
196 # present just to hold the docstrings for those instance attributes.
198 dataId: DataCoordinate
199 """A dict-like identifier for this record's primary keys
200 (`MinimalDataCoordinate`).
201 """
203 definition: ClassVar[DimensionElement]
205 fields: ClassVar[Tuple[str, ...]]