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 

60 `DimensionElement`. 

61 

62 For internal use by `DimensionRecord`. 

63 """ 

64 from ._schema import DimensionElementFields 

65 fields = DimensionElementFields(definition) 

66 slots = list(fields.standard.names) 

67 if definition.spatial: 

68 slots.append(SpatialRegionDatabaseRepresentation.NAME) 

69 if definition.temporal: 

70 slots.append(TimespanDatabaseRepresentation.NAME) 

71 d = { 

72 "definition": definition, 

73 "__slots__": tuple(slots), 

74 "fields": fields 

75 } 

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

77 

78 

79@immutable 

80class DimensionRecord: 

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

82 a `DimensionElement`. 

83 

84 Parameters 

85 ---------- 

86 **kwargs 

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

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

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

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

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

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

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

94 

95 Notes 

96 ----- 

97 `DimensionRecord` subclasses are created dynamically for each 

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

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

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

101 because it does not have overridable methods. 

102 

103 Record classes have attributes that correspond exactly to the 

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

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

106 elements (respectively). 

107 

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

109 directly from Python as well. 

110 

111 `DimensionRecord` instances are immutable. 

112 """ 

113 

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

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

116 # when they access self.__slots__. 

117 __slots__ = ("dataId",) 

118 

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

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

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

122 if isinstance(self.definition, Dimension): 

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

124 if v is None: 

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

126 if v is None: 

127 raise ValueError( 

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

129 ) 

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

131 else: 

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

133 if v != v2: 

134 raise ValueError( 

135 f"Multiple inconsistent values for " 

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

137 ) 

138 for name in self.__slots__: 

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

140 if self.definition.temporal is not None: 

141 if self.timespan is None: 

142 object.__setattr__( 

143 self, 

144 "timespan", 

145 Timespan( 

146 kwargs.get("datetime_begin"), 

147 kwargs.get("datetime_end"), 

148 ) 

149 ) 

150 

151 from ._coordinate import DataCoordinate 

152 object.__setattr__( 

153 self, 

154 "dataId", 

155 DataCoordinate.fromRequiredValues( 

156 self.definition.graph, 

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

158 ) 

159 ) 

160 

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

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

163 return False 

164 return self.dataId == other.dataId 

165 

166 def __hash__(self) -> int: 

167 return hash(self.dataId) 

168 

169 def __str__(self) -> str: 

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

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

172 return "\n".join(lines) 

173 

174 def __repr__(self) -> str: 

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

176 self.definition.name, 

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

178 ) 

179 

180 def __reduce__(self) -> tuple: 

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

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

183 

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

185 """Convert this class to a simple python type suitable for 

186 serialization. 

187 

188 Parameters 

189 ---------- 

190 minimal : `bool`, optional 

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

192 

193 Returns 

194 ------- 

195 names : `list` 

196 The names of the dimensions. 

197 """ 

198 if minimal: 

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

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

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

202 simple = self.dataId.to_simple() 

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

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

205 return simple 

206 

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

208 # If the item in mapping supports simplification update it 

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

210 try: 

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

212 except AttributeError: 

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

214 # YAML serialization specifies the class when it 

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

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

217 # This code needs to be migrated to sphgeom 

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

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

220 

221 return {"definition": definition, 

222 "record": mapping} 

223 

224 @classmethod 

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

226 universe: Optional[DimensionUniverse] = None, 

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

228 """Construct a new object from the data returned from the `to_simple` 

229 method. 

230 

231 Parameters 

232 ---------- 

233 simple : `dict` of `str` 

234 Value return from `to_simple`. 

235 universe : `DimensionUniverse` 

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

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

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

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

240 if universe is provided explicitly. 

241 

242 Returns 

243 ------- 

244 graph : `DimensionGraph` 

245 Newly-constructed object. 

246 """ 

247 # Minimal representation requires a registry 

248 if "element" in simple: 

249 if registry is None: 

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

251 element = simple.pop("element") 

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

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

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

255 return records[0] 

256 

257 if universe is None and registry is None: 

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

259 if universe is None and registry is not None: 

260 universe = registry.dimensions 

261 if universe is None: 

262 # this is for mypy 

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

264 

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

266 

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

268 # for now assume that those keys are special 

269 rec = simple["record"] 

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

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

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

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

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

275 

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

277 

278 to_json = to_json_generic 

279 from_json = classmethod(from_json_generic) 

280 

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

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

283 

284 Parameters 

285 ---------- 

286 splitTimespan : `bool`, optional 

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

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

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

290 """ 

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

292 if splitTimespan: 

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

294 if timespan is not None: 

295 results["datetime_begin"] = timespan.begin 

296 results["datetime_end"] = timespan.end 

297 return results 

298 

299 # DimensionRecord subclasses are dynamically created, so static type 

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

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

302 # checkers not to worry about missing attributes. 

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

304 raise AttributeError(name) 

305 

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

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

308 

309 dataId: DataCoordinate 

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

311 (`DataCoordinate`). 

312 """ 

313 

314 definition: ClassVar[DimensionElement] 

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

316 (`DimensionElement`). 

317 """ 

318 

319 fields: ClassVar[DimensionElementFields] 

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

321 (`DimensionElementFields`). 

322 """