Coverage for python / lsst / daf / butler / _butler.py: 30%

316 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-02-06 01:11 -0800

1# This file is part of daf_butler. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This software is dual licensed under the GNU General Public License and also 

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

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

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

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

14# 

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

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

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

18# (at your option) any later version. 

19# 

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

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

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

23# GNU General Public License for more details. 

24# 

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

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

27 

28from __future__ import annotations 

29 

30__all__ = ["Butler"] 

31 

32import dataclasses 

33import urllib.parse 

34import uuid 

35import warnings 

36from abc import abstractmethod 

37from collections.abc import Collection, Iterable, Iterator, Mapping, Sequence 

38from contextlib import AbstractContextManager 

39from types import EllipsisType 

40from typing import TYPE_CHECKING, Any, TextIO 

41 

42from lsst.resources import ResourcePath, ResourcePathExpression 

43from lsst.utils import doImportType 

44from lsst.utils.iteration import ensure_iterable 

45from lsst.utils.logging import getLogger 

46 

47from ._butler_collections import ButlerCollections 

48from ._butler_config import ButlerConfig, ButlerType 

49from ._butler_instance_options import ButlerInstanceOptions 

50from ._butler_metrics import ButlerMetrics 

51from ._butler_repo_index import ButlerRepoIndex 

52from ._config import Config, ConfigSubset 

53from ._exceptions import EmptyQueryResultError, InvalidQueryError 

54from ._limited_butler import LimitedButler 

55from ._query_all_datasets import QueryAllDatasetsParameters 

56from .datastore import Datastore 

57from .dimensions import DataCoordinate, DimensionConfig 

58from .registry import RegistryConfig, _RegistryFactory 

59from .repo_relocation import BUTLER_ROOT_TAG 

60from .utils import has_globs 

61 

62if TYPE_CHECKING: 

63 from ._dataset_existence import DatasetExistence 

64 from ._dataset_provenance import DatasetProvenance 

65 from ._dataset_ref import DatasetId, DatasetRef 

66 from ._dataset_type import DatasetType 

67 from ._deferredDatasetHandle import DeferredDatasetHandle 

68 from ._file_dataset import FileDataset 

69 from ._labeled_butler_factory import LabeledButlerFactoryProtocol 

70 from ._storage_class import StorageClass 

71 from ._timespan import Timespan 

72 from .datastore import DatasetRefURIs 

73 from .dimensions import DataId, DimensionGroup, DimensionRecord 

74 from .queries import Query 

75 from .registry import CollectionArgType, Registry 

76 from .transfers import RepoExportContext 

77 

78_LOG = getLogger(__name__) 

79 

80 

81@dataclasses.dataclass 

82class ParsedButlerDatasetURI: 

83 label: str 

84 dataset_id: uuid.UUID 

85 uri: str 

86 

87 

88@dataclasses.dataclass 

89class SpecificButlerDataset: 

90 butler: Butler 

91 dataset: DatasetRef | None 

92 

93 

94class _DeprecatedDefault: 

95 """Default value for a deprecated parameter.""" 

96 

97 

98class Butler(LimitedButler): # numpydoc ignore=PR02 

99 """Interface for data butler and factory for Butler instances. 

100 

101 Parameters 

102 ---------- 

103 config : `ButlerConfig`, `Config` or `str`, optional 

104 Configuration. Anything acceptable to the `ButlerConfig` constructor. 

105 If a directory path is given the configuration will be read from a 

106 ``butler.yaml`` file in that location. If `None` is given default 

107 values will be used. If ``config`` contains "cls" key then its value is 

108 used as a name of butler class and it must be a sub-class of this 

109 class, otherwise `DirectButler` is instantiated. 

110 collections : `str` or `~collections.abc.Iterable` [ `str` ], optional 

111 An expression specifying the collections to be searched (in order) when 

112 reading datasets. 

113 This may be a `str` collection name or an iterable thereof. 

114 See :ref:`daf_butler_collection_expressions` for more information. 

115 These collections are not registered automatically and must be 

116 manually registered before they are used by any method, but they may be 

117 manually registered after the `Butler` is initialized. 

118 run : `str`, optional 

119 Name of the `~CollectionType.RUN` collection new datasets should be 

120 inserted into. If ``collections`` is `None` and ``run`` is not `None`, 

121 ``collections`` will be set to ``[run]``. If not `None`, this 

122 collection will automatically be registered. If this is not set (and 

123 ``writeable`` is not set either), a read-only butler will be created. 

124 searchPaths : `list` of `str`, optional 

125 Directory paths to search when calculating the full Butler 

126 configuration. Not used if the supplied config is already a 

127 `ButlerConfig`. 

128 writeable : `bool`, optional 

129 Explicitly sets whether the butler supports write operations. If not 

130 provided, a read-write butler is created if any of ``run``, ``tags``, 

131 or ``chains`` is non-empty. 

132 inferDefaults : `bool`, optional 

133 If `True` (default) infer default data ID values from the values 

134 present in the datasets in ``collections``: if all collections have the 

135 same value (or no value) for a governor dimension, that value will be 

136 the default for that dimension. Nonexistent collections are ignored. 

137 If a default value is provided explicitly for a governor dimension via 

138 ``**kwargs``, no default will be inferred for that dimension. 

139 without_datastore : `bool`, optional 

140 If `True` do not attach a datastore to this butler. Any attempts 

141 to use a datastore will fail. 

142 metrics : `ButlerMetrics` or `None` 

143 External metrics object to be used for tracking butler usage. If `None` 

144 a new metrics object is created. 

145 **kwargs : `typing.Any` 

146 Additional keyword arguments passed to a constructor of actual butler 

147 class. 

148 

149 Notes 

150 ----- 

151 The preferred way to instantiate Butler is via the `from_config` method. 

152 The call to ``Butler(...)`` is equivalent to ``Butler.from_config(...)``, 

153 but ``mypy`` will complain about the former. 

154 """ 

155 

156 def __new__( 

157 cls, 

158 config: Config | ResourcePathExpression | None = None, 

159 *, 

160 collections: Any = None, 

161 run: str | None = None, 

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

163 writeable: bool | None = None, 

164 inferDefaults: bool = True, 

165 without_datastore: bool = False, 

166 metrics: ButlerMetrics | None = None, 

167 **kwargs: Any, 

168 ) -> Butler: 

169 if cls is Butler: 

170 return Butler.from_config( 

171 config=config, 

172 collections=collections, 

173 run=run, 

174 searchPaths=searchPaths, 

175 writeable=writeable, 

176 inferDefaults=inferDefaults, 

177 without_datastore=without_datastore, 

178 metrics=metrics, 

179 **kwargs, 

180 ) 

181 

182 # Note: we do not pass any parameters to __new__, Python will pass them 

183 # to __init__ after __new__ returns sub-class instance. 

184 return super().__new__(cls) 

185 

186 @classmethod 

187 def from_config( 

188 cls, 

189 config: Config | ResourcePathExpression | None = None, 

190 *, 

191 collections: Any = None, 

192 run: str | None = None, 

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

194 writeable: bool | None = None, 

195 inferDefaults: bool = True, 

196 without_datastore: bool = False, 

197 metrics: ButlerMetrics | None = None, 

198 **kwargs: Any, 

199 ) -> Butler: 

200 """Create butler instance from configuration. 

201 

202 Parameters 

203 ---------- 

204 config : `ButlerConfig`, `Config` or `str`, optional 

205 Configuration. Anything acceptable to the `ButlerConfig` 

206 constructor. If a directory path is given the configuration will be 

207 read from a ``butler.yaml`` file in that location. If `None` is 

208 given default values will be used. If ``config`` contains "cls" key 

209 then its value is used as a name of butler class and it must be a 

210 sub-class of this class, otherwise `DirectButler` is instantiated. 

211 collections : `str` or `~collections.abc.Iterable` [ `str` ], optional 

212 An expression specifying the collections to be searched (in order) 

213 when reading datasets. 

214 This may be a `str` collection name or an iterable thereof. 

215 See :ref:`daf_butler_collection_expressions` for more information. 

216 These collections are not registered automatically and must be 

217 manually registered before they are used by any method, but they 

218 may be manually registered after the `Butler` is initialized. 

219 run : `str`, optional 

220 Name of the `~CollectionType.RUN` collection new datasets should be 

221 inserted into. If ``collections`` is `None` and ``run`` is not 

222 `None`, ``collections`` will be set to ``[run]``. If not `None`, 

223 this collection will automatically be registered. If this is not 

224 set (and ``writeable`` is not set either), a read-only butler will 

225 be created. 

226 searchPaths : `list` of `str`, optional 

227 Directory paths to search when calculating the full Butler 

228 configuration. Not used if the supplied config is already a 

229 `ButlerConfig`. 

230 writeable : `bool`, optional 

231 Explicitly sets whether the butler supports write operations. If 

232 not provided, a read-write butler is created if any of ``run``, 

233 ``tags``, or ``chains`` is non-empty. 

234 inferDefaults : `bool`, optional 

235 If `True` (default) infer default data ID values from the values 

236 present in the datasets in ``collections``: if all collections have 

237 the same value (or no value) for a governor dimension, that value 

238 will be the default for that dimension. Nonexistent collections 

239 are ignored. If a default value is provided explicitly for a 

240 governor dimension via ``**kwargs``, no default will be inferred 

241 for that dimension. 

242 without_datastore : `bool`, optional 

243 If `True` do not attach a datastore to this butler. Any attempts 

244 to use a datastore will fail. 

245 metrics : `ButlerMetrics` or `None`, optional 

246 Metrics object to record butler usage statistics. 

247 **kwargs : `typing.Any` 

248 Default data ID key-value pairs. These may only identify 

249 "governor" dimensions like ``instrument`` and ``skymap``. 

250 

251 Returns 

252 ------- 

253 butler : `Butler` 

254 A `Butler` constructed from the given configuration. 

255 

256 Notes 

257 ----- 

258 Calling this factory method is identical to calling 

259 ``Butler(config, ...)``. Its only raison d'être is that ``mypy`` 

260 complains about ``Butler()`` call. 

261 

262 Examples 

263 -------- 

264 While there are many ways to control exactly how a `Butler` interacts 

265 with the collections in its `Registry`, the most common cases are still 

266 simple. 

267 

268 For a read-only `Butler` that searches one collection, do:: 

269 

270 butler = Butler.from_config( 

271 "/path/to/repo", collections=["u/alice/DM-50000"] 

272 ) 

273 

274 For a read-write `Butler` that writes to and reads from a 

275 `~CollectionType.RUN` collection:: 

276 

277 butler = Butler.from_config( 

278 "/path/to/repo", run="u/alice/DM-50000/a" 

279 ) 

280 

281 The `Butler` passed to a ``PipelineTask`` is often much more complex, 

282 because we want to write to one `~CollectionType.RUN` collection but 

283 read from several others (as well):: 

284 

285 butler = Butler.from_config( 

286 "/path/to/repo", 

287 run="u/alice/DM-50000/a", 

288 collections=[ 

289 "u/alice/DM-50000/a", 

290 "u/bob/DM-49998", 

291 "HSC/defaults", 

292 ], 

293 ) 

294 

295 This butler will `put` new datasets to the run ``u/alice/DM-50000/a``. 

296 Datasets will be read first from that run (since it appears first in 

297 the chain), and then from ``u/bob/DM-49998`` and finally 

298 ``HSC/defaults``. 

299 

300 Finally, one can always create a `Butler` with no collections:: 

301 

302 butler = Butler.from_config("/path/to/repo", writeable=True) 

303 

304 This can be extremely useful when you just want to use 

305 ``butler.registry``, e.g. for inserting dimension data or managing 

306 collections, or when the collections you want to use with the butler 

307 are not consistent. Passing ``writeable`` explicitly here is only 

308 necessary if you want to be able to make changes to the repo - usually 

309 the value for ``writeable`` can be guessed from the collection 

310 arguments provided, but it defaults to `False` when there are not 

311 collection arguments. 

312 """ 

313 # DirectButler used to have a way to specify a "copy constructor" by 

314 # passing the "butler" parameter to its constructor. This has 

315 # been moved out of the constructor into Butler.clone(). 

316 butler = kwargs.pop("butler", None) 

317 metrics = metrics if metrics is not None else ButlerMetrics() 

318 if butler is not None: 

319 if not isinstance(butler, Butler): 

320 raise TypeError("'butler' parameter must be a Butler instance") 

321 if config is not None or searchPaths is not None or writeable is not None: 

322 raise TypeError( 

323 "Cannot pass 'config', 'searchPaths', or 'writeable' arguments with 'butler' argument." 

324 ) 

325 return butler.clone( 

326 collections=collections, run=run, inferDefaults=inferDefaults, metrics=metrics, dataId=kwargs 

327 ) 

328 

329 options = ButlerInstanceOptions( 

330 collections=collections, 

331 run=run, 

332 writeable=writeable, 

333 inferDefaults=inferDefaults, 

334 metrics=metrics, 

335 kwargs=kwargs, 

336 ) 

337 

338 # Load the Butler configuration. This may involve searching the 

339 # environment to locate a configuration file. 

340 butler_config = ButlerConfig(config, searchPaths=searchPaths, without_datastore=without_datastore) 

341 butler_type = butler_config.get_butler_type() 

342 

343 # Make DirectButler if class is not specified. 

344 match butler_type: 

345 case ButlerType.DIRECT: 

346 from .direct_butler import DirectButler 

347 

348 return DirectButler.create_from_config( 

349 butler_config, 

350 options=options, 

351 without_datastore=without_datastore, 

352 ) 

353 case ButlerType.REMOTE: 

354 from .remote_butler._factory import RemoteButlerFactory 

355 

356 # Assume this is being created by a client who would like 

357 # default caching of remote datasets. 

358 factory = RemoteButlerFactory.create_factory_from_config(butler_config) 

359 return factory.create_butler_with_credentials_from_environment( 

360 butler_options=options, enable_datastore_cache=True 

361 ) 

362 case _: 

363 raise TypeError(f"Unknown Butler type '{butler_type}'") 

364 

365 @staticmethod 

366 def makeRepo( 

367 root: ResourcePathExpression, 

368 config: Config | str | None = None, 

369 dimensionConfig: Config | str | None = None, 

370 standalone: bool = False, 

371 searchPaths: list[str] | None = None, 

372 forceConfigRoot: bool = True, 

373 outfile: ResourcePathExpression | None = None, 

374 overwrite: bool = False, 

375 ) -> Config: 

376 """Create an empty data repository by adding a butler.yaml config 

377 to a repository root directory. 

378 

379 Parameters 

380 ---------- 

381 root : `lsst.resources.ResourcePathExpression` 

382 Path or URI to the root location of the new repository. Will be 

383 created if it does not exist. 

384 config : `Config` or `str`, optional 

385 Configuration to write to the repository, after setting any 

386 root-dependent Registry or Datastore config options. Can not 

387 be a `ButlerConfig` or a `ConfigSubset`. If `None`, default 

388 configuration will be used. Root-dependent config options 

389 specified in this config are overwritten if ``forceConfigRoot`` 

390 is `True`. 

391 dimensionConfig : `Config` or `str`, optional 

392 Configuration for dimensions, will be used to initialize registry 

393 database. 

394 standalone : `bool` 

395 If True, write all expanded defaults, not just customized or 

396 repository-specific settings. 

397 This (mostly) decouples the repository from the default 

398 configuration, insulating it from changes to the defaults (which 

399 may be good or bad, depending on the nature of the changes). 

400 Future *additions* to the defaults will still be picked up when 

401 initializing `Butlers` to repos created with ``standalone=True``. 

402 searchPaths : `list` of `str`, optional 

403 Directory paths to search when calculating the full butler 

404 configuration. 

405 forceConfigRoot : `bool`, optional 

406 If `False`, any values present in the supplied ``config`` that 

407 would normally be reset are not overridden and will appear 

408 directly in the output config. This allows non-standard overrides 

409 of the root directory for a datastore or registry to be given. 

410 If this parameter is `True` the values for ``root`` will be 

411 forced into the resulting config if appropriate. 

412 outfile : `lss.resources.ResourcePathExpression`, optional 

413 If not-`None`, the output configuration will be written to this 

414 location rather than into the repository itself. Can be a URI 

415 string. Can refer to a directory that will be used to write 

416 ``butler.yaml``. 

417 overwrite : `bool`, optional 

418 Create a new configuration file even if one already exists 

419 in the specified output location. Default is to raise 

420 an exception. 

421 

422 Returns 

423 ------- 

424 config : `Config` 

425 The updated `Config` instance written to the repo. 

426 

427 Raises 

428 ------ 

429 ValueError 

430 Raised if a ButlerConfig or ConfigSubset is passed instead of a 

431 regular Config (as these subclasses would make it impossible to 

432 support ``standalone=False``). 

433 FileExistsError 

434 Raised if the output config file already exists. 

435 os.error 

436 Raised if the directory does not exist, exists but is not a 

437 directory, or cannot be created. 

438 

439 Notes 

440 ----- 

441 Note that when ``standalone=False`` (the default), the configuration 

442 search path (see `ConfigSubset.defaultSearchPaths`) that was used to 

443 construct the repository should also be used to construct any Butlers 

444 to avoid configuration inconsistencies. 

445 """ 

446 if isinstance(config, ButlerConfig | ConfigSubset): 

447 raise ValueError("makeRepo must be passed a regular Config without defaults applied.") 

448 

449 # Ensure that the root of the repository exists or can be made 

450 root_uri = ResourcePath(root, forceDirectory=True) 

451 root_uri.mkdir() 

452 

453 config = Config(config) 

454 

455 # If we are creating a new repo from scratch with relative roots, 

456 # do not propagate an explicit root from the config file 

457 if "root" in config: 

458 del config["root"] 

459 

460 full = ButlerConfig(config, searchPaths=searchPaths) # this applies defaults 

461 imported_class = doImportType(full["datastore", "cls"]) 

462 if not issubclass(imported_class, Datastore): 

463 raise TypeError(f"Imported datastore class {full['datastore', 'cls']} is not a Datastore") 

464 datastoreClass: type[Datastore] = imported_class 

465 datastoreClass.setConfigRoot(BUTLER_ROOT_TAG, config, full, overwrite=forceConfigRoot) 

466 

467 # if key exists in given config, parse it, otherwise parse the defaults 

468 # in the expanded config 

469 if config.get(("registry", "db")): 

470 registryConfig = RegistryConfig(config) 

471 else: 

472 registryConfig = RegistryConfig(full) 

473 defaultDatabaseUri = registryConfig.makeDefaultDatabaseUri(BUTLER_ROOT_TAG) 

474 if defaultDatabaseUri is not None: 

475 Config.updateParameters( 

476 RegistryConfig, config, full, toUpdate={"db": defaultDatabaseUri}, overwrite=forceConfigRoot 

477 ) 

478 else: 

479 Config.updateParameters(RegistryConfig, config, full, toCopy=("db",), overwrite=forceConfigRoot) 

480 

481 if standalone: 

482 config.merge(full) 

483 else: 

484 # Always expand the registry.managers section into the per-repo 

485 # config, because after the database schema is created, it's not 

486 # allowed to change anymore. Note that in the standalone=True 

487 # branch, _everything_ in the config is expanded, so there's no 

488 # need to special case this. 

489 Config.updateParameters(RegistryConfig, config, full, toMerge=("managers",), overwrite=False) 

490 configURI: ResourcePathExpression 

491 if outfile is not None: 

492 # When writing to a separate location we must include 

493 # the root of the butler repo in the config else it won't know 

494 # where to look. 

495 config["root"] = root_uri.geturl() 

496 configURI = outfile 

497 else: 

498 configURI = root_uri 

499 # Check that if obscore key is present then its config must be there 

500 # too, this is to avoid common mistake when people copy butler.yaml 

501 # from existing repo with obscore but do not fill its config. 

502 if (obscore_key := ("registry", "managers", "obscore")) in config: 

503 obscore_config_key = ("registry", "managers", "obscore", "config") 

504 if obscore_config_key not in config or not config[obscore_config_key]: 

505 warnings.warn( 

506 "Obscore manager is declared in registry configuration, " 

507 "but obscore configuration is missing, obscore manager will be removed.", 

508 stacklevel=2, 

509 ) 

510 del config[obscore_key] 

511 # Strip obscore configuration, if it is present, before writing config 

512 # to a file, obscore config will be stored in registry. 

513 if (obscore_config_key := ("registry", "managers", "obscore", "config")) in config: 

514 config_to_write = config.copy() 

515 del config_to_write[obscore_config_key] 

516 config_to_write.dumpToUri(configURI, overwrite=overwrite) 

517 # configFile attribute is updated, need to copy it to original. 

518 config.configFile = config_to_write.configFile 

519 else: 

520 config.dumpToUri(configURI, overwrite=overwrite) 

521 

522 # Create Registry and populate tables 

523 registryConfig = RegistryConfig(config.get("registry")) 

524 dimensionConfig = DimensionConfig(dimensionConfig) 

525 registry = _RegistryFactory(registryConfig).create_from_config( 

526 dimensionConfig=dimensionConfig, butlerRoot=root_uri 

527 ) 

528 registry.close() 

529 

530 _LOG.verbose("Wrote new Butler configuration file to %s", configURI) 

531 

532 return config 

533 

534 @classmethod 

535 def get_repo_uri(cls, label: str, return_label: bool = False) -> ResourcePath: 

536 """Look up the label in a butler repository index. 

537 

538 Parameters 

539 ---------- 

540 label : `str` 

541 Label of the Butler repository to look up. 

542 return_label : `bool`, optional 

543 If ``label`` cannot be found in the repository index (either 

544 because index is not defined or ``label`` is not in the index) and 

545 ``return_label`` is `True` then return ``ResourcePath(label)``. 

546 If ``return_label`` is `False` (default) then an exception will be 

547 raised instead. 

548 

549 Returns 

550 ------- 

551 uri : `lsst.resources.ResourcePath` 

552 URI to the Butler repository associated with the given label or 

553 default value if it is provided. 

554 

555 Raises 

556 ------ 

557 KeyError 

558 Raised if the label is not found in the index, or if an index 

559 is not defined, and ``return_label`` is `False`. 

560 

561 Notes 

562 ----- 

563 See `~lsst.daf.butler.ButlerRepoIndex` for details on how the 

564 information is discovered. 

565 """ 

566 return ButlerRepoIndex.get_repo_uri(label, return_label) 

567 

568 @classmethod 

569 def get_known_repos(cls) -> set[str]: 

570 """Retrieve the list of known repository labels. 

571 

572 Returns 

573 ------- 

574 repos : `set` of `str` 

575 All the known labels. Can be empty if no index can be found. 

576 

577 Notes 

578 ----- 

579 See `~lsst.daf.butler.ButlerRepoIndex` for details on how the 

580 information is discovered. 

581 """ 

582 return ButlerRepoIndex.get_known_repos() 

583 

584 @classmethod 

585 def parse_dataset_uri(cls, uri: str) -> ParsedButlerDatasetURI: 

586 """Extract the butler label and dataset ID from a dataset URI. 

587 

588 Parameters 

589 ---------- 

590 uri : `str` 

591 The dataset URI to parse. 

592 

593 Returns 

594 ------- 

595 parsed : `ParsedButlerDatasetURI` 

596 The label associated with the butler repository from which this 

597 dataset originates and the ID of the dataset. 

598 

599 Notes 

600 ----- 

601 Supports dataset URIs of the forms 

602 ``ivo://org.rubinobs/usdac/dr1?repo=butler_label&id=UUID`` (see 

603 DMTN-302) and ``butler://butler_label/UUID``. The ``butler`` URI is 

604 deprecated and can not include ``/`` in the label string. ``ivo`` URIs 

605 can include anything supported by the `Butler` constructor, including 

606 paths to repositories and alias labels. 

607 

608 ivo://org.rubinobs/dr1?repo=/repo/main&id=UUID 

609 

610 will return a label of ``/repo/main``. 

611 

612 This method does not attempt to check that the dataset exists in the 

613 labeled butler. 

614 

615 Since the IVOID can be issued by any publisher to represent a Butler 

616 dataset there is no validation of the path or netloc component of the 

617 URI. The only requirement is that there are ``id`` and ``repo`` keys 

618 in the ``ivo`` URI query component. 

619 """ 

620 parsed = urllib.parse.urlparse(uri) 

621 parsed_scheme = parsed.scheme.lower() 

622 if parsed_scheme == "ivo": 

623 # Do not validate the netloc or the path values. 

624 qs = urllib.parse.parse_qs(parsed.query) 

625 if "repo" not in qs or "id" not in qs: 

626 raise ValueError(f"Missing 'repo' and/or 'id' query parameters in IVOID {uri}.") 

627 if len(qs["repo"]) != 1 or len(qs["id"]) != 1: 

628 raise ValueError(f"Butler IVOID only supports a single value of repo and id, got {uri}") 

629 label = qs["repo"][0] 

630 id_ = qs["id"][0] 

631 elif parsed_scheme == "butler": 

632 label = parsed.netloc # Butler label is case sensitive. 

633 # Need to strip the leading /. 

634 id_ = parsed.path[1:] 

635 else: 

636 raise ValueError(f"Unrecognized URI scheme: {uri!r}") 

637 # Strip trailing/leading whitespace from label. 

638 label = label.strip() 

639 if not label: 

640 raise ValueError(f"No butler repository label found in uri {uri!r}") 

641 try: 

642 dataset_id = uuid.UUID(hex=id_) 

643 except Exception as e: 

644 e.add_note(f"Error extracting dataset ID from uri {uri!r} with dataset ID string {id_!r}") 

645 raise 

646 

647 return ParsedButlerDatasetURI(label=label, dataset_id=dataset_id, uri=uri) 

648 

649 @classmethod 

650 def get_dataset_from_uri( 

651 cls, uri: str, factory: LabeledButlerFactoryProtocol | None = None 

652 ) -> SpecificButlerDataset: 

653 """Get the dataset associated with the given dataset URI. 

654 

655 Parameters 

656 ---------- 

657 uri : `str` 

658 The URI associated with a dataset. 

659 factory : `LabeledButlerFactoryProtocol` or `None`, optional 

660 Bound factory function that will be given the butler label 

661 and receive a `Butler`. If this is not provided the label 

662 will be tried directly. 

663 

664 Returns 

665 ------- 

666 result : `SpecificButlerDataset` 

667 The butler associated with this URI and the dataset itself. 

668 The dataset can be `None` if the UUID is valid but the dataset 

669 is not known to this butler. 

670 """ 

671 parsed = cls.parse_dataset_uri(uri) 

672 butler: Butler | None = None 

673 if factory is not None: 

674 # If the label is not recognized, it might be a path. 

675 try: 

676 butler = factory(parsed.label) 

677 except KeyError: 

678 pass 

679 if butler is None: 

680 butler = cls.from_config(parsed.label) 

681 return SpecificButlerDataset(butler=butler, dataset=butler.get_dataset(parsed.dataset_id)) 

682 

683 @abstractmethod 

684 def _caching_context(self) -> AbstractContextManager[None]: 

685 """Context manager that enables caching.""" 

686 raise NotImplementedError() 

687 

688 @abstractmethod 

689 def transaction(self) -> AbstractContextManager[None]: 

690 """Context manager supporting `Butler` transactions. 

691 

692 Transactions can be nested. 

693 """ 

694 raise NotImplementedError() 

695 

696 @abstractmethod 

697 def put( 

698 self, 

699 obj: Any, 

700 datasetRefOrType: DatasetRef | DatasetType | str, 

701 /, 

702 dataId: DataId | None = None, 

703 *, 

704 run: str | None = None, 

705 provenance: DatasetProvenance | None = None, 

706 **kwargs: Any, 

707 ) -> DatasetRef: 

708 """Store and register a dataset. 

709 

710 Parameters 

711 ---------- 

712 obj : `object` 

713 The dataset. 

714 datasetRefOrType : `DatasetRef`, `DatasetType`, or `str` 

715 When `DatasetRef` is provided, ``dataId`` should be `None`. 

716 Otherwise the `DatasetType` or name thereof. If a fully resolved 

717 `DatasetRef` is given the run and ID are used directly. 

718 dataId : `dict` or `DataCoordinate` 

719 A `dict` of `Dimension` link name, value pairs that label the 

720 `DatasetRef` within a Collection. When `None`, a `DatasetRef` 

721 should be provided as the second argument. 

722 run : `str`, optional 

723 The name of the run the dataset should be added to, overriding 

724 ``self.run``. Not used if a resolved `DatasetRef` is provided. 

725 provenance : `DatasetProvenance` or `None`, optional 

726 Any provenance that should be attached to the serialized dataset. 

727 Not supported by all serialization mechanisms. 

728 **kwargs 

729 Additional keyword arguments used to augment or construct a 

730 `DataCoordinate`. See `DataCoordinate.standardize` 

731 parameters. Not used if a resolve `DatasetRef` is provided. 

732 

733 Returns 

734 ------- 

735 ref : `DatasetRef` 

736 A reference to the stored dataset, updated with the correct id if 

737 given. 

738 

739 Raises 

740 ------ 

741 TypeError 

742 Raised if the butler is read-only or if no run has been provided. 

743 """ 

744 raise NotImplementedError() 

745 

746 @abstractmethod 

747 def getDeferred( 

748 self, 

749 datasetRefOrType: DatasetRef | DatasetType | str, 

750 /, 

751 dataId: DataId | None = None, 

752 *, 

753 parameters: dict | None = None, 

754 collections: Any = None, 

755 storageClass: str | StorageClass | None = None, 

756 timespan: Timespan | None = None, 

757 **kwargs: Any, 

758 ) -> DeferredDatasetHandle: 

759 """Create a `DeferredDatasetHandle` which can later retrieve a dataset, 

760 after an immediate registry lookup. 

761 

762 Parameters 

763 ---------- 

764 datasetRefOrType : `DatasetRef`, `DatasetType`, or `str` 

765 When `DatasetRef` the `dataId` should be `None`. 

766 Otherwise the `DatasetType` or name thereof. 

767 dataId : `dict` or `DataCoordinate`, optional 

768 A `dict` of `Dimension` link name, value pairs that label the 

769 `DatasetRef` within a Collection. When `None`, a `DatasetRef` 

770 should be provided as the first argument. 

771 parameters : `dict` 

772 Additional StorageClass-defined options to control reading, 

773 typically used to efficiently read only a subset of the dataset. 

774 collections : Any, optional 

775 Collections to be searched, overriding ``self.collections``. 

776 Can be any of the types supported by the ``collections`` argument 

777 to butler construction. 

778 storageClass : `StorageClass` or `str`, optional 

779 The storage class to be used to override the Python type 

780 returned by this method. By default the returned type matches 

781 the dataset type definition for this dataset. Specifying a 

782 read `StorageClass` can force a different type to be returned. 

783 This type must be compatible with the original type. 

784 timespan : `Timespan` or `None`, optional 

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

786 If not provided and this is a calibration dataset type, an attempt 

787 will be made to find the timespan from any temporal coordinate 

788 in the data ID. 

789 **kwargs 

790 Additional keyword arguments used to augment or construct a 

791 `DataId`. See `DataId` parameters. 

792 

793 Returns 

794 ------- 

795 obj : `DeferredDatasetHandle` 

796 A handle which can be used to retrieve a dataset at a later time. 

797 

798 Raises 

799 ------ 

800 LookupError 

801 Raised if no matching dataset exists in the `Registry` or 

802 datastore. 

803 ValueError 

804 Raised if a resolved `DatasetRef` was passed as an input, but it 

805 differs from the one found in the registry. 

806 TypeError 

807 Raised if no collections were provided. 

808 """ 

809 raise NotImplementedError() 

810 

811 @abstractmethod 

812 def get( 

813 self, 

814 datasetRefOrType: DatasetRef | DatasetType | str, 

815 /, 

816 dataId: DataId | None = None, 

817 *, 

818 parameters: dict[str, Any] | None = None, 

819 collections: Any = None, 

820 storageClass: StorageClass | str | None = None, 

821 timespan: Timespan | None = None, 

822 **kwargs: Any, 

823 ) -> Any: 

824 """Retrieve a stored dataset. 

825 

826 Parameters 

827 ---------- 

828 datasetRefOrType : `DatasetRef`, `DatasetType`, or `str` 

829 When `DatasetRef` the `dataId` should be `None`. 

830 Otherwise the `DatasetType` or name thereof. 

831 If a resolved `DatasetRef`, the associated dataset 

832 is returned directly without additional querying. 

833 dataId : `dict` or `DataCoordinate` 

834 A `dict` of `Dimension` link name, value pairs that label the 

835 `DatasetRef` within a Collection. When `None`, a `DatasetRef` 

836 should be provided as the first argument. 

837 parameters : `dict` 

838 Additional StorageClass-defined options to control reading, 

839 typically used to efficiently read only a subset of the dataset. 

840 collections : Any, optional 

841 Collections to be searched, overriding ``self.collections``. 

842 Can be any of the types supported by the ``collections`` argument 

843 to butler construction. 

844 storageClass : `StorageClass` or `str`, optional 

845 The storage class to be used to override the Python type 

846 returned by this method. By default the returned type matches 

847 the dataset type definition for this dataset. Specifying a 

848 read `StorageClass` can force a different type to be returned. 

849 This type must be compatible with the original type. 

850 timespan : `Timespan` or `None`, optional 

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

852 If not provided and this is a calibration dataset type, an attempt 

853 will be made to find the timespan from any temporal coordinate 

854 in the data ID. 

855 **kwargs 

856 Additional keyword arguments used to augment or construct a 

857 `DataCoordinate`. See `DataCoordinate.standardize` 

858 parameters. 

859 

860 Returns 

861 ------- 

862 obj : `object` 

863 The dataset. 

864 

865 Raises 

866 ------ 

867 LookupError 

868 Raised if no matching dataset exists in the `Registry`. 

869 TypeError 

870 Raised if no collections were provided. 

871 

872 Notes 

873 ----- 

874 When looking up datasets in a `~CollectionType.CALIBRATION` collection, 

875 this method requires that the given data ID include temporal dimensions 

876 beyond the dimensions of the dataset type itself, in order to find the 

877 dataset with the appropriate validity range. For example, a "bias" 

878 dataset with native dimensions ``{instrument, detector}`` could be 

879 fetched with a ``{instrument, detector, exposure}`` data ID, because 

880 ``exposure`` is a temporal dimension. 

881 """ 

882 raise NotImplementedError() 

883 

884 @abstractmethod 

885 def getURIs( 

886 self, 

887 datasetRefOrType: DatasetRef | DatasetType | str, 

888 /, 

889 dataId: DataId | None = None, 

890 *, 

891 predict: bool = False, 

892 collections: Any = None, 

893 run: str | None = None, 

894 **kwargs: Any, 

895 ) -> DatasetRefURIs: 

896 """Return the URIs associated with the dataset. 

897 

898 Parameters 

899 ---------- 

900 datasetRefOrType : `DatasetRef`, `DatasetType`, or `str` 

901 When `DatasetRef` the `dataId` should be `None`. 

902 Otherwise the `DatasetType` or name thereof. 

903 dataId : `dict` or `DataCoordinate` 

904 A `dict` of `Dimension` link name, value pairs that label the 

905 `DatasetRef` within a Collection. When `None`, a `DatasetRef` 

906 should be provided as the first argument. 

907 predict : `bool` 

908 If `True`, allow URIs to be returned of datasets that have not 

909 been written. 

910 collections : Any, optional 

911 Collections to be searched, overriding ``self.collections``. 

912 Can be any of the types supported by the ``collections`` argument 

913 to butler construction. 

914 run : `str`, optional 

915 Run to use for predictions, overriding ``self.run``. 

916 **kwargs 

917 Additional keyword arguments used to augment or construct a 

918 `DataCoordinate`. See `DataCoordinate.standardize` 

919 parameters. 

920 

921 Returns 

922 ------- 

923 uris : `DatasetRefURIs` 

924 The URI to the primary artifact associated with this dataset (if 

925 the dataset was disassembled within the datastore this may be 

926 `None`), and the URIs to any components associated with the dataset 

927 artifact. (can be empty if there are no components). 

928 """ 

929 raise NotImplementedError() 

930 

931 def getURI( 

932 self, 

933 datasetRefOrType: DatasetRef | DatasetType | str, 

934 /, 

935 dataId: DataId | None = None, 

936 *, 

937 predict: bool = False, 

938 collections: Any = None, 

939 run: str | None = None, 

940 **kwargs: Any, 

941 ) -> ResourcePath: 

942 """Return the URI to the Dataset. 

943 

944 Parameters 

945 ---------- 

946 datasetRefOrType : `DatasetRef`, `DatasetType`, or `str` 

947 When `DatasetRef` the `dataId` should be `None`. 

948 Otherwise the `DatasetType` or name thereof. 

949 dataId : `dict` or `DataCoordinate` 

950 A `dict` of `Dimension` link name, value pairs that label the 

951 `DatasetRef` within a Collection. When `None`, a `DatasetRef` 

952 should be provided as the first argument. 

953 predict : `bool` 

954 If `True`, allow URIs to be returned of datasets that have not 

955 been written. 

956 collections : Any, optional 

957 Collections to be searched, overriding ``self.collections``. 

958 Can be any of the types supported by the ``collections`` argument 

959 to butler construction. 

960 run : `str`, optional 

961 Run to use for predictions, overriding ``self.run``. 

962 **kwargs 

963 Additional keyword arguments used to augment or construct a 

964 `DataCoordinate`. See `DataCoordinate.standardize` 

965 parameters. 

966 

967 Returns 

968 ------- 

969 uri : `lsst.resources.ResourcePath` 

970 URI pointing to the Dataset within the datastore. If the 

971 Dataset does not exist in the datastore, and if ``predict`` is 

972 `True`, the URI will be a prediction and will include a URI 

973 fragment "#predicted". 

974 If the datastore does not have entities that relate well 

975 to the concept of a URI the returned URI string will be 

976 descriptive. The returned URI is not guaranteed to be obtainable. 

977 

978 Raises 

979 ------ 

980 LookupError 

981 A URI has been requested for a dataset that does not exist and 

982 guessing is not allowed. 

983 ValueError 

984 Raised if a resolved `DatasetRef` was passed as an input, but it 

985 differs from the one found in the registry. 

986 TypeError 

987 Raised if no collections were provided. 

988 RuntimeError 

989 Raised if a URI is requested for a dataset that consists of 

990 multiple artifacts. 

991 """ 

992 primary, components = self.getURIs( 

993 datasetRefOrType, dataId=dataId, predict=predict, collections=collections, run=run, **kwargs 

994 ) 

995 

996 if primary is None or components: 

997 raise RuntimeError( 

998 f"Dataset ({datasetRefOrType}) includes distinct URIs for components. " 

999 "Use Butler.getURIs() instead." 

1000 ) 

1001 return primary 

1002 

1003 @abstractmethod 

1004 def get_dataset_type(self, name: str) -> DatasetType: 

1005 """Get the `DatasetType`. 

1006 

1007 Parameters 

1008 ---------- 

1009 name : `str` 

1010 Name of the type. 

1011 

1012 Returns 

1013 ------- 

1014 type : `DatasetType` 

1015 The `DatasetType` associated with the given name. 

1016 

1017 Raises 

1018 ------ 

1019 lsst.daf.butler.MissingDatasetTypeError 

1020 Raised if the requested dataset type has not been registered. 

1021 

1022 Notes 

1023 ----- 

1024 This method handles component dataset types automatically, though most 

1025 other operations do not. 

1026 """ 

1027 raise NotImplementedError() 

1028 

1029 @abstractmethod 

1030 def get_dataset( 

1031 self, 

1032 id: DatasetId | str, 

1033 *, 

1034 storage_class: str | StorageClass | None = None, 

1035 dimension_records: bool = False, 

1036 datastore_records: bool = False, 

1037 ) -> DatasetRef | None: 

1038 """Retrieve a Dataset entry. 

1039 

1040 Parameters 

1041 ---------- 

1042 id : `DatasetId` 

1043 The unique identifier for the dataset, as an instance of 

1044 `uuid.UUID` or a string containing a hexadecimal number. 

1045 storage_class : `str` or `StorageClass` or `None` 

1046 A storage class to use when creating the returned entry. If given 

1047 it must be compatible with the default storage class. 

1048 dimension_records : `bool`, optional 

1049 If `True` the ref will be expanded and contain dimension records. 

1050 datastore_records : `bool`, optional 

1051 If `True` the ref will contain associated datastore records. 

1052 

1053 Returns 

1054 ------- 

1055 ref : `DatasetRef` or `None` 

1056 A ref to the Dataset, or `None` if no matching Dataset 

1057 was found. 

1058 """ 

1059 raise NotImplementedError() 

1060 

1061 @abstractmethod 

1062 def get_many_datasets(self, ids: Iterable[DatasetId | str]) -> list[DatasetRef]: 

1063 """Retrieve a list of dataset entries. 

1064 

1065 Parameters 

1066 ---------- 

1067 ids : `~collections.abc.Iterable` [ `DatasetId` or `str` ] 

1068 The unique identifiers for the datasets, as instances of 

1069 `uuid.UUID` or strings containing a hexadecimal number. 

1070 

1071 Returns 

1072 ------- 

1073 refs : `list` [ `DatasetRef` ] 

1074 A list containing a `DatasetRef` for each of the given dataset IDs. 

1075 If a dataset was not found, no error is thrown -- it is just not 

1076 included in the list. The returned datasets are in no particular 

1077 order. 

1078 """ 

1079 raise NotImplementedError() 

1080 

1081 @abstractmethod 

1082 def find_dataset( 

1083 self, 

1084 dataset_type: DatasetType | str, 

1085 data_id: DataId | None = None, 

1086 *, 

1087 collections: str | Sequence[str] | None = None, 

1088 timespan: Timespan | None = None, 

1089 storage_class: str | StorageClass | None = None, 

1090 dimension_records: bool = False, 

1091 datastore_records: bool = False, 

1092 **kwargs: Any, 

1093 ) -> DatasetRef | None: 

1094 """Find a dataset given its `DatasetType` and data ID. 

1095 

1096 This can be used to obtain a `DatasetRef` that permits the dataset to 

1097 be read from a `Datastore`. If the dataset is a component and can not 

1098 be found using the provided dataset type, a dataset ref for the parent 

1099 will be returned instead but with the correct dataset type. 

1100 

1101 Parameters 

1102 ---------- 

1103 dataset_type : `DatasetType` or `str` 

1104 A `DatasetType` or the name of one. If this is a `DatasetType` 

1105 instance, its storage class will be respected and propagated to 

1106 the output, even if it differs from the dataset type definition 

1107 in the registry, as long as the storage classes are convertible. 

1108 data_id : `dict` or `DataCoordinate`, optional 

1109 A `dict`-like object containing the `Dimension` links that identify 

1110 the dataset within a collection. If it is a `dict` the dataId 

1111 can include dimension record values such as ``day_obs`` and 

1112 ``seq_num`` or ``full_name`` that can be used to derive the 

1113 primary dimension. 

1114 collections : `str` or `list` [`str`], optional 

1115 A an ordered list of collections to search for the dataset. 

1116 Defaults to ``self.defaults.collections``. 

1117 timespan : `Timespan`, optional 

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

1119 If not provided, any `~CollectionType.CALIBRATION` collections 

1120 matched by the ``collections`` argument will not be searched. 

1121 storage_class : `str` or `StorageClass` or `None` 

1122 A storage class to use when creating the returned entry. If given 

1123 it must be compatible with the default storage class. 

1124 dimension_records : `bool`, optional 

1125 If `True` the ref will be expanded and contain dimension records. 

1126 datastore_records : `bool`, optional 

1127 If `True` the ref will contain associated datastore records. 

1128 **kwargs 

1129 Additional keyword arguments passed to 

1130 `DataCoordinate.standardize` to convert ``dataId`` to a true 

1131 `DataCoordinate` or augment an existing one. This can also include 

1132 dimension record metadata that can be used to derive a primary 

1133 dimension value. 

1134 

1135 Returns 

1136 ------- 

1137 ref : `DatasetRef` 

1138 A reference to the dataset, or `None` if no matching Dataset 

1139 was found. 

1140 

1141 Raises 

1142 ------ 

1143 lsst.daf.butler.NoDefaultCollectionError 

1144 Raised if ``collections`` is `None` and 

1145 ``self.collections`` is `None`. 

1146 LookupError 

1147 Raised if one or more data ID keys are missing. 

1148 lsst.daf.butler.MissingDatasetTypeError 

1149 Raised if the dataset type does not exist. 

1150 lsst.daf.butler.MissingCollectionError 

1151 Raised if any of ``collections`` does not exist in the registry. 

1152 

1153 Notes 

1154 ----- 

1155 This method simply returns `None` and does not raise an exception even 

1156 when the set of collections searched is intrinsically incompatible with 

1157 the dataset type, e.g. if ``datasetType.isCalibration() is False``, but 

1158 only `~CollectionType.CALIBRATION` collections are being searched. 

1159 This may make it harder to debug some lookup failures, but the behavior 

1160 is intentional; we consider it more important that failed searches are 

1161 reported consistently, regardless of the reason, and that adding 

1162 additional collections that do not contain a match to the search path 

1163 never changes the behavior. 

1164 

1165 This method handles component dataset types automatically, though most 

1166 other query operations do not. 

1167 """ 

1168 raise NotImplementedError() 

1169 

1170 @abstractmethod 

1171 def retrieve_artifacts_zip( 

1172 self, 

1173 refs: Iterable[DatasetRef], 

1174 destination: ResourcePathExpression, 

1175 overwrite: bool = True, 

1176 ) -> ResourcePath: 

1177 """Retrieve artifacts from a Butler and place in ZIP file. 

1178 

1179 Parameters 

1180 ---------- 

1181 refs : `~collections.abc.Iterable` [ `DatasetRef` ] 

1182 The datasets to be included in the zip file. 

1183 destination : `lsst.resources.ResourcePathExpression` 

1184 Directory to write the new ZIP file. This directory will 

1185 also be used as a staging area for the datasets being downloaded 

1186 from the datastore. 

1187 overwrite : `bool`, optional 

1188 If `False` the output Zip will not be written if a file of the 

1189 same name is already present in ``destination``. 

1190 

1191 Returns 

1192 ------- 

1193 zip_file : `lsst.resources.ResourcePath` 

1194 The path to the new ZIP file. 

1195 

1196 Raises 

1197 ------ 

1198 ValueError 

1199 Raised if there are no refs to retrieve. 

1200 """ 

1201 raise NotImplementedError() 

1202 

1203 @abstractmethod 

1204 def retrieveArtifacts( 

1205 self, 

1206 refs: Iterable[DatasetRef], 

1207 destination: ResourcePathExpression, 

1208 transfer: str = "auto", 

1209 preserve_path: bool = True, 

1210 overwrite: bool = False, 

1211 ) -> list[ResourcePath]: 

1212 """Retrieve the artifacts associated with the supplied refs. 

1213 

1214 Parameters 

1215 ---------- 

1216 refs : iterable of `DatasetRef` 

1217 The datasets for which artifacts are to be retrieved. 

1218 A single ref can result in multiple artifacts. The refs must 

1219 be resolved. 

1220 destination : `lsst.resources.ResourcePath` or `str` 

1221 Location to write the artifacts. 

1222 transfer : `str`, optional 

1223 Method to use to transfer the artifacts. Must be one of the options 

1224 supported by `~lsst.resources.ResourcePath.transfer_from()`. 

1225 "move" is not allowed. 

1226 preserve_path : `bool`, optional 

1227 If `True` the full path of the artifact within the datastore 

1228 is preserved. If `False` the final file component of the path 

1229 is used. 

1230 overwrite : `bool`, optional 

1231 If `True` allow transfers to overwrite existing files at the 

1232 destination. 

1233 

1234 Returns 

1235 ------- 

1236 targets : `list` of `lsst.resources.ResourcePath` 

1237 URIs of file artifacts in destination location. Order is not 

1238 preserved. 

1239 

1240 Notes 

1241 ----- 

1242 For non-file datastores the artifacts written to the destination 

1243 may not match the representation inside the datastore. For example 

1244 a hierarchical data structure in a NoSQL database may well be stored 

1245 as a JSON file. 

1246 """ 

1247 raise NotImplementedError() 

1248 

1249 @abstractmethod 

1250 def exists( 

1251 self, 

1252 dataset_ref_or_type: DatasetRef | DatasetType | str, 

1253 /, 

1254 data_id: DataId | None = None, 

1255 *, 

1256 full_check: bool = True, 

1257 collections: Any = None, 

1258 **kwargs: Any, 

1259 ) -> DatasetExistence: 

1260 """Indicate whether a dataset is known to Butler registry and 

1261 datastore. 

1262 

1263 Parameters 

1264 ---------- 

1265 dataset_ref_or_type : `DatasetRef`, `DatasetType`, or `str` 

1266 When `DatasetRef` the `dataId` should be `None`. 

1267 Otherwise the `DatasetType` or name thereof. 

1268 data_id : `dict` or `DataCoordinate` 

1269 A `dict` of `Dimension` link name, value pairs that label the 

1270 `DatasetRef` within a Collection. When `None`, a `DatasetRef` 

1271 should be provided as the first argument. 

1272 full_check : `bool`, optional 

1273 If `True`, a check will be made for the actual existence of a 

1274 dataset artifact. This will involve additional overhead due to 

1275 the need to query an external system. If `False`, this check will 

1276 be omitted, and the registry and datastore will solely be asked 

1277 if they know about the dataset but no direct check for the 

1278 artifact will be performed. 

1279 collections : Any, optional 

1280 Collections to be searched, overriding ``self.collections``. 

1281 Can be any of the types supported by the ``collections`` argument 

1282 to butler construction. 

1283 **kwargs 

1284 Additional keyword arguments used to augment or construct a 

1285 `DataCoordinate`. See `DataCoordinate.standardize` 

1286 parameters. 

1287 

1288 Returns 

1289 ------- 

1290 existence : `DatasetExistence` 

1291 Object indicating whether the dataset is known to registry and 

1292 datastore. Evaluates to `True` if the dataset is present and known 

1293 to both. 

1294 """ 

1295 raise NotImplementedError() 

1296 

1297 @abstractmethod 

1298 def _exists_many( 

1299 self, 

1300 refs: Iterable[DatasetRef], 

1301 /, 

1302 *, 

1303 full_check: bool = True, 

1304 ) -> dict[DatasetRef, DatasetExistence]: 

1305 """Indicate whether multiple datasets are known to Butler registry and 

1306 datastore. 

1307 

1308 This is an experimental API that may change at any moment. 

1309 

1310 Parameters 

1311 ---------- 

1312 refs : iterable of `DatasetRef` 

1313 The datasets to be checked. 

1314 full_check : `bool`, optional 

1315 If `True`, a check will be made for the actual existence of each 

1316 dataset artifact. This will involve additional overhead due to 

1317 the need to query an external system. If `False`, this check will 

1318 be omitted, and the registry and datastore will solely be asked 

1319 if they know about the dataset(s) but no direct check for the 

1320 artifact(s) will be performed. 

1321 

1322 Returns 

1323 ------- 

1324 existence : dict of [`DatasetRef`, `DatasetExistence`] 

1325 Mapping from the given dataset refs to an enum indicating the 

1326 status of the dataset in registry and datastore. 

1327 Each value evaluates to `True` if the dataset is present and known 

1328 to both. 

1329 """ 

1330 raise NotImplementedError() 

1331 

1332 @abstractmethod 

1333 def removeRuns( 

1334 self, 

1335 names: Iterable[str], 

1336 unstore: bool | type[_DeprecatedDefault] = _DeprecatedDefault, 

1337 *, 

1338 unlink_from_chains: bool = False, 

1339 ) -> None: 

1340 """Remove one or more `~CollectionType.RUN` collections and the 

1341 datasets within them. 

1342 

1343 Parameters 

1344 ---------- 

1345 names : `~collections.abc.Iterable` [ `str` ] 

1346 The names of the collections to remove. 

1347 unstore : `bool`, optional 

1348 If `True` (default), delete datasets from all datastores in which 

1349 they are present, and attempt to rollback the registry deletions if 

1350 datastore deletions fail (which may not always be possible). If 

1351 `False`, datastore records for these datasets are still removed, 

1352 but any artifacts (e.g. files) will not be. This parameter is now 

1353 deprecated and no longer has any effect. Files are always deleted 

1354 from datastores unless they were ingested using full URIs. 

1355 unlink_from_chains : `bool`, optional 

1356 If `True` remove the RUN collection from any chains prior to 

1357 removing the RUN. If `False` the removal will fail if any chains 

1358 still refer to the RUN. 

1359 

1360 Raises 

1361 ------ 

1362 TypeError 

1363 Raised if one or more collections are not of type 

1364 `~CollectionType.RUN`. 

1365 """ 

1366 raise NotImplementedError() 

1367 

1368 @abstractmethod 

1369 def ingest( 

1370 self, 

1371 *datasets: FileDataset, 

1372 transfer: str | None = "auto", 

1373 record_validation_info: bool = True, 

1374 skip_existing: bool = False, 

1375 ) -> None: 

1376 """Store and register one or more datasets that already exist on disk. 

1377 

1378 Parameters 

1379 ---------- 

1380 *datasets : `FileDataset` 

1381 Each positional argument is a struct containing information about 

1382 a file to be ingested, including its URI (either absolute or 

1383 relative to the datastore root, if applicable), a resolved 

1384 `DatasetRef`, and optionally a formatter class or its 

1385 fully-qualified string name. If a formatter is not provided, the 

1386 formatter that would be used for `put` is assumed. On successful 

1387 ingest all `FileDataset.formatter` attributes will be set to the 

1388 formatter class used. `FileDataset.path` attributes may be modified 

1389 to put paths in whatever the datastore considers a standardized 

1390 form. 

1391 transfer : `str`, optional 

1392 If not `None`, must be one of 'auto', 'move', 'copy', 'direct', 

1393 'split', 'hardlink', 'relsymlink' or 'symlink', indicating how to 

1394 transfer the file. 

1395 record_validation_info : `bool`, optional 

1396 If `True`, the default, the datastore can record validation 

1397 information associated with the file. If `False` the datastore 

1398 will not attempt to track any information such as checksums 

1399 or file sizes. This can be useful if such information is tracked 

1400 in an external system or if the file is to be compressed in place. 

1401 It is up to the datastore whether this parameter is relevant. 

1402 skip_existing : `bool`, optional 

1403 If `True`, a dataset will not be ingested if a dataset with the 

1404 same dataset ID already exists in the datastore. 

1405 If `False` (the default), a `ConflictingDefinitionError` will be 

1406 raised if any datasets with the same dataset ID already exist 

1407 in the datastore. 

1408 

1409 Returns 

1410 ------- 

1411 None 

1412 

1413 Raises 

1414 ------ 

1415 TypeError 

1416 Raised if the butler is read-only or if no run was provided. 

1417 NotImplementedError 

1418 Raised if the `Datastore` does not support the given transfer mode. 

1419 DatasetTypeNotSupportedError 

1420 Raised if one or more files to be ingested have a dataset type that 

1421 is not supported by the `Datastore`.. 

1422 FileNotFoundError 

1423 Raised if one of the given files does not exist. 

1424 FileExistsError 

1425 Raised if transfer is not `None` but the (internal) location the 

1426 file would be moved to is already occupied. 

1427 ConflictingDefinitionError 

1428 Raised if a dataset already exists in the repository and 

1429 ``skip_existing`` is `False`. 

1430 

1431 Notes 

1432 ----- 

1433 This operation is not fully exception safe: if a database operation 

1434 fails, the given `FileDataset` instances may be only partially updated. 

1435 

1436 It is atomic in terms of database operations (they will either all 

1437 succeed or all fail) providing the database engine implements 

1438 transactions correctly. It will attempt to be atomic in terms of 

1439 filesystem operations as well, but this cannot be implemented 

1440 rigorously for most datastores. 

1441 """ 

1442 raise NotImplementedError() 

1443 

1444 @abstractmethod 

1445 def ingest_zip( 

1446 self, 

1447 zip_file: ResourcePathExpression, 

1448 transfer: str = "auto", 

1449 *, 

1450 transfer_dimensions: bool = False, 

1451 dry_run: bool = False, 

1452 skip_existing: bool = False, 

1453 ) -> None: 

1454 """Ingest a Zip file into this butler. 

1455 

1456 The Zip file must have been created by `retrieve_artifacts_zip`. 

1457 

1458 Parameters 

1459 ---------- 

1460 zip_file : `lsst.resources.ResourcePathExpression` 

1461 Path to the Zip file. 

1462 transfer : `str`, optional 

1463 Method to use to transfer the Zip into the datastore. 

1464 transfer_dimensions : `bool`, optional 

1465 If `True`, dimension record data associated with the new datasets 

1466 will be transferred from the Zip file, if present. 

1467 dry_run : `bool`, optional 

1468 If `True` the ingest will be processed without any modifications 

1469 made to the target butler and as if the target butler did not 

1470 have any of the datasets. 

1471 skip_existing : `bool`, optional 

1472 If `True`, a zip will not be ingested if the dataset entries listed 

1473 in the index with the same dataset ID already exists in the butler. 

1474 If `False` (the default), a `ConflictingDefinitionError` will be 

1475 raised if any datasets with the same dataset ID already exist 

1476 in the repository. If, somehow, some datasets are known to the 

1477 butler and some are not, this is currently treated as an error 

1478 rather than attempting to do a partial ingest. 

1479 

1480 Notes 

1481 ----- 

1482 Run collections and dataset types are created as needed. 

1483 """ 

1484 raise NotImplementedError() 

1485 

1486 @abstractmethod 

1487 def export( 

1488 self, 

1489 *, 

1490 directory: str | None = None, 

1491 filename: str | None = None, 

1492 format: str | None = None, 

1493 transfer: str | None = None, 

1494 ) -> AbstractContextManager[RepoExportContext]: 

1495 """Export datasets from the repository represented by this `Butler`. 

1496 

1497 This method is a context manager that returns a helper object 

1498 (`RepoExportContext`) that is used to indicate what information from 

1499 the repository should be exported. 

1500 

1501 Parameters 

1502 ---------- 

1503 directory : `str`, optional 

1504 Directory dataset files should be written to if ``transfer`` is not 

1505 `None`. 

1506 filename : `str`, optional 

1507 Name for the file that will include database information associated 

1508 with the exported datasets. If this is not an absolute path and 

1509 ``directory`` is not `None`, it will be written to ``directory`` 

1510 instead of the current working directory. Defaults to 

1511 "export.{format}". 

1512 format : `str`, optional 

1513 File format for the database information file. If `None`, the 

1514 extension of ``filename`` will be used. 

1515 transfer : `str`, optional 

1516 Transfer mode passed to `Datastore.export`. 

1517 

1518 Raises 

1519 ------ 

1520 TypeError 

1521 Raised if the set of arguments passed is inconsistent. 

1522 

1523 Examples 

1524 -------- 

1525 Typically the `Registry.queryDataIds` and `Registry.queryDatasets` 

1526 methods are used to provide the iterables over data IDs and/or datasets 

1527 to be exported:: 

1528 

1529 with butler.export("exports.yaml") as export: 

1530 # Export all flats, but none of the dimension element rows 

1531 # (i.e. data ID information) associated with them. 

1532 export.saveDatasets( 

1533 butler.registry.queryDatasets("flat"), elements=() 

1534 ) 

1535 # Export all datasets that start with "deepCoadd_" and all of 

1536 # their associated data ID information. 

1537 export.saveDatasets(butler.registry.queryDatasets("deepCoadd_*")) 

1538 """ 

1539 raise NotImplementedError() 

1540 

1541 @abstractmethod 

1542 def import_( 

1543 self, 

1544 *, 

1545 directory: ResourcePathExpression | None = None, 

1546 filename: ResourcePathExpression | TextIO | None = None, 

1547 format: str | None = None, 

1548 transfer: str | None = None, 

1549 skip_dimensions: set | None = None, 

1550 record_validation_info: bool = True, 

1551 without_datastore: bool = False, 

1552 ) -> None: 

1553 """Import datasets into this repository that were exported from a 

1554 different butler repository via `~lsst.daf.butler.Butler.export`. 

1555 

1556 Parameters 

1557 ---------- 

1558 directory : `~lsst.resources.ResourcePathExpression`, optional 

1559 Directory containing dataset files to import from. If `None`, 

1560 ``filename`` and all dataset file paths specified therein must 

1561 be absolute. 

1562 filename : `~lsst.resources.ResourcePathExpression` or `TextIO` 

1563 A stream or name of file that contains database information 

1564 associated with the exported datasets, typically generated by 

1565 `~lsst.daf.butler.Butler.export`. If this a string (name) or 

1566 `~lsst.resources.ResourcePath` and is not an absolute path, 

1567 it will first be looked for relative to ``directory`` and if not 

1568 found there it will be looked for in the current working 

1569 directory. Defaults to "export.{format}". 

1570 format : `str`, optional 

1571 File format for ``filename``. If `None`, the extension of 

1572 ``filename`` will be used. 

1573 transfer : `str`, optional 

1574 Transfer mode passed to `~lsst.daf.butler.Datastore.ingest`. 

1575 skip_dimensions : `set`, optional 

1576 Names of dimensions that should be skipped and not imported. 

1577 record_validation_info : `bool`, optional 

1578 If `True`, the default, the datastore can record validation 

1579 information associated with the file. If `False` the datastore 

1580 will not attempt to track any information such as checksums 

1581 or file sizes. This can be useful if such information is tracked 

1582 in an external system or if the file is to be compressed in place. 

1583 It is up to the datastore whether this parameter is relevant. 

1584 without_datastore : `bool`, optional 

1585 If `True` only registry records will be imported and the datastore 

1586 will be ignored. 

1587 

1588 Raises 

1589 ------ 

1590 TypeError 

1591 Raised if the set of arguments passed is inconsistent, or if the 

1592 butler is read-only. 

1593 """ 

1594 raise NotImplementedError() 

1595 

1596 @abstractmethod 

1597 def transfer_dimension_records_from( 

1598 self, source_butler: LimitedButler | Butler, source_refs: Iterable[DatasetRef | DataCoordinate] 

1599 ) -> None: 

1600 """Transfer dimension records to this Butler from another Butler. 

1601 

1602 Parameters 

1603 ---------- 

1604 source_butler : `LimitedButler` or `Butler` 

1605 Butler from which the records are to be transferred. If data IDs 

1606 in ``source_refs`` are not expanded then this has to be a full 

1607 `Butler` whose registry will be used to expand data IDs. If the 

1608 source refs contain coordinates that are used to populate other 

1609 records then this will also need to be a full `Butler`. 

1610 source_refs : iterable of `DatasetRef` or `DataCoordinate` 

1611 Datasets or data IDs defined in the source butler whose dimension 

1612 records should be transferred to this butler. 

1613 """ 

1614 raise NotImplementedError() 

1615 

1616 @abstractmethod 

1617 def transfer_from( 

1618 self, 

1619 source_butler: LimitedButler, 

1620 source_refs: Iterable[DatasetRef], 

1621 transfer: str = "auto", 

1622 skip_missing: bool = True, 

1623 register_dataset_types: bool = False, 

1624 transfer_dimensions: bool = False, 

1625 dry_run: bool = False, 

1626 ) -> Collection[DatasetRef]: 

1627 """Transfer datasets to this Butler from a run in another Butler. 

1628 

1629 Parameters 

1630 ---------- 

1631 source_butler : `LimitedButler` 

1632 Butler from which the datasets are to be transferred. If data IDs 

1633 in ``source_refs`` are not expanded then this has to be a full 

1634 `Butler` whose registry will be used to expand data IDs. 

1635 source_refs : iterable of `DatasetRef` 

1636 Datasets defined in the source butler that should be transferred to 

1637 this butler. In most circumstances, ``transfer_from`` is faster if 

1638 the dataset refs are expanded. 

1639 transfer : `str`, optional 

1640 Transfer mode passed to `~lsst.daf.butler.Datastore.transfer_from`. 

1641 skip_missing : `bool` 

1642 If `True`, datasets with no datastore artifact associated with 

1643 them are not transferred. If `False` a registry entry will be 

1644 created even if no datastore record is created (and so will 

1645 look equivalent to the dataset being unstored). 

1646 register_dataset_types : `bool` 

1647 If `True` any missing dataset types are registered. Otherwise 

1648 an exception is raised. 

1649 transfer_dimensions : `bool`, optional 

1650 If `True`, dimension record data associated with the new datasets 

1651 will be transferred. 

1652 dry_run : `bool`, optional 

1653 If `True` the transfer will be processed without any modifications 

1654 made to the target butler and as if the target butler did not 

1655 have any of the datasets. 

1656 

1657 Returns 

1658 ------- 

1659 refs : `list` of `DatasetRef` 

1660 The refs added to this Butler. 

1661 

1662 Notes 

1663 ----- 

1664 The datastore artifact has to exist for a transfer 

1665 to be made but non-existence is not an error. 

1666 

1667 Datasets that already exist in this run will be skipped. 

1668 

1669 The datasets are imported as part of a transaction, although 

1670 dataset types are registered before the transaction is started. 

1671 This means that it is possible for a dataset type to be registered 

1672 even though transfer has failed. 

1673 """ 

1674 raise NotImplementedError() 

1675 

1676 @abstractmethod 

1677 def validateConfiguration( 

1678 self, 

1679 logFailures: bool = False, 

1680 datasetTypeNames: Iterable[str] | None = None, 

1681 ignore: Iterable[str] | None = None, 

1682 ) -> None: 

1683 """Validate butler configuration. 

1684 

1685 Checks that each `DatasetType` can be stored in the `Datastore`. 

1686 

1687 Parameters 

1688 ---------- 

1689 logFailures : `bool`, optional 

1690 If `True`, output a log message for every validation error 

1691 detected. 

1692 datasetTypeNames : iterable of `str`, optional 

1693 The `DatasetType` names that should be checked. This allows 

1694 only a subset to be selected. 

1695 ignore : iterable of `str`, optional 

1696 Names of DatasetTypes to skip over. This can be used to skip 

1697 known problems. If a named `DatasetType` corresponds to a 

1698 composite, all components of that `DatasetType` will also be 

1699 ignored. 

1700 

1701 Raises 

1702 ------ 

1703 ButlerValidationError 

1704 Raised if there is some inconsistency with how this Butler 

1705 is configured. 

1706 """ 

1707 raise NotImplementedError() 

1708 

1709 @property 

1710 @abstractmethod 

1711 def collection_chains(self) -> ButlerCollections: 

1712 """Object with methods for modifying collection chains 

1713 (`~lsst.daf.butler.ButlerCollections`). 

1714 

1715 Deprecated. Replaced with ``collections`` property. 

1716 """ 

1717 raise NotImplementedError() 

1718 

1719 @property 

1720 @abstractmethod 

1721 def collections(self) -> ButlerCollections: 

1722 """Object with methods for modifying and querying collections 

1723 (`~lsst.daf.butler.ButlerCollections`). 

1724 

1725 Use of this object is preferred over `registry` wherever possible. 

1726 """ 

1727 raise NotImplementedError() 

1728 

1729 @property 

1730 @abstractmethod 

1731 def run(self) -> str | None: 

1732 """Name of the run this butler writes outputs to by default (`str` or 

1733 `None`). 

1734 """ 

1735 raise NotImplementedError() 

1736 

1737 @property 

1738 @abstractmethod 

1739 def registry(self) -> Registry: 

1740 """The object that manages dataset metadata and relationships 

1741 (`Registry`). 

1742 

1743 Many operations that don't involve reading or writing butler datasets 

1744 are accessible only via `Registry` methods. Eventually these methods 

1745 will be replaced by equivalent `Butler` methods. 

1746 """ 

1747 raise NotImplementedError() 

1748 

1749 @abstractmethod 

1750 def query(self) -> AbstractContextManager[Query]: 

1751 """Context manager returning a `Query` object used for construction 

1752 and execution of complex queries. 

1753 """ 

1754 raise NotImplementedError() 

1755 

1756 def query_data_ids( 

1757 self, 

1758 dimensions: DimensionGroup | Iterable[str] | str, 

1759 *, 

1760 data_id: DataId | None = None, 

1761 where: str = "", 

1762 bind: Mapping[str, Any] | None = None, 

1763 with_dimension_records: bool = False, 

1764 order_by: Iterable[str] | str | None = None, 

1765 limit: int | None = -20_000, 

1766 explain: bool = True, 

1767 **kwargs: Any, 

1768 ) -> list[DataCoordinate]: 

1769 """Query for data IDs matching user-provided criteria. 

1770 

1771 Parameters 

1772 ---------- 

1773 dimensions : `DimensionGroup`, `str`, or \ 

1774 `~collections.abc.Iterable` [`str`] 

1775 The dimensions of the data IDs to yield, as either `DimensionGroup` 

1776 instances or `str`. Will be automatically expanded to a complete 

1777 `DimensionGroup`. 

1778 data_id : `dict` or `DataCoordinate`, optional 

1779 A data ID whose key-value pairs are used as equality constraints 

1780 in the query. 

1781 where : `str`, optional 

1782 A string expression similar to a SQL WHERE clause. May involve 

1783 any column of a dimension table or (as a shortcut for the primary 

1784 key column of a dimension table) dimension name. See 

1785 :ref:`daf_butler_dimension_expressions` for more information. 

1786 bind : `~collections.abc.Mapping`, optional 

1787 Mapping containing literal values that should be injected into the 

1788 ``where`` expression, keyed by the identifiers they replace. 

1789 Values of collection type can be expanded in some cases; see 

1790 :ref:`daf_butler_dimension_expressions_identifiers` for more 

1791 information. 

1792 with_dimension_records : `bool`, optional 

1793 If `True` (default is `False`) then returned data IDs will have 

1794 dimension records. 

1795 order_by : `~collections.abc.Iterable` [`str`] or `str`, optional 

1796 Names of the columns/dimensions to use for ordering returned data 

1797 IDs. Column name can be prefixed with minus (``-``) to use 

1798 descending ordering. 

1799 limit : `int` or `None`, optional 

1800 Upper limit on the number of returned records. `None` can be used 

1801 if no limit is wanted. A limit of ``0`` means that the query will 

1802 be executed and validated but no results will be returned. In this 

1803 case there will be no exception even if ``explain`` is `True`. 

1804 If a negative value is given a warning will be issued if the number 

1805 of results is capped by that limit. 

1806 explain : `bool`, optional 

1807 If `True` (default) then `EmptyQueryResultError` exception is 

1808 raised when resulting list is empty. The exception contains 

1809 non-empty list of strings explaining possible causes for empty 

1810 result. 

1811 **kwargs 

1812 Additional keyword arguments are forwarded to 

1813 `DataCoordinate.standardize` when processing the ``data_id`` 

1814 argument (and may be used to provide a constraining data ID even 

1815 when the ``data_id`` argument is `None`). 

1816 

1817 Returns 

1818 ------- 

1819 dataIds : `list` [`DataCoordinate`] 

1820 Data IDs matching the given query parameters. These are always 

1821 guaranteed to identify all dimensions (`DataCoordinate.hasFull` 

1822 returns `True`). 

1823 

1824 Raises 

1825 ------ 

1826 lsst.daf.butler.registry.DataIdError 

1827 Raised when ``data_id`` or keyword arguments specify unknown 

1828 dimensions or values, or when they contain inconsistent values. 

1829 lsst.daf.butler.registry.UserExpressionError 

1830 Raised when ``where`` expression is invalid. 

1831 lsst.daf.butler.EmptyQueryResultError 

1832 Raised when query generates empty result and ``explain`` is set to 

1833 `True`. 

1834 TypeError 

1835 Raised when the arguments are incompatible. 

1836 """ 

1837 if data_id is None: 

1838 data_id = DataCoordinate.make_empty(self.dimensions) 

1839 if order_by is None: 

1840 order_by = [] 

1841 query_limit = limit 

1842 warn_limit = False 

1843 if limit is not None and limit < 0: 

1844 query_limit = abs(limit) + 1 

1845 warn_limit = True 

1846 with self.query() as query: 

1847 result = ( 

1848 query.data_ids(dimensions) 

1849 .where(data_id, where, bind=bind, **kwargs) 

1850 .order_by(*ensure_iterable(order_by)) 

1851 .limit(query_limit) 

1852 ) 

1853 if with_dimension_records: 

1854 result = result.with_dimension_records() 

1855 data_ids = list(result) 

1856 if warn_limit and len(data_ids) == query_limit: 

1857 # We asked for one too many so must remove that from the list. 

1858 data_ids.pop(-1) 

1859 assert limit is not None # For mypy. 

1860 _LOG.warning("More data IDs are available than the requested limit of %d.", abs(limit)) 

1861 if explain and (limit is None or limit != 0) and not data_ids: 

1862 raise EmptyQueryResultError(list(result.explain_no_results())) 

1863 return data_ids 

1864 

1865 def query_datasets( 

1866 self, 

1867 dataset_type: str | DatasetType, 

1868 collections: str | Iterable[str] | None = None, 

1869 *, 

1870 find_first: bool = True, 

1871 data_id: DataId | None = None, 

1872 where: str = "", 

1873 bind: Mapping[str, Any] | None = None, 

1874 with_dimension_records: bool = False, 

1875 order_by: Iterable[str] | str | None = None, 

1876 limit: int | None = -20_000, 

1877 explain: bool = True, 

1878 **kwargs: Any, 

1879 ) -> list[DatasetRef]: 

1880 """Query for dataset references matching user-provided criteria. 

1881 

1882 Parameters 

1883 ---------- 

1884 dataset_type : `str` or `DatasetType` 

1885 Dataset type object or name to search for. 

1886 collections : collection expression, optional 

1887 A collection name or iterable of collection names to search. If not 

1888 provided, the default collections are used. Can be a wildcard if 

1889 ``find_first`` is `False` (if find first is requested the order 

1890 of collections matters and wildcards make the order indeterminate). 

1891 See :ref:`daf_butler_collection_expressions` for more information. 

1892 find_first : `bool`, optional 

1893 If `True` (default), for each result data ID, only yield one 

1894 `DatasetRef` of each `DatasetType`, from the first collection in 

1895 which a dataset of that dataset type appears (according to the 

1896 order of ``collections`` passed in). If `True`, ``collections`` 

1897 must not contain wildcards. 

1898 data_id : `dict` or `DataCoordinate`, optional 

1899 A data ID whose key-value pairs are used as equality constraints in 

1900 the query. 

1901 where : `str`, optional 

1902 A string expression similar to a SQL WHERE clause. May involve any 

1903 column of a dimension table or (as a shortcut for the primary key 

1904 column of a dimension table) dimension name. See 

1905 :ref:`daf_butler_dimension_expressions` for more information. 

1906 bind : `~collections.abc.Mapping`, optional 

1907 Mapping containing literal values that should be injected into the 

1908 ``where`` expression, keyed by the identifiers they replace. Values 

1909 of collection type can be expanded in some cases; see 

1910 :ref:`daf_butler_dimension_expressions_identifiers` for more 

1911 information. 

1912 with_dimension_records : `bool`, optional 

1913 If `True` (default is `False`) then returned data IDs will have 

1914 dimension records. 

1915 order_by : `~collections.abc.Iterable` [`str`] or `str`, optional 

1916 Names of the columns/dimensions to use for ordering returned data 

1917 IDs. Column name can be prefixed with minus (``-``) to use 

1918 descending ordering. 

1919 limit : `int` or `None`, optional 

1920 Upper limit on the number of returned records. `None` can be used 

1921 if no limit is wanted. A limit of ``0`` means that the query will 

1922 be executed and validated but no results will be returned. In this 

1923 case there will be no exception even if ``explain`` is `True`. 

1924 If a negative value is given a warning will be issued if the number 

1925 of results is capped by that limit. 

1926 explain : `bool`, optional 

1927 If `True` (default) then `EmptyQueryResultError` exception is 

1928 raised when resulting list is empty. The exception contains 

1929 non-empty list of strings explaining possible causes for empty 

1930 result. 

1931 **kwargs 

1932 Additional keyword arguments are forwarded to 

1933 `DataCoordinate.standardize` when processing the ``data_id`` 

1934 argument (and may be used to provide a constraining data ID even 

1935 when the ``data_id`` argument is `None`). 

1936 

1937 Returns 

1938 ------- 

1939 refs : `.queries.DatasetRefQueryResults` 

1940 Dataset references matching the given query criteria. Nested data 

1941 IDs are guaranteed to include values for all implied dimensions 

1942 (i.e. `DataCoordinate.hasFull` will return `True`). 

1943 

1944 Raises 

1945 ------ 

1946 lsst.daf.butler.registry.DatasetTypeExpressionError 

1947 Raised when ``dataset_type`` expression is invalid. 

1948 lsst.daf.butler.registry.DataIdError 

1949 Raised when ``data_id`` or keyword arguments specify unknown 

1950 dimensions or values, or when they contain inconsistent values. 

1951 lsst.daf.butler.registry.UserExpressionError 

1952 Raised when ``where`` expression is invalid. 

1953 lsst.daf.butler.EmptyQueryResultError 

1954 Raised when query generates empty result and ``explain`` is set to 

1955 `True`. 

1956 TypeError 

1957 Raised when the arguments are incompatible, such as when a 

1958 collection wildcard is passed when ``find_first`` is `True`, or 

1959 when ``collections`` is `None` and default butler collections are 

1960 not defined. 

1961 """ 

1962 if data_id is None: 

1963 data_id = DataCoordinate.make_empty(self.dimensions) 

1964 if order_by is None: 

1965 order_by = [] 

1966 if collections and has_globs(collections): 

1967 # Wild cards need to be expanded but can only be allowed if 

1968 # find_first=False because expanding wildcards does not return 

1969 # a guaranteed ordering. Querying collection registry to expand 

1970 # collections when we do not have wildcards is expensive so only 

1971 # do it if we need it. 

1972 if find_first: 

1973 raise InvalidQueryError( 

1974 f"Can not use wildcards in collections when find_first=True (given {collections})" 

1975 ) 

1976 collections = self.collections.query(collections) 

1977 query_limit = limit 

1978 warn_limit = False 

1979 if limit is not None and limit < 0: 

1980 query_limit = abs(limit) + 1 

1981 warn_limit = True 

1982 with self.query() as query: 

1983 result = ( 

1984 query.datasets(dataset_type, collections=collections, find_first=find_first) 

1985 .where(data_id, where, bind=bind, **kwargs) 

1986 .order_by(*ensure_iterable(order_by)) 

1987 .limit(query_limit) 

1988 ) 

1989 if with_dimension_records: 

1990 result = result.with_dimension_records() 

1991 refs = list(result) 

1992 if warn_limit and len(refs) == query_limit: 

1993 # We asked for one too many so must remove that from the list. 

1994 refs.pop(-1) 

1995 assert limit is not None # For mypy. 

1996 _LOG.warning("More datasets are available than the requested limit of %d.", abs(limit)) 

1997 if explain and (limit is None or limit != 0) and not refs: 

1998 raise EmptyQueryResultError(list(result.explain_no_results())) 

1999 return refs 

2000 

2001 def query_dimension_records( 

2002 self, 

2003 element: str, 

2004 *, 

2005 data_id: DataId | None = None, 

2006 where: str = "", 

2007 bind: Mapping[str, Any] | None = None, 

2008 order_by: Iterable[str] | str | None = None, 

2009 limit: int | None = -20_000, 

2010 explain: bool = True, 

2011 **kwargs: Any, 

2012 ) -> list[DimensionRecord]: 

2013 """Query for dimension information matching user-provided criteria. 

2014 

2015 Parameters 

2016 ---------- 

2017 element : `str` 

2018 The name of a dimension element to obtain records for. 

2019 data_id : `dict` or `DataCoordinate`, optional 

2020 A data ID whose key-value pairs are used as equality constraints 

2021 in the query. 

2022 where : `str`, optional 

2023 A string expression similar to a SQL WHERE clause. See 

2024 `queryDataIds` and :ref:`daf_butler_dimension_expressions` for more 

2025 information. 

2026 bind : `~collections.abc.Mapping`, optional 

2027 Mapping containing literal values that should be injected into the 

2028 ``where`` expression, keyed by the identifiers they replace. 

2029 Values of collection type can be expanded in some cases; see 

2030 :ref:`daf_butler_dimension_expressions_identifiers` for more 

2031 information. 

2032 order_by : `~collections.abc.Iterable` [`str`] or `str`, optional 

2033 Names of the columns/dimensions to use for ordering returned data 

2034 IDs. Column name can be prefixed with minus (``-``) to use 

2035 descending ordering. 

2036 limit : `int` or `None`, optional 

2037 Upper limit on the number of returned records. `None` can be used 

2038 if no limit is wanted. A limit of ``0`` means that the query will 

2039 be executed and validated but no results will be returned. In this 

2040 case there will be no exception even if ``explain`` is `True`. 

2041 If a negative value is given a warning will be issued if the number 

2042 of results is capped by that limit. 

2043 explain : `bool`, optional 

2044 If `True` (default) then `EmptyQueryResultError` exception is 

2045 raised when resulting list is empty. The exception contains 

2046 non-empty list of strings explaining possible causes for empty 

2047 result. 

2048 **kwargs 

2049 Additional keyword arguments are forwarded to 

2050 `DataCoordinate.standardize` when processing the ``data_id`` 

2051 argument (and may be used to provide a constraining data ID even 

2052 when the ``data_id`` argument is `None`). 

2053 

2054 Returns 

2055 ------- 

2056 records : `list` [`DimensionRecord`] 

2057 Dimension records matching the given query parameters. 

2058 

2059 Raises 

2060 ------ 

2061 lsst.daf.butler.registry.DataIdError 

2062 Raised when ``data_id`` or keyword arguments specify unknown 

2063 dimensions or values, or when they contain inconsistent values. 

2064 lsst.daf.butler.registry.UserExpressionError 

2065 Raised when ``where`` expression is invalid. 

2066 lsst.daf.butler.EmptyQueryResultError 

2067 Raised when query generates empty result and ``explain`` is set to 

2068 `True`. 

2069 TypeError 

2070 Raised when the arguments are incompatible, such as when a 

2071 collection wildcard is passed when ``find_first`` is `True`, or 

2072 when ``collections`` is `None` and default butler collections are 

2073 not defined. 

2074 """ 

2075 if data_id is None: 

2076 data_id = DataCoordinate.make_empty(self.dimensions) 

2077 if order_by is None: 

2078 order_by = [] 

2079 query_limit = limit 

2080 warn_limit = False 

2081 if limit is not None and limit < 0: 

2082 query_limit = abs(limit) + 1 

2083 warn_limit = True 

2084 with self.query() as query: 

2085 result = ( 

2086 query.dimension_records(element) 

2087 .where(data_id, where, bind=bind, **kwargs) 

2088 .order_by(*ensure_iterable(order_by)) 

2089 .limit(query_limit) 

2090 ) 

2091 dimension_records = list(result) 

2092 if warn_limit and len(dimension_records) == query_limit: 

2093 # We asked for one too many so must remove that from the list. 

2094 dimension_records.pop(-1) 

2095 assert limit is not None # For mypy. 

2096 _LOG.warning( 

2097 "More dimension records are available than the requested limit of %d.", abs(limit) 

2098 ) 

2099 if explain and (limit is None or limit != 0) and not dimension_records: 

2100 raise EmptyQueryResultError(list(result.explain_no_results())) 

2101 return dimension_records 

2102 

2103 def query_all_datasets( 

2104 self, 

2105 collections: str | Iterable[str] | None = None, 

2106 *, 

2107 name: str | Iterable[str] = "*", 

2108 find_first: bool = True, 

2109 data_id: DataId | None = None, 

2110 where: str = "", 

2111 bind: Mapping[str, Any] | None = None, 

2112 limit: int | None = -20_000, 

2113 **kwargs: Any, 

2114 ) -> list[DatasetRef]: 

2115 """Query for datasets of potentially multiple types. 

2116 

2117 Parameters 

2118 ---------- 

2119 collections : `str` or `~collections.abc.Iterable` [ `str` ], optional 

2120 The collection or collections to search, in order. If not provided 

2121 or `None`, the default collection search path for this butler is 

2122 used. 

2123 name : `str` or `~collections.abc.Iterable` [ `str` ], optional 

2124 Names or name patterns (glob-style) that returned dataset type 

2125 names must match. If an iterable, items are OR'd together. The 

2126 default is to include all dataset types in the given collections. 

2127 find_first : `bool`, optional 

2128 If `True` (default), for each result data ID, only yield one 

2129 `DatasetRef` of each `DatasetType`, from the first collection in 

2130 which a dataset of that dataset type appears (according to the 

2131 order of ``collections`` passed in). 

2132 data_id : `dict` or `DataCoordinate`, optional 

2133 A data ID whose key-value pairs are used as equality constraints in 

2134 the query. 

2135 where : `str`, optional 

2136 A string expression similar to a SQL WHERE clause. May involve any 

2137 column of a dimension table or (as a shortcut for the primary key 

2138 column of a dimension table) dimension name. See 

2139 :ref:`daf_butler_dimension_expressions` for more information. 

2140 bind : `~collections.abc.Mapping`, optional 

2141 Mapping containing literal values that should be injected into the 

2142 ``where`` expression, keyed by the identifiers they replace. Values 

2143 of collection type can be expanded in some cases; see 

2144 :ref:`daf_butler_dimension_expressions_identifiers` for more 

2145 information. 

2146 limit : `int` or `None`, optional 

2147 Upper limit on the number of returned records. `None` can be used 

2148 if no limit is wanted. A limit of ``0`` means that the query will 

2149 be executed and validated but no results will be returned. 

2150 If a negative value is given a warning will be issued if the number 

2151 of results is capped by that limit. If no limit is provided, by 

2152 default a maximum of 20,000 records will be returned. 

2153 **kwargs 

2154 Additional keyword arguments are forwarded to 

2155 `DataCoordinate.standardize` when processing the ``data_id`` 

2156 argument (and may be used to provide a constraining data ID even 

2157 when the ``data_id`` argument is `None`). 

2158 

2159 Raises 

2160 ------ 

2161 MissingDatasetTypeError 

2162 When no dataset types match ``name``, or an explicit (non-glob) 

2163 dataset type in ``name`` does not exist. 

2164 InvalidQueryError 

2165 If the parameters to the query are inconsistent or malformed. 

2166 MissingCollectionError 

2167 If a given collection is not found. 

2168 

2169 Returns 

2170 ------- 

2171 refs : `list` [ `DatasetRef` ] 

2172 Dataset references matching the given query criteria. Nested data 

2173 IDs are guaranteed to include values for all implied dimensions 

2174 (i.e. `DataCoordinate.hasFull` will return `True`), but will not 

2175 include dimension records (`DataCoordinate.hasRecords` will be 

2176 `False`). 

2177 """ 

2178 if collections is None: 

2179 collections = list(self.collections.defaults) 

2180 else: 

2181 collections = list(ensure_iterable(collections)) 

2182 

2183 if bind is None: 

2184 bind = {} 

2185 if data_id is None: 

2186 data_id = {} 

2187 

2188 warn_limit = False 

2189 if limit is not None and limit < 0: 

2190 # Add one to the limit so we can detect if we have exceeded it. 

2191 limit = abs(limit) + 1 

2192 warn_limit = True 

2193 

2194 args = QueryAllDatasetsParameters( 

2195 collections=collections, 

2196 name=list(ensure_iterable(name)), 

2197 find_first=find_first, 

2198 data_id=data_id, 

2199 where=where, 

2200 limit=limit, 

2201 bind=bind, 

2202 kwargs=kwargs, 

2203 with_dimension_records=False, 

2204 ) 

2205 with self._query_all_datasets_by_page(args) as pages: 

2206 result = [] 

2207 for page in pages: 

2208 result.extend(page) 

2209 

2210 if warn_limit and limit is not None and len(result) >= limit: 

2211 # Remove the extra dataset we added for the limit check. 

2212 result.pop() 

2213 _LOG.warning("More datasets are available than the requested limit of %d.", limit - 1) 

2214 

2215 return result 

2216 

2217 @abstractmethod 

2218 def _query_all_datasets_by_page( 

2219 self, args: QueryAllDatasetsParameters 

2220 ) -> AbstractContextManager[Iterator[list[DatasetRef]]]: 

2221 raise NotImplementedError() 

2222 

2223 def clone( 

2224 self, 

2225 *, 

2226 collections: CollectionArgType | None | EllipsisType = ..., 

2227 run: str | None | EllipsisType = ..., 

2228 inferDefaults: bool | EllipsisType = ..., 

2229 dataId: dict[str, str] | EllipsisType = ..., 

2230 metrics: ButlerMetrics | None = None, 

2231 ) -> Butler: 

2232 """Return a new Butler instance connected to the same repository 

2233 as this one, optionally overriding ``collections``, ``run``, 

2234 ``inferDefaults``, and default data ID. 

2235 

2236 Parameters 

2237 ---------- 

2238 collections : `~lsst.daf.butler.registry.CollectionArgType` or `None`,\ 

2239 optional 

2240 Same as constructor. If omitted, uses value from original object. 

2241 run : `str` or `None`, optional 

2242 Same as constructor. If `None`, no default run is used. If 

2243 omitted, copies value from original object. 

2244 inferDefaults : `bool`, optional 

2245 Same as constructor. If omitted, copies value from original 

2246 object. 

2247 dataId : `str` 

2248 Same as ``kwargs`` passed to the constructor. If omitted, copies 

2249 values from original object. 

2250 metrics : `ButlerMetrics` or `None`, optional 

2251 Metrics object to record butler statistics. 

2252 """ 

2253 raise NotImplementedError() 

2254 

2255 @abstractmethod 

2256 def close(self) -> None: 

2257 raise NotImplementedError() 

2258 

2259 @abstractmethod 

2260 def _expand_data_ids(self, data_ids: Iterable[DataCoordinate]) -> list[DataCoordinate]: 

2261 raise NotImplementedError()