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 ..timespan import Timespan, TimespanDatabaseRepresentation 

35from ..utils import immutable 

36from ._elements import Dimension, DimensionElement 

37 

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 

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(TimespanDatabaseRepresentation.NAME) 

64 d = { 

65 "definition": definition, 

66 "__slots__": tuple(slots), 

67 "fields": fields 

68 } 

69 return type(definition.name + ".RecordClass", (DimensionRecord,), d) 

70 

71 

72@immutable 

73class DimensionRecord: 

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

75 a `DimensionElement`. 

76 

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

87 

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. 

95 

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

100 

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

102 directly from Python as well. 

103 

104 `DimensionRecord` instances are immutable. 

105 """ 

106 

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

111 

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: 

135 object.__setattr__( 

136 self, 

137 "timespan", 

138 Timespan( 

139 kwargs.get("datetime_begin"), 

140 kwargs.get("datetime_end"), 

141 ) 

142 ) 

143 

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 ) 

153 

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

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

156 return False 

157 return self.dataId == other.dataId 

158 

159 def __hash__(self) -> int: 

160 return hash(self.dataId) 

161 

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) 

166 

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 ) 

172 

173 def __reduce__(self) -> tuple: 

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

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

176 

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

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

179 

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 

194 

195 # DimensionRecord subclasses are dynamically created, so static type 

196 # checkers can't know about them or their attributes. To avoid having to 

197 # put "type: ignore", everywhere, add a dummy __getattr__ that tells type 

198 # checkers not to worry about missing attributes. 

199 def __getattr__(self, name: str) -> Any: 

200 raise AttributeError(name) 

201 

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

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

204 

205 dataId: DataCoordinate 

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

207 (`DataCoordinate`). 

208 """ 

209 

210 definition: ClassVar[DimensionElement] 

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

212 (`DimensionElement`). 

213 """ 

214 

215 fields: ClassVar[DimensionElementFields] 

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

217 (`DimensionElementFields`). 

218 """