Coverage for python/lsst/daf/butler/dimensions/_config.py: 88%

228 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-22 17:01 +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__ = ("DimensionConfig", "SerializedDimensionConfig") 

31 

32import textwrap 

33from collections.abc import Mapping, Sequence, Set 

34from typing import Any, ClassVar, Literal, Union, final 

35 

36import pydantic 

37 

38from lsst.resources import ResourcePath, ResourcePathExpression 

39from lsst.sphgeom import PixelizationABC 

40from lsst.utils.doImport import doImportType 

41 

42from .._config import Config, ConfigSubset 

43from .._named import NamedValueSet 

44from .._topology import TopologicalSpace 

45from ._database import DatabaseTopologicalFamilyConstructionVisitor 

46from ._elements import Dimension, KeyColumnSpec, MetadataColumnSpec 

47from .construction import DimensionConstructionBuilder, DimensionConstructionVisitor 

48 

49# The default namespace to use on older dimension config files that only 

50# have a version. 

51_DEFAULT_NAMESPACE = "daf_butler" 

52 

53 

54class DimensionConfig(ConfigSubset): 

55 """Configuration that defines a `DimensionUniverse`. 

56 

57 The configuration tree for dimensions is a (nested) dictionary 

58 with five top-level entries: 

59 

60 - version: an integer version number, used as keys in a singleton registry 

61 of all `DimensionUniverse` instances; 

62 

63 - namespace: a string to be associated with the version in the singleton 

64 registry of all `DimensionUnivers` instances; 

65 

66 - skypix: a dictionary whose entries each define a `SkyPixSystem`, 

67 along with a special "common" key whose value is the name of a skypix 

68 dimension that is used to relate all other spatial dimensions in the 

69 `Registry` database; 

70 

71 - elements: a nested dictionary whose entries each define 

72 `StandardDimension` or `StandardDimensionCombination`. 

73 

74 - topology: a nested dictionary with ``spatial`` and ``temporal`` keys, 

75 with dictionary values that each define a `StandardTopologicalFamily`. 

76 

77 - packers: ignored. 

78 

79 See the documentation for the linked classes above for more information 

80 on the configuration syntax. 

81 

82 Parameters 

83 ---------- 

84 other : `Config` or `str` or `dict`, optional 

85 Argument specifying the configuration information as understood 

86 by `Config`. If `None` is passed then defaults are loaded from 

87 "dimensions.yaml", otherwise defaults are not loaded. 

88 validate : `bool`, optional 

89 If `True` required keys will be checked to ensure configuration 

90 consistency. 

91 searchPaths : `list` or `tuple`, optional 

92 Explicit additional paths to search for defaults. They should 

93 be supplied in priority order. These paths have higher priority 

94 than those read from the environment in 

95 `ConfigSubset.defaultSearchPaths()`. Paths can be `str` referring to 

96 the local file system or URIs, `lsst.resources.ResourcePath`. 

97 """ 

98 

99 requiredKeys = ("version", "elements", "skypix") 

100 defaultConfigFile = "dimensions.yaml" 

101 

102 def __init__( 

103 self, 

104 other: Config | ResourcePathExpression | Mapping[str, Any] | None = None, 

105 validate: bool = True, 

106 searchPaths: Sequence[ResourcePathExpression] | None = None, 

107 ): 

108 # if argument is not None then do not load/merge defaults 

109 mergeDefaults = other is None 

110 super().__init__(other=other, validate=validate, mergeDefaults=mergeDefaults, searchPaths=searchPaths) 

111 

112 def _updateWithConfigsFromPath( 

113 self, searchPaths: Sequence[str | ResourcePath], configFile: ResourcePath | str 

114 ) -> None: 

115 """Search the supplied paths reading config from first found. 

116 

117 Raises 

118 ------ 

119 FileNotFoundError 

120 Raised if config file is not found in any of given locations. 

121 

122 Notes 

123 ----- 

124 This method overrides base class method with different behavior. 

125 Instead of merging all found files into a single configuration it 

126 finds first matching file and reads it. 

127 """ 

128 uri = ResourcePath(configFile) 

129 if uri.isabs() and uri.exists(): 129 ↛ 131line 129 didn't jump to line 131 because the condition on line 129 was never true

130 # Assume this resource exists 

131 self._updateWithOtherConfigFile(configFile) 

132 self.filesRead.append(configFile) 

133 else: 

134 for pathDir in searchPaths: 134 ↛ 145line 134 didn't jump to line 145 because the loop on line 134 didn't complete

135 if isinstance(pathDir, str | ResourcePath): 135 ↛ 143line 135 didn't jump to line 143 because the condition on line 135 was always true

136 pathDir = ResourcePath(pathDir, forceDirectory=True) 

137 file = pathDir.join(configFile) 

138 if file.exists(): 138 ↛ 134line 138 didn't jump to line 134 because the condition on line 138 was always true

139 self.filesRead.append(file) 

140 self._updateWithOtherConfigFile(file) 

141 break 

142 else: 

143 raise TypeError(f"Unexpected search path type encountered: {pathDir!r}") 

144 else: 

145 raise FileNotFoundError(f"Could not find {configFile} in search path {searchPaths}") 

146 

147 def _updateWithOtherConfigFile(self, file: Config | str | ResourcePath | Mapping[str, Any]) -> None: 

148 """Override for base class method. 

149 

150 Parameters 

151 ---------- 

152 file : `Config`, `str`, `lsst.resources.ResourcePath`, or `dict` 

153 Entity that can be converted to a `ConfigSubset`. 

154 """ 

155 # Use this class to read the defaults so that subsetting can happen 

156 # correctly. 

157 externalConfig = type(self)(file, validate=False) 

158 self.update(externalConfig) 

159 

160 def to_simple(self) -> SerializedDimensionConfig: 

161 """Convert this configuration to a serializable Pydantic model. 

162 

163 Returns 

164 ------- 

165 model : `SerializedDimensionConfig` 

166 Serializable Pydantic version of this configuration. 

167 """ 

168 return SerializedDimensionConfig.model_validate(self.toDict()) 

169 

170 @staticmethod 

171 def from_simple(simple: SerializedDimensionConfig) -> DimensionConfig: 

172 """Load the configuration from a serialized version. 

173 

174 Parameters 

175 ---------- 

176 simple : `SerializedDimensionConfig` 

177 Serialized configuration to be loaded. 

178 

179 Returns 

180 ------- 

181 config : `DimensionConfig` 

182 Dimension configuration. 

183 """ 

184 return DimensionConfig( 

185 simple.model_dump( 

186 # Force sets back to lists for storage in the config. 

187 # The config does not do this sanitation itself and so 

188 # without this the config can not be serialized to JSON 

189 # form using the dump() method. 

190 mode="json", 

191 # Some of the fields in Pydantic model config have aliases 

192 # (e.g. remapping 'class_' to 'class'). Pydantic ignores these 

193 # in model_dump() by default, so we have to add by_alias to 

194 # make sure that we end up with the right names in the dict. 

195 by_alias=True, 

196 ) 

197 ) 

198 

199 def makeBuilder(self) -> DimensionConstructionBuilder: 

200 """Construct a `DimensionConstructionBuilder`. 

201 

202 The builder will reflect this configuration. 

203 

204 Returns 

205 ------- 

206 builder : `DimensionConstructionBuilder` 

207 A builder object populated with all visitors from this 

208 configuration. The `~DimensionConstructionBuilder.finish` method 

209 will not have been called. 

210 """ 

211 validated = self.to_simple() 

212 builder = DimensionConstructionBuilder( 

213 validated.version, 

214 validated.skypix.common, 

215 self, 

216 namespace=validated.namespace, 

217 ) 

218 for system_name, system_config in sorted(validated.skypix.systems.items()): 

219 builder.add(system_name, system_config) 

220 for element_name, element_config in validated.elements.items(): 

221 builder.add(element_name, element_config) 

222 for family_name, members in validated.topology.spatial.items(): 

223 builder.add( 

224 family_name, 

225 DatabaseTopologicalFamilyConstructionVisitor(space=TopologicalSpace.SPATIAL, members=members), 

226 ) 

227 for family_name, members in validated.topology.temporal.items(): 

228 builder.add( 

229 family_name, 

230 DatabaseTopologicalFamilyConstructionVisitor( 

231 space=TopologicalSpace.TEMPORAL, members=members 

232 ), 

233 ) 

234 return builder 

235 

236 

237@final 

238class _SkyPixSystemConfig(pydantic.BaseModel, DimensionConstructionVisitor): 

239 """Description of a hierarchical sky pixelization system in dimension 

240 universe configuration. 

241 """ 

242 

243 class_: str = pydantic.Field( 

244 alias="class", 

245 description="Fully-qualified name of an `lsst.sphgeom.PixelizationABC implementation.", 

246 ) 

247 

248 min_level: int = 1 

249 """Minimum level for this pixelization.""" 

250 

251 max_level: int | None 

252 """Maximum level for this pixelization.""" 

253 

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

255 # Docstring inherited from DimensionConstructionVisitor. 

256 return False 

257 

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

259 # Docstring inherited from DimensionConstructionVisitor. 

260 PixelizationClass = doImportType(self.class_) 

261 assert issubclass(PixelizationClass, PixelizationABC) 

262 if self.max_level is None: 262 ↛ 263line 262 didn't jump to line 263 because the condition on line 262 was never true

263 max_level: int | None = getattr(PixelizationClass, "MAX_LEVEL", None) 

264 if max_level is None: 

265 raise TypeError( 

266 f"Skypix pixelization class {self.class_} does" 

267 " not have MAX_LEVEL but no max level has been set explicitly." 

268 ) 

269 self.max_level = max_level 

270 

271 from ._skypix import SkyPixSystem 

272 

273 system = SkyPixSystem( 

274 name, 

275 maxLevel=self.max_level, 

276 PixelizationClass=PixelizationClass, 

277 ) 

278 builder.topology[TopologicalSpace.SPATIAL].add(system) 

279 for level in range(self.min_level, self.max_level + 1): 

280 dimension = system[level] 

281 builder.dimensions.add(dimension) 

282 builder.elements.add(dimension) 

283 

284 

285@final 

286class _SkyPixSectionConfig(pydantic.BaseModel): 

287 """Section of the dimension universe configuration that describes sky 

288 pixelizations. 

289 """ 

290 

291 common: str = pydantic.Field( 

292 description="Name of the dimension used to relate all other spatial dimensions." 

293 ) 

294 

295 systems: dict[str, _SkyPixSystemConfig] = pydantic.Field( 

296 default_factory=dict, description="Descriptions of the supported sky pixelization systems." 

297 ) 

298 

299 model_config = pydantic.ConfigDict(extra="allow") 

300 

301 @pydantic.model_validator(mode="after") 

302 def _move_extra_to_systems(self) -> _SkyPixSectionConfig: 

303 """Reinterpret extra fields in this model as members of the `systems` 

304 dictionary. 

305 """ 

306 if self.__pydantic_extra__ is None: 306 ↛ 307line 306 didn't jump to line 307 because the condition on line 306 was never true

307 self.__pydantic_extra__ = {} 

308 for name, data in self.__pydantic_extra__.items(): 

309 self.systems[name] = _SkyPixSystemConfig.model_validate(data) 

310 self.__pydantic_extra__.clear() 

311 return self 

312 

313 

314@final 

315class _TopologySectionConfig(pydantic.BaseModel): 

316 """Section of the dimension universe configuration that describes spatial 

317 and temporal relationships. 

318 """ 

319 

320 spatial: dict[str, list[str]] = pydantic.Field( 

321 default_factory=dict, 

322 description=textwrap.dedent( 

323 """\ 

324 Dictionary of spatial dimension elements, grouped by the "family" 

325 they belong to. 

326 

327 Elements in a family are ordered from fine-grained to coarse-grained. 

328 """ 

329 ), 

330 ) 

331 

332 temporal: dict[str, list[str]] = pydantic.Field( 

333 default_factory=dict, 

334 description=textwrap.dedent( 

335 """\ 

336 Dictionary of temporal dimension elements, grouped by the "family" 

337 they belong to. 

338 

339 Elements in a family are ordered from fine-grained to coarse-grained. 

340 """ 

341 ), 

342 ) 

343 

344 

345@final 

346class _LegacyGovernorDimensionStorage(pydantic.BaseModel): 

347 """Legacy storage configuration for governor dimensions.""" 

348 

349 cls: Literal["lsst.daf.butler.registry.dimensions.governor.BasicGovernorDimensionRecordStorage"] = ( 

350 "lsst.daf.butler.registry.dimensions.governor.BasicGovernorDimensionRecordStorage" 

351 ) 

352 

353 has_own_table: ClassVar[Literal[True]] = True 

354 """Whether this dimension needs a database table to be defined.""" 

355 

356 is_cached: ClassVar[Literal[True]] = True 

357 """Whether this dimension's records should be cached in clients.""" 

358 

359 implied_union_target: ClassVar[Literal[None]] = None 

360 """Name of another dimension that implies this one, whose values for this 

361 dimension define the set of allowed values for this dimension. 

362 """ 

363 

364 

365@final 

366class _LegacyTableDimensionStorage(pydantic.BaseModel): 

367 """Legacy storage configuration for regular dimension tables stored in the 

368 database. 

369 """ 

370 

371 cls: Literal["lsst.daf.butler.registry.dimensions.table.TableDimensionRecordStorage"] = ( 

372 "lsst.daf.butler.registry.dimensions.table.TableDimensionRecordStorage" 

373 ) 

374 

375 has_own_table: ClassVar[Literal[True]] = True 

376 """Whether this dimension element needs a database table to be defined.""" 

377 

378 is_cached: ClassVar[Literal[False]] = False 

379 """Whether this dimension element's records should be cached in clients.""" 

380 

381 implied_union_target: ClassVar[Literal[None]] = None 

382 """Name of another dimension that implies this one, whose values for this 

383 dimension define the set of allowed values for this dimension. 

384 """ 

385 

386 

387@final 

388class _LegacyImpliedUnionDimensionStorage(pydantic.BaseModel): 

389 """Legacy storage configuration for dimensions whose allowable values are 

390 computed from the union of the values in another dimension that implies 

391 this one. 

392 """ 

393 

394 cls: Literal["lsst.daf.butler.registry.dimensions.query.QueryDimensionRecordStorage"] = ( 

395 "lsst.daf.butler.registry.dimensions.query.QueryDimensionRecordStorage" 

396 ) 

397 

398 view_of: str 

399 """The dimension that implies this one and defines its values.""" 

400 

401 has_own_table: ClassVar[Literal[False]] = False 

402 """Whether this dimension needs a database table to be defined.""" 

403 

404 is_cached: ClassVar[Literal[False]] = False 

405 """Whether this dimension element's records should be cached in clients.""" 

406 

407 @property 

408 def implied_union_target(self) -> str: 

409 """Name of another dimension that implies this one, whose values for 

410 this dimension define the set of allowed values for this dimension. 

411 """ 

412 return self.view_of 

413 

414 

415@final 

416class _LegacyCachingDimensionStorage(pydantic.BaseModel): 

417 """Legacy storage configuration that wraps another to indicate that its 

418 records should be cached. 

419 """ 

420 

421 cls: Literal["lsst.daf.butler.registry.dimensions.caching.CachingDimensionRecordStorage"] = ( 

422 "lsst.daf.butler.registry.dimensions.caching.CachingDimensionRecordStorage" 

423 ) 

424 

425 nested: _LegacyTableDimensionStorage | _LegacyImpliedUnionDimensionStorage 

426 """Dimension storage configuration wrapped by this one.""" 

427 

428 @property 

429 def has_own_table(self) -> bool: 

430 """Whether this dimension needs a database table to be defined.""" 

431 return self.nested.has_own_table 

432 

433 is_cached: ClassVar[Literal[True]] = True 

434 """Whether this dimension element's records should be cached in clients.""" 

435 

436 @property 

437 def implied_union_target(self) -> str | None: 

438 """Name of another dimension that implies this one, whose values for 

439 this dimension define the set of allowed values for this dimension. 

440 """ 

441 return self.nested.implied_union_target 

442 

443 

444@final 

445class _ElementConfig(pydantic.BaseModel, DimensionConstructionVisitor): 

446 """Description of a single dimension or dimension join relation in 

447 dimension universe configuration. 

448 """ 

449 

450 doc: str = pydantic.Field(default="", description="Documentation for the dimension or relationship.") 

451 

452 keys: list[KeyColumnSpec] = pydantic.Field( 

453 default_factory=list, 

454 description=textwrap.dedent( 

455 """\ 

456 Key columns that (along with required dependency values) uniquely 

457 identify the dimension's records. 

458 

459 The first columns in this list is the primary key and is used in 

460 data coordinate values. Other columns are alternative keys and 

461 are defined with SQL ``UNIQUE`` constraints. 

462 """ 

463 ), 

464 ) 

465 

466 requires: set[str] = pydantic.Field( 

467 default_factory=set, 

468 description=( 

469 "Other dimensions whose primary keys are part of this dimension element's (compound) primary key." 

470 ), 

471 ) 

472 

473 implies: set[str] = pydantic.Field( 

474 default_factory=set, 

475 description="Other dimensions whose primary keys appear as foreign keys in this dimension element.", 

476 ) 

477 

478 metadata: list[MetadataColumnSpec] = pydantic.Field( 

479 default_factory=list, 

480 description="Non-key columns that provide extra information about a dimension element.", 

481 ) 

482 

483 is_cached: bool = pydantic.Field( 

484 default=False, 

485 description="Whether this element's records should be cached in the client.", 

486 ) 

487 

488 implied_union_target: str | None = pydantic.Field( 

489 default=None, 

490 description=textwrap.dedent( 

491 """\ 

492 Another dimension whose stored values for this dimension form the 

493 set of all allowed values. 

494 

495 The target dimension must have this dimension in its "implies" 

496 list. This means the current dimension will have no table of its 

497 own in the database. 

498 """ 

499 ), 

500 ) 

501 

502 governor: bool = pydantic.Field( 

503 default=False, 

504 description=textwrap.dedent( 

505 """\ 

506 Whether this is a governor dimension. 

507 

508 Governor dimensions are expected to have a tiny number of rows and 

509 must be explicitly provided in any dimension expression in which 

510 dependent dimensions appear. 

511 

512 Implies is_cached=True. 

513 """ 

514 ), 

515 ) 

516 

517 always_join: bool = pydantic.Field( 

518 default=False, 

519 description=textwrap.dedent( 

520 """\ 

521 Whether this dimension join relation should always be included in 

522 any query where its required dependencies appear. 

523 """ 

524 ), 

525 ) 

526 

527 populated_by: str | None = pydantic.Field( 

528 default=None, 

529 description=textwrap.dedent( 

530 """\ 

531 The name of a required dimension that this dimension join 

532 relation's rows should transferred alongside. 

533 """ 

534 ), 

535 ) 

536 

537 storage: Union[ 

538 _LegacyGovernorDimensionStorage, 

539 _LegacyTableDimensionStorage, 

540 _LegacyImpliedUnionDimensionStorage, 

541 _LegacyCachingDimensionStorage, 

542 None, 

543 ] = pydantic.Field( 

544 description="How this dimension element's rows should be stored in the database and client.", 

545 discriminator="cls", 

546 default=None, 

547 ) 

548 

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

550 # Docstring inherited from DimensionConstructionVisitor. 

551 return not (self.requires.isdisjoint(others) and self.implies.isdisjoint(others)) 

552 

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

554 # Docstring inherited from DimensionConstructionVisitor. 

555 if self.governor: 

556 from ._governor import GovernorDimension 

557 

558 governor = GovernorDimension( 

559 name, 

560 metadata_columns=NamedValueSet(self.metadata).freeze(), 

561 unique_keys=NamedValueSet(self.keys).freeze(), 

562 doc=self.doc, 

563 ) 

564 builder.dimensions.add(governor) 

565 builder.elements.add(governor) 

566 return 

567 # Expand required dependencies. 

568 for dependency_name in tuple(self.requires): # iterate over copy 

569 self.requires.update(builder.dimensions[dependency_name].required.names) 

570 # Transform required and implied Dimension names into instances, 

571 # and reorder to match builder's order. 

572 required: NamedValueSet[Dimension] = NamedValueSet() 

573 implied: NamedValueSet[Dimension] = NamedValueSet() 

574 for dimension in builder.dimensions: 

575 if dimension.name in self.requires: 

576 required.add(dimension) 

577 if dimension.name in self.implies: 

578 implied.add(dimension) 

579 # Elements with keys are Dimensions; the rest are 

580 # DimensionCombinations. 

581 if self.keys: 

582 from ._database import DatabaseDimension 

583 

584 dimension = DatabaseDimension( 

585 name, 

586 required=required, 

587 implied=implied.freeze(), 

588 metadata_columns=NamedValueSet(self.metadata).freeze(), 

589 unique_keys=NamedValueSet(self.keys).freeze(), 

590 is_cached=self.is_cached, 

591 implied_union_target=self.implied_union_target, 

592 doc=self.doc, 

593 ) 

594 builder.dimensions.add(dimension) 

595 builder.elements.add(dimension) 

596 else: 

597 from ._database import DatabaseDimensionCombination 

598 

599 combination = DatabaseDimensionCombination( 

600 name, 

601 required=required, 

602 implied=implied.freeze(), 

603 doc=self.doc, 

604 metadata_columns=NamedValueSet(self.metadata).freeze(), 

605 is_cached=self.is_cached, 

606 always_join=self.always_join, 

607 populated_by=( 

608 builder.dimensions[self.populated_by] if self.populated_by is not None else None 

609 ), 

610 ) 

611 builder.elements.add(combination) 

612 

613 @pydantic.model_validator(mode="after") 

614 def _primary_key_types(self) -> _ElementConfig: 

615 if self.keys and self.keys[0].type not in ("int", "string"): 615 ↛ 616line 615 didn't jump to line 616 because the condition on line 615 was never true

616 raise ValueError( 

617 "The dimension primary key type (the first entry in the keys list) must be 'int' " 

618 f"or 'string'; got '{self.keys[0].type}'." 

619 ) 

620 return self 

621 

622 @pydantic.model_validator(mode="after") 

623 def _not_nullable_keys(self) -> _ElementConfig: 

624 for key in self.keys: 

625 key.nullable = False 

626 return self 

627 

628 @pydantic.model_validator(mode="after") 

629 def _invalid_dimension_fields(self) -> _ElementConfig: 

630 if self.keys: 

631 if self.always_join: 631 ↛ 632line 631 didn't jump to line 632 because the condition on line 631 was never true

632 raise ValueError("Dimensions (elements with key columns) may not have always_join=True.") 

633 if self.populated_by: 633 ↛ 634line 633 didn't jump to line 634 because the condition on line 633 was never true

634 raise ValueError("Dimensions (elements with key columns) may not have populated_by.") 

635 return self 

636 

637 @pydantic.model_validator(mode="after") 

638 def _storage(self) -> _ElementConfig: 

639 if self.storage is not None: 639 ↛ 645line 639 didn't jump to line 645 because the condition on line 639 was always true

640 # 'storage' is legacy; pull its implications into the regular 

641 # attributes and set it to None for consistency. 

642 self.is_cached = self.storage.is_cached 

643 self.implied_union_target = self.storage.implied_union_target 

644 self.storage = None 

645 if self.governor: 

646 self.is_cached = True 

647 if self.implied_union_target is not None: 

648 if self.requires: 648 ↛ 649line 648 didn't jump to line 649 because the condition on line 648 was never true

649 raise ValueError("Implied-union dimension may not have required dependencies.") 

650 if self.implies: 650 ↛ 651line 650 didn't jump to line 651 because the condition on line 650 was never true

651 raise ValueError("Implied-union dimension may not have implied dependencies.") 

652 if len(self.keys) > 1: 652 ↛ 653line 652 didn't jump to line 653 because the condition on line 652 was never true

653 raise ValueError("Implied-union dimension may not have alternate keys.") 

654 if self.metadata: 654 ↛ 655line 654 didn't jump to line 655 because the condition on line 654 was never true

655 raise ValueError("Implied-union dimension may not have metadata columns.") 

656 return self 

657 

658 @pydantic.model_validator(mode="after") 

659 def _relationship_dependencies(self) -> _ElementConfig: 

660 if not self.keys and not self.requires: 660 ↛ 661line 660 didn't jump to line 661 because the condition on line 660 was never true

661 raise ValueError( 

662 "Dimension relationships (elements with no key columns) must have at least one " 

663 "required dependency." 

664 ) 

665 return self 

666 

667 

668@final 

669class SerializedDimensionConfig(pydantic.BaseModel): 

670 """Configuration that describes a complete dimension data model.""" 

671 

672 version: int = pydantic.Field( 

673 default=0, 

674 description=textwrap.dedent( 

675 """\ 

676 Integer version number for this universe. 

677 

678 This and 'namespace' are expected to uniquely identify a 

679 dimension universe. 

680 """ 

681 ), 

682 ) 

683 

684 namespace: str = pydantic.Field( 

685 default=_DEFAULT_NAMESPACE, 

686 description=textwrap.dedent( 

687 """\ 

688 String namespace for this universe. 

689 

690 This and 'version' are expected to uniquely identify a 

691 dimension universe. 

692 """ 

693 ), 

694 ) 

695 

696 skypix: _SkyPixSectionConfig = pydantic.Field( 

697 description="Hierarchical sky pixelization systems recognized by this dimension universe." 

698 ) 

699 

700 elements: dict[str, _ElementConfig] = pydantic.Field( 

701 default_factory=dict, description="Non-skypix dimensions and dimension join relations." 

702 ) 

703 

704 topology: _TopologySectionConfig = pydantic.Field( 

705 description="Spatial and temporal relationships between dimensions.", 

706 default_factory=_TopologySectionConfig, 

707 )