Coverage for python/lsst/daf/butler/registry/interfaces/_datasets.py: 58%

114 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-07 09:47 +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 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__ = ("DatasetRecordStorageManager", "DatasetRecordStorage", "DatasetIdFactory", "DatasetIdGenEnum") 

25 

26import enum 

27import uuid 

28from abc import ABC, abstractmethod 

29from collections.abc import Iterable, Iterator 

30from typing import TYPE_CHECKING, Any 

31 

32import sqlalchemy.sql 

33 

34from ...core import DataCoordinate, DatasetId, DatasetRef, DatasetType, SimpleQuery, Timespan, ddl 

35from .._exceptions import MissingDatasetTypeError 

36from ._versioning import VersionedExtension 

37 

38if TYPE_CHECKING: 38 ↛ 39line 38 didn't jump to line 39, because the condition on line 38 was never true

39 from .._collection_summary import CollectionSummary 

40 from ._collections import CollectionManager, CollectionRecord, RunRecord 

41 from ._database import Database, StaticTablesContext 

42 from ._dimensions import DimensionRecordStorageManager 

43 

44 

45class DatasetIdGenEnum(enum.Enum): 

46 """This enum is used to specify dataset ID generation options for 

47 ``insert()`` method. 

48 """ 

49 

50 UNIQUE = 0 

51 """Unique mode generates unique ID for each inserted dataset, e.g. 

52 auto-generated by database or random UUID. 

53 """ 

54 

55 DATAID_TYPE = 1 

56 """In this mode ID is computed deterministically from a combination of 

57 dataset type and dataId. 

58 """ 

59 

60 DATAID_TYPE_RUN = 2 

61 """In this mode ID is computed deterministically from a combination of 

62 dataset type, dataId, and run collection name. 

63 """ 

64 

65 

66class DatasetIdFactory: 

67 """Factory for dataset IDs (UUIDs). 

68 

69 For now the logic is hard-coded and is controlled by the user-provided 

70 value of `DatasetIdGenEnum`. In the future we may implement a configurable 

71 logic that can guess `DatasetIdGenEnum` value from other parameters. 

72 """ 

73 

74 NS_UUID = uuid.UUID("840b31d9-05cd-5161-b2c8-00d32b280d0f") 

75 """Namespace UUID used for UUID5 generation. Do not change. This was 

76 produced by `uuid.uuid5(uuid.NAMESPACE_DNS, "lsst.org")`. 

77 """ 

78 

79 def makeDatasetId( 

80 self, 

81 run: str, 

82 datasetType: DatasetType, 

83 dataId: DataCoordinate, 

84 idGenerationMode: DatasetIdGenEnum, 

85 ) -> uuid.UUID: 

86 """Generate dataset ID for a dataset. 

87 

88 Parameters 

89 ---------- 

90 run : `str` 

91 Name of the RUN collection for the dataset. 

92 datasetType : `DatasetType` 

93 Dataset type. 

94 dataId : `DataCoordinate` 

95 Expanded data ID for the dataset. 

96 idGenerationMode : `DatasetIdGenEnum` 

97 ID generation option. `~DatasetIdGenEnum.UNIQUE` makes a random 

98 UUID4-type ID. `~DatasetIdGenEnum.DATAID_TYPE` makes a 

99 deterministic UUID5-type ID based on a dataset type name and 

100 ``dataId``. `~DatasetIdGenEnum.DATAID_TYPE_RUN` makes a 

101 deterministic UUID5-type ID based on a dataset type name, run 

102 collection name, and ``dataId``. 

103 

104 Returns 

105 ------- 

106 datasetId : `uuid.UUID` 

107 Dataset identifier. 

108 """ 

109 if idGenerationMode is DatasetIdGenEnum.UNIQUE: 

110 return uuid.uuid4() 

111 else: 

112 # WARNING: If you modify this code make sure that the order of 

113 # items in the `items` list below never changes. 

114 items: list[tuple[str, str]] = [] 

115 if idGenerationMode is DatasetIdGenEnum.DATAID_TYPE: 

116 items = [ 

117 ("dataset_type", datasetType.name), 

118 ] 

119 elif idGenerationMode is DatasetIdGenEnum.DATAID_TYPE_RUN: 

120 items = [ 

121 ("dataset_type", datasetType.name), 

122 ("run", run), 

123 ] 

124 else: 

125 raise ValueError(f"Unexpected ID generation mode: {idGenerationMode}") 

126 

127 for name, value in sorted(dataId.byName().items()): 

128 items.append((name, str(value))) 

129 data = ",".join(f"{key}={value}" for key, value in items) 

130 return uuid.uuid5(self.NS_UUID, data) 

131 

132 

133class DatasetRecordStorage(ABC): 

134 """An interface that manages the records associated with a particular 

135 `DatasetType`. 

136 

137 Parameters 

138 ---------- 

139 datasetType : `DatasetType` 

140 Dataset type whose records this object manages. 

141 """ 

142 

143 def __init__(self, datasetType: DatasetType): 

144 self.datasetType = datasetType 

145 

146 @abstractmethod 

147 def insert( 

148 self, 

149 run: RunRecord, 

150 dataIds: Iterable[DataCoordinate], 

151 idGenerationMode: DatasetIdGenEnum = DatasetIdGenEnum.UNIQUE, 

152 ) -> Iterator[DatasetRef]: 

153 """Insert one or more dataset entries into the database. 

154 

155 Parameters 

156 ---------- 

157 run : `RunRecord` 

158 The record object describing the `~CollectionType.RUN` collection 

159 this dataset will be associated with. 

160 dataIds : `Iterable` [ `DataCoordinate` ] 

161 Expanded data IDs (`DataCoordinate` instances) for the 

162 datasets to be added. The dimensions of all data IDs must be the 

163 same as ``self.datasetType.dimensions``. 

164 idMode : `DatasetIdGenEnum` 

165 With `UNIQUE` each new dataset is inserted with its new unique ID. 

166 With non-`UNIQUE` mode ID is computed from some combination of 

167 dataset type, dataId, and run collection name; if the same ID is 

168 already in the database then new record is not inserted. 

169 

170 Returns 

171 ------- 

172 datasets : `Iterable` [ `DatasetRef` ] 

173 References to the inserted datasets. 

174 """ 

175 raise NotImplementedError() 

176 

177 @abstractmethod 

178 def import_( 

179 self, 

180 run: RunRecord, 

181 datasets: Iterable[DatasetRef], 

182 idGenerationMode: DatasetIdGenEnum = DatasetIdGenEnum.UNIQUE, 

183 reuseIds: bool = False, 

184 ) -> Iterator[DatasetRef]: 

185 """Insert one or more dataset entries into the database. 

186 

187 Parameters 

188 ---------- 

189 run : `RunRecord` 

190 The record object describing the `~CollectionType.RUN` collection 

191 this dataset will be associated with. 

192 datasets : `~collections.abc.Iterable` of `DatasetRef` 

193 Datasets to be inserted. Datasets can specify ``id`` attribute 

194 which will be used for inserted datasets. All dataset IDs must 

195 have the same type (`int` or `uuid.UUID`), if type of dataset IDs 

196 does not match type supported by this class then IDs will be 

197 ignored and new IDs will be generated by backend. 

198 idGenerationMode : `DatasetIdGenEnum` 

199 With `UNIQUE` each new dataset is inserted with its new unique ID. 

200 With non-`UNIQUE` mode ID is computed from some combination of 

201 dataset type, dataId, and run collection name; if the same ID is 

202 already in the database then new record is not inserted. 

203 reuseIds : `bool`, optional 

204 If `True` then forces re-use of imported dataset IDs for integer 

205 IDs which are normally generated as auto-incremented; exception 

206 will be raised if imported IDs clash with existing ones. This 

207 option has no effect on the use of globally-unique IDs which are 

208 always re-used (or generated if integer IDs are being imported). 

209 

210 Returns 

211 ------- 

212 datasets : `Iterable` [ `DatasetRef` ] 

213 References to the inserted or existing datasets. 

214 

215 Notes 

216 ----- 

217 The ``datasetType`` and ``run`` attributes of datasets are supposed to 

218 be identical across all datasets but this is not checked and it should 

219 be enforced by higher level registry code. This method does not need 

220 to use those attributes from datasets, only ``dataId`` and ``id`` are 

221 relevant. 

222 """ 

223 raise NotImplementedError() 

224 

225 @abstractmethod 

226 def find( 

227 self, collection: CollectionRecord, dataId: DataCoordinate, timespan: Timespan | None = None 

228 ) -> DatasetRef | None: 

229 """Search a collection for a dataset with the given data ID. 

230 

231 Parameters 

232 ---------- 

233 collection : `CollectionRecord` 

234 The record object describing the collection to search for the 

235 dataset. May have any `CollectionType`. 

236 dataId: `DataCoordinate` 

237 Complete (but not necessarily expanded) data ID to search with, 

238 with ``dataId.graph == self.datasetType.dimensions``. 

239 timespan : `Timespan`, optional 

240 A timespan that the validity range of the dataset must overlap. 

241 Required if ``collection.type is CollectionType.CALIBRATION``, and 

242 ignored otherwise. 

243 

244 Returns 

245 ------- 

246 ref : `DatasetRef` 

247 A resolved `DatasetRef` (without components populated), or `None` 

248 if no matching dataset was found. 

249 """ 

250 raise NotImplementedError() 

251 

252 @abstractmethod 

253 def delete(self, datasets: Iterable[DatasetRef]) -> None: 

254 """Fully delete the given datasets from the registry. 

255 

256 Parameters 

257 ---------- 

258 datasets : `Iterable` [ `DatasetRef` ] 

259 Datasets to be deleted. All datasets must be resolved and have 

260 the same `DatasetType` as ``self``. 

261 

262 Raises 

263 ------ 

264 AmbiguousDatasetError 

265 Raised if any of the given `DatasetRef` instances is unresolved. 

266 """ 

267 raise NotImplementedError() 

268 

269 @abstractmethod 

270 def associate(self, collection: CollectionRecord, datasets: Iterable[DatasetRef]) -> None: 

271 """Associate one or more datasets with a collection. 

272 

273 Parameters 

274 ---------- 

275 collection : `CollectionRecord` 

276 The record object describing the collection. ``collection.type`` 

277 must be `~CollectionType.TAGGED`. 

278 datasets : `Iterable` [ `DatasetRef` ] 

279 Datasets to be associated. All datasets must be resolved and have 

280 the same `DatasetType` as ``self``. 

281 

282 Raises 

283 ------ 

284 AmbiguousDatasetError 

285 Raised if any of the given `DatasetRef` instances is unresolved. 

286 

287 Notes 

288 ----- 

289 Associating a dataset with into collection that already contains a 

290 different dataset with the same `DatasetType` and data ID will remove 

291 the existing dataset from that collection. 

292 

293 Associating the same dataset into a collection multiple times is a 

294 no-op, but is still not permitted on read-only databases. 

295 """ 

296 raise NotImplementedError() 

297 

298 @abstractmethod 

299 def disassociate(self, collection: CollectionRecord, datasets: Iterable[DatasetRef]) -> None: 

300 """Remove one or more datasets from a collection. 

301 

302 Parameters 

303 ---------- 

304 collection : `CollectionRecord` 

305 The record object describing the collection. ``collection.type`` 

306 must be `~CollectionType.TAGGED`. 

307 datasets : `Iterable` [ `DatasetRef` ] 

308 Datasets to be disassociated. All datasets must be resolved and 

309 have the same `DatasetType` as ``self``. 

310 

311 Raises 

312 ------ 

313 AmbiguousDatasetError 

314 Raised if any of the given `DatasetRef` instances is unresolved. 

315 """ 

316 raise NotImplementedError() 

317 

318 @abstractmethod 

319 def certify( 

320 self, collection: CollectionRecord, datasets: Iterable[DatasetRef], timespan: Timespan 

321 ) -> None: 

322 """Associate one or more datasets with a calibration collection and a 

323 validity range within it. 

324 

325 Parameters 

326 ---------- 

327 collection : `CollectionRecord` 

328 The record object describing the collection. ``collection.type`` 

329 must be `~CollectionType.CALIBRATION`. 

330 datasets : `Iterable` [ `DatasetRef` ] 

331 Datasets to be associated. All datasets must be resolved and have 

332 the same `DatasetType` as ``self``. 

333 timespan : `Timespan` 

334 The validity range for these datasets within the collection. 

335 

336 Raises 

337 ------ 

338 AmbiguousDatasetError 

339 Raised if any of the given `DatasetRef` instances is unresolved. 

340 ConflictingDefinitionError 

341 Raised if the collection already contains a different dataset with 

342 the same `DatasetType` and data ID and an overlapping validity 

343 range. 

344 CollectionTypeError 

345 Raised if 

346 ``collection.type is not CollectionType.CALIBRATION`` or if 

347 ``self.datasetType.isCalibration() is False``. 

348 """ 

349 raise NotImplementedError() 

350 

351 @abstractmethod 

352 def decertify( 

353 self, 

354 collection: CollectionRecord, 

355 timespan: Timespan, 

356 *, 

357 dataIds: Iterable[DataCoordinate] | None = None, 

358 ) -> None: 

359 """Remove or adjust datasets to clear a validity range within a 

360 calibration collection. 

361 

362 Parameters 

363 ---------- 

364 collection : `CollectionRecord` 

365 The record object describing the collection. ``collection.type`` 

366 must be `~CollectionType.CALIBRATION`. 

367 timespan : `Timespan` 

368 The validity range to remove datasets from within the collection. 

369 Datasets that overlap this range but are not contained by it will 

370 have their validity ranges adjusted to not overlap it, which may 

371 split a single dataset validity range into two. 

372 dataIds : `Iterable` [ `DataCoordinate` ], optional 

373 Data IDs that should be decertified within the given validity range 

374 If `None`, all data IDs for ``self.datasetType`` will be 

375 decertified. 

376 

377 Raises 

378 ------ 

379 CollectionTypeError 

380 Raised if ``collection.type is not CollectionType.CALIBRATION``. 

381 """ 

382 raise NotImplementedError() 

383 

384 @abstractmethod 

385 def select( 

386 self, 

387 *collections: CollectionRecord, 

388 dataId: SimpleQuery.Select.Or[DataCoordinate] = SimpleQuery.Select, 

389 id: SimpleQuery.Select.Or[DatasetId | None] = SimpleQuery.Select, 

390 run: SimpleQuery.Select.Or[None] = SimpleQuery.Select, 

391 timespan: SimpleQuery.Select.Or[Timespan | None] = SimpleQuery.Select, 

392 ingestDate: SimpleQuery.Select.Or[Timespan | None] = None, 

393 rank: SimpleQuery.Select.Or[None] = None, 

394 ) -> sqlalchemy.sql.Selectable: 

395 """Return a SQLAlchemy object that represents a ``SELECT`` query for 

396 this `DatasetType`. 

397 

398 All arguments can either be a value that constrains the query or 

399 the `SimpleQuery.Select` tag object to indicate that the value should 

400 be returned in the columns in the ``SELECT`` clause. The default is 

401 `SimpleQuery.Select`. 

402 

403 Parameters 

404 ---------- 

405 *collections : `CollectionRecord` 

406 The record object(s) describing the collection(s) to query. May 

407 not be of type `CollectionType.CHAINED`. If multiple collections 

408 are passed, the query will search all of them in an unspecified 

409 order, and all collections must have the same type. 

410 dataId : `DataCoordinate` or `Select` 

411 The data ID to restrict results with, or an instruction to return 

412 the data ID via columns with names 

413 ``self.datasetType.dimensions.names``. 

414 id : `DatasetId`, `Select` or None, 

415 The primary key value for the dataset, an instruction to return it 

416 via a ``id`` column, or `None` to ignore it entirely. 

417 run : `None` or `Select` 

418 If `Select` (default), include the dataset's run key value (as 

419 column labeled with the return value of 

420 ``CollectionManager.getRunForeignKeyName``). 

421 If `None`, do not include this column (to constrain the run, 

422 pass a `RunRecord` as the ``collection`` argument instead). 

423 timespan : `None`, `Select`, or `Timespan` 

424 If `Select` (default), include the validity range timespan in the 

425 result columns. If a `Timespan` instance, constrain the results to 

426 those whose validity ranges overlap that given timespan. For 

427 collections whose type is not `~CollectionType.CALIBRATION`, if 

428 `Select` is passed a column with a literal ``NULL`` value will be 

429 added, and ``sqlalchemy.sql.expressions.Null` may be passed to 

430 force a constraint that the value be null (since `None` is 

431 interpreted as meaning "do not select or constrain this column"). 

432 ingestDate : `None`, `Select`, or `Timespan` 

433 If `Select` include the ingest timestamp in the result columns. 

434 If a `Timespan` instance, constrain the results to those whose 

435 ingest times which are inside given timespan and also include 

436 timestamp in the result columns. If `None` (default) then there is 

437 no constraint and timestamp is not returned. 

438 rank : `Select` or `None` 

439 If `Select`, include a calculated column that is the integer rank 

440 of the row's collection in the given list of collections, starting 

441 from zero. 

442 

443 Returns 

444 ------- 

445 query : `sqlalchemy.sql.Selectable` 

446 A SQLAlchemy object representing a simple ``SELECT`` query. 

447 """ 

448 raise NotImplementedError() 

449 

450 datasetType: DatasetType 

451 """Dataset type whose records this object manages (`DatasetType`). 

452 """ 

453 

454 

455class DatasetRecordStorageManager(VersionedExtension): 

456 """An interface that manages the tables that describe datasets. 

457 

458 `DatasetRecordStorageManager` primarily serves as a container and factory 

459 for `DatasetRecordStorage` instances, which each provide access to the 

460 records for a different `DatasetType`. 

461 """ 

462 

463 @classmethod 

464 @abstractmethod 

465 def initialize( 

466 cls, 

467 db: Database, 

468 context: StaticTablesContext, 

469 *, 

470 collections: CollectionManager, 

471 dimensions: DimensionRecordStorageManager, 

472 ) -> DatasetRecordStorageManager: 

473 """Construct an instance of the manager. 

474 

475 Parameters 

476 ---------- 

477 db : `Database` 

478 Interface to the underlying database engine and namespace. 

479 context : `StaticTablesContext` 

480 Context object obtained from `Database.declareStaticTables`; used 

481 to declare any tables that should always be present. 

482 collections: `CollectionManager` 

483 Manager object for the collections in this `Registry`. 

484 dimensions : `DimensionRecordStorageManager` 

485 Manager object for the dimensions in this `Registry`. 

486 

487 Returns 

488 ------- 

489 manager : `DatasetRecordStorageManager` 

490 An instance of a concrete `DatasetRecordStorageManager` subclass. 

491 """ 

492 raise NotImplementedError() 

493 

494 @classmethod 

495 @abstractmethod 

496 def getIdColumnType(cls) -> type: 

497 """Return type used for columns storing dataset IDs. 

498 

499 This type is used for columns storing `DatasetRef.id` values, usually 

500 a `type` subclass provided by SQLAlchemy. 

501 

502 Returns 

503 ------- 

504 dtype : `type` 

505 Type used for dataset identification in database. 

506 """ 

507 raise NotImplementedError() 

508 

509 @classmethod 

510 @abstractmethod 

511 def supportsIdGenerationMode(cls, mode: DatasetIdGenEnum) -> bool: 

512 """Test whether the given dataset ID generation mode is supported by 

513 `insert`. 

514 

515 Parameters 

516 ---------- 

517 mode : `DatasetIdGenEnum` 

518 Enum value for the mode to test. 

519 

520 Returns 

521 ------- 

522 supported : `bool` 

523 Whether the given mode is supported. 

524 """ 

525 raise NotImplementedError() 

526 

527 @classmethod 

528 @abstractmethod 

529 def addDatasetForeignKey( 

530 cls, 

531 tableSpec: ddl.TableSpec, 

532 *, 

533 name: str = "dataset", 

534 constraint: bool = True, 

535 onDelete: str | None = None, 

536 **kwargs: Any, 

537 ) -> ddl.FieldSpec: 

538 """Add a foreign key (field and constraint) referencing the dataset 

539 table. 

540 

541 Parameters 

542 ---------- 

543 tableSpec : `ddl.TableSpec` 

544 Specification for the table that should reference the dataset 

545 table. Will be modified in place. 

546 name: `str`, optional 

547 A name to use for the prefix of the new field; the full name is 

548 ``{name}_id``. 

549 onDelete: `str`, optional 

550 One of "CASCADE" or "SET NULL", indicating what should happen to 

551 the referencing row if the collection row is deleted. `None` 

552 indicates that this should be an integrity error. 

553 constraint: `bool`, optional 

554 If `False` (`True` is default), add a field that can be joined to 

555 the dataset primary key, but do not add a foreign key constraint. 

556 **kwargs 

557 Additional keyword arguments are forwarded to the `ddl.FieldSpec` 

558 constructor (only the ``name`` and ``dtype`` arguments are 

559 otherwise provided). 

560 

561 Returns 

562 ------- 

563 idSpec : `ddl.FieldSpec` 

564 Specification for the ID field. 

565 """ 

566 raise NotImplementedError() 

567 

568 @abstractmethod 

569 def refresh(self) -> None: 

570 """Ensure all other operations on this manager are aware of any 

571 dataset types that may have been registered by other clients since 

572 it was initialized or last refreshed. 

573 """ 

574 raise NotImplementedError() 

575 

576 def __getitem__(self, name: str) -> DatasetRecordStorage: 

577 """Return the object that provides access to the records associated 

578 with the given `DatasetType` name. 

579 

580 This is simply a convenience wrapper for `find` that raises `KeyError` 

581 when the dataset type is not found. 

582 

583 Returns 

584 ------- 

585 records : `DatasetRecordStorage` 

586 The object representing the records for the given dataset type. 

587 

588 Raises 

589 ------ 

590 KeyError 

591 Raised if there is no dataset type with the given name. 

592 

593 Notes 

594 ----- 

595 Dataset types registered by another client of the same repository since 

596 the last call to `initialize` or `refresh` may not be found. 

597 """ 

598 result = self.find(name) 

599 if result is None: 

600 raise MissingDatasetTypeError(f"Dataset type with name '{name}' not found.") 

601 return result 

602 

603 @abstractmethod 

604 def find(self, name: str) -> DatasetRecordStorage | None: 

605 """Return an object that provides access to the records associated with 

606 the given `DatasetType` name, if one exists. 

607 

608 Parameters 

609 ---------- 

610 name : `str` 

611 Name of the dataset type. 

612 

613 Returns 

614 ------- 

615 records : `DatasetRecordStorage` or `None` 

616 The object representing the records for the given dataset type, or 

617 `None` if there are no records for that dataset type. 

618 

619 Notes 

620 ----- 

621 Dataset types registered by another client of the same repository since 

622 the last call to `initialize` or `refresh` may not be found. 

623 """ 

624 raise NotImplementedError() 

625 

626 @abstractmethod 

627 def register(self, datasetType: DatasetType) -> tuple[DatasetRecordStorage, bool]: 

628 """Ensure that this `Registry` can hold records for the given 

629 `DatasetType`, creating new tables as necessary. 

630 

631 Parameters 

632 ---------- 

633 datasetType : `DatasetType` 

634 Dataset type for which a table should created (as necessary) and 

635 an associated `DatasetRecordStorage` returned. 

636 

637 Returns 

638 ------- 

639 records : `DatasetRecordStorage` 

640 The object representing the records for the given dataset type. 

641 inserted : `bool` 

642 `True` if the dataset type did not exist in the registry before. 

643 

644 Notes 

645 ----- 

646 This operation may not be invoked within a `Database.transaction` 

647 context. 

648 """ 

649 raise NotImplementedError() 

650 

651 @abstractmethod 

652 def remove(self, name: str) -> None: 

653 """Remove the dataset type. 

654 

655 Parameters 

656 ---------- 

657 name : `str` 

658 Name of the dataset type. 

659 """ 

660 raise NotImplementedError() 

661 

662 @abstractmethod 

663 def resolve_wildcard( 

664 self, 

665 expression: Any, 

666 components: bool | None = None, 

667 missing: list[str] | None = None, 

668 explicit_only: bool = False, 

669 ) -> dict[DatasetType, list[str | None]]: 

670 """Resolve a dataset type wildcard expression. 

671 

672 Parameters 

673 ---------- 

674 expression 

675 Expression to resolve. Will be passed to 

676 `DatasetTypeWildcard.from_expression`. 

677 components : `bool`, optional 

678 If `True`, apply all expression patterns to component dataset type 

679 names as well. If `False`, never apply patterns to components. If 

680 `None` (default), apply patterns to components only if their parent 

681 datasets were not matched by the expression. Fully-specified 

682 component datasets (`str` or `DatasetType` instances) are always 

683 included. 

684 

685 Values other than `False` are deprecated, and only `False` will be 

686 supported after v26. After v27 this argument will be removed 

687 entirely. 

688 missing : `list` of `str`, optional 

689 String dataset type names that were explicitly given (i.e. not 

690 regular expression patterns) but not found will be appended to this 

691 list, if it is provided. 

692 explicit_only : `bool`, optional 

693 If `True`, require explicit `DatasetType` instances or `str` names, 

694 with `re.Pattern` instances deprecated and ``...`` prohibited. 

695 

696 Returns 

697 ------- 

698 dataset_types : `dict` [ `DatasetType`, `list` [ `None`, `str` ] ] 

699 A mapping with resolved dataset types as keys and lists of 

700 matched component names as values, where `None` indicates the 

701 parent composite dataset type was matched. 

702 """ 

703 raise NotImplementedError() 

704 

705 @abstractmethod 

706 def getDatasetRef(self, id: DatasetId) -> DatasetRef | None: 

707 """Return a `DatasetRef` for the given dataset primary key 

708 value. 

709 

710 Parameters 

711 ---------- 

712 id : `DatasetId` 

713 Primary key value for the dataset. 

714 

715 Returns 

716 ------- 

717 ref : `DatasetRef` or `None` 

718 Object representing the dataset, or `None` if no dataset with the 

719 given primary key values exists in this layer. 

720 """ 

721 raise NotImplementedError() 

722 

723 @abstractmethod 

724 def getCollectionSummary(self, collection: CollectionRecord) -> CollectionSummary: 

725 """Return a summary for the given collection. 

726 

727 Parameters 

728 ---------- 

729 collection : `CollectionRecord` 

730 Record describing the collection for which a summary is to be 

731 retrieved. 

732 

733 Returns 

734 ------- 

735 summary : `CollectionSummary` 

736 Summary of the dataset types and governor dimension values in 

737 this collection. 

738 """ 

739 raise NotImplementedError()