Coverage for python/lsst/daf/butler/script/queryCollections.py: 7%
123 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-10 01:29 -0700
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-10 01:29 -0700
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/>.
28from __future__ import annotations
30from collections.abc import Iterable
31from fnmatch import fnmatch
32from typing import Literal
34from astropy.table import Column, Table, hstack, vstack
36from .._butler import Butler
37from .._butler_collections import CollectionInfo
38from .._collection_type import CollectionType
41def _parseDatasetTypes(dataset_types: frozenset[str] | list[str] | None) -> list[str]:
42 """Parse dataset types from a collection info object or a list of strings.
44 Parameters
45 ----------
46 dataset_types : `frozenset`[`str`] | `list`[`str`] | `None`
47 The dataset types to parse. If `None`, an empty list is returned.
48 If a `frozenset` or `list` is provided, it is returned as a list.
50 Returns
51 -------
52 dataset_types : `list`[`str`]
53 The parsed dataset types.
54 """
55 return [""] if not dataset_types else list(dataset_types)
58def _getTable(
59 repo: str,
60 glob: Iterable[str],
61 collection_type: Iterable[CollectionType],
62 inverse: bool,
63 show_dataset_types: bool = False,
64 exclude_dataset_types: Iterable[str] | None = None,
65) -> Table:
66 """Run queryCollections and return the results in Table form.
68 Only lists the first child (or parent if `inverse` is `True`) in the
69 description column.
71 Parameters
72 ----------
73 repo : `str`
74 The Butler repository location.
75 glob : `collections.abc.Iterable` of `str`
76 Wildcard to pass to ``queryCollections``.
77 collection_type
78 Same as `queryCollections`
79 inverse : `bool`
80 True if parent CHAINED datasets of each dataset should be listed in the
81 description column, False if children of CHAINED datasets should be
82 listed.
83 show_dataset_types : `bool`, optional
84 If `True`, also show the dataset types present within each collection.
85 exclude_dataset_types : `~collections.abc.Iterable` [ `str` ], optional
86 A glob-style iterable of dataset types to exclude.
87 Only has an effect if `show_dataset_types` is True.
89 Returns
90 -------
91 collections : `astropy.table.Table`
92 Same as `queryCollections`
93 """
94 typeCol = "Type"
95 descriptionCol = "Parents" if inverse else "Children"
96 table = Table(
97 names=("Name", typeCol, descriptionCol),
98 dtype=(str, str, str),
99 )
100 if show_dataset_types:
101 table.add_column(Column(name="Dataset Types", dtype=str))
102 butler = Butler.from_config(repo)
104 def addDatasetTypes(collection_table: Table, collection: str, dataset_types: list[str]) -> Table:
105 if dataset_types[0] == "":
106 cinfo = butler.collections.get_info(collection, include_summary=True)
107 dataset_types = _parseDatasetTypes(cinfo.dataset_types)
108 if exclude_dataset_types:
109 dataset_types = [
110 dt
111 for dt in dataset_types
112 if not any(fnmatch(dt, pattern) for pattern in exclude_dataset_types)
113 ]
114 dataset_types = _parseDatasetTypes(dataset_types)
115 types_table = Table({"Dataset Types": sorted(dataset_types)}, dtype=(str,))
116 collection_table = hstack([collection_table, types_table]).filled("")
117 return collection_table
119 def addCollection(info: CollectionInfo, relation: str) -> None:
120 try:
121 info_relatives = getattr(info, relation)
122 except AttributeError:
123 info_relatives = []
124 # Parent results can be returned in a non-deterministic order, so sort
125 # them to make the output deterministic.
126 if relation == "parents":
127 info_relatives = sorted(info_relatives)
128 if info_relatives:
129 collection_table = Table([[info.name], [info.type.name]], names=("Name", typeCol))
130 description_table = Table(names=(descriptionCol,), dtype=(str,))
131 for info_relative in info_relatives:
132 relative_table = Table([[info_relative]], names=(descriptionCol,))
133 if show_dataset_types:
134 relative_table = addDatasetTypes(relative_table, info_relative, [""])
135 description_table = vstack([description_table, relative_table])
136 collection_table = hstack([collection_table, description_table]).filled("")
137 for row in collection_table:
138 table.add_row(row)
139 else:
140 collection_table = Table(
141 [[info.name], [info.type.name], [""]], names=("Name", typeCol, descriptionCol)
142 )
143 if show_dataset_types:
144 collection_table = addDatasetTypes(collection_table, info.name, [""])
145 for row in collection_table:
146 table.add_row(row)
148 collections = sorted(
149 butler.collections.query_info(
150 glob or "*",
151 collection_types=frozenset(collection_type),
152 include_parents=inverse,
153 include_summary=show_dataset_types,
154 )
155 )
156 if inverse:
157 for info in collections:
158 addCollection(info, "parents")
159 # If none of the datasets has a parent dataset then remove the
160 # description column.
161 if not any(c for c in table[descriptionCol]):
162 del table[descriptionCol]
163 else:
164 for info in collections:
165 if info.type == CollectionType.CHAINED:
166 addCollection(info, "children")
167 else:
168 addCollection(info, "self")
169 # If there aren't any CHAINED datasets in the results then remove the
170 # description column.
171 if not any(columnVal == CollectionType.CHAINED.name for columnVal in table[typeCol]):
172 del table[descriptionCol]
174 return table
177def _getTree(
178 repo: str,
179 glob: Iterable[str],
180 collection_type: Iterable[CollectionType],
181 inverse: bool,
182 show_dataset_types: bool = False,
183 exclude_dataset_types: Iterable[str] | None = None,
184) -> Table:
185 """Run queryCollections and return the results in a table representing tree
186 form.
188 Recursively lists children (or parents if `inverse` is `True`)
190 Parameters
191 ----------
192 repo : `str`
193 Butler repository location.
194 glob : `collections.abc.Iterable` of `str`
195 Wildcards to pass to ``queryCollections``.
196 collection_type
197 Same as `queryCollections`
198 inverse : `bool`
199 True if parent CHAINED datasets of each dataset should be listed in the
200 description column, False if children of CHAINED datasets should be
201 listed.
202 show_dataset_types : `bool`, optional
203 If `True`, also show the dataset types present within each collection.
204 exclude_dataset_types : `~collections.abc.Iterable` [ `str` ], optional
205 A glob-style iterable of dataset types to exclude.
206 Only has an effect if `show_dataset_types` is True.
208 Returns
209 -------
210 collections : `astropy.table.Table`
211 Same as `queryCollections`
212 """
213 table = Table(
214 names=("Name", "Type"),
215 dtype=(str, str),
216 )
217 if show_dataset_types:
218 table.add_column(Column(name="Dataset Types", dtype=str))
219 butler = Butler.from_config(repo, without_datastore=True)
221 def addCollection(info: CollectionInfo, level: int = 0) -> None:
222 collection_table = Table([[" " * level + info.name], [info.type.name]], names=["Name", "Type"])
223 if show_dataset_types:
224 if info.type == CollectionType.CHAINED:
225 collection_table = hstack(
226 [collection_table, Table([[""] * len(collection_table)], names=["Dataset Types"])]
227 )
228 else:
229 dataset_types = _parseDatasetTypes(info.dataset_types)
230 if exclude_dataset_types:
231 dataset_types = [
232 dt
233 for dt in dataset_types
234 if not any(fnmatch(dt, pattern) for pattern in exclude_dataset_types)
235 ]
236 dataset_types = _parseDatasetTypes(dataset_types)
237 dataset_types_table = Table({"Dataset Types": sorted(dataset_types)}, dtype=(str,))
238 collection_table = hstack([collection_table, dataset_types_table]).filled("")
239 for row in collection_table:
240 table.add_row(row)
242 if inverse:
243 assert info.parents is not None # For mypy.
244 for pname in sorted(info.parents):
245 pinfo = butler.collections.get_info(
246 pname, include_parents=inverse, include_summary=show_dataset_types
247 )
248 addCollection(pinfo, level + 1)
249 else:
250 if info.type == CollectionType.CHAINED:
251 for name in info.children:
252 cinfo = butler.collections.get_info(name, include_summary=show_dataset_types)
253 addCollection(cinfo, level + 1)
255 collections = butler.collections.query_info(
256 glob or "*",
257 collection_types=frozenset(collection_type),
258 include_parents=inverse,
259 include_summary=show_dataset_types,
260 )
261 for collection in sorted(collections):
262 addCollection(collection)
263 return table
266def _getList(
267 repo: str,
268 glob: Iterable[str],
269 collection_type: Iterable[CollectionType],
270 flatten_chains: bool,
271 show_dataset_types: bool = False,
272 exclude_dataset_types: Iterable[str] | None = None,
273) -> Table:
274 """Return collection results as a table representing a flat list of
275 collections.
277 Parameters
278 ----------
279 repo : `str`
280 Butler repository location.
281 glob : `collections.abc.Iterable` of `str`
282 Wildcards to pass to ``queryCollections``.
283 collection_type
284 Same as `queryCollections`
285 flatten_chains : `bool`
286 If `True`, flatten the tree of CHAINED datasets.
287 show_dataset_types : `bool`, optional
288 If `True`, also show the dataset types present within each collection.
289 exclude_dataset_types : `~collections.abc.Iterable` [ `str` ], optional
290 A glob-style iterable of dataset types to exclude.
291 Only has an effect if `show_dataset_types` is True.
293 Returns
294 -------
295 collections : `astropy.table.Table`
296 Same as `queryCollections`
297 """
298 table = Table(
299 names=("Name", "Type"),
300 dtype=(str, str),
301 )
302 if show_dataset_types:
303 table.add_column(Column(name="Dataset Types", dtype=str))
304 butler = Butler.from_config(repo)
306 def addCollection(info: CollectionInfo) -> None:
307 collection_table = Table([[info.name], [info.type.name]], names=["Name", "Type"])
308 if show_dataset_types:
309 dataset_types = _parseDatasetTypes(info.dataset_types)
310 if exclude_dataset_types:
311 dataset_types = [
312 dt
313 for dt in dataset_types
314 if not any(fnmatch(dt, pattern) for pattern in exclude_dataset_types)
315 ]
316 dataset_types = _parseDatasetTypes(dataset_types)
317 dataset_types_table = Table({"Dataset Types": sorted(dataset_types)}, dtype=(str,))
318 collection_table = hstack([collection_table, dataset_types_table]).filled("")
319 for row in collection_table:
320 table.add_row(row)
322 collections = list(
323 butler.collections.query_info(
324 glob or "*",
325 collection_types=frozenset(collection_type),
326 flatten_chains=flatten_chains,
327 include_summary=show_dataset_types,
328 )
329 )
330 for collection in collections:
331 addCollection(collection)
333 return table
336def queryCollections(
337 repo: str,
338 glob: Iterable[str],
339 collection_type: Iterable[CollectionType],
340 chains: Literal["INVERSE-TABLE", "TABLE", "TREE", "INVERSE-TREE", "FLATTEN", "NO-CHILDREN"],
341 show_dataset_types: bool = False,
342 exclude_dataset_types: Iterable[str] | None = None,
343) -> Table:
344 """Get the collections whose names match an expression.
346 Parameters
347 ----------
348 repo : `str`
349 URI to the location of the repo or URI to a config file describing the
350 repo and its location.
351 glob : `~collections.abc.Iterable` [`str`]
352 A list of glob-style search string that fully or partially identify
353 the dataset type names to search for.
354 collection_type : `~collections.abc.Iterable` [ `CollectionType` ], \
355 optional
356 If provided, only return collections of these types.
357 chains : `str`
358 Affects contents and formatting of results, see
359 ``cli.commands.query_collections``.
360 show_dataset_types : `bool`, optional
361 If `True`, include the dataset types present within each collection.
362 exclude_dataset_types : `~collections.abc.Iterable` [ `str` ], optional
363 A glob-style iterable of dataset types to exclude.
364 Only has an effect if `show_dataset_types` is True.
366 Returns
367 -------
368 collections : `astropy.table.Table`
369 A table containing information about collections.
370 """
371 if (inverse := chains == "INVERSE-TABLE") or chains == "TABLE":
372 return _getTable(repo, glob, collection_type, inverse, show_dataset_types, exclude_dataset_types)
373 elif (inverse := chains == "INVERSE-TREE") or chains == "TREE":
374 return _getTree(repo, glob, collection_type, inverse, show_dataset_types, exclude_dataset_types)
375 elif chains == "FLATTEN" or chains == "NO-CHILDREN":
376 flatten = chains == "FLATTEN"
377 return _getList(repo, glob, collection_type, flatten, show_dataset_types, exclude_dataset_types)
378 raise RuntimeError(f"Value for --chains not recognized: {chains}")