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

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