Coverage for python/lsst/daf/butler/datastore/_datastore.py: 96%
289 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-06 09:46 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-06 09:46 +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/>.
28"""Support for generic data stores."""
30from __future__ import annotations
32from .record_data import DatastoreRecordTable
34__all__ = (
35 "DatasetRefURIs",
36 "Datastore",
37 "DatastoreConfig",
38 "DatastoreOpaqueTable",
39 "DatastoreTransaction",
40 "DatastoreValidationError",
41 "DuplicateIdError",
42 "NullDatastore",
43)
45import contextlib
46import dataclasses
47import logging
48import time
49from abc import ABCMeta, abstractmethod
50from collections import abc, defaultdict
51from collections.abc import Callable, Collection, Iterable, Iterator, Mapping
52from typing import TYPE_CHECKING, Any, ClassVar
54from lsst.utils import doImportType
56from .._config import Config, ConfigSubset
57from .._exceptions import DatasetTypeNotSupportedError, ValidationError
58from .._file_dataset import FileDataset
59from .._storage_class import StorageClassFactory
60from ._transfer import FileTransferMap, FileTransferSource
61from .constraints import Constraints
63if TYPE_CHECKING:
64 from lsst.resources import ResourcePath, ResourcePathExpression
66 from .. import ddl
67 from .._config_support import LookupKey
68 from .._dataset_provenance import DatasetProvenance
69 from .._dataset_ref import DatasetId, DatasetRef
70 from .._dataset_type import DatasetType
71 from .._storage_class import StorageClass
72 from ..datastores.file_datastore.get import DatasetLocationInformation
73 from ..datastores.file_datastore.retrieve_artifacts import ArtifactIndexInfo
74 from ..registry.interfaces import DatasetIdRef, DatastoreRegistryBridgeManager
75 from .record_data import DatastoreRecordData
76 from .stored_file_info import StoredDatastoreItemInfo
78_LOG = logging.getLogger(__name__)
81class DatastoreConfig(ConfigSubset):
82 """Configuration for Datastores."""
84 component = "datastore"
85 requiredKeys = ("cls",)
86 defaultConfigFile = "datastore.yaml"
89class DatastoreValidationError(ValidationError):
90 """There is a problem with the Datastore configuration."""
92 pass
95class DuplicateIdError(Exception):
96 """Exception raised when trying to store dataset with the existing ID."""
98 pass
101@dataclasses.dataclass(frozen=True)
102class Event:
103 """Representation of an event that can be rolled back."""
105 __slots__ = {"name", "undoFunc", "args", "kwargs"}
106 name: str
107 undoFunc: Callable
108 args: tuple
109 kwargs: dict
112@dataclasses.dataclass(frozen=True)
113class DatastoreOpaqueTable:
114 """Definition of the opaque table which stores datastore records.
116 Table definition contains `.ddl.TableSpec` for a table and a class
117 of a record which must be a subclass of `StoredDatastoreItemInfo`.
118 """
120 __slots__ = {"table_spec", "record_class"}
121 table_spec: ddl.TableSpec
122 record_class: type[StoredDatastoreItemInfo]
125class IngestPrepData:
126 """A helper base class for `Datastore` ingest implementations.
128 Datastore implementations will generally need a custom implementation of
129 this class.
131 Should be accessed as ``Datastore.IngestPrepData`` instead of via direct
132 import.
134 Parameters
135 ----------
136 refs : `~collections.abc.Iterable` of `DatasetRef`
137 References for the datasets that can be ingested by this datastore.
138 """
140 def __init__(self, refs: Iterable[DatasetRef]):
141 self.refs = {ref.id: ref for ref in refs}
144class DatastoreTransaction:
145 """Keeps a log of `Datastore` activity and allow rollback.
147 Parameters
148 ----------
149 parent : `DatastoreTransaction`, optional
150 The parent transaction (if any).
152 Notes
153 -----
154 This transaction object must be thread safe.
155 """
157 Event: ClassVar[type] = Event
159 parent: DatastoreTransaction | None
160 """The parent transaction. (`DatastoreTransaction`, optional)"""
162 def __init__(self, parent: DatastoreTransaction | None = None):
163 self.parent = parent
164 self._log: list[Event] = []
166 def registerUndo(self, name: str, undoFunc: Callable, *args: Any, **kwargs: Any) -> None:
167 """Register event with undo function.
169 Parameters
170 ----------
171 name : `str`
172 Name of the event.
173 undoFunc : `~collections.abc.Callable`
174 Function to undo this event.
175 *args : `tuple`
176 Positional arguments to ``undoFunc``.
177 **kwargs
178 Keyword arguments to ``undoFunc``.
179 """
180 self._log.append(self.Event(name, undoFunc, args, kwargs))
182 @contextlib.contextmanager
183 def undoWith(self, name: str, undoFunc: Callable, *args: Any, **kwargs: Any) -> Iterator[None]:
184 """Register undo function if nested operation succeeds.
186 Calls `registerUndo`.
188 This can be used to wrap individual undo-able statements within a
189 DatastoreTransaction block. Multiple statements that can fail
190 separately should not be part of the same `undoWith` block.
192 All arguments are forwarded directly to `registerUndo`.
194 Parameters
195 ----------
196 name : `str`
197 The name to associate with this event.
198 undoFunc : `~collections.abc.Callable`
199 Function to undo this event.
200 *args : `tuple`
201 Positional arguments for ``undoFunc``.
202 **kwargs : `typing.Any`
203 Keyword arguments for ``undoFunc``.
204 """
205 try:
206 yield None
207 except BaseException:
208 raise
209 else:
210 self.registerUndo(name, undoFunc, *args, **kwargs)
212 def rollback(self) -> None:
213 """Roll back all events in this transaction."""
214 log = logging.getLogger(__name__)
215 while self._log:
216 ev = self._log.pop()
217 try:
218 log.debug(
219 "Rolling back transaction: %s: %s(%s,%s)",
220 ev.name,
221 ev.undoFunc,
222 ",".join(str(a) for a in ev.args),
223 ",".join(f"{k}={v}" for k, v in ev.kwargs.items()),
224 )
225 except Exception:
226 # In case we had a problem in stringification of arguments
227 log.warning("Rolling back transaction: %s", ev.name)
228 try:
229 ev.undoFunc(*ev.args, **ev.kwargs)
230 except BaseException as e:
231 # Deliberately swallow error that may occur in unrolling
232 log.warning("Exception: %s caught while unrolling: %s", e, ev.name)
233 pass
235 def commit(self) -> None:
236 """Commit this transaction."""
237 if self.parent is None:
238 # Just forget about the events, they have already happened.
239 return
240 else:
241 # We may still want to events from this transaction as part of
242 # the parent.
243 self.parent._log.extend(self._log)
246@dataclasses.dataclass
247class DatasetRefURIs(abc.Sequence):
248 """Represents the primary and component ResourcePath(s) associated with a
249 DatasetRef.
251 This is used in places where its members used to be represented as a tuple
252 (``primaryURI``, ``componentURIs``). To maintain backward compatibility
253 this inherits from Sequence and so instances can be treated as a two-item
254 tuple.
256 Parameters
257 ----------
258 primaryURI : `lsst.resources.ResourcePath` or `None`, optional
259 The URI to the primary artifact associated with this dataset. If the
260 dataset was disassembled within the datastore this may be `None`.
261 componentURIs : `dict` [`str`, `~lsst.resources.ResourcePath`] or `None`
262 The URIs to any components associated with the dataset artifact
263 indexed by component name. This can be empty if there are no
264 components.
265 """
267 def __init__(
268 self,
269 primaryURI: ResourcePath | None = None,
270 componentURIs: dict[str, ResourcePath] | None = None,
271 ):
272 self.primaryURI = primaryURI
273 self.componentURIs = componentURIs or {}
275 def __getitem__(self, index: Any) -> Any:
276 """Get primaryURI and componentURIs by index.
278 Provides support for tuple-like access.
279 """
280 if index == 0:
281 return self.primaryURI
282 elif index == 1:
283 return self.componentURIs
284 raise IndexError("list index out of range")
286 def __len__(self) -> int:
287 """Get the number of data members.
289 Provides support for tuple-like access.
290 """
291 return 2
293 def __repr__(self) -> str:
294 return f"DatasetRefURIs({repr(self.primaryURI)}, {repr(self.componentURIs)})"
296 def iter_all(self) -> Iterator[ResourcePath]:
297 """Iterate over all URIs without regard to whether they are primary
298 or component.
299 """
300 if self.primaryURI is not None:
301 yield self.primaryURI
302 yield from self.componentURIs.values()
305class Datastore(FileTransferSource, metaclass=ABCMeta):
306 """Datastore interface.
308 Parameters
309 ----------
310 config : `DatastoreConfig` or `str`
311 Load configuration either from an existing config instance or by
312 referring to a configuration file.
313 bridgeManager : `DatastoreRegistryBridgeManager`
314 Object that manages the interface between `Registry` and datastores.
316 See Also
317 --------
318 lsst.daf.butler.Butler
319 """
321 defaultConfigFile: ClassVar[str | None] = None
322 """Path to configuration defaults. Accessed within the ``config`` resource
323 or relative to a search path. Can be None if no defaults specified.
324 """
326 containerKey: ClassVar[str | None] = None
327 """Name of the key containing a list of subconfigurations that also
328 need to be merged with defaults and will likely use different Python
329 datastore classes (but all using DatastoreConfig). Assumed to be a
330 list of configurations that can be represented in a DatastoreConfig
331 and containing a "cls" definition. None indicates that no containers
332 are expected in this Datastore."""
334 isEphemeral: bool = False
335 """Indicate whether this Datastore is ephemeral or not. An ephemeral
336 datastore is one where the contents of the datastore will not exist
337 across process restarts. This value can change per-instance."""
339 config: DatastoreConfig
340 """Configuration used to create Datastore."""
342 name: str
343 """Label associated with this Datastore."""
345 storageClassFactory: StorageClassFactory
346 """Factory for creating storage class instances from name."""
348 constraints: Constraints
349 """Constraints to apply when putting datasets into the datastore."""
351 # MyPy does not like for this to be annotated as any kind of type, because
352 # it can't do static checking on type variables that can change at runtime.
353 IngestPrepData: ClassVar[Any] = IngestPrepData
354 """Helper base class for ingest implementations.
355 """
357 @classmethod
358 @abstractmethod
359 def setConfigRoot(cls, root: str, config: Config, full: Config, overwrite: bool = True) -> None:
360 """Set filesystem-dependent config options for this datastore.
362 The options will be appropriate for a new empty repository with the
363 given root.
365 Parameters
366 ----------
367 root : `str`
368 Filesystem path to the root of the data repository.
369 config : `Config`
370 A `Config` to update. Only the subset understood by
371 this component will be updated. Will not expand
372 defaults.
373 full : `Config`
374 A complete config with all defaults expanded that can be
375 converted to a `DatastoreConfig`. Read-only and will not be
376 modified by this method.
377 Repository-specific options that should not be obtained
378 from defaults when Butler instances are constructed
379 should be copied from ``full`` to ``config``.
380 overwrite : `bool`, optional
381 If `False`, do not modify a value in ``config`` if the value
382 already exists. Default is always to overwrite with the provided
383 ``root``.
385 Notes
386 -----
387 If a keyword is explicitly defined in the supplied ``config`` it
388 will not be overridden by this method if ``overwrite`` is `False`.
389 This allows explicit values set in external configs to be retained.
390 """
391 raise NotImplementedError()
393 @staticmethod
394 def fromConfig(
395 config: Config,
396 bridgeManager: DatastoreRegistryBridgeManager,
397 butlerRoot: ResourcePathExpression | None = None,
398 ) -> Datastore:
399 """Create datastore from type specified in config file.
401 Parameters
402 ----------
403 config : `Config` or `~lsst.resources.ResourcePathExpression`
404 Configuration instance.
405 bridgeManager : `DatastoreRegistryBridgeManager`
406 Object that manages the interface between `Registry` and
407 datastores.
408 butlerRoot : `str`, optional
409 Butler root directory.
410 """
411 config = DatastoreConfig(config)
412 cls = doImportType(config["cls"])
413 if not issubclass(cls, Datastore): 413 ↛ 414line 413 didn't jump to line 414 because the condition on line 413 was never true
414 raise TypeError(f"Imported child class {config['cls']} is not a Datastore")
415 return cls._create_from_config(config=config, bridgeManager=bridgeManager, butlerRoot=butlerRoot)
417 def __init__(
418 self,
419 config: DatastoreConfig,
420 bridgeManager: DatastoreRegistryBridgeManager,
421 ):
422 self.config = config
423 self.name = "ABCDataStore"
424 self._transaction: DatastoreTransaction | None = None
426 # All Datastores need storage classes and constraints
427 self.storageClassFactory = StorageClassFactory()
429 # And read the constraints list
430 constraintsConfig = self.config.get("constraints")
431 self.constraints = Constraints(constraintsConfig, universe=bridgeManager.universe)
433 @classmethod
434 @abstractmethod
435 def _create_from_config(
436 cls,
437 config: DatastoreConfig,
438 bridgeManager: DatastoreRegistryBridgeManager,
439 butlerRoot: ResourcePathExpression | None,
440 ) -> Datastore:
441 """`Datastore`.``fromConfig`` calls this to instantiate Datastore
442 subclasses. This is the primary constructor for the individual
443 Datastore subclasses.
444 """
445 raise NotImplementedError()
447 @abstractmethod
448 def clone(self, bridgeManager: DatastoreRegistryBridgeManager) -> Datastore:
449 """Make an independent copy of this Datastore with a different
450 `DatastoreRegistryBridgeManager` instance.
452 Parameters
453 ----------
454 bridgeManager : `DatastoreRegistryBridgeManager`
455 New `DatastoreRegistryBridgeManager` object to use when
456 instantiating managers.
458 Returns
459 -------
460 datastore : `Datastore`
461 New `Datastore` instance with the same configuration as the
462 existing instance.
463 """
464 raise NotImplementedError()
466 def __str__(self) -> str:
467 return self.name
469 def __repr__(self) -> str:
470 return self.name
472 @property
473 def names(self) -> tuple[str, ...]:
474 """Names associated with this datastore returned as a list.
476 Can be different to ``name`` for a chaining datastore.
477 """
478 # Default implementation returns solely the name itself
479 return (self.name,)
481 @property
482 def roots(self) -> dict[str, ResourcePath | None]:
483 """Return the root URIs for each named datastore.
485 Mapping from datastore name to root URI. The URI can be `None`
486 if a datastore has no concept of a root URI.
487 (`dict` [`str`, `lsst.resources.ResourcePath` | `None`])
488 """
489 return {self.name: None}
491 @contextlib.contextmanager
492 def transaction(self) -> Iterator[DatastoreTransaction]:
493 """Context manager supporting `Datastore` transactions.
495 Transactions can be nested, and are to be used in combination with
496 `Registry.transaction`.
497 """
498 self._transaction = DatastoreTransaction(self._transaction)
499 try:
500 yield self._transaction
501 except BaseException:
502 self._transaction.rollback()
503 raise
504 else:
505 self._transaction.commit()
506 self._transaction = self._transaction.parent
508 def _set_trust_mode(self, mode: bool) -> None:
509 """Set the trust mode for this datastore.
511 Parameters
512 ----------
513 mode : `bool`
514 If `True`, get requests will be attempted even if the datastore
515 does not know about the dataset.
517 Notes
518 -----
519 This is a private method to indicate that trust mode might be a
520 transitory property that we do not want to make fully public. For now
521 only a `~lsst.daf.butler.datastores.FileDatastore` understands this
522 concept. By default this method does nothing.
523 """
524 return
526 @abstractmethod
527 def knows(self, ref: DatasetRef) -> bool:
528 """Check if the dataset is known to the datastore.
530 Does not check for existence of any artifact.
532 Parameters
533 ----------
534 ref : `DatasetRef`
535 Reference to the required dataset.
537 Returns
538 -------
539 exists : `bool`
540 `True` if the dataset is known to the datastore.
541 """
542 raise NotImplementedError()
544 def knows_these(self, refs: Iterable[DatasetRef]) -> dict[DatasetRef, bool]:
545 """Check which of the given datasets are known to this datastore.
547 This is like ``mexist()`` but does not check that the file exists.
549 Parameters
550 ----------
551 refs : `~collections.abc.Iterable` [ `DatasetRef` ]
552 The datasets to check.
554 Returns
555 -------
556 exists : `dict` [`DatasetRef`, `bool`]
557 Mapping of dataset to boolean indicating whether the dataset
558 is known to the datastore.
559 """
560 # Non-optimized default calls knows() repeatedly.
561 return {ref: self.knows(ref) for ref in refs}
563 def mexists(
564 self, refs: Iterable[DatasetRef], artifact_existence: dict[ResourcePath, bool] | None = None
565 ) -> dict[DatasetRef, bool]:
566 """Check the existence of multiple datasets at once.
568 Parameters
569 ----------
570 refs : `~collections.abc.Iterable` of `DatasetRef`
571 The datasets to be checked.
572 artifact_existence : `dict` [`lsst.resources.ResourcePath`, `bool`]
573 Optional mapping of datastore artifact to existence. Updated by
574 this method with details of all artifacts tested. Can be `None`
575 if the caller is not interested.
577 Returns
578 -------
579 existence : `dict` of [`DatasetRef`, `bool`]
580 Mapping from dataset to boolean indicating existence.
581 """
582 existence: dict[DatasetRef, bool] = {}
583 # Non-optimized default.
584 for ref in refs:
585 existence[ref] = self.exists(ref)
586 return existence
588 @abstractmethod
589 def exists(self, datasetRef: DatasetRef) -> bool:
590 """Check if the dataset exists in the datastore.
592 Parameters
593 ----------
594 datasetRef : `DatasetRef`
595 Reference to the required dataset.
597 Returns
598 -------
599 exists : `bool`
600 `True` if the entity exists in the `Datastore`.
601 """
602 raise NotImplementedError("Must be implemented by subclass")
604 @abstractmethod
605 def get(
606 self,
607 datasetRef: DatasetRef,
608 parameters: Mapping[str, Any] | None = None,
609 storageClass: StorageClass | str | None = None,
610 ) -> Any:
611 """Load an in-memory dataset from the store.
613 Parameters
614 ----------
615 datasetRef : `DatasetRef`
616 Reference to the required Dataset.
617 parameters : `dict`
618 `StorageClass`-specific parameters that specify a slice of the
619 Dataset to be loaded.
620 storageClass : `StorageClass` or `str`, optional
621 The storage class to be used to override the Python type
622 returned by this method. By default the returned type matches
623 the dataset type definition for this dataset. Specifying a
624 read `StorageClass` can force a different type to be returned.
625 This type must be compatible with the original type.
627 Returns
628 -------
629 inMemoryDataset : `object`
630 Requested Dataset or slice thereof as an in-memory dataset.
631 """
632 raise NotImplementedError("Must be implemented by subclass")
634 def prepare_get_for_external_client(self, ref: DatasetRef) -> list[DatasetLocationInformation] | None:
635 """Retrieve data that can be used to execute a ``get()``.
637 Parameters
638 ----------
639 ref : `DatasetRef`
640 Reference to the required dataset.
642 Returns
643 -------
644 payload : `list` [ `DatasetLocationInformation` ] | `None`
645 Information needed to perform a get() operation. Returns `None` if
646 the dataset is not known to this datastore.
647 """
648 raise NotImplementedError()
650 @abstractmethod
651 def put(
652 self, inMemoryDataset: Any, datasetRef: DatasetRef, provenance: DatasetProvenance | None = None
653 ) -> None:
654 """Write an in-memory dataset with a given `DatasetRef` to the store.
656 Parameters
657 ----------
658 inMemoryDataset : `object`
659 The Dataset to store.
660 datasetRef : `DatasetRef`
661 Reference to the associated Dataset.
662 provenance : `DatasetProvenance` or `None`, optional
663 Any provenance that should be attached to the serialized dataset.
664 Not supported by all serialization mechanisms.
666 Raises
667 ------
668 DuplicateIdError
669 Raised if datastore already has a dataset with the same dataset ID.
670 """
671 raise NotImplementedError("Must be implemented by subclass")
673 def _overrideTransferMode(self, *datasets: FileDataset, transfer: str | None = None) -> str | None:
674 """Allow ingest transfer mode to be defaulted based on datasets.
676 Parameters
677 ----------
678 *datasets : `FileDataset`
679 Each positional argument is a struct containing information about
680 a file to be ingested, including its path (either absolute or
681 relative to the datastore root, if applicable), a complete
682 `DatasetRef` (with ``dataset_id not None``), and optionally a
683 formatter class or its fully-qualified string name. If a formatter
684 is not provided, this method should populate that attribute with
685 the formatter the datastore would use for `put`. Subclasses are
686 also permitted to modify the path attribute (typically to put it
687 in what the datastore considers its standard form).
688 transfer : `str`, optional
689 How (and whether) the dataset should be added to the datastore.
690 See `ingest` for details of transfer modes.
692 Returns
693 -------
694 newTransfer : `str`
695 Transfer mode to use. Will be identical to the supplied transfer
696 mode unless "auto" is used.
697 """
698 if transfer != "auto":
699 return transfer
700 raise RuntimeError(f"{transfer} is not allowed without specialization.")
702 def _prepIngest(self, *datasets: FileDataset, transfer: str | None = None) -> IngestPrepData:
703 """Process datasets to identify which ones can be ingested.
705 Parameters
706 ----------
707 *datasets : `FileDataset`
708 Each positional argument is a struct containing information about
709 a file to be ingested, including its path (either absolute or
710 relative to the datastore root, if applicable), a complete
711 `DatasetRef` (with ``dataset_id not None``), and optionally a
712 formatter class or its fully-qualified string name. If a formatter
713 is not provided, this method should populate that attribute with
714 the formatter the datastore would use for `put`. Subclasses are
715 also permitted to modify the path attribute (typically to put it
716 in what the datastore considers its standard form).
717 transfer : `str`, optional
718 How (and whether) the dataset should be added to the datastore.
719 See `ingest` for details of transfer modes.
721 Returns
722 -------
723 data : `IngestPrepData`
724 An instance of a subclass of `IngestPrepData`, used to pass
725 arbitrary data from `_prepIngest` to `_finishIngest`. This should
726 include only the datasets this datastore can actually ingest;
727 others should be silently ignored (`Datastore.ingest` will inspect
728 `IngestPrepData.refs` and raise `DatasetTypeNotSupportedError` if
729 necessary).
731 Raises
732 ------
733 NotImplementedError
734 Raised if the datastore does not support the given transfer mode
735 (including the case where ingest is not supported at all).
736 FileNotFoundError
737 Raised if one of the given files does not exist.
738 FileExistsError
739 Raised if transfer is not `None` but the (internal) location the
740 file would be moved to is already occupied.
741 DuplicateIdError
742 Raised if dataset ID of any dataset already exists in this
743 datastore.
745 Notes
746 -----
747 This method (along with `_finishIngest`) should be implemented by
748 subclasses to provide ingest support instead of implementing `ingest`
749 directly.
751 `_prepIngest` should not modify the data repository or given files in
752 any way; all changes should be deferred to `_finishIngest`.
754 When possible, exceptions should be raised in `_prepIngest` instead of
755 `_finishIngest`. `NotImplementedError` exceptions that indicate that
756 the transfer mode is not supported must be raised by `_prepIngest`
757 instead of `_finishIngest`.
758 """
759 raise NotImplementedError(f"Datastore {self} does not support direct file-based ingest.")
761 def _finishIngest(
762 self, prepData: IngestPrepData, *, transfer: str | None = None, record_validation_info: bool = True
763 ) -> None:
764 """Complete an ingest operation.
766 Parameters
767 ----------
768 prepData : `IngestPrepData`
769 An instance of a subclass of `IngestPrepData`. Guaranteed to be
770 the direct result of a call to `_prepIngest` on this datastore.
771 transfer : `str`, optional
772 How (and whether) the dataset should be added to the datastore.
773 See `ingest` for details of transfer modes.
774 record_validation_info : `bool`, optional
775 If `True`, the default, the datastore can record validation
776 information associated with the file. If `False` the datastore
777 will not attempt to track any information such as checksums
778 or file sizes. This can be useful if such information is tracked
779 in an external system or if the file is to be compressed in place.
780 It is up to the datastore whether this parameter is relevant.
782 Raises
783 ------
784 FileNotFoundError
785 Raised if one of the given files does not exist.
786 FileExistsError
787 Raised if transfer is not `None` but the (internal) location the
788 file would be moved to is already occupied.
790 Notes
791 -----
792 This method (along with `_prepIngest`) should be implemented by
793 subclasses to provide ingest support instead of implementing `ingest`
794 directly.
795 """
796 raise NotImplementedError(f"Datastore {self} does not support direct file-based ingest.")
798 def ingest(
799 self, *datasets: FileDataset, transfer: str | None = None, record_validation_info: bool = True
800 ) -> None:
801 """Ingest one or more files into the datastore.
803 Parameters
804 ----------
805 *datasets : `FileDataset`
806 Each positional argument is a struct containing information about
807 a file to be ingested, including its path (either absolute or
808 relative to the datastore root, if applicable), a complete
809 `DatasetRef` (with ``dataset_id not None``), and optionally a
810 formatter class or its fully-qualified string name. If a formatter
811 is not provided, the one the datastore would use for ``put`` on
812 that dataset is assumed.
813 transfer : `str`, optional
814 How (and whether) the dataset should be added to the datastore.
815 If `None` (default), the file must already be in a location
816 appropriate for the datastore (e.g. within its root directory),
817 and will not be modified. Other choices include "move", "copy",
818 "link", "symlink", "relsymlink", and "hardlink". "link" is a
819 special transfer mode that will first try to make a hardlink and
820 if that fails a symlink will be used instead. "relsymlink" creates
821 a relative symlink rather than use an absolute path.
822 Most datastores do not support all transfer modes.
823 "auto" is a special option that will let the
824 data store choose the most natural option for itself.
825 record_validation_info : `bool`, optional
826 If `True`, the default, the datastore can record validation
827 information associated with the file. If `False` the datastore
828 will not attempt to track any information such as checksums
829 or file sizes. This can be useful if such information is tracked
830 in an external system or if the file is to be compressed in place.
831 It is up to the datastore whether this parameter is relevant.
833 Returns
834 -------
835 None
837 Raises
838 ------
839 NotImplementedError
840 Raised if the datastore does not support the given transfer mode
841 (including the case where ingest is not supported at all).
842 DatasetTypeNotSupportedError
843 Raised if one or more files to be ingested have a dataset type that
844 is not supported by the datastore.
845 FileNotFoundError
846 Raised if one of the given files does not exist.
847 FileExistsError
848 Raised if transfer is not `None` but the (internal) location the
849 file would be moved to is already occupied.
850 DuplicateIdError
851 Raised if dataset ID of any dataset already exists in this
852 datastore.
854 Notes
855 -----
856 Subclasses should implement `_prepIngest` and `_finishIngest` instead
857 of implementing `ingest` directly. Datastores that hold and
858 delegate to child datastores may want to call those methods as well.
860 Subclasses are encouraged to document their supported transfer modes
861 in their class documentation.
862 """
863 # Allow a datastore to select a default transfer mode
864 transfer = self._overrideTransferMode(*datasets, transfer=transfer)
865 prepData = self._prepIngest(*datasets, transfer=transfer)
866 refs = {ref.id: ref for dataset in datasets for ref in dataset.refs}
867 if refs.keys() != prepData.refs.keys():
868 unsupported = refs.keys() - prepData.refs.keys()
869 # Group unsupported refs by DatasetType for an informative
870 # but still concise error message.
871 byDatasetType = defaultdict(list)
872 for datasetId in unsupported:
873 ref = refs[datasetId]
874 byDatasetType[ref.datasetType].append(ref)
875 raise DatasetTypeNotSupportedError(
876 "DatasetType(s) not supported in ingest: "
877 + ", ".join(f"{k.name} ({len(v)} dataset(s))" for k, v in byDatasetType.items())
878 )
879 self._finishIngest(prepData, transfer=transfer, record_validation_info=record_validation_info)
881 def transfer_from(
882 self,
883 source_records: FileTransferMap,
884 refs: Collection[DatasetRef],
885 transfer: str = "auto",
886 artifact_existence: dict[ResourcePath, bool] | None = None,
887 dry_run: bool = False,
888 ) -> tuple[set[DatasetRef], set[DatasetRef]]:
889 """Transfer dataset artifacts from another datastore to this one.
891 Parameters
892 ----------
893 source_records : `FileTransferMap`
894 The artifacts to be transferred into this datastore.
895 refs : `~collections.abc.Collection` of `DatasetRef`
896 The datasets to transfer from the source datastore.
897 transfer : `str`, optional
898 How (and whether) the dataset should be added to the datastore.
899 Choices include "move", "copy",
900 "link", "symlink", "relsymlink", and "hardlink". "link" is a
901 special transfer mode that will first try to make a hardlink and
902 if that fails a symlink will be used instead. "relsymlink" creates
903 a relative symlink rather than use an absolute path.
904 Most datastores do not support all transfer modes.
905 "auto" (the default) is a special option that will let the
906 data store choose the most natural option for itself.
907 If the source location and transfer location are identical the
908 transfer mode will be ignored.
909 artifact_existence : `dict` [`lsst.resources.ResourcePath`, `bool`]
910 Optional mapping of datastore artifact to existence. Updated by
911 this method with details of all artifacts tested. Can be `None`
912 if the caller is not interested.
913 dry_run : `bool`, optional
914 Process the supplied source refs without updating the target
915 datastore.
917 Returns
918 -------
919 accepted : `set` [`DatasetRef`]
920 The datasets that were transferred.
921 rejected : `set` [`DatasetRef`]
922 The datasets that were rejected due to a constraints violation.
924 Raises
925 ------
926 TypeError
927 Raised if the two datastores are not compatible.
928 """
929 raise NotImplementedError(f"Datastore {type(self)} does not implement a transfer_from method.")
931 def getManyURIs(
932 self,
933 refs: Iterable[DatasetRef],
934 predict: bool = False,
935 allow_missing: bool = False,
936 ) -> dict[DatasetRef, DatasetRefURIs]:
937 """Return URIs associated with many datasets.
939 Parameters
940 ----------
941 refs : `~collections.abc.Iterable` of `DatasetIdRef`
942 References to the required datasets.
943 predict : `bool`, optional
944 If `True`, allow URIs to be returned of datasets that have not
945 been written.
946 allow_missing : `bool`
947 If `False`, and ``predict`` is `False`, will raise if a
948 `DatasetRef` does not exist.
950 Returns
951 -------
952 URIs : `dict` of [`DatasetRef`, `DatasetRefUris`]
953 A dict of primary and component URIs, indexed by the passed-in
954 refs.
956 Raises
957 ------
958 FileNotFoundError
959 A URI has been requested for a dataset that does not exist and
960 guessing is not allowed.
962 Notes
963 -----
964 In file-based datastores, getManyURIs does not check that the file is
965 really there, it's assuming it is if datastore is aware of the file
966 then it actually exists.
967 """
968 uris: dict[DatasetRef, DatasetRefURIs] = {}
969 missing_refs = []
970 for ref in refs:
971 try:
972 uris[ref] = self.getURIs(ref, predict=predict)
973 except FileNotFoundError:
974 missing_refs.append(ref)
975 if missing_refs and not allow_missing:
976 num_missing = len(missing_refs)
977 raise FileNotFoundError(
978 f"Missing {num_missing} refs from datastore out of "
979 f"{num_missing + len(uris)} and predict=False."
980 )
981 return uris
983 @abstractmethod
984 def getURIs(self, datasetRef: DatasetRef, predict: bool = False) -> DatasetRefURIs:
985 """Return URIs associated with dataset.
987 Parameters
988 ----------
989 datasetRef : `DatasetRef`
990 Reference to the required dataset.
991 predict : `bool`, optional
992 If the datastore does not know about the dataset, controls whether
993 it should return a predicted URI or not.
995 Returns
996 -------
997 uris : `DatasetRefURIs`
998 The URI to the primary artifact associated with this dataset (if
999 the dataset was disassembled within the datastore this may be
1000 `None`), and the URIs to any components associated with the dataset
1001 artifact. (can be empty if there are no components).
1002 """
1003 raise NotImplementedError()
1005 @abstractmethod
1006 def getURI(self, datasetRef: DatasetRef, predict: bool = False) -> ResourcePath:
1007 """URI to the Dataset.
1009 Parameters
1010 ----------
1011 datasetRef : `DatasetRef`
1012 Reference to the required Dataset.
1013 predict : `bool`
1014 If `True` attempt to predict the URI for a dataset if it does
1015 not exist in datastore.
1017 Returns
1018 -------
1019 uri : `str`
1020 URI string pointing to the Dataset within the datastore. If the
1021 Dataset does not exist in the datastore, the URI may be a guess.
1022 If the datastore does not have entities that relate well
1023 to the concept of a URI the returned URI string will be
1024 descriptive. The returned URI is not guaranteed to be obtainable.
1026 Raises
1027 ------
1028 FileNotFoundError
1029 A URI has been requested for a dataset that does not exist and
1030 guessing is not allowed.
1031 """
1032 raise NotImplementedError("Must be implemented by subclass")
1034 @abstractmethod
1035 def retrieveArtifacts(
1036 self,
1037 refs: Iterable[DatasetRef],
1038 destination: ResourcePath,
1039 transfer: str = "auto",
1040 preserve_path: bool = True,
1041 overwrite: bool = False,
1042 write_index: bool = True,
1043 add_prefix: bool = False,
1044 ) -> dict[ResourcePath, ArtifactIndexInfo]:
1045 """Retrieve the artifacts associated with the supplied refs.
1047 Parameters
1048 ----------
1049 refs : `~collections.abc.Iterable` of `DatasetRef`
1050 The datasets for which artifacts are to be retrieved.
1051 A single ref can result in multiple artifacts. The refs must
1052 be resolved.
1053 destination : `lsst.resources.ResourcePath`
1054 Location to write the artifacts.
1055 transfer : `str`, optional
1056 Method to use to transfer the artifacts. Must be one of the options
1057 supported by `lsst.resources.ResourcePath.transfer_from`.
1058 "move" is not allowed.
1059 preserve_path : `bool`, optional
1060 If `True` the full path of the artifact within the datastore
1061 is preserved. If `False` the final file component of the path
1062 is used.
1063 overwrite : `bool`, optional
1064 If `True` allow transfers to overwrite existing files at the
1065 destination.
1066 write_index : `bool`, optional
1067 If `True` write a file at the top level containing a serialization
1068 of a `ZipIndex` for the downloaded datasets.
1069 add_prefix : `bool`, optional
1070 If `True` and if ``preserve_path`` is `False`, apply a prefix to
1071 the filenames corresponding to some part of the dataset ref ID.
1072 This can be used to guarantee uniqueness.
1074 Returns
1075 -------
1076 artifact_map : `dict` [ `lsst.resources.ResourcePath`, \
1077 `ArtifactIndexInfo` ]
1078 Mapping of retrieved file to associated index information.
1080 Notes
1081 -----
1082 For non-file datastores the artifacts written to the destination
1083 may not match the representation inside the datastore. For example
1084 a hierarchical data structure in a NoSQL database may well be stored
1085 as a JSON file.
1086 """
1087 raise NotImplementedError()
1089 @abstractmethod
1090 def ingest_zip(self, zip_path: ResourcePath, transfer: str | None, *, dry_run: bool = False) -> None:
1091 """Ingest an indexed Zip file and contents.
1093 The Zip file must have an index file as created by `retrieveArtifacts`.
1095 Parameters
1096 ----------
1097 zip_path : `lsst.resources.ResourcePath`
1098 Path to the Zip file.
1099 transfer : `str`
1100 Method to use for transferring the Zip file into the datastore.
1101 dry_run : `bool`, optional
1102 If `True` the ingest will be processed without any modifications
1103 made to the target datastore and as if the target datastore did not
1104 have any of the datasets.
1106 Raises
1107 ------
1108 DuplicateIdError
1109 Raised if dataset ID of any dataset already exists in this
1110 datastore.
1111 """
1112 raise NotImplementedError()
1114 @abstractmethod
1115 def remove(self, datasetRef: DatasetRef) -> None:
1116 """Indicate to the Datastore that a Dataset can be removed.
1118 Parameters
1119 ----------
1120 datasetRef : `DatasetRef`
1121 Reference to the required Dataset.
1123 Raises
1124 ------
1125 FileNotFoundError
1126 When Dataset does not exist.
1128 Notes
1129 -----
1130 Some Datastores may implement this method as a silent no-op to
1131 disable Dataset deletion through standard interfaces.
1132 """
1133 raise NotImplementedError("Must be implemented by subclass")
1135 @abstractmethod
1136 def forget(self, refs: Iterable[DatasetRef]) -> None:
1137 """Indicate to the Datastore that it should remove all records of the
1138 given datasets, without actually deleting them.
1140 Parameters
1141 ----------
1142 refs : `~collections.abc.Iterable` [ `DatasetRef` ]
1143 References to the datasets being forgotten.
1145 Notes
1146 -----
1147 Asking a datastore to forget a `DatasetRef` it does not hold should be
1148 a silent no-op, not an error.
1149 """
1150 raise NotImplementedError("Must be implemented by subclass")
1152 @abstractmethod
1153 def trash(self, ref: DatasetRef | Iterable[DatasetRef], ignore_errors: bool = True) -> None:
1154 """Indicate to the Datastore that a Dataset can be moved to the trash.
1156 Parameters
1157 ----------
1158 ref : `DatasetRef` or iterable thereof
1159 Reference(s) to the required Dataset.
1160 ignore_errors : `bool`, optional
1161 Determine whether errors should be ignored. When multiple
1162 refs are being trashed there will be no per-ref check.
1164 Returns
1165 -------
1166 None
1168 Raises
1169 ------
1170 FileNotFoundError
1171 When Dataset does not exist and errors are not ignored. Only
1172 checked if a single ref is supplied (and not in a list).
1174 Notes
1175 -----
1176 Some Datastores may implement this method as a silent no-op to
1177 disable Dataset deletion through standard interfaces.
1178 """
1179 raise NotImplementedError("Must be implemented by subclass")
1181 @abstractmethod
1182 def emptyTrash(
1183 self, ignore_errors: bool = True, refs: Collection[DatasetRef] | None = None, dry_run: bool = False
1184 ) -> set[ResourcePath]:
1185 """Remove all datasets from the trash.
1187 Parameters
1188 ----------
1189 ignore_errors : `bool`, optional
1190 Determine whether errors should be ignored.
1191 refs : `collections.abc.Collection` [ `DatasetRef` ] or `None`
1192 Explicit list of datasets that can be removed from trash. If listed
1193 datasets are not already stored in the trash table they will be
1194 ignored. If `None` every entry in the trash table will be
1195 processed.
1196 dry_run : `bool`, optional
1197 If `True`, the trash table will be queried and results reported
1198 but no artifacts will be removed.
1200 Returns
1201 -------
1202 removed : `set` [ `lsst.resources.ResourcePath` ]
1203 List of artifacts that were removed. Can return nothing if
1204 artifacts cannot be represented by URIs.
1206 Notes
1207 -----
1208 Some Datastores may implement this method as a silent no-op to
1209 disable Dataset deletion through standard interfaces.
1210 """
1211 raise NotImplementedError("Must be implemented by subclass")
1213 @abstractmethod
1214 def transfer(self, inputDatastore: Datastore, datasetRef: DatasetRef) -> None:
1215 """Transfer a dataset from another datastore to this datastore.
1217 Parameters
1218 ----------
1219 inputDatastore : `Datastore`
1220 The external `Datastore` from which to retrieve the Dataset.
1221 datasetRef : `DatasetRef`
1222 Reference to the required Dataset.
1223 """
1224 raise NotImplementedError("Must be implemented by subclass")
1226 def export(
1227 self,
1228 refs: Iterable[DatasetRef],
1229 *,
1230 directory: ResourcePathExpression | None = None,
1231 transfer: str | None = "auto",
1232 ) -> Iterable[FileDataset]:
1233 """Export datasets for transfer to another data repository.
1235 Parameters
1236 ----------
1237 refs : `~collections.abc.Iterable` of `DatasetRef`
1238 Dataset references to be exported.
1239 directory : `str`, optional
1240 Path to a directory that should contain files corresponding to
1241 output datasets. Ignored if ``transfer`` is explicitly `None`.
1242 transfer : `str`, optional
1243 Mode that should be used to move datasets out of the repository.
1244 Valid options are the same as those of the ``transfer`` argument
1245 to ``ingest``, and datastores may similarly signal that a transfer
1246 mode is not supported by raising `NotImplementedError`. If "auto"
1247 is given and no ``directory`` is specified, `None` will be
1248 implied.
1250 Returns
1251 -------
1252 dataset : `~collections.abc.Iterable` of `DatasetTransfer`
1253 Structs containing information about the exported datasets, in the
1254 same order as ``refs``.
1256 Raises
1257 ------
1258 NotImplementedError
1259 Raised if the given transfer mode is not supported.
1260 """
1261 raise NotImplementedError(f"Transfer mode {transfer} not supported.")
1263 @abstractmethod
1264 def validateConfiguration(
1265 self, entities: Iterable[DatasetRef | DatasetType | StorageClass], logFailures: bool = False
1266 ) -> None:
1267 """Validate some of the configuration for this datastore.
1269 Parameters
1270 ----------
1271 entities : `~collections.abc.Iterable` [`DatasetRef` | `DatasetType` |\
1272 `StorageClass`]
1273 Entities to test against this configuration. Can be differing
1274 types.
1275 logFailures : `bool`, optional
1276 If `True`, output a log message for every validation error
1277 detected.
1279 Raises
1280 ------
1281 DatastoreValidationError
1282 Raised if there is a validation problem with a configuration.
1284 Notes
1285 -----
1286 Which parts of the configuration are validated is at the discretion
1287 of each Datastore implementation.
1288 """
1289 raise NotImplementedError("Must be implemented by subclass")
1291 @abstractmethod
1292 def validateKey(self, lookupKey: LookupKey, entity: DatasetRef | DatasetType | StorageClass) -> None:
1293 """Validate a specific look up key with supplied entity.
1295 Parameters
1296 ----------
1297 lookupKey : `LookupKey`
1298 Key to use to retrieve information from the datastore
1299 configuration.
1300 entity : `DatasetRef`, `DatasetType`, or `StorageClass`
1301 Entity to compare with configuration retrieved using the
1302 specified lookup key.
1304 Returns
1305 -------
1306 None
1308 Raises
1309 ------
1310 DatastoreValidationError
1311 Raised if there is a problem with the combination of entity
1312 and lookup key.
1314 Notes
1315 -----
1316 Bypasses the normal selection priorities by allowing a key that
1317 would normally not be selected to be validated.
1318 """
1319 raise NotImplementedError("Must be implemented by subclass")
1321 @abstractmethod
1322 def getLookupKeys(self) -> set[LookupKey]:
1323 """Return all the lookup keys relevant to this datastore.
1325 Returns
1326 -------
1327 keys : `set` of `LookupKey`
1328 The keys stored internally for looking up information based
1329 on `DatasetType` name or `StorageClass`.
1330 """
1331 raise NotImplementedError("Must be implemented by subclass")
1333 def needs_expanded_data_ids(
1334 self,
1335 transfer: str | None,
1336 entity: DatasetRef | DatasetType | StorageClass | None = None,
1337 ) -> bool:
1338 """Test whether this datastore needs expanded data IDs to ingest.
1340 Parameters
1341 ----------
1342 transfer : `str` or `None`
1343 Transfer mode for ingest.
1344 entity : `DatasetRef` or `DatasetType` or `StorageClass` or `None`, \
1345 optional
1346 Object representing what will be ingested. If not provided (or not
1347 specific enough), `True` may be returned even if expanded data
1348 IDs aren't necessary.
1350 Returns
1351 -------
1352 needed : `bool`
1353 If `True`, expanded data IDs may be needed. `False` only if
1354 expansion definitely isn't necessary.
1355 """
1356 return True
1358 @abstractmethod
1359 def import_records(
1360 self,
1361 data: Mapping[str, DatastoreRecordData],
1362 ) -> None:
1363 """Import datastore location and record data from an in-memory data
1364 structure.
1366 Parameters
1367 ----------
1368 data : `~collections.abc.Mapping` [ `str`, `DatastoreRecordData` ]
1369 Datastore records indexed by datastore name. May contain data for
1370 other `Datastore` instances (generally because they are chained to
1371 this one), which should be ignored.
1373 Notes
1374 -----
1375 Implementations should generally not check that any external resources
1376 (e.g. files) referred to by these records actually exist, for
1377 performance reasons; we expect higher-level code to guarantee that they
1378 do.
1380 Implementations are responsible for calling
1381 `DatastoreRegistryBridge.insert` on all datasets in ``data.locations``
1382 where the key is in `names`, as well as loading any opaque table data.
1384 Implementations may assume that datasets are either fully present or
1385 not at all (single-component exports are not permitted).
1386 """
1387 raise NotImplementedError()
1389 @abstractmethod
1390 def export_records(
1391 self,
1392 refs: Iterable[DatasetIdRef],
1393 ) -> Mapping[str, DatastoreRecordData]:
1394 """Export datastore records and locations to an in-memory data
1395 structure.
1397 Parameters
1398 ----------
1399 refs : `~collections.abc.Iterable` [ `DatasetIdRef` ]
1400 Datasets to save. This may include datasets not known to this
1401 datastore, which should be ignored. May not include component
1402 datasets.
1404 Returns
1405 -------
1406 data : `~collections.abc.Mapping` [ `str`, `DatastoreRecordData` ]
1407 Exported datastore records indexed by datastore name.
1408 """
1409 raise NotImplementedError()
1411 def export_table(self, datasets: Collection[DatasetId]) -> DatastoreRecordTable:
1412 """Export datastore records to an arrow table.
1414 Parameters
1415 ----------
1416 datasets
1417 Dataset UUIDs for the records to export.
1419 Returns
1420 -------
1421 table
1422 Datastore records table.
1423 """
1424 return DatastoreRecordTable.create_empty()
1426 def import_table(self, table: DatastoreRecordTable) -> None:
1427 """Import datastore records from an arrow table.
1429 Parameters
1430 ----------
1431 table
1432 Table containing the datastore records to import.
1434 Raises
1435 ------
1436 ValueError
1437 If the given table contains entries for a datastore that is not
1438 known to this Butler.
1439 """
1440 pass
1442 def export_predicted_records(self, refs: Iterable[DatasetRef]) -> dict[str, DatastoreRecordData]:
1443 """Export predicted datastore records and locations to an in-memory
1444 data structure.
1446 Parameters
1447 ----------
1448 refs : `~collections.abc.Iterable` [ `DatasetRef` ]
1449 Datastore records that would be used if the given refs were to
1450 exist in this datastore. No attempt is made to determine if these
1451 datasets actually exist.
1453 Returns
1454 -------
1455 data : `~collections.abc.Mapping` [ `str`, `DatastoreRecordData` ]
1456 Exported datastore records indexed by datastore name.
1457 """
1458 raise NotImplementedError()
1460 def set_retrieve_dataset_type_method(self, method: Callable[[str], DatasetType | None] | None) -> None:
1461 """Specify a method that can be used by datastore to retrieve
1462 registry-defined dataset type.
1464 Parameters
1465 ----------
1466 method : `~collections.abc.Callable` | `None`
1467 Method that takes a name of the dataset type and returns a
1468 corresponding `DatasetType` instance as defined in Registry. If
1469 dataset type name is not known to registry `None` is returned.
1471 Notes
1472 -----
1473 This method is only needed for a Datastore supporting a "trusted" mode
1474 when it does not have an access to datastore records and needs to
1475 guess dataset location based on its stored dataset type.
1476 """
1477 pass
1479 @abstractmethod
1480 def get_opaque_table_definitions(self) -> Mapping[str, DatastoreOpaqueTable]:
1481 """Make definitions of the opaque tables used by this Datastore.
1483 Returns
1484 -------
1485 tables : `~collections.abc.Mapping` [ `str`, `.ddl.TableSpec` ]
1486 Mapping of opaque table names to their definitions. This can be an
1487 empty mapping if Datastore does not use opaque tables to keep
1488 datastore records.
1489 """
1490 raise NotImplementedError()
1492 def get_file_info_for_transfer(self, dataset_ids: Iterable[DatasetId]) -> FileTransferMap:
1493 raise NotImplementedError(f"Transferring files is not supported by datastore {self}")
1495 def locate_missing_files_for_transfer(
1496 self, refs: Iterable[DatasetRef], artifact_existence: dict[ResourcePath, bool]
1497 ) -> FileTransferMap:
1498 return {}
1501class NullDatastore(Datastore):
1502 """A datastore that implements the `Datastore` API but always fails when
1503 it accepts any request.
1505 Parameters
1506 ----------
1507 config : `Config` or `~lsst.resources.ResourcePathExpression` or `None`
1508 Ignored.
1509 bridgeManager : `DatastoreRegistryBridgeManager` or `None`
1510 Ignored.
1511 butlerRoot : `~lsst.resources.ResourcePathExpression` or `None`
1512 Ignored.
1513 """
1515 @classmethod
1516 def _create_from_config(
1517 cls,
1518 config: Config,
1519 bridgeManager: DatastoreRegistryBridgeManager,
1520 butlerRoot: ResourcePathExpression | None = None,
1521 ) -> NullDatastore:
1522 return NullDatastore(config, bridgeManager, butlerRoot)
1524 def clone(self, bridgeManager: DatastoreRegistryBridgeManager) -> Datastore:
1525 return self
1527 @classmethod
1528 def setConfigRoot(cls, root: str, config: Config, full: Config, overwrite: bool = True) -> None:
1529 # Nothing to do. This is not a real Datastore.
1530 pass
1532 def __init__(
1533 self,
1534 config: Config | ResourcePathExpression | None,
1535 bridgeManager: DatastoreRegistryBridgeManager | None,
1536 butlerRoot: ResourcePathExpression | None = None,
1537 ):
1538 # Name ourselves with the timestamp the datastore
1539 # was created.
1540 self.name = f"{type(self).__name__}@{time.time()}"
1541 _LOG.debug("Creating datastore %s", self.name)
1542 self._transaction: DatastoreTransaction | None = None
1543 return
1545 def knows(self, ref: DatasetRef) -> bool:
1546 return False
1548 def exists(self, datasetRef: DatasetRef) -> bool:
1549 return False
1551 def get(
1552 self,
1553 datasetRef: DatasetRef,
1554 parameters: Mapping[str, Any] | None = None,
1555 storageClass: StorageClass | str | None = None,
1556 ) -> Any:
1557 raise FileNotFoundError("This is a no-op datastore that can not access a real datastore")
1559 def put(
1560 self, inMemoryDataset: Any, datasetRef: DatasetRef, provenance: DatasetProvenance | None = None
1561 ) -> None:
1562 raise NotImplementedError("This is a no-op datastore that can not access a real datastore")
1564 def ingest(
1565 self, *datasets: FileDataset, transfer: str | None = None, record_validation_info: bool = True
1566 ) -> None:
1567 raise NotImplementedError("This is a no-op datastore that can not access a real datastore")
1569 def transfer_from(
1570 self,
1571 source_records: FileTransferMap,
1572 refs: Iterable[DatasetRef],
1573 transfer: str = "auto",
1574 artifact_existence: dict[ResourcePath, bool] | None = None,
1575 dry_run: bool = False,
1576 ) -> tuple[set[DatasetRef], set[DatasetRef]]:
1577 raise NotImplementedError("This is a no-op datastore that can not access a real datastore")
1579 def getURIs(self, datasetRef: DatasetRef, predict: bool = False) -> DatasetRefURIs:
1580 raise FileNotFoundError("This is a no-op datastore that can not access a real datastore")
1582 def getURI(self, datasetRef: DatasetRef, predict: bool = False) -> ResourcePath:
1583 raise FileNotFoundError("This is a no-op datastore that can not access a real datastore")
1585 def ingest_zip(self, zip_path: ResourcePath, transfer: str | None, *, dry_run: bool = False) -> None:
1586 raise NotImplementedError("Can only ingest a Zip into a real datastore.")
1588 def retrieveArtifacts(
1589 self,
1590 refs: Iterable[DatasetRef],
1591 destination: ResourcePath,
1592 transfer: str = "auto",
1593 preserve_path: bool = True,
1594 overwrite: bool = False,
1595 write_index: bool = True,
1596 add_prefix: bool = False,
1597 ) -> dict[ResourcePath, ArtifactIndexInfo]:
1598 raise NotImplementedError("This is a no-op datastore that can not access a real datastore")
1600 def remove(self, datasetRef: DatasetRef) -> None:
1601 raise NotImplementedError("This is a no-op datastore that can not access a real datastore")
1603 def forget(self, refs: Iterable[DatasetRef]) -> None:
1604 raise NotImplementedError("This is a no-op datastore that can not access a real datastore")
1606 def trash(self, ref: DatasetRef | Iterable[DatasetRef], ignore_errors: bool = True) -> None:
1607 raise NotImplementedError("This is a no-op datastore that can not access a real datastore")
1609 def emptyTrash(
1610 self, ignore_errors: bool = True, refs: Collection[DatasetRef] | None = None, dry_run: bool = False
1611 ) -> set[ResourcePath]:
1612 raise NotImplementedError("This is a no-op datastore that can not access a real datastore")
1614 def transfer(self, inputDatastore: Datastore, datasetRef: DatasetRef) -> None:
1615 raise NotImplementedError("This is a no-op datastore that can not access a real datastore")
1617 def export(
1618 self,
1619 refs: Iterable[DatasetRef],
1620 *,
1621 directory: ResourcePathExpression | None = None,
1622 transfer: str | None = "auto",
1623 ) -> Iterable[FileDataset]:
1624 raise NotImplementedError("This is a no-op datastore that can not access a real datastore")
1626 def validateConfiguration(
1627 self, entities: Iterable[DatasetRef | DatasetType | StorageClass], logFailures: bool = False
1628 ) -> None:
1629 # No configuration so always validates.
1630 pass
1632 def validateKey(self, lookupKey: LookupKey, entity: DatasetRef | DatasetType | StorageClass) -> None:
1633 pass
1635 def getLookupKeys(self) -> set[LookupKey]:
1636 raise NotImplementedError("This is a no-op datastore that can not access a real datastore")
1638 def import_records(
1639 self,
1640 data: Mapping[str, DatastoreRecordData],
1641 ) -> None:
1642 raise NotImplementedError("This is a no-op datastore that can not access a real datastore")
1644 def export_records(
1645 self,
1646 refs: Iterable[DatasetIdRef],
1647 ) -> Mapping[str, DatastoreRecordData]:
1648 raise NotImplementedError("This is a no-op datastore that can not access a real datastore")
1650 def get_opaque_table_definitions(self) -> Mapping[str, DatastoreOpaqueTable]:
1651 return {}