Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# This file is part of ctrl_mpexec. 

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 program is free software: you can redistribute it and/or modify 

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

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

12# (at your option) any later version. 

13# 

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

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

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

17# GNU General Public License for more details. 

18# 

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

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

21 

22"""Module defining `makeParser` factory method. 

23""" 

24 

25__all__ = ["makeParser"] 

26 

27# ------------------------------- 

28# Imports of standard modules -- 

29# ------------------------------- 

30from argparse import Action, ArgumentParser, RawDescriptionHelpFormatter 

31import collections 

32import copy 

33import re 

34import textwrap 

35 

36# ----------------------------- 

37# Imports for other modules -- 

38# ----------------------------- 

39 

40# ---------------------------------- 

41# Local non-exported definitions -- 

42# ---------------------------------- 

43 

44# Class which determines an action that needs to be performed 

45# when building pipeline, its attributes are: 

46# action: the name of the action, e.g. "new_task", "delete_task" 

47# label: task label, can be None if action does not require label 

48# value: argument value excluding task label. 

49_PipelineAction = collections.namedtuple("_PipelineAction", "action,label,value") 

50 

51 

52class _PipelineActionType: 

53 """Class defining a callable type which converts strings into 

54 _PipelineAction instances. 

55 

56 Parameters 

57 ---------- 

58 action : str 

59 Name of the action, will become `action` attribute of instance. 

60 regex : str 

61 Regular expression for argument value, it can define groups 'label' 

62 and 'value' which will become corresponding attributes of a 

63 returned instance. 

64 """ 

65 

66 def __init__(self, action, regex='.*', valueType=str): 

67 self.action = action 

68 self.regex = re.compile(regex) 

69 self.valueType = valueType 

70 

71 def __call__(self, value): 

72 match = self.regex.match(value) 

73 if not match: 

74 raise TypeError("Unrecognized option syntax: " + value) 

75 # get "label" group or use None as label 

76 try: 

77 label = match.group("label") 

78 except IndexError: 

79 label = None 

80 # if "value" group is not defined use whole string 

81 try: 

82 value = match.group("value") 

83 except IndexError: 

84 pass 

85 value = self.valueType(value) 

86 return _PipelineAction(self.action, label, value) 

87 

88 def __repr__(self): 

89 """String representation of this class. 

90 

91 argparse can use this for some error messages, default implementation 

92 makes those messages incomprehensible. 

93 """ 

94 return f"_PipelineActionType(action={self.action})" 

95 

96 

97_ACTION_ADD_TASK = _PipelineActionType("new_task", "(?P<value>[^:]+)(:(?P<label>.+))?") 

98_ACTION_DELETE_TASK = _PipelineActionType("delete_task", "(?P<value>)(?P<label>.+)") 

99_ACTION_CONFIG = _PipelineActionType("config", "(?P<label>.+):(?P<value>.+=.+)") 

100_ACTION_CONFIG_FILE = _PipelineActionType("configfile", "(?P<label>.+):(?P<value>.+)") 

101_ACTION_ADD_INSTRUMENT = _PipelineActionType("add_instrument", "(?P<value>[^:]+)") 

102 

103 

104class _LogLevelAction(Action): 

105 """Action class which collects logging levels. 

106 

107 This action class collects arguments in the form "LEVEL" or 

108 "COMPONENT=LEVEL" where LEVEL is the name of the logging level (case- 

109 insensitive). It converts the series of arguments into the list of 

110 tuples (COMPONENT, LEVEL). If component name is missing then first 

111 item in tuple is set to `None`. Second item in tuple is converted to 

112 upper case. 

113 """ 

114 

115 permittedLevels = set(['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL']) 

116 

117 def __call__(self, parser, namespace, values, option_string=None): 

118 """Re-implementation of the base class method. 

119 

120 See `argparse.Action` documentation for parameter description. 

121 """ 

122 dest = getattr(namespace, self.dest) 

123 if dest is None: 

124 dest = [] 

125 setattr(namespace, self.dest, dest) 

126 

127 component, _, levelStr = values.partition("=") 

128 if not levelStr: 

129 levelStr, component = component, None 

130 logLevelUpr = levelStr.upper() 

131 if logLevelUpr not in self.permittedLevels: 

132 parser.error("loglevel=%s not one of %s" % (levelStr, tuple(self.permittedLevels))) 

133 dest.append((component, logLevelUpr)) 

134 

135 

136class _InputCollectionAction(Action): 

137 """Action class which collects input collection names. 

138 

139 This action type accepts string values in format: 

140 

141 value :== collection[,collection[...]] 

142 collection :== [dataset_type:]collection_name 

143 

144 and converts value into a dictionary whose keys a dataset type names 

145 (or empty string when dataset type name is missing) and values are 

146 ordered lists of collection names. Values from multiple arguments are 

147 all collected into the same dictionary. Resulting list of collections 

148 could contain multiple instances of the same collection name if it 

149 appears multiple time on command line. 

150 """ 

151 

152 def __call__(self, parser, namespace, values, option_string=None): 

153 """Re-implementation of the base class method. 

154 

155 See `argparse.Action` documentation for parameter description. 

156 """ 

157 dest = getattr(namespace, self.dest, {}) 

158 # In case default is set to a dict (empty or not) we want to use 

159 # new deep copy of that dictionary as initial value to avoid 

160 # modifying default value. 

161 if dest is self.default: 

162 dest = copy.deepcopy(dest) 

163 

164 # split on commas, collection can be preceeded by dataset type 

165 for collstr in values.split(","): 

166 dsType, sep, coll = collstr.partition(':') 

167 if not sep: 

168 dsType, coll = "", dsType 

169 dest.setdefault(dsType, []).append(coll) 

170 

171 setattr(namespace, self.dest, dest) 

172 

173 

174def _outputCollectionType(value): 

175 """Special argument type for input collections. 

176 

177 Accepts string vlues in format: 

178 

179 value :== collection[,collection[...]] 

180 collection :== [dataset_type:]collection_name 

181 

182 and converts value into a dictionary whose keys a dataset type names 

183 (or empty string when dataset type name is missing) and values are 

184 collection names. 

185 

186 Parameters 

187 ---------- 

188 value : `str` 

189 Value of the command line option 

190 

191 Returns 

192 ------- 

193 `dict` 

194 

195 Raises 

196 ------ 

197 `ValueError` if there is more than one collection per dataset type. 

198 """ 

199 res = {} 

200 for collstr in value.split(","): 

201 dsType, sep, coll = collstr.partition(':') 

202 if not sep: 

203 dsType, coll = "", dsType 

204 if dsType in res: 

205 raise ValueError("multiple collection names: " + value) 

206 res[dsType] = coll 

207 return res 

208 

209 

210_EPILOG = """\ 

211Notes: 

212 * many options can appear multiple times; all values are used, in order 

213 left to right 

214 * @file reads command-line options from the specified file: 

215 * data may be distributed among multiple lines (e.g. one option per line) 

216 * data after # is treated as a comment and ignored 

217 * blank lines and lines starting with # are ignored 

218""" 

219 

220 

221def _makeButlerOptions(parser): 

222 """Add a set of options for data butler to a parser. 

223 

224 Parameters 

225 ---------- 

226 parser : `argparse.ArgumentParser` 

227 """ 

228 group = parser.add_argument_group("Data repository and selection options") 

229 group.add_argument("-b", "--butler-config", dest="butler_config", default=None, metavar="PATH", 

230 help="Location of the gen3 butler/registry config file.") 

231 group.add_argument("-i", "--input", dest="input", action=_InputCollectionAction, 

232 metavar="COLL,DSTYPE:COLL", default={}, 

233 help=("Comma-separated names of the data butler collection. " 

234 "If collection includes dataset type name separated by colon " 

235 "then collection is only used for that specific dataset type. " 

236 "Pre-flight uses these collections to search for input datasets. " 

237 "Task execution stage only uses first global collection name " 

238 "to override collection specified in Butler configuration file.")) 

239 group.add_argument("-o", "--output", dest="output", type=_outputCollectionType, 

240 metavar="COLL,DSTYPE:COLL", default={}, 

241 help=("Comma-separated names of the data butler collection. " 

242 "See description of --input option. This option only allows " 

243 "single collection (per-dataset type or global).")) 

244 group.add_argument("-d", "--data-query", dest="data_query", default="", metavar="QUERY", 

245 help="User data selection expression.") 

246 

247 

248def _makeMetaOutputOptions(parser): 

249 """Add a set of options describing output metadata. 

250 

251 Parameters 

252 ---------- 

253 parser : `argparse.ArgumentParser` 

254 """ 

255 group = parser.add_argument_group("Meta-information output options") 

256 group.add_argument("--skip-init-writes", dest="skip_init_writes", default=False, 

257 action="store_true", 

258 help="Do not write collection-wide 'init output' datasets (e.g. schemas).") 

259 group.add_argument("--init-only", dest="init_only", default=False, 

260 action="store_true", 

261 help=("Do not actually run; just register dataset types and/or save init outputs.")) 

262 group.add_argument("--register-dataset-types", dest="register_dataset_types", default=False, 

263 action="store_true", 

264 help="Register DatasetTypes that do not already exist in the Registry.") 

265 group.add_argument("--clobber-config", action="store_true", dest="clobberConfig", default=False, 

266 help=("backup and then overwrite existing config files instead of checking them " 

267 "(safe with -j, but not all other forms of parallel execution)")) 

268 group.add_argument("--no-backup-config", action="store_true", dest="noBackupConfig", default=False, 

269 help="Don't copy config to file~N backup.") 

270 group.add_argument("--clobber-versions", action="store_true", dest="clobberVersions", default=False, 

271 help=("backup and then overwrite existing package versions instead of checking" 

272 "them (safe with -j, but not all other forms of parallel execution)")) 

273 group.add_argument("--no-versions", action="store_true", dest="noVersions", default=False, 

274 help="don't check package versions; useful for development") 

275 

276 

277def _makeLoggingOptions(parser): 

278 """Add a set of options for logging configuration. 

279 

280 Parameters 

281 ---------- 

282 parser : `argparse.ArgumentParser` 

283 """ 

284 group = parser.add_argument_group("Logging options") 

285 group.add_argument("-L", "--loglevel", action=_LogLevelAction, default=[], 

286 help="logging level; supported levels are [trace|debug|info|warn|error|fatal]", 

287 metavar="LEVEL|COMPONENT=LEVEL") 

288 group.add_argument("--longlog", action="store_true", help="use a more verbose format for the logging") 

289 group.add_argument("--debug", action="store_true", dest="enableLsstDebug", 

290 help="enable debugging output using lsstDebug facility (imports debug.py)") 

291 

292 

293def _makePipelineOptions(parser): 

294 """Add a set of options for building a pipeline. 

295 

296 Parameters 

297 ---------- 

298 parser : `argparse.ArgumentParser` 

299 """ 

300 group = parser.add_argument_group("Pipeline building options") 

301 group.add_argument("-p", "--pipeline", dest="pipeline", 

302 help="Location of a pipeline definition file in YAML format.", 

303 metavar="PATH") 

304 group.add_argument("-t", "--task", metavar="TASK[:LABEL]", 

305 dest="pipeline_actions", action='append', type=_ACTION_ADD_TASK, 

306 help="Task name to add to pipeline, must be a fully qualified task name. " 

307 "Task name can be followed by colon and " 

308 "label name, if label is not given than task base name (class name) " 

309 "is used as label.") 

310 group.add_argument("--delete", metavar="LABEL", 

311 dest="pipeline_actions", action='append', type=_ACTION_DELETE_TASK, 

312 help="Delete task with given label from pipeline.") 

313 group.add_argument("-c", "--config", metavar="LABEL:NAME=VALUE", 

314 dest="pipeline_actions", action='append', type=_ACTION_CONFIG, 

315 help="Configuration override(s) for a task with specified label, " 

316 "e.g. -c task:foo=newfoo -c task:bar.baz=3.") 

317 group.add_argument("-C", "--configfile", metavar="LABEL:PATH", 

318 dest="pipeline_actions", action='append', type=_ACTION_CONFIG_FILE, 

319 help="Configuration override file(s), applies to a task with a given label.") 

320 group.add_argument("--order-pipeline", dest="order_pipeline", 

321 default=False, action="store_true", 

322 help="Order tasks in pipeline based on their data dependencies, " 

323 "ordering is performed as last step before saving or executing " 

324 "pipeline.") 

325 group.add_argument("-s", "--save-pipeline", dest="save_pipeline", 

326 help="Location for storing resulting pipeline definition in YAML format.", 

327 metavar="PATH") 

328 group.add_argument("--pipeline-dot", dest="pipeline_dot", 

329 help="Location for storing GraphViz DOT representation of a pipeline.", 

330 metavar="PATH") 

331 group.add_argument("--instrument", metavar="instrument", 

332 dest="pipeline_actions", action="append", type=_ACTION_ADD_INSTRUMENT, 

333 help="Add an instrument which will be used to load config overrides when" 

334 " defining a pipeline. This must be the fully qualified class name") 

335 

336 

337def _makeQuntumGraphOptions(parser): 

338 """Add a set of options controlling quantum graph generation. 

339 

340 Parameters 

341 ---------- 

342 parser : `argparse.ArgumentParser` 

343 """ 

344 group = parser.add_argument_group("Quantum graph building options") 

345 group.add_argument("-g", "--qgraph", dest="qgraph", 

346 help="Location for a serialized quantum graph definition " 

347 "(pickle file). If this option is given then all input data " 

348 "options and pipeline-building options cannot be used.", 

349 metavar="PATH") 

350 groupex = group.add_mutually_exclusive_group() 

351 groupex.add_argument("--skip-existing", dest="skip_existing", 

352 default=False, action="store_true", 

353 help="If all Quantum outputs already exist in output collection " 

354 "then Quantum will be excluded from QuantumGraph.") 

355 groupex.add_argument("--clobber-output", dest="clobber_output", 

356 default=False, action="store_true", 

357 help="Ignore or replace existing output datasets in output collecton. " 

358 "With this option existing output datasets are ignored when generating " 

359 "QuantumGraph, and they are removed from a collection prior to " 

360 "executing individual Quanta. This option is exclusive with " 

361 "--skip-existing option.") 

362 group.add_argument("-q", "--save-qgraph", dest="save_qgraph", 

363 help="Location for storing a serialized quantum graph definition " 

364 "(pickle file).", 

365 metavar="PATH") 

366 group.add_argument("--save-single-quanta", dest="save_single_quanta", 

367 help="Format string of locations for storing individual quantum graph " 

368 "definition (pickle files). The curly brace {} in the input string " 

369 "will be replaced by a quantum number.", 

370 metavar="PATH") 

371 group.add_argument("--qgraph-dot", dest="qgraph_dot", 

372 help="Location for storing GraphViz DOT representation of a " 

373 "quantum graph.", 

374 metavar="PATH") 

375 

376 

377def _makeExecOptions(parser): 

378 """Add options controlling how tasks are executed. 

379 

380 Parameters 

381 ---------- 

382 parser : `argparse.ArgumentParser` 

383 """ 

384 group = parser.add_argument_group("Execution options") 

385 group.add_argument("--doraise", action="store_true", 

386 help="raise an exception on error (else log a message and continue)?") 

387 group.add_argument("--profile", metavar="PATH", help="Dump cProfile statistics to filename") 

388 

389 # parallelism options 

390 group.add_argument("-j", "--processes", type=int, default=1, help="Number of processes to use") 

391 group.add_argument("--timeout", type=float, 

392 help="Timeout for multiprocessing; maximum wall time (sec)") 

393 

394# ------------------------ 

395# Exported definitions -- 

396# ------------------------ 

397 

398 

399def makeParser(fromfile_prefix_chars='@', parser_class=ArgumentParser, **kwargs): 

400 """Make instance of command line parser for `CmdLineFwk`. 

401 

402 Creates instance of parser populated with all options that are supported 

403 by command line activator. There is no additional logic in this class, 

404 all semantics is handled by the activator class. 

405 

406 Parameters 

407 ---------- 

408 fromfile_prefix_chars : `str`, optional 

409 Prefix for arguments to be used as options files (default: `@`) 

410 parser_class : `type`, optional 

411 Specifies the class of the argument parser, by default 

412 `ArgumentParser` is used. 

413 kwargs : extra keyword arguments 

414 Passed directly to `parser_class` constructor 

415 

416 Returns 

417 ------- 

418 instance of `parser_class` 

419 """ 

420 

421 parser = parser_class(usage="%(prog)s subcommand [options]", 

422 fromfile_prefix_chars=fromfile_prefix_chars, 

423 epilog=_EPILOG, 

424 formatter_class=RawDescriptionHelpFormatter, 

425 **kwargs) 

426 

427 # define sub-commands 

428 subparsers = parser.add_subparsers(dest="subcommand", 

429 title="commands", 

430 description=("Valid commands, use `<command> --help' to get " 

431 "more info about each command:"), 

432 prog=parser.prog) 

433 # Python3 workaround, see http://bugs.python.org/issue9253#msg186387 

434 # The issue was fixed in Python 3.6, workaround is not need starting with that version 

435 subparsers.required = True 

436 

437 for subcommand in ("build", "qgraph", "run"): 

438 # show/run sub-commands, they are all identical except for the 

439 # command itself and description 

440 

441 if subcommand == "build": 

442 description = textwrap.dedent("""\ 

443 Build and optionally save pipeline definition. 

444 This does not require input data to be specified.""") 

445 elif subcommand == "qgraph": 

446 description = textwrap.dedent("""\ 

447 Build and optionally save pipeline and quantum graph.""") 

448 else: 

449 description = textwrap.dedent("""\ 

450 Build and execute pipeline and quantum graph.""") 

451 

452 subparser = subparsers.add_parser(subcommand, 

453 description=description, 

454 epilog=_EPILOG, 

455 formatter_class=RawDescriptionHelpFormatter) 

456 subparser.set_defaults(subparser=subparser, 

457 pipeline_actions=[]) 

458 _makeLoggingOptions(subparser) 

459 _makePipelineOptions(subparser) 

460 

461 if subcommand in ("qgraph", "run"): 

462 _makeQuntumGraphOptions(subparser) 

463 _makeButlerOptions(subparser) 

464 

465 if subcommand == "run": 

466 _makeExecOptions(subparser) 

467 _makeMetaOutputOptions(subparser) 

468 

469 subparser.add_argument("--show", metavar="ITEM|ITEM=VALUE", action="append", default=[], 

470 help="Dump various info to standard output. Possible items are: " 

471 "`config', `config=[Task::]<PATTERN>' or " 

472 "`config=[Task::]<PATTERN>:NOIGNORECASE' to dump configuration " 

473 "fields possibly matching given pattern and/or task label; " 

474 "`history=<FIELD>' to dump configuration history for a field, " 

475 "field name is specified as [Task::][SubTask.]Field; " 

476 "`dump-config', `dump-config=Task' to dump complete configuration " 

477 "for a task given its label or all tasks; " 

478 "`pipeline' to show pipeline composition; " 

479 "`graph' to show information about quanta; " 

480 "`workflow' to show information about quanta and their dependency; " 

481 "`tasks' to show task composition.") 

482 

483 return parser