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 Optional, 

31 TYPE_CHECKING, 

32 Type, 

33) 

34 

35import lsst.sphgeom 

36 

37from .._topology import SpatialRegionDatabaseRepresentation 

38from ..timespan import Timespan, TimespanDatabaseRepresentation 

39from ..utils import immutable 

40from ._elements import Dimension, DimensionElement 

41from ..json import from_json_generic, to_json_generic 

42 

43if TYPE_CHECKING: # Imports needed only for type annotations; may be circular. 43 ↛ 44line 43 didn't jump to line 44, because the condition on line 43 was never true

44 from ._coordinate import DataCoordinate 

45 from ._schema import DimensionElementFields 

46 from ._graph import DimensionUniverse 

47 from ...registry import Registry 

48 

49 

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

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

52 

53 For internal use by `DimensionRecord`. 

54 """ 

55 return definition.RecordClass(**mapping) 

56 

57 

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

59 """Create a dynamic subclass of `DimensionRecord` for the given element. 

60 

61 For internal use by `DimensionRecord`. 

62 """ 

63 from ._schema import DimensionElementFields 

64 fields = DimensionElementFields(definition) 

65 slots = list(fields.standard.names) 

66 if definition.spatial: 

67 slots.append(SpatialRegionDatabaseRepresentation.NAME) 

68 if definition.temporal: 

69 slots.append(TimespanDatabaseRepresentation.NAME) 

70 d = { 

71 "definition": definition, 

72 "__slots__": tuple(slots), 

73 "fields": fields 

74 } 

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

76 

77 

78@immutable 

79class DimensionRecord: 

80 """Base class for the Python representation of database records. 

81 

82 Parameters 

83 ---------- 

84 **kwargs 

85 Field values for this record. Unrecognized keys are ignored. If this 

86 is the record for a `Dimension`, its primary key value may be provided 

87 with the actual name of the field (e.g. "id" or "name"), the name of 

88 the `Dimension`, or both. If this record class has a "timespan" 

89 attribute, "datetime_begin" and "datetime_end" keyword arguments may 

90 be provided instead of a single "timespan" keyword argument (but are 

91 ignored if a "timespan" argument is provided). 

92 

93 Notes 

94 ----- 

95 `DimensionRecord` subclasses are created dynamically for each 

96 `DimensionElement` in a `DimensionUniverse`, and are accessible via the 

97 `DimensionElement.RecordClass` attribute. The `DimensionRecord` base class 

98 itself is pure abstract, but does not use the `abc` module to indicate this 

99 because it does not have overridable methods. 

100 

101 Record classes have attributes that correspond exactly to the 

102 `~DimensionElementFields.standard` fields in the related database table, 

103 plus "region" and "timespan" attributes for spatial and/or temporal 

104 elements (respectively). 

105 

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

107 directly from Python as well. 

108 

109 `DimensionRecord` instances are immutable. 

110 """ 

111 

112 # Derived classes are required to define __slots__ as well, and it's those 

113 # derived-class slots that other methods on the base class expect to see 

114 # when they access self.__slots__. 

115 __slots__ = ("dataId",) 

116 

117 def __init__(self, **kwargs: Any): 

118 # Accept either the dimension name or the actual name of its primary 

119 # key field; ensure both are present in the dict for convenience below. 

120 if isinstance(self.definition, Dimension): 

121 v = kwargs.get(self.definition.primaryKey.name) 

122 if v is None: 

123 v = kwargs.get(self.definition.name) 

124 if v is None: 

125 raise ValueError( 

126 f"No value provided for {self.definition.name}.{self.definition.primaryKey.name}." 

127 ) 

128 kwargs[self.definition.primaryKey.name] = v 

129 else: 

130 v2 = kwargs.setdefault(self.definition.name, v) 

131 if v != v2: 

132 raise ValueError( 

133 f"Multiple inconsistent values for " 

134 f"{self.definition.name}.{self.definition.primaryKey.name}: {v!r} != {v2!r}." 

135 ) 

136 for name in self.__slots__: 

137 object.__setattr__(self, name, kwargs.get(name)) 

138 if self.definition.temporal is not None: 

139 if self.timespan is None: 

140 object.__setattr__( 

141 self, 

142 "timespan", 

143 Timespan( 

144 kwargs.get("datetime_begin"), 

145 kwargs.get("datetime_end"), 

146 ) 

147 ) 

148 

149 from ._coordinate import DataCoordinate 

150 object.__setattr__( 

151 self, 

152 "dataId", 

153 DataCoordinate.fromRequiredValues( 

154 self.definition.graph, 

155 tuple(kwargs[dimension] for dimension in self.definition.required.names) 

156 ) 

157 ) 

158 

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

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

161 return False 

162 return self.dataId == other.dataId 

163 

164 def __hash__(self) -> int: 

165 return hash(self.dataId) 

166 

167 def __str__(self) -> str: 

168 lines = [f"{self.definition.name}:"] 

169 lines.extend(f" {name}: {getattr(self, name)!r}" for name in self.__slots__) 

170 return "\n".join(lines) 

171 

172 def __repr__(self) -> str: 

173 return "{}.RecordClass({})".format( 

174 self.definition.name, 

175 ", ".join(f"{name}={getattr(self, name)!r}" for name in self.__slots__) 

176 ) 

177 

178 def __reduce__(self) -> tuple: 

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

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

181 

182 def to_simple(self, minimal: bool = False) -> Dict[str, Any]: 

183 """Convert this class to a simple python type. 

184 

185 This makes it suitable for serialization. 

186 

187 Parameters 

188 ---------- 

189 minimal : `bool`, optional 

190 Use minimal serialization. Has no effect on for this class. 

191 

192 Returns 

193 ------- 

194 names : `list` 

195 The names of the dimensions. 

196 """ 

197 if minimal: 

198 # The DataId is sufficient if you are willing to do a deferred 

199 # query. This may not be overly useful since to reconstruct 

200 # a collection of records will require repeated registry queries. 

201 simple = self.dataId.to_simple() 

202 # Need some means of indicating this is not a full record 

203 simple["element"] = self.definition.name 

204 return simple 

205 

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

207 # If the item in mapping supports simplification update it 

208 for k, v in mapping.items(): 

209 try: 

210 mapping[k] = v.to_simple(minimal=minimal) 

211 except AttributeError: 

212 if isinstance(v, lsst.sphgeom.Region): 

213 # YAML serialization specifies the class when it 

214 # doesn't have to. This is partly for explicitness 

215 # and also history. Here use a different approach. 

216 # This code needs to be migrated to sphgeom 

217 mapping[k] = {"encoded_region": v.encode().hex()} 

218 definition = self.definition.to_simple(minimal=minimal) 

219 

220 return {"definition": definition, 

221 "record": mapping} 

222 

223 @classmethod 

224 def from_simple(cls, simple: Dict[str, Any], 

225 universe: Optional[DimensionUniverse] = None, 

226 registry: Optional[Registry] = None) -> DimensionRecord: 

227 """Construct a new object from the simplified form. 

228 

229 This is generally data returned from the `to_simple` 

230 method. 

231 

232 Parameters 

233 ---------- 

234 simple : `dict` of `str` 

235 Value return from `to_simple`. 

236 universe : `DimensionUniverse` 

237 The special graph of all known dimensions of which this graph will 

238 be a subset. Can be `None` if `Registry` is provided. 

239 registry : `lsst.daf.butler.Registry`, optional 

240 Registry from which a universe can be extracted. Can be `None` 

241 if universe is provided explicitly. 

242 

243 Returns 

244 ------- 

245 graph : `DimensionGraph` 

246 Newly-constructed object. 

247 """ 

248 # Minimal representation requires a registry 

249 if "element" in simple: 

250 if registry is None: 

251 raise ValueError("Registry is required to decode minimalist form of dimensions record") 

252 element = simple.pop("element") 

253 records = list(registry.queryDimensionRecords(element, dataId=simple)) 

254 if (n := len(records)) != 1: 

255 raise RuntimeError(f"Unexpectedly got {n} records for element {element} dataId {simple}") 

256 return records[0] 

257 

258 if universe is None and registry is None: 

259 raise ValueError("One of universe or registry is required to convert names to a DimensionGraph") 

260 if universe is None and registry is not None: 

261 universe = registry.dimensions 

262 if universe is None: 

263 # this is for mypy 

264 raise ValueError("Unable to determine a usable universe") 

265 

266 definition = DimensionElement.from_simple(simple["definition"], universe=universe) 

267 

268 # Timespan and region have to be converted to native form 

269 # for now assume that those keys are special 

270 rec = simple["record"] 

271 if (ts := "timespan") in rec: 

272 rec[ts] = Timespan.from_simple(rec[ts], universe=universe, registry=registry) 

273 if (reg := "region") in rec: 

274 encoded = bytes.fromhex(rec[reg]["encoded_region"]) 

275 rec[reg] = lsst.sphgeom.Region.decode(encoded) 

276 

277 return _reconstructDimensionRecord(definition, simple["record"]) 

278 

279 to_json = to_json_generic 

280 from_json = classmethod(from_json_generic) 

281 

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

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

284 

285 Parameters 

286 ---------- 

287 splitTimespan : `bool`, optional 

288 If `True` (`False` is default) transform any "timespan" key value 

289 from a `Timespan` instance into a pair of regular 

290 ("datetime_begin", "datetime_end") fields. 

291 """ 

292 results = {name: getattr(self, name) for name in self.__slots__} 

293 if splitTimespan: 

294 timespan = results.pop("timespan", None) 

295 if timespan is not None: 

296 results["datetime_begin"] = timespan.begin 

297 results["datetime_end"] = timespan.end 

298 return results 

299 

300 # DimensionRecord subclasses are dynamically created, so static type 

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

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

303 # checkers not to worry about missing attributes. 

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

305 raise AttributeError(name) 

306 

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

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

309 

310 dataId: DataCoordinate 

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

312 (`DataCoordinate`). 

313 """ 

314 

315 definition: ClassVar[DimensionElement] 

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

317 (`DimensionElement`). 

318 """ 

319 

320 fields: ClassVar[DimensionElementFields] 

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

322 (`DimensionElementFields`). 

323 """