Coverage for python/lsst/daf/butler/cli/opt/options.py: 82%

47 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-05 11:07 +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 

28 

29__all__ = ( 

30 "CollectionTypeCallback", 

31 "collection_type_option", 

32 "collections_option", 

33 "components_option", 

34 "config_option", 

35 "config_file_option", 

36 "confirm_option", 

37 "dataset_type_option", 

38 "datasets_option", 

39 "log_level_option", 

40 "long_log_option", 

41 "log_file_option", 

42 "log_label_option", 

43 "log_tty_option", 

44 "options_file_option", 

45 "processes_option", 

46 "regex_option", 

47 "register_dataset_types_option", 

48 "run_option", 

49 "transfer_dimensions_option", 

50 "transfer_option", 

51 "transfer_option_no_short", 

52 "verbose_option", 

53 "where_option", 

54 "order_by_option", 

55 "limit_option", 

56 "offset_option", 

57) 

58 

59from functools import partial 

60from typing import Any 

61 

62import click 

63from lsst.daf.butler.registry import CollectionType 

64 

65from ..cliLog import CliLog 

66from ..utils import MWOptionDecorator, MWPath, split_commas, split_kv, unwrap, yaml_presets 

67 

68 

69class CollectionTypeCallback: 

70 """Helper class for handling different collection types.""" 

71 

72 collectionTypes = tuple(collectionType.name for collectionType in CollectionType.all()) 

73 

74 @staticmethod 

75 def makeCollectionTypes( 

76 context: click.Context, param: click.Option, value: tuple[str, ...] | str 

77 ) -> tuple[CollectionType, ...]: 

78 if not value: 

79 # Click seems to demand that the default be an empty tuple, rather 

80 # than a sentinal like None. The behavior that we want is that 

81 # not passing this option at all passes all collection types, while 

82 # passing it uses only the passed collection types. That works 

83 # fine for now, since there's no command-line option to subtract 

84 # collection types, and hence the only way to get an empty tuple 

85 # is as the default. 

86 return tuple(CollectionType.all()) 

87 

88 return tuple(CollectionType.from_name(item) for item in split_commas(context, param, value)) 

89 

90 

91collection_type_option = MWOptionDecorator( 

92 "--collection-type", 

93 callback=CollectionTypeCallback.makeCollectionTypes, 

94 multiple=True, 

95 help="If provided, only list collections of this type.", 

96 type=click.Choice(choices=CollectionTypeCallback.collectionTypes, case_sensitive=False), 

97) 

98 

99 

100collections_option = MWOptionDecorator( 

101 "--collections", 

102 help=unwrap( 

103 """One or more expressions that fully or partially identify 

104 the collections to search for datasets. If not provided all 

105 datasets are returned.""" 

106 ), 

107 multiple=True, 

108 callback=split_commas, 

109) 

110 

111 

112components_option = MWOptionDecorator( 

113 "--components/--no-components", 

114 default=False, 

115 help=unwrap( 

116 """For --components, apply all expression patterns to 

117 component dataset type names as well. For --no-components, 

118 never apply patterns to components. Default is False. 

119 Fully-specified component datasets (`str` or `DatasetType` 

120 instances) are always included.""" 

121 ), 

122) 

123 

124 

125def _config_split(*args: Any) -> dict[str, str]: 

126 # Config values might include commas so disable comma-splitting. 

127 result = split_kv(*args, multiple=False) 

128 assert isinstance(result, dict), "For mypy check that we get the expected result" 

129 return result 

130 

131 

132config_option = MWOptionDecorator( 

133 "-c", 

134 "--config", 

135 callback=_config_split, 

136 help="Config override, as a key-value pair.", 

137 metavar="TEXT=TEXT", 

138 multiple=True, 

139) 

140 

141 

142config_file_option = MWOptionDecorator( 

143 "-C", 

144 "--config-file", 

145 help=unwrap( 

146 """Path to a pex config override to be included after the 

147 Instrument config overrides are applied.""" 

148 ), 

149) 

150 

151 

152confirm_option = MWOptionDecorator( 

153 "--confirm/--no-confirm", 

154 default=True, 

155 help="Print expected action and a confirmation prompt before executing. Default is --confirm.", 

156) 

157 

158 

159dataset_type_option = MWOptionDecorator( 

160 "-d", "--dataset-type", callback=split_commas, help="Specific DatasetType(s) to validate.", multiple=True 

161) 

162 

163 

164datasets_option = MWOptionDecorator("--datasets") 

165 

166 

167logLevelChoices = ["CRITICAL", "ERROR", "WARNING", "INFO", "VERBOSE", "DEBUG", "TRACE"] 

168log_level_option = MWOptionDecorator( 

169 "--log-level", 

170 callback=partial( 

171 split_kv, 

172 choice=click.Choice(choices=logLevelChoices, case_sensitive=False), 

173 normalize=True, 

174 unseparated_okay=True, 

175 add_to_default=True, 

176 default_key=None, # No separator 

177 ), 

178 help=f"The logging level. Without an explicit logger name, will only affect the default root loggers " 

179 f"({', '.join(CliLog.root_loggers())}). To modify the root logger use '.=LEVEL'. " 

180 f"Supported levels are [{'|'.join(logLevelChoices)}]", 

181 is_eager=True, 

182 metavar="LEVEL|COMPONENT=LEVEL", 

183 multiple=True, 

184) 

185 

186 

187long_log_option = MWOptionDecorator( 

188 "--long-log", help="Make log messages appear in long format.", is_flag=True 

189) 

190 

191log_file_option = MWOptionDecorator( 

192 "--log-file", 

193 default=None, 

194 multiple=True, 

195 callback=split_commas, 

196 type=MWPath(file_okay=True, dir_okay=False, writable=True), 

197 help="File(s) to write log messages. If the path ends with '.json' then" 

198 " JSON log records will be written, else formatted text log records" 

199 " will be written. This file can exist and records will be appended.", 

200) 

201 

202log_label_option = MWOptionDecorator( 

203 "--log-label", 

204 default=None, 

205 multiple=True, 

206 callback=split_kv, 

207 type=str, 

208 help="Keyword=value pairs to add to MDC of log records.", 

209) 

210 

211log_tty_option = MWOptionDecorator( 

212 "--log-tty/--no-log-tty", 

213 default=True, 

214 help="Log to terminal (default). If false logging to terminal is disabled.", 

215) 

216 

217options_file_option = MWOptionDecorator( 

218 "--options-file", 

219 "-@", 

220 expose_value=False, # This option should not be forwarded 

221 help=unwrap( 

222 """URI to YAML file containing overrides 

223 of command line options. The YAML should be organized 

224 as a hierarchy with subcommand names at the top 

225 level options for that subcommand below.""" 

226 ), 

227 callback=yaml_presets, 

228) 

229 

230 

231processes_option = MWOptionDecorator( 

232 "-j", "--processes", default=1, help="Number of processes to use.", type=click.IntRange(min=1) 

233) 

234 

235 

236regex_option = MWOptionDecorator("--regex") 

237 

238 

239register_dataset_types_option = MWOptionDecorator( 

240 "--register-dataset-types", 

241 help="Register DatasetTypes that do not already exist in the Registry.", 

242 is_flag=True, 

243) 

244 

245run_option = MWOptionDecorator("--output-run", help="The name of the run datasets should be output to.") 

246 

247_transfer_params = dict( 

248 default="auto", # set to `None` if using `required=True` 

249 help="The external data transfer mode.", 

250 type=click.Choice( 

251 choices=["auto", "link", "symlink", "hardlink", "copy", "move", "relsymlink", "direct"], 

252 case_sensitive=False, 

253 ), 

254) 

255 

256transfer_option_no_short = MWOptionDecorator( 

257 "--transfer", 

258 **_transfer_params, 

259) 

260 

261transfer_option = MWOptionDecorator( 

262 "-t", 

263 "--transfer", 

264 **_transfer_params, 

265) 

266 

267 

268transfer_dimensions_option = MWOptionDecorator( 

269 "--transfer-dimensions/--no-transfer-dimensions", 

270 is_flag=True, 

271 default=True, 

272 help=unwrap( 

273 """If true, also copy dimension records along with datasets. 

274 If the dmensions are already present in the destination butler it 

275 can be more efficient to disable this. The default is to transfer 

276 dimensions.""" 

277 ), 

278) 

279 

280 

281verbose_option = MWOptionDecorator("-v", "--verbose", help="Increase verbosity.", is_flag=True) 

282 

283 

284where_option = MWOptionDecorator( 

285 "--where", default="", help="A string expression similar to a SQL WHERE clause." 

286) 

287 

288 

289order_by_option = MWOptionDecorator( 

290 "--order-by", 

291 help=unwrap( 

292 """One or more comma-separated names used to order records. Names can be dimension names, 

293 metadata field names, or "timespan.begin" / "timespan.end" for temporal dimensions. 

294 In some cases the dimension for a metadata field or timespan bound can be inferred, but usually 

295 qualifying these with "<dimension>.<field>" is necessary. 

296 To reverse ordering for a name, prefix it with a minus sign. 

297 """ 

298 ), 

299 multiple=True, 

300 callback=split_commas, 

301) 

302 

303 

304limit_option = MWOptionDecorator( 

305 "--limit", 

306 help=unwrap("Limit the number of records, by default all records are shown."), 

307 type=int, 

308 default=0, 

309) 

310 

311offset_option = MWOptionDecorator( 

312 "--offset", 

313 help=unwrap("Skip initial number of records, only used when --limit is specified."), 

314 type=int, 

315 default=0, 

316)