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__ = ( 

25 "Dimension", 

26 "DimensionCombination", 

27 "DimensionElement", 

28) 

29 

30from abc import abstractmethod 

31 

32from typing import ( 

33 Any, 

34 Optional, 

35 Type, 

36 TYPE_CHECKING, 

37) 

38 

39from ..named import NamedValueAbstractSet, NamedValueSet 

40from ..utils import cached_getter 

41from .. import ddl 

42from .._topology import TopologicalRelationshipEndpoint 

43 

44 

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

46 from ._universe import DimensionUniverse 

47 from ._governor import GovernorDimension 

48 from ._graph import DimensionGraph 

49 from ._records import DimensionRecord 

50 

51 

52class DimensionElement(TopologicalRelationshipEndpoint): 

53 """A named data-organization concept that defines a label and/or metadata 

54 in the dimensions system. 

55 

56 A `DimensionElement` instance typically corresponds to a _logical_ table in 

57 the `Registry`: either an actual database table or a way of generating rows 

58 on-the-fly that can similarly participate in queries. The rows in that 

59 table are represented by instances of a `DimensionRecord` subclass. Most 

60 `DimensionElement` instances are instances of its `Dimension` subclass, 

61 which is used for elements that can be used as data ID keys. 

62 

63 Notes 

64 ----- 

65 `DimensionElement` instances should always be constructed by and retreived 

66 from a `DimensionUniverse`. They are immutable after they are fully 

67 constructed, and should never be copied. 

68 

69 Pickling a `DimensionElement` just records its name and universe; 

70 unpickling one actually just looks up the element via the singleton 

71 dictionary of all universes. This allows pickle to be used to transfer 

72 elements between processes, but only when each process initializes its own 

73 instance of the same `DimensionUniverse`. 

74 """ 

75 

76 def __str__(self) -> str: 

77 return self.name 

78 

79 def __repr__(self) -> str: 

80 return f"{type(self).__name__}({self.name})" 

81 

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

83 try: 

84 return self.name == other.name 

85 except AttributeError: 

86 # TODO: try removing this fallback; it's not really consistent with 

87 # base class intent, and it could be confusing 

88 return self.name == other 

89 

90 def __hash__(self) -> int: 

91 return hash(self.name) 

92 

93 # TODO: try removing comparison operators; DimensionUniverse.sorted should 

94 # be adequate. 

95 

96 def __lt__(self, other: DimensionElement) -> bool: 

97 try: 

98 return self.universe.getElementIndex(self.name) < self.universe.getElementIndex(other.name) 

99 except KeyError: 

100 return NotImplemented 

101 

102 def __le__(self, other: DimensionElement) -> bool: 

103 try: 

104 return self.universe.getElementIndex(self.name) <= self.universe.getElementIndex(other.name) 

105 except KeyError: 

106 return NotImplemented 

107 

108 def __gt__(self, other: DimensionElement) -> bool: 

109 try: 

110 return self.universe.getElementIndex(self.name) > self.universe.getElementIndex(other.name) 

111 except KeyError: 

112 return NotImplemented 

113 

114 def __ge__(self, other: DimensionElement) -> bool: 

115 try: 

116 return self.universe.getElementIndex(self.name) >= self.universe.getElementIndex(other.name) 

117 except KeyError: 

118 return NotImplemented 

119 

120 @classmethod 

121 def _unpickle(cls, universe: DimensionUniverse, name: str) -> DimensionElement: 

122 """Callable used for unpickling. 

123 

124 For internal use only. 

125 """ 

126 return universe[name] 

127 

128 def __reduce__(self) -> tuple: 

129 return (self._unpickle, (self.universe, self.name)) 

130 

131 def __deepcopy__(self, memo: dict) -> DimensionElement: 

132 # DimensionElement is recursively immutable; see note in @immutable 

133 # decorator. 

134 return self 

135 

136 def hasTable(self) -> bool: 

137 """Return `True` if this element is associated with a table 

138 (even if that table "belongs" to another element). 

139 """ 

140 return True 

141 

142 universe: DimensionUniverse 

143 """The universe of all compatible dimensions with which this element is 

144 associated (`DimensionUniverse`). 

145 """ 

146 

147 @property # type: ignore 

148 @cached_getter 

149 def governor(self) -> Optional[GovernorDimension]: 

150 """The `GovernorDimension` that is a required dependency of this 

151 element, or `None` if there is no such dimension (`GovernorDimension` 

152 or `None`). 

153 """ 

154 if len(self.graph.governors) == 1: 

155 (result,) = self.graph.governors 

156 return result 

157 elif len(self.graph.governors) > 1: 

158 raise RuntimeError( 

159 f"Dimension element {self.name} has multiple governors: {self.graph.governors}." 

160 ) 

161 else: 

162 return None 

163 

164 @property 

165 @abstractmethod 

166 def required(self) -> NamedValueAbstractSet[Dimension]: 

167 """Dimensions that are necessary to uniquely identify a record of this 

168 dimension element. 

169 

170 For elements with a database representation, these dimension are 

171 exactly those used to form the (possibly compound) primary key, and all 

172 dimensions here that are not ``self`` are also used to form foreign 

173 keys. 

174 

175 For `Dimension` instances, this should be exactly the same as 

176 ``graph.required``, but that may not be true for `DimensionElement` 

177 instances in general. When they differ, there are multiple 

178 combinations of dimensions that uniquely identify this element, but 

179 this one is more direct. 

180 """ 

181 raise NotImplementedError() 

182 

183 @property 

184 @abstractmethod 

185 def implied(self) -> NamedValueAbstractSet[Dimension]: 

186 """Other dimensions that are uniquely identified directly by a record 

187 of this dimension element. 

188 

189 For elements with a database representation, these are exactly the 

190 dimensions used to form foreign key constraints whose fields are not 

191 (wholly) also part of the primary key. 

192 

193 Unlike ``self.graph.implied``, this set is not expanded recursively. 

194 """ 

195 raise NotImplementedError() 

196 

197 @property # type: ignore 

198 @cached_getter 

199 def dimensions(self) -> NamedValueAbstractSet[Dimension]: 

200 """The union of `required` and `implied`, with all elements in 

201 `required` before any elements in `implied`. 

202 

203 This differs from ``self.graph.dimensions`` both in order and in 

204 content: 

205 

206 - as in ``self.implied``, implied dimensions are not expanded 

207 recursively here; 

208 - implied dimensions appear after required dimensions here, instead of 

209 being topologically ordered. 

210 

211 As a result, this set is ordered consistently with 

212 ``self.RecordClass.fields``. 

213 """ 

214 return NamedValueSet(list(self.required) + list(self.implied)).freeze() 

215 

216 @property # type: ignore 

217 @cached_getter 

218 def graph(self) -> DimensionGraph: 

219 """Minimal graph that includes this element (`DimensionGraph`). 

220 

221 ``self.graph.required`` includes all dimensions whose primary key 

222 values are sufficient (often necessary) to uniquely identify ``self`` 

223 (including ``self`` if ``isinstance(self, Dimension)``. 

224 ``self.graph.implied`` includes all dimensions also identified 

225 (possibly recursively) by this set. 

226 """ 

227 return self.universe.extract(self.dimensions.names) 

228 

229 @property # type: ignore 

230 @cached_getter 

231 def RecordClass(self) -> Type[DimensionRecord]: 

232 """The `DimensionRecord` subclass used to hold records for this element 

233 (`type`). 

234 

235 Because `DimensionRecord` subclasses are generated dynamically, this 

236 type cannot be imported directly and hence can only be obtained from 

237 this attribute. 

238 """ 

239 from ._records import _subclassDimensionRecord 

240 return _subclassDimensionRecord(self) 

241 

242 @property 

243 @abstractmethod 

244 def metadata(self) -> NamedValueAbstractSet[ddl.FieldSpec]: 

245 """Additional metadata fields included in this element's table 

246 (`NamedValueSet` of `FieldSpec`). 

247 """ 

248 raise NotImplementedError() 

249 

250 @property 

251 def viewOf(self) -> Optional[str]: 

252 """Name of another table this element's records are drawn from (`str` 

253 or `None`). 

254 """ 

255 return None 

256 

257 @property 

258 def alwaysJoin(self) -> bool: 

259 """If `True`, always include this element in any query or data ID in 

260 which its ``required`` dimensions appear, because it defines a 

261 relationship between those dimensions that must always be satisfied. 

262 """ 

263 return False 

264 

265 

266class Dimension(DimensionElement): 

267 """A named data-organization concept that can be used as a key in a data 

268 ID. 

269 """ 

270 

271 @property 

272 @abstractmethod 

273 def uniqueKeys(self) -> NamedValueAbstractSet[ddl.FieldSpec]: 

274 """All fields that can individually be used to identify records of this 

275 element, given the primary keys of all required dependencies 

276 (`NamedValueAbstractSet` of `FieldSpec`). 

277 """ 

278 raise NotImplementedError() 

279 

280 @property # type: ignore 

281 @cached_getter 

282 def primaryKey(self) -> ddl.FieldSpec: 

283 """The primary key field for this dimension (`FieldSpec`). 

284 

285 Note that the database primary keys for dimension tables are in general 

286 compound; this field is the only field in the database primary key that 

287 is not also a foreign key (to a required dependency dimension table). 

288 """ 

289 primaryKey, *_ = self.uniqueKeys 

290 return primaryKey 

291 

292 @property # type: ignore 

293 @cached_getter 

294 def alternateKeys(self) -> NamedValueAbstractSet[ddl.FieldSpec]: 

295 """Additional unique key fields for this dimension that are not the 

296 primary key (`NamedValueAbstractSet` of `FieldSpec`). 

297 

298 If this dimension has required dependencies, the keys of those 

299 dimensions are also included in the unique constraints defined for 

300 these alternate keys. 

301 """ 

302 _, *alternateKeys = self.uniqueKeys 

303 return NamedValueSet(alternateKeys).freeze() 

304 

305 

306class DimensionCombination(DimensionElement): 

307 """A `DimensionElement` that provides extra metadata and/or relationship 

308 endpoint information for a combination of dimensions. 

309 """ 

310 pass