Coverage for python / lsst / daf / butler / cli / opt / options.py: 84%
48 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-26 08:49 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-26 08:49 +0000
1# This file is part of daf_butler.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This software is dual licensed under the GNU General Public License and also
10# under a 3-clause BSD license. Recipients may choose which of these licenses
11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12# respectively. If you choose the GPL option then the following text applies
13# (but note that there is still no warranty even if you opt for BSD instead):
14#
15# This program is free software: you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation, either version 3 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program. If not, see <http://www.gnu.org/licenses/>.
27from __future__ import annotations
29__all__ = (
30 "CollectionTypeCallback",
31 "collection_type_option",
32 "collections_option",
33 "config_file_option",
34 "config_option",
35 "confirm_option",
36 "dataset_type_option",
37 "datasets_option",
38 "limit_option",
39 "log_file_option",
40 "log_label_option",
41 "log_level_option",
42 "log_tty_option",
43 "long_log_option",
44 "offset_option",
45 "options_file_option",
46 "order_by_option",
47 "processes_option",
48 "regex_option",
49 "register_dataset_types_option",
50 "run_option",
51 "track_file_attrs_option",
52 "transfer_dimensions_option",
53 "transfer_option",
54 "transfer_option_no_short",
55 "verbose_option",
56 "where_option",
57)
59from functools import partial
60from typing import Any
62import click
64from lsst.daf.butler import CollectionType
66from ..cliLog import CliLog
67from ..utils import MWOptionDecorator, MWPath, split_commas, split_kv, unwrap, yaml_presets
70class CollectionTypeCallback:
71 """Helper class for handling different collection types."""
73 collectionTypes = tuple(collectionType.name for collectionType in CollectionType.all())
75 @staticmethod
76 def makeCollectionTypes(
77 context: click.Context, param: click.Option, value: tuple[str, ...] | str
78 ) -> tuple[CollectionType, ...]:
79 if not value:
80 # Click seems to demand that the default be an empty tuple, rather
81 # than a sentinal like None. The behavior that we want is that
82 # not passing this option at all passes all collection types, while
83 # passing it uses only the passed collection types. That works
84 # fine for now, since there's no command-line option to subtract
85 # collection types, and hence the only way to get an empty tuple
86 # is as the default.
87 return tuple(CollectionType.all())
89 return tuple(CollectionType.from_name(item) for item in split_commas(context, param, value))
92collection_type_option = MWOptionDecorator(
93 "--collection-type",
94 callback=CollectionTypeCallback.makeCollectionTypes,
95 multiple=True,
96 help="If provided, only list collections of this type.",
97 type=click.Choice(choices=CollectionTypeCallback.collectionTypes, case_sensitive=False),
98)
101collections_option = MWOptionDecorator(
102 "--collections",
103 help=unwrap(
104 """One or more expressions that fully or partially identify
105 the collections to search for datasets. If not provided all
106 datasets are returned."""
107 ),
108 multiple=True,
109 callback=split_commas,
110)
113def _config_split(*args: Any) -> dict[str | None, str]:
114 # Config values might include commas so disable comma-splitting.
115 result = split_kv(*args, multiple=False)
116 assert isinstance(result, dict), "For mypy check that we get the expected result"
117 return result
120config_option = MWOptionDecorator(
121 "-c",
122 "--config",
123 callback=_config_split,
124 help="Config override, as a key-value pair.",
125 metavar="TEXT=TEXT",
126 multiple=True,
127)
130config_file_option = MWOptionDecorator(
131 "-C",
132 "--config-file",
133 help=unwrap(
134 """Path to a pex config override to be included after the
135 Instrument config overrides are applied."""
136 ),
137)
140confirm_option = MWOptionDecorator(
141 "--confirm/--no-confirm",
142 default=True,
143 help="Print expected action and a confirmation prompt before executing. Default is --confirm.",
144)
147dataset_type_option = MWOptionDecorator(
148 "-d", "--dataset-type", callback=split_commas, help="Specific DatasetType(s) to validate.", multiple=True
149)
152datasets_option = MWOptionDecorator("--datasets")
155logLevelChoices = ["CRITICAL", "ERROR", "WARNING", "INFO", "VERBOSE", "DEBUG", "TRACE"]
156log_level_option = MWOptionDecorator(
157 "--log-level",
158 callback=partial(
159 split_kv,
160 choice=click.Choice(choices=logLevelChoices, case_sensitive=False),
161 normalize=True,
162 unseparated_okay=True,
163 add_to_default=True,
164 default_key=None, # No separator
165 ),
166 help=f"The logging level. Without an explicit logger name, will only affect the default root loggers "
167 f"({', '.join(CliLog.root_loggers())}). To modify the root logger use '.=LEVEL'. "
168 f"Supported levels are [{'|'.join(logLevelChoices)}]",
169 is_eager=True,
170 metavar="LEVEL|COMPONENT=LEVEL",
171 multiple=True,
172)
175long_log_option = MWOptionDecorator(
176 "--long-log", help="Make log messages appear in long format.", is_flag=True
177)
179log_file_option = MWOptionDecorator(
180 "--log-file",
181 default=None,
182 multiple=True,
183 callback=split_commas,
184 type=MWPath(file_okay=True, dir_okay=False, writable=True),
185 help="File(s) to write log messages. If the path ends with '.json' then"
186 " JSON log records will be written, else formatted text log records"
187 " will be written. This file can exist and records will be appended.",
188)
190log_label_option = MWOptionDecorator(
191 "--log-label",
192 default=None,
193 multiple=True,
194 callback=split_kv,
195 type=str,
196 help="Keyword=value pairs to add to MDC of log records.",
197)
199log_tty_option = MWOptionDecorator(
200 "--log-tty/--no-log-tty",
201 default=True,
202 help="Log to terminal (default). If false logging to terminal is disabled.",
203)
205options_file_option = MWOptionDecorator(
206 "--options-file",
207 "-@",
208 expose_value=False, # This option should not be forwarded
209 help=unwrap(
210 """URI to YAML file containing overrides
211 of command line options. The YAML should be organized
212 as a hierarchy with subcommand names at the top
213 level options for that subcommand below."""
214 ),
215 callback=yaml_presets,
216)
219processes_option = MWOptionDecorator(
220 "-j", "--processes", default=1, help="Number of processes to use.", type=click.IntRange(min=1)
221)
224regex_option = MWOptionDecorator("--regex")
227register_dataset_types_option = MWOptionDecorator(
228 "--register-dataset-types",
229 help="Register DatasetTypes that do not already exist in the Registry.",
230 is_flag=True,
231)
233run_option = MWOptionDecorator("--output-run", help="The name of the run datasets should be output to.")
235_transfer_params = dict(
236 default="auto", # set to `None` if using `required=True`
237 help="The external data transfer mode.",
238 type=click.Choice(
239 choices=["auto", "link", "symlink", "hardlink", "copy", "move", "relsymlink", "direct"],
240 case_sensitive=False,
241 ),
242)
244transfer_option_no_short = MWOptionDecorator(
245 "--transfer",
246 **_transfer_params,
247)
249transfer_option = MWOptionDecorator(
250 "-t",
251 "--transfer",
252 **_transfer_params,
253)
256transfer_dimensions_option = MWOptionDecorator(
257 "--transfer-dimensions/--no-transfer-dimensions",
258 is_flag=True,
259 default=True,
260 help=unwrap(
261 """If true, also copy dimension records along with datasets.
262 If the dmensions are already present in the destination butler it
263 can be more efficient to disable this. The default is to transfer
264 dimensions."""
265 ),
266)
269verbose_option = MWOptionDecorator("-v", "--verbose", help="Increase verbosity.", is_flag=True)
272where_option = MWOptionDecorator(
273 "--where", default="", help="A string expression similar to a SQL WHERE clause."
274)
277order_by_option = MWOptionDecorator(
278 "--order-by",
279 help=unwrap(
280 """One or more comma-separated names used to order records. Names can be dimension names,
281 metadata field names, or "timespan.begin" / "timespan.end" for temporal dimensions.
282 In some cases the dimension for a metadata field or timespan bound can be inferred, but usually
283 qualifying these with "<dimension>.<field>" is necessary.
284 To reverse ordering for a name, prefix it with a minus sign.
285 """
286 ),
287 multiple=True,
288 callback=split_commas,
289)
292_default_limit = -20_000
293limit_option = MWOptionDecorator(
294 "--limit",
295 help=unwrap(
296 f"""Limit the number of results that are processed. 0 means no limit. A negative
297 value specifies a cap where a warning will be issued if the cap is hit.
298 Default value is {_default_limit}."""
299 ),
300 type=int,
301 default=_default_limit,
302)
304offset_option = MWOptionDecorator(
305 "--offset",
306 help=unwrap("Skip initial number of records, only used when --limit is specified."),
307 type=int,
308 default=0,
309)
311track_file_attrs_option = MWOptionDecorator(
312 "--track-file-attrs/--no-track-file-attrs",
313 default=True,
314 help="Indicate to the datastore whether file attributes such as file size"
315 " or checksum should be tracked or not. Whether this parameter is honored"
316 " depends on the specific datastore implementation.",
317)