Coverage for python/lsst/daf/butler/core/dimensions/_records.py : 25%

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 TYPE_CHECKING,
31 Type,
32)
34from .._topology import SpatialRegionDatabaseRepresentation
35from ..timespan import Timespan, TimespanDatabaseRepresentation
36from ..utils import immutable
37from ._elements import Dimension, DimensionElement
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 from ._coordinate import DataCoordinate
41 from ._schema import DimensionElementFields
44def _reconstructDimensionRecord(definition: DimensionElement, mapping: Dict[str, Any]) -> DimensionRecord:
45 """Unpickle implementation for `DimensionRecord` subclasses.
47 For internal use by `DimensionRecord`.
48 """
49 return definition.RecordClass(**mapping)
52def _subclassDimensionRecord(definition: DimensionElement) -> Type[DimensionRecord]:
53 """Create a dynamic subclass of `DimensionRecord` for the given
54 `DimensionElement`.
56 For internal use by `DimensionRecord`.
57 """
58 from ._schema import DimensionElementFields
59 fields = DimensionElementFields(definition)
60 slots = list(fields.standard.names)
61 if definition.spatial:
62 slots.append(SpatialRegionDatabaseRepresentation.NAME)
63 if definition.temporal:
64 slots.append(TimespanDatabaseRepresentation.NAME)
65 d = {
66 "definition": definition,
67 "__slots__": tuple(slots),
68 "fields": fields
69 }
70 return type(definition.name + ".RecordClass", (DimensionRecord,), d)
73@immutable
74class DimensionRecord:
75 """Base class for the Python representation of database records for
76 a `DimensionElement`.
78 Parameters
79 ----------
80 **kwargs
81 Field values for this record. Unrecognized keys are ignored. If this
82 is the record for a `Dimension`, its primary key value may be provided
83 with the actual name of the field (e.g. "id" or "name"), the name of
84 the `Dimension`, or both. If this record class has a "timespan"
85 attribute, "datetime_begin" and "datetime_end" keyword arguments may
86 be provided instead of a single "timespan" keyword argument (but are
87 ignored if a "timespan" argument is provided).
89 Notes
90 -----
91 `DimensionRecord` subclasses are created dynamically for each
92 `DimensionElement` in a `DimensionUniverse`, and are accessible via the
93 `DimensionElement.RecordClass` attribute. The `DimensionRecord` base class
94 itself is pure abstract, but does not use the `abc` module to indicate this
95 because it does not have overridable methods.
97 Record classes have attributes that correspond exactly to the
98 `~DimensionElementFields.standard` fields in the related database table,
99 plus "region" and "timespan" attributes for spatial and/or temporal
100 elements (respectively).
102 Instances are usually obtained from a `Registry`, but can be constructed
103 directly from Python as well.
105 `DimensionRecord` instances are immutable.
106 """
108 # Derived classes are required to define __slots__ as well, and it's those
109 # derived-class slots that other methods on the base class expect to see
110 # when they access self.__slots__.
111 __slots__ = ("dataId",)
113 def __init__(self, **kwargs: Any):
114 # Accept either the dimension name or the actual name of its primary
115 # key field; ensure both are present in the dict for convenience below.
116 if isinstance(self.definition, Dimension):
117 v = kwargs.get(self.definition.primaryKey.name)
118 if v is None:
119 v = kwargs.get(self.definition.name)
120 if v is None:
121 raise ValueError(
122 f"No value provided for {self.definition.name}.{self.definition.primaryKey.name}."
123 )
124 kwargs[self.definition.primaryKey.name] = v
125 else:
126 v2 = kwargs.setdefault(self.definition.name, v)
127 if v != v2:
128 raise ValueError(
129 f"Multiple inconsistent values for "
130 f"{self.definition.name}.{self.definition.primaryKey.name}: {v!r} != {v2!r}."
131 )
132 for name in self.__slots__:
133 object.__setattr__(self, name, kwargs.get(name))
134 if self.definition.temporal is not None:
135 if self.timespan is None:
136 object.__setattr__(
137 self,
138 "timespan",
139 Timespan(
140 kwargs.get("datetime_begin"),
141 kwargs.get("datetime_end"),
142 )
143 )
145 from ._coordinate import DataCoordinate
146 object.__setattr__(
147 self,
148 "dataId",
149 DataCoordinate.fromRequiredValues(
150 self.definition.graph,
151 tuple(kwargs[dimension] for dimension in self.definition.required.names)
152 )
153 )
155 def __eq__(self, other: Any) -> bool:
156 if type(other) != type(self):
157 return False
158 return self.dataId == other.dataId
160 def __hash__(self) -> int:
161 return hash(self.dataId)
163 def __str__(self) -> str:
164 lines = [f"{self.definition.name}:"]
165 lines.extend(f" {name}: {getattr(self, name)!r}" for name in self.__slots__)
166 return "\n".join(lines)
168 def __repr__(self) -> str:
169 return "{}.RecordClass({})".format(
170 self.definition.name,
171 ", ".join(f"name={getattr(self, name)!r}" for name in self.__slots__)
172 )
174 def __reduce__(self) -> tuple:
175 mapping = {name: getattr(self, name) for name in self.__slots__}
176 return (_reconstructDimensionRecord, (self.definition, mapping))
178 def toDict(self, splitTimespan: bool = False) -> Dict[str, Any]:
179 """Return a vanilla `dict` representation of this record.
181 Parameters
182 ----------
183 splitTimespan : `bool`, optional
184 If `True` (`False` is default) transform any "timespan" key value
185 from a `Timespan` instance into a pair of regular
186 ("datetime_begin", "datetime_end") fields.
187 """
188 results = {name: getattr(self, name) for name in self.__slots__}
189 if splitTimespan:
190 timespan = results.pop("timespan", None)
191 if timespan is not None:
192 results["datetime_begin"] = timespan.begin
193 results["datetime_end"] = timespan.end
194 return results
196 # DimensionRecord subclasses are dynamically created, so static type
197 # checkers can't know about them or their attributes. To avoid having to
198 # put "type: ignore", everywhere, add a dummy __getattr__ that tells type
199 # checkers not to worry about missing attributes.
200 def __getattr__(self, name: str) -> Any:
201 raise AttributeError(name)
203 # Class attributes below are shadowed by instance attributes, and are
204 # present just to hold the docstrings for those instance attributes.
206 dataId: DataCoordinate
207 """A dict-like identifier for this record's primary keys
208 (`DataCoordinate`).
209 """
211 definition: ClassVar[DimensionElement]
212 """The `DimensionElement` whose records this class represents
213 (`DimensionElement`).
214 """
216 fields: ClassVar[DimensionElementFields]
217 """A categorized view of the fields in this class
218 (`DimensionElementFields`).
219 """