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

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 .elements import Dimension
35from ..timespan import Timespan, DatabaseTimespanRepresentation
37if TYPE_CHECKING: # Imports needed only for type annotations; may be circular. 37 ↛ 38line 37 didn't jump to line 38, because the condition on line 37 was never true
38 from .elements import DimensionElement
39 from .coordinate import DataCoordinate
40 from .schema import DimensionElementFields
43def _reconstructDimensionRecord(definition: DimensionElement, mapping: Dict[str, Any]) -> DimensionRecord:
44 """Unpickle implementation for `DimensionRecord` subclasses.
46 For internal use by `DimensionRecord`.
47 """
48 return definition.RecordClass(**mapping)
51def _subclassDimensionRecord(definition: DimensionElement) -> Type[DimensionRecord]:
52 """Create a dynamic subclass of `DimensionRecord` for the given
53 `DimensionElement`.
55 For internal use by `DimensionRecord`.
56 """
57 from .schema import DimensionElementFields, REGION_FIELD_SPEC
58 fields = DimensionElementFields(definition)
59 slots = list(fields.standard.names)
60 if definition.spatial:
61 slots.append(REGION_FIELD_SPEC.name)
62 if definition.temporal:
63 slots.append(DatabaseTimespanRepresentation.NAME)
64 d = {
65 "definition": definition,
66 "__slots__": tuple(slots),
67 "fields": fields
68 }
69 return type(definition.name + ".RecordClass", (DimensionRecord,), d)
72class DimensionRecord:
73 """Base class for the Python representation of database records for
74 a `DimensionElement`.
76 Parameters
77 ----------
78 **kwargs
79 Field values for this record. Unrecognized keys are ignored. If this
80 is the record for a `Dimension`, its primary key value may be provided
81 with the actual name of the field (e.g. "id" or "name"), the name of
82 the `Dimension`, or both. If this record class has a "timespan"
83 attribute, "datetime_begin" and "datetime_end" keyword arguments may
84 be provided instead of a single "timespan" keyword argument (but are
85 ignored if a "timespan" argument is provided).
87 Notes
88 -----
89 `DimensionRecord` subclasses are created dynamically for each
90 `DimensionElement` in a `DimensionUniverse`, and are accessible via the
91 `DimensionElement.RecordClass` attribute. The `DimensionRecord` base class
92 itself is pure abstract, but does not use the `abc` module to indicate this
93 because it does not have overridable methods.
95 Record classes have attributes that correspond exactly to the
96 `~DimensionElementFields.standard` fields in the related database table,
97 plus "region" and "timespan" attributes for spatial and/or temporal
98 elements (respectively).
100 Instances are usually obtained from a `Registry`, but can be constructed
101 directly from Python as well.
103 `DimensionRecord` instances are immutable.
104 """
106 # Derived classes are required to define __slots__ as well, and it's those
107 # derived-class slots that other methods on the base class expect to see
108 # when they access self.__slots__.
109 __slots__ = ("dataId",)
111 def __init__(self, **kwargs: Any):
112 # Accept either the dimension name or the actual name of its primary
113 # key field; ensure both are present in the dict for convenience below.
114 if isinstance(self.definition, Dimension):
115 v = kwargs.get(self.definition.primaryKey.name)
116 if v is None:
117 v = kwargs.get(self.definition.name)
118 if v is None:
119 raise ValueError(
120 f"No value provided for {self.definition.name}.{self.definition.primaryKey.name}."
121 )
122 kwargs[self.definition.primaryKey.name] = v
123 else:
124 v2 = kwargs.setdefault(self.definition.name, v)
125 if v != v2:
126 raise ValueError(
127 f"Multiple inconsistent values for "
128 f"{self.definition.name}.{self.definition.primaryKey.name}: {v!r} != {v2!r}."
129 )
130 for name in self.__slots__:
131 object.__setattr__(self, name, kwargs.get(name))
132 if self.definition.temporal is not None:
133 if self.timespan is None: # type: ignore
134 self.timespan = Timespan(
135 kwargs.get("datetime_begin"),
136 kwargs.get("datetime_end"),
137 )
139 from .coordinate import DataCoordinate
140 object.__setattr__(
141 self,
142 "dataId",
143 DataCoordinate.fromRequiredValues(
144 self.definition.graph,
145 tuple(kwargs[dimension] for dimension in self.definition.required.names)
146 )
147 )
149 def __eq__(self, other: Any) -> bool:
150 if type(other) != type(self):
151 return False
152 return self.dataId == other.dataId
154 def __hash__(self) -> int:
155 return hash(self.dataId)
157 def __str__(self) -> str:
158 lines = [f"{self.definition.name}:"]
159 lines.extend(f" {name}: {getattr(self, name)!r}" for name in self.__slots__)
160 return "\n".join(lines)
162 def __repr__(self) -> str:
163 return "{}.RecordClass({})".format(
164 self.definition.name,
165 ", ".join(repr(getattr(self, name)) for name in self.__slots__)
166 )
168 def __reduce__(self) -> tuple:
169 mapping = {name: getattr(self, name) for name in self.__slots__}
170 return (_reconstructDimensionRecord, (self.definition, mapping))
172 def toDict(self, splitTimespan: bool = False) -> Dict[str, Any]:
173 """Return a vanilla `dict` representation of this record.
175 Parameters
176 ----------
177 splitTimespan : `bool`, optional
178 If `True` (`False` is default) transform any "timespan" key value
179 from a `Timespan` instance into a pair of regular
180 ("datetime_begin", "datetime_end") fields.
181 """
182 results = {name: getattr(self, name) for name in self.__slots__}
183 if splitTimespan:
184 timespan = results.pop("timespan", None)
185 if timespan is not None:
186 results["datetime_begin"] = timespan.begin
187 results["datetime_end"] = timespan.end
188 return results
190 # Class attributes below are shadowed by instance attributes, and are
191 # present just to hold the docstrings for those instance attributes.
193 dataId: DataCoordinate
194 """A dict-like identifier for this record's primary keys
195 (`DataCoordinate`).
196 """
198 definition: ClassVar[DimensionElement]
199 """The `DimensionElement` whose records this class represents
200 (`DimensionElement`).
201 """
203 fields: ClassVar[DimensionElementFields]
204 """A categorized view of the fields in this class
205 (`DimensionElementFields`).
206 """