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 .._topology import SpatialRegionDatabaseRepresentation 

35from ..timespan import Timespan, TimespanDatabaseRepresentation 

36from ..utils import immutable 

37from ._elements import Dimension, DimensionElement 

38 

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 

42 

43 

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

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

46 

47 For internal use by `DimensionRecord`. 

48 """ 

49 return definition.RecordClass(**mapping) 

50 

51 

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

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

54 `DimensionElement`. 

55 

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) 

71 

72 

73@immutable 

74class DimensionRecord: 

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

76 a `DimensionElement`. 

77 

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

88 

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. 

96 

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

101 

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

103 directly from Python as well. 

104 

105 `DimensionRecord` instances are immutable. 

106 """ 

107 

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

112 

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 ) 

144 

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 ) 

154 

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

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

157 return False 

158 return self.dataId == other.dataId 

159 

160 def __hash__(self) -> int: 

161 return hash(self.dataId) 

162 

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) 

167 

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 ) 

173 

174 def __reduce__(self) -> tuple: 

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

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

177 

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

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

180 

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 

195 

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) 

202 

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

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

205 

206 dataId: DataCoordinate 

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

208 (`DataCoordinate`). 

209 """ 

210 

211 definition: ClassVar[DimensionElement] 

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

213 (`DimensionElement`). 

214 """ 

215 

216 fields: ClassVar[DimensionElementFields] 

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

218 (`DimensionElementFields`). 

219 """