Hide keyboard shortcuts

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/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ("DimensionRecord",) 

25 

26from typing import ( 

27 Any, 

28 ClassVar, 

29 Dict, 

30 TYPE_CHECKING, 

31 Type, 

32) 

33 

34from .elements import Dimension 

35from ..timespan import Timespan, DatabaseTimespanRepresentation 

36 

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 

41 

42 

43def _reconstructDimensionRecord(definition: DimensionElement, mapping: Dict[str, Any]) -> DimensionRecord: 

44 """Unpickle implementation for `DimensionRecord` subclasses. 

45 

46 For internal use by `DimensionRecord`. 

47 """ 

48 return definition.RecordClass(**mapping) 

49 

50 

51def _subclassDimensionRecord(definition: DimensionElement) -> Type[DimensionRecord]: 

52 """Create a dynamic subclass of `DimensionRecord` for the given 

53 `DimensionElement`. 

54 

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) 

70 

71 

72class DimensionRecord: 

73 """Base class for the Python representation of database records for 

74 a `DimensionElement`. 

75 

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). 

86 

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. 

94 

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). 

99 

100 Instances are usually obtained from a `Registry`, but can be constructed 

101 directly from Python as well. 

102 

103 `DimensionRecord` instances are immutable. 

104 """ 

105 

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",) 

110 

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 ) 

138 

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 ) 

148 

149 def __eq__(self, other: Any) -> bool: 

150 if type(other) != type(self): 

151 return False 

152 return self.dataId == other.dataId 

153 

154 def __hash__(self) -> int: 

155 return hash(self.dataId) 

156 

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) 

161 

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 ) 

167 

168 def __reduce__(self) -> tuple: 

169 mapping = {name: getattr(self, name) for name in self.__slots__} 

170 return (_reconstructDimensionRecord, (self.definition, mapping)) 

171 

172 def toDict(self, splitTimespan: bool = False) -> Dict[str, Any]: 

173 """Return a vanilla `dict` representation of this record. 

174 

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 

189 

190 # Class attributes below are shadowed by instance attributes, and are 

191 # present just to hold the docstrings for those instance attributes. 

192 

193 dataId: DataCoordinate 

194 """A dict-like identifier for this record's primary keys 

195 (`DataCoordinate`). 

196 """ 

197 

198 definition: ClassVar[DimensionElement] 

199 """The `DimensionElement` whose records this class represents 

200 (`DimensionElement`). 

201 """ 

202 

203 fields: ClassVar[DimensionElementFields] 

204 """A categorized view of the fields in this class 

205 (`DimensionElementFields`). 

206 """