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-12 09:01 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-12 09: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 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/>.
22from __future__ import annotations
24__all__ = ("DatasetRecordStorageManager", "DatasetRecordStorage", "DatasetIdFactory", "DatasetIdGenEnum")
26import enum
27import uuid
28from abc import ABC, abstractmethod
29from collections.abc import Iterable, Iterator
30from typing import TYPE_CHECKING, Any
32import sqlalchemy.sql
34from ...core import DataCoordinate, DatasetId, DatasetRef, DatasetType, SimpleQuery, Timespan, ddl
35from .._exceptions import MissingDatasetTypeError
36from ._versioning import VersionedExtension
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
45class DatasetIdGenEnum(enum.Enum):
46 """This enum is used to specify dataset ID generation options for
47 ``insert()`` method.
48 """
50 UNIQUE = 0
51 """Unique mode generates unique ID for each inserted dataset, e.g.
52 auto-generated by database or random UUID.
53 """
55 DATAID_TYPE = 1
56 """In this mode ID is computed deterministically from a combination of
57 dataset type and dataId.
58 """
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 """
66class DatasetIdFactory:
67 """Factory for dataset IDs (UUIDs).
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 """
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 """
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.
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``.
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}")
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)
133class DatasetRecordStorage(ABC):
134 """An interface that manages the records associated with a particular
135 `DatasetType`.
137 Parameters
138 ----------
139 datasetType : `DatasetType`
140 Dataset type whose records this object manages.
141 """
143 def __init__(self, datasetType: DatasetType):
144 self.datasetType = datasetType
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.
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.
170 Returns
171 -------
172 datasets : `Iterable` [ `DatasetRef` ]
173 References to the inserted datasets.
174 """
175 raise NotImplementedError()
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.
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).
210 Returns
211 -------
212 datasets : `Iterable` [ `DatasetRef` ]
213 References to the inserted or existing datasets.
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()
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.
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.
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()
252 @abstractmethod
253 def delete(self, datasets: Iterable[DatasetRef]) -> None:
254 """Fully delete the given datasets from the registry.
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``.
262 Raises
263 ------
264 AmbiguousDatasetError
265 Raised if any of the given `DatasetRef` instances is unresolved.
266 """
267 raise NotImplementedError()
269 @abstractmethod
270 def associate(self, collection: CollectionRecord, datasets: Iterable[DatasetRef]) -> None:
271 """Associate one or more datasets with a collection.
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``.
282 Raises
283 ------
284 AmbiguousDatasetError
285 Raised if any of the given `DatasetRef` instances is unresolved.
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.
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()
298 @abstractmethod
299 def disassociate(self, collection: CollectionRecord, datasets: Iterable[DatasetRef]) -> None:
300 """Remove one or more datasets from a collection.
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``.
311 Raises
312 ------
313 AmbiguousDatasetError
314 Raised if any of the given `DatasetRef` instances is unresolved.
315 """
316 raise NotImplementedError()
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.
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.
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()
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.
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.
377 Raises
378 ------
379 CollectionTypeError
380 Raised if ``collection.type is not CollectionType.CALIBRATION``.
381 """
382 raise NotImplementedError()
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`.
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`.
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.
443 Returns
444 -------
445 query : `sqlalchemy.sql.Selectable`
446 A SQLAlchemy object representing a simple ``SELECT`` query.
447 """
448 raise NotImplementedError()
450 datasetType: DatasetType
451 """Dataset type whose records this object manages (`DatasetType`).
452 """
455class DatasetRecordStorageManager(VersionedExtension):
456 """An interface that manages the tables that describe datasets.
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 """
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.
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`.
487 Returns
488 -------
489 manager : `DatasetRecordStorageManager`
490 An instance of a concrete `DatasetRecordStorageManager` subclass.
491 """
492 raise NotImplementedError()
494 @classmethod
495 @abstractmethod
496 def getIdColumnType(cls) -> type:
497 """Return type used for columns storing dataset IDs.
499 This type is used for columns storing `DatasetRef.id` values, usually
500 a `type` subclass provided by SQLAlchemy.
502 Returns
503 -------
504 dtype : `type`
505 Type used for dataset identification in database.
506 """
507 raise NotImplementedError()
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`.
515 Parameters
516 ----------
517 mode : `DatasetIdGenEnum`
518 Enum value for the mode to test.
520 Returns
521 -------
522 supported : `bool`
523 Whether the given mode is supported.
524 """
525 raise NotImplementedError()
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.
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).
561 Returns
562 -------
563 idSpec : `ddl.FieldSpec`
564 Specification for the ID field.
565 """
566 raise NotImplementedError()
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()
576 def __getitem__(self, name: str) -> DatasetRecordStorage:
577 """Return the object that provides access to the records associated
578 with the given `DatasetType` name.
580 This is simply a convenience wrapper for `find` that raises `KeyError`
581 when the dataset type is not found.
583 Returns
584 -------
585 records : `DatasetRecordStorage`
586 The object representing the records for the given dataset type.
588 Raises
589 ------
590 KeyError
591 Raised if there is no dataset type with the given name.
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
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.
608 Parameters
609 ----------
610 name : `str`
611 Name of the dataset type.
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.
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()
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.
631 Parameters
632 ----------
633 datasetType : `DatasetType`
634 Dataset type for which a table should created (as necessary) and
635 an associated `DatasetRecordStorage` returned.
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.
644 Notes
645 -----
646 This operation may not be invoked within a `Database.transaction`
647 context.
648 """
649 raise NotImplementedError()
651 @abstractmethod
652 def remove(self, name: str) -> None:
653 """Remove the dataset type.
655 Parameters
656 ----------
657 name : `str`
658 Name of the dataset type.
659 """
660 raise NotImplementedError()
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.
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.
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.
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()
705 @abstractmethod
706 def getDatasetRef(self, id: DatasetId) -> DatasetRef | None:
707 """Return a `DatasetRef` for the given dataset primary key
708 value.
710 Parameters
711 ----------
712 id : `DatasetId`
713 Primary key value for the dataset.
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()
723 @abstractmethod
724 def getCollectionSummary(self, collection: CollectionRecord) -> CollectionSummary:
725 """Return a summary for the given collection.
727 Parameters
728 ----------
729 collection : `CollectionRecord`
730 Record describing the collection for which a summary is to be
731 retrieved.
733 Returns
734 -------
735 summary : `CollectionSummary`
736 Summary of the dataset types and governor dimension values in
737 this collection.
738 """
739 raise NotImplementedError()