Coverage for python/lsst/daf/butler/dimensions/_database.py: 58%

113 statements  

« prev     ^ index     » next       coverage.py v7.4.3, created at 2024-03-07 11:04 +0000

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 software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

16# it under the terms of the GNU General Public License as published by 

17# the Free Software Foundation, either version 3 of the License, or 

18# (at your option) any later version. 

19# 

20# This program is distributed in the hope that it will be useful, 

21# but WITHOUT ANY WARRANTY; without even the implied warranty of 

22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23# GNU General Public License for more details. 

24# 

25# You should have received a copy of the GNU General Public License 

26# along with this program. If not, see <http://www.gnu.org/licenses/>. 

27 

28from __future__ import annotations 

29 

30__all__ = ( 

31 "DatabaseDimension", 

32 "DatabaseDimensionCombination", 

33 "DatabaseDimensionElement", 

34 "DatabaseTopologicalFamily", 

35) 

36 

37from collections.abc import Iterable, Mapping, Set 

38from types import MappingProxyType 

39from typing import TYPE_CHECKING 

40 

41from lsst.utils.classes import cached_getter 

42 

43from .._named import NamedValueAbstractSet, NamedValueSet 

44from .._topology import TopologicalFamily, TopologicalSpace 

45from ._elements import Dimension, DimensionCombination, DimensionElement, KeyColumnSpec, MetadataColumnSpec 

46from .construction import DimensionConstructionBuilder, DimensionConstructionVisitor 

47 

48if TYPE_CHECKING: 

49 from ._governor import GovernorDimension 

50 from ._universe import DimensionUniverse 

51 

52 

53class DatabaseTopologicalFamily(TopologicalFamily): 

54 """Database topological family implementation. 

55 

56 A `TopologicalFamily` implementation for the `DatabaseDimension` and 

57 `DatabaseDimensionCombination` objects that have direct database 

58 representations. 

59 

60 Parameters 

61 ---------- 

62 name : `str` 

63 Name of the family. 

64 space : `TopologicalSpace` 

65 Space in which this family's regions live. 

66 members : `NamedValueAbstractSet` [ `DimensionElement` ] 

67 The members of this family, ordered according to the priority used 

68 in `choose` (first-choice member first). 

69 """ 

70 

71 def __init__( 

72 self, 

73 name: str, 

74 space: TopologicalSpace, 

75 *, 

76 members: NamedValueAbstractSet[DimensionElement], 

77 ): 

78 super().__init__(name, space) 

79 self.members = members 

80 

81 def choose(self, endpoints: Set[str], universe: DimensionUniverse) -> DimensionElement: 

82 # Docstring inherited from TopologicalFamily. 

83 for member in self.members: 

84 if member.name in endpoints: 

85 return member 

86 raise RuntimeError(f"No recognized endpoints for {self.name} in {endpoints}.") 

87 

88 @property 

89 @cached_getter 

90 def governor(self) -> GovernorDimension: 

91 """Return `GovernorDimension` common to all members of this family. 

92 

93 (`GovernorDimension`). 

94 """ 

95 governors = {m.governor for m in self.members} 

96 if None in governors: 

97 raise RuntimeError( 

98 f"Bad {self.space.name} family definition {self.name}: at least one member " 

99 f"in {self.members} has no GovernorDimension dependency." 

100 ) 

101 try: 

102 (result,) = governors 

103 except ValueError: 

104 raise RuntimeError( 

105 f"Bad {self.space.name} family definition {self.name}: multiple governors {governors} " 

106 f"in {self.members}." 

107 ) from None 

108 return result # type: ignore 

109 

110 members: NamedValueAbstractSet[DimensionElement] 

111 """The members of this family, ordered according to the priority used in 

112 `choose` (first-choice member first). 

113 """ 

114 

115 

116class DatabaseTopologicalFamilyConstructionVisitor(DimensionConstructionVisitor): 

117 """A construction visitor for `DatabaseTopologicalFamily`. 

118 

119 This visitor depends on (and is thus visited after) its members. 

120 

121 Parameters 

122 ---------- 

123 space : `TopologicalSpace` 

124 Space in which this family's regions live. 

125 members : `~collections.abc.Iterable` [ `str` ] 

126 The names of the members of this family, ordered according to the 

127 priority used in `choose` (first-choice member first). 

128 """ 

129 

130 def __init__(self, space: TopologicalSpace, members: Iterable[str]): 

131 self._space = space 

132 self._members = tuple(members) 

133 

134 def has_dependencies_in(self, others: Set[str]) -> bool: 

135 # Docstring inherited from DimensionConstructionVisitor. 

136 return not others.isdisjoint(self._members) 

137 

138 def visit(self, name: str, builder: DimensionConstructionBuilder) -> None: 

139 # Docstring inherited from DimensionConstructionVisitor. 

140 members = NamedValueSet(builder.elements[member_name] for member_name in self._members) 

141 family = DatabaseTopologicalFamily(name, self._space, members=members.freeze()) 

142 builder.topology[self._space].add(family) 

143 for member in members: 

144 assert isinstance(member, DatabaseDimension | DatabaseDimensionCombination) 

145 other = member._topology.setdefault(self._space, family) 

146 if other is not family: 

147 raise RuntimeError( 

148 f"{member.name} is declared to be a member of (at least) two " 

149 f"{self._space.name} families: {other.name} and {family.name}." 

150 ) 

151 

152 

153class DatabaseDimensionElement(DimensionElement): 

154 """An intermediate base class for `DimensionElement` database classes. 

155 

156 Instances of these element classes map directly to a database table or 

157 query. 

158 

159 Parameters 

160 ---------- 

161 name : `str` 

162 Name of the dimension. 

163 implied : `NamedValueAbstractSet` [ `Dimension` ] 

164 Other dimensions whose keys are included in this dimension's (logical) 

165 table as foreign keys. 

166 metadata_columns : `NamedValueAbstractSet` [ `MetadataColumnSpec` ] 

167 Field specifications for all non-key fields in this dimension's table. 

168 is_cached : `bool` 

169 Whether this element's records should be persistently cached in the 

170 client. 

171 doc : `str` 

172 Extended description of this element. 

173 """ 

174 

175 def __init__( 

176 self, 

177 name: str, 

178 *, 

179 implied: NamedValueAbstractSet[Dimension], 

180 metadata_columns: NamedValueAbstractSet[MetadataColumnSpec], 

181 is_cached: bool, 

182 doc: str, 

183 ): 

184 self._name = name 

185 self._implied = implied 

186 self._metadata_columns = metadata_columns 

187 self._topology: dict[TopologicalSpace, DatabaseTopologicalFamily] = {} 

188 self._is_cached = is_cached 

189 self._doc = doc 

190 

191 @property 

192 def name(self) -> str: 

193 # Docstring inherited from TopologicalRelationshipEndpoint. 

194 return self._name 

195 

196 @property 

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

198 # Docstring inherited from DimensionElement. 

199 return self._implied 

200 

201 @property 

202 def metadata_columns(self) -> NamedValueAbstractSet[MetadataColumnSpec]: 

203 # Docstring inherited from DimensionElement. 

204 return self._metadata_columns 

205 

206 @property 

207 def is_cached(self) -> bool: 

208 # Docstring inherited. 

209 return self._is_cached 

210 

211 @property 

212 def documentation(self) -> str: 

213 # Docstring inherited from DimensionElement. 

214 return self._doc 

215 

216 @property 

217 def topology(self) -> Mapping[TopologicalSpace, DatabaseTopologicalFamily]: 

218 # Docstring inherited from TopologicalRelationshipEndpoint 

219 return MappingProxyType(self._topology) 

220 

221 @property 

222 def spatial(self) -> DatabaseTopologicalFamily | None: 

223 # Docstring inherited from TopologicalRelationshipEndpoint 

224 return self.topology.get(TopologicalSpace.SPATIAL) 

225 

226 @property 

227 def temporal(self) -> DatabaseTopologicalFamily | None: 

228 # Docstring inherited from TopologicalRelationshipEndpoint 

229 return self.topology.get(TopologicalSpace.TEMPORAL) 

230 

231 

232class DatabaseDimension(Dimension, DatabaseDimensionElement): 

233 """A `Dimension` class that maps directly to a database table or query. 

234 

235 Parameters 

236 ---------- 

237 name : `str` 

238 Name of the dimension. 

239 required : `NamedValueSet` [ `Dimension` ] 

240 Other dimensions whose keys are part of the compound primary key for 

241 this dimension's (logical) table, as well as references to their own 

242 tables. The ``required`` parameter does not include ``self`` (it 

243 can't, of course), but the corresponding attribute does - it is added 

244 by the constructor. 

245 implied : `NamedValueAbstractSet` [ `Dimension` ] 

246 Other dimensions whose keys are included in this dimension's (logical) 

247 table as foreign keys. 

248 metadata_columns : `NamedValueAbstractSet` [ `MetadataColumnSpec` ] 

249 Field specifications for all non-key fields in this dimension's table. 

250 unique_keys : `NamedValueAbstractSet` [ `KeyColumnSpec` ] 

251 Fields that can each be used to uniquely identify this dimension (given 

252 values for all required dimensions). The first of these is used as 

253 (part of) this dimension's table's primary key, while others are used 

254 to define unique constraints. 

255 implied_union_target : `str` or `None` 

256 If not `None`, the name of an element whose implied values for 

257 this element form the set of allowable values. 

258 is_cached : `bool` 

259 Whether this element's records should be persistently cached in the 

260 client. 

261 doc : `str` 

262 Extended description of this element. 

263 

264 Notes 

265 ----- 

266 `DatabaseDimension` objects may belong to a `TopologicalFamily`, but it is 

267 the responsibility of `DatabaseTopologicalFamilyConstructionVisitor` to 

268 update the `~TopologicalRelationshipEndpoint.topology` attribute of their 

269 members. 

270 """ 

271 

272 def __init__( 

273 self, 

274 name: str, 

275 *, 

276 required: NamedValueSet[Dimension], 

277 implied: NamedValueAbstractSet[Dimension], 

278 metadata_columns: NamedValueAbstractSet[MetadataColumnSpec], 

279 unique_keys: NamedValueAbstractSet[KeyColumnSpec], 

280 implied_union_target: str | None, 

281 is_cached: bool, 

282 doc: str, 

283 ): 

284 super().__init__( 

285 name, implied=implied, metadata_columns=metadata_columns, is_cached=is_cached, doc=doc 

286 ) 

287 required.add(self) 

288 self._required = required.freeze() 

289 self._unique_keys = unique_keys 

290 self._implied_union_target = implied_union_target 

291 

292 @property 

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

294 # Docstring inherited from DimensionElement. 

295 return self._required 

296 

297 @property 

298 def unique_keys(self) -> NamedValueAbstractSet[KeyColumnSpec]: 

299 # Docstring inherited from Dimension. 

300 return self._unique_keys 

301 

302 @property 

303 def implied_union_target(self) -> DimensionElement | None: 

304 # Docstring inherited from DimensionElement. 

305 # This is a bit encapsulation-breaking, but it'll all be cleaned up 

306 # soon when we get rid of the storage objects entirely. 

307 return self.universe[self._implied_union_target] if self._implied_union_target is not None else None 

308 

309 

310class DatabaseDimensionCombination(DimensionCombination, DatabaseDimensionElement): 

311 """A combination class that maps directly to a database table or query. 

312 

313 Parameters 

314 ---------- 

315 name : `str` 

316 Name of the dimension. 

317 required : `NamedValueAbstractSet` [ `Dimension` ] 

318 Dimensions whose keys define the compound primary key for this 

319 combinations's (logical) table, as well as references to their own 

320 tables. 

321 implied : `NamedValueAbstractSet` [ `Dimension` ] 

322 Dimensions whose keys are included in this combinations's (logical) 

323 table as foreign keys. 

324 metadata_columns : `NamedValueAbstractSet` [ `MetadataColumnSpec` ] 

325 Field specifications for all non-key fields in this combination's 

326 table. 

327 is_cached : `bool` 

328 Whether this element's records should be persistently cached in the 

329 client. 

330 always_join : `bool`, optional 

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

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

333 relationship between those dimensions that must always be satisfied. 

334 populated_by : `Dimension` or `None` 

335 The dimension that this element's records are always inserted, 

336 exported, and imported alongside. 

337 doc : `str` 

338 Extended description of this element. 

339 

340 Notes 

341 ----- 

342 `DatabaseDimensionCombination` objects may belong to a `TopologicalFamily`, 

343 but it is the responsibility of 

344 `DatabaseTopologicalFamilyConstructionVisitor` to update the 

345 `~TopologicalRelationshipEndpoint.topology` attribute of their members. 

346 

347 This class has a lot in common with `DatabaseDimension`, but they are 

348 expected to diverge in future changes, and the only way to make them share 

349 method implementations would be via multiple inheritance. Given the 

350 trivial nature of all of those implementations, this does not seem worth 

351 the drawbacks (particularly the constraints it imposes on constructor 

352 signatures). 

353 """ 

354 

355 def __init__( 

356 self, 

357 name: str, 

358 *, 

359 required: NamedValueAbstractSet[Dimension], 

360 implied: NamedValueAbstractSet[Dimension], 

361 metadata_columns: NamedValueAbstractSet[MetadataColumnSpec], 

362 is_cached: bool, 

363 always_join: bool, 

364 populated_by: Dimension | None, 

365 doc: str, 

366 ): 

367 super().__init__( 

368 name, implied=implied, metadata_columns=metadata_columns, is_cached=is_cached, doc=doc 

369 ) 

370 self._required = required 

371 self._always_join = always_join 

372 self._populated_by = populated_by 

373 

374 @property 

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

376 # Docstring inherited from DimensionElement. 

377 return self._required 

378 

379 @property 

380 def alwaysJoin(self) -> bool: 

381 # Docstring inherited from DimensionElement. 

382 return self._always_join 

383 

384 @property 

385 def defines_relationships(self) -> bool: 

386 # Docstring inherited from DimensionElement. 

387 return self._always_join or bool(self.implied) 

388 

389 @property 

390 def populated_by(self) -> Dimension | None: 

391 # Docstring inherited. 

392 return self._populated_by